SOP-605:端口退款审批

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

文档元数据

  • 文档类型:SOP / Checklist
  • 适用场景:外采端口退款(端口充值的产品化逆向操作 + 差错冲正 + 外部退回款),完成后从端口余额扣减并写出账流水
  • 配置文件
  • 20260428_rename_business_details_table.sqlext_payment_detailstrans_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.jsonCashServiceReviewService 模块(特别是 POST /api/v1/cash/porter_refundPOST /api/v1/cash/trans_ordersGET /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].configporter_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_detailsbusiness_type / business_date / payee_account_type / payee_account_name 应为可空
Casbin 已放行 CashService.PorterRefund gateway/config/policy.csvp,$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::GetTransOrderListTransOrders 必须 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.PorterRefundStartAutoLoadPolicy 默认 3 分钟刷新

进度采集模板

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

附录

相关配置文件

  • migrations/20260428_rename_business_details_table.sql - 表演进:ext_payment_detailstrans_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