| | |
| | | # 自动提现沙盒与受控真实金额验收手册 |
| | | # 支付与自动提现验收手册(存量商户 + 真实小额) |
| | | |
| | | 本文件是工单 06「完整链路验收与受控金额验证」的执行手册。自动化部分已经落地并可重复运行 |
| | | (`AutoWithdrawalEndToEndAcceptanceTests`);需要斗拱沙盒账号、公网回调和真实到账的部分只能由人执行, |
| | | 按本手册逐条勾选并回填结果。 |
| | | 本手册是工单 06 的执行版本,取代原先的「沙盒全链路」写法。原因:**斗拱的本地沙箱不模拟商户进件, |
| | | 也不模拟取现**(官方原文:本地沙箱「不验证 … 清结算、发票、取现和生产资金结果」),官方 API 文档里也没有 |
| | | 文档化的测试环境。因此验收路径改为:**把斗拱上已有的真实企业商户号接入系统(跳过进件), |
| | | 再用真实极小金额验证「下单 → 支付 → 核销 → 日汇总 → 提现 → 到账」全链路。** |
| | | |
| | | ## 1. 开关现状与前置约束 |
| | | 自动化部分仍然有效并可重复运行(`AutoWithdrawalEndToEndAcceptanceTests` 等)。本手册只负责自动化覆盖不到、 |
| | | 必须人工执行的部分。 |
| | | |
| | | 先看清楚现在的默认值,不要照抄一份「以为默认是关」的心理模型: |
| | | ## 0. 本次验收的边界(先读这一段) |
| | | |
| | | | 开关 | 当前默认值 | 作用 | |
| | | **能验**:收款能力判定、下单时固定收款子商户、微信支付拉起、回调落账或查询补偿、商户核销、 |
| | | 日汇总生成提现记录、提交取现、结果收敛、商户端与后台的状态展示。 |
| | | |
| | | **不能验**(跳过进件换来的,必须如实记账): |
| | | |
| | | | 跳过的能力 | 为什么跳过 | 遗留风险 | |
| | | | --- | --- | --- | |
| | | | `app.payment.huifu.enabled` | `application.yml` 里写死 `true` | 斗拱通道总开关;改为 `false` 时支付、退款、提现网关直接抛「Huifu payment is disabled」 | |
| | | | `HUIFU_MERCHANT_CAPABILITY_REQUIRED` | `true` | 是否要求已确认的能力快照才能收款;置 `false` 会退化成只校验本地进件与银行卡 | |
| | | | `AUTO_WITHDRAWAL_ENABLED` | **`false`** | 自动提现汇总任务开关;只有沙盒全链路通过后才显式打开 | |
| | | | 平台进件(企业 / 个人) | 官方要求进件材料必须真实合法,不能编造;斗拱没有进件沙箱 | 进件入参缺 `beneficiary_info`、`settle_cycle`、`cash_fee_party` 等必填项,真实接商户前必须补齐 | |
| | | | 结算卡绑卡 / 换卡 | 被 `requireApproved`(要求进件 approved)锁死,跳过进件后无入口 | 绑卡回调与 `token_no` 落库路径未被真实覆盖 | |
| | | | 微信商户配置、业务开通 | 正常由进件流程串起来 | 依赖导入的存量商户在斗拱侧「已经配好」 | |
| | | | 退汇(`re_exchange=Y`)与回账(`acct_status=B`) | 代码未实现 | 到账后被退汇不会回到「提现失败」,金额也不会再次顺延 | |
| | | |
| | | 也就是说**默认状态就会调用真实通道**(`enabled: true` 且 `environment: prod`),沙盒阶段必须显式把 |
| | | `app.payment.huifu.enabled` 与环境相关配置指向沙盒;只有自动提现任务是默认关闭的。 |
| | | ## 1. 前置条件 |
| | | |
| | | 验收顺序必须是:**先沙盒(测试环境)→ 再真实极小金额**。真实阶段另有四道硬约束,缺一不可: |
| | | ### 1.1 环境 |
| | | |
| | | 1. **商户白名单**:只允许指定测试商户的 `huifu_id`。当前**没有**任何脚本或配置实现这道过滤,它是执行 |
| | | 纪律:先按 `merchant_id` 确认进件与结算配置,再手工触发汇总,不要在生产上打开定时任务。 |
| | | 2. **银行卡白名单**:只允许指定的一张结算卡(按 `merchant_bank_card.id` + `token_no` 双确认)。 |
| | | 3. **账号约束**:只允许指定运营账号在指定环境下执行;密钥只从部署密钥库注入,不落盘、不进日志。 |
| | | 4. **金额上限**:单笔与单日都是固定极小金额,见第 5 节;超过上限的取现请求一律不发起。 |
| | | | 项 | 当前值 | |
| | | | --- | --- | |
| | | | 服务端 | `http://192.168.110.15:8080`(本机 WLAN;真机访问需放行 Windows 防火墙 8080) | |
| | | | 数据库 | `127.0.0.1:8134/talent_card`(WSL Docker 里的 MySQL 8,账号 `root` / `123456`) | |
| | | | Redis | `127.0.0.1:6368`,密码 `123456` | |
| | | | 管理后台 | `npm run dev`(5273,Vite 代理 `/api` 到 8080),账号 **`admin` / `123456`** | |
| | | | 人才端小程序 | `rencaika/`,mp-weixin AppID `wxcfee477acb54d179` | |
| | | | 商户端小程序 | `rencaika_merchant/`,AppID `wx550e520ad19458ea` | |
| | | | 两个 C 端接口地址 | 各自 `config/api.uts` 的 `developmentBaseUrl`,已改为 `http://192.168.110.15:8080` | |
| | | |
| | | > 安全前置:`application.yml` 里当前**硬编码了真实的 `private-key`、`platform-public-key`、系统号/商户号 |
| | | > 与微信 AppSecret**。这套凭据已经进了版本库,属于必须轮换的暴露面;真实金额验收之前先轮换,并把新值 |
| | | > 放进部署密钥库(`HUIFU_NOTIFY_URL` 等少数字段已经支持环境变量覆盖,密钥字段目前只读 yml)。 |
| | | > **主库不跑 Flyway**(`spring.flyway.enabled: false`),`ddl-auto` 只是 `validate`。 |
| | | > 库结构必须是新的:`merchant_withdrawal` 必须含 `withdrawal_date` 与 `store_key`。 |
| | | > 结构过旧时用 §1.2 的克隆办法同步,否则日汇总会以 `Unknown column 'withdrawal_date'` 失败。 |
| | | |
| | | ## 2. 沙盒环境配置 |
| | | ### 1.2 让开发库结构保持最新 |
| | | |
| | | `application.yml` 里只有一部分字段支持环境变量覆盖,其余是硬编码值。沙盒阶段要改的是**硬编码的那部分**: |
| | | |
| | | | 配置项 | 环境变量 | 现状 | |
| | | | --- | --- | --- | |
| | | | `enabled` | — | 硬编码 `true`,沙盒/生产都走真实通道 | |
| | | | `environment` | — | 硬编码 `prod`;沙盒要改成 `test` | |
| | | | `system-id` / `product-id` / `merchant-id` | — | 硬编码,沙盒需替换为沙盒分配的号 | |
| | | | `private-key` / `platform-public-key` | — | 硬编码在 yml,见上一节的安全前置 | |
| | | | `withdrawal-product-id` | `HUIFU_WITHDRAWAL_PRODUCT_ID` | 有默认值 `KAZX`,沙盒按取现产品覆盖 | |
| | | | `withdrawal-into-account-date-type` | — | 只由 `HuifuPaymentProperties` 的 Java 默认值 `DM` 决定 | |
| | | | 六个 `notify-url` | `HUIFU_*_NOTIFY_URL` | 有默认值(内网 IP),沙盒必须覆盖成公网 HTTPS | |
| | | | `app.merchant.auto-withdrawal.*` | `AUTO_WITHDRAWAL_*` | 全部支持环境变量覆盖 | |
| | | |
| | | ```text |
| | | HUIFU_NOTIFY_URL=https://<公网域名>/api/payments/huifu/notify |
| | | HUIFU_WITHDRAWAL_NOTIFY_URL=https://<公网域名>/api/payments/huifu/withdrawal-notify |
| | | HUIFU_MERCHANT_ONBOARDING_NOTIFY_URL=https://<公网域名>/api/payments/huifu/merchant-onboarding-notify |
| | | HUIFU_MERCHANT_WECHAT_CONFIG_NOTIFY_URL=https://<公网域名>/api/payments/huifu/merchant-wechat-config-notify |
| | | HUIFU_MERCHANT_BUSINESS_OPEN_NOTIFY_URL=https://<公网域名>/api/payments/huifu/merchant-business-open-notify |
| | | HUIFU_MERCHANT_BUSINESS_NOTIFY_URL=https://<公网域名>/api/payments/huifu/merchant-business-notify |
| | | HUIFU_WITHDRAWAL_PRODUCT_ID=... |
| | | AUTO_WITHDRAWAL_ENABLED=true |
| | | AUTO_WITHDRAWAL_MIN_AMOUNT_CENTS=1000 |
| | | AUTO_WITHDRAWAL_QUERY_AFTER_MINUTES=5 |
| | | ``` |
| | | |
| | | 六个回调地址都必须是公网可达 HTTPS,并在斗拱控制台登记同一份地址;网关/WAF 不得改写 |
| | | `resp_data`/`sign`。`AUTO_WITHDRAWAL_MIN_AMOUNT_CENTS` 填的是**分**,调用斗拱时转成两位小数的元。 |
| | | |
| | | ## 3. 复现「凌晨汇总」而不等一天 |
| | | |
| | | 日汇总按北京时间的前一自然日执行。手工验收有两条等价路径: |
| | | |
| | | - 直接触发任务(无需等待):`AutoWithdrawalAggregationService.run(businessDate)` 支持指定业务日, |
| | | 补跑脚本或后台一次性检查都可以用它。 |
| | | - 用定时任务:把 `AUTO_WITHDRAWAL_CRON` 临时改成 `0 */5 * * * *`(每 5 分钟),任务只会汇总 |
| | | 「核销时间在业务日窗口内」的订单,重复执行不会重复出账。 |
| | | |
| | | 窗口规则:`核销时间 < 业务日 24:00` 且 `>= 业务日 - 30 天`。超窗口的订单留给人工。 |
| | | |
| | | ## 4. 沙盒验收清单 |
| | | |
| | | 每条都要记录「实际观察到的值」,不要只打勾。SQL 都按本次验收创建的单号过滤。 |
| | | |
| | | ### 4.1 主链路(进件 → 到账) |
| | | |
| | | 1. **进件**:商户端提交进件(个人/企业),观察 `merchant_onboarding_application`: |
| | | `status='pending'`,`provider_huifu_id` 非空,`initial_bank_card_id` 指向同一笔银行卡。 |
| | | - 企业进件额外确认 `business_license_url`、`legal_id_front_url/back_url`、`registered_capital` 已落库。 |
| | | 2. **绑卡与 token**:`merchant_bank_card.status='approved'` 且 `provider_token_no` 非空。 |
| | | 若通道只回 `apply_no` 未回 `token_no`,卡片保持 `pending`,等业务开通回调补齐。 |
| | | 3. **进件通过**:微信商户配置与商户业务开通都变为 `approved` 后,进件才变 `approved`。 |
| | | 确认此时 `capability_verified_at` **已被填充**(`merchant_stat`/`auto_settle_status`/`cash_switch_status` |
| | | 三个快照字段非空)。若为空,说明能力快照刷新失败,先查服务日志里的 |
| | | `Merchant capability refresh failed`,此时商户无法收款。 |
| | | 4. **能力就绪**:`GET /api/admin/management/merchant-settlement/onboardings/{id}/payment-capability` |
| | | 返回 `merchantStatus=NORMAL`、`autoSettleDisabled=true`、`cashAvailable=true`。 |
| | | 5. **下单**:人才端下待支付订单,确认 `benefit_order.collect_onboarding_id / collect_store_id / |
| | | collect_huifu_id` 与本次进件一致;商品零元单不绑定收款方。 |
| | | 6. **支付**:发起支付,确认请求里的 `huifu_id` 是订单上的 `collect_huifu_id`(不是平台商户号), |
| | | 客户端能拉起微信支付。 |
| | | 7. **回调**:在斗拱沙盒完成支付。**重点确认沙盒回调真的下发 `huifu_id` 和 `trans_amt`**—— |
| | | 缺任何一个,订单都会保持未支付并落 `payment_reconciliation_issue`,需要靠补查或人工处理。 |
| | | 成功后:`payment_transaction.status='SUCCEEDED'`、`payment_callback_event.signature_valid=1`、 |
| | | `benefit_order.order_status=1`、`verify_status` 仍为 0(核销是商户的动作)。 |
| | | 8. **核销**:商户端核销,确认 `verify_record.status=1`、`benefit_order.verify_status=1`, |
| | | 且同一订单不可能有第二条有效核销记录(`uk_verify_record_active_order`)。 |
| | | 9. **次日汇总**:跑一次日汇总,确认 `merchant_withdrawal.status='pending'`、`amount_cents` 等于 |
| | | 来源订单 `pay_amount` 之和、`withdrawal_date` 是业务日、`merchant_withdrawal_order` 有对应关联, |
| | | 且订单 `withdrawal_status=1`。 |
| | | 10. **提交取现**:任务提交后确认本地状态 `processing`,并记下 `withdrawal_no`(= 发给斗拱的 |
| | | `req_seq_id`)、`provider_request_date`、`provider_huifu_id`。 |
| | | - 幂等:重复跑任务,斗拱侧不能出现第二笔取现;若通道回 `20000000 重复交易`,本地**必须保持 |
| | | 处理中**,绝不能判失败。 |
| | | 11. **结果收敛**:等异步通知 `msg_type=01`(通道)和 `msg_type=02`(账务)都到达;或者等 |
| | | `AUTO_WITHDRAWAL_QUERY_AFTER_MINUTES` 之后由查询收敛。终态 `paid` 时 |
| | | `provider_trade_no` 非空、`paid_at` 非空。 |
| | | 12. **商户端展示**:`GET /api/merchant/withdrawals` 只返回 `processing`/`paid`/`failed` 三种状态, |
| | | 金额与银行卡掩码正确;重复通知不改写终态。 |
| | | |
| | | ### 4.2 各环节独立异常 |
| | | |
| | | 「自动化」一列说明该行是否已被 `AutoWithdrawalEndToEndAcceptanceTests` 或既有测试类断言; |
| | | 只有沙盒/人工才能构造的行必须实际执行并记录观察值。 |
| | | |
| | | | 环节 | 构造方式 | 期望 | 自动化 | |
| | | | --- | --- | --- | --- | |
| | | | 支付 | 重复发送同一成功回调 | 状态与金额不变,只更新回调审计时间 | 是(`PaymentSettlementTests`) | |
| | | | 支付 | 回调金额改一位小数 | 订单保持未支付,落 `PAYMENT_AMOUNT_MISMATCH` | 是(验收类 + `PaymentSettlementTests`) | |
| | | | 支付 | 回调用别的 `huifu_id` | 订单保持未支付,落 `PAYMENT_MERCHANT_MISMATCH` | 是(`PaymentSettlementTests`) | |
| | | | 支付 | 成功回调但不带 `huifu_id` | 订单保持未支付,落 `PAYMENT_MERCHANT_UNVERIFIED` | 是(`PaymentSettlementTests`) | |
| | | | 支付 | 回调被延迟/拦截,超过阈值后补查 | 补查成功即落账;仍未终态则落 `PAYMENT_PENDING_TIMEOUT` | 是(`PaymentSettlementTests`) | |
| | | | 支付 | 回调签名错误 | 直接拒绝,不写审计行、不落账,不暴露报文与密钥 | 是(验收类;真实验签由 `HuifuSignatureServiceTests` 覆盖) | |
| | | | 核销 | 未支付订单核销 | 拒绝 | 是(验收类) | |
| | | | 核销 | 同幂等键重复核销 | 返回首次结果,只有一条有效核销记录 | 是(验收类) | |
| | | | 核销 | 换幂等键重复核销 | 拒绝(订单已核销) | 是(验收类) | |
| | | | 退款 | 已支付未核销订单退款 | 受理后 `order_refund.refund_status=2`、订单 `refund_status=2` | 是(验收类) | |
| | | | 退款 | 退款在途时核销 / 汇总 | 都被拒;该订单当天不进提现记录 | 是(验收类) | |
| | | | 退款 | 通道宣告退款失败后核销 | 订单回到可核销状态 | 是(`OrderFundBoundaryTests`) | |
| | | | 退款 | 退款成功后补发失败通知 | 不重新打开已退款订单 | 是(`OrderFundBoundaryTests`) | |
| | | | 提现 | 提交超时/读不出结果 | 保持 `processing`,用同一 `req_seq_id` 补查,**不新建记录** | 是(`AutoWithdrawalResultProcessingTests`) | |
| | | | 提现 | 查询回 `23000001 交易不存在` | 超过等待期后按失败顺延 | 是(`AutoWithdrawalResultProcessingTests`) | |
| | | | 提现 | 提交回 `22000004 暂未开通取现权限` | 失败并保存原因,来源订单解锁 | 是(验收类) | |
| | | | 提现 | 回 `23000003 取现金额不足以支付内扣手续费` | 失败顺延;据此校准 `AUTO_WITHDRAWAL_FEE_BUFFER_CENTS` | **否:只能沙盒构造**(本地把它当普通失败处理,映射逻辑已被 `22000004` 覆盖) | |
| | | | 提现 | 失败后次日重新汇总 | 同一订单连同新订单合并成**一条**新记录 | 是(验收类) | |
| | | | 提现 | 同一订单累计 3 次失败 | 不再自动解锁,金额留在失败记录,后台「提现失败待处理」可见 | 是(`AutoWithdrawalResultProcessingTests`) | |
| | | | 提现 | 后台尝试改状态 / 商户尝试发起提现 | 路由不存在,请求被拒 | 是(验收类查路由表;真实 HTTP 拒绝需人工确认) | |
| | | | 提现 | 重复通知 / 迟到的相反结果 | 终态不变,矛盾结果只写 WARN 日志供人工核对 | 是(验收类 + `AutoWithdrawalResultProcessingTests`) | |
| | | |
| | | ### 4.3 覆盖这些场景的自动化用例 |
| | | 离线环境没有 Flyway 的 jar,`mvnw spring-boot:run` 也用不了(插件依赖缺失),所以用「把测试库结构克隆到开发库」的办法: |
| | | |
| | | ```powershell |
| | | # 先把测试库跑到最新结构(顺便验证代码) |
| | | cd talent-card-server |
| | | .\scripts\run-server-tests.ps1 -Test AutoWithdrawalEndToEndAcceptanceTests |
| | | .\scripts\run-server-tests.ps1 |
| | | |
| | | # 再把它的表结构克隆到开发库 |
| | | $jar='C:\Users\15708\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar' |
| | | java -cp $jar ..\.scratch\automatic-withdrawal\tools\CloneSchema.java talent_card_test talent_card |
| | | ``` |
| | | |
| | | `AutoWithdrawalEndToEndAcceptanceTests` 用真实服务(`submitOnboarding` → 业务开通回调 → |
| | | 能力快照 → 下单 → 支付 → 验签回调 → 核销 → 日汇总 → 提交取现 → 通知/查询)跑完整条链路, |
| | | 所有斗拱网关都用 `@MockitoBean` 替换,零出网调用。上表里自动化已覆盖:无效验签回调被直接拒绝、 |
| | | 金额被改动的回调不履约并落 `PAYMENT_AMOUNT_MISMATCH`、未支付订单不能核销也不能汇总、 |
| | | `22000004` 按失败处理并展示原因、`23000001` 按失败顺延、处理中不被过早查询、终态不被改写。 |
| | | ### 1.3 造测试数据 |
| | | |
| | | 自动化**没有**覆盖、必须靠沙盒或人工确认的:通道是否真的下发 `huifu_id`/`trans_amt`/`fee_amt`/ |
| | | `acct_status`、`23000003`(内扣手续费吃掉本金)的真实触发与所需缓冲、各错误码的真实取值、 |
| | | `re_exchange=Y` 与 `acct_status=B` 的当前行为。逐环节的其余边界由 |
| | | `PaymentSettlementTests`、`OrderFundBoundaryTests`、`AutoWithdrawalAggregationTests`、 |
| | | `AutoWithdrawalResultProcessingTests`、`MerchantWithdrawalVisibilityTests` 覆盖。 |
| | | ```powershell |
| | | $env:MYSQL_URL='jdbc:mysql://127.0.0.1:8134/talent_card?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false' |
| | | $env:SQL_HUIFU_ID='<斗拱上的真实子商户号>' |
| | | $env:SQL_TOKEN_NO='<结算卡 token,拿不到可留空>' |
| | | $env:SQL_TALENT_MOBILE='<你的微信手机号>' |
| | | $jar='C:\Users\15708\.m2\repository\mysql\mysql-connector-java\8.0.23\mysql-connector-java-8.0.23.jar' |
| | | java -cp $jar .scratch\automatic-withdrawal\tools\SeedMerchant.java |
| | | |
| | | ## 5. 受控真实极小金额到账验证 |
| | | # 结构是从测试库克隆来的,迁移里的种子数据不会跟过来,所以管理员账号要单独补 |
| | | java -cp $jar .scratch\automatic-withdrawal\tools\AdminSeed.java admin 123456 |
| | | ``` |
| | | |
| | | 沙盒全链路通过后才执行本节,并且必须先登记再执行。 |
| | | `token_no` 留空时,卡片先没有 token;服务端在能力判定时**会自己去斗拱查** |
| | | (回执里 `qry_cash_card_info_list[].token_no` 就是它),查回来才写入。**提现提交必须要有这个值**。 |
| | | |
| | | **登记(执行前填好,缺一项不执行)** |
| | | 脚本幂等,可反复执行。它会打印 `merchantId / storeId / bankCardId / productId / talentId`, |
| | | 后面所有 SQL 都按它们过滤。它同时补上:进件(`approved` + 微信配置/业务开通 `approved` + 能力快照已确认)、 |
| | | 结算卡(`approved`,带 token)、到店消费规则、0.01 元的已审核商品、人才用户与有效个人卡、 |
| | | 商户端登录账号(默认 `verify_merchant` / `123456`)。 |
| | | |
| | | ### 1.4 启动参数 |
| | | |
| | | 在 IntelliJ 的 `TalentCardServerApplication` 运行配置里加 Program arguments: |
| | | |
| | | ``` |
| | | --app.merchant.auto-withdrawal.enabled=false |
| | | --app.merchant.auto-withdrawal.query-after-minutes=1 |
| | | ``` |
| | | |
| | | - `enabled=false`:验证期不跑凌晨自动汇总,只用手动补跑,避免它扫全库商户。 |
| | | - **`result-check-enabled` 保持默认 true**:结果收敛(补提交 + 查询)不受 `enabled` 影响;关掉 `enabled` |
| | | 之后已生成的提现记录仍会被提交并收敛。 |
| | | - 不需要打开 `app.payment.huifu.reconciliation.enabled`:回调收不到时用 §3.3 的后台接口手动补偿。 |
| | | |
| | | ### 1.5 斗拱侧必须先配好的四件事 |
| | | |
| | | 1. **结算配置**:自动结算必须**关闭**(能力查询里 `settle_status = 0`)。代码没有下发结算配置的能力,只能在控台关。 |
| | | **为什么必须关**:自动结算会把子商户账户的**全部余额**按日结到银行卡,包括用户已支付但**尚未核销**的订单金额—— |
| | | 那部分钱是要留作退款的。我们的主动取现只提**已核销**订单的金额,两者语义完全不同。 |
| | | 2. **取现权限已开通**(`switch_state = 1`)。只能在斗拱运营控台开。 |
| | | 3. **微信配置已关联人才端小程序** `wxcfee477acb54d179`。 |
| | | 4. **账户里有余额**:全新商户号余额为 0,必须先有一笔真实付款进账(§3 就是这笔)。 |
| | | |
| | | ## 2. 收款能力核对(零成本,先做) |
| | | |
| | | ``` |
| | | GET /api/admin/management/merchant-settlement/onboardings/{onboardingId}/payment-capability |
| | | ``` |
| | | |
| | | 期望:`merchantStatus=NORMAL`、`autoSettleDisabled=true`、`cashAvailable=true`。 |
| | | 任何一项不满足都不要往下走。 |
| | | |
| | | 再确认结算卡有 token(没有 token 提现请求根本提交不出去): |
| | | |
| | | ```sql |
| | | select id, status, provider_token_no from merchant_bank_card where merchant_id = {merchantId}; |
| | | ``` |
| | | |
| | | 为空时先调一次 `/payment-capability/refresh`,由服务端去斗拱查询后再看。 |
| | | |
| | | ## 3. 主链路(每笔 1 分钱) |
| | | |
| | | 金额取 1 分:**没有最低提现金额,1 分钱也会生成提现记录**。每步都记录「实际观察到的值」。 |
| | | |
| | | ### 3.1 商户端发单(商户视角) |
| | | |
| | | 1. 商户端小程序用种子脚本打印的账号登录(默认 `verify_merchant` / `123456`)。 |
| | | 2. 首页「扫码核销」扫人才端出示的**人才动态码**(人才端「我的卡」→ 亮码,**60 秒有效**)。 |
| | | 3. 扫到 `TALENT_CODE` 后按提示录入消费金额并「发送订单」。 |
| | | |
| | | ```sql |
| | | select order_no, merchant_id, store_id, pay_amount, order_status, verify_status |
| | | from benefit_order where merchant_id = {merchantId} order by id desc limit 3; |
| | | ``` |
| | | |
| | | 期望:`order_status=0`、`verify_status=0`,**`store_id` 等于进件门店**(商户代客下单这条路本来就写门店)。 |
| | | |
| | | > 注意:商户扫码**不是核销**。人才码只能用来「录金额发订单」,核销要等用户付款后再扫一次订单核销码。 |
| | | |
| | | ### 3.2 人才端付款 |
| | | |
| | | 1. 人才端小程序用**你自己的微信**登录(服务端按手机号匹配种子里的 `talent_user`)。 |
| | | 2. 「使用记录 → 待支付」打开订单,点支付,微信拉起付款 1 分钱。 |
| | | |
| | | ```sql |
| | | select payment_no, status, amount, provider_transaction_no, paid_at from payment_transaction order by id desc limit 3; |
| | | select provider_request_no, signature_valid, process_status, received_at from payment_callback_event order by id desc limit 3; |
| | | select order_no, order_status, verify_status from benefit_order where order_no = '{orderNo}'; |
| | | ``` |
| | | |
| | | 期望:`SUCCEEDED`、`order_status=1`、`verify_status=0`。 |
| | | |
| | | ### 3.3 回调没到就手动补偿(本地几乎一定需要) |
| | | |
| | | 斗拱登记的回调地址是给公网实例的,本机 `192.168.110.15` 收不到。后台已有补偿接口,它会**主动向斗拱查单** |
| | | 并把结果落账(与回调走同一套结算逻辑): |
| | | |
| | | ``` |
| | | POST /api/admin/management/payment-reconciliation/run?pendingAfterMinutes=1 |
| | | ``` |
| | | |
| | | > **时序**:该接口只查「创建时间早于 N 分钟」的交易,付款后要**等够 1 分钟**再调,否则查不到; |
| | | > 想立刻验证就把参数改成 `0`。 |
| | | |
| | | 等 1 分钟后调用,再查 §3.2 的三条 SQL。疑问记录在 `payment_reconciliation_issue`。 |
| | | |
| | | > 这一条替代了「公网可达回调」。代价:**回调路径本身未被真实覆盖**,必须在结论里写明。 |
| | | |
| | | ### 3.4 商户端核销 |
| | | |
| | | 1. 人才端「使用记录 → 待使用」出示订单核销码(**60 秒有效**,自动刷新)。 |
| | | 2. 商户端「扫码核销」再扫一次,确认核销。 |
| | | |
| | | ```sql |
| | | select id, order_id, status, verify_time from verify_record |
| | | where order_id = (select id from benefit_order where order_no = '{orderNo}'); |
| | | select order_no, verify_status, withdrawal_status from benefit_order where order_no = '{orderNo}'; |
| | | ``` |
| | | |
| | | 期望:`verify_record.status=1`、`verify_status=1`、`withdrawal_status=0`。 |
| | | |
| | | ### 3.5 手动补跑日汇总 |
| | | |
| | | ``` |
| | | POST /api/admin/management/merchant-settlement/auto-withdrawal/run?businessDate=yyyyMMdd |
| | | ``` |
| | | |
| | | `businessDate` 用**当天**(窗口是 `[业务日-30天, 业务日 24:00)`)。定时任务平时汇总前一日, |
| | | 补跑接口可以指定当天,所以不用等到凌晨。接口会顺带提交并收敛一次结果。 |
| | | |
| | | ```sql |
| | | select id, withdrawal_no, amount_cents, status, withdrawal_date, store_id |
| | | from merchant_withdrawal where merchant_id = {merchantId} order by id desc limit 3; |
| | | |
| | | select withdrawal_id, order_id, amount_cents from merchant_withdrawal_order |
| | | where withdrawal_id = (select max(id) from merchant_withdrawal where merchant_id = {merchantId}); |
| | | |
| | | select order_no, withdrawal_status from benefit_order where order_no = '{orderNo}'; |
| | | ``` |
| | | |
| | | 期望:金额 = 该订单实付金额(1 分)、`withdrawal_date` 是传入的业务日、来源订单已关联、 |
| | | `benefit_order.withdrawal_status=1`。 |
| | | |
| | | ### 3.6 结果收敛与到账 |
| | | |
| | | ```sql |
| | | select withdrawal_no, status, provider_trade_no, provider_request_date, paid_at, failure_reason |
| | | from merchant_withdrawal where merchant_id = {merchantId} order by id desc limit 1; |
| | | ``` |
| | | |
| | | 期望终态 `paid`、`provider_trade_no` 与 `paid_at` 非空;`failed` 时记录 `failure_reason`。 |
| | | 仍是 `processing` 时,等 `query-after-minutes`(设成 1 分钟)后再调一次补跑接口触发收敛。 |
| | | |
| | | ### 3.7 商户端与后台展示 |
| | | |
| | | - **后台**:`提现管理`(`/merchant-settlement?kind=withdrawals`)按商家名称筛出这条记录。 |
| | | **这一页目前不显示门店、到账时间、通道流水号、失败原因**(后端返回了,前端未渲染)。 |
| | | - **商户端**:提现页 `pages/withdrawal/withdrawal` 只在 `pages.json` 注册,**商户中心没有入口**, |
| | | 只能直达路由打开。用直达路由确认待提现金额、结算卡掩码、三种状态、失败原因。 |
| | | |
| | | ## 4. 各环节异常 |
| | | |
| | | | 环节 | 构造方式 | 期望 | 自动化已覆盖 | |
| | | | --- | --- | --- | --- | |
| | | | 支付 | 重复调用对账接口 | 状态与金额不变 | 是 | |
| | | | 支付 | 回调金额改一位小数 | 订单保持未支付,落 `PAYMENT_AMOUNT_MISMATCH` | 是 | |
| | | | 支付 | 回调用别的 `huifu_id` | 落 `PAYMENT_MERCHANT_MISMATCH` | 是 | |
| | | | 支付 | 回调签名错误 | 直接拒绝,不写审计行、不落账 | 是 | |
| | | | 核销 | 同订单重复核销 / 换幂等键 | 拒绝,只有一条有效核销记录 | 是 | |
| | | | 核销 | 未支付订单核销 | 拒绝 | 是 | |
| | | | 提现 | 同一业务日重复补跑 | 不生成第二条记录 | 是 | |
| | | | 提现 | 通道回失败(例如取现权限未开) | 记录 `failed` 存原因、来源订单解锁、次日重新汇总成一笔 | 部分 | |
| | | | 提现 | 后台改状态 / 商户发起提现 | 路由不存在 | 是(查路由表) | |
| | | |
| | | ## 5. 受控真实金额登记(执行前填好,缺一项不执行) |
| | | |
| | | | 项 | 值 | |
| | | | --- | --- | |
| | | | 环境 | 生产 / 真实通道(不是 test) | |
| | | | 测试商户 `merchant_id` | ____ | |
| | | | 测试商户 `huifu_id` | ____ | |
| | | | 结算银行卡 `merchant_bank_card.id` | ____ | |
| | | | 卡 `token_no` | ____ | |
| | | | 到账账号(脱敏) | ____ | |
| | | | 单笔金额上限 | ≤ __ 元(建议 0.01~1.00 元) | |
| | | | 单日累计上限 | ≤ __ 元 | |
| | | | 单日取现笔数上限 | ≤ __ 笔 | |
| | | | 执行人 / 审批人 | ____ / ____ | |
| | | | 环境 | 真实通道(`app.payment.huifu.environment=prod`) | |
| | | | 商户 `merchant_id` / `huifu_id` | ___ / ___ | |
| | | | 结算卡 `merchant_bank_card.id` / `token_no` | ___ / ___ | |
| | | | 到账账号(脱敏) | ___ | |
| | | | 人才 `talent_user.id` / 手机号 | ___ / ___ | |
| | | | 单笔金额上限 | ≤ __ 元 | |
| | | | 单日累计 / 笔数上限 | ≤ __ 元 / __ 笔 | |
| | | | 执行人 / 审批人 | ___ / ___ | |
| | | |
| | | **执行步骤** |
| | | **失败即停**:金额超过上限、到账目标与登记不符、同一 `req_seq_id` 被提交两次、 |
| | | 或 `paid` 之后金额又被顺延,立即停止。 |
| | | |
| | | 1. 只放行上述白名单:`merchant_id`、银行卡、账号三者的组合必须完全匹配,任一不符立即终止。 |
| | | 2. 关掉真实环境的定时任务(`AUTO_WITHDRAWAL_ENABLED=false`),改为手工按业务日触发一次汇总, |
| | | 避免任务选中白名单之外的商户。 |
| | | 3. 用最小金额造一笔已支付已核销订单(金额取「最低提现金额」临界值,优先验证 |
| | | `AUTO_WITHDRAWAL_MIN_AMOUNT_CENTS` 是否可用;被 `23000003` 拒绝就说明内扣手续费吃掉了本金, |
| | | 据此调整手续费缓冲)。 |
| | | 4. 触发一次汇总并提交取现,记录 `withdrawal_no`、`cash_amt`(元)、`into_acct_date_type`。 |
| | | 5. 确认 `processing` → 等通知/查询 → `paid`。 |
| | | 6. 在银行侧核对**真实到账**:金额 = `cash_amt` 减去内扣手续费,到账日符合到账类型。 |
| | | 记录银行流水号与到账时间。 |
| | | 7. 汇总当天的取现笔数与累计金额,确认没有超过登记的上限。 |
| | | 8. 收尾:恢复定时任务开关(或保持关闭直到正式上线决策),把结果回填到工单 06。 |
| | | ## 6. 未解决的问题(上线前必须有结论) |
| | | |
| | | **失败即停**:出现金额超过上限、到账目标与白名单不符、同一 `req_seq_id` 被提交两次、 |
| | | 或 `paid` 之后金额又被顺延,立即停止并保持 `AUTO_WITHDRAWAL_ENABLED=false`。 |
| | | |
| | | ## 6. 需要沙盒/真实阶段确认后才敢打开的遗留项 |
| | | |
| | | 这些是代码里明确留白、必须由本轮验收给出结论的点: |
| | | |
| | | 1. **`AUTO_WITHDRAWAL_QUERY_AFTER_MINUTES` 的取值**:等待期是「通道还没落库」与「通道从未受理」 |
| | | 之间唯一的区分手段。要实测一笔已受理的取现在多久之后可以被查到;如果沙盒里 5 分钟不够, |
| | | 失败会被误判并解锁订单,必须调大。 |
| | | 2. **最低取现金额、单笔与单日上限、内扣手续费**由斗拱与银行按商户配置,需要在沙盒/真实环境 |
| | | 确认实际取值,才能定 `AUTO_WITHDRAWAL_MIN_AMOUNT_CENTS` 与 |
| | | `AUTO_WITHDRAWAL_FEE_BUFFER_CENTS`。 |
| | | 3. **`into_acct_date_type` 的到账口径**:文档给的是 `D0`/`T1`/`D1`/`DM`,要以目标产品与沙盒 |
| | | 实际返回为准;当前默认 `DM`。 |
| | | 4. **沙盒回调字段完整性**:成功回调是否一定带 `huifu_id`、`trans_amt`;取现回调是否一定带 |
| | | `trans_status`、`fee_amt`、`acct_status`。 |
| | | 5. **未处理的通道语义(代码尚未实现,需要单独工单)**: |
| | | - 取现回调的 `re_exchange=Y`(已退汇):到账后被退汇目前不会回到「提现失败」,金额也不会再次顺延。 |
| | | - `acct_status=B`(回账成功):同上。 |
| | | 这两项在沙盒里就能构造,验收时要明确记录当前行为(钱回来了但本地仍是 `paid`)。 |
| | | 1. **`AUTO_WITHDRAWAL_QUERY_AFTER_MINUTES` 的取值**:等待期是区分「通道还没落库」与「通道从未受理」的唯一手段, |
| | | 要实测一笔已受理的取现多久后能查到;太短会把失败误判并解锁订单。 |
| | | 2. **内扣手续费的真实情况**:本商户取现无手续费(手续费在支付时内扣),其他商户/产品是否如此未知。 |
| | | 官方错误码 `23000003`「取现金额不足以支付内扣手续费」说明存在这种可能。**最低金额与手续费缓冲配置已删除**, |
| | | 将来若真遇到该错误码,应重新引入门槛而不是放宽别处。 |
| | | 3. **`into_acct_date_type` 到账口径**:文档给 `D0/T1/D1/DM`,当前默认 `DM`,以目标产品实际返回为准。 |
| | | 4. **回调字段完整性**:真实回调是否一定带 `huifu_id`、`trans_amt`、`fee_amt`、`acct_status`。 |
| | | 本手册用查询补偿绕过了回调,**回调路径未被真实覆盖**。 |
| | | 5. **退汇与回账未实现**:`re_exchange=Y` 与 `acct_status=B` 不改变本地状态,钱回来了记录仍是 `paid`。 |
| | | 6. **提现金额与通道余额无对账**:`cash_amt` 取自已核销订单汇总,从不校验通道可用余额 `avl_bal`。 |
| | | 已核销金额尚未结算到账时,取现会因余额不足失败并顺延,重试循环里看不到根因。 |
| | | 7. **多门店仍无门店选择**:`benefit_order.store_id` 已修复(人才端自主下单会写门店),但 |
| | | `AppOrderCreateRequest` 只有 `merchantId/productId/couponId`,接入多门店前需让下单入口支持选门店。 |
| | | 8. **商户端提现页无入口**;后台提现列表不显示门店 / 到账时间 / 通道流水号 / 失败原因。 |
| | | 9. **`login_name` 全局唯一**:进件网关用进件单号拼管理员账号(`HuifuEnterpriseOnboardingGateway:129`), |
| | | 同一主体重复提交进件会撞唯一约束,真实接商户前必须改成稳定唯一的生成规则。 |
| | | 10. **商户端 `verification/confirm` 的幂等键是假的**:收到 `X-Idempotency-Key` 但不落库, |
| | | 真正兜底是 `merchant_verification_attempt`;`/orders/{orderNo}/verify` 才是真幂等。 |