--- name: unipay description: "Sage unipay 支付模块开发 — 微信Native/H5、转账邮件、QR生成、DSPY编写" version: 1.0.0 author: Hermes Agent platforms: [linux] metadata: hermes: tags: [unipay, wechat, payment, transfer, dspy, sage] related_skills: [supplychain-pitfalls, pricing-data-format] --- # unipay 支付模块开发 ## 微信支付 ### Native 扫码 vs H5 跳转 - **Native** (`/v3/pay/transactions/native`): 返回 `code_url` (weixin://...),不能被浏览器直接打开。需要生成二维码让用户扫码。 - **H5** (`/v3/pay/transactions/h5`): 返回 `h5_url`,浏览器可直接打开跳转。 ### 二维码生成 ```python # 在 init.py 的 create_payment 中,对 wechat 生成 QR: if provider == 'wechat': qrpath = await env.data2qrpath(res) # res = Native code_url res = env.entire_url('/idfile') + '?path=' + env.quote(qrpath) ``` `data2qrpath()` 已注册在 ServerEnv,生成 `files/` 目录下的 PNG,返回 webpath。 ### recharge.dspy 前端 微信返回 VBox+Image 显示二维码;支付宝/转账返回 NewWindow 跳转。 ```python if params_kw.provider == 'wechat': return {"widgettype": "VBox", ..., "subwidgets": [ {"widgettype": "Image", "options": {"url": url}}, {"widgettype": "Text", "options": {"text": "请使用微信扫描二维码完成支付"}} ]} return {"widgettype": "NewWindow", "options": {"url": url, ...}} ``` ## 转账邮件处理 ### 邮件正则 ```python # 金额: "交易金额:¥1.00" — 注意 ¥ 符号 match = re.search(r'交易金额\s*[::]\s*¥?\s*(\d+(?:\.\d+)?)', mail.body) # 转账码: "摘要:1912412" — 7 位数字 match = re.search(r'摘要[::]\s*(\d{7})(?!\d)', mail.body) ``` ### check_transfer 逻辑 - **provider.check_transfer(tcode, env)** — 返回 `(title, message)` - 先查 `transfercode(status='1')` → 已入账直接返回 - 扫描最近 2 天邮件,新→旧,遇旧邮件 `break` - 所有匹配 code 且 `transfercode.status='0'` 的都入账 - DSPY 只调用 `provider.check_transfer(tcode, env)`,逻辑全在 Python ### 后台轮询 - `add_startup(run)` 会阻塞服务启动(无限循环) - 改为手动按钮触发:`transfer_info.ui` → "我已完成转账" → `transfer_check.dspy` ### EmailClient 在 `transfer.py` 中定义,DSPY 无法直接访问。必须在 Python 方法中封装逻辑,DSPY 通过 `env.PROVIDERS['transfer']` 间接调用。 ## Provider 注册 所有 provider 通过 `load_unipay()` 注册到 `PROVIDERS` dict,然后暴露到 ServerEnv: ```python env.PROVIDERS = PROVIDERS # DSPY 中通过 env.PROVIDERS.get('transfer') 访问 ``` ## 微信支付故障排查:APPID_MCHID_NOT_MATCH\n\n### 症状\n微信 Native 支付返回 `APPID_MCHID_NOT_MATCH`:\n```\nException: ret={'code': 'APPID_MCHID_NOT_MATCH', 'message': 'appid和mch_id不匹配'}\n```\n\n### 排查步骤\n\n1. **用 xxd 验证环境变量无隐藏字符**:\n ```bash\n grep 'WXP_APPID' start.sh | xxd | head -3\n # 正常:3d 77 78 38 39 32 ... (=wx892...) → 无 BOM、无特殊空格\n ```\n\n2. **检查证书归属**:\n ```bash\n openssl x509 -in apiclient_cert.pem -noout -subject -serial\n # subject 中 CN = <商户号> — 证书绑定商户号,非 appid\n # serial 必须匹配 WXP_SERIAL 环境变量\n ```\n\n3. **确认微信商户平台 AppID 绑定**:\n - 登录 pay.weixin.qq.com → 产品中心 → AppID账号管理\n - 确认目标 appid 状态为\"已授权\"(非\"待授权\")\n - 同一商户号可绑定多个 appid(不同应用),每个都需独立授权\n\n4. **用已验证的 appid 对比测试**:临时切到已知可用的 appid,确认 unipay 模块本身没问题,再排查 appid 绑定。\n\n### 常见根因\n- 环境变量配了正确的 appid 但该 appid 与商户号的绑定未在微信平台完成\n- 环境变量有不可见字符(BOM、全角空格)— `xxd` 可检出\n- 证书的商户号与请求中的 mchid 不一致(证书绑定商户,非 appid)\n\n## DSPY 编写规则 见 `supplychain-pitfalls` 技能。关键: - **禁止 import** - **禁止 f-string**(exec 上下文中 `{}` 与模板语法冲突,报 `SyntaxError: '{' was never closed`) - 用 `+` 拼接字符串或 `.format()`