支付系统优化与官方支付接入方案
| 项目 | 内容 |
|---|---|
| 站点 | 39.96.90.85(纯 HTTP :80,无域名、无证书、无备案信息) |
| 技术栈 | PHP 8.2.33(nginx + PHP-FPM)+ MySQL 5.7.40 + 宝塔面板 |
| 站点根目录 | /www/wwwroot/video |
| 代码版本 | CMS_VERSION = v0.3.81(config/config.php:27,本方案不提升) |
| 文档性质 | 设计与审计交付物。本轮未改动任何业务代码、未建表、未新增依赖、未改面板核心 |
| 编制日期 | 2026-09-30 |
| 关联文档 | docs/网站BUG修复.md(BUG-2026-0027 ~ 0030:收款码/动态码体系) |
落地状态(v0.3.85 补注,阅读本文档前必读)
本方案已于 v0.3.82 ~ v0.3.84 分批落地:
includes/payment/已随包交付,官方支付通道已可用。 本文档保留为历史设计与审计记录,上表"不提升CMS_VERSION""本轮未改动任何业务代码", 以及第三节「不做清单」与附录 A 中的同类表述,仅描述编制当时(v0.3.81)的站点状态,不代表当前交付包。第四节 4.1 的"目标架构"文件名与实际交付的差异如下:
方案中的文件 实际交付形态 AlipayFaceToFaceGateway.php/WechatNativeGateway.phpAlipayGateway.php/WechatGateway.phpSigner.php/Certs.phpPaymentSigner.php/PaymentConfig.php(密钥装载并入 PaymentConfig)payment-page.php未单独实现:收款码/动态码由 payment-code.php承载,下单在payment-create.php+payment-guide.phppayment-return-alipay.php未单独实现:同步跳转后的状态确认由 payment-status.php轮询完成admin/payment-refund.php未交付:渠道层 PaymentGateway::refund()(支付宝/微信均已实现)无编排与后台入口,退款需到渠道商户平台人工操作admin/payment-proof.php未交付:支付凭证未做受控输出页,凭证以渠道回执/后台订单原始报文为准 cron-payment-reconcile.php/cron-payment-sweep.php合并为 cron-payment.php:默认 sweep + reconcile,--no-query只关单不查单,--limit=N限流admin/payment-orders.php、payment-create.php、payment-status.php、payment-notify-alipay.php、payment-notify-wechat.php已交付(与方案同名) 方案之外额外交付:
payment-receipt.php(安卓微信收款回执监听,BUG-2026-0016)与includes/payment/schema.sql(4 张支付表 DDL)。 后者不并入database.sql,启用官方支付前需手工执行一次,详见docs/安装配置说明.md第二节第 5 项。
结论摘要(先读这一段)
- 本站当前不存在任何支付网关代码。 全站检索
notify_url/out_trade_no/precreate/mchid/WECHATPAY等关键字,业务代码命中数为 0。现有"支付"= 静态/套餐收款码 + 用户上传转账截图 + 管理员人工点击审核到账。 - 因此"接入官方支付接口"不是改造,而是新增一条与现有人工流程并行、可灰度、可回退的通道。现有流程必须保留为降级兜底。
- 存在 3 个硬阻断项(详见第十四节):无域名/无 HTTPS(微信支付 v3 强制要求 HTTPS 回调)、支付渠道资质与密钥未提供、是否允许引入官方 SDK(当前无
composer、无vendor/)与新增数据表未确认。 - 在资质到位前,P0 阶段可立即落地不依赖渠道的加固(凭证裁剪缺陷、并发重复到账、订单号暴露、监控与对账),这些不触及任何外部接口,收益立即可见(详见第十三节)。
一、现状审计
1.1 支付链路实测(含代码级证据)
下单
- 入口:
token-recharge.php(POST 处理区token-recharge.php:47-119)。 - 客户端只提交
package_id/price/pay_method/ 可选pay_proof。 - 服务端做了正确的防篡改处理:套餐订单的金额与 Token 数均从
token_packages回查(token-recharge.php:75-88),拒绝前端伪造额度——这一点是本项目现存代码的优点,新通道必须沿用同一原则。 - 落库:
createTokenOrder()(includes/token-functions.php:254-261)
```sql INSERT INTO token_orders (user_id, package_id, token_amount, price, pay_method, pay_proof, status) VALUES (?, ?, ?, ?, ?, ?, 0) ```
status = 0(待审核)。全站仅此一处创建订单。
- 随后
buildPaymentGuide()(includes/token-functions.php:1107)渲染收款码卡片,pay_method取值仅alipay/wechat(token-recharge.php:316-317)。
到账(唯一路径:人工)
admin/token-orders.php:159-196:
| 动作 | 代码位置 | 行为 |
|---|---|---|
| 通过 | :166-187 | 事务内 addTokens(user_id, token_amount) → UPDATE token_orders SET status=1 → returnExpiredTokens(),异常整体回滚 |
| 拒绝 | :188-193 | UPDATE token_orders SET status=2, admin_note=? |
addTokens()(includes/token-functions.php:135-148)为 UPDATE users SET token_balance = COALESCE(token_balance,0) + ?。
- 另一处状态相关写入:
includes/token-functions.php:698附近的过期额度返还(returnExpiredTokens()),与支付渠道无关。
关键缺口(本节结论的支撑)
- 无异步回调端点、无主动查单、无退款、无渠道流水号(
token_orders无trade_no类字段)、无与渠道账单的核对。 - 现有"对账"仅
admin/token-dashboard.php的站内 Token 账目恒等式(消耗/充值/余额自洽),与渠道账单完全脱离——即"用户说付了但没付"这类问题在系统层面无法被自动发现。 admin/token-orders.php依赖管理员肉眼识别截图。而截图经过processIconUpload()处理后已被裁剪(见 1.4 / P-02)。
1.2 数据模型现状
实测表结构(SHOW COLUMNS):
token_orders(11 列)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int unsigned PK | |
| user_id | int unsigned, MUL | |
| package_id | int unsigned, 默认 0 | 0 = 自定义充值 |
| token_amount | decimal(10,2) | 服务端推导 |
| price | decimal(10,2) | 单位:元 |
| pay_method | varchar(30) | alipay / wechat |
| pay_proof | varchar(500) | 截图相对路径 |
| status | tinyint(1), MUL | 0 待审 / 1 通过 / 2 拒绝 |
| admin_note | varchar(255) | |
| created_at / updated_at | timestamp |
status = 1同时表示"已到账",没有"已支付待入账"这一中间态——这是引入网关后必须拆分的关键点。
token_packages(8 列):id / name / token_amount / price / sort_order / status / created_at / updated_at。
settings:key_name VARCHAR(60) PRIMARY KEY,value TEXT;实测 54 项。约束:新增配置键名必须 ≤ 60 字符,且沿用现有命名前缀风格(pay_qr_*、token_*)。
1.3 环境与基础设施事实(实测)
| 项 | 实测值 | 对方案的影响 |
|---|---|---|
| PHP | 8.2.33 CLI/FPM | 可用 random_bytes、严格类型、Throwable |
| OpenSSL | 1.1.1k(FIPS) | RSA2(SHA256withRSA)与 AES-256-GCM 均可原生实现 |
| 扩展 | openssl=1、curl=1、bcmath=1、sodium=1、gd=1、gmp=0 | 微信 v3 不需要 gmp;支付宝 RSA2 不需要 gmp |
| TLS 信任链 | curl.cainfo = /etc/pki/tls/certs/ca-bundle.crt(已配置) | 出网调用默认开启证书校验,无需额外改动 |
| MySQL | 5.7.40 | 无 JSON_TABLE;JSON 类型可用但本项目统一用 TEXT 存原始报文 |
| composer / vendor | 均不存在 | 官方 SDK(alipay-sdk-php、wechatpay-php)无法直接安装,见 5.4 |
| nginx vhost | /www/server/panel/vhost/nginx/39.96.90.85.conf:listen 80; server_name 39.96.90.85; | 纯 HTTP、无域名、无 ssl_certificate |
| 计划任务 | 仅 cron-token-expiry.php | 无任何支付相关定时任务 |
settings.site_url | 空字符串 | 生成绝对 notify_url 无现成配置可用,需新增 |
config/generated.php | 0644 www:www,常量:DB_*、GO_API_KEY | 权限偏松;支付私钥不得存放于此 |
1.4 已就绪、可直接复用的资产
| 资产 | 位置 | 用途 |
|---|---|---|
数据库封装(fetch/fetchAll/insert/execute/fetchColumn/beginTransaction) | includes/db.php | 全部新代码走同一入口 |
| 行级锁先例 | includes/token-functions.php:81、:580(SELECT ... FOR UPDATE) | 入账/退款加锁沿用 |
| 惰性生成并持久化密钥的先例 | getStreamSecret()(includes/token-functions.php:340-354) | 站点级密钥自举模式 |
| 文件级密钥 + AES-256-CBC 的加密先例 | includes/NumberPool.php:58-95(data/.pool_key,0600) | 支付私钥/证书落盘权限的既定基线 |
| 邮件告警通道 | includes/Mailer.php(smtp_* 配置,当前未配置) | 告警出口(需先配 SMTP) |
| 面板计划任务 | 宝塔 → 计划任务 | cron 补偿/对账 |
1.5 待实测项(本方案中标注为「未验证」)
| 编号 | 未验证内容 | 原因 |
|---|---|---|
| U-1 | 后台表单实际提交与前台登录态渲染 | 无管理凭据、环境无浏览器 |
| U-2 | 真实浏览器中收款码/二维码渲染效果 | 同上 |
| U-3 | 第三方渠道侧"金额是否真被固定"、回调报文真实结构 | 无渠道账号,无法发起真实交易 |
| U-4 | 现有短信/邮件告警是否可用 | smtp_* 未配置 |
二、问题清单(分级)
级别定义:P0 = 造成资金/数据错误或安全风险,需优先修复;P1 = 影响可用性、对账或人工成本;P2 = 体验与工程规范。
| 编号 | 级别 | 问题 | 证据 | 影响 |
|---|---|---|---|---|
| P-01 | P0 | 无渠道回调、无主动查单,到账完全依赖人工 | 全站关键字检索为 0;admin/token-orders.php:166-194 | 用户已付款但长期不到账;无法自动发现"付款未上报";人工成本随订单量线性增长 |
| P-02 | P0 | 支付凭证被中心裁剪为 48×48 方图,管理员无法辨识金额/时间/收款方 | token-recharge.php:60 → includes/fn-upload.php:32-73,第 67 行 resizeIconToSquare($file['tmp_name'], $filepath, 48);resizeIconToSquare()(:263-290)为 max-ratio 中心裁剪,实测 extension_loaded('gd') === true,故裁剪路径必然生效 | 审核依据不可用 → 误判通过/拒绝,直接的资金风险 |
| P-03 | P0 | 凭证大小上限 512KB、格式仅 jpg/png/gif/webp | includes/fn-upload.php:34-35 | 手机截图常 > 1MB、iPhone 默认 HEIC → 用户上传失败,被迫改用无凭证下单 |
| P-04 | P0 | 审核动作存在"读后写"竞态,可重复到账 | admin/token-orders.php:163 先读 status != 0 判断,:171-183 才在事务内改状态;事务未锁定订单行(无 FOR UPDATE),并发两次通过请求可各自读到 status=0 | 同一订单重复 addTokens() → 重复发币 |
| P-05 | P1 | 无幂等键、无渠道流水号,无法与渠道对账 | token_orders 无 trade_no/out_trade_no 列 | 出现争议时无法举证;对账只能靠人工核银行流水 |
| P-06 | P1 | 无退款能力 | 无任何退款代码 | 用户投诉只能手工改余额,无审计轨迹、无渠道侧退款 |
| P-07 | P1 | 无监控与告警:下单失败、待审积压、到账异常均无通知 | 仅 admin/token-dashboard.php 静态展示 | 故障被发现的时间 = 用户来投诉的时间 |
| P-08 | P1 | 无渠道账单核对(T+1) | 无对账代码、无对账表 | 渠道侧差异(漏单/多单/金额不符)无法闭环 |
| P-09 | P1 | pay_method 服务端未做白名单校验 | token-recharge.php:55、:93 仅判空串 | 可写入任意字符串,污染统计与后续渠道映射 |
| P-10 | P1 | 站点纯 HTTP、无域名、无备案 → 微信支付 v3 无法接收回调 | nginx vhost listen 80; server_name 39.96.90.85; | 微信通道硬阻断;支付宝当面付虽可 http 回调,但生产环境强烈建议 HTTPS,且无域名不利于资质审核 |
| P-11 | P1 | 现有收款方式为个人收款码,不可用于经营性收款 | settings.pay_qr_alipay / pay_qr_wechat 及套餐专用码(BUG-2026-0030) | 合规风险:资金性质属个人转账,无商户凭证、无法开票、易触发账户风控 |
| P-12 | P2 | 敏感配置以明文存 settings | stream_secret 等明文入库;getSetting() 无加密层 | 一旦库被读走即全部泄露;支付私钥不得沿用此模式 |
| P-13 | P2 | 后台展示凭证为原文件名路径,无访问控制 | pay_proof varchar(500) 指向 uploads/proofs/(Web 可直接访问) | 凭证(含姓名/账号)可被枚举下载 |
| P-14 | P2 | 无操作审计:谁在何时通过了哪笔订单无独立日志 | 仅有 admin_note | 内部风险不可追溯 |
| P-15 | P2 | uploads/proofs/ 目录当前不存在(实测 0 文件) | ls uploads/proofs 为空 | 说明凭证功能实际上极少被使用或用户上传即失败,佐证 P-02/P-03 |
| P-16 | P1 | docs/ 目录整体可被公网匿名读取(含架构、BUG、部署等内部文档) | 实测 curl -H 'Host: 39.96.90.85' http://127.0.0.1/docs/architecture.md → 200、/docs/网站BUG修复.md → 200、/docs/安装配置说明.md → 200;nginx vhost 的反向规则(39.96.90.85.conf:31-40)只按文件名拦截 README.md/composer.json/.env 等,未覆盖 docs/ 目录 | 内部架构与实现细节(含本方案)暴露;本方案文档中已刻意不含任何凭据,但设计与风控阈值属内部信息 |
说明:P-02 在本轮被重新实证确认(此前为待复核项)。结论:
gd已加载 →resizeIconToSquare(..., 48)必然执行 → 支付凭证被裁成 48×48 中心方块。
三、优化目标与范围
3.1 目标(可量化)
| 目标 | 指标 | 现状 | 目标值 |
|---|---|---|---|
| 提升成功率 | 支付发起→渠道成功转化率 | 不可测 | ≥ 99%(灰度期沙箱/小额实测),失败可归因 |
| 提升稳定性 | 回调丢失后自动补入账时长 | 人工,无上限 | ≤ 5 分钟(cron 周期 1 分钟 + 退避查单) |
| 提升安全性 | 密钥泄露面 | 若照现行做法将明文入库 | 私钥/证书落盘 0600、不入库、不进日志、不进响应 |
| 提升合规性 | 收款凭证可举证率 | 0% | 100%(渠道流水号 + 原始报文留存 ≥ 3 年) |
| 提升对账效率 | T+1 对账人工工时 | 全人工 | 自动对账,人工只处理差异(目标差异率 ≤ 0.1%,差异全量落表) |
| 降低失败 | 用户"付了没到账"工单 | 不可测 | 降为 0(自动到账 + 超时关单 + 主动查单) |
| 降低重复扣款 | 重复入账笔数 | 存在竞态风险(P-04) | 0(三层幂等 + 行锁,压测验证) |
| 降低回调丢失 | 回调最终成功率 | 无回调 | ≥ 99.99%(渠道重试 + 我方查询补偿双保险) |
| 降低人工成本 | 单笔到账人工操作次数 | 1 次审核 | 0(仅异常转人工) |
3.2 方案范围
包含:审计与问题清单;目标架构与模块划分;数据模型(4 张新表);订单状态机与合法迁移;三层幂等;重试与补偿;日志/监控/告警;灰度与回滚;官方接口接入要点(支付宝当面付、微信支付 v3 Native);订单号与金额规则;接口清单;请求/响应示例;错误码;安全合规要点;测试用例;验收标准;上线与监控;实施路线图。
不包含(明确不做):
- ❌ 任何非官方、破解、绕过风控的方案:聚合个人码、伪造支付回调、"免签约/免资质"通道、修改渠道 SDK 校验、关闭验签、
verify=false。 - ❌ 本项目内不实施代码改动(本轮仅交付方案;实施需另行确认)。
- ❌ 不修改面板核心、不触碰授权机制(
data/.license_warnings必须保持count:10、属主www:www)。 - ❌ 不提升
CMS_VERSION(维持 v0.3.81)。 - ❌ 不引入新依赖(除非用户明确批准,见 5.4)。
- ❌ 不做用户资金托管/钱包化(不改变"余额 = Token 记账"的既有模型)。
3.3 设计原则
- 客户端不可信:金额、Token 数一律服务端按
package_id回查;客户端只允许传package_id与渠道。 - 渠道不可全信:回调必须验签 + 校验金额/商户号/应用 ID + 校验本地订单存在且状态合法。
- 钱账分离:支付事实(渠道)与记账事实(站内 Token)分表,二者通过幂等键关联。
- 只增不删:新表新字段只做增量;旧流程保留,通过开关切换。
- 一切可回退:关闭开关即回到"人工收款码 + 人工审核",无需数据回滚。
四、目标架构
4.1 模块划分与目录
includes/payment/
├── PaymentGateway.php 接口定义:createOrder/queryOrder/closeOrder/refund/verifyNotify/ackNotify
├── AlipayFaceToFaceGateway.php 支付宝当面付实现(RSA2 签名/验签)
├── WechatNativeGateway.php 微信支付 v3 Native 实现(SHA256-RSA2048 + AES-256-GCM)
├── PaymentService.php 编排层:下单/入账/退款/关单(事务、幂等、状态机唯一入口)
├── PaymentStore.php 数据访问:payment_orders/notifies/refunds/reconcile
├── Signer.php 渠道无关的签名/验签原语(RSA2、SHA256withRSA、AES-256-GCM)
├── Certs.php 密钥与证书装载(落盘读取、序列号解析、轮换校验)
└── Ledger.php 站内记账:调用 addTokens()、写 token_orders(status=1)、审计
payment-create.php 下单(登录 + CSRF)
payment-status.php 状态轮询(登录 + 仅限本人订单)
payment-page.php 收银台(展示二维码/跳转,轮询状态)
payment-notify-alipay.php 支付宝异步通知(无登录,验签后处理)
payment-notify-wechat.php 微信 v3 异步通知(无登录,验签+解密后处理)
payment-return-alipay.php 支付宝同步跳转回执(仅做展示,不作为入账依据)
admin/payment-orders.php 管理端:订单列表/手动补单/手动关单/查看原始报文
admin/payment-refund.php 管理端:发起退款(requireAdmin + CSRF + 限额 + 二次确认)
admin/payment-proof.php 管理端:受控输出支付凭证(替代 Web 直读)
cron-payment-reconcile.php 查单补偿 + T+1 账单对账 + 告警
cron-payment-sweep.php 超时关单 + 长期未决订单转人工
分层约束:PaymentService 是唯一允许修改 payment_orders.status 与调用 Ledger 的入口;admin/* 与 cron-* 只能调用 PaymentService;网关实现类不得直接写库。
4.2 数据模型(新表 DDL)
字符集
utf8mb4/utf8mb4_unicode_ci,引擎 InnoDB,与现网一致。MySQL 5.7 兼容。
-- 支付单:一次"支付尝试"的全部事实
CREATE TABLE `payment_orders` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`out_trade_no` VARCHAR(32) NOT NULL COMMENT '我方订单号,渠道不重复',
`user_id` INT UNSIGNED NOT NULL,
`package_id` INT UNSIGNED NOT NULL DEFAULT 0,
`token_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '下单时快照',
`amount_fen` INT UNSIGNED NOT NULL COMMENT '应付金额(分),避免浮点',
`currency` CHAR(3) NOT NULL DEFAULT 'CNY',
`channel` VARCHAR(16) NOT NULL COMMENT 'alipay / wechat',
`subject` VARCHAR(128) NOT NULL DEFAULT '',
`status` VARCHAR(16) NOT NULL DEFAULT 'INIT'
COMMENT 'INIT/PENDING/PAID/CREDITED/CLOSED/REFUNDING/REFUNDED/PARTIAL_REFUNDED/REVIEW/FAILED',
`channel_trade_no` VARCHAR(64) NOT NULL DEFAULT '',
`token_order_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '入账后生成的 token_orders.id(0=未入账)',
`code_url` TEXT NULL COMMENT '渠道返回的二维码内容',
`client_ip` VARCHAR(45) NOT NULL DEFAULT '',
`time_expire` DATETIME NULL,
`paid_at` DATETIME NULL,
`credited_at` DATETIME NULL,
`closed_at` DATETIME NULL,
`refunded_fen` INT UNSIGNED NOT NULL DEFAULT 0,
`error_code` VARCHAR(32) NOT NULL DEFAULT '',
`error_msg` VARCHAR(255) NOT NULL DEFAULT '',
`attach` TEXT NULL COMMENT 'JSON:下单上下文(套餐名等)',
`channel_raw` TEXT NULL COMMENT 'JSON:最近一次渠道应答(脱敏)',
`reconcile_state` VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT 'NONE/MATCH/DIFF',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_out_trade_no` (`out_trade_no`),
KEY `idx_status_created` (`status`, `created_at`),
KEY `idx_user` (`user_id`),
KEY `idx_channel_trade` (`channel`, `channel_trade_no`),
KEY `idx_token_order` (`token_order_id`),
KEY `idx_paid` (`paid_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 渠道通知/查单原始报文(审计与幂等依据)
CREATE TABLE `payment_notifies` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`channel` VARCHAR(16) NOT NULL,
`source` VARCHAR(16) NOT NULL COMMENT 'notify / query / refund_notify',
`notify_key` VARCHAR(128) NOT NULL COMMENT '幂等键:渠道通知ID 或 交易号+事件类型',
`out_trade_no` VARCHAR(32) NOT NULL DEFAULT '',
`channel_trade_no` VARCHAR(64) NOT NULL DEFAULT '',
`sign_verified` TINYINT(1) NOT NULL DEFAULT 0,
`payload` TEXT NULL COMMENT '原始报文(脱敏)',
`headers` TEXT NULL COMMENT '需要的请求头(脱敏)',
`processed` TINYINT(1) NOT NULL DEFAULT 0,
`process_result` VARCHAR(255) NOT NULL DEFAULT '',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_channel_notifykey` (`channel`, `notify_key`),
KEY `idx_out_trade_no` (`out_trade_no`),
KEY `idx_processed_created` (`processed`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 退款单
CREATE TABLE `payment_refunds` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`out_refund_no` VARCHAR(32) NOT NULL COMMENT '退款请求号(渠道幂等键)',
`out_trade_no` VARCHAR(32) NOT NULL,
`channel` VARCHAR(16) NOT NULL,
`refund_amount_fen` INT UNSIGNED NOT NULL,
`reason` VARCHAR(128) NOT NULL DEFAULT '',
`status` VARCHAR(16) NOT NULL DEFAULT 'INIT'
COMMENT 'INIT/PENDING/SUCCESS/FAILED/CLOSED',
`channel_refund_id` VARCHAR(64) NOT NULL DEFAULT '',
`operator_admin_id` INT UNSIGNED NOT NULL DEFAULT 0,
`channel_raw` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_out_refund_no` (`out_refund_no`),
KEY `idx_out_trade_no` (`out_trade_no`),
KEY `idx_status_created` (`status`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- T+1 对账结果
CREATE TABLE `payment_reconcile` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`bill_date` DATE NOT NULL,
`channel` VARCHAR(16) NOT NULL,
`bill_count` INT NOT NULL DEFAULT 0,
`bill_amount_fen` BIGINT NOT NULL DEFAULT 0,
`local_count` INT NOT NULL DEFAULT 0,
`local_amount_fen` BIGINT NOT NULL DEFAULT 0,
`diff_count` INT NOT NULL DEFAULT 0,
`diff_detail` TEXT NULL COMMENT 'JSON:{only_channel:[],only_local:[],amount_mismatch:[]}',
`file_hash_ok` TINYINT(1) NOT NULL DEFAULT 0,
`status` VARCHAR(16) NOT NULL DEFAULT 'OK' COMMENT 'OK/DIFF/FAILED',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_date_channel` (`bill_date`, `channel`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
关于 token_orders:本方案不做结构变更。
理由(最小改动原则):
- 渠道到账时直接创建
token_orders行并置status=1、admin_note='渠道自动到账',同事务内addTokens()——人工审核队列不会被未支付订单污染。 - 两者的关联通过
payment_orders.token_order_id反向查询/JOIN实现。
若用户要求双向可见,再追加一列
token_orders.out_trade_no VARCHAR(32) DEFAULT ''(可空、只增)。此项列为待确认(见第十四节)。
4.3 调用链路
A. 下单(用户 → 渠道)
用户选择套餐
→ POST /payment-create.php (登录校验 + CSRF + 频控)
→ PaymentService::create()
1. 校验 package_id 存在且 status=1;服务端取 price/token_amount 快照
2. 生成 out_trade_no(规则见 5.3),INSERT payment_orders(status=INIT)
3. 调 Gateway::createOrder() → 渠道返回 code_url
4. UPDATE status=PENDING, code_url=..., time_expire=now()+15min
→ 返回 {out_trade_no, code_url, amount_fen}
→ 跳转 /payment-page.php?out_trade_no=xxx 展示二维码
B. 支付成功(渠道 → 我方,主路径)
渠道异步通知
→ POST /payment-notify-{channel}.php
→ 1) 验签(RSA2 / SHA256withRSA)
2) 幂等占位:INSERT payment_notifies(uk: channel+notify_key),
重复 → 直接按渠道格式应答"成功"并结束(不再入账)
3) 校验:app_id/mchid 匹配、金额与 payment_orders.amount_fen 一致、
本地订单存在且 status ∈ {INIT,PENDING}
4) PaymentService::markPaid()
BEGIN
SELECT * FROM payment_orders WHERE out_trade_no=? FOR UPDATE
UPDATE payment_orders SET status='PAID', channel_trade_no=?, paid_at=?
WHERE id=? AND status IN ('INIT','PENDING')
Ledger::credit() ← 同事务内:INSERT token_orders(status=1) + addTokens()
UPDATE payment_orders SET status='CREDITED', credited_at=NOW() WHERE id=?
COMMIT
5) 应答:支付宝返回纯文本 `success`;微信返回 200 + {"code":"SUCCESS","message":"成功"}
C. 回调丢失(渠道 → 我方,补偿路径)
cron-payment-reconcile.php(每分钟)
→ 取 status=PENDING 且 created_at < now()-2min 的订单
→ Gateway::queryOrder()
渠道=已支付 → 复用 B 的 markPaid()(同一幂等键,天然安全)
渠道=已关闭 → status=CLOSED
渠道=未知/异常 → 记录失败次数,指数退避(2m→5m→15m→30m),
超过 24h 仍未知 → status=REVIEW + 告警
D. 超时关单
cron-payment-sweep.php(每分钟)
→ status=PENDING 且 time_expire < now() 的订单
→ Gateway::closeOrder()
成功 → status=CLOSED
渠道报"订单已支付" → 立刻走查询补偿(先付款后关单的竞态)
E. 退款(仅管理员)
POST /admin/payment-refund.php(requireAdmin + CSRF + 二次确认 + 单笔/日限额)
→ 校验 out_trade_no 状态 ∈ {PAID, CREDITED}、refund_amount ≤ amount_fen - refunded_fen
→ 生成 out_refund_no(渠道幂等键),INSERT payment_refunds(status=INIT)
→ Gateway::refund() → status=PENDING, 记 channel_refund_id
→ 退款通知/查询确认 → status=SUCCESS
→ 同事务:扣减 users.token_balance(Ledger::debit),
payment_orders.refunded_fen += refund_amount_fen,
全额则 status=REFUNDED,部分则 PARTIAL_REFUNDED
→ 写 admin 审计日志
F. T+1 对账
cron-payment-reconcile.php(每日,建议 09:30)
→ 支付宝:alipay.data.dataservice.bill.downloadurl.query 取账单下载地址 → 下载 CSV/ZIP
→ 微信:GET /v3/bill/tradebill?bill_date=D&bill_type=ALL
→ 校验下载文件 SHA1(渠道返回 hash)→ 解压 CSV(gzip)
→ 与本库 payment_orders(status ∈ PAID/CREDITED/REFUND*)逐笔比对
→ 差异全量写 payment_reconcile.diff_detail,diff_count>0 即告警
4.4 状态机与合法迁移表
┌──────── 下单失败 ────────┐
▼ │
[INIT] ──渠道下单成功──▶ [PENDING] ──渠道确认支付──▶ [PAID] ──入账成功──▶ [CREDITED]
│ │ │ │
│ │ └──超时/关单──▶ [CLOSED] │
│ └──异常──▶ [REVIEW] ──人工──▶ [CLOSED]/[CREDITED] │
└──渠道拒绝/参数错──▶ [FAILED] │
│
[CREDITED] ──全额退款──▶ [REFUNDED] ◀──[REFUNDING] ◀──发起退款───────────────┘
──部分退款──▶ [PARTIAL_REFUNDED]
合法迁移表(PaymentService 强制校验,非法迁移一律抛 PAY_1014/PAY_1006)
| 当前态 | 允许迁移到 | 触发者 | 备注 |
|---|---|---|---|
| INIT | PENDING / FAILED | 下单 | 立即迁移,INIT 不对外可见 |
| PENDING | PAID / CLOSED / REVIEW | 回调 / 查单 / 关单 cron | |
| PENDING | PENDING | 重复回调 | 幂等,不产生迁移 |
| PAID | CREDITED | 入账 | 必须与 addTokens 同事务 |
| PAID | REVIEW | 金额不符等 | 人工介入前不得入账 |
| CLOSED | PAID / CREDITED | 回调/查单(晚到) | 必须支持:先付款后关单的竞态 |
| CREDITED | REFUNDING | 管理员退款 | 需权限与限额校验 |
| REFUNDING | REFUNDED / PARTIAL_REFUNDED / REVIEW | 退款结果 | |
| REFUNDED | — | — | 终态 |
| REVIEW | CREDITED / CLOSED / FAILED | 管理员 | 需写审计 |
| FAILED | — | — | 终态(可新建单重试) |
关键约束:
PAID → CREDITED必须满足credited_at IS NULL,用条件更新保证影响行数 = 1。CLOSED可回到PAID:这是唯一允许"逆向"的迁移,因为渠道的支付事实优先于我方关单动作。REFUNDED后禁止再次退款(refunded_fen >= amount_fen即拒绝)。
4.5 三层幂等
| 层 | 机制 | 实现 | 防的问题 |
|---|---|---|---|
| L1 创建层 | 唯一键 payment_orders.uk_out_trade_no + 前端一次性 request_id(存 attach,10 分钟内重复请求返回同一单) | INSERT 捕获重复键 → 返回已有单 | 用户连点、刷新重复下单 |
| L2 通知层 | 唯一键 payment_notifies.uk_channel_notifykey | INSERT IGNORE 失败即判定重复 → 立即返回渠道"成功应答"、不再进入业务 | 渠道按 15s/15s/30s… 重试;网络重放 |
| L3 入账层 | UPDATE payment_orders SET status='CREDITED' WHERE id=? AND status='PAID' AND credited_at IS NULL,影响行数=1 才执行 addTokens();且整段在 SELECT ... FOR UPDATE 事务内 | PaymentService::markPaid() | 并发回调同时到达(两家渠道都会重试)、cron 查单与回调同时触发 |
L3 是唯一能产生
addTokens()的路径。人工审核路径(admin/token-orders.php)同时加固为UPDATE ... WHERE id=? AND status=0校验影响行数(修复 P-04)。
4.6 重试与补偿
| 场景 | 策略 | 上限 |
|---|---|---|
| 下单调用渠道超时/5xx | 立即重试 1 次(同一 out_trade_no,绝不换号) | 1 次,失败则 FAILED,提示用户重试 |
| 回调处理内部异常 | 返回渠道失败应答,让渠道重试(支付宝 25h 内 4m,10m,10m,1h,2h,6h,15h;微信 15s→…→6h,共 24h) | 渠道重试 + cron 查单双保险 |
| 回调丢失 | cron 查单补偿,指数退避 2m→5m→15m→30m | 24h 后转 REVIEW + 告警 |
| 关单失败 | 重试 3 次,间隔 1m | 3 次后保持 PENDING,交查单处理 |
| 退款结果未知 | 用同一 out_refund_no 重试(渠道幂等)或查询退款单 | 24h 后转 REVIEW |
| 对账失败 | 次轮重跑(uk(date,channel) 保证不重复插入) | 连续 3 天失败告警 |
补偿原则:所有对外调用使用幂等键(下单用 out_trade_no、退款用 out_refund_no、通知用 notify_key),因此"重试"在任何时候都安全。
4.7 日志、监控与告警
日志分级
| 级别 | 内容 | 去向 |
|---|---|---|
| INFO | 下单/支付/入账/退款/关单的状态迁移(含 out_trade_no、金额分、耗时 ms) | error_log(),前缀 [Payment] |
| WARN | 验签失败、金额不符、渠道返回非预期状态、重复回调被忽略 | error_log(),前缀 [Payment][WARN] |
| ERROR | 入账事务失败、渠道调用异常、对账差异 | error_log() + 触发告警 |
| AUDIT | 原始报文、管理员退款操作 | payment_notifies.payload / 独立审计写入 |
脱敏要求:日志与 payload 中不得出现私钥、APIv3 密钥、证书内容、完整 buyer_logon_id/openid(保留前 4 后 4)、手机号、真实姓名。
监控指标(4 项必备)
| 指标 | 计算方式 | 告警阈值 |
|---|---|---|
| 下单失败率 | status=FAILED / 总下单数(5 分钟窗口) | > 5% 连续 3 窗口 |
| 回调失败率 | payment_notifies.processed=0 / 总回调数(5 分钟) | > 1% |
| 待入账订单数 | status='PAID' AND credited_at IS NULL 且 paid_at < now()-10min | ≥ 1 |
| 对账差异 | payment_reconcile.diff_count | > 0 |
| 待决订单数 | status='REVIEW' | ≥ 1 |
告警通道:优先复用 includes/Mailer.php(需先配置 smtp_*,当前为空——见 U-4);同时写入宝塔面板通知。告警需含 out_trade_no 与运维动作建议。
4.8 灰度发布与回滚
灰度开关:新增配置键(≤ 60 字符)
| 键名 | 取值 | 说明 |
|---|---|---|
payment_gateway_enabled | 0 / 1 | 总开关,0 即完全回退到人工收款码 |
payment_channels | alipay,wechat | 启用的渠道白名单 |
payment_gray_users | 1,2,3 | 灰度用户 ID 白名单 |
payment_gray_percent | 0 / 5 / 100 | 按用户 ID 取模的灰度比例 |
payment_site_base_url | https://pay.example.com | 生成 notify_url 的基址(必须 HTTPS) |
灰度阶段
- 阶段 0(单测/沙箱):
payment_gateway_enabled=1但payment_channels="",仅跑单元与沙箱测试。 - 阶段 1(内部):
payment_gray_users加入 2~3 个内部账号,金额限制为最低套餐,人工核对每笔。 - 阶段 2(5%):
payment_gray_percent=5,观察 3 天:回调成功率、入账时延、告警数。 - 阶段 3(全量):
payment_gray_percent=100;人工收款码入口保留但降级为折叠项(老用户/异常兜底)。
回滚
| 层级 | 动作 | 生效时间 | 数据影响 |
|---|---|---|---|
| L1 功能回滚 | payment_gateway_enabled=0 | 立即(读 settings,无缓存即生效) | 无(存量 PENDING 单由 cron 继续收敛,或手动关单) |
| L2 渠道回滚 | 从 payment_channels 摘掉某渠道 | 立即 | 该渠道新单不再创建 |
| L3 代码回滚 | 还原新增文件(新增文件均为新文件,不覆盖既有文件→回滚即删除) | 分钟级 | 无 |
| L4 数据回滚 | 不需要(只增不删;已入账 Token 不做自动回收,人工核账) | — | — |
五、官方接口接入要点
以下均为官方开放接口,按官方文档实现签名/验签;禁止任何绕过风控或非官方通道的做法。
5.1 支付宝当面付(F2F)
- 网关:
https://openapi.alipay.com/gateway.do - 签名算法:
sign_type=RSA2(SHA256withRSA),应用私钥(PKCS8 PEM)签名,支付宝公钥验签。 - 签名串构造(请求):除
sign外全部请求参数,按参数名 ASCII 升序,k=v以&连接,值为未 URL 编码的原始值;无值参数不参与。 - 公共参数:
app_id、method、format=JSON、charset=utf-8、sign_type=RSA2、timestamp(Y-m-d H:i:s,Asia/Shanghai)、version=1.0、notify_url、biz_content(JSON 字符串)、sign。 - 接口清单
| 用途 | method | 关键 biz_content | 关键返回 |
|---|---|---|---|
| 下单(生成二维码) | alipay.trade.precreate | out_trade_no、total_amount(元,两位小数)、subject、timeout_express(如 15m) | code=10000、qr_code |
| 查单 | alipay.trade.query | out_trade_no 或 trade_no | trade_status:WAIT_BUYER_PAY / TRADE_SUCCESS / TRADE_FINISHED / TRADE_CLOSED |
| 关单 | alipay.trade.close | out_trade_no 或 trade_no | code=10000 |
| 退款 | alipay.trade.refund | out_trade_no、refund_amount、out_request_no(幂等键,部分退款必传)、refund_reason | code=10000、fund_change |
| 撤销 | alipay.trade.cancel | out_trade_no | 仅当面付/条码支付,当日未结算时使用 |
| 对账单下载 | alipay.data.dataservice.bill.downloadurl.query | bill_type=trade、bill_date | 账单 ZIP 下载地址 |
- 应答验签:
sign为顶层字段,被签名内容是响应中的业务节点(如alipay_trade_precreate_response)的原始 JSON 片段。必须先验签再解析业务字段。 - 异步通知(
notify_url):application/x-www-form-urlencodedPOST。校验顺序:
- 用支付宝公钥对通知参数(剔除
sign、sign_type,值为解码后的明文)按 ASCII 排序拼串验签; - 校验
app_id与本应用一致; - 校验
seller_id与签约商户一致; - 校验
total_amount与本地payment_orders.amount_fen(元)一致; - 校验
out_trade_no在本地存在; - 仅在
trade_status ∈ {TRADE_SUCCESS, TRADE_FINISHED}时入账。
- 应答格式:纯文本
success(无 BOM、无 HTML、无 JSON 包裹、无多余空白),否则支付宝将持续重试。 - 注意:同一
out_trade_no可能收到多次TRADE_SUCCESS(业务方可能多次 notify),必须幂等(L2/L3)。
5.2 微信支付 v3(Native 扫码)
- 域名:
https://api.mch.weixin.qq.com - 请求签名(
Authorization: WECHATPAY2-SHA256-RSA2048)
``` mchid="1900000001",nonce_str="...",signature="...",timestamp="...",serial_no="..." ```
签名串(每行以 \n 结尾,共 5 行):
``` HTTP方法\n URL(含 query,如 /v3/pay/transactions/native)\n 时间戳(秒)\n 随机串\n 请求体原文\n ```
算法:RSA-SHA256 + 商户 API 私钥(apiclient_key.pem,PKCS1v15);serial_no = 商户 API 证书序列号。
- 应答验签:用微信支付平台证书公钥(或平台公钥)验证,签名串 =
时间戳\n随机串\n响应体\n,请求头Wechatpay-Timestamp/Wechatpay-Nonce/Wechatpay-Signature/Wechatpay-Serial。必须验签后才解析业务数据。 - 接口清单
| 用途 | 方法与路径 | 关键字段 |
|---|---|---|
| 下单 | POST /v3/pay/transactions/native | appid、mchid、description、out_trade_no、notify_url、amount:{total(分),currency:"CNY"}、time_expire(RFC3339)→ 返回 code_url |
| 查单 | GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid=xxx | trade_state:SUCCESS / NOTPAY / CLOSED / REFUND / USERPAYING / PAYERROR |
| 关单 | POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close | body {mchid} |
| 申请退款 | POST /v3/refund/domestic/refunds | out_trade_no、out_refund_no、reason、notify_url、amount:{refund,total,currency} |
| 查询退款 | GET /v3/refund/domestic/refunds/{out_refund_no} | status:SUCCESS / PROCESSING / CLOSED / ABNORMAL |
| 对账单 | GET /v3/bill/tradebill?bill_date=YYYY-MM-DD&bill_type=ALL | 返回 download_url + hash(SHA1) |
| 平台证书 | GET /v3/certificates | AES-256-GCM 加密的证书列表(含 serial_no) |
- 回调处理:
- 验签(同上,用平台证书/平台公钥);
- 解密
resource:
``` algorithm = AEAD_AES_256_GCM key = APIv3 密钥(32 字节) iv = resource.nonce(12 字节) aad = resource.associated_data plain = AES-256-GCM-decrypt(resource.ciphertext, key, iv, aad, tag[末尾16字节]) ```
- 校验
event_type == "TRANSACTION.SUCCESS"、明文中的appid/mchid/out_trade_no/amount.total(分); - 入账(复用 L2/L3 幂等)。
- 应答格式:HTTP
200,body{"code":"SUCCESS","message":"成功"};失败返回4xx/5xx+{"code":"FAIL","message":"..."}。渠道重试节奏:15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h,累计约 24 小时(以官方文档为准)。 - 证书轮换:平台证书有效期约 5 年但微信会不定期更新;建议使用平台公钥模式(公钥 ID)以简化轮换;若用平台证书,
cron-payment-reconcile.php每日检查serial_no是否变化并告警(PAY_1016)。 - ⚠️ 硬约束:
notify_url必须为 HTTPS(当前站点为http://39.96.90.85)→ 微信通道在取得域名与证书前无法上线(P-10)。
5.3 金额与订单号规则
订单号 out_trade_no(同时满足两渠道最严约束:微信 6~32 字符、仅数字/大小写字母/_-|*@;支付宝 ≤ 64 字符、字母数字下划线)
T + yyMMddHHmmss + 4位随机(字母数字) + 3位自增
示例:T260930143005K7Q0021
长度:1 + 12 + 4 + 3 = 20 字符
- 前缀固定
T(不使用下划线,避免日志/CSV 转义问题); - 唯一性由
uk_out_trade_no兜底,冲突则重新生成(最多 3 次); - 退款单号同规则,前缀改
R。
金额
| 层 | 单位 | 类型 | 说明 |
|---|---|---|---|
| 数据库 | 分 | INT UNSIGNED | payment_orders.amount_fen / refunded_fen,避免浮点误差 |
| 支付宝 | 元(两位小数,字符串) | number_format($fen/100, 2, '.', '') | 如 9900 → "99.00" |
| 微信 | 分(整数) | 直接使用 | total: 9900 |
站内 token_orders.price | 元 | decimal(10,2) | 写回时反向换算 |
校验规则:amount_fen > 0;回调金额与本地金额必须完全相等(不等 → PAY_1004 → 转 REVIEW,绝不入账);退款金额 ≤ amount_fen - refunded_fen。
5.4 官方 SDK 依赖策略(当前无 composer)
站点不存在 composer.json 与 vendor/。两条路径:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| A. 自实现协议(推荐,零依赖) | 按官方文档用 ext-openssl + ext-curl 实现签名/验签/AES-GCM,代码量约 400~600 行 | 不新增依赖、可控、可审计、无版本冲突 | 需自写测试,轮换逻辑需自行维护 |
| B. 引入官方 SDK | 需先安装 composer(或手工放置 SDK 压缩包到 vendor/),并写一个极简 PSR-4 autoloader | 官方维护、边界情况少 | 属新增依赖,需用户明确批准;阿里/微信 SDK 均依赖 PSR 组件(psr/http-message、guzzle 等),体积大 |
本方案默认按 方案 A 设计(不引入任何依赖)。若用户批准方案 B,需在实施前单独确认安装范围与来源。
六、接口清单
6.1 站内 HTTP 接口
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
/payment-create.php | POST | 登录 + CSRF + 频控(10 次/分钟/用户) | 下单,返回 out_trade_no 与 code_url |
/payment-page.php | GET | 登录(仅本人订单) | 收银台,展示二维码 + 轮询状态 |
/payment-status.php | GET | 登录(仅本人订单) | 轮询返回 {status, credited, token_amount} |
/payment-notify-alipay.php | POST | 验签 | 支付宝异步通知,应答纯文本 success |
/payment-notify-wechat.php | POST | 验签 + 解密 | 微信 v3 通知,应答 {"code":"SUCCESS"} |
/payment-return-alipay.php | GET | 无 | 同步跳转展示页,不作为入账依据 |
/admin/payment-orders.php | GET/POST | requireAdmin() + CSRF | 订单列表 / 手动补单 / 手动关单 / 查看原始报文 |
/admin/payment-refund.php | POST | requireAdmin() + CSRF + 二次确认 + 限额 | 发起退款 |
/admin/payment-proof.php | GET | requireAdmin() | 受控输出凭证(替代 Web 直读 uploads/proofs/) |
6.2 定时任务
| 脚本 | 建议频率 | 职责 |
|---|---|---|
cron-payment-reconcile.php --query | 每分钟 | PENDING 单查单补偿 |
cron-payment-reconcile.php --bill | 每日 09:30 | T+1 账单对账 |
cron-payment-sweep.php | 每分钟 | 超时关单 + REVIEW 提醒 |
cron-payment-reconcile.php --cert | 每日 10:00 | 微信平台证书序列号变化检查 |
均需
cli运行(参考既有cron-token-expiry.php的实现风格),并加文件锁(flock)防止重入。
6.3 外部渠道接口(只读引用,见第五节)
支付宝:alipay.trade.precreate / alipay.trade.query / alipay.trade.close / alipay.trade.refund / alipay.trade.cancel / alipay.data.dataservice.bill.downloadurl.query
微信:/v3/pay/transactions/native / /v3/pay/transactions/out-trade-no/{no} / .../close / /v3/refund/domestic/refunds / /v3/bill/tradebill / /v3/certificates
七、请求/响应示例
7.1 站内:下单
请求
POST /payment-create.php HTTP/1.1
Host: 39.96.90.85
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=***
package_id=2&channel=alipay&csrf_token=***
成功响应
{
"success": true,
"out_trade_no": "T260930143005K7Q0021",
"channel": "alipay",
"amount_fen": 9900,
"amount_text": "99.00",
"token_amount": "1000.00",
"code_url": "https://qr.alipay.com/bax08xxxxxxxxxxxxxxxxxx",
"time_expire": "2026-09-30 14:45:05",
"pay_page": "/payment-page.php?out_trade_no=T260930143005K7Q0021"
}
失败响应
{ "success": false, "error_code": "PAY_1003", "message": "渠道下单失败,请稍后重试" }
7.2 支付宝:异步通知(渠道 → 我方)
请求(form-urlencoded,节选)
notify_time=2026-09-30+14%3A32%3A11
notify_type=trade_status_sync
notify_id=ac05099524730693a8b330c5ecf72da9786
app_id=2021000000000000
out_trade_no=T260930143005K7Q0021
trade_no=2026093022001441911000000001
trade_status=TRADE_SUCCESS
total_amount=99.00
seller_id=2088000000000001
gmt_payment=2026-09-30+14%3A32%3A05
sign_type=RSA2
sign=***
我方应答(必须为纯文本,无 BOM)
success
处理失败时(促发渠道重试)
failure
7.3 微信 v3:下单与回调
下单请求
POST /v3/pay/transactions/native HTTP/1.1
Host: api.mch.weixin.qq.com
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900000001",nonce_str="A1B2C3D4E5",signature="***",timestamp="1759213925",serial_no="4A3B2C1D..."
Content-Type: application/json
Accept: application/json
{
"appid": "wx0000000000000000",
"mchid": "1900000001",
"description": "奇点中文网-Token充值-1000",
"out_trade_no": "T260930143005K7Q0021",
"notify_url": "https://pay.example.com/payment-notify-wechat.php",
"time_expire": "2026-09-30T14:45:05+08:00",
"amount": { "total": 9900, "currency": "CNY" }
}
下单响应
{ "code_url": "weixin://wxpay/bizpayurl?pr=AbCdEfG" }
回调请求体
{
"id": "EV-2026093014320512345678",
"create_time": "2026-09-30T14:32:05+08:00",
"event_type": "TRANSACTION.SUCCESS",
"resource_type": "encrypt-resource",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "***",
"associated_data": "transaction",
"nonce": "Ab1Cd2Ef3Gh"
}
}
解密后明文(节选)
{
"mchid": "1900000001",
"appid": "wx0000000000000000",
"out_trade_no": "T260930143005K7Q0021",
"transaction_id": "4200001234202609301234567890",
"trade_type": "NATIVE",
"trade_state": "SUCCESS",
"success_time": "2026-09-30T14:32:00+08:00",
"amount": { "total": 9900, "payer_total": 9900, "currency": "CNY" }
}
我方应答
{ "code": "SUCCESS", "message": "成功" }
7.4 退款
请求(管理端)
POST /admin/payment-refund.php HTTP/1.1
Host: 39.96.90.85
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=***
out_trade_no=T260930143005K7Q0021&refund_amount=99.00&reason=用户申请&confirm=1&csrf_token=***
响应
{ "success": true, "out_refund_no": "R260930150000X9Z0001", "status": "PENDING", "message": "退款已提交,结果以渠道回调为准" }
八、错误码表
| 错误码 | 常量名 | 含义 | 处置 |
|---|---|---|---|
| PAY_1001 | ERR_CFG_MISSING | 渠道参数/密钥未配置 | 后台提示补齐配置;不创建订单 |
| PAY_1002 | ERR_SIGN_FAILED | 本地签名失败(私钥不可用/格式错) | 告警;订单置 FAILED |
| PAY_1003 | ERR_CHANNEL_CREATE | 渠道下单失败(业务码非成功) | 返回用户可读提示;重试 1 次 |
| PAY_1004 | ERR_AMOUNT_MISMATCH | 回调金额与本地不符 | 不入账,置 REVIEW,告警 |
| PAY_1005 | ERR_ORDER_NOT_FOUND | 本地订单不存在 | 不入账,记录原始报文 |
| PAY_1006 | ERR_STATUS_INVALID | 状态不允许该操作 | 返回提示;写审计 |
| PAY_1007 | DUPLICATE_NOTIFY | 重复回调(幂等忽略) | 按渠道格式应答成功,不重复入账 |
| PAY_1008 | ERR_VERIFY_FAILED | 回调验签失败 | 拒绝并入 payment_notifies(sign_verified=0),告警 |
| PAY_1009 | ERR_DECRYPT_FAILED | 微信回调解密失败 | 返回失败应答,促渠道重试;连续失败告警 |
| PAY_1010 | ERR_CHANNEL_TIMEOUT | 渠道查询/调用超时 | 退避重试 |
| PAY_1011 | ERR_REFUND_EXCEED | 退款金额超出可退余额 | 拒绝 |
| PAY_1012 | ERR_REFUND_DUP | 退款请求号重复 | 返回已有退款单结果 |
| PAY_1013 | ERR_STATE_UNKNOWN | 渠道返回未知状态 | 保留 PENDING,交 cron 与新查询 |
| PAY_1014 | ERR_ILLEGAL_TRANSITION | 非法状态迁移 | 拒绝 + 告警(可能存在逻辑/并发缺陷) |
| PAY_1015 | ERR_CREDIT_FAILED | 入账失败(DB 异常) | 事务回滚,返回渠道失败应答,促重试 |
| PAY_1016 | ERR_CERT_EXPIRED | 平台证书过期/序列号不匹配 | 告警,暂停该渠道下单 |
| PAY_1017 | ERR_CHANNEL_RISK | 渠道风控拒绝 | 提示用户更换渠道;不重试 |
| PAY_1018 | ERR_DB_WRITE | 数据库写入失败 | 重试;连续失败告警 |
| PAY_1019 | ERR_BILL_HASH | 对账文件哈希校验失败 | 重下;连续失败告警 |
| PAY_1020 | ERR_UNSUPPORTED | 不支持的业务类型/渠道 | 拒绝 |
九、安全与合规要点
9.1 密钥与证书管理
| 项 | 要求 |
|---|---|
| 存放位置 | data/payment/{alipay,wechat}/(不在 Web 根;data/.htaccess 已存在用于拒绝直接访问) |
| 目录/文件权限 | 目录 0750,文件 0600,属主 www:www(沿用 data/.pool_key 现行基线) |
data/payment/ 内容 | alipay_app_private.pem、alipay_public.pem、wechat_apiclient_key.pem、wechat_apiclient_cert.pem、wechat_platform_cert.pem(或平台公钥)、apiv3_key(32 字节) |
| 非敏感配置 | 仅 app_id、mchid、serial_no、base_url 存 settings(不存任何私钥/密钥内容) |
| 若必须入库 | 用 AES-256-GCM 加密后存 TEXT,主密钥放 data/payment/.payment_key(0600,不入库)——沿用 NumberPool 既有权衡 |
| 禁止 | 私钥/密钥出现在:Web 根目录、settings 明文、日志、错误响应、channel_raw、版本库、截图 |
| 轮换 | 支持"新旧双密钥并存"窗口:*_key_current / *_key_previous,验签时依次尝试 |
9.2 回调安全
- 强制验签,失败一律拒绝(不允许"调试模式跳过验签"这类开关)。
- 金额、商户号、应用 ID 三项核对,任一不符即不入账并转
REVIEW。 - 时间窗校验:通知时间与本机时间偏差 > 5 分钟 → 拒绝(防重放;仅在证据充分时放宽)。
- 幂等:
uk_channel_notifykey唯一键在最前,重复直接返回成功。 - 限流:回调入口按 IP 与
out_trade_no频控(如 60 次/分钟),防刷。 - 只接受 POST(支付宝通知/微信通知均为 POST),其他方法返回 405。
- 应答严格:支付宝必须纯文本
success;微信必须 HTTP 200 +{"code":"SUCCESS","message":"成功"}。 - 日志脱敏:
payment_notifies.payload存储前按白名单字段筛选并掩码 PII。
9.3 传输与管理端
- 出网 TLS 校验开启(
curl.cainfo已配置,禁止CURLOPT_SSL_VERIFYPEER=false)。 notify_url必须 HTTPS;payment_site_base_url仅允许https://前缀,否则拒绝保存。- 管理端退款:
requireAdmin()+ CSRF + 二次确认 + 单笔限额 + 日累计限额 + 完整审计(操作者 ID、时间、金额、原因、结果)。 - 凭证访问:
admin/payment-proof.php做requireAdmin()+ 路径白名单 +realpath()前缀校验(防目录穿越)+Content-Disposition: inline。 - 订单查询:
payment-status.php必须先WHERE user_id = 当前用户(防越权遍历)。
9.4 合规
- 停止使用个人收款码做经营性收款(P-11):个人码无法开具商户凭证、不支持对账、易触发风控,且与"正版支付接口"目标冲突。网关上线后,收款码降级为异常兜底并逐步下线。
- 交易凭证留存 ≥ 3 年(渠道流水号、原始报文、金额、时间)——落
payment_notifies与payment_reconcile。 - 用户协议与退款政策需明示:支付渠道、到账时效、退款条件与处理时限。
- 主体一致:支付商户主体应与站点运营主体一致;不一致时需在页面明示收款方,避免投诉。
- 不得:使用非官方通道、伪造/篡改回调、绕过渠道风控、为他人代收(二清风险)。
- 个人信息:回调中的买家标识(
buyer_logon_id/openid)属个人信息,按最小必要存储并脱敏。
十、测试用例
10.1 单元测试(不依赖网络)
| 编号 | 用例 | 断言 |
|---|---|---|
| UT-01 | 支付宝签名串构造:含空值参数、中文 subject、金额 99.00 | 与官方文档示例一致;空值参数被剔除 |
| UT-02 | 支付宝验签:篡改任一字段 | 必须失败(返回 false) |
| UT-03 | 微信 v3 签名串:方法\nURL\n时间戳\n随机串\nbody\n | 行尾 \n 数量与位置正确(含 body 为空时的 5 行) |
| UT-04 | 金额换算 元↔分 | 99.00 ↔ 9900;无浮点误差(0.1+0.2 场景) |
| UT-05 | 订单号生成 10000 次 | 全部匹配 ^T[0-9]{12}[A-Za-z0-9]{4}[0-9]{3}$,长度 20,无重复 |
| UT-06 | AES-256-GCM 解密:错 tag / 错 aad | 必须失败且不抛未捕获异常 |
| UT-07 | 状态机:枚举全部非法迁移 | 全部抛 PAY_1014 |
| UT-08 | payment_site_base_url 非 https:// | 保存被拒绝 |
10.2 集成测试(沙箱)
| 编号 | 用例 | 期望 |
|---|---|---|
| IT-01 | 支付宝沙箱:下单 → 扫码支付 → 回调 | PENDING → PAID → CREDITED,users.token_balance 增加快照值 |
| IT-02 | 支付宝:查单(未支付) | WAIT_BUYER_PAY,本地仍 PENDING |
| IT-03 | 支付宝:关单后查单 | 本地 CLOSED |
| IT-04 | 支付宝:全额退款 | CREDITED → REFUNDING → REFUNDED,余额扣回,refunded_fen == amount_fen |
| IT-05 | 微信沙箱:Native 下单 → 支付 → 回调解密 | 明文校验通过,入账一次 |
| IT-06 | 微信:退款查询 | SUCCESS → 本地 REFUNDED |
| IT-07 | 微信:证书序列号变化 | 触发 PAY_1016 告警 |
10.3 幂等与并发测试(重点)
| 编号 | 用例 | 期望 |
|---|---|---|
| ID-01 | 同一回调连续投递 5 次 | addTokens 仅执行 1 次;token_balance 只增一次;4 次走 PAY_1007 |
| ID-02 | 并发 10 个线程同时投递同一回调 | 同上,且无死锁(FOR UPDATE 串行化) |
| ID-03 | 回调与 cron 查单同时命中同一单 | 仅一次入账 |
| ID-04 | 用户连点下单 5 次(同一 request_id) | 仅创建 1 个 payment_orders,返回同一 out_trade_no |
| ID-05 | 管理员并发点击"通过"(现有流程加固) | 仅一次 addTokens(P-04 修复验证) |
| ID-06 | 同一 out_refund_no 重复提交退款 | 返回首次结果,渠道侧只退一次 |
10.4 异常与安全测试
| 编号 | 用例 | 期望 |
|---|---|---|
| EX-01 | 篡改回调金额为 0.01 | 不入账,置 REVIEW,PAY_1004 |
| EX-02 | 篡改 app_id / mchid | 拒绝 |
| EX-03 | 删除 sign 或伪造签名 | PAY_1008,sign_verified=0 |
| EX-04 | 重放 24 小时前的通知 | 时间窗拒绝(或幂等命中) |
| EX-05 | payment-status.php 访问他人订单 | 403/空结果 |
| EX-06 | admin/payment-proof.php 传 ../ 路径 | 拒绝 |
| EX-07 | 非 POST 访问回调入口 | 405 |
| EX-08 | 私钥文件权限检查 | 0600;ls -l 与 grep -r 全站扫描无私钥内容 |
| EX-09 | 订单状态被手工改成 CREDITED 后再次回调 | 幂等,不入账 |
10.5 补偿与边界测试
| 编号 | 用例 | 期望 |
|---|---|---|
| CP-01 | 回调被防火墙丢弃,用户已支付 | ≤ 5 分钟内 cron 查单入账 |
| CP-02 | 下单后未支付超时 | PENDING → CLOSED,渠道侧关单成功 |
| CP-03 | 关单瞬间用户支付成功(竞态) | 迟到回调将 CLOSED 恢复为 PAID → CREDITED |
| CP-04 | 套餐支付后下架 | 仍按订单快照金额与 Token 数入账 |
| CP-05 | 最低金额 0.01 元 | 下单成功(若渠道允许) |
| CP-06 | 渠道返回 REVIEW 未知状态 | 订单不误判,转人工 |
十一、验收标准
| 类别 | 标准(可量化) | 验证方式 | |
|---|---|---|---|
| 功能 | 支付宝/微信各完成"下单→支付→入账→退款"全链路 | IT-01 ~ IT-06 通过 | |
| 幂等 | 重复/并发回调导致重复入账笔数 = 0 | ID-01 ~ ID-03、EX-09 | |
| 重复扣款 | 同一 out_trade_no 不退两次;同一订单不入账两次 | ID-06、ID-03 | |
| 金额安全 | 被篡改金额的成功入账笔数 = 0 | EX-01 | |
| 回调可靠性 | 回调丢失场景 100% 在 5 分钟内 补偿入账 | CP-01 | |
| 性能 | 回调处理 P95 ≤ 300ms(不含渠道网络);入账事务 ≤ 100ms | 压测脚本 | |
| 对账 | D+1 对账差异自动检出,diff_count 与人工核对结果一致 | 造 3 类差异(漏单/多单/金额不符)验证 | |
| 安全 | 全站 grep 无私有密钥内容;`grep -rn "SSL_VERIFYPEER.*false\ | 跳过验签" includes/payment/` 为 0 | 静态扫描 |
| 安全 | 密钥/证书文件权限 0600、目录 0750、不在 Web 根 | ls -l、HTTP 直接访问 403/404 | |
| 审计 | 每笔退款可查到操作人、时间、原因、渠道退款号 | payment_refunds + 审计日志 | |
| 监控 | 4 项指标可在后台/日志中查询,告警可触发(造 REVIEW 单验证) | 人工触发 | |
| 可回滚 | payment_gateway_enabled=0 后 60 秒内新单只走人工流程 | 手工验证 | |
| 合规 | 交易凭证留存字段完整(渠道流水号、金额、时间、原始报文) | 抽样 10 笔 | |
| 代码质量 | 新增文件 php -l 全部 PASS;无新增依赖;无临时文件残留 | php -l + git status/目录检查 | |
| 回归 | 现有页面 /、/token-recharge.php、/admin/token-orders.php 行为不变 | HTTP 冒烟 |
十二、上线与监控方案
12.1 上线前准备清单
| # | 项 | 责任方 |
|---|---|---|
| 1 | 域名 + ICP 备案 + HTTPS 证书(宝塔可申请) | 用户 |
| 2 | 支付宝:应用创建、RSA2 密钥对、应用私钥、支付宝公钥、签约"当面付" | 用户 |
| 3 | 微信:商户号、APIv3 密钥、apiclient_cert/key.pem、appid 绑定、开通 Native | 用户 |
| 4 | payment_site_base_url 配置为 HTTPS 域名 | 运维 |
| 5 | 密钥文件落盘并 chmod 600、属主 www:www | 运维 |
| 6 | 4 张新表 DDL 在低峰期执行(仅 CREATE TABLE,无锁风险) | 运维 |
| 7 | 宝塔计划任务新增 4 条 cron | 运维 |
| 8 | SMTP 配置(用于告警) | 运维 |
| 9 | 数据备份:mysqldump 全库 + 站点目录 | 运维 |
12.2 上线步骤
- 执行建表(仅新增,不回滚旧结构)。
- 部署代码(全部为新增文件,不动既有业务文件)。
- 配置密钥与
settings(payment_gateway_enabled=0)。 - 跑沙箱/小额真实交易验证 IT-01 ~ IT-06。
- 按 4.8 节灰度阶段 1 → 2 → 3 推进。
- 每阶段结束后核对:
payment_reconcile(差异 0)、payment_notifies(无sign_verified=0聚集)、告警数。
12.3 监控与告警落地
| 监控项 | 数据源 | 频率 | 阈值 | 告警动作 |
|---|---|---|---|---|
| 下单失败率 | payment_orders | 5 分钟 | > 5% | 邮件 + 面板通知 |
| 回调失败率 | payment_notifies | 5 分钟 | > 1% | 邮件 + 面板通知 |
| 待入账积压 | payment_orders | 5 分钟 | ≥ 1 且 > 10 分钟 | 邮件(含订单号) |
| 对账差异 | payment_reconcile | 每日 | > 0 | 邮件 + 人工工单 |
| 待决订单 | payment_orders.status='REVIEW' | 5 分钟 | ≥ 1 | 面板通知 |
| 证书有效期 | 平台证书/商户证书 | 每日 | < 30 天 | 邮件 |
12.4 回滚与应急预案
| 场景 | 动作 |
|---|---|
| 渠道大面积故障(回调失败率飙升) | payment_gateway_enabled=0 切回人工收款码;存量 PENDING 单交由 cron 收敛 |
| 入账逻辑出现错误(多入/少入) | 立即关开关 + 暂停 cron;按 payment_notifies 与渠道账单逐笔复盘,人工修正余额并留审计 |
| 密钥泄露 | 渠道后台重置密钥/重新签发证书 → 更新落盘文件 → 全站日志排查 → 通知用户改密 |
| 对账持续差异 | 暂停该渠道下单,人工核对 T-3 ~ T-1 账单 |
12.5 例行运维
- 每日:查看对账结果、
REVIEW队列、告警。 - 每周:抽查 5 笔退款与 5 笔入账的原始报文完整性。
- 每月:核对密钥/证书有效期;复核退款限额与审计日志;清理
payment_notifies中超过保留期(建议 12 个月)的payload(保留统计字段)。
十三、实施路线图
| 阶段 | 内容 | 依赖 | 预估工作量 | 风险 |
|---|---|---|---|---|
| P0-A 无渠道加固 | ① 修 P-02/P-03:新增 processPaymentProofUpload()(5MB、保原图、生成缩略图、jpg/png/webp/pdf),凭证改存 data/payment/proofs/ 并由 admin/payment-proof.php 受控输出;② 修 P-04:审核改为条件更新 + 行锁;③ 修 P-09:pay_method 白名单;④ 修 P-13/P-14:凭证访问控制 + 操作审计;⑤ 修 P-16:为 docs/ 目录增加 nginx 拒绝规则(改动 vhost,需单独授权) | 无(⑤ 需 vhost 变更授权) | 0.5~1 天 | 低 |
| P0-B 可观测性 | 新增 cron-payment-sweep.php 雏形:统计待审订单、超时未审订单、异常订单并邮件告警(复用 Mailer);对现有"人工到账"做站内对账报表 | 无(SMTP 可选) | 0.5 天 | 低 |
| P1 支付宝当面付 | includes/payment/*(Signer/AlipayGateway/PaymentService/PaymentStore/Certs/Ledger)、payment-create.php、payment-page.php、payment-status.php、payment-notify-alipay.php、admin/payment-orders.php、4 张表;下单→支付→入账→查单补偿闭环 | 域名/HTTPS(建议)+ 支付宝资质与密钥 | 3~5 天 | 中(验签/状态机是实现难点) |
| P2 微信 v3 Native | WechatNativeGateway(签名、验签、AES-256-GCM、证书装载与轮换)、payment-notify-wechat.php、证书检查 cron | HTTPS 域名(强制)+ 微信商户号与 APIv3 密钥 | 3~4 天 | 中高(回调必须 HTTPS;证书轮换复杂) |
| P3 退款与对账 | admin/payment-refund.php、退款回调、T+1 账单下载与比对、payment_reconcile 报表与告警 | P1 或 P2 至少一项完成 | 2~3 天 | 中(账单格式与编码解析) |
| P4 灰度与全量 | 灰度开关、双轨并行、监控面板、人工收款码下线 | 上述全部 | 1~2 天 | 低 |
立即可做:P0-A 与 P0-B 不依赖任何渠道资质、域名或 HTTPS,且直接消除当前最严重的资金风险(P-04 重复到账、P-02 凭证不可读)。建议优先执行。
十四、需用户补充的阻断信息
以下信息缺失将导致对应模块无法实施(不是"可以猜"的问题)。请逐项确认。
14.1 硬阻断(缺失则无法接入官方支付)
| # | 需补充信息 | 影响 |
|---|---|---|
| B-01 | 渠道选择:只做支付宝、只做微信,还是两者都做? | 决定 P1/P2 优先级与工作量 |
| B-02 | 是否有可用的 ICP 备案域名?能否为该域名申请 HTTPS 证书? | 微信 v3 强制 HTTPS 回调,无域名则微信通道无法上线(P-10) |
| B-03 | 经营主体类型:企业 / 个体工商户 / 个人? | 个人无法签约支付宝当面付与微信 Native;主体不符则本方案无法落地 |
| B-04 | 支付资质是否已开通:支付宝"当面付"产品签约状态;微信支付商户号 + "Native 支付"权限 | 未开通则需先走渠道申请流程 |
| B-05 | 密钥材料(请勿在对话中粘贴明文内容,改为告知已就位并交付方式):支付宝 app_id、应用私钥、支付宝公钥;微信 mchid、appid、APIv3 密钥、apiclient_cert.pem/apiclient_key.pem | 无法联调 |
| B-06 | 是否允许引入官方 SDK(需 composer 或手工放置 vendor/)? | 决定走 5.4 的方案 A(自实现,零依赖)或方案 B(官方 SDK) |
| B-07 | 是否允许新增 4 张数据表?建表时间窗口? | 无表则无法记录支付事实 |
| B-08 | 是否允许新增宝塔计划任务(4 条 cron)? | 无 cron 则无查单补偿与对账,回调丢失即无法自愈 |
14.2 业务规则(缺失则先按建议默认值实施并在文档标注)
| # | 需确认 | 建议默认值 |
|---|---|---|
| B-09 | 订单支付有效期 | 15 分钟 |
| B-10 | 单笔最小/最大充值金额 | 最小 1 元,最大 5000 元 |
| B-11 | 是否允许用户自助申请退款?还是仅管理员发起? | 仅管理员 |
| B-12 | 退款手续费承担方式(渠道手续费是否从退款中扣除) | 原路全额退,手续费由平台承担 |
| B-13 | 是否保留"人工收款码 + 人工审核"作为兜底? | 保留(降级为折叠入口) |
| B-14 | 对账时间与口径(T+1 账单下载时间、结算周期) | 每日 09:30 拉取 T-1 账单 |
| B-15 | 告警渠道(邮件 / 短信 / 面板通知)与收件人 | 邮件 + 面板通知 |
| B-16 | 是否需要把渠道支付与现有 token_orders 做双向字段关联(新增 out_trade_no 列) | 不加列,用 payment_orders.token_order_id 关联 |
14.3 现网风险确认
| # | 需确认 | 说明 |
|---|---|---|
| B-17 | 现网是否已在用个人收款码收款?存量订单量级? | 涉及 P-11 合规风险与迁移策略 |
| B-18 | uploads/proofs/ 为空的真实原因(用户未上传 / 上传失败)? | 佐证 P-02/P-03 的实际影响面 |
十五、假设清单
以下为本方案在信息不足时主动标注的假设。若与实情不符,需相应调整方案。
| 编号 | 假设 | 依据 | 若不成立的影响 |
|---|---|---|---|
| A-01 | 站点为单机部署(无多实例负载均衡) | 实测单 nginx vhost、单 PHP-FPM | 多实例下需引入分布式锁(Redis/DB 行锁),flock 文件锁失效 |
| A-02 | 以"人工审核到账"为当前唯一到账路径,且无任何渠道回调 | 全站关键字检索为 0 | 方案已覆盖,无影响 |
| A-03 | 允许使用 ext-openssl + ext-curl 自实现官方协议(不引入依赖) | 无 composer/vendor;openssl/curl 均可用 | 若必须用官方 SDK,需用户批准安装 composer |
| A-04 | MySQL 5.7 无法使用 JSON_TABLE 等新特性,原始报文以 TEXT 存储 | 实测 5.7.40 | 无影响(设计已规避) |
| A-05 | 时区为 Asia/Shanghai | config/config.php 设置 | 签名时间戳会偏移,导致渠道报错(timestamp 必须是东八区) |
| A-06 | 支付金额与 Token 数完全由服务端按 package_id 回查,客户端不可信 | 现有代码 token-recharge.php:75-88 已如此 | 无影响(沿用) |
| A-07 | 支付宝当面付在无 HTTPS 时可通过 HTTP 回调(仅阶段性);生产务必 HTTPS | 官方要求"回调地址可访问" | 若渠道强制 HTTPS,支付宝通道同样需等待域名(与微信一致) |
| A-08 | 现有 token_orders 的 status=1 语义保持不变,网关到账直接写 status=1 | 避免污染人工审核队列 | 若后台需要区分"人工/自动",再加 admin_note 或新列 |
| A-09 | 站点无外部 MQ,异步处理仅靠 cron | 无 Redis/MQ 迹象(未在本轮确认,标记为假设) | 若有 MQ 可用,可将补偿改为事件驱动,降低延迟 |
| A-10 | 现有 settings 明文存储惯例不适用于支付私钥 | P-12 与安全基线 | 若用户坚持入库明文,需书面确认风险 |
| A-11 | 面板 data/.license_warnings 与授权机制与本任务无关,不触碰 | 硬约束 | 无影响 |
| A-12 | 本方案中所有"官方接口"细节以官方文档为准,实施前需按当时最新文档复核签名串与错误码 | 渠道文档会更新 | 需在实施阶段重新核对一次 |
附录 A:本次交付边界声明
- 本轮仅交付"审计 + 方案"文档,位于
docs/支付系统优化与官方支付接入方案.md。 - 未改动任何业务代码、未新增/修改任何数据表、未新增依赖、未写生产库、未改
CMS_VERSION(仍为v0.3.81)、未触碰面板核心与授权机制。 - 文中的代码行号与表结构均来自本轮实际只读读取;环境能力(扩展、OpenSSL、MySQL 版本、nginx 配置、目录内容)均为实测。
- 标注为「未验证」的内容(第五、1.5 节)无法在本环境完成验证:无真实渠道账号、无浏览器、无管理凭据。
- 实施任何代码改动、建表、依赖引入前,需用户按第十四节确认阻断信息。
附录 B:与本项目既有 BUG 文档的关系
| 编号 | 主题 | 关系 |
|---|---|---|
| BUG-2026-0027 | 收款码 / 动态码体系 | 现存"支付"实现,本方案将其定位为降级兜底(P-11 合规风险) |
| BUG-2026-0028 / 0029 | 收款码图片解析链路 | 与本方案无关(解析的是收款码,非支付凭证) |
| BUG-2026-0030 | 套餐 × 渠道固定金额收款码 | 被本方案 P-02(凭证裁剪)部分的用途重复覆盖:网关上线后由渠道固定金额替代 |
| 本方案 | 支付系统优化与官方支付接入 | 独立方案文档,非 BUG 条目;后续实施产生的缺陷另立 BUG 编号(下一个可用:0031) |