本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/支付中心插件开发计划_2026-09-30.md。
支付中心插件(payment_center)开发计划
版本:计划稿 v1 | 日期:2026-09-30 | 状态:待老搭档确认,尚未动手
依据:
docs/插件开发规范.mdv1.4(2026-09-30)参考实现:
plugins/points_mall(订单/兑换事务/后台配置页)、plugins/third_party_login(外部 API + 回调 + 验签范式)、plugins/props(积分扣减与后台 Tab)目标:给 FlintHub 提供一套可插拔的收款能力——充值余额、兑换积分、购买 VIP/道具,通道层支持聚合支付与官方直连。
零、先说结论(TL;DR)
| 问题 | 结论 |
|---|---|
| 微信/支付宝直连能不能做 | 能做,但建议后置。微信支付 V3 需商户资质(营业执照/个体户);支付宝「当面付」个人可申请,门槛较低。两条都归入 P2/P3。 |
| 聚合支付选谁 | 不绑定单一平台,做成「易支付协议(EPAY)适配器」——一次实现覆盖蓝兔支付、彩虹易支付及大量同协议平台。另附虎皮椒(xunhupay)独立适配器作为个人免签备选。 |
| 为什么不直接推某个平台 | 聚合支付平台的跑路风险是真实的(搜索到的多篇避坑指南都指向同一组判据:企业备案 / 对公结算 / 运营 ≥1 年 / 通道实测)。做成协议适配器,平台可随时更换,代码不用改。 |
| 最关键的一条安全原则 | 到账一律以「异步回调 + 验签」为准,同步回跳页只做展示,绝不到账。这是支付系统第一大坑。 |
| 工作量 | 分 5 阶段。P0+P1(可上线的最小闭环)≈ 主体工作量;P2 起是通道扩展,可独立排期。 |
| 需要你拍板的事 | 见 第九章(余额落点、通道优先级、合规底线)。 |
一、需求与边界
1.1 要做的
- 充值:用户选套餐 → 选通道 → 支付 → 到账。
- 消费:余额/积分兑换 → 兑换积分、购买 VIP 用户组、购买道具(对接
props/points_mall的既有能力,经核心钩子或核心服务,不直连插件表)。 - 订单管理:用户侧「我的订单 / 资金流水」;后台订单列表、筛选、手动关闭、退款、手动调账。
- 通道管理:后台可视化配置各通道的商户号/密钥,支持启用停用、排序、测试连通性。
- 对账与告警:订单超时自动关闭;掉单检测(本地待支付 + 上游已支付);每日对账差异告警。
1.2 明确不做的(一期)
- 不做提现/转账:一旦「充值 → 提现」,性质就变成资金池 + 二清,个人站碰不得。余额只能消费、不可提现(写死在产品定义里,不是技术上做不到)。
- 不做代付/分账:同上,需要支付牌照。
- 不做信用卡/花呗分期等信贷产品。
- 不做卡密自动发货的电商(那是另一个插件的活)。
1.3 合规底线(必须在页面上明示)
这一节不是免责声明,是真实风险提示,请务必看完再决定要不要上。
- 个人站点直接收「充值款」并沉淀为站内余额,在监管口径下容易被认定为预付卡/资金池;若还能提现即构成二清(无牌照从事支付结算)。
- 现实做法:① 明示「充值仅用于站内虚拟服务消费,不可提现、不可退款」;② 保留完整的资金流水台账(本插件的
pc_ledger就是干这个的);③ 金额控制在小额赞助量级,不做大额充值;④ 若日后有商业化诉求,走持牌聚合支付服务商(资金由持牌机构清算,平台不碰钱)。 - 建议:一期定位为「赞助/打赏 + 站内积分兑换」,而非「钱包」。这个定位差别很大,见第九章待确认项。
二、支付通道选型
2.1 通道全景
| 通道 | 资质要求 | 接入难度 | 资金安全 | 费率 | 本计划归属 |
|---|---|---|---|---|---|
| 易支付协议(EPAY) | 无(选平台) | 中(协议标准化,一次实现多平台通吃) | 取决于所选平台 | 显著高于官方 | P1(主推) |
| 虎皮椒 xunhupay | 个人可申请 | 低(有公开文档 + PHP SDK 生态) | 官方清算,无需提现 | 较高 | P2 |
| 支付宝当面付 | 个人可申请(门槛较低) | 中(RSA2 签名,文档完备) | 支付宝官方 | 低(官方费率) | P2 |
| 微信支付 V3 Native | 需营业执照/个体户 | 高(证书 + V3 签名 + AES-GCM 回调解密) | 微信官方 | 低(官方费率) | P3 |
| 支付宝电脑网站支付 | 需企业资质 | 中 | 官方 | 低 | 暂不做(与当面付重叠) |
| 手动到账 | 无 | 极低 | — | — | P0(测试/兜底用) |
费率以各官方公示为准,本表只表达量级关系:官方直连 < 聚合/免签。
2.2 为什么主推「易支付协议」而不是某个平台
「易支付」本质是一套标准化接口协议(EPAY),市面上大量聚合收款平台、自建支付系统都兼容它。做成协议适配器后:
- 换平台 = 改后台的
接口地址 + 商户ID + 商户密钥三个字段,代码零改动; - 同一个适配器同时覆盖蓝兔支付、彩虹易支付及各类自建易支付站;
- 上游平台跑路时,切换成本最低。
选平台的四条硬判据(来源:多篇行业避坑指南,交叉一致):
- 域名 ICP 备案主体必须是「企业」,个人备案或未备案直接排除;
- 结算走对公账户或持牌机构代付,凡要求提现到私人微信/支付宝/银行卡的,本质是资金池,直接排除;
- 运营时间 ≥ 1 年(whois 查域名注册时间 + 搜索引擎查历史评价);
- 小额实测通道:用 0.1~1 元逐条通道实测,付款时收款方显示应为企业主体,出现个人账户或无关企业名即排除。
⚠️ 本计划不指定具体平台——这类平台的稳定性随时间变化,且本助手无法为你担保任何第三方平台的资金安全。上线前请按上述四条自行核验,并先用最小金额跑通一笔完整链路再放开。
2.3 各通道技术要点(供实现时对照)
| 通道 | 下单 | 签名 | 回调验签 | 回调响应 |
|---|---|---|---|---|
| 易支付 V1 | POST {gateway}/submit.php(表单跳转)或 mapi.php(API 取码) | 参数按键名 ASCII 升序拼 k=v&...,末尾拼商户密钥,MD5 后小写 | 同法重算比对 | 输出纯文本 success |
| 易支付 V2 | 同 V1(网关不同) | RSA 签名(商户私钥签、平台公钥验) | 平台公钥验签 | 同上 |
| 虎皮椒 | POST https://api.xunhupay.com/payment/do.html | 参数排序拼接 + appsecret,MD5 | 同法重算比对 | 输出 success |
| 支付宝当面付 | alipay.trade.precreate(生成二维码)/ alipay.trade.query(查单) | RSA2(应用私钥签) | 支付宝公钥验签 | 输出纯文本 success |
| 微信 V3 Native | POST /v3/pay/transactions/native | SHA256withRSA,请求头 Authorization: WECHATPAY2-SHA256-RSA2048 ... | 用微信平台证书验签 + AES-256-GCM 解密 resource | HTTP 200 空体(否则微信重试) |
共同点:下单只负责「拿到支付入口(二维码/跳转 URL)」,到账判断全部依赖异步回调。
三、插件架构设计
3.1 目录结构
plugins/payment_center/
├── plugin.json # 清单(hooks + admin_url + permissions)
├── Plugin.php # 主类:db/ddl/生命周期 + 订单状态机 + 余额账本 + 通道配置读写(含加解密)
├── FrontController.php # 前台:充值中心 / 下单 / 收银台 / 查单 / 回跳 / 回调 / 流水
├── AdminController.php # 后台:订单 / 通道 / 套餐 / 设置 / 退款 / 调账
├── Channel/ # ★ 通道适配器(一通道一文件,统一接口)
│ ├── ChannelInterface.php # 契约:create() / query() / verifyNotify() / refund()
│ ├── ManualChannel.php # 手动到账(P0,测试与兜底)
│ ├── EpayChannel.php # 易支付协议 V1(MD5)(P1)
│ ├── EpayRsaChannel.php # 易支付协议 V2(RSA)(P1,可选)
│ ├── XunhuChannel.php # 虎皮椒(P2)
│ ├── AlipayFaceChannel.php # 支付宝当面付(P2)
│ └── WechatV3Channel.php # 微信支付 V3 Native(P3)
├── hook/
│ ├── init_after.php # 幂等建表兜底(缓存标记 protected/.payment_center_table_cache)
│ ├── route_register.php # 前台路由
│ ├── admin_route_register.php # 后台路由(需 route:admin)
│ ├── nav_plugin_links.php # 左栏「应用」+ 顶栏「应用」下拉入口(16px)
│ ├── nav_user_menu_items.php # PC 下拉 + 移动「我的」(14px,按 placement 分别渲染)
│ ├── layout_head_end.php # 按 $template 条件加载 CSS
│ └── route_after_dispatch.php # 订单超时关闭(节流,非每请求执行)
├── views/
│ ├── index.php # 充值中心(套餐 + 余额卡片 + 最近订单)
│ ├── cashier.php # 收银台(选通道 / 二维码 / 轮询)
│ ├── return.php # 支付回跳落地页(只展示,不到账)
│ ├── orders.php # 我的订单
│ ├── ledger.php # 我的资金流水
│ ├── admin_orders.php # 后台订单管理
│ ├── admin_channels.php # 后台通道配置
│ ├── admin_products.php # 后台套餐管理
│ └── admin_settings.php # 后台其他设置
├── assets/ # style.css / admin.css / script.js(零内联)
├── lang/ # zh.php / en.php / zh_tw.php
├── data/ # 独立库 + certs/(微信证书等,自动防 Web 访问)
└── 说明.md # 插件说明(体例同 points_mall/说明.md)3.2 命名与权限(照规范硬性要求)
- 目录名:
payment_center(含下划线)。 - 命名空间:
Plugin\PaymentCenter(仓内主流写法,命中自动加载器第 ② 条候选camelToSnake('PaymentCenter') = payment_center)。 - ⚠️ 必须在
Plugin.php末尾加class_alias(规范 §4.1):
```php
if (!\class_exists('Plugin\\payment_center\\Plugin', false)) {
\class_alias(Plugin::class, 'Plugin\\payment_center\\Plugin', false);
}
```
否则核心按目录名硬拼类名落空 → uninstall() 被静默跳过 → 卸载留孤儿表。
- plugin.json 的 permissions:
["route:admin", "net:fetch", "system:settings"] route:admin—— 注册后台路由(强制模式下未声明即 DENY)net:fetch—— 调通道 API(走Plugin::http(),自带 SSRF 防护)system:settings—— 若使用Settings::update()写站点级配置- 发布前必跑:
python .workbuddy-ai/tools/plugin_case_lint.py plugins/payment_center(§11.34)
3.3 数据表设计(插件独立库,SQLite 语法)
全部放
plugins/payment_center/data/payment_center.sqlite,不碰核心business.sqlite(规范 §10.1)。
金额一律用「分」存整数,杜绝浮点误差。
-- ① 订单主表
CREATE TABLE IF NOT EXISTS pc_orders (
id INTEGER PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE, -- 本站订单号(不可枚举,见 §6.1-⑪)
user_id INTEGER NOT NULL,
amount_fen INTEGER NOT NULL, -- 应付金额(分)
paid_fen INTEGER NOT NULL DEFAULT 0, -- 实收金额(分,回调回填,用于比对)
subject TEXT NOT NULL, -- 商品标题
channel TEXT NOT NULL, -- 通道标识
channel_trade_no TEXT NOT NULL DEFAULT '', -- 上游交易号
status INTEGER NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已关闭 3已退款 4失败
biz_type TEXT NOT NULL DEFAULT 'balance', -- balance/points/vip/props
biz_value INTEGER NOT NULL DEFAULT 0, -- 兑换值(积分数 / 用户组id / 道具id)
product_id INTEGER NOT NULL DEFAULT 0,
client_ip TEXT NOT NULL DEFAULT '',
paid_at TEXT,
closed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pc_orders_user ON pc_orders (user_id, id DESC);
CREATE INDEX IF NOT EXISTS idx_pc_orders_status ON pc_orders (status, id DESC);
CREATE INDEX IF NOT EXISTS idx_pc_orders_ctime ON pc_orders (created_at);
-- ② 余额账户(一用户一行;若采用「充值即兑积分」方案则本表可省,见 §9.1)
CREATE TABLE IF NOT EXISTS pc_accounts (
user_id INTEGER PRIMARY KEY,
balance_fen INTEGER NOT NULL DEFAULT 0,
total_recharge_fen INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL
);
-- ③ 资金流水(只增不改,审计台账)
CREATE TABLE IF NOT EXISTS pc_ledger (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
delta_fen INTEGER NOT NULL, -- 正=入账,负=出账
balance_after_fen INTEGER NOT NULL, -- 变动后余额(快照,便于核对)
type TEXT NOT NULL, -- recharge/consume/refund/admin_adjust
ref_order_no TEXT NOT NULL DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pc_ledger_user ON pc_ledger (user_id, id DESC);
-- ④ 回调原始日志(排障 + 防重放取证)
CREATE TABLE IF NOT EXISTS pc_notify_log (
id INTEGER PRIMARY KEY,
order_no TEXT NOT NULL,
channel TEXT NOT NULL,
raw TEXT NOT NULL DEFAULT '', -- 原始报文(脱敏后)
verified INTEGER NOT NULL DEFAULT 0, -- 验签是否通过
result TEXT NOT NULL DEFAULT '', -- success/fail/replay/amount_mismatch
ip TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pc_notify_order ON pc_notify_log (order_no, id DESC);
-- ⑤ 通道配置(密钥加密存储)
CREATE TABLE IF NOT EXISTS pc_channels (
channel TEXT PRIMARY KEY, -- epay/xunhupay/alipay_face/wechat_v3/manual
enabled INTEGER NOT NULL DEFAULT 0,
config_enc TEXT NOT NULL DEFAULT '', -- AES-256-GCM 密文(JSON)
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL
);
-- ⑥ 充值套餐
CREATE TABLE IF NOT EXISTS pc_products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
amount_fen INTEGER NOT NULL, -- 售价(分)
biz_type TEXT NOT NULL DEFAULT 'balance', -- balance/points/vip/props
biz_value INTEGER NOT NULL DEFAULT 0, -- 到账值(余额分 / 积分数 / 组id)
sort_order INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL
);关于 pc_notify_log 的唯一约束:不要对 raw 建唯一索引(长文本 + 不同通道重复字段多)。幂等靠订单表条件更新,见 §6.1-⑤。
3.4 通道适配器契约
namespace Plugin\PaymentCenter\Channel;
interface ChannelInterface
{
/** 下单:返回 ['ok'=>bool, 'pay_url'=>'', 'qr_text'=>'', 'raw'=>[], 'error'=>''] */
public function create(array $order, array $cfg): array;
/** 主动查单(用于掉单补偿 / 后台手动同步) */
public function query(string $orderNo, array $cfg): array;
/** 回调验签:返回 ['ok'=>bool, 'order_no'=>'', 'amount_fen'=>0, 'trade_no'=>'', 'error'=>''] */
public function verifyNotify(array $post, array $raw, array $cfg): array;
/** 回调应答体(各通道要求不同:success / JSON / 空体) */
public function notifyAck(): string;
/** 退款(P3 起实现,其余通道返回「不支持」) */
public function refund(array $order, array $cfg): array;
}新增一个通道 = 新增一个类 + 在后台通道列表注册,不改核心、不改其他通道。
四、路由清单
4.1 前台(hook/route_register.php)
| 方法 | 路径 | 说明 | 鉴权 |
|---|---|---|---|
| GET | /pay | 充值中心(套餐 + 余额 + 最近订单) | 登录 |
| GET | /pay/orders | 我的订单(分页) | 登录 |
| GET | /pay/ledger | 我的资金流水 | 登录 |
| POST | /pay/create | 创建订单 → 302 到收银台 | 登录 → CSRF(顺序不可颠倒,§11.28) |
| GET | /pay/cashier/{order_no} | 收银台(选通道 / 展示二维码) | 登录 + 归属校验 |
| GET | /pay/status/{order_no} | 轮询订单状态(AJAX,返回 JSON) | 登录 + 归属校验 |
| GET | /pay/return/{channel} | 同步回跳落地页(只展示,不到账) | 登录 |
| POST | /pay/notify/{channel} | 异步回调(上游服务器调用) | 无登录、无 CSRF,靠验签 ★ |
| POST | /pay/cancel/{order_no} | 用户主动关闭未支付订单 | 登录 → CSRF |
4.2 后台(hook/admin_route_register.php,路径不带 /admin 前缀,§11.22)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /payment | 概览(今日/累计收款、待支付、异常订单)+ 订单列表 |
| GET | /payment/orders | 订单管理(按状态/通道/用户/时间筛选,分页,CSV 导出) |
| POST | /payment/order/close | 手动关闭订单 |
| POST | /payment/order/sync | 手动同步上游状态(查单补偿) |
| POST | /payment/order/refund | 退款(P3,走通道 API + 本地流水) |
| POST | /payment/account/adjust | 手动调账(必写 AuditLog) |
| GET | /payment/channels | 通道配置页 |
| POST | /payment/channels/save | 保存通道配置(含密钥加密) |
| POST | /payment/channels/test | 测试通道连通性 |
| GET | /payment/products | 套餐管理 |
| POST | /payment/products/save \ | /delete \ |
| GET | /payment/settings | 其他设置(订单超时分钟数、最低充值额、是否开启积分兑换…) |
| POST | /payment/settings/save | 保存设置 |
4.3 关于回调路由的三个特殊约定
- 不校验 CSRF(外部服务器不可能持有 token)——这是规范红线 8 的唯一合法例外,替代防护 = 验签 + 金额比对 + 幂等 + 限流 + 日志取证(§6.1)。
- 不
requireLogin(),但必须校验order_no存在、channel匹配、appid/pid属于本站配置。 - 应答体必须符合通道要求(易支付/虎皮椒/支付宝要纯文本
success;微信 V3 要 HTTP 200 空体)。应答体写错 = 上游持续重试 + 后台反复告警。
五、钩子清单(共 7 个)
| 钩子 | 用途 | 关键约束 |
|---|---|---|
init_after | 幂等建表兜底 | 带缓存标记文件,避免每请求 CREATE TABLE |
route_register | 前台路由 | 顶部 if (!isset($router)) return; |
admin_route_register | 后台路由 | 需 route:admin;路径不写 /admin 前缀 |
nav_plugin_links | 「应用」入口(左栏 + 顶栏下拉) | ⚠️ 一次渲染触发两次,钩子文件禁声明顶层函数/类(否则降级 include_once,第二处入口静默消失);图标 16px(mn-fs-16) |
nav_user_menu_items | PC 下拉 + 移动「我的」 | 按 placement(pc/mobile)分别渲染;图标 14px(mn-fs-14),复用 .mn-dropdown-item |
layout_head_end | 按 $template 加载插件 CSS | 渲染钩子:禁 die()/exit()/header();$this 不可用(闭包作用域,§11.24) |
route_after_dispatch | 订单超时关闭(节流执行) | 规范 §11.20 指定的收尾类钩子位置;不得每请求全表扫 |
所有渲染钩子里没有
$this—— 转义一律htmlspecialchars($s, ENT_QUOTES, 'UTF-8'),CSRF 用\app\Helpers\Csrf::token(),登录链接用\app\Helpers\Auth::loginUrl()。
六、核心流程与安全设计
6.1 安全清单(本插件的重点,逐条落地)
① 金额只信服务端
前端只传 product_id 和 channel,金额由服务端反查 pc_products 得出。绝不接受前端传金额。
② 到账只信异步回调
/pay/return 同步回跳只做展示("支付处理中,请稍候" + 轮询)。任何情况下都不在 return 页触发到账。
这是支付系统第一大漏洞:同步回跳可被用户伪造(URL 直接访问),据此发货 = 白嫖。
③ 回调必须验签
按各通道口径重算签名比对(§2.3)。验签失败 → pc_notify_log 记 verified=0 + error_log + 返回失败。不泄露失败原因(防探测)。
④ 回调必须校验业务一致性(三重比对)
order_no 存在 && order.channel === 回调通道 && order.amount_fen === 回调金额 && order.status === 0任一不满足即拒绝。只验签不验金额 = 1 分钱买 100 元套餐。
⑤ 幂等(防重复回调 / 上游重试)
UPDATE pc_orders
SET status = 1, paid_fen = :amount, channel_trade_no = :tno,
paid_at = :now, updated_at = :now
WHERE order_no = :no AND status = 0rowCount() === 0 → 说明已被处理过 → 直接返回通道要求的成功应答(不要报错,否则上游会一直重试)。
到账动作(写余额 + 写流水)与订单状态更新必须在同一个事务内。
⑥ 密钥加密存储
- 通道密钥经 AES-256-GCM 加密后存
pc_channels.config_enc(复用核心Mailer::encryptSmtpPass同款算法范式)。 - 加密密钥解析顺序(与
config.php的MAIL_PASS_KEY同范式):
- 环境变量
FLINTHUB_PAYMENT_KEY protected/payment_key.php(内容<?php return '你的密钥';,不进发行包)- 回退
MAIL_PASS_KEY
- 后台密钥输入框永不回显明文,只显示后 4 位掩码;留空提交 = 保持原值(⚠️ 用
array_key_exists判断,§11.32)。
⑦ 防重放
回调报文带时间戳时,拒绝超过 5 分钟的通知;pc_notify_log 保留原始报文用于取证。
⑧ 限流
- 下单:
RateLimiter::check('payment_create', 10, 60)(每人每分钟 10 单) - 回调:
RateLimiter::check('payment_notify', 60, 60)(防打爆日志表)
⑨ 日志脱敏
pc_notify_log.raw 落库前剔除密钥类字段(key/sign 只留前 4 位)。严禁把明文密钥写进任何日志。
⑩ 后台敏感操作审计
改通道密钥、退款、手动调账 → 一律 \app\Helpers\AuditLog::log($action, $targetType, $targetId, $detail)。
⑪ 订单号不可枚举
order_no = date('YmdHis') . bin2hex(random_bytes(8))。所有按订单号的操作(查状态/取消)必须校验 user_id 归属(防 IDOR)。
⑫ 余额扣减原子化(红线 5)
UPDATE pc_accounts SET balance_fen = balance_fen - :amt
WHERE user_id = :uid AND balance_fen >= :amt检查 rowCount(),为 0 即余额不足。严禁先 SELECT 再 UPDATE。
⑬ 外呼一律走 Plugin::http()
通道 API 调用必须经核心 Plugin::http()(已内置 SSRF 防护、方法白名单、超时控制),禁止自己写 curl。
⑭ 二维码本地渲染
收银台二维码在服务端生成(纯 PHP 生成二维码图片或 data: URL),不引外部二维码 JS(避免 CSP 与供应链风险)。
⚠️ CSP
img-src不含blob:(§11.33)——若走前端生成,必须FileReader.readAsDataURL(),但本方案推荐服务端生成,绕开该坑。
⑮ 事务写法
beginTransaction() / commit() / rollBack(),catch 内先 inTransaction() 再回滚(规范 §10.2)。AJAX 接口任何异常路径都必须返回 JSON。
6.2 主流程时序
【下单】
用户 → POST /pay/create (product_id, channel)
├─ requireLogin() → Csrf::verifyOrDie() ← 顺序不可颠倒
├─ 反查套餐 → 得到 amount_fen(服务端定价)
├─ 生成 order_no,写 pc_orders(status=0)
└─ 调通道 create() → 得到 pay_url / qr_text
→ 302 到 /pay/cashier/{order_no}
【支付】
用户在收银台扫码/跳转 → 上游完成收款
【到账】★ 唯一可信路径
上游 → POST /pay/notify/{channel}
├─ 通道 verifyNotify() 验签
├─ 三重比对(通道/金额/状态)
├─ 事务内:条件 UPDATE 订单(status=0→1) + 写 pc_accounts + 写 pc_ledger
├─ 写 pc_notify_log
└─ 返回通道要求的成功应答
→ 触发 'payment_center_paid' 自定义钩子(供 props/points_mall 等扩展消费)
【回跳】
上游 → GET /pay/return/{channel} → 只展示 + 前端轮询 /pay/status6.3 订单超时关闭
挂 route_after_dispatch,节流执行(文件锁 + 60s 间隔),每次最多关 20 条:
UPDATE pc_orders SET status = 2, closed_at = :now, updated_at = :now
WHERE status = 0 AND created_at < :deadline LIMIT 20超时时长由后台设置项配置(默认 30 分钟)。
七、分阶段实施计划
每阶段结束停下来等你确认再进下一阶段(沿用你的既有节奏)。
P0 —— 地基(可演示,不接真钱)
交付物
- 插件骨架(目录/plugin.json/Plugin.php/hook/views/assets/lang)
- 独立库 6 张表 + 生命周期方法(含
class_alias) - 订单状态机 + 余额账本 + 流水(含原子扣减)
ChannelInterface+ManualChannel(后台点「确认到账」模拟支付成功)- 前台:充值中心 / 收银台 / 我的订单 / 资金流水
- 后台:订单列表 / 套餐管理 / 设置页(Tab 复用核心
.admin-tabs) - 三语语言包 +
说明.md
验收
php -l全过;plugin_case_lint.py全绿- 激活 → 建表成功;卸载 → 表删除、无残留
- 完整闭环:下单 → 手动到账 → 余额增加 → 流水正确 → 订单状态 = 已支付
- 幂等:重复点「确认到账」不重复加钱
- 零内联扫描零命中;i18n 三语键集合零差异
P1 —— 易支付协议(首个真钱通道)
交付物
EpayChannel(V1 MD5 签名)+ 可选EpayRsaChannel(V2 RSA)- 回调
/pay/notify/epay(验签 + 三重比对 + 幂等 +success应答) - 后台通道配置页(密钥加密存储 + 掩码回显 + 连通性测试)
- 订单超时自动关闭
- 下单限流
验收
- 沙箱/小额实付一笔,从下单到到账全链路跑通
- 构造伪造回调(改金额 / 改订单号 / 坏签名)→ 全部被拒 +
pc_notify_log有记录 - 重复投递同一回调 → 只到账一次,且返回成功应答
- 回调响应体格式与平台要求一致(否则上游会重试)
P2 —— 个人可申请的直连通道
交付物
XunhuChannel(虎皮椒)AlipayFaceChannel(支付宝当面付,RSA2 + 支付宝公钥验签)- 收银台支持多通道并列展示
验收
- 两通道各实付一笔跑通
- 支付宝公钥验签用真实回调报文验证
P3 —— 微信直连 + 退款 + 对账
交付物
WechatV3Channel(Native 下单 + 平台证书验签 + AES-256-GCM 解resource)- 商户证书存放
data/certs/(已防 Web 访问),后台可上传/更换 - 退款(
refund()实现)+ 退款流水 - 掉单补偿(定时查单)+ 每日对账差异告警
验收
- 微信实付一笔 + 退款一笔,资金与流水双向一致
- 掉单场景模拟:先删本地订单状态再回滚 → 查单补偿能纠正
P4 —— 增值(可选,按需排期)
- 余额 → 兑换积分(走
Points::award())、购买 VIP(走User::updateGroupId())、购买道具 - 触发自定义钩子
payment_center_paid,供props/points_mall等插件监听消费 - 充值卡密 / 优惠券 / 首充双倍
- 统计报表(按日/通道/套餐)
- CSV 导出对账
八、验证方案(对齐规范 §12)
| 类别 | 手段 | 通过标准 |
|---|---|---|
| 语法 | php -l 全部新文件 | 零错误 |
| 命名空间 | .workbuddy-ai/tools/plugin_case_lint.py | Linux 精确匹配全绿 |
| 建表/卸载 | 激活 → 查表 → 卸载 → 查表 | 表存在→表删除、无残留 |
| 资金一致性 | CLI 探针(复用单 PDO 连接) | 订单总额 = 流水总额 = 余额总额(三向对平) |
| 幂等 | 同一回调投递 3 次 | 余额只加一次 |
| 伪造回调 | 改金额/改签名/改订单号 各 1 次 | 全部拒绝且日志留痕 |
| 越权 | 用户 A 访问用户 B 的订单/收银台 | 404(且自设 http_response_code(404),非软 404) |
| CSRF | 无 token 提交下单 | 被拒 |
| 登录回跳 | 会话过期后提交 | 跳登录且能回跳原页 |
| 零内联 | `grep -nE '<style\ | <script>\ |
| i18n | 视图/控制器键 vs 三语字典 | 键集合零差异 |
| 性能 | 每请求钩子查询总数 | ≤ 5 次(红线) |
| 全角标点 | .workbuddy-ai/tools/php_var_cjk_check.php | 零命中 |
| HTTP 冒烟 | php -S + --noproxy '*' | 路由 200 / 回调应答格式正确 |
| 双主题 | 浅色 + 暗夜星辰 | 可读性正常、无硬编码色 |
⚠️ 探针数据只按自己创建的订单号/id 清理,禁按硬编码 id 或
user_id IN (...)批量删(§D)。用户正在自测时不清页面缓存、不删数据。
九、待你拍板的问题
9.1 余额的落点(影响架构,先定这个)
| 方案 | 做法 | 优点 | 代价 |
|---|---|---|---|
| A. 独立余额账本 | 充值进 pc_accounts.balance_fen,消费时扣减 | 账目清晰、可退款、可对账;不污染核心积分 | 多一套账;需处理与积分的兑换关系 |
| B. 充值即兑积分 | 充值成功后直接 Points::award() 加积分,不设余额 | 复用现有积分体系与商城,改动最小 | 无「余额」概念,退不了款;积分与钱强绑定,积分通胀时账目说不清 |
| C. 混合 | 余额为主,支持「余额 → 积分」兑换 | 灵活 | 最复杂 |
我的建议:A。理由是资金台账必须独立于积分体系(积分会被各种玩法增发,混在一起永远对不平账)。若你只想做「赞助换积分」,选 B 最省事。
9.2 通道优先级
P1 先做易支付协议(覆盖面最广),P2 做虎皮椒 + 支付宝当面付,P3 微信。是否认可这个顺序? 若你有企业/个体户资质,可以把微信提到 P2。
9.3 合规定位
一期定位为「赞助/打赏 + 站内虚拟消费」还是「钱包/充值余额」?
前者页面文案与功能设计更保守(不可提现、不可退款、小额),后者功能更完整但风险更高。这一条建议你先想清楚,它决定前台文案与产品边界。
9.4 是否要「余额 → 积分 / VIP / 道具」的兑换
若要,需要在 P4 增加与 props / points_mall 的联动(经核心钩子 payment_center_paid 解耦,不直连插件表)。
十、风险清单
| 风险 | 等级 | 应对 |
|---|---|---|
| 聚合平台跑路 / 冻结资金 | 高 | 协议适配器解耦,可随时换平台;按 §2.2 四条判据选型;小额实测后放开 |
| 个人站收款被认定资金池/二清 | 高 | 明示不可提现;保留完整台账;小额化;有商业化诉求时走持牌机构 |
| 同步回跳被伪造导致白嫖 | 高 | 到账只信异步回调(§6.1-②),写进代码注释与验收用例 |
| 回调漏处理 / 上游重试风暴 | 中 | 幂等条件更新 + 正确应答体 + 限流 |
| 密钥泄露 | 中 | AES-256-GCM 加密 + 密钥文件不进发行包 + 日志脱敏 |
| 上游 API 变更 | 中 | 通道适配器隔离,改动收敛在单文件 |
| 微信 V3 证书轮换 | 低 | 平台证书自动下载 + 后台手动更新入口 |
| 订单表膨胀 | 低 | 索引 + 后台归档(按年分表或定期清理已完成订单) |
十一、工作量粗估(相对量级,非承诺工时)
| 阶段 | 相对量 | 说明 |
|---|---|---|
| P0 | ████████ | 骨架 + 6 张表 + 状态机 + 前后台页面,工作量最大的一块 |
| P1 | ██████ | 易支付适配器 + 回调验签 + 通道配置页 |
| P2 | ████ | 两个直连通道(签名/验签各自不同) |
| P3 | ██████ | 微信 V3(证书 + AES-GCM 解密)最难,加退款与对账 |
| P4 | ████ | 增值功能,可拆散做 |
本计划仅覆盖设计,未创建任何插件目录或代码。确认后再进入 P0。