版本:1.0.0 | 作者:FlintHub |
功能:通过聚合支付 / 官方直连通道接收用户赞助,赞助成功后自动兑换为站点积分。 支持易支付协议(EPAY)、虎皮椒、支付宝当面付、微信支付 V3 与手动到账五种通道。
定位:赞助 / 打赏 —— 不产生站内余额,不可提现,从产品层面规避资金池与二清风险。
一、功能简介
1.1 前台
| 页面 | 路径 | 说明 |
|---|---|---|
| 赞助中心 | /sponsor |
档位列表 + 我的积分 + 最近赞助 |
| 我的赞助记录 | /sponsor/orders |
分页列表,可继续支付 / 查看详情 |
| 收银台 | /sponsor/cashier/{订单号} |
选择支付方式 → 扫码 / 跳转 → 轮询到账 |
| 支付结果 | /sponsor/result/{订单号} |
展示状态;待支付时自动轮询刷新 |
1.2 后台(/admin/sponsor)
| 页面 | 路径 | 说明 |
|---|---|---|
| 概览 + 订单 | /admin/sponsor |
四项统计、订单筛选、手动确认到账 / 关闭 / 补发积分、最近回调日志 |
| 通道配置 | /admin/sponsor/channels |
五通道密钥配置(加密存储)、启用停用、回调地址展示、订单超时设置 |
| 档位管理 | /admin/sponsor/products |
档位增删改、启停、排序 |
三页之间靠页内导航(.pc-tabs)互跳 —— 核心插件卡片的「设置」按钮进的是
admin_url = /admin/sponsor(订单页),配置页不在卡片上,必须靠页内导航进入。
导航样式在插件自己的 assets/admin.css(核心无全局 Tab 组件,不改核心资源)。
1.3 支付通道
| 通道标识 | 名称 | 资质要求 | 说明 |
|---|---|---|---|
epay |
易支付(聚合) | 无(选平台) | 兼容 EPAY 协议的聚合平台;换平台只改后台三个字段,代码零改动 |
xunhupay |
虎皮椒 | 个人可申请 | 免签通道,官方清算 |
alipay_face |
支付宝当面付 | 个人可申请 | 官方直连,RSA2 签名 + 支付宝公钥验签 |
wechat_v3 |
微信支付 V3 | 需商户资质 | Native 扫码,平台公钥验签 + AES-256-GCM 解回调 |
manual |
手动到账 | 无 | 线下赞助,管理员后台确认;未配置在线通道时的兜底 |
二、目录结构
plugins/payment_center/
├── plugin.json # 插件清单(6 个钩子 + admin_url + permissions)
├── Plugin.php # 主类:建表/迁移、订单状态机、通道配置加解密、积分发放(幂等)
├── FrontController.php # 前台:赞助中心 / 下单 / 收银台 / 轮询 / 回跳 / 异步回调
├── AdminController.php # 后台:订单 / 通道 / 档位 / 设置(继承 Admin\BaseController)
├── Channel/ # 通道适配器
│ ├── ChannelInterface.php # 契约:create / query / verifyNotify / notifyAck / refund
│ ├── ChannelFactory.php # 通道标识 → 实现类映射
│ ├── ChannelUtil.php # 签名 / 验签 / 密钥规范化 / HTTP / 金额换算
│ ├── ManualChannel.php # 手动到账
│ ├── EpayChannel.php # 易支付协议 V1(MD5)
│ ├── XunhuChannel.php # 虎皮椒
│ ├── AlipayFaceChannel.php # 支付宝当面付(RSA2)
│ └── WechatV3Channel.php # 微信支付 V3 Native
├── hook/
│ ├── init_after.php # 幂等建表兜底(版本化 marker 门控)
│ ├── route_register.php # 前台路由
│ ├── admin_route_register.php # 后台路由(不写 /admin 前缀)
│ ├── nav_plugin_links.php # 「应用」入口(双触发)
│ ├── nav_user_menu_items.php # 用户菜单入口(PC + 移动,双触发)
│ └── route_after_dispatch.php # 订单超时关闭(60s 节流 + 文件锁)
├── views/ # 9 个视图(前台 4 + 后台 3 + 共用)
├── assets/ # style.css / admin.css / script.js(零内联)
├── lang/ # zh / en / zh_tw(各 168 键,键集合与键序零差异)
└── data/ # 独立库 + 建表 marker + 节流/锁文件(自动防 Web 访问)
三、数据表(插件独立库)
库文件:plugins/payment_center/data/payment_center.sqlite
| 表 | 用途 | 关键字段 |
|---|---|---|
pc_orders |
订单主表(也是资金台账) | order_no(UNIQUE) / amount_fen / paid_fen / channel / status / biz_value / points_awarded |
pc_notify_log |
回调原始报文(排障 + 取证) | order_no / channel / raw(脱敏) / verified / result / ip |
pc_channels |
通道配置 | channel(PK) / enabled / config_enc(AES-256-GCM 密文) |
pc_products |
赞助档位 | name / amount_fen / biz_type / biz_value / enabled |
pc_settings |
插件设置 | skey / sval(订单超时时长等) |
订单状态:0 待支付 | 1 已支付(已到账) | 2 已关闭 | 3 已退款(预留) | 4 支付失败
⚠️ 金额一律以「分」存整数,杜绝浮点误差。 ⚠️ 本插件不建余额表 —— 定位是「赞助换积分」,不产生站内余额。
四、路由清单
4.1 前台(hook/route_register.php)
| 方法 | 路径 | 鉴权 |
|---|---|---|
| GET | /sponsor |
登录 |
| GET | /sponsor/orders |
登录 |
| GET | /sponsor/cashier/{orderNo} |
登录 + 归属校验 |
| GET | /sponsor/result/{orderNo} |
登录 + 归属校验 |
| POST | /sponsor/create |
登录 → CSRF |
| POST | /sponsor/pay/{orderNo} |
登录(JSON 401) → CSRF |
| POST | /sponsor/cancel/{orderNo} |
登录 → CSRF |
| GET | /sponsor/status/{orderNo} |
登录(只读) |
| GET | /sponsor/return/{channel} |
登录(只跳转,不到账) |
| POST | /sponsor/notify/{channel} |
无登录、无 CSRF,靠验签 ★ |
4.2 后台(hook/admin_route_register.php,不写 /admin 前缀)
/sponsor(概览+订单)、/sponsor/order/close|confirm|retry-award、
/sponsor/channels + /save|toggle、/sponsor/settings/save、
/sponsor/products + /save|delete|toggle
五、核心规则(改这个插件前必读)
5.1 ★ 到账只信异步回调
- 唯一可信入口 =
POST /sponsor/notify/{channel},必须通过验签 + 三重比对。 /sponsor/return/{channel}与/sponsor/result/{orderNo}只做展示,绝不到账。同步回跳 URL 可被用户直接访问伪造,据它发货就是白嫖。这是支付系统第一大坑。
5.2 回调的替代防护(规范红线 8 的唯一合法例外)
外部服务器无法持有 CSRF token,故回调不校验 CSRF,替代防护为:
- 验签(各通道口径不同,见
Channel/*.php) - 三重比对:订单号存在 + 通道匹配 + 金额一致 + 状态为待支付
- 幂等:
UPDATE ... WHERE order_no = :no AND status = 0+rowCount() - 限流:
RateLimiter60 次/分钟 - 取证:原始报文(脱敏后)落
pc_notify_log
⚠️ 只验签不验金额 = 「1 分钱买 100 元档位」。
5.3 幂等与跨库事务
订单在插件库、积分在核心库 —— 两个独立 SQLite 文件,无法跨库事务。因此顺序固定为:
① 条件 UPDATE 订单 status 0→1(原子闸门,重复回调只有第一次能过)
② 发积分(失败则 points_awarded 保持 0,后台「补发积分」可重试)
发积分前会查 points_log(related_id + related_type='payment_center') 去重,
防「发放成功但标记失败」的窗口重复发。
5.4 金额只信服务端
前端只传 product_id + channel,金额由服务端反查 pc_products 得出。绝不接受前端传金额。
5.5 密钥加密
- 通道密钥经 AES-256-GCM 加密后存
pc_channels.config_enc。 - 加密密钥解析顺序:
FLINTHUB_PAYMENT_KEY环境变量 →protected/payment_key.php→ 回退MAIL_PASS_KEY(后台会提示加固)。 - 后台永不回显明文,只显示「已配置 / 未配置」;留空提交 = 保持原值。
5.6 订单号不可枚举
SP + yyyyMMddHHmmss + 10 位随机十六进制。所有按订单号的操作必须校验 user_id 归属(防 IDOR)。
六、注意事项(踩过的坑)
- 路由占位符名必须与方法参数名逐字一致 ——
Router::dispatch()用call_user_func_array($handler, $params)传关联数组,PHP 8 下按参数名匹配:{order_no}配$orderNo会抛Error: Unknown named parameter。故本插件统一用{orderNo}。 - 目录名含下划线 →
Plugin.php末尾必须class_alias(核心按目录名硬拼类名, 否则生命周期三方法静默跳过 → 卸载留孤儿表)。 - 后台控制器必须继承
app\Controllers\Admin\BaseController(构造函数内含管理员门禁 + 二次验证); 继承app\Core\Controller会绕过后台门禁。 - 建表 marker 带版本号(
data/.schema.v1.ok)—— 表结构变更时必须递增, 否则老安装不会重跑activate()。每个后台入口另有ensureTables()兜底。 nav_plugin_links双触发 —— 该钩子文件禁声明顶层函数/类,否则降级为include_once, 第二处入口静默消失。route_after_dispatch必须节流 —— 该钩子每请求触发,未加节流会把超时关闭变成每请求全表扫。- 微信 V3 回调应答必须是 HTTP 200 + 空体,否则微信会持续重试。
- ★
RateLimiter::check()的契约是true=允许 / false=超限(app/Helpers/RateLimiter.php:223)—— 写成if (RateLimiter::check(...)) { 拒绝 }会把每一次请求都拒掉。本插件下单与回调两处 都曾写反:回调恒得429 busy→ 资金链路 100% 不到账。它不报错、error.log无痕、 静态检查看不出,只有发真实 HTTP 才能发现。 - ★ 文本列比较禁用
(int)强转 ——(int)'points' === 0,于是if ((int)($order['biz_type'] ?? 'points') !== 'points') return;会恒真提前返回,积分一列不发。biz_type是 TEXT 列,必须用(string)比较。 - 发布前必跑:
python .workbuddy-ai/tools/plugin_case_lint.py plugins/payment_center.workbuddy-ai/tools/php_var_cjk_check.php plugins/payment_center- 零内联扫描 + 三语键集合比对
七、上线前检查清单
- 聚合支付平台按四条判据核验:企业备案 / 对公结算 / 运营满 1 年 / 小额实测通道
- 先配
manual通道跑通一笔,再启用在线通道 - 每个通道小额实付一笔,确认从下单到积分到账全链路
- 后台 → 通道配置页复制「回调地址」填到支付平台后台
- 配置独立加密密钥(
protected/payment_key.php或FLINTHUB_PAYMENT_KEY) - 前台文案已按「赞助 / 打赏」定位(不出现「充值」「余额」「提现」)
- 部署后执行一次后台「维护 → 清理缓存」