Skip to content

[文件補充] 虛擬支付:自建 HTTP 回調驗真、query_order 與 refund_order 的完整協議 #1132

Description

@JasonCoffeeLab

希望補齊微信小程序虛擬支付(xpay)的服務端接入文件,尤其是企業主體、自建 HTTP 服務接收通知的情況。

已閱讀以下官方資料:

資料已說明出站 HMAC 簽名與部分請求範例。仍希望提供下列完整官方契約,或指向適用的第一方 SDK/範例與確切文件章節:

1. 自建 HTTP 接收通知的驗真與解密

  • GET URL 校驗與 POST 業務通知,各自的簽名參數位置、簽名原串、Token/加密 Key 的角色。
  • 明文/加密模式的適用範圍,解密封裝及 AppID/接收方校驗要求。
  • xpay 發貨、退款、iOS 退款問詢事件的 HTTP 狀態與應答本文要求,以及重送/重放處理規則。

官方 wechat-notify 範例註明集成中心網關已完成驗簽/解密。因此需要確認沒有該網關時,自建接收端應完成哪些安全步驟;出站 pay_sig 算法本身不足以說明入站驗真。

2. POST /xpay/query_order

已有 openid、env、order_id 的請求範例。希望補齊完整必填參數、鑑權位置、回應結構、訂單與退款狀態枚舉、各種訂單 ID 對應關係,以及錯誤碼。尤其需要明確哪些狀態可判定支付或退款已最終完成。

3. POST /xpay/refund_order

希望補齊請求/回應欄位、退款冪等鍵及重試規則、部分與累計退款限制、支付渠道適用範圍、錯誤碼,以及發起退款後如何透過通知或查單確認最終結果。需要區分「退款任務受理成功」與「退款完成」。

若上述內容已有官方文件,請提供對應詳節及適用版本;若目前指引只覆蓋部分接入方式,也希望明確標註其範圍,避免把網關代處理或其他支付產品的規則套到自建 xpay 接收端。

Activity

  1. github-actions commented on Oct 3, 2026

    @github-actions
    Contributor

    🤖 AI Analysis

    No files found


    Generated automatically by CodeBuddy CLI headless mode.

  2. added
    ai-processedAI automation already processed this issue
    and removed
    ai-processingAI automation is processing this issue
    on Oct 3, 2026
  3. JasonCoffeeLab commented on Oct 3, 2026

    @JasonCoffeeLab
    Author

    補充說明:本問題是虛擬支付接入文件的完整性詢問,正文已附官方參考資料,不依賴提交原始碼附件;自動分析的「No files found」尚未回答這些協議問題。

    請維護者協助確認以下三項的完整官方文件或第一方範例:

    1. 自建 HTTP 服務接收 xpay 通知時,GET/POST 的來源驗簽、解密、接收方校驗及應答規則。CloudBase 範例由網關代處理的部分,需要說明自建服務如何完成。
    2. /xpay/query_order 的完整回應結構、訂單/退款狀態枚舉與最終完成判定。
    3. /xpay/refund_order 的完整參數、冪等與重試規則、部分退款限制、渠道適用範圍及結果確認方式。

    只提供出站 HMAC 範例或接口名稱,仍不足以實作上述完整流程。若已有對應詳節,請提供確切連結、版本及適用接入方式;若這些問題應交由其他微信官方技術支援管道處理,也請指出正確入口。謝謝。

  4. binggg commented on Oct 4, 2026

    @binggg
    Member

    这三项的完整契约在微信开放文档。本仓库的虚拟支付参考面向微信云开发 / CloudBase:回调由平台或集成中心网关验签后再交给业务代码。自建 HTTP 接收端要自己做小程序消息推送,不能把出站 pay_sig 当成入站验签。

    1. 自建 HTTP 接收通知

    配置入口:小程序管理后台 → 开发 → 开发管理 → 消息推送。协议:

    https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html

    • GET(提交配置时的 URL 校验):query 带 signature、timestamp、nonce、echostr。将 Token、timestamp、nonce 做字典序排序后拼接,SHA1,与 signature 一致则原样返回 echostr。这一步没有 body,也没有 pay_sig。
    • POST(发货 / 退款 / iOS 退款问询):明文模式直接读 body。安全模式(推荐)用 query 里的 msg_signature 验签,用 EncodingAESKey 解密,解密后校验 AppID。兼容模式两种都有,不建议。字节布局以该页示例为准。
    • Token 只参与签名,EncodingAESKey 只参与消息体加解密。二者都不是虚拟支付 AppKey。出站 pay_sig 是 AppKey 对 uri + '&' + post_body 的 HMAC-SHA256,只用于本服务调用微信 /xpay/* 以及 C 端 requestVirtualPayment。
    • 事件字段见企业接入说明:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
    • 发货成功应答:<xml><ErrCode>0</ErrCode><ErrMsg><![CDATA[success]]></ErrMsg></xml>,否则平台重试,最多 15 次。重放用 wx_order_id 做幂等。xpay_subscribe_ios_refund_query_notify 须在 3 秒内返回 result_code(0 建议退款,1 拒绝)。

    CloudBase 集成中心示例和微信云开发云函数收到的是网关或平台已经验签的事件,业务代码不要再套 pay_sig,也不要自己做消息推送 AES。微信云托管走内网推送;公网开启时只处理带 x-wx-source 的请求:https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloudrun/src/guide/weixin/push.html

    2. POST /xpay/query_order

    https://developers.weixin.qq.com/miniprogram/dev/server/API/VirtualPayment/api_query_order

    POST https://api.weixin.qq.com/xpay/query_order?access_token=ACCESS_TOKEN&pay_sig=PAY_SIG
    

    pay_sig 的 uri 是 /xpay/query_order(不带 query),body 必须与实际发出的原始 JSON 一致。签名算法:https://developers.weixin.qq.com/minigame/introduction/commercialization/virtual-payment/signature.html

    Body:openid、env(0 正式 / 1 沙箱)必填;order_id(即 outTradeNo)与 wx_order_id 二选一。本接口不支持云调用。

    order.status 是数字,不是 "paid":

    status 含义
    2 已支付,待发货(查单兜底以此发货)
    4 已发货
    5 已经退款
    7 退款失败
    8 用户退款完成

    order_type:0 普通支付,1 普通退款,7 iOS 支付,8 iOS 退款。支付单的 left_fee 是剩余可退金额(分)。渠道单号、结算状态等其余字段以该页 Res.order 为准。

    3. POST /xpay/refund_order

    https://developers.weixin.qq.com/miniprogram/dev/server/API/VirtualPayment/api_refund_order

    POST https://api.weixin.qq.com/xpay/refund_order?access_token=ACCESS_TOKEN&pay_sig=PAY_SIG
    

    文档写明:此接口只是启动退款任务成功。errcode = 0 不是退款完成。返回里会有 refund_order_id、refund_wx_order_id、pay_order_id、pay_wx_order_id。完成后要再查 query_order,直到 order.status 为 5 或 8,或收到 xpay_refund_notify。status 7 是退款失败。

    可对支付后 365 天内的订单发起。支付 180 天内退款平台退还手续费,超过 180 天不退手续费。iOS 订单不能走开发者主动退款,用户在 App Store 申请,Apple 通过后推 xpay_refund_notify。

    字段 必填 说明
    openid 是 下单时的用户 openid
    order_id / wx_order_id 二选一 outTradeNo,或微信侧支付单号
    refund_order_id 是 长度 [8,32],仅字母、数字、_、-。同一次退款重试用同一值
    left_fee 是 当前剩余可退金额(分),必须来自最新的 query_order
    refund_fee 是 本次金额(分),须满足 (0, left_fee]。小于 left_fee 即部分退款
    biz_meta 是 长度 [0,1024],查单时原样返回
    refund_reason 是 0 暂无描述,1 产品问题,2 售后问题,3 用户主动退款,4 价格问题,5 其他
    req_from 是 1 人工客服,2 用户自己发起,3 其他
    env 是 0 正式,1 沙箱

    pay_sig 的 uri 是 /xpay/refund_order。该页调用 URL 只带 access_token 和 pay_sig。

    268490014:退款进行中,用相同参数稍后重试,不要换 refund_order_id。268490016:left_fee 与实际不符,重新查单后再发起。268490013:禁止对已核销的单退款。

    以上页面就是当前第一方契约。虚拟支付参考会补上「网关已验签 vs 自建消息推送」的边界,以及查单状态、退款受理与完成的区别;完整字段表仍以微信文档为准,避免和接口改版漂移。

  5. JasonCoffeeLab commented on Oct 4, 2026

    @JasonCoffeeLab
    Author

    謝謝補充連結及 CloudBase 與自建接收端的邊界。接入情境是企業主體、阿里雲自建 HTTP 服務,不經 CloudBase 集成中心網關。針對上一則回覆,還想確認以下三項可直接實作與驗證的契約:

    1. 自建接收端的入站驗簽、解密與各事件應答

    請提供適用此接入方式的第一方規格或完整範例,涵蓋 GET URL 校驗、POST 明文/安全模式的簽名參數位置、簽名原串、解密封裝、AppID/接收方校驗,以及驗證用測試向量(範例輸入與預期簽名、解密結果)。也請分別列明發貨、退款通知及 iOS 退款問詢的 HTTP 狀態、Content-Type、完整應答封裝、逾時與重送規則。

    對照我手上的官方文件截圖,發貨通知在 JSON 推送模式下有 JSON 應答範例,因此上一則的 XML 應答似乎需要標明適用模式,不能直接作為所有模式的通用應答。iOS 退款問詢的應答也列有 evidence/result_info,只有 result_code 尚不足以組出完整回應。請協助確認這些欄位的必填條件、層級與完整範例,以及相關文件的版本和適用事件。

    1. /xpay/query_order 的完整回應與最終退款判定

    請提供完整回應 schema(欄位型別、必填/可選、空值及錯誤回應)、order_type/order.status 的完整枚舉與狀態轉移,並明確說明發起退款後應查支付單還是退款單:refund_order_id、refund_wx_order_id、pay_order_id、pay_wx_order_id 各自如何帶入查單參數?

    尤其請區分部分退款與全額退款時,支付單及每筆退款單的狀態和金額如何變化,以及 5/8 是否在不同 order_type、渠道下都代表同一種最終完成結果。

    官方退款通知截圖列明 RetCode=0 為成功、非零為失敗。因此,上一則提到「或收到 xpay_refund_notify」時,是否應再校驗 RetCode 並關聯對應退款單,才能判定成功?請提供成功、失敗及通知與查單結果暫時不一致時的第一方處理範例,避免只以通知到達就判定退款完成。

    1. /xpay/refund_order 的冪等、重試、部分退款與併發契約

    同一 refund_order_id 重複請求時,哪些參數必須保持一致?相同鍵但金額或其他參數不同、請求逾時而結果未知、退款仍處理中、退款已成功或失敗,各自會返回什麼結果,應繼續查單、以原鍵重試,還是建立新的退款請求?

    另外,請說明多次部分退款的累計限制,以及同一支付單併發退款時 left_fee 的一致性要求。特別是遇到 268490016 後重新查單取得新的 left_fee,是否能沿用原 refund_order_id 更新該參數?這與「同一次退款用相同參數重試」的適用邊界是什麼?也請補充 268490014、268490013 及其他相關失敗碼的可重試性和後續處理規則。

    上述差異可能涉及文件版本、推送格式或接入模式,請提供對應的確切章節、適用版本與第一方請求/回應範例,協助確認自建服務應採用的規格。謝謝。

  6. binggg commented on Oct 4, 2026

    @binggg
    Member

    上一则有两处说宽了,按企业接入文档更正:

    1. XML 的 ErrCode 只适用于推送数据格式选了 XML 的配置。JSON 推送要回 JSON。不能把 XML 当成所有模式的通用应答。
    2. xpay_refund_notify 到达不等于退款成功。请求体里的 RetCode 才是退款结果,响应里的 ErrCode 只表示这条推送已收下。

    下面只写公开页面里有的内容。页面没有版本号,以当前正文的章节为准。自建 HTTP、不经 CloudBase 网关时,适用的是「开发者服务器接收消息推送」,不是云函数那一节。

    1. 入站验签、解密与应答

    两份第一方页面:

    消息推送页的测试向量用的是 debug_demo,不是 xpay 事件。算法和期望值可以直接拿来验证验签、解密实现;xpay 的字段表在 2.4,不要把 debug_str 当成发货字段。

    GET URL 校验(该页原文示例)

    Token=AAAAA

    https://www.qq.com/revice?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249&timestamp=1714036504&nonce=1514711492

    原串:Token、timestamp、nonce 字典序排序后拼接,即 15147114921714036504AAAAA,SHA1 应为 f464b24fc39322e44b38aa78f5edd27bd1441696。通过后响应体原样返回 echostr:4375120948345356249。没有 body,也没有 pay_sig。

    POST 明文模式(该页原文示例,数据格式 JSON)

    URL:https://www.qq.com/recive?signature=899cf89e464efb63f54ddac96b0a0a235f53aa78&timestamp=1714037059&nonce=486452656

    签名参数在 query 的 signature。原串同样是 Token、timestamp、nonce 字典序拼接:1714037059486452656AAAAA,SHA1 应为 899cf89e464efb63f54ddac96b0a0a235f53aa78。body 即明文事件。

    POST 安全模式(该页原文示例)

    EncodingAESKey=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,AppID wxba5fad812f8e6fb9。

    URL 带 signature、timestamp、nonce、openid、encrypt_type=aes、msg_signature。验签用 msg_signature,不要用 signature。原串是 Token、timestamp、nonce、body 内 Encrypt 四个字段字典序拼接后 SHA1,期望 046e02f8204d34f8ba5fa3b1db94908f3df2e9b3。

    解密:AESKey = Base64_Decode(EncodingAESKey + "="),AES-256-CBC、PKCS#7。明文布局 random(16B) + msg_len(4B, 网络字节序) + msg + appid。该示例期望 random=a8eedb185eb2fecf,msg_len=167,appid=wxba5fad812f8e6fb9,msg 为 debug_demo 的 JSON。appid 必须等于本小程序。

    加密回包的测试向量也在同一节:明文 {"demo_resp":"good luck"},random=707722b803182950,msg_len=25,期望 Encrypt=ELGduP2YcVatjqIS+eZbp80MNLoAUWvzzyJxgGzxZO/5sAvd070Bs6qrLARC9nVHm48Y4hyRbtzve1L32tmxSQ==,MsgSignature=1b9339964ed2e271e7c7b6ff2b0ef902fc94dea1。这是回包加密算法的向量,不是 xpay 业务应答。

    xpay 应答(企业接入 2.4「推送响应格式说明」)

    格式必须和后台选定的推送数据格式一致。发货、退款通知、投诉的返回参数表相同:ErrCode 必填(0 成功,其他失败),ErrMsg 可选。这里的 ErrCode 表示开发者已收下推送,不是 RetCode。

    • XML 推送:<xml><ErrCode>0</ErrCode><ErrMsg><![CDATA[success]]></ErrMsg></xml>
    • JSON 推送:{"ErrCode":0,"ErrMsg":"success"}
    • 空串或 success 等价于 ErrCode=0

    格式不对时最多重试 15 次。退款通知等带 RetryTimes 的事件写明:从 0 开始,间隔 2、4、8、16…,最多 15 次。除 iOS 问询的 3 秒外,这两页没有写服务器处理超时秒数,也没有写应答的 HTTP 状态码和 Content-Type。不能从示例补一个。

    安全模式的回包加密:消息推送页写明,接口没有特定回包要求时回空串或 success 且不加密;其他回包内容需加密。2.4 的 ErrCode 示例是按 XML/JSON 给出的明文,没有「安全模式下发货 ErrCode 回包」的专用向量。iOS 问询回包带业务字段,按消息推送规则属于要加密的回包,加密算法用上面的测试向量自测。

    iOS 退款问询

    事件 xpay_subscribe_ios_refund_query_notify。企业接入 2.4 的应答结构是 IosRefundQueryResponse:

    字段 说明
    result_code 0 建议退款,1 拒绝退款
    result_info 结果描述
    evidence 决策凭据。企业接入页标注必填

    云函数回调页把必填写得更明确,并给出目前唯一的完整嵌套示例(这是云函数 return,不是单独的 HTTP Content-Type 说明):https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/guide/wechatpay/virtual-payment-callback.html

    result_code 必填,evidence 必填,result_info 选填。外层仍是 ErrCode / ErrMsg,内层是 IosRefundQueryResponse。必须 3 秒内返回;连续 3 次未应答,平台向 Apple 返回「不确定」。Apple 最终是否退款仍以之后的 xpay_refund_notify 为准,并且要看 RetCode。

    2. query_order 与退款最终判定

    https://developers.weixin.qq.com/miniprogram/dev/server/API/VirtualPayment/api_query_order

    该页的 Res.order 表就是已发布的成功响应字段和类型,并带有 status、order_type 的完整枚举。成功时还有 errcode、errmsg。错误响应是 errcode / errmsg,错误码表在同一页。

    这张表没有标响应字段必填或可选,没有定义空值和缺省,也没有状态转移图。不能在这之外再补一套 schema。

    order.status:0 初始化,1 创建成功,2 已支付待发货,3 发货中,4 已发货,5 已经退款,6 已关闭,7 退款失败,8 用户退款完成,9 回收广告金完成,10 分账回退完成。

    order_type:0 普通虚拟支付,1 普通退款,7 苹果 iOS 支付,8 苹果 iOS 退款。

    上一则把 5 和 8 都写成最终完成,这超出了枚举原文。5 的描述是「已经退款」,8 是「用户退款完成」。页面没有说它们在所有 order_type 和渠道下是同一种结果,也没有部分退款与全额退款的状态、金额变化示例。已写明的金额关系只有:支付单的 left_fee 是退款后剩余可退金额(分);订单类型为退款单时 refund_fee 是该笔退款金额(分)。

    查哪一张单:refund_order 页写的是「启动后需要调用 query_order 来查询退款单状态,等状态变成退款完成后即为最终成功」。它没有把 refund_order_id、refund_wx_order_id、pay_order_id、pay_wx_order_id 映射到 query_order 的 order_id 或 wx_order_id。通知里的 MchRefundId、WxRefundId、MchOrderId、WxOrderId 也没有对照表。所以不能把上一则的 status 判断当成已经核实的查单入参。

    RetCode 需要校验。企业接入 2.4 对 xpay_refund_notify 写明:RetCode 0 为成功,非 0 为失败,RetMsg 为失败原因。应用 MchRefundId / WxRefundId 对上该笔退款。RetCode 非 0 是失败通知,不是成功。通知和查单暂时不一致时,这两页没有处理示例。查单是文档指定的状态接口;不能只因为通知到达就记退款完成。

    3. refund_order 的幂等、重试、部分退款

    https://developers.weixin.qq.com/miniprogram/dev/server/API/VirtualPayment/api_refund_order

    已写明的只有这些:

    • errcode=0 只表示退款任务启动成功。
    • 268490014:退款操作进行中,稍后可以使用相同参数重试。这一句只覆盖「仍在处理中」。
    • 268490016:left_fee 与实际不符,通过 query_order 确认。没有写确认后能否沿用原 refund_order_id 只改 left_fee。
    • 268490013:禁止对核销状态的单退款。没有「稍后重试」。
    • 单次 refund_fee 须在 (0, left_fee],所以可以小于剩余额。left_fee 必须来自查单。支付后 365 天内可发起;180 天内退手续费,超过 180 天不退手续费。
    • 268490004 的说明范围是赠送、代币支付和广告金充值,不要把它当成 refund_order 的成功幂等码。

    同一 refund_order_id 在金额或其他参数不同、请求超时结果未知、退款已成功或已失败时分别返回什么,应该查单、原键重试还是新建退款单,多次部分退款除 left_fee 以外的累计上限,以及同一支付单并发退款时的锁规则,接口页都没有写。「014 的相同参数」不能外推成「016 之后换 left_fee 仍用原键」。

    这些缺口不在 CloudBase 网关侧,自建 HTTP 也没有另一份 CloudBase 契约。需要微信开放平台按「企业主体、虚拟支付、开发者服务器消息推送」确认。提问入口是微信开放社区:https://developers.weixin.qq.com/community/develop/question

    本仓库的虚拟支付参考会改成和上面一致:应答跟 XML/JSON 配置走,退款完成要看 RetCode 和退款单查询,未发布的单号映射和幂等矩阵不再写成已确定规则。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-processedAI automation already processed this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions