Files
YwxAppThink/addon/paycenter/README.md
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

6.6 KiB
Raw Blame History

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,验证只能真实小额支付后退款。