跳到主要内容
奇点CMS v0.4.70 · 官方网站 登录 / 注册 在线激活

支付系统优化与官方支付接入方案

1. 本站当前不存在任何支付网关代码。 全站检索 notifyurl / outtradeno / precreate / mchid / WECHATPAY 等关键字,业务代码命中数为 0。

цФпф╗Шч│╗ч╗Яф╝ШхМЦф╕ОхоШцЦ╣цФпф╗ШцОехЕецЦ╣цбИ.md 文档版本 v0.4.70 71 KB 更新于 2026-10-14

支付系统优化与官方支付接入方案

项目内容
站点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.php
Signer.php / Certs.phpPaymentSigner.php / PaymentConfig.php(密钥装载并入 PaymentConfig)
payment-page.php未单独实现:收款码/动态码由 payment-code.php 承载,下单在 payment-create.php + payment-guide.php
payment-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 项。


结论摘要(先读这一段)

  1. 本站当前不存在任何支付网关代码。 全站检索 notify_url / out_trade_no / precreate / mchid / WECHATPAY 等关键字,业务代码命中数为 0。现有"支付"= 静态/套餐收款码 + 用户上传转账截图 + 管理员人工点击审核到账。
  2. 因此"接入官方支付接口"不是改造,而是新增一条与现有人工流程并行、可灰度、可回退的通道。现有流程必须保留为降级兜底。
  3. 存在 3 个硬阻断项(详见第十四节):无域名/无 HTTPS(微信支付 v3 强制要求 HTTPS 回调)、支付渠道资质与密钥未提供、是否允许引入官方 SDK(当前无 composer、无 vendor/)与新增数据表未确认。
  4. 在资质到位前,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-193UPDATE 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 列)

字段类型说明
idint unsigned PK
user_idint unsigned, MUL
package_idint unsigned, 默认 00 = 自定义充值
token_amountdecimal(10,2)服务端推导
pricedecimal(10,2)单位:元
pay_methodvarchar(30)alipay / wechat
pay_proofvarchar(500)截图相对路径
statustinyint(1), MUL0 待审 / 1 通过 / 2 拒绝
admin_notevarchar(255)
created_at / updated_attimestamp

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 环境与基础设施事实(实测)

项实测值对方案的影响
PHP8.2.33 CLI/FPM可用 random_bytes、严格类型、Throwable
OpenSSL1.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(已配置)出网调用默认开启证书校验,无需额外改动
MySQL5.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.php0644 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-01P0无渠道回调、无主动查单,到账完全依赖人工全站关键字检索为 0;admin/token-orders.php:166-194用户已付款但长期不到账;无法自动发现"付款未上报";人工成本随订单量线性增长
P-02P0支付凭证被中心裁剪为 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-03P0凭证大小上限 512KB、格式仅 jpg/png/gif/webpincludes/fn-upload.php:34-35手机截图常 > 1MB、iPhone 默认 HEIC → 用户上传失败,被迫改用无凭证下单
P-04P0审核动作存在"读后写"竞态,可重复到账admin/token-orders.php:163 先读 status != 0 判断,:171-183 才在事务内改状态;事务未锁定订单行(无 FOR UPDATE),并发两次通过请求可各自读到 status=0同一订单重复 addTokens() → 重复发币
P-05P1无幂等键、无渠道流水号,无法与渠道对账token_orders 无 trade_no/out_trade_no 列出现争议时无法举证;对账只能靠人工核银行流水
P-06P1无退款能力无任何退款代码用户投诉只能手工改余额,无审计轨迹、无渠道侧退款
P-07P1无监控与告警:下单失败、待审积压、到账异常均无通知仅 admin/token-dashboard.php 静态展示故障被发现的时间 = 用户来投诉的时间
P-08P1无渠道账单核对(T+1)无对账代码、无对账表渠道侧差异(漏单/多单/金额不符)无法闭环
P-09P1pay_method 服务端未做白名单校验token-recharge.php:55、:93 仅判空串可写入任意字符串,污染统计与后续渠道映射
P-10P1站点纯 HTTP、无域名、无备案 → 微信支付 v3 无法接收回调nginx vhost listen 80; server_name 39.96.90.85;微信通道硬阻断;支付宝当面付虽可 http 回调,但生产环境强烈建议 HTTPS,且无域名不利于资质审核
P-11P1现有收款方式为个人收款码,不可用于经营性收款settings.pay_qr_alipay / pay_qr_wechat 及套餐专用码(BUG-2026-0030)合规风险:资金性质属个人转账,无商户凭证、无法开票、易触发账户风控
P-12P2敏感配置以明文存 settingsstream_secret 等明文入库;getSetting() 无加密层一旦库被读走即全部泄露;支付私钥不得沿用此模式
P-13P2后台展示凭证为原文件名路径,无访问控制pay_proof varchar(500) 指向 uploads/proofs/(Web 可直接访问)凭证(含姓名/账号)可被枚举下载
P-14P2无操作审计:谁在何时通过了哪笔订单无独立日志仅有 admin_note内部风险不可追溯
P-15P2uploads/proofs/ 目录当前不存在(实测 0 文件)ls uploads/proofs 为空说明凭证功能实际上极少被使用或用户上传即失败,佐证 P-02/P-03
P-16P1docs/ 目录整体可被公网匿名读取(含架构、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 设计原则

  1. 客户端不可信:金额、Token 数一律服务端按 package_id 回查;客户端只允许传 package_id 与渠道。
  2. 渠道不可全信:回调必须验签 + 校验金额/商户号/应用 ID + 校验本地订单存在且状态合法。
  3. 钱账分离:支付事实(渠道)与记账事实(站内 Token)分表,二者通过幂等键关联。
  4. 只增不删:新表新字段只做增量;旧流程保留,通过开关切换。
  5. 一切可回退:关闭开关即回到"人工收款码 + 人工审核",无需数据回滚。

四、目标架构

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)

当前态允许迁移到触发者备注
INITPENDING / FAILED下单立即迁移,INIT 不对外可见
PENDINGPAID / CLOSED / REVIEW回调 / 查单 / 关单 cron
PENDINGPENDING重复回调幂等,不产生迁移
PAIDCREDITED入账必须与 addTokens 同事务
PAIDREVIEW金额不符等人工介入前不得入账
CLOSEDPAID / CREDITED回调/查单(晚到)必须支持:先付款后关单的竞态
CREDITEDREFUNDING管理员退款需权限与限额校验
REFUNDINGREFUNDED / PARTIAL_REFUNDED / REVIEW退款结果
REFUNDED——终态
REVIEWCREDITED / 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_notifykeyINSERT 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→30m24h 后转 REVIEW + 告警
关单失败重试 3 次,间隔 1m3 次后保持 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_enabled0 / 1总开关,0 即完全回退到人工收款码
payment_channelsalipay,wechat启用的渠道白名单
payment_gray_users1,2,3灰度用户 ID 白名单
payment_gray_percent0 / 5 / 100按用户 ID 取模的灰度比例
payment_site_base_urlhttps://pay.example.com生成 notify_url 的基址(必须 HTTPS)

灰度阶段

  1. 阶段 0(单测/沙箱):payment_gateway_enabled=1 但 payment_channels="",仅跑单元与沙箱测试。
  2. 阶段 1(内部):payment_gray_users 加入 2~3 个内部账号,金额限制为最低套餐,人工核对每笔。
  3. 阶段 2(5%):payment_gray_percent=5,观察 3 天:回调成功率、入账时延、告警数。
  4. 阶段 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.precreateout_trade_no、total_amount(元,两位小数)、subject、timeout_express(如 15m)code=10000、qr_code
查单alipay.trade.queryout_trade_no 或 trade_notrade_status:WAIT_BUYER_PAY / TRADE_SUCCESS / TRADE_FINISHED / TRADE_CLOSED
关单alipay.trade.closeout_trade_no 或 trade_nocode=10000
退款alipay.trade.refundout_trade_no、refund_amount、out_request_no(幂等键,部分退款必传)、refund_reasoncode=10000、fund_change
撤销alipay.trade.cancelout_trade_no仅当面付/条码支付,当日未结算时使用
对账单下载alipay.data.dataservice.bill.downloadurl.querybill_type=trade、bill_date账单 ZIP 下载地址
  • 应答验签:sign 为顶层字段,被签名内容是响应中的业务节点(如 alipay_trade_precreate_response)的原始 JSON 片段。必须先验签再解析业务字段。
  • 异步通知(notify_url):application/x-www-form-urlencoded POST。校验顺序:
  1. 用支付宝公钥对通知参数(剔除 sign、sign_type,值为解码后的明文)按 ASCII 排序拼串验签;
  2. 校验 app_id 与本应用一致;
  3. 校验 seller_id 与签约商户一致;
  4. 校验 total_amount 与本地 payment_orders.amount_fen(元)一致;
  5. 校验 out_trade_no 在本地存在;
  6. 仅在 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/nativeappid、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=xxxtrade_state:SUCCESS / NOTPAY / CLOSED / REFUND / USERPAYING / PAYERROR
关单POST /v3/pay/transactions/out-trade-no/{out_trade_no}/closebody {mchid}
申请退款POST /v3/refund/domestic/refundsout_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/certificatesAES-256-GCM 加密的证书列表(含 serial_no)
  • 回调处理:
  1. 验签(同上,用平台证书/平台公钥);
  2. 解密 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字节]) ```

  1. 校验 event_type == "TRANSACTION.SUCCESS"、明文中的 appid/mchid/out_trade_no/amount.total(分);
  2. 入账(复用 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 UNSIGNEDpayment_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.phpPOST登录 + CSRF + 频控(10 次/分钟/用户)下单,返回 out_trade_no 与 code_url
/payment-page.phpGET登录(仅本人订单)收银台,展示二维码 + 轮询状态
/payment-status.phpGET登录(仅本人订单)轮询返回 {status, credited, token_amount}
/payment-notify-alipay.phpPOST验签支付宝异步通知,应答纯文本 success
/payment-notify-wechat.phpPOST验签 + 解密微信 v3 通知,应答 {"code":"SUCCESS"}
/payment-return-alipay.phpGET无同步跳转展示页,不作为入账依据
/admin/payment-orders.phpGET/POSTrequireAdmin() + CSRF订单列表 / 手动补单 / 手动关单 / 查看原始报文
/admin/payment-refund.phpPOSTrequireAdmin() + CSRF + 二次确认 + 限额发起退款
/admin/payment-proof.phpGETrequireAdmin()受控输出凭证(替代 Web 直读 uploads/proofs/)

6.2 定时任务

脚本建议频率职责
cron-payment-reconcile.php --query每分钟PENDING 单查单补偿
cron-payment-reconcile.php --bill每日 09:30T+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_1001ERR_CFG_MISSING渠道参数/密钥未配置后台提示补齐配置;不创建订单
PAY_1002ERR_SIGN_FAILED本地签名失败(私钥不可用/格式错)告警;订单置 FAILED
PAY_1003ERR_CHANNEL_CREATE渠道下单失败(业务码非成功)返回用户可读提示;重试 1 次
PAY_1004ERR_AMOUNT_MISMATCH回调金额与本地不符不入账,置 REVIEW,告警
PAY_1005ERR_ORDER_NOT_FOUND本地订单不存在不入账,记录原始报文
PAY_1006ERR_STATUS_INVALID状态不允许该操作返回提示;写审计
PAY_1007DUPLICATE_NOTIFY重复回调(幂等忽略)按渠道格式应答成功,不重复入账
PAY_1008ERR_VERIFY_FAILED回调验签失败拒绝并入 payment_notifies(sign_verified=0),告警
PAY_1009ERR_DECRYPT_FAILED微信回调解密失败返回失败应答,促渠道重试;连续失败告警
PAY_1010ERR_CHANNEL_TIMEOUT渠道查询/调用超时退避重试
PAY_1011ERR_REFUND_EXCEED退款金额超出可退余额拒绝
PAY_1012ERR_REFUND_DUP退款请求号重复返回已有退款单结果
PAY_1013ERR_STATE_UNKNOWN渠道返回未知状态保留 PENDING,交 cron 与新查询
PAY_1014ERR_ILLEGAL_TRANSITION非法状态迁移拒绝 + 告警(可能存在逻辑/并发缺陷)
PAY_1015ERR_CREDIT_FAILED入账失败(DB 异常)事务回滚,返回渠道失败应答,促重试
PAY_1016ERR_CERT_EXPIRED平台证书过期/序列号不匹配告警,暂停该渠道下单
PAY_1017ERR_CHANNEL_RISK渠道风控拒绝提示用户更换渠道;不重试
PAY_1018ERR_DB_WRITE数据库写入失败重试;连续失败告警
PAY_1019ERR_BILL_HASH对账文件哈希校验失败重下;连续失败告警
PAY_1020ERR_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 回调安全

  1. 强制验签,失败一律拒绝(不允许"调试模式跳过验签"这类开关)。
  2. 金额、商户号、应用 ID 三项核对,任一不符即不入账并转 REVIEW。
  3. 时间窗校验:通知时间与本机时间偏差 > 5 分钟 → 拒绝(防重放;仅在证据充分时放宽)。
  4. 幂等:uk_channel_notifykey 唯一键在最前,重复直接返回成功。
  5. 限流:回调入口按 IP 与 out_trade_no 频控(如 60 次/分钟),防刷。
  6. 只接受 POST(支付宝通知/微信通知均为 POST),其他方法返回 405。
  7. 应答严格:支付宝必须纯文本 success;微信必须 HTTP 200 + {"code":"SUCCESS","message":"成功"}。
  8. 日志脱敏: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 合规

  1. 停止使用个人收款码做经营性收款(P-11):个人码无法开具商户凭证、不支持对账、易触发风控,且与"正版支付接口"目标冲突。网关上线后,收款码降级为异常兜底并逐步下线。
  2. 交易凭证留存 ≥ 3 年(渠道流水号、原始报文、金额、时间)——落 payment_notifies 与 payment_reconcile。
  3. 用户协议与退款政策需明示:支付渠道、到账时效、退款条件与处理时限。
  4. 主体一致:支付商户主体应与站点运营主体一致;不一致时需在页面明示收款方,避免投诉。
  5. 不得:使用非官方通道、伪造/篡改回调、绕过渠道风控、为他人代收(二清风险)。
  6. 个人信息:回调中的买家标识(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-06AES-256-GCM 解密:错 tag / 错 aad必须失败且不抛未捕获异常
UT-07状态机:枚举全部非法迁移全部抛 PAY_1014
UT-08payment_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-05payment-status.php 访问他人订单403/空结果
EX-06admin/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 通过
幂等重复/并发回调导致重复入账笔数 = 0ID-01 ~ ID-03、EX-09
重复扣款同一 out_trade_no 不退两次;同一订单不入账两次ID-06、ID-03
金额安全被篡改金额的成功入账笔数 = 0EX-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用户
4payment_site_base_url 配置为 HTTPS 域名运维
5密钥文件落盘并 chmod 600、属主 www:www运维
64 张新表 DDL 在低峰期执行(仅 CREATE TABLE,无锁风险)运维
7宝塔计划任务新增 4 条 cron运维
8SMTP 配置(用于告警)运维
9数据备份:mysqldump 全库 + 站点目录运维

12.2 上线步骤

  1. 执行建表(仅新增,不回滚旧结构)。
  2. 部署代码(全部为新增文件,不动既有业务文件)。
  3. 配置密钥与 settings(payment_gateway_enabled=0)。
  4. 跑沙箱/小额真实交易验证 IT-01 ~ IT-06。
  5. 按 4.8 节灰度阶段 1 → 2 → 3 推进。
  6. 每阶段结束后核对:payment_reconcile(差异 0)、payment_notifies(无 sign_verified=0 聚集)、告警数。

12.3 监控与告警落地

监控项数据源频率阈值告警动作
下单失败率payment_orders5 分钟> 5%邮件 + 面板通知
回调失败率payment_notifies5 分钟> 1%邮件 + 面板通知
待入账积压payment_orders5 分钟≥ 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 NativeWechatNativeGateway(签名、验签、AES-256-GCM、证书装载与轮换)、payment-notify-wechat.php、证书检查 cronHTTPS 域名(强制)+ 微信商户号与 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-18uploads/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-04MySQL 5.7 无法使用 JSON_TABLE 等新特性,原始报文以 TEXT 存储实测 5.7.40无影响(设计已规避)
A-05时区为 Asia/Shanghaiconfig/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:本次交付边界声明

  1. 本轮仅交付"审计 + 方案"文档,位于 docs/支付系统优化与官方支付接入方案.md。
  2. 未改动任何业务代码、未新增/修改任何数据表、未新增依赖、未写生产库、未改 CMS_VERSION(仍为 v0.3.81)、未触碰面板核心与授权机制。
  3. 文中的代码行号与表结构均来自本轮实际只读读取;环境能力(扩展、OpenSSL、MySQL 版本、nginx 配置、目录内容)均为实测。
  4. 标注为「未验证」的内容(第五、1.5 节)无法在本环境完成验证:无真实渠道账号、无浏览器、无管理凭据。
  5. 实施任何代码改动、建表、依赖引入前,需用户按第十四节确认阻断信息。

附录 B:与本项目既有 BUG 文档的关系

编号主题关系
BUG-2026-0027收款码 / 动态码体系现存"支付"实现,本方案将其定位为降级兜底(P-11 合规风险)
BUG-2026-0028 / 0029收款码图片解析链路与本方案无关(解析的是收款码,非支付凭证)
BUG-2026-0030套餐 × 渠道固定金额收款码被本方案 P-02(凭证裁剪)部分的用途重复覆盖:网关上线后由渠道固定金额替代
本方案支付系统优化与官方支付接入独立方案文档,非 BUG 条目;后续实施产生的缺陷另立 BUG 编号(下一个可用:0031)

原始文件:docs/цФпф╗Шч│╗ч╗Яф╝ШхМЦф╕ОхоШцЦ╣цФпф╗ШцОехЕецЦ╣цбИ.md · 出于安全考虑,站点不直接暴露 docs/ 目录下的原始文件(其中含内部资料),请在交付包内查看全文。

全部文档
站点公告 官网试运行中,如需购买或试用请通过「联系我们」留言。