diff --git a/AGENTS.md b/AGENTS.md index 0e74551c..ecc7dd71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,8 @@ ## 海外模块内部快速索引(海外仓全流程) +退货库存与退货箱号:`modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue`、`api/returnInventory.ts`,由 `routes.ts` 的 `return-inventory/return-cartons` 分别以 `mode=inventory/cartons` 进入;只消费 Lastleg `returnreceipt/` 的平台库存、箱号、流水和标签接口。退货出库从 `pages/OutboundsView.vue` 进入,区别于 `ExceptionOrdersView.vue` 的派送异常退回。 + | 关键词 | 页面/路由入口 | API 与联查 | |---|---|---| | 尾程订单控制与柜级入仓计划转换 | `modules/overseas-warehouse/pages/LastMileOrdersView.vue`、`LastMileOrderDetailView.vue` | 柜子工作台按 `sourceContainerBookingId` 消费 `/orders/container-summaries`;服务端分页返回全部稳定来源柜,计划已经齐全的柜仍保留用于接柜仓查询和修改,并按冻结 `plannedArrivalAt` 升序、空值最后排序。详情返回全柜订单且不受列表分页截断;单票手工计划保存后,由服务端在整柜最后一票计划齐全时自动生成并绑定全柜托盘码,一票柜立即生成;`resourcePendingPlanCount` 只表示自动生成异常的重试入口,不是正常人工步骤。柜级确认弹框先调用 `/inbound-plans/container-preview`,按服务端 FBA/一件代发/私人地址分组展示托数、`readiness/blockers`,再把同一 `snapshotVersion/snapshotHash` 原样提交 `/inbound-plans/convert-container`;前端不计算托数、不逐票建计划、不预占托盘,快照冲突后重新获取预览。服务端只为剩余订单逐票生成独立计划,并为异常遗留的人工计划补齐真实托盘,整批原子且重放不得产生第二批资源。单票完整入库计划、暂扣/解除和八行人工节点均从 `api/lastMileOrders.ts` 提交订单版本与幂等键;入仓码只读展示并提交订单冻结值,不允许人工改写,暂扣资格、接柜仓锁定原因和操作能力只消费服务端结果,人工节点一经保存不得回退柜级节点。转仓候选只取 `modules/iam/api/platformIam.ts` 的平台公共 `ACTIVE + FBA` 仓主档;Backend `lastleg-service/` | diff --git a/src/modules/finance/AGENTS.md b/src/modules/finance/AGENTS.md index 31169650..b90db654 100644 --- a/src/modules/finance/AGENTS.md +++ b/src/modules/finance/AGENTS.md @@ -8,7 +8,7 @@ ## 客户资金账户 -- `pages/FinanceCustomerAccountsView.vue` 的列表和详情展示实际余额、冻结金额、授信额度、已用授信、冻结授信和总可用余额;操作列固定第一列。客户上传的充值申请在详情内按待审核优先展示,审核、驳回及凭证预览以服务端申请版本和客户归属为准。 +- `pages/FinanceCustomerAccountsView.vue` 的列表和详情展示实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信和总可用余额;操作列固定第一列。授信到期日必须晚于今天且不限制最远日期,到期日当天仍有效。客户上传的充值申请在详情内按待审核优先展示,审核、驳回及凭证预览以服务端申请版本和客户归属为准。 - 平台手工充值、扣款和授信调整必须提交账户版本与幂等键;扣款及授信下限由服务端校验。页面只展示服务端余额、授信和流水结果,不在浏览器补算或回退静态数据。 ## 客户筛选与统计 diff --git a/src/modules/finance/api/finance.test.ts b/src/modules/finance/api/finance.test.ts index 075bc006..6fe6106f 100644 --- a/src/modules/finance/api/finance.test.ts +++ b/src/modules/finance/api/finance.test.ts @@ -79,12 +79,12 @@ describe('finance api', () => { }) it('uses the formal G1 account list path and fixed page DTO', async () => { - const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', accountVersion: 7, pendingRechargeCount: 2 }], page: { page: 2, pageSize: 20, total: 21 } }) + const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 2 }], page: { page: 2, pageSize: 20, total: 21 } }) const result = await listFinanceAccounts({ page: 2, size: 20, customerId: '10001', status: 'ACTIVE' }) expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts', { params: { page: 2, pageSize: 20, customerId: '10001', status: 'ACTIVE' } }) - expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', accountVersion: 7, pendingRechargeCount: 2 }], total: 21, pages: 2 }) + expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 2 }], total: 21, pages: 2 }) }) it('uses the formal G1 account ledger path and fixed page DTO', async () => { @@ -116,9 +116,9 @@ describe('finance api', () => { it('updates the customer credit limit with account version and idempotency', async () => { const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: { ledgerId: 102, businessNo: 'AC-102', changeType: 'CREDIT_LIMIT', amount: '20000.00', creditLimitAfter: '20000.00', creditUsedAfter: '0.00', availableCreditAfter: '20000.00' } }) - const result = await updateFinanceCreditLimit('10001', { creditLimit: '20000.00', remark: '初始化授信', expectedAccountVersion: 7 }) + const result = await updateFinanceCreditLimit('10001', { creditLimit: '20000.00', creditExpiresOn: '2028-09-10', remark: '初始化授信', expectedAccountVersion: 7 }) - expect(put).toHaveBeenCalledWith('/platform/finance/v1/accounts/10001/credit-limit', { creditLimit: '20000.00', remark: '初始化授信', expectedAccountVersion: 7 }, { headers: { 'Idempotency-Key': expect.any(String) } }) + expect(put).toHaveBeenCalledWith('/platform/finance/v1/accounts/10001/credit-limit', { creditLimit: '20000.00', creditExpiresOn: '2028-09-10', remark: '初始化授信', expectedAccountVersion: 7 }, { headers: { 'Idempotency-Key': expect.any(String) } }) expect(result).toMatchObject({ ledgerId: 102, changeType: 'CREDIT_LIMIT', creditLimitAfter: '20000.00', availableCreditAfter: '20000.00' }) }) diff --git a/src/modules/finance/api/finance.ts b/src/modules/finance/api/finance.ts index 8b5f7173..24a8e79e 100644 --- a/src/modules/finance/api/finance.ts +++ b/src/modules/finance/api/finance.ts @@ -141,7 +141,8 @@ function normalizeFinanceAccount(item: RawResource): FinanceAccountSummary { paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt), actualBalance: stringValue(item.actualBalance), frozenBalance: stringValue(item.frozenBalance), availableBalance: stringValue(item.availableBalance), creditLimit: stringValue(item.creditLimit), creditUsed: stringValue(item.creditUsed), - availableCredit: stringValue(item.availableCredit), accountVersion: numberValue(item.accountVersion), pendingRechargeCount: numberValue(item.pendingRechargeCount), + availableCredit: stringValue(item.availableCredit), creditExpiresOn: nullableStringValue(item.creditExpiresOn), creditActive: Boolean(item.creditActive), + accountVersion: numberValue(item.accountVersion), pendingRechargeCount: numberValue(item.pendingRechargeCount), } } @@ -159,6 +160,7 @@ export async function listFinanceAccountLedger(customerId: string, params: { pag availableBalanceAfter: nullableStringValue(item.availableBalanceAfter), sourceType: nullableStringValue(item.sourceType), creditLimitAfter: nullableStringValue(item.creditLimitAfter), creditUsedAfter: nullableStringValue(item.creditUsedAfter), availableCreditAfter: nullableStringValue(item.availableCreditAfter), + creditExpiresOnAfter: nullableStringValue(item.creditExpiresOnAfter), sourceNo: nullableStringValue(item.sourceNo), occurredDate: nullableStringValue(item.occurredDate), paymentMethod: nullableStringValue(item.paymentMethod), referenceNo: nullableStringValue(item.referenceNo), createdAt: nullableStringValue(item.createdAt), @@ -201,7 +203,7 @@ export async function adjustFinanceAccount(customerId: string, payload: FinanceA return unwrapApiResponse(response) } -export async function updateFinanceCreditLimit(customerId: string, payload: { creditLimit: string; remark?: string; expectedAccountVersion: number }) { +export async function updateFinanceCreditLimit(customerId: string, payload: { creditLimit: string; creditExpiresOn: string; remark?: string; expectedAccountVersion: number }) { const response = await http.put, ApiEnvelope>(`${basePath}/accounts/${customerId}/credit-limit`, payload, mutationHeaders('credit-limit')) return unwrapApiResponse(response) } diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index 475074d9..a8c498f8 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -38,7 +38,7 @@ function memoryStorage(): Storage { const accountFixture = { customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, totalCost: 90, paidCost: 50, outstandingPayable: 40, - actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', accountVersion: 7, pendingRechargeCount: 1, + actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 1, status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00', } @@ -151,7 +151,7 @@ describe('FinanceCustomerAccountsView delivery', () => { vi.mocked(listFinanceRechargeRequests).mockReset().mockResolvedValue({ records: [{ requestId: '91', requestNo: 'RC-91', amount: '500.00', paymentMethod: 'WECHAT', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'PENDING', version: 3, submittedByName: '客户A', reviewedByName: '', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '' }], total: 1, pages: 1 }) vi.mocked(reviewFinanceRechargeRequest).mockReset().mockResolvedValue({ requestId: '91', requestNo: 'RC-91', amount: '500.00', paymentMethod: 'WECHAT', voucherFileId: '81', voucherFileName: 'receipt.png', status: 'APPROVED', version: 4, submittedByName: '客户A', reviewedByName: '财务A', reviewRemark: '', createdAt: '2026-09-04T10:00:00', reviewedAt: '2026-09-05T10:00:00' }) vi.mocked(adjustFinanceAccount).mockReset().mockResolvedValue({ ...accountFixture, actualBalance: '127080.50', availableBalance: '118580.50', accountVersion: 8 }) - vi.mocked(updateFinanceCreditLimit).mockReset().mockResolvedValue({ ledgerId: '102', businessNo: 'AC-102', changeType: 'CREDIT_LIMIT', amount: '20000.00', actualBalanceAfter: '126580.50', frozenBalanceAfter: '8500.00', availableBalanceAfter: '138080.50', creditLimitAfter: '20000.00', creditUsedAfter: '0.00', availableCreditAfter: '20000.00', sourceType: 'CREDIT_LIMIT_ADJUSTMENT', sourceNo: '', occurredAt: '', occurredDate: '2026-09-05', paymentMethod: '', referenceNo: '', operatorName: '财务A', remark: '', createdAt: '2026-09-05T10:00:00' }) + vi.mocked(updateFinanceCreditLimit).mockReset().mockResolvedValue({ ledgerId: '102', businessNo: 'AC-102', changeType: 'CREDIT_LIMIT', amount: '20000.00', actualBalanceAfter: '126580.50', frozenBalanceAfter: '8500.00', availableBalanceAfter: '138080.50', creditLimitAfter: '20000.00', creditUsedAfter: '0.00', availableCreditAfter: '20000.00', creditExpiresOnAfter: '2028-09-10', sourceType: 'CREDIT_LIMIT_ADJUSTMENT', sourceNo: '', occurredAt: '', occurredDate: '2026-09-05', paymentMethod: '', referenceNo: '', operatorName: '财务A', remark: '', createdAt: '2026-09-05T10:00:00' }) vi.mocked(downloadFinanceRechargeVoucher).mockReset().mockResolvedValue(new Blob(['image'], { type: 'image/png' })) vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-1', taskNo: 'EXP-1', taskName: '客户账户', status: 'PENDING' } as never) @@ -179,7 +179,7 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(metrics.text()).toContain('实际余额¥126580.50CNY') expect(metrics.text()).toContain('冻结金额¥8500.00CNY') expect(metrics.text()).toContain('总可用余额¥138080.50CNY') - expect(metrics.text()).toContain('授信额度¥20000.00CNY') + expect(metrics.text()).toContain('授信额度¥20000.002028-09-10 到期') expect(metrics.text()).toContain('已用授信¥0.00CNY') expect(metrics.text()).toContain('可用授信¥20000.00CNY') for (const auxiliary of ['待开账金额', '本期已收', '总成本', '已付成本', '已收客户款', '客户线下支付']) expect(metrics.text()).not.toContain(auxiliary) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.vue b/src/modules/finance/pages/FinanceCustomerAccountsView.vue index d5768975..5d512753 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.vue +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.vue @@ -58,7 +58,7 @@ const adjustmentAccount = ref(null) const adjustmentForm = reactive({ type: 'RECHARGE' as 'RECHARGE' | 'DEDUCTION', amount: '', occurredDate: '', paymentMethod: 'BANK_TRANSFER', referenceNo: '', remark: '' }) const creditLimitOpen = ref(false) const creditLimitSaving = ref(false) -const creditLimitForm = reactive({ amount: '', remark: '' }) +const creditLimitForm = reactive({ amount: '', expiresOn: '', remark: '' }) const reviewOpen = ref(false) const reviewSaving = ref(false) const reviewRequest = ref(null) @@ -90,6 +90,7 @@ const columns: DataTableColumn[] = [ { key: 'frozenBalance', label: '冻结金额(CNY)', width: 150, slotName: 'frozenBalance' }, { key: 'availableBalance', label: '总可用余额(CNY)', width: 155, slotName: 'availableBalance' }, { key: 'creditLimit', label: '授信额度(CNY)', width: 155, slotName: 'creditLimit' }, + { key: 'creditExpiresOn', label: '授信有效期', width: 140, slotName: 'creditExpiresOn' }, { key: 'creditUsed', label: '已用授信(CNY)', width: 150, slotName: 'creditUsed' }, { key: 'pendingRechargeCount', label: '待审核充值', width: 120, slotName: 'pendingRechargeCount' }, { key: 'status', label: '客户状态', width: 110, slotName: 'status' }, @@ -216,6 +217,15 @@ function today() { const day = String(date.getDate()).padStart(2, '0') return `${date.getFullYear()}-${month}-${day}` } +function disableCreditExpiry(date: Date) { + const boundary = new Date() + boundary.setHours(0, 0, 0, 0) + return date.getTime() <= boundary.getTime() +} +function creditExpiryLabel(account: FinanceAccountSummary) { + if (!account.creditExpiresOn) return '未设置' + return account.creditActive ? `${account.creditExpiresOn} 到期` : `${account.creditExpiresOn} 已到期` +} function paymentMethodLabel(value: unknown) { return ({ BANK_TRANSFER: '银行转账', WECHAT: '微信', ALIPAY: '支付宝', CASH: '现金', OTHER: '其他' } as Record)[String(value)] || '其他' } @@ -247,7 +257,11 @@ async function submitAdjustment() { } function openCreditLimit(account: FinanceAccountSummary) { adjustmentAccount.value = account - Object.assign(creditLimitForm, { amount: Number(account.creditLimit || 0).toFixed(2), remark: '' }) + Object.assign(creditLimitForm, { + amount: Number(account.creditLimit || 0).toFixed(2), + expiresOn: account.creditActive ? account.creditExpiresOn || '' : '', + remark: '', + }) creditLimitOpen.value = true } function handleAccountMore(command: string, account: FinanceAccountSummary) { @@ -262,10 +276,15 @@ async function submitCreditLimit() { ElMessage.warning('授信额度必须是大于或等于 0 且最多两位小数的金额') return } + if (!creditLimitForm.expiresOn || creditLimitForm.expiresOn <= today()) { + ElMessage.warning('授信到期日必须晚于今天') + return + } creditLimitSaving.value = true try { await updateFinanceCreditLimit(adjustmentAccount.value.customerId, { - creditLimit: amount.toFixed(2), remark: creditLimitForm.remark.trim() || undefined, + creditLimit: amount.toFixed(2), creditExpiresOn: creditLimitForm.expiresOn, + remark: creditLimitForm.remark.trim() || undefined, expectedAccountVersion: adjustmentAccount.value.accountVersion, }) creditLimitOpen.value = false @@ -408,7 +427,7 @@ onBeforeUnmount(clearVoucherPreview) - + @@ -419,7 +438,7 @@ onBeforeUnmount(clearVoucherPreview)