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.json 中 CashService 与 ReviewService 模块(特别是 POST /api/v1/cash/transfer、POST /api/v1/cash/trans_orders、GET /api/v1/cash/trans_orders/{id}、POST /api/v1/cash/porter_balance、GET /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.TransferDirect 或 CashService.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].config 含 account_id=payload.AccountID / amount=payload.Amount |
否则结算 action 会报字段缺失 |
| 抽屉里"充值"按钮可点击 |
当前 advertiser 已绑定 customerId 且 policyLabels 含 status='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=FAILED;accounts.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/porters 看 balance |
直充前后该端口 balance 值不变 |
接口响应 |
截图 |
[ ] |
步骤 3 |
| 步骤3:PorterBalance 接口的端口余额 |
POST /api/v1/cash/porter_balance 看 porterAccounts[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