安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
6.6 KiB
Paycenter — 支付中心插件
把「怎么收钱」从业务里剥离:插件负责验签、下单、查单、回调、退款;业务(应用市场等) 只负责建订单和收「已支付」事件后发货。支付宝与微信并列一等公民,业务侧代码不区分渠道。
一、边界(谁管什么)
| 层 | 归属 | 说明 |
|---|---|---|
业务订单 appmall_addon_orders |
业务(不动) | 建单、授权签发、分红、退款冲正 |
支付流水 wxapp_paycenter_trade |
本插件 | 每笔下单/回调/查单留痕,可独立对账 |
| 验签 / 网关参数 / 场景差异 | 本插件 | 业务代码里不该出现任何 Pay::alipay() |
二、安装
composer require yansongda/pay # ^3.0
php think addon:manage paycenter -a develop # 开发模式安装:建表 + 注入菜单 + 启用
php think paycenter:check --ping # 自检(--channel=alipay|wechat|all)
三、配置
密钥只走 .env(不进数据库),后台配置页(/backend/addon/setting?addon=paycenter)
只放开关与默认场景:
| 配置项 | 说明 |
|---|---|
alipay_enable / wechat_enable |
渠道开关(未配置商户参数时自动不可用) |
default_channel |
业务未指定渠道时用哪个 |
alipay_scene / wechat_scene |
默认场景 |
.env 键(模板见 .env.example):PAY_ALIPAY_*(含三份证书路径)、
PAY_WECHAT_MCH_ID/APP_ID/SECRET_KEY/SECRET_CERT/PUBLIC_CERT_PATH、
PAY_HTTP_CA(Windows 无 CA 包时报 cURL error 60 时用)。
四、业务侧怎么用
// 1) 拉起支付
$pay = \addon\paycenter\service\Payment::create('alipay', [
'trade_no' => $order['trade_no'],
'amount' => 99.00,
'subject' => '购买插件:xxx',
'biz' => 'appmall', // 业务标识(回调用)
'biz_id' => $order['id'],
'attach' => ['aid' => 1], // 可选,JSON 存流水
], 'web', [
'notify' => 'https://host/paycenter/api/notify?channel=alipay', // 留空则用插件默认
'return' => 'https://host/xxx/payResult',
]);
// $pay = ['type' => 'form|qrcode|url|params', 'content' => ..., 'trade_no' => ...]
// 2) 查单(回调丢了时兜底)
Payment::query('alipay', $tradeNo); // ['paid'=>bool,'amount'=>float,'gateway_no'=>string]
// 3) 退款(按流水自动认渠道,无需业务记录支付方式)
Payment::refund($tradeNo);
// 4) 可用支付方式(收银台渲染)
Payment::methods(); // [['code'=>'alipay','name'=>'支付宝','scenes'=>[...],'scene'=>'web']]
发货:监听 PayPaid 事件
插件验签通过后触发(事件名 Payment::EVENT_PAID)。在 app/event.php 注册监听器:
'PayPaid' => [ app\appmall\listener\OrderPaid::class ],
事件载荷:trade_no / channel / amount / gateway_no / biz / biz_id / attach / scene / trade。
监听器要做三件事:金额校验 → 幂等判断 → 发货(参考 app\appmall\listener\OrderPaid)。
五、场景映射
| 渠道 | scene | 网关产品 | 返回 type |
|---|---|---|---|
| 支付宝 | web |
电脑网站支付 | form(自动提交表单 HTML) |
| 支付宝 | wap |
手机网站支付 | form |
| 支付宝 | qr |
当面付扫码 | qrcode |
| 支付宝 | app |
App 支付 | params |
| 微信 | qr |
Native 扫码 | qrcode(code_url) |
| 微信 | h5 |
H5 | url(h5_url) |
| 微信 | mp |
公众号 JSAPI | params(需 openid) |
| 微信 | mini |
小程序 | params(需 openid) |
| 微信 | app |
App | params |
六、回调地址
统一入口:POST /paycenter/api/notify?channel=alipay|wechat(免登录)。
业务侧下单时把 notify_url 指向它即可;应用市场原先的 /api/addon/notify
保留转发(内部同样委托 Payment::notify())。
七、目录结构
addon/paycenter/
├─ service/Payment.php 门面:methods / create / query / notify / refund
├─ library/contract/Channel.php 渠道接口(新增渠道实现它 + 登记到 Payment::CHANNELS)
├─ library/AlipayChannel.php / WechatChannel.php
├─ library/PayFactory.php SDK 装配(两级配置、CA、日志)
├─ library/PayConfig.php 配置合并(插件静态配置 ← 核心 config/pay.php 覆盖)
├─ library/TradeLog.php 流水读写
├─ library/Schema.php 建表/补列自愈(事实源 install.sql)
├─ command/Check.php php think paycenter:check
├─ controller/api/Notify.php 回调入口
├─ controller/backend/Trade.php + view/backend/trade/index.html 后台交易流水
└─ controller/backend/Channel.php + view/backend/channel/index.html 渠道状态(只读诊断)
菜单约定(踩过的坑)
AddonService::createMenu() 会把 menu.json 的 backend 段挂到「应用 → 插件名目录」下,
当 backend 只有一项且该项带 child 时,框架把它当「包裹层」展开,只注入它的子项
(表现为:侧栏只剩按钮节点,真正的菜单页消失)。因此本插件保留了两个顶层项
(交易流水 + 渠道状态),菜单项与按钮节点的层级才是正常的:
应用
└ 支付中心 paycenter:backend (目录,框架自动生成)
├ 交易流水 paycenter:backend:trade/index (菜单)
│ ├ 流水数据 / 详情 / 主动查单 / 发起退款 paycenter:backend:trade:* (按钮)
└ 渠道状态 paycenter:backend:channel/index(菜单)
权限名必须形如 <插件>:backend:<控制器>/<动作> 或 <插件>:backend:<控制器>:<动作>,
与 ywxapp\library\Auth 的候选匹配保持一致,否则非超管访问会 403。
改完 menu.json 后执行 php think addon:repair paycenter --menu 重建。
八、已知坑(都已在代码里处理)
- SDK 配置是两级的:必须
['alipay' => ['default' => [...]]],直接传扁平config('pay.alipay')取不到。 - 每单回调地址只能走 params:支付宝
_notify_url/_return_url、微信notify_url; 写进配置会被容器忽略(Artful::config()已存在时直接 return)。 - 私钥 PKCS8/PKCS1:PKCS8 必须自带
PRIVATE KEY头,PKCS1 必须交裸 base64 让 SDK 包 RSA 头, 反了就会openssl_sign(): Supplied key param cannot be coerced into a private key。 - 证书相对路径:SDK 按进程 CWD 解析,Web 入口 CWD 是
public/→ 已在配置层统一转绝对路径。 - 微信金额单位是分,且必须是整数。
- 微信没有官方沙箱:
mode恒为 0,验证只能真实小额支付后退款。