适用提交: 6c42f8f | 最后验证: 2026-09-18 | 何时会失效:
application.yml里app.payment.huifu块的配置键、回调端点数、或任一@Scheduled任务的配置键发生变化时
面向「接手后怎么把环境跑起来、怎么上线」。排障看 07;协议契约看 05;实现细节见附录 A
(talent-card-server/docs/huifu-payment-integration.md),冲突时以附录 A 为准。
本文件不写任何凭据值。 私钥、密钥、商户号、汇付号、AppID、公网 IP、银行卡号一律脱敏或省略。
置信度四档同 07;凡标【实测】的都是我们自己跑出来的,**汇付侧未书面确认**。
以根目录 CONTEXT.md 为准,这里只列上线验收时会直接用到、且最容易混的四个:
| 术语 | 一句话 | 别写成 |
|---|---|---|
| 收款能力 | 通道侧对这家子商户当下是否可用的**判定事实**(商户状态 + 取现/结算开关);不由平台决定,会随控台操作改变 | 收款就绪 |
| 收款就绪 | 商户可以接受微信付款的**某一时点判定结果**,除收款能力正常外还要求微信支付已开通、微信实名与授权已完成 | 进件通过 |
| 延迟户 / 交易确认 | 未核销的钱停在通道**延迟户**(不可结算、不可取现);核销后调**交易确认**分给商户基本户 | 中间户 |
| 微信待实名 | 微信支付开通过程中仍需法人本人扫码授权并满足 authorize_stat=1 的阶段,**不是开通失败** |
开通失败 |
五个端点都在 PaymentCallbackController(/api/payments/huifu),地址各自一个配置键。
当前它们全部指向同一个公网 IP 与端口(形如 http://221.x.x.x:11451/...)【实测】。app.payment.huifu.notify-url 之外的四个键如果留空,HuifuPaymentProperties 会**从 notify-url 推导**出同 host
同路径下的地址——所以「地址为空」不会报错,只会静默消失。启动时 HuifuCallbackUrlAudit 会把五个端点逐条念一遍。
| 端点 | 配置键 | 环境变量 | 承接什么通知 |
|---|---|---|---|
POST /notify |
notify-url |
HUIFU_NOTIFY_URL |
支付成功 + 退款结果 |
POST /merchant-business-notify |
merchant-business-notify-url |
HUIFU_MERCHANT_BUSINESS_NOTIFY_URL |
绑卡(业务开通)结果 |
POST /merchant-onboarding-notify |
merchant-onboarding-notify-url |
HUIFU_MERCHANT_ONBOARDING_NOTIFY_URL |
进件审核结果 |
POST /merchant-wechat-config-notify |
merchant-wechat-config-notify-url |
HUIFU_MERCHANT_WECHAT_CONFIG_NOTIFY_URL |
微信小程序配置结果 |
POST /merchant-business-open-notify |
merchant-business-open-notify-url |
HUIFU_MERCHANT_BUSINESS_OPEN_NOTIFY_URL |
商户业务开通结果 |
三条上线必须做到的事:
merchant-business-notify-url 曾经是**生产 bug**(键原本不存在,notify-url 推导,于是「通道侧到底注册了哪个地址」取决于另一个配置);现在它显式写在 yml 里【实测】。POST 表单(resp_data + sign),且**不得重写**这两个字段。RECV_ORD_ID_<req_seq_id>【实测】;验签不过直接 403,落不进 payment_callback_event。通道侧实际注册的地址,本地看不出来——HuifuCallbackUrlAudit 只能报「我们配了什么」。
必须在控台逐条比对并留档。[待确认]
注意层级:fund-confirmation-retry 与 stranded-refund 在 app.payment 下,**不在** app.payment.huifu 下。
| 配置键 | yml 默认值 | 作用 |
|---|---|---|
app.payment.huifu.enabled |
true |
总开关。置 false 时**不发起任何通道调用**,收款能力只校验本地规则 |
app.payment.huifu.environment |
prod |
决定 SDK 用生产还是沙箱模式(Java 字段默认值是 test,两处不一致) |
...huifu.reconciliation.enabled |
true |
支付与退款唯一的后台兜底;关掉它只剩「用户点了取消/重申支付」这一个补救入口 |
...huifu.reconciliation.pending-after-minutes |
5 |
慢车道:支付挂起超过这么久才查一次通道 |
...huifu.reconciliation.fixed-delay-ms |
60000 |
扫描间隔。它同时是**关单补偿的重试下限**(通道不允许关闭一分钟以内的订单),调慢会让「关单每分钟重试一次」不成立 |
...huifu.capability-refresh.enabled |
true |
收款能力快照的定时核实 |
...huifu.capability-refresh.stale-hours |
6 |
只扫「超过这么久没核实」的进件。**它是被控台关停的商户最长约 6 小时的感知空窗**,代价与通道查询量成反比 |
...huifu.capability-refresh.fixed-delay-ms |
1800000 |
能力核实每 30 分钟一轮 |
...huifu.merchant-capability-required |
true |
是否要求通道确认能力;设 false 仅用于刻意跳过通道确认的部署 |
app.payment.fund-confirmation-retry.enabled |
true |
交易确认补偿(钱冻在延迟户时的重试) |
app.payment.fund-confirmation-retry.fixed-delay-ms |
300000 |
5 分钟一轮 |
app.payment.stranded-refund.enabled |
true |
悬账自动退款(退回付款人);通道拒绝则升级告警转人工 |
app.payment.stranded-refund.fixed-delay-ms |
300000 |
5 分钟一轮 |
...huifu.merchant-use-chains-flag |
空(不下发) | 不要下发 3:那要求上级本身有商户级微信配置,我们的上级是渠道商,会被拒「失败:复用交易信息,上级商户号为空」【实测】 |
...huifu.merchant-wechat-scenes |
1,2,3 |
微信支付场景:线下反扫/线下公众号/线下小程序。**2026-09-24 起统一走线下**;回退线上配 4,5,12(场景、CID、费率、fee-type、进件场景是一套口径,要一起换) |
...huifu.merchant-wechat-fee-type |
02 |
业务开通类型:线下 02/线上 01。它同时决定 AppID 绑定的 fee_type、实名与实名查询的 pay_scene、以及判定时按 fee_type 匹配配置项的口径——四处必须同一个值 |
...huifu.merchant-wechat-fee-rate |
0.30 |
微信费率(线下)。Java 字段默认值已与 yml 统一为 0.30(线上是 0.70)。实际生效值以商户详情 qry_wx_conf_list[].fee_rate 读回为准 |
...huifu.merchant-wechat-fee-rule-id / -individual / -sole-proprietor |
758 / 770 / 762 |
CID 按「支付场景 × 业务开通类型 × 主体类型」三维定位:**线下(类型 02)企业 758/小微 770/个体户 762**;线上(回退用)是 756/775/761。不能全平台共用一个号 |
...huifu.merchant-onboarding-scene-type |
OFFLINE |
进件的 scene_type:线下报备口径(要实体经营场所材料)。回退线上配 ONLINE |
...huifu.merchant-micro-business-type |
MICRO_TYPE_STORE |
小微经营类型(个人主体开通微信业务必填):线下门店场所取 MICRO_TYPE_STORE,线上商品服务交易才是 MICRO_TYPE_ONLINE |
app.order.pending-expiry.fixed-delay-ms |
yml 里没有这一行 | 订单到期清理,靠注解默认 60000 生效;它**没有** enabled 开关 |
两处过期的注释要当心:enabled: true 上方仍写着 Keep disabled until the Huifu test/production product, merchant and keys are provisioned.,
而它已经是 true 且 environment: prod,指向真实通道;merchant-use-chains-flag 的注释里那条 6666xxxxxxxxxxxx
是**渠道商号**,属脱敏范围,不要抄进任何外部文档。
${ENV:default} 覆盖机制配置里统一用 ${ENV_VAR:default} 写法:环境变量存在就用它,不存在就退回冒号后的默认值。
逐键覆盖、无需整块替换,所以部署侧只要注入必要的几个变量即可,其余沿用默认。
判定某键是否已被外部注入,看它**是否以 ${ 开头**即可。
同一份文件里目前两种做法并存:全文件共 42 处使用 ${...} 占位(连 onboarding-upper-huifu-id 都是占位),
但 app.payment.huifu 块下**四个最敏感的键是字面量明文、不是占位**:
| 键 | 现状(**不抄值**) |
|---|---|
system-id |
字面量明文,形如 6666xxxxxxxxxxxx |
merchant-id |
字面量明文,形如 6666xxxxxxxxxxxx |
private-key |
字面量明文 RSA 私钥(***) |
platform-public-key |
字面量明文(***) |
这四个值随 application.yml 进了 git 历史。同块里更晚新增的键都自觉用了占位——**没有一并改掉存量那四个**。
详见 .scratch/huifu-handover/issues/01-plaintext-credentials-in-application-yml.md。
凭据的唯一合法来源是部署侧的密钥存储(K8s Secret / 环境变量注入 / 配置中心),由部署流程注入上面那张表里的
环境变量名。application.yml 只允许出现 ${HUIFU_PRIVATE_KEY} 这类**占位**,不允许出现任何真实值。
两阶段处理,**不要合并成一步**(完整论证见.scratch/huifu-handover/issues/01-plaintext-credentials-in-application-yml.md):
第一阶段:改占位(小、可独立评审)。 把四个键改成与同文件其他 42 处一致的写法:
system-id: ${HUIFU_SYSTEM_ID}
merchant-id: ${HUIFU_MERCHANT_ID}
private-key: ${HUIFU_PRIVATE_KEY}
platform-public-key: ${HUIFU_PLATFORM_PUBLIC_KEY}
${VAR} 而部署没配,启动会因空值报错或用空串签名。application.yml 是 AGENTS.md 点名的**全仓共享文件**,这一改要单独一个工单、单独一次评审,不要夹在别的任务里。第二阶段:按流程轮换(必须做,需要汇付侧配合)。
改占位不等于止血——私钥已经进过 git 历史,任何拿到仓库(现在或将来)的人都能对汇付侧伪造请求签名;
而平台身份对目标商户有数据权限,泄露的私钥能实际操作商户数据。仓库是多人按业务域协作的,历史里还有合并、
fork、worktree,**无法确认副本的分布范围**。所以必须申请新的 system-id / merchant-id / 私钥 / 平台公钥,
并在控台重新绑定回调地址。轮换前先拿到汇付侧的轮换窗口与操作顺序(旧凭据还能用多久)、确认onboarding-upper-huifu-id 与渠道商关系是否随换号变动、准备好回滚方案(换号期间支付与进件回调都可能中断)。
长期:加一条提交前检查——*.yml 里 private-key/platform-public-key 的值只要不以 ${ 开头就拒绝提交;
日志与报错出口统一脱敏(测试曾经打印过运行配置里的明文私钥,后改为使用 keySummary 摘要函数)。
全仓共 5 个 @Scheduled,全部走 fixedDelayString(上一轮跑完再计时)。
| 任务 | 配置键 | 注解默认值 | yml 有没有写 | 作用 |
|---|---|---|---|---|
PaymentReconciliationScheduler |
app.payment.huifu.reconciliation.enabled |
true |
有 | 支付与退款的后台兜底 |
| 同上 | ...reconciliation.fixed-delay-ms |
60000 |
有 | 扫描间隔(同时是关单补偿的下限) |
MerchantPaymentCapabilityScheduler |
app.payment.huifu.capability-refresh.enabled |
true |
有 | 收款能力快照核实 |
| 同上 | ...capability-refresh.fixed-delay-ms |
1800000 |
有 | 30 分钟一轮 |
PaymentFundConfirmationRetry |
app.payment.fund-confirmation-retry.enabled |
true |
有 | 延迟户资金确认补偿 |
| 同上 | ...fund-confirmation-retry.fixed-delay-ms |
300000 |
有 | 5 分钟一轮 |
StrandedPaymentRefundRetry |
app.payment.stranded-refund.enabled |
true |
有 | 悬账自动退款 |
| 同上 | ...stranded-refund.fixed-delay-ms |
300000 |
有 | 5 分钟一轮 |
PendingOrderExpiryScheduler |
app.order.pending-expiry.fixed-delay-ms |
60000 |
没有 | 订单到期清理;也**没有** enabled 开关 |
这条约束必须守住:注解上的默认值必须与 yml 里的默认值一致。 原因是一段真实历史:
定时对账原先**注解默认 false、靠 yml 那一行打开**,于是**删掉那行配置,支付与退款唯一的后台兜底就静默消失**;
交易确认补偿的开关更极端——javadoc 里宣称有 app.payment.fund-confirmation-retry.enabled,
但**代码从未读取这个键**,任务永远在跑。
现在两者都已对齐成「注解与 yml 默认值都是开」,并由 PaymentSchedulingConfigurationTests 钉住:
它直接读 application.yml 的文本与 @Value 表达式字符串做断言,所以任何人改动默认值都会红。
注意 PendingOrderExpiryScheduler 不在这个测试的覆盖范围内(它既没有 enabled 也没有 yml 条目),
上线前请确认这个任务的间隔是否符合预期。[待确认]
每条主链路都要**在真实通道上**走一遍。括号里是「看哪里能证明它成立」。
Unknown column ...(merchant_onboarding_application 有行、无 500)store_*_pic 与业务开通的 extended_material_list)payment_callback_event 出现 merchant-onboarding-notify 的请求流水号)90000000 是「受理/审核中」,「微信支付」阶段不落在 failed)authorize_stat=1 能让页面从「待办理」翻成已确认,二维码收起bank/merchantNormal)capability_verified_at 非空,merchant_stat、cash_switch_status 有值)capability-refresh 在 stale-hours 内刷新payment_transaction.status = SUCCEEDED、benefit_order.order_status = 1、paid_at 有值payment_callback_event.process_status = PROCESSEDreceived_atpayment_reconciliation_issue 新增 PAYMENT_AMOUNT_MISMATCHPAYMENT_MERCHANT_MISMATCHpending-after-minutes 后把状态收敛或记一条 open 问题记录23000000/10000016)时落成 CLOSED,**不再**交回客户端90000000)时记为 CANCEL_UNCONFIRMED(不是 CLOSED)CANCEL_UNCONFIRMED 在「不自动取消」之列)order_refund.refund_status = 2,且 provider_refund_no 非空provider_refund_no 覆盖成空串stranded-refund 自动退回付款人,问题记录被结掉STRANDED_REFUND_REJECTED 并留 open,**不静默吞掉**funds_confirmation_status 回到 pending;被通道拒绝时给出**可分辨**的文案(不是笼统「撤销失败」)pageSize 已被钉在 50 以内)failures 里talent-card-server/src/main/resources/db/migration/ 当前的**最大版本号**V155)。本仓库由多人按业务域分工,两人从同一祖先各自独立占同一段编号时 git 不报冲突,.sql + 1 个 Java 版迁移src/main/java/db/migration/V125__parking_lot_registration_snapshot.java,包名 db.migration)。1..155 里真正的空档只有 6 个:97 / 105 / 109 / 110 / 111 / 126。resources/db/migration/*.sql——那样会漏掉 Java 版迁移,把 V125 误判成空档Successfully validated N migrations。经过见 §8.2。V148–V151 → V151–V154(V148__merchant_capability_sync_until→V151、V149__payment_attempt_convergence→V152、V150__merchant_numeric_code_attempt→V153、V151__order_cancel_request→V154)。经过见 .scratch/pending-order-payment-flow/README.md。scripts/repair-local-mysql-v*.sql 就是为此付出的代价。ADD COLUMN IF NOT EXISTS:那是 MariaDB 扩展,MySQL 8.0.36 上直接 ERROR 1064V143/V145/V146 的头注释里被记了三次)。迁移只执行一次,本来也不需要它。flyway.enabled=false,测试库是 8.0.36 且 Flyway 打开。.\scripts\apply-sql-file.ps1 -File <某个 .sql> -Yes,默认连开发库,不带 -Yes 只打印)。cd talent-card-server
.\scripts\run-server-tests.ps1 # 全量
.\scripts\run-server-tests.ps1 -Test PaymentSettlementTests # 单个类
.\scripts\run-server-tests.ps1 -Reset # 重置库后再跑
target/,症状是看起来不可能的报错,例如No qualifying bean of type 'HuifuPaymentProperties'——遇到这类「不可能」的 bean/classpath 错误,-Clean,再怀疑代码。另外 mvnw.cmd 总是用 PowerShell 5.1 引导,5.1 会把 Maven 打到 stderr 的pwsh 跑。drop database,也不要手删 flyway_schema_history 里的某一行;用.\scripts\reset-test-db.ps1(它对名为 talent_card 的库一律拒绝——开发库里有迁移从未创建的手建表,wsl.exe -d Ubuntu-26.04 -u root -- bash -lc "sleep 1800")。docs/agents/testing.md 的「已知基线失败」表;修好其中任何一项时,删掉对应行。docs/agents/testing.md 的分工一句话:**测试环境、跑测试的命令、重置库、已知基线失败、WSL 的坑一律以 docs/agents/testing.md 为准**
(那里是唯一维护点,要更新就改它);本文件只讲**部署与上线**(凭据、回调地址、开关、验收清单、迁移编号规约),
测试部分只做索引,**不重复维护**。
这不是生产部署。 这台机器目前承担的是**开发测试环境**:没有域名、没有备案、没有 TLS,
公网入向只开了8081/8082,外部访问仍要过堡垒机,进程还是systemd-run临时单元(重启不自动拉起)。
本节只记录「环境能起来、迁移能在 8.4 上跑通」这一层事实,**不代表上线验收**(验收见 §4)。
第一次在**空库**上把迁移集完整跑一遍,验证 MySQL 8.4 兼容性。目标机 Ubuntu 26.04.1、
MySQL 8.4.11-0ubuntu0.26.04.1(原生 apt,非容器;datadir 迁至 /data/mysql,AppArmor 走/etc/apparmor.d/local/usr.sbin.mysqld 覆盖而非改主 profile)、Redis 8.0.5、JDK 17.0.20。
Successfully applied 148 migrations to schema talent_card, now at version v154(60.9 秒)。flyway_schema_history 148 行、success 全为 1、最高版本 154;建出 97 张表。jpa.hibernate.ddl-auto: validate 通过,应用正常启动 —— 实体与迁移建出的结构自洽。Successfully validated 148 migrations + Schema talent_card is up to date. No migration necessary.(checksum 一致,可重复校验)。154 是**最大版本号**;1..154 里真正的空档是 6 个:97 / 105 / 109 / 110 / 111 / 126。.sql 文件 147 个,另有 1 个 Java 版迁移:V125__parking_lot_registration_snapshotsrc/main/java/db/migration/,它在 flyway_schema_history 里的 type 是 JDBC 而不是 SQL)。resources/db/migration/*.sql 会把 Java 版迁移漏掉,从而把 V125 误判成空档 —— 这个错Successfully validated N migrations。| 输出 | 含义 |
|---|---|
PROCEDURE ... does not exist(1305),15 条 |
15 个迁移用「DROP PROCEDURE IF EXISTS → CREATE → CALL → DROP」做幂等 ALTER。**全新库上第一次 DROP IF EXISTS 必然打这条**(MySQL 返回 warning 而非 error,Flyway 把任何返回消息都记成 WARN)。只在全新库出现,预期内。 |
Apparent connection leak detected(HikariCP) |
yml 里 leak-detection-threshold: 10000,而启动期主线程会长占连接(Flyway 长迁移、支行目录导入)。日志随后就跟着 ... was returned to the pool (unleaked),**误报**。 |
VALUES function is deprecated(1287),7 条 |
这条要认真对待:INSERT ... ON DUPLICATE KEY UPDATE ... VALUES(col) 自 MySQL 8.0.20 起弃用,**未来版本会移除**。8.4 上仍能运行。 |
1. MYSQL_URL 必须带 rewriteBatchedStatements=true(Connector/J 默认 false)。不加这个参数,
代码里的 batchUpdate 在驱动层会退化成逐条发送。实测对比(同一张 bank_branch_directory,约 15.4 万行):
| 语句执行次数 | 耗时 | 单行耗时 | |
|---|---|---|---|
| 不带该参数 | 52,915 | 986.4 秒 | 18.6 ms |
| 带该参数 | 155 | 28.5 秒 | 0.18 ms |
约 100 倍差距(performance_schema.events_statements_summary_by_digest 实测)。副作用:批处理被改写后executeBatch() 返回 SUCCESS_NO_INFO,BankBranchDirectoryInitializer 会打出Imported 0 bank branch directory rows —— 数据是全的,只是那个计数器失真了,不要据此判断导入失败。
2. REDIS_PORT / REDIS_PASSWORD 必须注入,不能因为「业务代码零调用 Redis」就跳过。
Actuator 注册了 Redis 健康指示器:应用默认连 localhost:6368 + 密码,而 Ubuntu 装的 Redis 在 6379
且默认无密码 → /actuator/health 永远 503(management.endpoint.health.probes.enabled: true 时
liveness/readiness 一并受影响)。实测把这五项都注入后 health 恢复 {"status":"UP"}:MYSQL_URL / MYSQL_USERNAME / MYSQL_PASSWORD / REDIS_PORT / REDIS_PASSWORD。
3. CORS_ALLOWED_ORIGINS 必须包含部署后浏览器实际访问的那个 origin**,哪怕是同源。**
这条曾经被想当然地漏掉(理由是「前端和 API 走同一个端口 = 同源 = 不需要配 CORS」),
结果**登录直接 403 Invalid CORS request**。机制是:**浏览器对同源的非简单请求也会带 Origin 头**
(POST + Content-Type: application/json 就是),而 ApiCorsConfig 给 /api/** 注册的 CorsFilter
只要看到 Origin 就按白名单校验,不在名单里就 403。**「同源」并不等于「不会有 Origin 头」。**
实测判据(改前 / 改后):
| 请求 | 改前 | 改后 |
|---|---|---|
POST /api/admin/auth/login + Origin: <部署域名> |
403 Invalid CORS request |
400 account must not be blank(已进业务代码) |
OPTIONS 预检 + 同 Origin |
— | 200,带 Access-Control-Allow-Origin |
POST + Origin: http://evil.example.com |
403 | 仍 403(白名单照样生效) |
注意 yml 里还有 allowed-origin-patterns(默认 http://localhost:[*]、http://127.0.0.1:[*]),
它覆盖本机随机端口的调试来源、**与精确白名单是两个键**,覆盖 CORS_ALLOWED_ORIGINS 不会动它——
所以本地 vite / HBuilderX 调试不受影响。**上线时把真实域名加进 CORS_ALLOWED_ORIGINS 即可。**
这条坑会跟着域名的每一次变化重演。 origin 由「协议 + 主机 + 端口」三者共同决定,**任何一个变了
都是一个新 origin,都要单独加**。2026-09-22 一天里演了两次:先用 IP 访问(http://117.172.73.106:8081)
踩了一次;把域名 www.dqrcfwpt.com 解析好、**改用域名访问又踩了一次**(登录再次 403,表现与第一次
一模一样)。所以:**换域名、换端口、上 HTTPS 都要同步改这个变量**;http://host:8081 与 https://host
是两个不同 origin,端口不同也是两个。
4. 凭据要逐条过一遍「弱口令」口径。 政务云的安全扫描不是形式主义——2026-09-22 它**真的登进来了一次**
(抓到请求体 {"account":"admin","password":"123456"},响应 200 带 accessToken)。部署时以下三件事一起做:
| 凭据 | 要求 |
|---|---|
APP_PRIVACY_ENCRYPTION_KEY |
必须在**写入任何隐私数据之前**覆盖仓库里的开发默认值(见 §8.6)。 |
| MySQL / Redis 口令 | 服务商口径是「大小写 + 数字 + 特殊符号」组合。**注意纯随机生成不保证四类齐全**:32 位随机实测有约 1% 概率一个特殊符号都没有(本次第一次就漏了,重生成到第 5 次才齐),所以生成时要**循环到四类都出现**再落库。特殊符号只用 @%^*+_.-,避开会破坏 systemd EnvironmentFile 解析与 shell 展开的 $ " ' \ ! 和空格。 |
| 后台引导账号 | V76 种下的 admin 口令**经实测就是 123456**(用 node 按迁移里的参数算 PBKDF2,结果逐字符相同),而 where not exists 意味着**每个新库都会重种一遍** —— 空库部署后第一件事就是把它换掉。根因与修法见 .scratch/weak-credentials-hardening/issues/01-*。 |
改 Redis 口令有个坑:库上**已经设过** requirepass 时,redis-cli config set requirepass <新> 会报NOAUTH Authentication required(未认证不许改)。要么先 redis-cli -a <旧> config set ...,
要么**直接改 /etc/redis/redis.conf 的 requirepass 再 systemctl restart redis-server**(更省事)。
轮换后**旧口令仍会留在备份文件里**(已失效但还在磁盘上),记得同步刷新,只留一份当前状态的备份。
不必担心把自己锁死:MySQL 的 root@localhost 走 auth_socket,口令改坏了也能用 root 身份重设。
虚拟 CPU 缺 SSE4.2 / POPCNT / AVX。仓库 AGENTS.md 只把它记在「以 x86-64-v2 为基线的镜像跑不起来」上,
但它**同样拖慢原生 MySQL**:InnoDB 页校验退化为软件实现,写入路径明显变慢。根治要请服务商把虚拟机
CPU 型号改成 host-passthrough。
另外 innodb_buffer_pool_size 是 MySQL 默认的 128MB,在 15G 内存的机器上当生产库太小,上线前应调大。
APP_PRIVACY_ENCRYPTION_KEY 必须在写入任何隐私数据之前覆盖:默认值是开发用的 key,app.payment.huifu 的四个键与 app.wechat.mini-program.app-secret 在 application.yml 里是字面量明文,.scratch/huifu-handover/issues/01-*.md。不要把它当成这台机器的问题。 非公网环境收不到回调是**已经记录在案的既定事实**,而且整套设计本来就不
依赖回调:见 ADR 0004(「通道状态以主动查询为权威,回调只作
加速器」),以及 .scratch/pending-order-payment-flow/README.md 的 F 系列清单 —— F1 正是「回调地址指向
公网 IP,开发环境不可达」。那里还定过一条边界:**不允许为了开发便利开「跳过真实通道」的置付捷径**,
所以开发/测试环境的收敛只能走真实查询,不能靠伪造资金事实。
不依赖回调的收敛路径(**订单不会因为没回调而卡住**):
PendingOrderPortalController 会当场经PaymentReconciliationService 问一次通道;顺带把「关单处理中」也问出结论(concludeUndecidedCloses)。reconciliation.fixed-delay-ms 默认 60 秒一轮。pending-after-minutes(默认 5)与 last_reconciled_at,只对长时间没被问过的行唯一要守住的是 app.payment.huifu.reconciliation.enabled 保持默认 true:它是支付与退款唯一的后台兜底,
关掉就只剩「用户点了取消 / 重申支付」这一个入口(见 §1.2)。
微信支付口径整体从线上切到线下(见 §1 的环境变量表:场景 1,2,3、类型 02、费率 0.30、
CID 758/770/762、进件 scene_type=OFFLINE)。**存量商户是在控台手工改的口径**,所以发布后要逐个迁移:
missing」的商户 —— 也就是**收款闸门会挡住收款**的那批:select a.provider_huifu_id as huifu_id,
a.merchant_name,
json_extract(p.stages_json, '$.wechat') as wechat_stage,
p.last_synced_at
from merchant_onboarding_application a
join merchant_onboarding_progress p on p.application_id = a.id
where a.provider_huifu_id is not null and a.provider_huifu_id <> ''
and json_extract(p.stages_json, '$.wechat') <> 'confirmed';
1/2/3),并**确认人才端小程序 AppID 绑到了该商户实际收款的05 第 19c 条)—— 不绑就会出现「业务执行失败[失败]」。wechat 回到 confirmed 即恢复收款。注意:判定完全跟随配置,所以回退线上要连场景、CID、费率、fee-type、进件 scene_type 一起改回去
(05 第 19b 条),只改一处会出现「通道已开通、阶段永远 missing」。