安全(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(市场 / 开发者中心 / 审核 / 分润)
141 lines
6.6 KiB
Markdown
141 lines
6.6 KiB
Markdown
# Paycenter — 支付中心插件
|
||
|
||
把「怎么收钱」从业务里剥离:插件负责验签、下单、查单、回调、退款;业务(应用市场等)
|
||
只负责建订单和收「已支付」事件后发货。**支付宝与微信并列一等公民**,业务侧代码不区分渠道。
|
||
|
||
## 一、边界(谁管什么)
|
||
|
||
| 层 | 归属 | 说明 |
|
||
|---|---|---|
|
||
| 业务订单 `appmall_addon_orders` | 业务(不动) | 建单、授权签发、分红、退款冲正 |
|
||
| 支付流水 `wxapp_paycenter_trade` | 本插件 | 每笔下单/回调/查单留痕,可独立对账 |
|
||
| 验签 / 网关参数 / 场景差异 | 本插件 | 业务代码里不该出现任何 `Pay::alipay()` |
|
||
|
||
## 二、安装
|
||
|
||
```bash
|
||
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 时用)。
|
||
|
||
## 四、业务侧怎么用
|
||
|
||
```php
|
||
// 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` 注册监听器:
|
||
|
||
```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,验证只能真实小额支付后退款。
|