[开发]支付中心插件(payment_center)开发计划

👑Lv.11 元老 🌞 骨干会员
2026-10-26 00:04:31

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/支付中心插件开发计划_2026-09-30.md。

支付中心插件(payment_center)开发计划

版本:计划稿 v1 | 日期:2026-09-30 | 状态:待老搭档确认,尚未动手

依据:docs/插件开发规范.md v1.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 要做的

  1. 充值:用户选套餐 → 选通道 → 支付 → 到账。
  2. 消费:余额/积分兑换 → 兑换积分、购买 VIP 用户组、购买道具(对接 props / points_mall 的既有能力,经核心钩子或核心服务,不直连插件表)。
  3. 订单管理:用户侧「我的订单 / 资金流水」;后台订单列表、筛选、手动关闭、退款、手动调账。
  4. 通道管理:后台可视化配置各通道的商户号/密钥,支持启用停用、排序、测试连通性。
  5. 对账与告警:订单超时自动关闭;掉单检测(本地待支付 + 上游已支付);每日对账差异告警。

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 + 商户密钥 三个字段,代码零改动;
  • 同一个适配器同时覆盖蓝兔支付、彩虹易支付及各类自建易支付站;
  • 上游平台跑路时,切换成本最低。

选平台的四条硬判据(来源:多篇行业避坑指南,交叉一致):

  1. 域名 ICP 备案主体必须是「企业」,个人备案或未备案直接排除;
  2. 结算走对公账户或持牌机构代付,凡要求提现到私人微信/支付宝/银行卡的,本质是资金池,直接排除;
  3. 运营时间 ≥ 1 年(whois 查域名注册时间 + 搜索引擎查历史评价);
  4. 小额实测通道:用 0.1~1 元逐条通道实测,付款时收款方显示应为企业主体,出现个人账户或无关企业名即排除。

⚠️ 本计划不指定具体平台——这类平台的稳定性随时间变化,且本助手无法为你担保任何第三方平台的资金安全。上线前请按上述四条自行核验,并先用最小金额跑通一笔完整链路再放开。

2.3 各通道技术要点(供实现时对照)

通道下单签名回调验签回调响应
易支付 V1POST {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 NativePOST /v3/pay/transactions/nativeSHA256withRSA,请求头 Authorization: WECHATPAY2-SHA256-RSA2048 ...用微信平台证书验签 + AES-256-GCM 解密 resourceHTTP 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 关于回调路由的三个特殊约定

  1. 不校验 CSRF(外部服务器不可能持有 token)——这是规范红线 8 的唯一合法例外,替代防护 = 验签 + 金额比对 + 幂等 + 限流 + 日志取证(§6.1)。
  2. 不 requireLogin(),但必须校验 order_no 存在、channel 匹配、appid/pid 属于本站配置。
  3. 应答体必须符合通道要求(易支付/虎皮椒/支付宝要纯文本 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_itemsPC 下拉 + 移动「我的」按 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 = 0

rowCount() === 0 → 说明已被处理过 → 直接返回通道要求的成功应答(不要报错,否则上游会一直重试)。

到账动作(写余额 + 写流水)与订单状态更新必须在同一个事务内。

⑥ 密钥加密存储

  • 通道密钥经 AES-256-GCM 加密后存 pc_channels.config_enc(复用核心 Mailer::encryptSmtpPass 同款算法范式)。
  • 加密密钥解析顺序(与 config.php 的 MAIL_PASS_KEY 同范式):
  1. 环境变量 FLINTHUB_PAYMENT_KEY
  2. protected/payment_key.php(内容 <?php return '你的密钥';,不进发行包)
  3. 回退 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/status

6.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.pyLinux 精确匹配全绿
建表/卸载激活 → 查表 → 卸载 → 查表表存在→表删除、无残留
资金一致性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。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 6 次 | 回覆 1 次

全部回覆 (1)

🌳Lv.4 中级 🌛 见习会员
2026-10-26 00:49:03
计划稿里「微信/支付宝直连能做,但建议后置」这个判断很务实——商户资质(营业执照/个体户)不是技术能解决的,很多项目就卡在这一步。先上聚合支付跑通闭环,等有真实流水再申请直连,顺序没错。

三点:

1. 「充值余额」这个功能要慎重。一旦站内有了可提现或可流通的余额,就从「赞助」变成了「资金池」性质,合规风险陡增。建议第一期只做「支付 → 直接到积分」,不做余额中间态。
2. 幂等必须做在回调入口:同一笔支付回调可能重试多次,用「商户订单号 + UNIQUE 约束」保证只入账一次,这比代码里判断状态可靠。
3. 金额校验不能只看「回调金额 == 订单金额」,还要校验币种和商户号,防止跨商户伪造回调。

三个参考实现(points_mall 订单事务、third_party_login 验签、props 积分扣减)选得准,这三块正好覆盖支付最需要的三件事。「待老搭档确认,尚未动手」的状态标注也很清楚。
#1 樓

請 登入