Add WeChat Pay local skills

This commit is contained in:
2026-05-15 03:35:30 +08:00
parent 2eded08bc7
commit 6672867c6f
535 changed files with 114971 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""
品牌直连 - 查询用户商品券详情
API文档https://pay.weixin.qq.com/doc/brand/4015736414
依赖Python3macOS/Linux 自带)
无需 pip install 任何第三方库。
所有参数通过命令行传入,脚本内不硬编码任何配置。
模型负责交互式收集参数,拼装命令行调用本脚本。
签名模式用户在自己的服务器上完成签名后将签名值Base64、时间戳、随机串传入
脚本直接使用这些值构造 Authorization 头并发送请求。
用法:
python3 查询用户商品券详情_品牌.py \
--brand-id <品牌ID> \
--serial-no <API证书序列号> \
--signature <Base64签名值> \
--timestamp <签名时使用的时间戳> \
--nonce-str <签名时使用的随机串> \
--wechat-pay-public-key-id <微信支付公钥ID> \
--coupon-code <券码> \
--openid <用户OpenID> \
--product-coupon-id <商品券ID> \
--stock-id <批次ID> \
--appid <AppID>
"""
import argparse
import json
import sys
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from urllib.parse import quote
def build_authorization(brand_id: str, serial_no: str, method: str, uri: str,
signature: str, timestamp: str, nonce_str: str, body: str = "") -> str:
"""
构造完整的 Authorization 头。
使用用户提供的预签名结果signature + timestamp + nonce_str直接构造。
"""
return (
f'WECHATPAY-BRAND-SHA256-RSA2048 '
f'brand_id="{brand_id}",'
f'nonce_str="{nonce_str}",'
f'signature="{signature}",'
f'timestamp="{timestamp}",'
f'serial_no="{serial_no}"'
)
def print_coupon_analysis(data: dict):
"""解析并结构化输出券的关键信息,方便排障。"""
print("\n========== 券关键信息 ==========")
print(f" 券码: {data.get('coupon_code', '-')}")
print(f" 券状态: {data.get('coupon_state', '-')}")
print(f" 有效期开始: {data.get('valid_begin_time', '-')}")
print(f" 有效期结束: {data.get('valid_end_time', '-')}")
print(f" 领取时间: {data.get('receive_time', '-')}")
print(f" 发放渠道: {data.get('send_channel', '-')}")
# 确认发放信息
if data.get("confirm_time"):
print(f" 确认发放时间: {data['confirm_time']}")
print(f" 确认发放单号: {data.get('confirm_request_no', '-')}")
else:
print(f" 确认发放时间: 未确认发放")
# 失效信息
if data.get("deactivate_time"):
print(f" 失效时间: {data['deactivate_time']}")
print(f" 失效原因: {data.get('deactivate_reason', '-')}")
# 单券核销详情
single_detail = data.get("single_usage_detail")
if single_detail and single_detail.get("use_time"):
print(f" 核销时间: {single_detail['use_time']}")
print(f" 核销单号: {single_detail.get('use_request_no', '-')}")
order_info = single_detail.get("associated_order_info")
if order_info:
print(f" 关联订单号: {order_info.get('transaction_id', '-')}")
# 多次优惠核销详情
bundle_detail = data.get("progressive_bundle_usage_detail")
if bundle_detail and bundle_detail.get("use_time"):
print(f" 多次优惠核销: {bundle_detail['use_time']}")
# 多次优惠信息
bundle_info = data.get("user_product_coupon_bundle_info")
if bundle_info:
print(f" 多次优惠总次: {bundle_info.get('total_count', '-')}")
print(f" 已用次数: {bundle_info.get('used_count', '-')}")
print("================================")
# ====== 自动诊断提示 ======
state = data.get("coupon_state", "")
print("\n---------- 自动诊断 ----------")
if state == "EXPIRED":
print("⚠️ 券已过期EXPIRED")
print(" → 请对比「有效期结束」时间与你系统记录的有效期是否一致。")
print(" → 常见原因:有效期起算以回调时间为准,而非发券接口返回时间。")
if data.get("confirm_time") and data.get("receive_time"):
print(f" → 领取时间: {data['receive_time']},确认发放时间: {data['confirm_time']}")
print(" → 如果两者间隔较大,说明确认发放延迟消耗了有效期。")
elif state == "CONFIRMING":
print("⚠️ 券状态为待确认发放CONFIRMING尚未调用确认发放接口。")
print(" → 必须先调「确认发放」接口,券才能进入 EFFECTIVE 可核销状态。")
elif state == "EFFECTIVE":
print("✅ 券状态正常EFFECTIVE可以核销。")
elif state == "USED":
print(" 券已被核销USED")
elif state == "DELETED":
print("⚠️ 券已被删除DELETED可能是用户在微信卡包中手动删除。")
print(" → 注意:用户卡包删券没有回调通知,商户侧无法实时感知。")
elif state == "DEACTIVATED":
print("⚠️ 券已失效DEACTIVATED")
if data.get("deactivate_reason"):
print(f" → 失效原因: {data['deactivate_reason']}")
elif state == "PENDING":
print(" 券状态为待生效PENDING可能配置了领取后等待N天生效。")
else:
print(f" 券状态: {state}")
print("-------------------------------")
def main():
parser = argparse.ArgumentParser(
description="品牌直连 - 查询用户商品券详情",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python3 查询用户商品券详情_品牌.py \\
--brand-id 11490 \\
--serial-no 2047739FFE173C9C5385A55A9CBF4208AAD91988 \\
--signature "Base64编码的签名值" \\
--timestamp 1700000000 \\
--nonce-str abcdef1234567890 \\
--wechat-pay-public-key-id PUB_KEY_ID_0123456789 \\
--coupon-code Code_123456 \\
--openid oh-394z-6CGkNoJrsDLTTUKiAnp4 \\
--product-coupon-id 1000000013 \\
--stock-id 1000000013001 \\
--appid wx233544546545989
""",
)
# 品牌配置参数
parser.add_argument("--brand-id", required=True, help="品牌ID")
parser.add_argument("--serial-no", required=True, help="品牌API证书序列号")
parser.add_argument("--wechat-pay-public-key-id", required=True, help="微信支付公钥ID")
# 签名参数(必填)
sign_group = parser.add_argument_group("签名参数")
sign_group.add_argument("--signature", required=True, help="用户在自己服务器上生成的 Base64 签名值")
sign_group.add_argument("--timestamp", required=True, help="签名时使用的时间戳10位Unix秒")
sign_group.add_argument("--nonce-str", required=True, help="签名时使用的随机字符串")
# 业务参数
parser.add_argument("--coupon-code", required=True, help="用户券码")
parser.add_argument("--openid", required=True, help="用户OpenID")
parser.add_argument("--product-coupon-id", required=True, help="商品券ID")
parser.add_argument("--stock-id", required=True, help="批次ID")
parser.add_argument("--appid", required=True, help="AppID")
args = parser.parse_args()
# ====== 构造请求 URL ======
uri = (
f"/brand/marketing/product-coupon/users/{quote(args.openid, safe='')}"
f"/coupons/{quote(args.coupon_code, safe='')}"
f"?product_coupon_id={quote(args.product_coupon_id, safe='')}"
f"&stock_id={quote(args.stock_id, safe='')}"
f"&appid={quote(args.appid, safe='')}"
)
full_url = f"https://api.mch.weixin.qq.com{uri}"
# ====== 打印待签名串供用户核对 ======
method = "GET"
body = ""
sign_str = f"{method}\n{uri}\n{args.timestamp}\n{args.nonce_str}\n{body}\n"
print("========== 预签名核对 ==========")
print("脚本计算的待签名串(请与您在服务器上使用的待签名串核对):")
print("--- 开始 ---")
print(sign_str, end="")
print("--- 结束 ---")
print("如果上述待签名串与您签名时使用的不一致,签名验证将失败。")
print("================================\n")
# ====== 构造签名和请求头 ======
authorization = build_authorization(
brand_id=args.brand_id,
serial_no=args.serial_no,
method="GET",
uri=uri,
signature=args.signature,
timestamp=args.timestamp,
nonce_str=args.nonce_str,
)
# ====== 打印请求信息 ======
print("========== 查询用户商品券详情 ==========")
print(f" 品牌ID: {args.brand_id}")
print(f" 券码: {args.coupon_code}")
print(f" OpenID: {args.openid}")
print(f" 商品券ID: {args.product_coupon_id}")
print(f" 批次ID: {args.stock_id}")
print(f" AppID: {args.appid}")
print("=========================================\n")
# ====== 发送请求 ======
req = Request(full_url, method="GET")
req.add_header("Accept", "application/json")
req.add_header("Authorization", authorization)
req.add_header("Wechatpay-Serial", args.wechat_pay_public_key_id)
try:
with urlopen(req) as resp:
status_code = resp.status
body = resp.read().decode("utf-8")
except HTTPError as e:
status_code = e.code
body = e.read().decode("utf-8")
except URLError as e:
print(f"错误: 网络请求失败: {e.reason}", file=sys.stderr)
sys.exit(1)
# ====== 输出结果 ======
print(f"========== 响应结果 ==========")
print(f"HTTP 状态码: {status_code}")
print("响应内容:")
try:
data = json.loads(body)
print(json.dumps(data, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print(body)
print("===============================")
sys.exit(1)
print("===============================")
# ====== 结构化分析 ======
if 200 <= status_code < 300:
print_coupon_analysis(data)
else:
# 请求失败,输出错误信息
print("\n---------- 请求失败 ----------")
print(f" 错误码: {data.get('code', '-')}")
print(f" 错误信息: {data.get('message', '-')}")
detail = data.get("detail", {})
if isinstance(detail, dict) and detail.get("field"):
print(f" 问题字段: {detail['field']}")
print(f" 字段错误: {detail.get('issue', '-')}")
print("-------------------------------")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""
服务商 - 查询用户商品券详情
API文档https://pay.weixin.qq.com/doc/partner/4013080339
依赖Python3macOS/Linux 自带)
无需 pip install 任何第三方库。
所有参数通过命令行传入,脚本内不硬编码任何配置。
模型负责交互式收集参数,拼装命令行调用本脚本。
签名模式用户在自己的服务器上完成签名后将签名值Base64、时间戳、随机串传入
脚本直接使用这些值构造 Authorization 头并发送请求。
用法:
python3 查询用户商品券详情_服务商.py \
--mchid <服务商商户号> \
--serial-no <API证书序列号> \
--signature <Base64签名值> \
--timestamp <签名时使用的时间戳> \
--nonce-str <签名时使用的随机串> \
--wechat-pay-public-key-id <微信支付公钥ID> \
--brand-id <品牌ID> \
--coupon-code <券码> \
--openid <用户OpenID> \
--product-coupon-id <商品券ID> \
--stock-id <批次ID> \
--appid <AppID>
"""
import argparse
import json
import sys
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from urllib.parse import quote
def build_authorization(mchid: str, serial_no: str, method: str, uri: str,
signature: str, timestamp: str, nonce_str: str, body: str = "") -> str:
"""
构造完整的 Authorization 头(服务商签名方案)。
使用用户提供的预签名结果signature + timestamp + nonce_str直接构造。
"""
return (
f'WECHATPAY2-SHA256-RSA2048 '
f'mchid="{mchid}",'
f'nonce_str="{nonce_str}",'
f'signature="{signature}",'
f'timestamp="{timestamp}",'
f'serial_no="{serial_no}"'
)
def print_coupon_analysis(data: dict):
"""解析并结构化输出券的关键信息,方便排障。"""
print("\n========== 券关键信息 ==========")
print(f" 券码: {data.get('coupon_code', '-')}")
print(f" 券状态: {data.get('coupon_state', '-')}")
print(f" 有效期开始: {data.get('valid_begin_time', '-')}")
print(f" 有效期结束: {data.get('valid_end_time', '-')}")
print(f" 领取时间: {data.get('receive_time', '-')}")
print(f" 发放渠道: {data.get('send_channel', '-')}")
# 确认发放信息
if data.get("confirm_time"):
print(f" 确认发放时间: {data['confirm_time']}")
print(f" 确认发放单号: {data.get('confirm_request_no', '-')}")
else:
print(f" 确认发放时间: 未确认发放")
# 失效信息
if data.get("deactivate_time"):
print(f" 失效时间: {data['deactivate_time']}")
print(f" 失效原因: {data.get('deactivate_reason', '-')}")
# 单券核销详情
single_detail = data.get("single_usage_detail")
if single_detail and single_detail.get("use_time"):
print(f" 核销时间: {single_detail['use_time']}")
print(f" 核销单号: {single_detail.get('use_request_no', '-')}")
order_info = single_detail.get("associated_order_info")
if order_info:
print(f" 关联订单号: {order_info.get('transaction_id', '-')}")
# 多次优惠核销详情
bundle_detail = data.get("progressive_bundle_usage_detail")
if bundle_detail and bundle_detail.get("use_time"):
print(f" 多次优惠核销: {bundle_detail['use_time']}")
# 多次优惠信息
bundle_info = data.get("user_product_coupon_bundle_info")
if bundle_info:
print(f" 多次优惠总次: {bundle_info.get('total_count', '-')}")
print(f" 已用次数: {bundle_info.get('used_count', '-')}")
print("================================")
# ====== 自动诊断提示 ======
state = data.get("coupon_state", "")
print("\n---------- 自动诊断 ----------")
if state == "EXPIRED":
print("⚠️ 券已过期EXPIRED")
print(" → 请对比「有效期结束」时间与你系统记录的有效期是否一致。")
print(" → 常见原因:有效期起算以回调时间为准,而非发券接口返回时间。")
if data.get("confirm_time") and data.get("receive_time"):
print(f" → 领取时间: {data['receive_time']},确认发放时间: {data['confirm_time']}")
print(" → 如果两者间隔较大,说明确认发放延迟消耗了有效期。")
elif state == "CONFIRMING":
print("⚠️ 券状态为待确认发放CONFIRMING尚未调用确认发放接口。")
print(" → 必须先调「确认发放」接口,券才能进入 EFFECTIVE 可核销状态。")
elif state == "EFFECTIVE":
print("✅ 券状态正常EFFECTIVE可以核销。")
elif state == "USED":
print(" 券已被核销USED")
elif state == "DELETED":
print("⚠️ 券已被删除DELETED可能是用户在微信卡包中手动删除。")
print(" → 注意:用户卡包删券没有回调通知,商户/服务商侧无法实时感知。")
elif state == "DEACTIVATED":
print("⚠️ 券已失效DEACTIVATED")
if data.get("deactivate_reason"):
print(f" → 失效原因: {data['deactivate_reason']}")
elif state == "PENDING":
print(" 券状态为待生效PENDING可能配置了领取后等待N天生效。")
else:
print(f" 券状态: {state}")
print("-------------------------------")
def main():
parser = argparse.ArgumentParser(
description="服务商 - 查询用户商品券详情",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python3 查询用户商品券详情_服务商.py \\
--mchid 1900000001 \\
--serial-no 5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3A4B \\
--signature "Base64编码的签名值" \\
--timestamp 1700000000 \\
--nonce-str abcdef1234567890 \\
--wechat-pay-public-key-id PUB_KEY_ID_0123456789 \\
--brand-id 11490 \\
--coupon-code Code_123456 \\
--openid oh-394z-6CGkNoJrsDLTTUKiAnp4 \\
--product-coupon-id 1000000013 \\
--stock-id 1000000013001 \\
--appid wx233544546545989
""",
)
# 服务商配置参数
parser.add_argument("--mchid", required=True, help="服务商商户号")
parser.add_argument("--serial-no", required=True, help="服务商API证书序列号")
parser.add_argument("--wechat-pay-public-key-id", required=True, help="微信支付公钥ID")
# 签名参数(必填)
sign_group = parser.add_argument_group("签名参数")
sign_group.add_argument("--signature", required=True, help="用户在自己服务器上生成的 Base64 签名值")
sign_group.add_argument("--timestamp", required=True, help="签名时使用的时间戳10位Unix秒")
sign_group.add_argument("--nonce-str", required=True, help="签名时使用的随机字符串")
# 业务参数
parser.add_argument("--brand-id", required=True, help="品牌ID")
parser.add_argument("--coupon-code", required=True, help="用户券码")
parser.add_argument("--openid", required=True, help="用户OpenID")
parser.add_argument("--product-coupon-id", required=True, help="商品券ID")
parser.add_argument("--stock-id", required=True, help="批次ID")
parser.add_argument("--appid", required=True, help="AppID")
args = parser.parse_args()
# ====== 构造请求 URL ======
# 服务商 API 路径前缀为 /v3/,且查询参数额外需要 brand_id
uri = (
f"/v3/marketing/partner/product-coupon/users/{quote(args.openid, safe='')}"
f"/coupons/{quote(args.coupon_code, safe='')}"
f"?product_coupon_id={quote(args.product_coupon_id, safe='')}"
f"&stock_id={quote(args.stock_id, safe='')}"
f"&appid={quote(args.appid, safe='')}"
f"&brand_id={quote(args.brand_id, safe='')}"
)
full_url = f"https://api.mch.weixin.qq.com{uri}"
# ====== 打印待签名串供用户核对 ======
method = "GET"
body = ""
sign_str = f"{method}\n{uri}\n{args.timestamp}\n{args.nonce_str}\n{body}\n"
print("========== 预签名核对 ==========")
print("脚本计算的待签名串(请与您在服务器上使用的待签名串核对):")
print("--- 开始 ---")
print(sign_str, end="")
print("--- 结束 ---")
print("如果上述待签名串与您签名时使用的不一致,签名验证将失败。")
print("================================\n")
# ====== 构造签名和请求头 ======
authorization = build_authorization(
mchid=args.mchid,
serial_no=args.serial_no,
method="GET",
uri=uri,
signature=args.signature,
timestamp=args.timestamp,
nonce_str=args.nonce_str,
)
# ====== 打印请求信息 ======
print("========== 查询用户商品券详情(服务商) ==========")
print(f" 服务商商户号: {args.mchid}")
print(f" 品牌ID: {args.brand_id}")
print(f" 券码: {args.coupon_code}")
print(f" OpenID: {args.openid}")
print(f" 商品券ID: {args.product_coupon_id}")
print(f" 批次ID: {args.stock_id}")
print(f" AppID: {args.appid}")
print("==================================================\n")
# ====== 发送请求 ======
req = Request(full_url, method="GET")
req.add_header("Accept", "application/json")
req.add_header("Authorization", authorization)
req.add_header("Wechatpay-Serial", args.wechat_pay_public_key_id)
try:
with urlopen(req) as resp:
status_code = resp.status
body = resp.read().decode("utf-8")
except HTTPError as e:
status_code = e.code
body = e.read().decode("utf-8")
except URLError as e:
print(f"错误: 网络请求失败: {e.reason}", file=sys.stderr)
sys.exit(1)
# ====== 输出结果 ======
print(f"========== 响应结果 ==========")
print(f"HTTP 状态码: {status_code}")
print("响应内容:")
try:
data = json.loads(body)
print(json.dumps(data, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print(body)
print("===============================")
sys.exit(1)
print("===============================")
# ====== 结构化分析 ======
if 200 <= status_code < 300:
print_coupon_analysis(data)
else:
# 请求失败,输出错误信息
print("\n---------- 请求失败 ----------")
print(f" 错误码: {data.get('code', '-')}")
print(f" 错误信息: {data.get('message', '-')}")
detail = data.get("detail", {})
if isinstance(detail, dict) and detail.get("field"):
print(f" 问题字段: {detail['field']}")
print(f" 字段错误: {detail.get('issue', '-')}")
print("-------------------------------")
if __name__ == "__main__":
main()