# Quefa 自建兑换页面开发指南 版本:2026-09-26,联调版。此文档可提供给代理商;不要将平台内部配置、数据库或部署环境文件一并交付。 ## 一、能力与接入条件 代理商可在自己的域名上搭建直接充值、CDK 兑换与结果查询页面。链路为“客户浏览器 → 代理商后端 → Quefa 接口”,结果原路返回。没有开卡、卡信息查询或支付工具管理接口。 代理商不会收到底层服务地址、密钥、原始兑换凭证、内部订单编号或原始错误正文。界面可采用自己的品牌,但不得虚构与其他品牌的官方合作关系。 平台需提供实际联调地址、`partner_id`、`key_id`、`client_secret`、授权商品和测试订单/CDK,并开通 `customRedemptionEnabled`。文中的 `http://127.0.0.1:3200` 仅为本地演示。需要回调时另行注册地址并领取 Webhook 密钥。 网页工作台与 API 共用商品、订单、余额,但账号密码、Cookie 与 API 密钥不能混用。本版工作台面向代理与平台人员,不提供公众零售账号注册。 ## 二、下单和资金模式 接入前须在工作台完成“采购充值实际到账核实 → API 申请 → 平台审核 → 创建/领取密钥”。申请和审批时采购余额均须大于零,未到账申请不算。现有演示/预发密钥也受门槛限制,未开通返回 `403 api_access_required`。 API 接入和新自建兑换开关是两项授权。开通 API 后再申请 `customRedemptionEnabled`。已批准 API 不因余额正常消费至零而关闭:仍可查旧任务和创建平台代收订单;余额采购按供货价检查并原子扣款,不能透支。 `GET /v1/products` 查询商品;`POST /v1/orders` 创建订单: ```json { "merchant_order_no": "SHOP-20260926-0001", "product_code": "chatgpt_plus_1m", "quantity": 1, "sale_amount": "135.00", "collection_mode": "platform_collect" } ``` | collection_mode | 谁收客户零售款 | Quefa 订单何时 paid | 分销收益 | |---|---|---|---| | platform_collect | Quefa 平台支付渠道 | 平台核实零售款到账 | 履约后财务核对释放 | | agent_collect | 代理自己的支付渠道 | 成功扣除代理采购余额 | 不在 Quefa 重复记差价 | 代理自收款须先获平台授权,创建采购订单立即扣供货价。`payment_scope=procurement` 只表示供货款已扣,不代表客户已向代理付款。代理须自行验签其支付渠道回调或主动查单,不能相信浏览器声称已支付。 代理自收款的零售退款由代理处理,平台采购退款是另一笔资金动作。不要向 Quefa 提交代理支付宝私钥。 已付直充订单可打开 `fulfillment_url` 使用 Quefa 托管页,或使用下述自建页接口。CDK 商品须等待 `voucher_code` 或 `cdk.issued`,客户只收到 Quefa 的 `QF-` 码。发码不等于充值成功。 ## 三、服务端签名 浏览器不能持有签名密钥。代理后端发送: ```text X-Partner-Id: X-Key-Id: X-Timestamp: <当前 Unix 秒> X-Nonce: <每次网络请求不同的随机值> X-Signature: Idempotency-Key: <同一业务提交固定不变的键> Content-Type: application/json ``` 签名原文是以下 8 行,使用换行符连接,末尾不追加换行: ```text 大写 METHOD PATH(不含域名与查询串) 按编码后键和值排序的 RFC3986 查询串,无则空行 TIMESTAMP NONCE KEY_ID IDEMPOTENCY_KEY(GET 可以空) SHA256(实际发送的原始 BODY 字节) 小写十六进制 ``` `signature = HMAC_SHA256(client_secret, 原文)`。JSON 只序列化一次,签名和发送用相同字节。重试更新时间戳/Nonce,但保留原业务幂等键与请求体。不要记录完整签名头、CDK 和充值凭据。可运行实现见随包的 `examples/redemption-demo/server.mjs`。 ## 四、提交兑换 `POST /v1/redemptions`,必须带 8–120 位幂等键,允许字母、数字、`:_.-`。 直接充值: ```json { "mode": "direct", "order_id": "<本代理已支付的 Quefa 订单 ID>", "credential": {"mode": "session", "session": "<客户明确授权的充值资料>"}, "customer_confirmed_email": true } ``` CDK 兑换: ```json { "mode": "cdk", "code": "<本代理销售的 QF-兑换码>", "credential": {"mode": "access_token", "access_token": "<客户明确授权的充值资料>"}, "customer_confirmed_email": true } ``` 凭据三选一,不混填: | credential.mode | 其他字段 | |---|---| | session | `session`,最大 32000 字符 | | access_token | `access_token`,最大 32000 字符 | | mailbox | `email` 与 `password`;仅在商品和客户授权允许时使用 | 页面必须让客户核对充值账号并主动确认,不可默认代确认。测试环境只使用模拟资料。 受理成功返回 HTTP 202: ```json { "data": { "redemption_id": "ful_example", "order_id": "ord_example", "fulfillment_mode": "cdk", "status": "queued", "failure_code": null, "message": "充值任务已进入队列", "account_email_masked": null, "created_at": "2026-09-26T10:00:00.000Z", "finished_at": null } } ``` 202 / queued 仅表示受理。受控实单可能等待平台逐笔批准,不能显示充值成功。 同幂等键、完全相同的原始请求体返回原受理结果,并带 `Idempotent-Replayed: true`。这份结果可能仍是 queued,须查询最新状态。同键不同内容返回 409。一个订单已有 queued/running/succeeded 时,不能再创建有效任务。CDK 受理时占用,成功后消费。 超时请按原键、原请求体重试或查已知任务,不要自动创建新订单反复扣款。明确失败后由客户核对资料、确认可重试,再用新键提交。CDK 是否可重试由 Quefa 判断。 ## 五、查询与通知 `GET /v1/redemptions/{redemption_id}` 使用同样签名认证,返回上节的 data。建议从 5 秒轮询逐步退避至 30 秒,终态停止。关闭创建开关后,原代理仍可查询已有任务。 | status | 页面含义 | |---|---| | queued | 已受理,等待派发 | | running | 处理中或结果确认中,不重复提交 | | succeeded | 充值成功 | | failed | 明确失败,按统一错误提示处理 | | cancelled | 已取消 | 可接收 `fulfillment.succeeded`、`fulfillment.failed`、`fulfillment.cancelled`,`fulfillment_id` 等于 `redemption_id`。必须验证 Quefa 签名并按 `event_id` 幂等。终态不能被较旧的处理中状态覆盖;收到通知可签名查单确认。 Webhook 验签规则与完整接收实现见随包 `examples/partner-demo/server.mjs` 和开放接口主文档。使用专用 Webhook 密钥、原始请求体和时间窗口验证;通知先持久化再返回 2xx。通知可能重试,不保证只投递一次。 ## 六、错误与兼容 | HTTP / code | 处理 | |---|---| | 401 / 签名错误 | 核对时间、原始字节、密钥,不回显密钥 | | 403 / custom_redemption_disabled | 联系平台开通新接口 | | 403 / api_access_required | 先完成采购充值核实与 API 审核,或联系平台核对停用原因 | | 404 / voucher_not_found、order_not_found、redemption_not_found | 无效或非本代理资源 | | 409 / order_not_paid | 等实际付款或采购扣款 | | 409 / voucher_unavailable、fulfillment_already_exists | 查原任务,不自动重建 | | 409 / idempotency_conflict | 排查同键不同内容 | | 409 / procurement_balance_insufficient | 采购余额不足,充值申请不等于到账 | | 500 / internal_error | 保留 request_id,联系平台并按原键安全重试 | 旧 `/v1/orders/{order_id}/fulfillments` 直充接口保留兼容和原授权规则。`customRedemptionEnabled` 只控制新增 `/v1/redemptions` 创建入口,不撤销旧接口权限;要整体停用代理 API,应停用其应用/密钥。新项目建议统一采用本指南。 ## 七、代理网站自身的安全责任 1. 后端验证当前客户的订单归属,再映射为 Quefa 订单 ID。Quefa 的代理隔离不代替网站自己的客户级隔离。 2. 密钥不得进入前端 JS、安装包或 localStorage。后端不允许任意路径转发,不用管理接口替消费者充值。 3. 凭据只用于本次授权充值,不能进入日志、埋点、错误监控、客服工单或默认长期存储;使用 HTTPS、请求大小限制、客户鉴权/随机兑换能力令牌、CSRF 防护与限流。 4. QF 码有实际价值,不放公开查询串或日志。查询接口使用本站会话绑定或随机查询能力令牌,不把订单 ID 当授权。 5. 不把完整 Quefa 订单返回消费者,避免泄露供货价。只返回本站订单编号、统一状态、脱敏账号及提示。 6. 浏览器不直连或跳转到底层服务,不透传完整服务错误和通知。底层更换渠道不应要求代理改页面。 7. 本地示例只监听回环地址,没有生产客户账号、订单归属校验、持久化查询令牌及网关限流等完整能力,不能原样公网发布。 ## 八、联调步骤与等级申请 启动 Quefa API/Worker → 专用模拟代理完成测试充值记账、API 申请/审批 → 开启自建兑换接口 → 创建并模拟支付测试订单/领取 QF 码 → 按 `examples/redemption-demo/README.md` 运行独立页面 → 提交并查终态。模拟记账只允许隔离模拟库;真实资金须核实实际到账。 至少验证:未开通拒绝、未付订单拒绝、跨代理资源拒绝、同键只产生一个任务、同键改内容冲突、状态与通知一致、页面源码/浏览器请求/响应无底层信息。 等级申请:`POST /v1/tier-applications`,body 为 `{"targetTier":"gold","reason":"申请说明","requestKey":"申请唯一号"}`。返回结构化工单,`GET /v1/tickets` 跟进。只有平台管理员审批;升级不自动改商品价格、支付方式或账号角色,实际价格以 `/v1/products` 为准。