SOP-606:外采帐户直充审批

标准 SOP 测试文档;每个任务(Task ID)代表一个独立的测试步骤,便于人工/自动化测试按序号完成并记录进度。

文档元数据

  • 文档类型:SOP / Checklist
  • 适用场景:外采广告帐户"直充"——财务直接对外采广告帐户做单边入账(差错纠偏 / 外部直接到账落账),不动端口余额、不走媒介审批,与 SOP-601(外采帐户充值,经端口)和 SOP-602(外采端口入帐转账)平级互不重叠
  • 入口位置:CRM → 广告账户 → 「外采」Tab → 选行打开抽屉 → 顶部「充值」按钮(外采账户上下文中已替换为"直充"实现,非外采账户仍走原 Transfer)
  • 配置文件
  • migrations/20260429_create_external_direct_deposit_review_template.sql(SOP-606 审批模板)
  • migrations/20260429_rollback_external_direct_deposit_review_template.sql(回滚脚本)
  • 关联 spec.kiro/specs/external-account-direct/
  • 创建者:财务 / 运营 / QA
  • 最近更新时间:2026-04-29
  • 相关接口定义:参考 crm.swagger.jsonCashServiceReviewService 模块(特别是 POST /api/v1/cash/transferPOST /api/v1/cash/trans_ordersGET /api/v1/cash/trans_orders/{id}POST /api/v1/cash/porter_balanceGET /api/v1/reviews/{flow_id}POST /api/v1/reviews/{flow_id}/actions

工作流概述

流程图

graph LR
    A[抽屉点充值发起申请] --> B[财务审批]
    B -->|通过| C[settle_external_direct_deposit 原子结算]
    C --> D1[accounts.balance += amount]
    C --> D2[写 transactions RECHARGE / 外采帐户直充]
    C --> D3[订单状态 SUCCESS]
    B -->|驳回| E[update_status FAILED 不动账]

审批角色

步骤 审批人 模式 说明
财务审批 往来会计 / 总账会计 / 税务会计 / 财务主管 / 财务总 any(单步审批) 不经上级领导,与 SOP-605 端口退款一致;与 SOP-604 外采付款的两步不同

与既有外采流程的差异速查

维度 SOP-601 外采帐户充值 SOP-602 外采端口入帐 SOP-606 外采帐户直充
入口 RPC Recharge(IsExternal=true) Transfer(IsExternal=true) Transfer(mode=TRANSFER_MODE_DIRECT_DEPOSIT)
媒介审批
动 ext_corps.balance 是(扣端口余额) 是(扣端口余额) 否(端口余额只读)
动 accounts.balance 是(增帐户) 是(增帐户) 是(增帐户)
transactions.type RECHARGE TRANSFER RECHARGE
transactions.sub_act_type 端口充值 (由业务定) 外采帐户直充
OrderType (走 SOP-601 通道) (走 SOP-602 通道) ORDER_TYPE_EXTERNAL_DIRECT_DEPOSIT
审批事件 transOrder.external.recharge transOrder.external.transfer transOrder.external.directDeposit

前置条件

条件 检查方式
已使用有权限用户登录(拥有 CashService.TransferDirectCashService.TransferFull Casbin policy.csv 中含 p,$角色,*,CashService.TransferDirect
目标广告帐户存在且 provider='EXTERNAL'、关联端口存在 SELECT account_id, provider FROM advertisers WHERE account_id=?agency.porter 非空
客户的外采帐户行已存在 SELECT id FROM accounts WHERE customer_id=? AND account_type='外采' AND porter_id=?(一般已通过 SOP-601 建立)
已配置 SOP-606 审批模板 SELECT id FROM review_templates WHERE sop_number='SOP-606' AND is_active=1
审批模板 on_approved_actions[0].configaccount_id=payload.AccountID / amount=payload.Amount 否则结算 action 会报字段缺失
抽屉里"充值"按钮可点击 当前 advertiser 已绑定 customerIdpolicyLabelsstatus='SUCCESS' 的标签(main.tsx 的 canOperate 守门)

任务矩阵

任务 T1:进入外采广告帐户抽屉

入口:CRM 菜单 → 广告账户

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:打开 /crm/adaccount 默认 Tab "全部" 页面加载 截图 [ ] 步骤 2
步骤2:切到「外采」Tab 点击 Tab 列表只剩 provider=EXTERNAL 的广告帐户 列字段 provider 全为 EXTERNAL 截图 [ ] 步骤 3
步骤3:点击账户行打开抽屉 单击行 右侧抽屉打开,顶部三按钮显示「充值 / 退款 / 广告户转账」 抽屉渲染 截图 [ ] 任务 T2

任务 T2:填写并提交直充表单

入口:抽屉 → 顶部「充值」按钮(外采账户上下文已替换为直充)

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:点击「充值」 Dialog 打开,标题「外采帐户直充」 标题正确 截图 [ ] 步骤 2
步骤2:核对只读卡 顶部展示客户名 + 外采广告账户(name - accountId 字段值与抽屉中一致 截图 [ ] 步骤 3
步骤3:核对端口资金卡 端口名 / 端口余额 / 累计充值 / 累计提现 全部展示;端口未关联时显示「该广告账户未关联端口」 POST /api/v1/cash/porter_balance 返回一致 截图 [ ] 步骤 4
步骤4:填金额 amount=100 输入框显示 100 字段值正确 截图 [ ] 步骤 5
步骤5:填直充事由 direct_deposit_reason="差错纠偏 e2e 测试" textarea 显示输入内容 字段值正确 截图 [ ] 步骤 6
步骤6:(可选)备注 / 附件 备注 e2e;附件 0~10 个 字段正常显示 UI 显示 截图 [ ] 步骤 7
步骤7:提交 点击「提交审批」 trans_orders 新增一行 order_type=ORDER_TYPE_EXTERNAL_DIRECT_DEPOSIT, status=REVIEWING,触发审批 SELECT order_type, status FROM trans_orders ORDER BY id DESC LIMIT 1 截图 [ ] 任务 T3
步骤8:边界——空事由 留空 direct_deposit_reason 提交 HTTP 400,消息 direct_deposit_reason 不能为空 Toast 截图 [ ]
步骤9:边界——金额 ≤ 0 amount=0 HTTP 400,消息 直充金额必须大于0 Toast 截图 [ ]
步骤10:边界——非外采账户(绕过 UI) 直接 POST /api/v1/cash/transfer mode=DIRECT_DEPOSIT to_account_id=GDT账户 HTTP 400,消息 广告帐户 X 不是外采帐户(provider=GDT) curl + 200 截图 [ ]
步骤11:边界——拒绝源端字段 body 加 from_account_id / use_credit / use_pending HTTP 400,消息 直充分支不接受 from_account_id(或对应字段名) curl + 400 截图 [ ]
步骤12:边界——客户外采帐户行不存在 选未通过 SOP-601 建过外采帐户的客户 HTTP 400,消息含 请先通过 SOP-601 建立外采帐户 Toast 截图 [ ]

任务 T3:查看审批详情

入口:审批中心 → 我发起的 / 工作流详情

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:打开审批详情 点击刚创建的审批 payload 含 businessDetails.businessReason 与提交一致 UI 渲染 截图 [ ] 步骤 2
步骤2:核对扁平字段 customerId / porterId / amount 与提交一致;isExternal=true 详情面板字段 截图 [ ] 任务 T4

任务 T4:财务审批(唯一步骤)

入口:审批中心 → 待我审批(财务 position 登录)

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:财务登录 position{往来会计, 总账会计, 税务会计, 财务主管, 财务总} 登录成功 页面加载 截图 [ ] 步骤 2
步骤2:查看待审批 筛选 SOP-606 列表出现该单 列表含 "外采帐户直充审批" 截图 [ ] 步骤 3
步骤3:通过审批 点击「通过」 review_instances.status=APPROVED,触发 settle_external_direct_deposit DB + log 截图 [ ] 任务 T5
步骤4:(独立场景)驳回 点击「驳回」 trans_orders.status=FAILEDaccounts.balance 不变;transactions 无新行;ext_corps.balance 不变 DB 查询 截图 [ ]

任务 T5:验证资金落账

入口:MySQL 直查

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:accounts.balance 增加 SELECT balance, total_income, cash_income, cash_balance FROM accounts WHERE customer_id=? AND account_type='外采' AND porter_id=? 4 个值都 +=amount 数值比对 截图 [ ] 步骤 2
步骤2:transactions 新增 SELECT type, sub_act_type, amount, status, account_type, porter_id FROM transactions WHERE trans_order_id=TO_ID RECHARGE / 外采帐户直充 / amount / SUCCESS / 外采 / porter_id 全字段核对 截图 [ ] 步骤 3
步骤3:ext_corps.balance 不变 SELECT balance, total_balance FROM ext_corps WHERE id=PORTER_ID(前后对比) 两个值与提交前一致 数值比对 截图 [ ] 步骤 4
步骤4:trans_orders 最终态 SELECT status, err_msg FROM trans_orders WHERE id=TO_ID SUCCESS + err_msg 空 字段核对 截图 [ ] 步骤 5
步骤5:审批失败路径——客户外采帐户行不存在 故意删 accounts 行后审批通过 trans_orders.status=FAILED + err_msg外采帐户不存在accounts 无副作用 字段核对 截图 [ ]

任务 T6:列表与详情字段验证

入口:CRM → 审批中心 / 后台直查

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:通用列表过滤 POST /api/v1/cash/trans_orders filters: {field:"orderType", op:"EQUAL", value:"ORDER_TYPE_EXTERNAL_DIRECT_DEPOSIT"} 仅显示直充单 接口返回行数与 DB 一致 截图 [ ] 步骤 2
步骤2:列表透出 business_details businessDetails.businessReason 字段非空 与提交值一致 接口响应 截图 [ ] 步骤 3
步骤3:单条详情透出 business_details GET /api/v1/cash/trans_orders/{id} businessDetails.businessReason 与提交一致 接口响应 截图 [ ]

任务 T7:对账兼容性

入口:worker 日志 + DB

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:触发 reconcile_porter_balance 等待 worker 周期或手动触发 该端口的 drift 仍在阈值内(直充流水被 sub_act_type='外采帐户直充' 排除规则识别,不计入端口余额聚合) worker 日志 截图 [ ] 步骤 2
步骤2:ListPorters 接口的端口余额 POST /api/v1/advertiser/portersbalance 直充前后该端口 balance 值不变 接口响应 截图 [ ] 步骤 3
步骤3:PorterBalance 接口的端口余额 POST /api/v1/cash/porter_balanceporterAccounts[0].totalBalance 直充前后值不变(口径已与 ListPorters 对齐) 接口响应 截图 [ ]

任务 T8:SOP-605 端口退款回归(共享 trans_order_business_details)

入口:CRM → 外采 → 端口退款单 Tab

Action Steps Input / Payload 预期结果 验证方式 证据 状态 下游任务
步骤1:发起一笔端口退款 走 SOP-605 完整链路 审批通过、settle_porter_refund 落地 DB 查询 截图 [ ] 步骤 2
步骤2:业务详情字段 SELECT business_reason FROM trans_order_business_details WHERE trans_order_id=TO_ID 与提交事由一致(验证表共享未破坏既有路径) DB 查询 截图 [ ]

API 参考

相关接口

接口 方法 描述
/api/v1/cash/transfer POST 直充入口(mode=TRANSFER_MODE_DIRECT_DEPOSIT
/api/v1/cash/trans_orders POST 列订单(filters 含 order_type=ORDER_TYPE_EXTERNAL_DIRECT_DEPOSIT
/api/v1/cash/trans_orders/{id} GET 单条详情
/api/v1/cash/porter_balance POST Dialog 顶部「端口资金」展示
/api/v1/customers/{id}/advertisers GET 客户名下广告账户(前端按 provider=EXTERNAL 筛)
/api/v1/reviews/{flow_id} GET 审批详情
/api/v1/reviews/{flow_id}/actions POST approve / reject

直充不提供专属列表 / 详情 RPC——直接复用通用 ListTransOrders / GetTransOrder

测试数据示例

成功案例

请求参数:

{
  "customer_id": 1024,
  "to_account_id": "1100000007",
  "amount": 100,
  "mode": "TRANSFER_MODE_DIRECT_DEPOSIT",
  "direct_deposit_reason": "差错纠偏 e2e 测试",
  "memo": "e2e",
  "attachments": []
}

预期响应:

{
  "transOrder": {
    "id": 902,
    "status": "REVIEWING",
    "orderType": "ORDER_TYPE_EXTERNAL_DIRECT_DEPOSIT",
    "amount": 100,
    "isExternal": true,
    "porterId": 10000,
    "businessDetails": { "businessReason": "差错纠偏 e2e 测试" }
  },
  "review": { "id": 933, "flowId": "...", "status": "REVIEWING" }
}

失败案例

场景 错误提示 HTTP 状态码
未传 to_account_id to_account_id 不能为空 400
广告帐户不存在 广告帐户 X 不存在 404
广告帐户 provider 非 EXTERNAL 广告帐户 X 不是外采帐户(provider=...) 400
广告帐户缺端口归属 外采帐户 X 缺少端口归属 400
客户外采帐户行不存在 客户 N 的外采帐户行不存在(请先通过 SOP-601 建立外采帐户) 400
金额 ≤ 0 直充金额必须大于0 400
空 direct_deposit_reason direct_deposit_reason 不能为空 400
设了源端字段 from_account_id 直充分支不接受 from_account_id 400
设了源端字段 from_policy_labels 直充分支不接受 from_policy_labels 400
设了源端字段 use_credit 直充分支不接受 use_credit 400
设了源端字段 use_pending 直充分支不接受 use_pending 400
角色未授权 角色 X 无权执行外采帐户直充(需 CashService.TransferDirect 权限) 403
模板未配置 config.account_id / config.amount 异步路径置 trans_orders.status=FAILED 审批返回 200
审批通过但 accounts 行被删 trans_orders.status=FAILED + err_msg='外采帐户不存在' 审批返回 200

异常处理

异常场景 预期行为 处理方法
抽屉「充值」按钮 disabled 当前账户未绑定 customer 或没有有效政策标签 main.tsx 的 canOperate 守门;先走标签流程
端口资金卡显示「端口余额为 0 或暂无流水」 porter_balance 接口对 total_balance > 0 做了过滤,余额为 0 时返回空 不影响直充流程(直充不依赖端口余额)
审批详情卡片缺字段 仅显示 business_reason 直充不写 business_type / business_date / payee_*,属正常
详情 / 列表 businessDetails 为 null service 未 Preload GetTransOrder / ListTransOrders 必须 Preload("BusinessDetails")
结算静默失败 trans_orders 停在 REVIEWING err_msg;正常路径 completion actions 失败会写回 err_msg + status=FAILED
审批通过但 accounts.balance 未变 排查 executeSettleExternalDirectDeposit 是否走了 RowsAffected=0 分支 看 trans_orders.err_msg + zap 日志 method=SettleExternalDirectDeposit
调用接口 401 / 403 Casbin 拒绝 gateway/config/policy.csv 是否含 p,$角色,*,CashService.TransferDirect(或 TransferFull);StartAutoLoadPolicy 默认 3 分钟刷新
端口余额漂移告警 reconcile_porter_balance worker 误识别直充流水 worker 与 ListPorters 已排除 sub_act_type='外采帐户直充',若仍漂移检查 SQL 是否回滚到旧版

进度采集模板

任务ID 任务名称 执行人 开始时间 结束时间 状态 备注
T1 进入外采广告帐户抽屉
T2 填写并提交直充表单
T3 查看审批详情
T4 财务审批
T5 验证资金落账
T6 列表与详情字段验证
T7 对账兼容性
T8 SOP-605 端口退款回归

附录

相关配置文件

  • migrations/20260429_create_external_direct_deposit_review_template.sql — SOP-606 审批模板(事件 transOrder.external.directDeposit + settle_external_direct_deposit 动作 + 财务岗审批人)
  • migrations/20260429_rollback_external_direct_deposit_review_template.sql — SOP-606 回滚脚本

高危规则

  • review_templates.on_approved_actions[*].type='settle_external_direct_deposit' 是高危字段:任何手工修改 type 都会切换执行器:
  • 改为 update_status → 仅置 SUCCESS,不动账、不写流水(直接资损)
  • 改为 settle_external_payment / settle_porter_refund → 跑成完全不同的资金路径
  • description 字段与 type 不联动;改前必须对照 internal/review/executor/action_executor.go::Execute switch case 一一确认(参考 SOP-604/605 hotfix 事故先例)。
  • payload 字段映射不能改config.account_id=payload.AccountID / config.amount=payload.Amount 与 settle 函数中的 extractCustomerIDFromPayload / extractPorterIDFromConfig({"porter_id":"payload.PorterID"}) 强耦合;改字段会立即让结算 step 1 找不到 accounts 行,订单 FAILED。

相关文档

  • docs/SOP/sop-601-external-recharge.md — 外采帐户充值审批(经端口的"正路")
  • docs/SOP/sop-602-external-transfer.md — 外采端口入帐转账
  • docs/SOP/sop-604-external-payment.md — 外采付款审批(共用 trans_order_business_details 子表)
  • docs/SOP/sop-605-porter-refund.md — 端口退款审批(同样单步财务审批)
  • docs/SOP/sop-607-external-direct-refund.md — 外采帐户直退(SOP-606 的逆向)

e2e 脚本

  • e2e/external_direct_deposit/lib.sh + e2e/external_direct_deposit/run_s1_happy_path.sh(core 仓库):创建 → 审批 → 资金落账 → ext_corps.balance 不变验证 全链路

文档版本:v1.0.0 最后更新:2026-04-29 维护者:财务团队 / 运营团队 / QA