This commit is contained in:
2026-10-20 18:44:53 +08:00
parent 20858dbe33
commit 78893f1144
4 changed files with 33 additions and 5 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
| 企业资料、成员与角色 | `src/modules/tenant-management/`、`src/modules/account/` | Backend `iam/`、`auth/`、`file/`;公司联系电话/邮箱是企业资料,不作为登录标识;成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随营业执照提交,也可在已有营业执照后单独补传或替换。前端角色树到服务端权限的映射统一维护在 `contracts.ts` 的 `FRONTEND_TO_MERCHANT_PERMISSION_CODES`,海外入仓、采购、本土卡车和散板权限必须显式进入该映射 |
| 工作台与系统入口 | `src/modules/dashboard/`、`src/modules/system/` | `src/navigation/clientNavigation.ts`、`src/router/index.ts` |
| 企业成员消息收件箱 | `src/modules/notifications/` | Backend `iam/src/main/java/com/baiyue/iam/notification/`;列表、详情、已读与未读数契约见 `iam/src/main/resources/openapi/notification-v1.yaml` |
| 头程下单、中文批量导入、入仓码关系、箱唛、履约进度与入库费用确认 | `src/modules/first-leg/`;列表与批量入口 `views/FirstLegOrdersView.vue`,模板/导入 API `api/firstLeg.ts`,入仓详情 `components/InboundCodeAssociationDialog.vue`、`api/inboundCodeAssociations.ts`,费用确认弹框 `components/InboundBillingPanel.vue`、`api/inboundBilling.ts` | 中文 XLSX 模板按当前客户生成地址、全部有效仓库和渠道下拉,最多 50 行,只创建草稿并支持部分/原子导入;导入结果只显示 Excel 行号,不展示内部 `clientRowId`。所有订单行均可进入费用确认弹框,尚未推送时展示明确空态;只有具备编辑权限且服务端返回当前 `SENT` 费用时才可确认并提交 `expectedFeeVersion` 与幂等键,成功后版本锁定、平台不得再修改。Backend `order/`、`firstleg/`、`warehouse/`,不得用内部 ID 补齐缺失业务编号 |
| 头程下单、中文批量导入、入仓码关系、箱唛、履约进度与入库费用确认 | `src/modules/first-leg/`;列表与批量入口 `views/FirstLegOrdersView.vue`,模板/导入 API `api/firstLeg.ts`,入仓详情 `components/InboundCodeAssociationDialog.vue`、`api/inboundCodeAssociations.ts`,费用确认弹框 `components/InboundBillingPanel.vue`、`api/inboundBilling.ts` | 中文 XLSX 模板按当前客户生成地址、全部有效仓库和渠道下拉,最多 50 行,只创建草稿并支持部分/原子导入;导入结果只显示 Excel 行号,不展示内部 `clientRowId`。列表“确认计费重”仅在具备确认权限且服务端 `customerFeeConfirmationRequired=true` 时显示,已确认或未推送费用时隐藏;详情可查看费用;确认提交 `expectedFeeVersion` 与幂等键,成功后版本锁定、平台不得再修改。Backend `order/`、`firstleg/`、`warehouse/`,不得用内部 ID 补齐缺失业务编号 |
| 装柜完成后的退税报关资料 | `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` 和幂等键 |
+1 -1
View File
@@ -25,7 +25,7 @@
- 头程订单只用当前正式读写契约;不得扩展 `LEGACY_MANUAL`、旧系统入仓码、C02 不可用回退旧 API、历史部分响应兼容或其他新旧并跑逻辑。触及该链路时同步删除生产分支、类型、文案、Mock 和测试。
- 快速下单只创建草稿,不提交、不审核;逐行失败必须回填原因与入仓码,不用内部 ID 代替业务编号。
- 入库费用确认携带 `expectedFeeVersion` 与幂等键,金额与冻结状态只认服务端结果;所有订单行都可进入费用确认弹框,不按行类型隐藏。
- 入库费用确认携带 `expectedFeeVersion` 与幂等键,金额与冻结状态只认服务端结果;列表“确认计费重”仅在具备确认权限且服务端 `customerFeeConfirmationRequired=true` 时显示,已确认或未推送费用的订单不显示;详情保留费用查看入口。
- 报关资料在开始后只读冻结资料,未装柜或无资格时不展示入口;`utils/customsEligibility.ts` 是唯一资格判断入口。
- 模块间协作只能从本模块 `public.ts` 导入,不穿透其他模块的 `views/` 或内部目录。
@@ -642,15 +642,41 @@ test('opens billing-weight confirmation independently from order details', () =>
assert.match(source, /unavailable-message="未推送费用"/)
})
test('uses one published-fee dialog for billing-weight confirmation on every order row', () => {
test('only offers published-fee confirmation for pending orders with confirmation permission', () => {
const actions = source.slice(source.indexOf('function getOrderRowActions'), source.indexOf('function getOrderRowActions') + 1800)
assert.match(actions, /actions\.push\(\{ key: 'fee', label: '确认计费重', run: \(\) => openFeeConfirmation\(order\) \}\)/)
assert.doesNotMatch(actions, /customerFeeConfirmationRequired|label: '确认费用'|key: 'weight'/)
assert.match(actions, /if \(canConfirmInboundFee\.value && order\.businessSummary\.customerFeeConfirmationRequired\) \{\s*actions\.push\(\{ key: 'fee'/)
assert.doesNotMatch(actions, /label: '确认费用'|key: 'weight'/)
assert.doesNotMatch(source, /ChargeableWeightDialog|weightConfirmationOrderId/)
assert.match(source, /:row-actions="getOrderRowActions"/)
assert.match(orderStoreSource, /customerFeeConfirmationRequired: summary\?\.customerFeeConfirmationRequired \?\? false/)
})
test('hides confirmation for confirmed, unpushed and unauthorized orders and refreshes after confirmation', () => {
const start = source.indexOf('function getOrderRowActions')
const end = source.indexOf('\nasync function copyOrder', start)
const body = source.slice(start, end)
.replace('function getOrderRowActions(row: object): TableAction[]', 'function getOrderRowActions(row)')
.replace('const order = row as FirstLegOrder', 'const order = row')
.replace('const actions: TableAction[]', 'const actions')
const actionsFor = new Function('canConfirmInboundFee', 'canCreateOrder', 'canEditOrder',
'canOfferCancellation', 'eligibleCustomsDetail', 'openFeeConfirmation',
`${body}; return getOrderRowActions`)
for (const [permitted, pending] of [[true, true], [true, false], [false, true], [false, false]]) {
const opened = []
const order = { id: 'order-1', businessSummary: { customerFeeConfirmationRequired: pending } }
const getActions = actionsFor({ value: permitted }, { value: false }, { value: false },
() => false, () => false, row => opened.push(row.id))
const confirm = getActions(order).filter(action => action.key === 'fee')
assert.equal(confirm.length, permitted && pending ? 1 : 0)
if (confirm.length) {
confirm[0].run()
assert.deepEqual(opened, ['order-1'])
}
}
assert.match(source, /function handleInboundFeeConfirmed\(\) \{\s*if \(!isCreatePage\.value && !isDetailPage\.value\) void loadOrderPage\(\)/)
})
test('submits only selected draft orders in one batch action', () => {
assert.match(source, /selectedDraftOrders = computed\(\(\) => selectedOrders\.value\.filter\(\(order\) => order\.status === 'DRAFT'\)\)/)
assert.match(source, /Promise\.allSettled\(ids\.map\(id => store\.submitSaved/)
@@ -1361,7 +1361,9 @@ function getOrderRowActions(row: object): TableAction[] {
if (canCreateOrder.value) {
actions.push({ key: 'copy', label: '复制下单', loading: copyingOrderId.value === order.id, run: () => copyOrder(order) })
}
actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) })
if (canConfirmInboundFee.value && order.businessSummary.customerFeeConfirmationRequired) {
actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) })
}
if (canEditOrder.value && canClientEditFirstLegOrder(order.status)) {
actions.push({ key: 'edit', label: '编辑', loading: openingEditor.value, run: () => openEdit(order) })
}