SOP-605:端口退款审批
标准 SOP 测试文档;每个任务(Task ID)代表一个独立的测试步骤,便于人工/自动化测试按序号完成并记录进度。
文档元数据
- 文档类型:SOP / Checklist
- 适用场景:外采端口退款(端口充值的产品化逆向操作 + 差错冲正 + 外部退回款),完成后从端口余额扣减并写出账流水
- 配置文件:
20260428_rename_business_details_table.sql(ext_payment_details→trans_order_business_details表演进 + 字段中性化 + 4 列 NOT NULL 放宽)20260428_create_porter_refund_review_template.sql(SOP-605 审批模板)20260428_rollback_porter_refund_review_template.sql(SOP-605 回滚脚本)- 创建者:财务 / 运营 / QA
- 最近更新时间:2026-04-28
- 相关接口定义:参考
crm.swagger.json中CashService与ReviewService模块(特别是POST /api/v1/cash/porter_refund、POST /api/v1/cash/trans_orders、GET /api/v1/cash/trans_orders/{id}、GET /api/v1/reviews/{flow_id}、POST /api/v1/reviews/{flow_id}/actions)
工作流概述
流程图
graph LR
A[发起申请] --> B[财务审批]
B --> C[自动结算]
C --> D1[扣减端口余额]
C --> D2[写 transactions 出账流水]
C --> D3[写 porter_cashflows 资金流水]
C --> D4[订单状态 SUCCESS]
审批角色
| 步骤 | 审批人 | 说明 |
|---|---|---|
| 财务审批 | 往来会计 / 总账会计 / 税务会计 / 财务主管 / 财务总 | any 模式(单步审批,不经上级领导) |
与 SOP-604 外采付款不同:端口退款仅一步财务审批,没有上级领导审批。
前置条件
| 条件 | 检查方式 |
|---|---|
已使用有权限的用户登录(拥有 CashService.PorterRefund + CashService.ListTransOrders + CashService.GetTransOrder 权限) |
登录成功,外采菜单"端口退款单" Tab 可见 |
目标外采端口存在且 provider='EXTERNAL'、余额充足 |
SELECT id, balance, total_balance FROM ext_corps WHERE provider='EXTERNAL' |
| 已配置 SOP-605 审批模板 | SELECT id FROM review_templates WHERE sop_number='SOP-605' AND is_active=1 |
审批模板 on_approved_actions[0].config 含 porter_id=payload.PorterID / amount=payload.Amount |
否则 settle_porter_refund 会报 porter_id field is required |
已执行表演进 migration(trans_order_business_details 已存在并放宽 NOT NULL) |
DESC trans_order_business_details 中 business_type / business_date / payee_account_type / payee_account_name 应为可空 |
Casbin 已放行 CashService.PorterRefund |
gateway/config/policy.csv 含 p,$Basic,*,CashService.PorterRefund |
| 携带 origin 退款时:原充值订单状态=SUCCESS 且端口一致 | SELECT id, order_type, status, porter_id FROM trans_orders WHERE id=ORIGIN_ID |
任务矩阵
任务 T1:进入端口退款列表
入口:CRM 菜单 → 外采 → "端口退款单" Tab
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
步骤1:打开 /crm/external |
— | 展示"外采"页面,默认在"外采付款单" Tab | 页面标题正确 | 截图 | [ ] |
步骤 2 |
| 步骤2:切换到"端口退款单" Tab | 点击 Tab | 切换到端口退款单列表 | 表格加载、按钮变为"新建端口退款单" | 截图 | [ ] |
步骤 3 |
| 步骤3:点击行展开箭头 | 点击订单号前的 ▸ | 展开详情面板 | 面板显示"退款事由"等字段 | 截图 | [ ] |
任务 T2 |
任务 T2:填写并提交端口退款表单
入口:端口退款单列表页 → 新建端口退款单
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:选择端口 | porter_name="飞天经纬"(外采端口) |
端口字段显示已选端口 | 字段值正确 | 截图 | [ ] |
步骤 2 |
| 步骤2:填金额 | amount=100 |
金额显示 100 | 输入框显示 | 截图 | [ ] |
步骤 3 |
| 步骤3:填退款事由 | refund_reason="差错冲正 e2e 测试" |
事由显示输入内容 | 字段值正确 | 截图 | [ ] |
步骤 4 |
| 步骤4:(可选)填原充值订单 ID | origin_trans_order_id=685(一笔 status=SUCCESS 的端口充值单) |
字段值正确 | 后端会校验该单存在/类型/状态/端口一致 | 截图 | [ ] |
步骤 5 |
| 步骤5:(可选)填备注 / 上传附件 | 备注 e2e;附件 0~10 个 |
字段正确显示 | UI 显示 | 截图 | [ ] |
步骤 6 |
| 步骤6:提交 | 点击"提交审批" | trans_orders 新增一行 order_type=ORDER_TYPE_PORTER_REFUND, status=REVIEWING 并触发审批 |
SELECT order_type, status FROM trans_orders ORDER BY id DESC LIMIT 1 返回 ORDER_TYPE_PORTER_REFUND / REVIEWING |
截图 | [ ] |
任务 T3 |
| 步骤7:边界——空退款事由 | 留空提交 | HTTP 400,消息 refund_reason 不能为空 |
Toast 错误显示 | 截图 | [ ] |
— |
| 步骤8:边界——金额 ≤ 0 | amount=0 |
HTTP 400,消息 退款金额必须大于0 |
Toast 错误显示 | 截图 | [ ] |
— |
| 步骤9:边界——非外采端口 | 选 provider!=EXTERNAL 的端口 |
HTTP 400,消息 端口 X 不是外采端口 |
Toast 错误显示 | 截图 | [ ] |
— |
| 步骤10:边界——origin 类型不符 | 填一笔非端口充值的订单 ID | HTTP 400,消息含 不是端口充值订单 |
Toast 错误显示 | 截图 | [ ] |
— |
| 步骤11:边界——origin 端口不一致 | 填一笔他人端口的充值单 ID | HTTP 400,消息含 端口与本次退款不一致 |
Toast 错误显示 | 截图 | [ ] |
— |
任务 T3:查看审批详情
入口:审批中心 → 我发起的 / 工作流详情
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:打开审批详情 | 点击刚创建的审批 | 顶部显示业务详情卡片,标题为"端口退款详情"或共享卡片 | 卡片仅显示"退款事由(business_reason)",外采付款字段(业务类型/收款人)不显示 | 截图 | [ ] |
步骤 2 |
| 步骤2:核对扁平字段 | — | businessDetails.businessReason 与提交值一致 |
肉眼核对 | 截图 | [ ] |
任务 T4 |
任务 T4:财务审批(唯一步骤)
入口:审批中心 → 待我审批(财务 position 登录)
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:财务登录 | position ∈ {往来会计, 总账会计, 税务会计, 财务主管, 财务总} |
登录成功 | 页面加载 | 截图 | [ ] |
步骤 2 |
| 步骤2:查看待审批 | 筛选 SOP-605 | 列表出现该单 | 列表含 "端口退款审批" | 截图 | [ ] |
步骤 3 |
| 步骤3:通过审批 | 点击"通过" | review_instances.status=APPROVED,触发 settle_porter_refund |
DB 查询 + log | 截图 | [ ] |
任务 T5 |
| 步骤4:(独立场景)驳回 | 点击"驳回" | trans_orders.status=FAILED,端口余额与流水均无变化 |
DB 查询 | 截图 | [ ] |
— |
任务 T5:验证资金池核算
入口:MySQL 直查
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:端口余额扣减 | SELECT balance, total_balance FROM ext_corps WHERE id=PORTER_ID |
两个值都 -=amount(乐观锁 WHERE balance>=amount) |
数值比对 | 截图 | [ ] |
步骤 2 |
| 步骤2:transactions 新增 | SELECT type, sub_act_type, amount, status FROM transactions WHERE trans_order_id=TO_ID |
EXPEND / 端口退款 / amount / SUCCESS |
全字段核对 | 截图 | [ ] |
步骤 3 |
| 步骤3:porter_cashflows 新增 | SELECT flow_type, amount, discount, real_amount FROM porter_cashflows WHERE trans_order_id=TO_ID |
FLOW_TYPE_EXPEND / amount / 0 / amount |
全字段核对 | 截图 | [ ] |
步骤 4 |
| 步骤4:trans_orders 最终态 | SELECT status, err_msg FROM trans_orders WHERE id=TO_ID |
SUCCESS + err_msg 空 |
字段核对 | 截图 | [ ] |
步骤 5 |
| 步骤5:审批失败路径——余额不足 | 主动让 porter 余额 < amount,再发起一笔退款 | trans_orders.status=FAILED + err_msg 含 端口余额不足;端口余额 / 流水均无副作用 |
字段核对 | 截图 | [ ] |
步骤 6 |
| 步骤6:审批失败路径——origin 累计超额 | 同一原充值单连续两笔退款,累计金额 > 原充值 | 第二笔 trans_orders.status=FAILED + err_msg 含 超过原充值金额上限;ext_corps 不扣 |
字段核对 + 并发场景 | 截图 | [ ] |
— |
任务 T6:列表与详情字段验证
入口:CRM → 外采 → 端口退款单 Tab + GetTransOrder 接口
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:列表页过滤 | filters: {field:"orderType", op:"EQUAL", value:"ORDER_TYPE_PORTER_REFUND"} |
仅显示端口退款单 | POST /api/v1/cash/trans_orders 返回行数与 DB 一致 |
截图 | [ ] |
步骤 2 |
| 步骤2:列表透出 business_details | 列字段 businessDetails.businessReason |
列值与提交一致 | aggrid 列读出 | 截图 | [ ] |
步骤 3 |
| 步骤3:单条详情透出 business_details | GET /api/v1/cash/trans_orders/{id} |
响应 businessDetails.businessReason 与提交一致 |
接口响应核对 | 截图 | [ ] |
步骤 4 |
| 步骤4:金额展示方向 | 列表 / 详情 | 金额按 NEGATIVE_ORDER_TYPES 显示为负方向(红色 / 出账) | UI 渲染 | 截图 | [ ] |
— |
任务 T7:对账兼容性
入口:worker 日志 + DB
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:触发 reconcile_porter_balance | 等待 worker 周期或手动触发 | 该端口的 drift 在阈值内 | worker 日志 | 截图 | [ ] |
步骤 2 |
| 步骤2:transactions 流水识别 | 端口退款的出账行 type='EXPEND', sub_act_type='端口退款' 被对账口径识别 |
累计扣减与 ext_corps.balance 一致 | SQL 求和比对 | 截图 | [ ] |
— |
任务 T8:SOP-604 外采付款回归(schema 演进副作用)
入口:外采 → 外采付款单 Tab(依赖同一张 trans_order_business_details 子表)
| Action Steps | Input / Payload | 预期结果 | 验证方式 | 证据 | 状态 | 下游任务 |
|---|---|---|---|---|---|---|
| 步骤1:发起一笔外采付款 | 走 SOP-604 完整链路(含 leader + 财务两步) | 审批通过、settle_external_payment 落地 |
DB 查询 | 截图 | [ ] |
步骤 2 |
| 步骤2:业务详情字段 | SELECT business_type, business_reason, business_date, payee_* FROM trans_order_business_details WHERE trans_order_id=TO_ID |
全套字段值与提交一致(验证表演进未破坏既有字段) | DB 查询 | 截图 | [ ] |
步骤 3 |
| 步骤3:ListExternalTransOrders 字段一致 | 调用 POST /api/v1/cash/external_trans_orders |
返回 businessReason / businessDate / payee_* 与重命名前一致 |
接口响应核对 | 截图 | [ ] |
— |
API 参考
相关接口
| 接口 | 方法 | 描述 |
|---|---|---|
/api/v1/cash/porter_refund |
POST | 提交端口退款申请 |
/api/v1/cash/trans_orders |
POST | 列订单(filters 含 order_type=ORDER_TYPE_PORTER_REFUND,返回含 businessDetails) |
/api/v1/cash/trans_orders/{id} |
GET | 单条详情(返回含 businessDetails) |
/api/v1/reviews/{flow_id} |
GET | 审批详情(payload 含 businessDetails 扩展字段) |
/api/v1/reviews/{flow_id}/actions |
POST | approve / reject |
端口退款不提供专属列表 / 详情 RPC——直接复用通用
ListTransOrders/GetTransOrder,由 admin 在 GORM Preload 链中带出BusinessDetails。
测试数据示例
成功案例
请求参数:
{
"porter_name": "飞天经纬",
"amount": 100,
"refund_reason": "差错冲正 e2e 测试",
"origin_trans_order_id": 685,
"attachments": [],
"memo": "e2e"
}
origin_trans_order_id可选;不填即为独立退款(差错冲正之外的场景)。
预期响应:
{
"transOrder": {
"id": 901,
"status": "REVIEWING",
"orderType": "ORDER_TYPE_PORTER_REFUND",
"amount": 100,
"orderId": 685,
"businessDetails": { "businessReason": "差错冲正 e2e 测试" }
},
"review": { "id": 932, "flowId": "...", "status": "REVIEWING" }
}
失败案例
| 场景 | 错误提示 | HTTP 状态码 |
|---|---|---|
| 未填端口 | 必须提供 porter_id 或 porter_name |
400 |
| 端口非外采 | 端口 X 不是外采端口 |
400 |
| 端口不存在 | 端口ID/名称 X 不存在... |
404 |
| 金额 ≤ 0 | 退款金额必须大于0 |
400 |
| 空 refund_reason | refund_reason 不能为空 |
400 |
| origin 不存在 | 原端口充值订单 X 不存在 |
400 |
| origin 类型不符 | 原订单 X 不是端口充值订单(OrderType=...) |
400 |
| origin 状态非 SUCCESS | 原端口充值订单 X 状态不是 SUCCESS(当前=...) |
400 |
| origin 端口不一致 | 原端口充值订单 X 的端口与本次退款不一致 |
400 |
| 端口余额不足(结算瞬间) | trans_orders.status=FAILED + err_msg='端口余额不足'(同步审批返回成功,但订单 FAILED) |
审批返回 200 |
| origin 累计退款超额 | trans_orders.status=FAILED + err_msg 含 超过原充值金额上限 |
审批返回 200 |
模板未配置 config.porter_id / config.amount |
porter_id field is required / amount field is required |
同上 |
异常处理
| 异常场景 | 预期行为 | 处理方法 |
|---|---|---|
| 列表无数据 | 列表为空 | 检查 trans_orders.order_type='ORDER_TYPE_PORTER_REFUND' 是否有数据;前端 filter 是否传对 |
| 审批详情卡片缺字段 | 仅显示退款事由 | 端口退款不写 business_type / business_date / payee_*,只写 business_reason,属正常 |
详情 / 列表 businessDetails 为 null |
service 未 Preload | services/cash/service.go::GetTransOrder 与 ListTransOrders 必须 Preload("BusinessDetails");AttachBusinessDetailsToProto 必须在所有序列化点调用 |
| 结算静默失败 | trans_orders 停在 REVIEWING | 查 err_msg;正常路径 completion actions 失败会写回 err_msg + status=FAILED |
| 审批通过但端口余额未变 | 排查 executeSettlePorterRefund 是否走了乐观锁失败分支(RowsAffected=0) |
看 trans_orders.err_msg + zap 日志 method=SettlePorterRefund |
| 调用接口 401 | Casbin 拒绝 | gateway/config/policy.csv 是否含 p,$Basic,*,CashService.PorterRefund;StartAutoLoadPolicy 默认 3 分钟刷新 |
进度采集模板
| 任务ID | 任务名称 | 执行人 | 开始时间 | 结束时间 | 状态 | 备注 |
|---|---|---|---|---|---|---|
| T1 | 进入端口退款列表 | ⬜ | ||||
| T2 | 填写并提交端口退款表单 | ⬜ | ||||
| T3 | 查看审批详情 | ⬜ | ||||
| T4 | 财务审批 | ⬜ | ||||
| T5 | 验证资金池核算 | ⬜ | ||||
| T6 | 列表与详情字段验证 | ⬜ | ||||
| T7 | 对账兼容性 | ⬜ | ||||
| T8 | SOP-604 外采付款回归 | ⬜ |
附录
相关配置文件
migrations/20260428_rename_business_details_table.sql- 表演进:ext_payment_details→trans_order_business_details+ 字段中性化 + 4 列 NOT NULL 放宽(外采付款与端口退款共用同一张子表)migrations/20260428_create_porter_refund_review_template.sql- SOP-605 审批模板(事件transOrder.porter.refund+settle_porter_refund动作 + 财务岗审批人)migrations/20260428_rollback_porter_refund_review_template.sql- SOP-605 回滚脚本
高危规则
review_templates.on_approved_actions[*].type='settle_porter_refund'是高危字段:任何手工修改 type 都会切换执行器(如改成update_status/update_porter_balance/settle_external_payment),description 不联动;改前必须对照internal/review/executor/action_executor.go::Execute确认(参考 SOP-604 hotfix 事故先例)。
与 SOP-604 的差异速查
| 维度 | SOP-604 外采付款 | SOP-605 端口退款 |
|---|---|---|
| 资金方向 | 出账(端口余额 -) | 出账(端口余额 -) |
| 业务详情字段 | business_type + business_reason + business_date + payee_* 全套 | 仅 business_reason |
| 审批步骤 | 上级领导 → 财务(两步) | 财务(单步) |
| 结算 action | settle_external_payment | settle_porter_refund |
| transactions.sub_act_type | 外采付款 | 端口退款 |
| origin 关联校验 | 无 | 可选 origin(限端口充值 SUCCESS 单),结算瞬间断言累计 ≤ 原充值 |
| 专属列表/详情 RPC | 有(ListExternalTransOrders 扁平视图) | 无(复用通用 ListTransOrders / GetTransOrder) |
相关文档
docs/SOP/sop-603-porter-recharge.md- 端口充值审批(SOP-605 的逆向操作)docs/SOP/sop-604-external-payment.md- 外采付款审批(共用trans_order_business_details子表)docs/SOP/sop-601-external-recharge.md- 外采账户充值审批
e2e 脚本
e2e/porter_refund/lib.sh+e2e/porter_refund/run_s1_happy_path.sh(core 仓库):创建 → 审批 → 资金池核算 → ListTransOrders + GetTransOrder business_details 透出 全链路
文档版本:v1.0.0 最后更新:2026-04-28 维护者:财务团队 / 运营团队 / QA