、
客户信用额度完善解决方案
适用系统:升鲜宝 SCM 后端 scm--admin 与前端 scm-admin-ui 源代码
|--------|---------------------------------------------------|
| 项目 | 说明 |
| 文档版本 | V1.0 |
| 文档状态 | 开发评审稿 / 可作为研发落地基线 |
| 编制日期 | 2021-07-04 |
| 覆盖范围 | 客户授信总额、冻结授信、可用授信、逾期状态、订单创建/修改/审核/作废/对账转应收的授信占用与释放 |
| 核心目标 | 统一授信计算口径,保证订单生命周期中的冻结、释放、转应收和逾期拦截准确、可审计、可并发 |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 核心结论 客户信用额度不是 cus_customer.credit_amount 一个字段可以完成的功能,而是由授信总额、客户欠款、未转应收订单冻结占用、逾期应收与风控策略共同决定。 订单未转应收前占用 frozenCreditAmount;转应收后释放冻结占用,并由 cus_customer_debt.debt_amount 承接风险敞口。 所有新增、修改、审核、反审核、作废、转应收场景必须以服务端授信控制为准,前端预校验只提升体验,不能替代后端校验。 |
目录
-
- 文档目标与边界
-
- 总体方案与核心原则
-
- 业务口径与计算公式
-
- 关键数据模型
-
- 服务分层与代码落点
-
- 授信校验规则
-
- 订单生命周期授信流程
-
- 并发、幂等与事务控制
-
- 客户信息页面与接口方案
-
- 额度下调保护
-
- 风控策略与强制提交审计
-
- 数据巡检、修复与监控
-
- 实施计划与优先级
-
- 测试用例
-
- 验收标准
-
- 风险与后续优化
1. 文档目标与边界
本文档用于规范客户信用额度在升鲜宝 SCM 中的设计、开发、联调和验收口径,重点解决"可用授信如何计算、订单如何占用/释放授信、逾期如何拦截、并发下如何防止超额占用、强制提交如何审计"等问题。
|--------|---------------------------------------------------------------------------------------------------------|------------------------------------------------|
| 范围 | 纳入内容 | 不纳入内容 |
| 客户信息 | creditAmount、frozenCreditAmount、availableCreditAmount、overdueState、overdueStateStr 的展示、编辑、导入导出和 DTO 对齐。 | 不在客户页面直接维护 frozenCreditAmount;冻结授信只能由授信控制服务变更。 |
| 订单流程 | 新增、修改、保存、审核、反审核、作废、对账转应收中的授信校验、占用、调整、释放。 | 不替代订单价格、库存、配送、支付等业务校验。 |
| 财务应收 | 订单转应收后释放冻结占用,并通过 cus_customer_debt.debt_amount 承接欠款。 | 不在本文展开收款、核销、坏账处理的完整财务方案。 |
| 风控策略 | 授信不足、逾期应收的跳过策略、强制提交标记和审计。 | 不定义复杂授信评级模型,仅保留策略接入点。 |
| 数据治理 | 冻结金额与占用日志一致性、逾期数据巡检、历史异常修复。 | 不直接批量修改生产数据,需经过候选统计和业务确认。 |
2. 总体方案与核心原则
目标架构采用"授信快照服务 + 授信控制服务 + 订单生命周期集成 + 风控审计"的方式收口。客户详情、客户列表、订单预校验、订单正式提交都不直接散落计算授信,而是复用统一服务。
|-----------|-------------------------------------------------------------|-----------------------------------------------------|
| 原则 | 说明 | 落地要求 |
| 服务端为准 | 前端保存/审核前可以预校验,但最终是否允许保存、审核、占用和释放必须以后端事务内结果为准。 | save、update、audit 等接口内部必须再次调用授信控制服务。 |
| 冻结与欠款分段承接 | 订单未转应收前占用 frozenCreditAmount;转应收后释放冻结占用,同时欠款 debtAmount 增加。 | 风险敞口连续,不因转应收动作凭空恢复可用授信。 |
| 占用按订单幂等 | 同一订单只保留一条有效占用主日志,修改订单只调整差额。 | 使用 source_type + order_code 或 idempotent_key 唯一约束。 |
| 释放可重复 | 反审核、作废、转应收等释放动作可能被重复触发,重复释放不得导致 frozenCreditAmount 为负。 | 只释放 occupy_state=1 的记录,并使用 greatest 保护。 |
| 逾期独立校验 | creditAmount 为空或小于等于 0 时按不启用额度上限处理,但逾期应收仍需拦截。 | 除非命中风控策略放行,否则 overdue=true 时不允许提交。 |
| 强制提交可审计 | 风控跳过授信不足或逾期限制必须可追溯。 | 记录 policyId、policyName、forceSubmitReason、操作人、时间和场景。 |
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 推荐的目标架构 CustomerCreditSnapshotService:负责客户授信快照、冻结订单列表、逾期状态、可用授信展示口径。 CustomerCreditControlService:负责订单授信校验、占用、调整、释放和日志。 订单服务:只编排业务生命周期,不自行实现授信公式。 风控策略服务:只决定是否允许跳过授信不足或逾期限制,不直接改写授信金额。 |
3. 业务口径与计算公式
|-----------------------|----------------------------------------------------|--------------------------------------------------|----------------------------------------|
| 指标 | 数据来源 | 业务含义 | 维护方式 |
| creditAmount | cus_customer.credit_amount | 客户授信总额。为空或 <=0 时,后端按"不启用额度上限"处理。 | 客户新增、编辑、导入维护;下调受保护。 |
| currentDebt | cus_customer_debt.debt_amount | 客户当前欠款,应收生成、收款、核销等财务动作影响。 | 财务应收与收款链路维护。 |
| frozenCreditAmount | cus_customer.frozen_credit_amount | 未转应收订单对授信的冻结占用。 | 只能由 CustomerCreditControlService 原子增减。 |
| availableCreditAmount | creditAmount - currentDebt - frozenCreditAmount | 当前可用授信。用于客户列表、详情和订单提示。 | 由授信快照服务实时计算,不建议落库。 |
| nextOrderAmount | 订单金额;已支付时按 0 | 本次订单将新增的风险敞口。 | 订单保存/审核前动态计算。 |
| exposureAmount | currentDebt + frozenCreditAmount + nextOrderAmount | 校验本次订单后的总风险敞口。 | 授信校验时计算。 |
| overdue | oms_order_bill_debt | 存在 still_debt_amount > 0 且 due_date < 当前时间的应收。 | 应收快照与账期到期日共同决定。 |
3.1 核心公式
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| availableCreditAmount = creditAmount - currentDebt - frozenCreditAmount nextOrderAmount = paymentState == 1 ? 0 : orderAmount exposureAmount = currentDebt + frozenCreditAmount + nextOrderAmount creditEnough = creditAmount == null || creditAmount <= 0 || exposureAmount <= creditAmount allowed = !overdue && creditEnough |
3.2 信用额度为空或小于等于 0 的推荐口径
- **额度控制:**creditAmount 为空、0 或负数时,不按额度上限拦截订单。
- **逾期控制:**额度上限不启用不代表放开逾期;逾期应收仍然拦截,除非风控策略允许跳过逾期。
- **页面展示:**建议 availableCreditAmount 保持后端计算值,同时增加 availableCreditAmountStr。当 creditAmount <= 0 时展示"不限制"或"未启用额度上限",避免用户误解为可用额度为负。
- **报表口径:**授信总额未启用的客户可单独分组统计,避免与正常授信客户混合计算额度使用率。
3.3 与客户账期/逾期状态的关系
授信控制中的 overdue 来源于 oms_order_bill_debt 的 due_date 与 still_debt_amount。由于 due_date 由客户账期快照生成,客户账期方案必须保证订单应收、期初应收、客户账单生成时写入准确的到期日。授信模块只消费应收快照,不应重新解析客户账期主数据。
4. 关键数据模型
|------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------|----------------------------------------------------------------------|
| 表/对象 | 关键字段 | 作用 | 开发要求 |
| cus_customer | credit_amount、frozen_credit_amount | 保存客户授信总额和冻结授信占用。 | frozen_credit_amount 默认 0;普通客户编辑接口不得直接覆盖冻结金额。 |
| cus_customer_debt | customer_id、debt_amount | 保存客户当前欠款。 | customer_id 建议唯一;应收、收款、核销必须保证欠款同步准确。 |
| oms_order_bill_debt | customer_id、still_debt_amount、due_date、overdue_state、settlement_state | 逾期应收判断基础。 | 建立 customer_id + due_date + still_debt_amount 的组合查询索引。 |
| cus_credit_occupy_log | customer_id、order_code、source_type、idempotent_key、occupy_amount、release_amount、occupy_state、occupy_time、release_time | 订单级授信占用主日志。 | source_type + order_code 或 idempotent_key 必须唯一;释放只处理 occupy_state=1。 |
| cus_credit_occupy_detail_log | occupy_log_id、stage、scene、before_amount、after_amount、delta_amount、payment_state | 授信占用变化明细日志。 | 新增、修改、审核、释放、转应收均记录明细,便于追溯。 |
4.1 索引与约束建议
|------------------------------|--------------------------------------------------------------------|----------------------|
| 对象 | 建议索引/约束 | 目的 |
| cus_credit_occupy_log | UNIQUE(source_type, order_code) | 防止同一来源订单重复生成占用记录。 |
| cus_credit_occupy_log | UNIQUE(idempotent_key) | 统一幂等标识,支持后续多来源扩展。 |
| cus_credit_occupy_log | INDEX(customer_id, occupy_state, source_type) | 客户详情查询冻结占用订单列表。 |
| cus_credit_occupy_detail_log | INDEX(occupy_log_id, create_time) | 追溯单个订单授信变化过程。 |
| cus_customer_debt | UNIQUE(customer_id) | 确保一个客户只有一条当前欠款记录。 |
| oms_order_bill_debt | INDEX(customer_id, due_date, still_debt_amount, del_flag, enabled) | 快速判断客户是否存在逾期未结应收。 |
| cus_customer | CHECK/默认值 frozen_credit_amount >= 0 | 避免异常释放或手工修复导致冻结金额为负。 |
5. 服务分层与代码落点
5.1 现有关键代码位置
|-----------|--------------------------------------------------------------------------|------------------------------|
| 模块 | 代码位置 | 职责 |
| 授信控制 | com.loveinway.modules.cus.service.CustomerCreditControlService | 授信校验、占用、调整、释放的统一接口。 |
| 授信实现 | com.loveinway.modules.cus.service.impl.CustomerCreditControlServiceImpl | 读取授信、欠款、冻结占用和逾期,执行规则判断与日志记录。 |
| 授信 Mapper | mapper/cus/CusCreditOccupyLogDao.xml | 行锁查询、原子增加/减少冻结金额、查询客户有效占用订单。 |
| 客户详情 | com.loveinway.modules.shop.service.impl.MallShopServiceImpl | 客户详情授信快照、信用额度下调保护。 |
| 客户控制器 | com.loveinway.modules.cus.controller.CusMallShopController | 客户端客户详情、冻结占用订单列表接口。 |
| 订单保存 | com.loveinway.modules.oms.service.impl.OmsOrderSubmitServiceImpl | 新增、修改订货单时授信校验和冻结。 |
| 订单审核 | com.loveinway.modules.oms.service.impl.OmsOrderAuditServiceImpl | 审核、反审核、作废订货单时授信处理。 |
| 对账转应收 | OmsOrderReconciliationServiceImpl / CustomerReceivablePostingServiceImpl | 订单转应收后释放冻结授信。 |
5.2 推荐新增/抽取 CustomerCreditSnapshotService
当前 CusMallShopController 和 MallShopServiceImpl 均存在授信快照计算逻辑,建议抽取到统一服务,客户列表、客户详情、订单授信校验共用该服务,减少重复实现和口径差异。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public interface CustomerCreditSnapshotService { RespCustomerCreditSnapshotDTO getSnapshot(Long customerId); RespCustomerCreditSnapshotDTO getSnapshot(Long customerId, String excludeOrderCode); List<RespCreditOccupyOrderDTO> listActiveOccupyOrders(Long customerId); boolean hasOverdueReceivable(Long customerId); } |
|-------------------------------------------|-------------------|-------------------------------------------------------------------------------|------------------------------|
| 方法 | 输入 | 输出/行为 | 说明 |
| getSnapshot | customerId | creditAmount、debtAmount、frozenCreditAmount、availableCreditAmount、overdueState | 客户详情、客户列表、订单校验统一使用。 |
| getSnapshot(customerId, excludeOrderCode) | customerId、当前订单编码 | 扣除当前订单已有占用后的授信快照 | 订单编辑/审核场景避免重复计算本订单冻结占用。 |
| listActiveOccupyOrders | customerId | 未释放订单占用列表 | 客户详情冻结占用订单明细。 |
| hasOverdueReceivable | customerId | boolean | 基于 oms_order_bill_debt 判断逾期。 |
6. 授信校验规则
checkOrderAllowed(customerId, orderAmount, excludeOrderCode, paymentState, scene) 只做校验,不改变冻结授信。任何会改变授信占用的动作必须调用 reserveOrderCredit、adjustOrderCredit 或 releaseOrderCredit。
6.1 校验输入
|------------------|-------------|--------------------------------------------------|
| 参数 | 含义 | 处理规则 |
| customerId | 客户 ID | 必填,校验客户存在且未删除。 |
| orderAmount | 订单目标金额 | 小于 0 直接拒绝;为空按 0 或业务默认处理,建议后端统一 BigDecimal.ZERO。 |
| excludeOrderCode | 编辑/审核当前订单编码 | 读取冻结占用时扣除当前订单已有占用,避免重复计算。 |
| paymentState | 支付状态 | paymentState=1 表示已支付,本次 nextOrderAmount 按 0 处理。 |
| scene | 业务场景 | SAVE、UPDATE、AUDIT、FORCE_SUBMIT 等,用于日志、风控策略和提示文案。 |
6.2 校验流程
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| snapshot = customerCreditSnapshotService.getSnapshot(customerId, excludeOrderCode) nextOrderAmount = paymentState == 1 ? 0 : orderAmount exposureAmount = snapshot.debtAmount + snapshot.frozenCreditAmount + nextOrderAmount creditEnough = snapshot.creditAmount == null || snapshot.creditAmount <= 0 || exposureAmount <= snapshot.creditAmount overdue = customerCreditSnapshotService.hasOverdueReceivable(customerId) allowed = !overdue && creditEnough if (!allowed) { forceSubmitAllowed = riskPolicyService.canSkip(overdue, !creditEnough, customerId, scene) } return RespOrderCreditCheckDTO(...) |
6.3 返回 DTO
|-------------------------------|---------------|------------------------------------|
| 字段 | 类型 | 说明 |
| allowed | Boolean | 是否允许直接提交。 |
| creditEnough | Boolean | 授信额度是否足够;creditAmount<=0 时为 true。 |
| overdue | Boolean | 是否存在逾期未结应收。 |
| forceSubmitAllowed | Boolean | 不允许直接提交时,是否允许在风控策略下强制提交。 |
| message | String | 用户提示文案。 |
| creditAmount | BigDecimal | 授信总额。 |
| currentDebt | BigDecimal | 当前欠款。 |
| frozenCreditAmount | BigDecimal | 冻结授信。 |
| orderAmount | BigDecimal | 本次订单金额。 |
| exposureAmount | BigDecimal | 本次提交后的风险敞口。 |
| riskPolicyId / riskPolicyName | Long / String | 命中的风控策略。 |
6.4 拦截提示文案建议
|----------|-----------------------------------------------------------------------------------------------------------------|
| 场景 | 提示文案 |
| 授信不足 | 客户授信额度不足:授信总额 {creditAmount},当前欠款 {debtAmount},冻结授信 {frozenAmount},本次订单 {orderAmount},提交后风险敞口 {exposureAmount}。 |
| 存在逾期 | 客户存在逾期未结应收,暂不允许提交订单。请先完成收款/核销,或按风控策略发起强制提交。 |
| 授信不足且逾期 | 客户存在逾期未结应收,且授信额度不足。需处理逾期或由具备权限的人员按风控策略强制提交。 |
| 额度未启用但逾期 | 客户未启用额度上限,但存在逾期未结应收,暂不允许提交订单。 |
| 强制提交通过 | 已根据风控策略【{policyName}】强制提交,请在业务日志中确认原因。 |
7. 订单生命周期授信流程
|----------|-------------------------------------------------|----------------------------------------------------|---------------------|---------------------------------|
| 业务动作 | 前端动作 | 后端授信动作 | 冻结授信变化 | 日志要求 |
| 新增订单 | 保存前调用 /oms/omsorderbill/checkCreditBeforeSave | 保存接口内部再次 reserveOrderCredit | 未支付且金额>0 时增加占用 | 主日志创建/更新,明细记录 SAVE 场景。 |
| 修改订单 | 按目标金额重新预校验 | reserveOrderCredit 读取已有占用并调整差额 | 金额增加则增冻,金额减少则释放差额 | 明细记录 UPDATE 场景和 deltaAmount。 |
| 审核订单 | 审核前调用 /oms/omsorderbill/checkCreditBeforeSubmit | 审核流程再次 checkOrderAllowed 并 reserveOrderCredit | 确保审核时订单金额和支付状态仍满足规则 | 记录 AUDIT 场景;强制提交记录原因。 |
| 反审核 | 无需前端计算冻结金额 | releaseOrderCredit(orderCode, 反审核订货单释放授信占用) | 释放该订单有效占用 | 明细记录 RELEASE_REVERSE_AUDIT。 |
| 作废订单 | 作废确认 | releaseOrderCredit(orderCode, 作废订货单释放授信占用) | 释放该订单有效占用 | 明细记录 RELEASE_CANCEL。 |
| 对账转应收 | 对账确认 | 插入应收后 releaseOrderCredit(orderCode, 订单对账转应收释放授信占用) | 冻结减少,欠款增加 | 明细记录 POST_TO_RECEIVABLE,风险敞口连续。 |
| 已支付订单 | 支付状态传入校验接口 | nextOrderAmount 按 0;如已有占用则应释放或调整为 0 | 不占用授信 | 记录 paymentState=1。 |
| 金额为 0 | 正常保存/审核 | 不新增占用;如原有占用则调整为 0 | 释放原占用 | 记录 deltaAmount。 |
7.1 生命周期示意
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 订单新增/修改 -> checkCreditBeforeSave 仅预校验 -> save/update 事务内 reserveOrderCredit -> cus_credit_occupy_log 生效,cus_customer.frozen_credit_amount 增加/调整 订单审核 -> checkCreditBeforeSubmit 预校验 -> audit 事务内再次校验并 reserveOrderCredit -> 审核通过,冻结占用保持准确 反审核 / 作废 -> releaseOrderCredit -> 释放有效占用,重复释放无影响 对账转应收 -> 插入 oms_order_bill_debt -> cus_customer_debt.debt_amount 增加 -> releaseOrderCredit -> 风险敞口从冻结授信转为应收欠款 |
7.2 reserveOrderCredit / adjustOrderCredit 推荐逻辑
- **读取当前占用:**按 source_type + order_code 查询主日志,并使用 selectByOrderCodeForUpdate 加行锁。
- **计算目标占用:**未支付且订单金额大于 0 时 targetOccupyAmount=orderAmount;已支付或金额为 0 时 targetOccupyAmount=0。
- **计算差额:**deltaAmount = targetOccupyAmount - currentActiveOccupyAmount。
- **差额大于 0:**调用 increaseFrozenCredit 原子增加冻结金额;SQL 条件必须校验额度足够。
- **差额小于 0:**调用 decreaseFrozenCredit 释放差额;使用 greatest 防止出现负数。
- **差额等于 0:**不更新 cus_customer,仅记录必要的审计日志或直接返回。
- **更新主日志:**保持 occupy_amount 为目标占用金额,release_amount 累加释放金额,occupy_state 与占用金额一致。
- **写入明细日志:**每次差额调整均记录前后金额、delta、场景、支付状态、操作人。
8. 并发、幂等与事务控制
|----------|-------------------------------------------------------------------------------------------------|-----------------------------|
| 控制点 | 现有/推荐实现 | 防护目标 |
| 同一订单并发修改 | selectByOrderCodeForUpdate 对 cus_credit_occupy_log 按 source_type + order_code 加行锁。 | 避免同一订单重复冻结或重复释放。 |
| 幂等标识 | idempotent_key = OMS_ORDER:{orderCode}。 | 同一订单重复请求不会生成多条有效占用。 |
| 冻结增加 | increaseFrozenCredit 使用 SQL 条件校验 credit_amount >= debt_amount + frozen_credit_amount + 本次新增占用。 | 并发订单下防止超额授信。 |
| 冻结释放 | decreaseFrozenCredit 使用 greatest(frozen_credit_amount - amount, 0)。 | 释放动作重复调用不导致负数。 |
| 释放状态 | 只处理 occupy_state=1 的有效占用记录。 | 已释放或不存在的记录直接返回,保证释放幂等。 |
| 事务边界 | 订单状态变更、占用日志、cus_customer 冻结金额变更应在同一事务内完成。 | 避免订单已审核但授信未冻结、或冻结已释放但订单未作废。 |
| 异常处理 | SQL 更新行数为 0 时抛出授信不足或并发冲突异常。 | 调用方明确失败原因并回滚订单动作。 |
8.1 原子增加冻结授信的伪 SQL
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| UPDATE cus_customer s LEFT JOIN cus_customer_debt d ON d.customer_id = s.id SET s.frozen_credit_amount = IFNULL(s.frozen_credit_amount, 0) + :deltaAmount WHERE s.id = :customerId AND ( s.credit_amount IS NULL OR s.credit_amount <= 0 OR s.credit_amount >= IFNULL(d.debt_amount, 0) + IFNULL(s.frozen_credit_amount, 0) + :deltaAmount ); |
说明:如果更新行数为 0,表示在当前并发状态下额度不足或客户状态异常,应抛出业务异常并回滚订单动作。
8.2 一致性巡检公式
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 客户冻结授信一致性: cus_customer.frozen_credit_amount == SUM(cus_credit_occupy_log.occupy_amount - cus_credit_occupy_log.release_amount) WHERE occupy_state = 1 AND customer_id = cus_customer.id 风险敞口: exposureAmount = cus_customer_debt.debt_amount + cus_customer.frozen_credit_amount 可用授信: availableCreditAmount = cus_customer.credit_amount - exposureAmount |
9. 客户信息页面与接口方案
9.1 客户信息页面展示
|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| 页面 | 字段/功能 | 要求 |
| cus/mallshop.vue 客户列表 | creditAmount、frozenCreditAmount、availableCreditAmount、overdueStateStr | 列表展示授信总额、冻结授信、可用授信、逾期状态;额度未启用时展示明确文案。 |
| cus/mallshopview.vue 客户详情 | creditAmount 可编辑;frozenCreditAmount、availableCreditAmount、overdueStateStr 只读 | 编辑保存时走额度下调保护;冻结授信不可手工输入。 |
| 冻结占用订单明细 | orderCode、billStateName、creditOccupyAmount、creditOccupyStageName、orderAmount、sendoutAmount、affireAmount、deliveryDateTime、occupyTime | 调用 /cus/mallshop/creditOccupyOrders;只展示 occupy_state=1 的有效占用。 |
| shop/mallshop.vue / shop/mallshopview.vue | creditAmount 展示或编辑 | 如果仍保留编辑能力,必须复用后端同一额度下调保护;冻结/可用展示建议逐步统一到客户模块。 |
9.2 接口清单
|--------|--------------------------------------------------|----------------|-----------------------------------------------|
| 方法 | 接口 | 用途 | 关键说明 |
| GET | /cus/mallshop/{id} | 客户详情 | 返回授信总额、冻结授信、可用授信、逾期状态。 |
| GET | /cus/mallshop/page | 客户列表 | 列表展示授信字段,建议统一走快照服务。 |
| GET | /cus/mallshop/creditOccupyOrders?customerId={id} | 冻结占用订单列表 | 返回未释放订单占用明细。 |
| POST | /oms/omsorderbill/checkCreditBeforeSave | 订货单新增/修改保存前预校验 | 只校验,不改变冻结授信。 |
| POST | /oms/omsorderbill/checkCreditBeforeSubmit | 订货单审核前预校验 | 只校验,返回是否允许强制提交。 |
| POST | /oms/omsorderbill/save | 新增订货单 | 服务端仍会 reserveOrderCredit。 |
| PUT | /oms/omsorderbill/update | 修改订货单 | 服务端按目标金额调整占用。 |
| 现有接口 | 审核、反审核、作废、对账转应收 | 订单生命周期动作 | 内部集成 reserveOrderCredit 或 releaseOrderCredit。 |
9.3 前端交互建议
- **保存/审核前预校验:**订单保存前调用 checkCreditBeforeSave,审核前调用 checkCreditBeforeSubmit。
- **二次确认弹窗:**授信不足或逾期时展示 creditAmount、currentDebt、frozenCreditAmount、orderAmount、exposureAmount 和具体原因。
- **强制提交入口:**仅当 forceSubmitAllowed=true 且当前用户具备权限时显示强制提交按钮,并要求填写 forceSubmitReason。
- **客户详情:**冻结占用订单列表可点击订单号跳转订单详情,便于业务人员处理占用。
- **只读字段保护:**frozenCreditAmount、availableCreditAmount、overdueStateStr 前端只读,后端也不接受普通编辑接口覆盖。
10. 额度下调保护
客户信用额度允许上调或保持不变;当新额度低于旧额度时,必须校验新额度不能低于当前已发生的风险敞口。该规则避免把客户授信调低到欠款与冻结占用以下,造成可用授信口径失真。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| lowerLimit = currentDebt + frozenCreditAmount if (newCreditAmount < oldCreditAmount && newCreditAmount < lowerLimit) { throw new BusinessException( "授信额度不能低于当前欠款与冻结授信占用合计" ); } |
|-------------|---------|--------|--------|---------|-----------------|
| 场景 | 旧额度 | 欠款 | 冻结 | 新额度 | 结果 |
| 上调额度 | 1000 | 600 | 200 | 1500 | 允许。 |
| 保持额度 | 1000 | 600 | 200 | 1000 | 允许。 |
| 下调但不低于风险敞口 | 1000 | 600 | 200 | 800 | 允许,最低可设额度为 800。 |
| 下调低于风险敞口 | 1000 | 600 | 200 | 700 | 拒绝。 |
| 额度未启用改为启用 | 0/null | 600 | 200 | 1000 | 允许;新额度 >= 800。 |
| 额度未启用改为过低额度 | 0/null | 600 | 200 | 500 | 拒绝。 |
11. 风控策略与强制提交审计
当授信校验不通过时,系统可以通过 B2bRiskControlPolicyService 判断是否允许跳过额度不足或逾期限制。强制提交不是绕过规则,而是在有策略、有权限、有原因、有日志的前提下进行受控放行。
|---------|--------------------------------------------------------------------------------------------|
| 控制项 | 要求 |
| 策略范围 | 明确策略可跳过的是"额度不足""逾期限制"还是两者都可跳过。 |
| 权限控制 | 强制提交按钮和后端接口均校验权限,不能仅靠前端隐藏。 |
| 原因填写 | forceSubmitReason 必填,并限制长度、过滤空白。 |
| 审计字段 | policyId、policyName、forceSubmitReason、operatorId、operatorName、operateTime、scene、orderCode。 |
| 报表追溯 | 强制提交订单可在报表中筛选,便于风控复盘。 |
| 二次校验 | 正式提交接口仍需重新计算当前授信快照,避免预校验后金额或欠款发生变化。 |
11.1 强制提交结果处理
|----------|------------|--------------|
| 校验状态 | 策略结果 | 最终结果 |
| 额度不足,未逾期 | 策略允许跳过额度不足 | 允许强制提交并记录审计。 |
| 逾期,额度足够 | 策略允许跳过逾期 | 允许强制提交并记录审计。 |
| 逾期且额度不足 | 策略同时允许两项跳过 | 允许强制提交并记录审计。 |
| 逾期且额度不足 | 策略只允许其中一项 | 不允许强制提交。 |
| 策略不存在或失效 | 不允许跳过 | 拒绝提交。 |
12. 数据巡检、修复与监控
12.1 上线前数据库确认
-
确认 cus_customer.credit_amount、cus_customer.frozen_credit_amount 字段存在,frozen_credit_amount 默认值为 0,历史空值可统一回填为 0。
-
确认 cus_customer_debt.customer_id 唯一,debt_amount 与应收/收款链路一致。
-
确认 cus_credit_occupy_log、cus_credit_occupy_detail_log 字段完整,source_type + order_code 或 idempotent_key 有唯一约束。
-
确认 oms_order_bill_debt 的 due_date、still_debt_amount、del_flag、enabled 等字段可支撑逾期查询。
-
确认已有订单状态与占用日志不存在明显不一致,例如已作废订单仍处于 occupy_state=1。
12.2 巡检脚本方向
|---------------------------------------------|-----------------|--------------------------------------|
| 巡检项 | 异常含义 | 处理建议 |
| frozen_credit_amount < 0 | 重复释放或手工改库异常。 | 立即修正为 0,并追查释放日志。 |
| 客户冻结金额 != 有效占用日志汇总 | 占用日志与客户冻结金额不一致。 | 以有效占用日志为准生成候选修复清单,业务确认后修复。 |
| occupy_state=1 但订单已作废/已反审核/已转应收 | 订单生命周期释放缺失。 | 补调用 releaseOrderCredit 或按日志生成修复 SQL。 |
| 订单未转应收且未作废但没有占用日志 | 历史订单未接入授信占用。 | 根据订单状态和金额评估是否补冻结,避免直接批量冻结影响客户。 |
| creditAmount>0 且 debt+frozen>creditAmount | 客户已超授信。 | 可能来自历史数据或强制提交,纳入风控报表。 |
| 逾期应收 due_date 为空 | 授信逾期判断可能漏拦截。 | 先修客户账期/应收到期日快照,再启用严格逾期控制。 |
12.3 监控指标
- 每日统计冻结授信不一致客户数、金额差异合计。
- 每日统计强制提交订单数、涉及金额、命中策略分布。
- 每日统计逾期拦截次数、授信不足拦截次数、并发额度不足异常次数。
- 对 frozen_credit_amount 负数、占用日志重复、释放后仍有效等严重异常建立告警。
- 定期核对 cus_customer_debt.debt_amount 与应收明细未结金额汇总差异。
13. 实施计划与优先级
|---------|----------|-------------------------------------------------------------------------------|-------------------|
| 优先级 | 任务 | 主要内容 | 交付物 |
| P0 | 数据库与约束确认 | 确认字段、默认值、唯一约束、索引;补齐必要 DDL。 | 数据库检查清单 / DDL 脚本。 |
| P0 | 统一授信快照服务 | 抽取 CustomerCreditSnapshotService,统一客户详情、列表、订单校验口径。 | 服务类、DTO、单元测试。 |
| P0 | 授信控制服务闭环 | 完善 checkOrderAllowed、reserveOrderCredit、adjustOrderCredit、releaseOrderCredit。 | 核心服务实现与异常文案。 |
| P0 | 订单生命周期接入 | 保存、修改、审核、反审核、作废、转应收全部接入授信控制。 | 订单服务改造与回归用例。 |
| P0 | 额度下调保护 | 客户编辑、导入更新均校验新额度不低于 debt+frozen。 | 后端校验与前端提示。 |
| P1 | 客户页面展示 | 客户列表/详情展示授信总额、冻结、可用、逾期与冻结占用订单。 | 前端页面与接口联调。 |
| P1 | 风控强制提交 | 接入策略、权限、原因、日志和报表。 | 策略联调与审计日志。 |
| P1 | 导入导出对齐 | 客户导入导出保留 creditAmount,导入不得直接覆盖 frozenCreditAmount。 | 模板、校验、错误行提示。 |
| P1 | 数据巡检脚本 | 冻结金额一致性、有效占用日志、逾期应收、超授信客户巡检。 | 只读巡检 SQL 与修复候选清单。 |
| P2 | 历史数据治理 | 按候选清单分批修复历史冻结/日志/逾期数据。 | 修复记录与后置校验报告。 |
| P2 | 代码治理 | 清理乱码注释、复核 MallShopDao.updateCreditAmount 语义、抽象 source_type。 | 代码清理 PR。 |
13.1 推荐开发顺序
-
先完成数据库字段、索引、唯一约束确认,避免后续服务无法保证幂等。
-
抽取授信快照服务,统一客户详情、列表、订单校验的数据来源。
-
完善授信控制服务的校验、占用、调整、释放逻辑和异常文案。
-
改造订单新增、修改、审核、反审核、作废、对账转应收链路。
-
改造客户信息页面和额度下调保护。
-
接入风控策略、强制提交审计和报表字段。
-
补充自动化/手工测试,并执行上线前数据巡检。
-
上线后观察巡检指标,再按审批结果处理历史修复。
14. 测试用例
|--------|------------------------------------------|---------------------------------|-----------------------|----------------------------------------|
| 编号 | 场景 | 前置条件 | 操作 | 期望结果 |
| TC01 | 授信充足 | credit=1000,debt=200,frozen=100 | 新订单 600 | 允许提交,exposure=900。 |
| TC02 | 授信不足 | credit=1000,debt=200,frozen=100 | 新订单 800 | 拒绝提交,exposure=1100,creditEnough=false。 |
| TC03 | 额度未启用 | credit=0 或 null,无逾期 | 新订单任意金额 | 额度上限不拦截,仍返回 creditEnough=true。 |
| TC04 | 额度未启用但逾期 | credit=0 或 null,存在逾期应收 | 新订单提交 | 拒绝提交,除非风控策略允许跳过逾期。 |
| TC05 | 订单金额调增 | 原订单占用 300 | 修改为 500 | frozenCreditAmount 只增加 200。 |
| TC06 | 订单金额调减 | 原订单占用 500 | 修改为 300 | frozenCreditAmount 释放 200。 |
| TC07 | 订单修改为已支付 | 原订单占用 500 | paymentState=1 | 目标占用为 0,释放原占用。 |
| TC08 | 重复保存 | 同一订单重复调用 reserveOrderCredit | 并发/重复请求 | 不重复冻结,主日志保持一条有效记录。 |
| TC09 | 审核前预校验 | 订单已有占用 300 | 审核传入 excludeOrderCode | 校验时扣除本订单已有占用,避免重复计算。 |
| TC10 | 反审核释放 | 订单占用有效 | 反审核 | 释放占用,重复反审核释放无影响。 |
| TC11 | 作废释放 | 订单占用有效 | 作废订单 | 释放占用,occupy_state 更新为已释放。 |
| TC12 | 转应收 | 订单占用 500 | 对账转应收 | frozen 减少 500,debt 增加 500,可用授信风险口径连续。 |
| TC13 | 额度下调保护 | debt=600,frozen=200 | 额度从 1000 调到 700 | 拒绝,最低可设置额度为 800。 |
| TC14 | 额度下调刚好等于风险敞口 | debt=600,frozen=200 | 额度从 1000 调到 800 | 允许。 |
| TC15 | 并发占用 | 客户剩余额度只够一笔订单 | 两个订单同时提交 | 只有满足 SQL 条件的请求成功,另一请求返回授信不足。 |
| TC16 | 强制提交审计 | 存在授信不足,策略允许跳过 | 填写原因强制提交 | 订单成功,记录 policyId、policyName、原因、操作人、时间。 |
| TC17 | 策略不足 | 逾期且授信不足,策略只允许跳过额度不足 | 强制提交 | 仍拒绝,因为逾期未被策略放行。 |
| TC18 | 客户冻结订单列表 | 客户存在多笔有效占用 | 打开客户详情 | 列表展示订单号、状态、占用金额、占用时间等字段。 |
| TC19 | 冻结一致性巡检 | 人工制造 frozen 与日志汇总不一致 | 执行巡检 SQL | 输出异常客户和差异金额。 |
| TC20 | cus_customer.frozen_credit_amount 不可直接编辑 | 调用客户编辑接口传入 frozenCreditAmount | 保存客户 | 后端忽略或拒绝该字段,不允许普通接口覆盖冻结金额。 |
15. 验收标准
- 客户列表和详情能正确展示授信总额、冻结授信、可用授信和逾期状态。
- availableCreditAmount、overdueState、frozenCreditAmount 的计算口径在客户列表、详情、订单校验中一致。
- 新增、修改、审核订单均会在服务端执行授信校验和冻结占用,不能只依赖前端预校验。
- 同一订单多次保存、审核或并发修改不会重复冻结;订单调额只调整差额。
- 反审核、作废、对账转应收可以释放冻结占用,且重复释放不影响结果。
- 订单转应收后 frozenCreditAmount 减少,cus_customer_debt.debt_amount 增加,风险敞口连续。
- creditAmount 为空或小于等于 0 时不启用额度上限,但逾期应收仍然拦截。
- 额度下调不能低于 debtAmount + frozenCreditAmount。
- 授信不足、逾期、强制提交均有明确提示和审计日志。
- cus_credit_occupy_log 具备幂等约束,cus_credit_occupy_detail_log 能追溯每次变化。
- 上线前巡检无严重异常;如存在历史异常,必须形成候选清单、业务确认和修复记录。
16. 风险与后续优化
|---------------------------------------|-------------------------------|-------------------------------------------|
| 风险/问题 | 影响 | 建议处理 |
| 授信快照计算逻辑重复 | 客户详情、客户列表、订单校验结果可能不一致。 | 抽取 CustomerCreditSnapshotService 并统一调用。 |
| MallShopDao.updateCreditAmount 语义疑似不清 | 方法名像更新额度,SQL 却可能是扣减额度,存在误用风险。 | 复核所有调用点,必要时重命名为 decreaseCreditAmount 或废弃。 |
| 冻结金额与日志汇总不一致 | 客户可用授信展示错误,订单可能误拦截或误放行。 | 上线前后执行一致性巡检,修复必须留痕。 |
| 逾期 due_date 依赖账期快照 | 到期日缺失会导致逾期拦截失效。 | 先确保应收明细到期日完整,再启用严格逾期控制。 |
| 强制提交缺少审计 | 风控放行不可追溯,后续责任不清。 | 强制提交必须记录策略、原因、人员、时间和订单。 |
| 多订单来源扩展 | 不同来源 orderCode 可能冲突。 | source_type 必须纳入唯一键,idempotent_key 按来源扩展。 |
| 中文注释/Swagger 乱码 | 影响接口文档和后续维护。 | 作为 P2 技术债治理,不与核心授信逻辑混改。 |
| 历史订单未接入授信 | 上线初期冻结金额可能低估。 | 以巡检报告和业务确认决定是否补冻结,不建议直接盲目回填。 |
附录 A:开发检查清单
|--------------------------------------------|----------|--------------------------------------------------------------------------------------|
| 检查项 | 是否完成 | 备注 |
| 数据库字段与唯一约束已确认 | □ | cus_customer、cus_customer_debt、cus_credit_occupy_log、detail_log、oms_order_bill_debt。 |
| CustomerCreditSnapshotService 已抽取 | □ | 客户列表、详情、订单校验共用。 |
| CustomerCreditControlService 支持校验/占用/调整/释放 | □ | 含异常文案和事务控制。 |
| 订单新增、修改、审核接入 reserveOrderCredit | □ | 服务端二次校验。 |
| 反审核、作废、转应收接入 releaseOrderCredit | □ | 释放幂等。 |
| 客户额度下调保护已接入 | □ | 编辑与导入均生效。 |
| 风控强制提交审计已完成 | □ | 策略、原因、操作人、时间。 |
| 前端授信提示和占用订单列表已完成 | □ | 客户详情可追溯冻结来源。 |
| 自动化/手工测试用例已执行 | □ | 至少覆盖本文 TC01-TC20。 |
| 上线前数据巡检已执行 | □ | 异常已确认或修复。 |
附录 B:建议保留的核心方法清单
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| CustomerCreditControlService - checkOrderAllowed(customerId, orderAmount, excludeOrderCode, paymentState, scene) - reserveOrderCredit(customerId, orderCode, orderAmount, paymentState, scene) - adjustOrderCredit(customerId, orderCode, targetOccupyAmount, scene) - releaseOrderCredit(orderCode, releaseReason) CustomerCreditSnapshotService - getSnapshot(customerId) - getSnapshot(customerId, excludeOrderCode) - listActiveOccupyOrders(customerId) - hasOverdueReceivable(customerId) |