diff --git a/AGENTS.md b/AGENTS.md index e454460..ac782aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ | 装柜完成后的退税报关资料 | `src/modules/first-leg/views/FirstLegOrdersView.vue` 的“报关资料”、`components/CustomsDocumentsDialog.vue`、`api/customs.ts` | 只显示登录客户在同柜的退税入仓码;报关开始前可上传、替换、调整同一资料覆盖的一个或多个入仓码及删除误传,全部写入携带幂等键和版本;开始后只读冻结资料,整柜完成后才能下载平台回传的一对一完成文件。Backend `firstleg/customs/container/`、`customs-v1.yaml`,二进制归 `file/` | | 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 | | 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 | -| 客户资金账户与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际、冻结、可用余额和不可变流水;充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | +| 客户资金账户、授信与充值申请 | `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;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`;Backend `lastleg-service/` 的 `relabel/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` | diff --git a/src/api/overseasHttp.ts b/src/api/overseasHttp.ts index cfcaadc..72e4a1c 100644 --- a/src/api/overseasHttp.ts +++ b/src/api/overseasHttp.ts @@ -39,14 +39,23 @@ export async function overseasApiRequest(config: AxiosRequestConfig) { try { const response = await transport.request>(config) if (!isApiEnvelope(response.data) || response.data.code !== '200') { - throw new ApiError(isApiEnvelope(response.data) ? response.data.message : '海外接口响应格式错误', isApiEnvelope(response.data) ? response.data.code : 'INVALID_RESPONSE') + throw new ApiError( + isApiEnvelope(response.data) ? response.data.message : '海外接口响应格式错误', + isApiEnvelope(response.data) ? response.data.code : 'INVALID_RESPONSE', + undefined, + undefined, + isApiEnvelope(response.data) ? response.data.traceId : null, + isApiEnvelope(response.data) ? response.data.requestId : null, + ) } return response.data.data } catch (error) { if (error instanceof ApiError) throw error if (axios.isAxiosError(error)) { const body = error.response?.data - if (isApiEnvelope(body)) throw new ApiError(body.message, body.code, error.response?.status) + if (isApiEnvelope(body)) { + throw new ApiError(body.message, body.code, error.response?.status, undefined, body.traceId, body.requestId) + } 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/modules/order-management/api/inboundPlans.ts b/src/modules/order-management/api/inboundPlans.ts index d62dfae..d49d921 100644 --- a/src/modules/order-management/api/inboundPlans.ts +++ b/src/modules/order-management/api/inboundPlans.ts @@ -17,8 +17,8 @@ export interface InboundPlanSummary { planNo: string inboundCode: string orderType: string | null - warehouseCode: string | null - warehouseName: string | null + overseasWarehouseCode: string | null + overseasWarehouseName: string | null fbaWarehouseCode: string | null packageCount: number pickedPackageCount: number @@ -52,6 +52,8 @@ export interface InboundPlanPallet { export interface InboundPlanDetail { planNo: string containerNo: string | null + overseasWarehouseCode: string | null + overseasWarehouseName: string | null status: string lineCount: number palletCount: number diff --git a/src/modules/order-management/inboundOrders.test.mjs b/src/modules/order-management/inboundOrders.test.mjs index a9036aa..45d5392 100644 --- a/src/modules/order-management/inboundOrders.test.mjs +++ b/src/modules/order-management/inboundOrders.test.mjs @@ -5,6 +5,7 @@ import test from 'node:test' const api = readFileSync(new URL('./api/inboundPlans.ts', import.meta.url), 'utf8') const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8') const view = readFileSync(new URL('./views/InboundOrdersView.vue', import.meta.url), 'utf8') +const overseasHttp = readFileSync(new URL('../../api/overseasHttp.ts', import.meta.url), 'utf8') test('activates the customer-scoped overseas inbound list and detail', () => { assert.match(routes, /path: 'orders\/inbound'[\s\S]*?release: 'active'[\s\S]*?permission: 'merchant:warehouse:inbound:list'/) @@ -23,3 +24,21 @@ test('keeps actions first and exposes filters loading errors empty state paginat assert.match(view, /detail\.planNo/) assert.doesNotMatch(view, /planId|orderId|warehouseId|customerId/) }) + +test('shows only the server-provided overseas receiving warehouse code and name', () => { + assert.match(api, /overseasWarehouseCode: string \| null/) + assert.match(api, /overseasWarehouseName: string \| null/) + assert.doesNotMatch(api, /overseasWarehouseId/) + assert.match(view, /label="海外接柜仓"/) + assert.match(view, /overseasWarehouseLabel\(scope\.row\.overseasWarehouseCode, scope\.row\.overseasWarehouseName\)/) + assert.match(view, /overseasWarehouseLabel\(detail\.overseasWarehouseCode, detail\.overseasWarehouseName\)/) + assert.match(view, /if \(typeof code !== 'string' \|\| !code\.trim\(\) \|\| typeof name !== 'string' \|\| !name\.trim\(\)\) return '—'/) + assert.doesNotMatch(view, /warehouseName \|\| warehouseCode|destinationWarehouse(?:Id|Code|Name).*overseasWarehouse/i) +}) + +test('preserves overseas API diagnostic metadata without rendering it as a business identifier', () => { + assert.match(overseasHttp, /response\.data\.traceId/) + assert.match(overseasHttp, /response\.data\.requestId/) + assert.match(overseasHttp, /body\.traceId, body\.requestId/) + assert.doesNotMatch(view, /requestId|traceId/) +}) diff --git a/src/modules/order-management/views/InboundOrdersView.vue b/src/modules/order-management/views/InboundOrdersView.vue index cb0d25d..35627ec 100644 --- a/src/modules/order-management/views/InboundOrdersView.vue +++ b/src/modules/order-management/views/InboundOrdersView.vue @@ -14,6 +14,10 @@ const query = reactive({ }) function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) } +function overseasWarehouseLabel(code: unknown, name: unknown) { + if (typeof code !== 'string' || !code.trim() || typeof name !== 'string' || !name.trim()) return '—' + return `${code.trim()} · ${name.trim()}` +} function dateTime(value: unknown) { return value ? new Date(String(value)).toLocaleString('zh-CN', { hour12: false }) : '—' } function statusLabel(value: unknown) { return ({ PENDING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', UNAPPOINTED: '未预约', SUCCESS: '预约成功', FAILED: '预约失败', SCHEDULED: '已排车', UNSCHEDULED: '未排车' } as Record)[String(value)] || text(value) @@ -82,7 +86,7 @@ onMounted(() => void load()) - + @@ -100,6 +104,7 @@ onMounted(() => void load())
计划号
{{ detail.planNo }}
集装箱号
{{ text(detail.containerNo) }}
+
海外接柜仓
{{ overseasWarehouseLabel(detail.overseasWarehouseCode, detail.overseasWarehouseName) }}
状态
{{ statusLabel(detail.status) }}
入仓码数
{{ detail.lineCount }}
托盘数
{{ detail.palletCount }}
总体积(m³)
{{ detail.totalVolumeCbm }}
计划箱数
{{ detail.expectedPackageCount }}
已拣箱数
{{ detail.pickedPackageCount }}