diff --git a/AGENTS.md b/AGENTS.md index ce0f91d..da6bea4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ | 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | | 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 | | 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID | -| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | +| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## 租户、认证与数据安全 diff --git a/docker-compose.test.yml b/docker-compose.test.yml index c3d40ea..697c93c 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -12,6 +12,8 @@ services: restart: unless-stopped ports: - "${HOST_PORT:-9002}:80" + extra_hosts: + - "host.docker.internal:host-gateway" environment: - API_UPSTREAM: "http://test-api-cn.buggoglobal.com" - OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com" + API_UPSTREAM: "${API_UPSTREAM:-http://host.docker.internal:9000}" + OVERSEAS_API_UPSTREAM: "${OVERSEAS_API_UPSTREAM:-http://host.docker.internal:9300}" diff --git a/src/api/overseasHttp.ts b/src/api/overseasHttp.ts index 3abc261..e567603 100644 --- a/src/api/overseasHttp.ts +++ b/src/api/overseasHttp.ts @@ -65,3 +65,27 @@ export async function overseasApiRequest(config: AxiosRequestConfig) { throw new ApiError(error instanceof Error ? error.message : '海外接口请求失败') } } + +export async function overseasBlobRequest(config: AxiosRequestConfig) { + try { + const response = await transport.request({ ...config, responseType: 'blob' }) + return response.data + } catch (error) { + if (error instanceof ApiError) throw error + if (axios.isAxiosError(error)) { + const body = error.response?.data + if (body instanceof Blob && body.type.includes('json')) { + try { + const parsed: unknown = JSON.parse(await body.text()) + if (isApiEnvelope(parsed) || isApiFailure(parsed)) { + throw new ApiError(parsed.message, parsed.code, error.response?.status, undefined, parsed.traceId, parsed.requestId) + } + } catch (parseError) { + if (parseError instanceof ApiError) throw parseError + } + } + throw new ApiError(error.code === 'ERR_NETWORK' ? '无法连接海外服务' : error.message, error.code || 'OVERSEAS_HTTP_ERROR', error.response?.status) + } + throw new ApiError(error instanceof Error ? error.message : '海外文件下载失败') + } +} diff --git a/src/components.d.ts b/src/components.d.ts index befd71f..f15b3f0 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -33,6 +33,7 @@ declare module 'vue' { ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElImage: typeof import('element-plus/es')['ElImage'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElOption: typeof import('element-plus/es')['ElOption'] diff --git a/src/deployment/nginxConfig.test.mjs b/src/deployment/nginxConfig.test.mjs index 691c0f4..2a1ced2 100644 --- a/src/deployment/nginxConfig.test.mjs +++ b/src/deployment/nginxConfig.test.mjs @@ -22,8 +22,10 @@ test('does not cache the SPA entry while keeping hashed assets immutable', () => }) test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => { - assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/) - assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/) + assert.match(testCompose, /- "host\.docker\.internal:host-gateway"/) + assert.match(testCompose, /API_UPSTREAM: "\$\{API_UPSTREAM:-http:\/\/host\.docker\.internal:9000\}"/) + assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "\$\{OVERSEAS_API_UPSTREAM:-http:\/\/host\.docker\.internal:9300\}"/) + assert.doesNotMatch(testCompose, /http:\/\/test-api-(?:cn|global)\.buggoglobal\.com/) assert.match(testCompose, /VITE_ADMIN_WEB_ORIGIN: "\$\{VITE_ADMIN_WEB_ORIGIN:-https:\/\/test-admin\.buggoglobal\.com\}"/) assert.match(config, /location \/overseas-api\/ \{[\s\S]*?rewrite \^\/overseas-api\/\(\.\*\)\$ \/api\/\$1 break;[\s\S]*?proxy_pass \$\{OVERSEAS_API_UPSTREAM\};/) assert.match(config, /proxy_set_header Host \$proxy_host;/) diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index 7a9b649..e571c71 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -8,13 +8,17 @@ const view = readFileSync(new URL('./components/BusinessOrdersView.vue', import. test('activates all three customer cross-border pages with frozen permissions', () => { for (const [path, permission] of [ - ['cross-border/procurement', 'merchant:cross-border:procurement'], - ['cross-border/domestic-trucking', 'merchant:cross-border:domestic-trucking'], - ['cross-border/pallet-shipping', 'merchant:cross-border:pallet-shipping'], + ['cross-border/procurement', 'merchant:cross-border:procurement:list'], + ['cross-border/domestic-trucking', 'merchant:cross-border:domestic-trucking:list'], + ['cross-border/pallet-shipping', 'merchant:cross-border:pallet-shipping:list'], ]) { assert.match(routes, new RegExp(`path: '${path}'[\\s\\S]*?release: 'active'[\\s\\S]*?permission: '${permission}'`)) } assert.doesNotMatch(view, /PrototypeFeatureView|mock|customerId|tenantId/i) + assert.match(view, /merchant:cross-border:procurement:create/) + assert.match(view, /merchant:cross-border:domestic-trucking:create/) + assert.match(view, /merchant:cross-border:pallet-shipping:create/) + assert.match(view, /v-else-if="canCreate"/) }) test('uses only merchant overseas endpoints and keeps retry idempotency stable', () => { diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index 9bba647..c3a0118 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -17,6 +17,11 @@ import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '../containerSp const props = defineProps<{ kind: CrossBorderBusinessKind }>() const auth = useAuthStore() +const canCreate = computed(() => auth.hasPermission(({ + procurement: 'merchant:cross-border:procurement:create', + trucking: 'merchant:cross-border:domestic-trucking:create', + looseboard: 'merchant:cross-border:pallet-shipping:create', +} as Record)[props.kind])) type BusinessRow = ProcurementSummary | TransportSummary type BusinessDetail = ProcurementDetail | TransportDetail @@ -277,7 +282,7 @@ onMounted(() => void load())

{{ mode === 'list' ? configuration.description : mode === 'create' ? '请填写订单信息,带 * 项为必填项' : `订单号:${detail?.summary.orderNo || '—'}` }}

返回列表 - 新增下单 + 新增下单