# -*- coding: utf-8 -*-
"""客户端 api 第 4 批接口冒烟:图形/短信验证码、登录、收银台、支付回调。
用法:起 Java 18080 后执行
python tools/test_api_batch4.py
环境变量:YOSHOP_API_BASE(默认 http://127.0.0.1:18080)
说明:
- 图形验证码的答案从 Redis 读取(模拟用户看图输入),验证码接口本身走真实链路;
- 短信通道未接入,验证码只落 Redis,脚本同样从 Redis 取值完成登录;
- 会真实下单/支付/回调,finally 中删除测试订单、交易记录、充值单、余额流水与测试用户,
并恢复用户余额 / 总支付金额 / 商品销量。
"""
import hashlib
import json
import os
import re
import socket
import sys
import pymysql
import requests
from _root import ROOT
from redis_probe import cmd
BASE = os.environ.get("YOSHOP_API_BASE", "http://127.0.0.1:18080")
TOKEN = "apitest-batch4"
USER_ID = 10001
# 登录用测试手机号(脚本会创建并在 finally 删除)
TEST_MOBILE = "13900001111"
# 支付链路测试商品(下单减库存、单规格由 make_order 固定为 goods_sku_id)
GOODS_ID = 10009
SKU_ID = "10009"
YML = str(ROOT / "yoshop-java/src/main/resources/application.yml")
TEXT = open(YML, encoding="utf-8").read()
DB_PWD = re.findall(r"password:\s*(.+)", TEXT)[0].strip().strip("\"'")
REDIS_PWD = re.findall(r"password:\s*(.+)", TEXT)[1].strip().strip("\"'")
S = requests.Session()
S.trust_env = False
passed = 0
failed = 0
created_orders = []
created_trades = []
created_recharge_orders = []
created_payments = []
backup = {}
def check(name, cond, detail=""):
global passed, failed
if cond:
passed += 1
print(f" PASS {name}")
else:
failed += 1
print(f" FAIL {name} {detail}")
def call(method, path, params=None, body=None, token=TOKEN):
headers = {"platform": "MP-WEIXIN"}
if token:
headers["Access-Token"] = token
if method == "GET":
resp = S.get(BASE + path, params=params or {}, headers=headers, timeout=60)
else:
resp = S.post(BASE + path, params=params or {}, json=body, headers=headers, timeout=60)
try:
return resp.json()
except Exception:
return {"status": resp.status_code, "raw": resp.text[:300]}
def get(path, params=None, token=TOKEN):
return call("GET", path, params=params, token=token)
def post(path, body=None, params=None, token=TOKEN):
return call("POST", path, params=params, body=body, token=token)
def raw_post(path, text=None, data=None, headers=None):
resp = S.post(BASE + path, data=text if text is not None else data,
headers=headers or {}, timeout=60)
return resp.text
def snake_keys(obj, prefix="$"):
found = []
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(k, str) and re.fullmatch(r"[a-z0-9]+(_[a-z0-9]+)+", k):
found.append(f"{prefix}.{k}")
found.extend(snake_keys(v, f"{prefix}.{k}"))
elif isinstance(obj, list):
for i, v in enumerate(obj[:3]):
found.extend(snake_keys(v, f"{prefix}[{i}]"))
return found
def db(sql):
conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
database="shop", charset="utf8mb4", autocommit=True)
try:
cur = conn.cursor()
cur.execute(sql)
return cur.fetchall()
finally:
conn.close()
def db_exec(sql):
conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
database="shop", charset="utf8mb4", autocommit=True)
try:
cur = conn.cursor()
cur.execute(sql)
return cur.lastrowid
finally:
conn.close()
def redis_cmd(*args):
sock = socket.create_connection(("192.168.1.177", 6379), timeout=5)
try:
cmd(sock, "AUTH", REDIS_PWD)
return cmd(sock, *args)
finally:
sock.close()
def redis_get(key):
return redis_cmd("GET", key)
def redis_del(key):
return redis_cmd("DEL", key)
def field(obj, name, default=None):
return obj.get(name, default) if isinstance(obj, dict) else default
def new_captcha():
"""取一张新图形验证码,返回 (接口数据, 明文验证码)"""
data = get("/api/captcha/image", token=None).get("data") or {}
raw = redis_get("yoshop:api:captcha:" + str(data.get("key")))
code = json.loads(raw)["code"] if raw else None
return data, code
def send_sms(mobile):
"""完整走一遍「图形验证码 → 发送短信」,返回 (响应, 短信验证码, 所用图形验证码)"""
data, code = new_captcha()
resp = post("/api/captcha/sendSmsCaptcha",
{"mobile": mobile, "captchaCode": code, "captchaKey": data.get("key")}, token=None)
raw = redis_get("yoshop:api:sms:" + mobile)
sms = json.loads(raw)["code"] if raw else None
return resp, sms, data
def make_order(order_status=10, pay_status=10, order_no=None):
order_no = order_no or ("TEST" + str(os.getpid()) + str(len(created_orders)))
order_id = db_exec(
"INSERT INTO yoshop_order (order_no, order_type, total_price, order_price, coupon_id, coupon_money, "
"points_money, points_num, pay_price, update_price, buyer_remark, pay_method, pay_status, trade_id, "
"delivery_type, express_price, delivery_status, receipt_status, order_status, points_bonus, "
"is_settled, is_comment, order_source, order_source_id, order_source_data, platform, user_id, "
"is_delete, store_id, create_time, update_time) VALUES "
f"('{order_no}', 10, 80.00, 80.00, 0, 0.00, 0.00, 0, 83.00, 0.00, '', '', {pay_status}, 0, "
f"10, 3.00, 10, 10, {order_status}, 0, 0, 0, 10, 0, '[]', "
f"'H5', {USER_ID}, 0, 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
created_orders.append(order_id)
db_exec(
"INSERT INTO yoshop_order_goods (goods_id, goods_type, goods_name, image_id, deduct_stock_type, "
"spec_type, goods_sku_id, goods_props, content, goods_no, goods_price, line_price, goods_weight, "
"is_user_grade, grade_ratio, grade_goods_price, grade_total_money, coupon_money, points_money, "
"points_num, points_bonus, total_num, total_price, total_pay_price, delivery_status, delivery_num, "
"is_comment, order_id, user_id, goods_source_id, store_id, create_time) VALUES "
f"({GOODS_ID}, 10, '正宗荔浦芋头', 0, 10, 20, '{SKU_ID}', '', '', '', 80.00, 100.00, 4.5, "
f"0, 0, 0.00, 0.00, 0.00, 0.00, 0, 0, 1, 80.00, 80.00, 10, 0, 0, {order_id}, {USER_ID}, 0, "
"10001, UNIX_TIMESTAMP())")
return order_id, order_no
def make_trade(order_id, order_no, method="wechat", order_type=10, user_id=USER_ID):
out_trade_no = "NT" + str(os.getpid()) + str(len(created_trades)) + str(int(order_id))
trade_id = db_exec(
"INSERT INTO yoshop_payment_trade (out_trade_no, client, pay_method, order_type, order_id, order_no, "
"user_id, trade_no, prepay_id, trade_state, store_id, create_time, update_time) VALUES "
f"('{out_trade_no}', 'MP-WEIXIN', '{method}', {order_type}, {order_id}, '{order_no}', {user_id}, "
f"'', '', 10, 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
created_trades.append(trade_id)
return trade_id, out_trade_no
def cleanup():
try:
ids = ",".join(str(i) for i in created_orders) or "0"
tids = ",".join(str(i) for i in created_trades) or "0"
rids = ",".join(str(i) for i in created_recharge_orders) or "0"
db_exec(f"DELETE FROM yoshop_payment_trade WHERE trade_id IN ({tids})")
db_exec(f"DELETE FROM yoshop_order_goods WHERE order_id IN ({ids})")
db_exec(f"DELETE FROM yoshop_order WHERE order_id IN ({ids})")
db_exec(f"DELETE FROM yoshop_recharge_order WHERE order_id IN ({rids})")
pids = ",".join(str(i) for i in created_payments) or "0"
db_exec(f"DELETE FROM yoshop_payment WHERE payment_id IN ({pids})")
db_exec(f"DELETE FROM yoshop_user_balance_log WHERE user_id={USER_ID} AND "
f"(`describe` LIKE '用户消费:TEST%' OR `describe` LIKE '用户充值:TEST%')")
if backup:
db_exec(f"UPDATE yoshop_user SET balance={backup['balance']}, pay_money={backup['pay_money']}, "
f"points={backup['points']} WHERE user_id={USER_ID}")
db_exec(f"UPDATE yoshop_goods SET sales_actual={backup['sales_actual']} WHERE goods_id={GOODS_ID}")
db_exec(f"DELETE FROM yoshop_user_oauth WHERE user_id IN "
f"(SELECT user_id FROM yoshop_user WHERE mobile='{TEST_MOBILE}')")
db_exec(f"DELETE FROM yoshop_user WHERE mobile='{TEST_MOBILE}'")
for key in ["yoshop:api:sms:" + TEST_MOBILE, "yoshop:api:sms:send:" + TEST_MOBILE,
"yoshop:api:token:" + TOKEN]:
redis_del(key)
print(" CLEAN 测试数据已清理")
except Exception as e: # noqa: BLE001
print(" CLEAN 失败:", e)
def main():
print("== 客户端 api 第 4 批(验证码 / 登录 / 收银台 / 支付回调) ==")
redis_cmd("SET", "yoshop:api:token:" + TOKEN, str(USER_ID))
redis_cmd("EXPIRE", "yoshop:api:token:" + TOKEN, "3600")
row = db(f"SELECT balance, pay_money, points FROM yoshop_user WHERE user_id={USER_ID}")[0]
backup.update(balance=row[0], pay_money=row[1], points=row[2],
sales_actual=db(f"SELECT sales_actual FROM yoshop_goods WHERE goods_id={GOODS_ID}")[0][0])
for key in ["yoshop:api:sms:" + TEST_MOBILE, "yoshop:api:sms:send:" + TEST_MOBILE]:
redis_del(key)
# ---------------------------------------------------------------- 图形验证码
print("\n[1] 图形验证码")
r = get("/api/captcha/image", token=None)
data = field(r, "data") or {}
check("captcha/image 成功", r.get("status") == 200, r)
check("出参仅 base64/key/md5", sorted(data.keys()) == ["base64", "key", "md5"], list(data.keys()))
base64_text = str(data.get("base64"))
check("base64 为 png data URI", base64_text.startswith("data:image/png;base64,") and len(base64_text) > 1000,
base64_text[:40])
check("key 为 bcrypt 摘要", str(data.get("key", "")).startswith("$2"), str(data.get("key"))[:10])
cache = redis_get("yoshop:api:captcha:" + str(data.get("key")))
cached = json.loads(cache) if cache else {}
check("验证码写入 Redis", cached.get("code") and cached.get("times") == 5, cache)
check("md5 为验证码摘要",
data.get("md5") == hashlib.md5(str(cached.get("code")).encode()).hexdigest(), data.get("md5"))
ttl = redis_cmd("TTL", "yoshop:api:captcha:" + str(data.get("key")))
check("验证码 TTL 为 1800 秒内", isinstance(ttl, int) and 0 < ttl <= 1800, ttl)
# ---------------------------------------------------------------- 发送短信验证码
print("\n[2] 发送短信验证码")
r = post("/api/captcha/sendSmsCaptcha", {"captchaCode": "abc", "captchaKey": "x"}, token=None)
check("缺手机号报错", r.get("status") == 500 and "手机号不能为空" in (r.get("message") or ""), r)
r = post("/api/captcha/sendSmsCaptcha", {"mobile": TEST_MOBILE}, token=None)
check("缺图形验证码报错", r.get("status") == 500 and "图形验证码code不能为空" in (r.get("message") or ""), r)
r = post("/api/captcha/sendSmsCaptcha",
{"mobile": TEST_MOBILE, "captchaCode": "wrong", "captchaKey": data.get("key")}, token=None)
check("图形验证码错误报错", r.get("status") == 500 and "图形验证码不正确" in (r.get("message") or ""), r)
r = post("/api/captcha/sendSmsCaptcha",
{"mobile": TEST_MOBILE, "captchaCode": "x"}, token=None)
check("图形验证码 key 为空报错", r.get("status") == 500 and "图形验证码key不能为空" in (r.get("message") or ""), r)
r, sms_code, used_captcha = send_sms(TEST_MOBILE)
check("短信发送成功", r.get("status") == 200 and "发送成功" in (r.get("message") or ""), r)
check("发送成功 data 为空数组", r.get("data") == [], r.get("data"))
check("短信验证码为 6 位数字", bool(sms_code) and re.fullmatch(r"\d{6}", sms_code), sms_code)
r = post("/api/captcha/sendSmsCaptcha",
{"mobile": TEST_MOBILE, "captchaCode": "x", "captchaKey": used_captcha.get("key")}, token=None)
check("图形验证码用后即焚", r.get("status") == 500 and "图形验证码不存在" in (r.get("message") or ""), r)
# 每日发送次数限制:首次已用 1 次,再发 9 次后应为 0 次
last = None
for _ in range(9):
last, _, _ = send_sms(TEST_MOBILE)
check("第 10 次发送仍成功", last.get("status") == 200, last)
last, _, _ = send_sms(TEST_MOBILE)
check("超出每日发送次数被拒", last.get("status") == 500 and "已超出今日最大发送次数限制" in (last.get("message") or ""), last)
left = redis_get("yoshop:api:sms:send:" + TEST_MOBILE)
check("发送次数计为 0", str(left) == "0", left)
# 重置发送次数窗口,避免影响后续登录用例
redis_del("yoshop:api:sms:send:" + TEST_MOBILE)
# ---------------------------------------------------------------- 登录
print("\n[3] 手机号 + 短信验证码登录")
r = post("/api/passport/login", {"mobile": TEST_MOBILE}, token=None)
check("缺短信验证码报错", r.get("status") == 500 and "短信验证码不能为空" in (r.get("message") or ""), r)
r = post("/api/passport/login", {"smsCode": "123456"}, token=None)
check("缺手机号报错", r.get("status") == 500 and "手机号不能为空" in (r.get("message") or ""), r)
redis_del("yoshop:api:sms:" + TEST_MOBILE)
r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "123456"}, token=None)
check("验证码不存在报错", r.get("status") == 500 and "短信验证码不存在" in (r.get("message") or ""), r)
r = post("/api/passport/login", {"mobile": "12345", "smsCode": "123456"}, token=None)
check("手机号格式校验", r.get("status") == 500 and "手机号格式不正确" in (r.get("message") or ""), r)
_, sms_code, _ = send_sms(TEST_MOBILE)
r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "000000"}, token=None)
check("验证码错误报错", r.get("status") == 500 and "短信验证码不正确" in (r.get("message") or ""), r)
check("验证码错误后次数减一", json.loads(redis_get("yoshop:api:sms:" + TEST_MOBILE)).get("times") == 4,
redis_get("yoshop:api:sms:" + TEST_MOBILE))
r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code}, token=None)
login_data = field(r, "data") or {}
check("登录成功", r.get("status") == 200 and r.get("message") == "登录成功", r)
check("返回 userId 与 token", isinstance(login_data.get("userId"), int)
and re.fullmatch(r"[0-9a-f]{32}", str(login_data.get("token"))) is not None, login_data)
check("登录后验证码用后即焚", redis_get("yoshop:api:sms:" + TEST_MOBILE) is None)
user_id = login_data.get("userId")
check("新用户写入库", bool(user_id) and db(f"SELECT COUNT(*) FROM yoshop_user WHERE user_id={user_id}")[0][0] == 1)
nick = db(f"SELECT nick_name, platform FROM yoshop_user WHERE user_id={user_id}")[0] if user_id else ("", "")
check("默认昵称为打码手机号", nick[0] == "139****1111", nick)
check("注册来源取 platform 头", nick[1] == "MP-WEIXIN", nick)
login_token = str(login_data.get("token"))
r = get("/api/user/info", token=login_token)
info = field(field(r, "data"), "userInfo") or {}
check("登录态可用", r.get("status") == 200 and info.get("userId") == user_id, r)
check("用户信息手机号打码", info.get("mobile") == "139****1111", info.get("mobile"))
_, sms_code, _ = send_sms(TEST_MOBILE)
r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code},
token=None)
check("重复登录复用同一账号", field(r, "data", {}).get("userId") == user_id, r)
# ---------------------------------------------------------------- 微信小程序登录
print("\n[4] 微信小程序登录")
r = post("/api/passport/loginMpWx", {"partyData": {"oauth": "MP-WEIXIN", "userInfo": {}}}, token=None)
check("缺 code 报错", r.get("status") == 500 and "code 不能为空" in (r.get("message") or ""), r)
r = post("/api/passport/loginMpWx",
{"partyData": {"oauth": "MP-WEIXIN", "code": "fake-code", "userInfo": {}}}, token=None)
check("未配置小程序参数时报错",
r.get("status") == 500 and "AppID和AppSecret" in (r.get("message") or ""), r)
r = post("/api/passport/loginMpWxMobile",
{"code": "fake-code", "encryptedData": "x", "iv": "y"}, token=None)
check("授权手机号登录同样受小程序配置限制",
r.get("status") == 500 and "AppID和AppSecret" in (r.get("message") or ""), r)
r = post("/api/passport/isPersonalMpweixin", {"code": "fake-code"}, token=None)
check("isPersonalMpweixin 关闭时直接返回 false",
r.get("status") == 200 and field(r, "data", {}).get("isPersonalMpweixin") is False, r)
check("微信登录接口零 snake key", not snake_keys(r), snake_keys(r))
# ---------------------------------------------------------------- 收银台
print("\n[5] 收银台")
r = get("/api/cashier/orderInfo", {"orderId": 1, "client": "MP-WEIXIN"}, token=None)
check("未登录访问收银台 401", r.get("status") == 401, r)
order_id, order_no = make_order()
cancelled_id, _ = make_order(order_status=20)
r = get("/api/cashier/orderInfo", {"orderId": 999999, "client": "MP-WEIXIN"})
check("订单不存在报错", r.get("status") == 500 and "订单不存在" in (r.get("message") or ""), r)
r = get("/api/cashier/orderInfo", {"orderId": cancelled_id, "client": "MP-WEIXIN"})
check("已取消订单不可支付", r.get("status") == 500 and "不允许支付" in (r.get("message") or ""), r)
r = get("/api/cashier/orderInfo", {"orderId": order_id, "client": "MP-WEIXIN"})
info = field(r, "data") or {}
order_part = info.get("order") or {}
check("收银台返回订单/用户/支付方式", r.get("status") == 200
and sorted(info.keys()) == ["order", "paymentMethods", "personal"], list(info.keys()))
check("订单金额两位小数字符串", order_part.get("payPrice") == "83.00", order_part)
check("返回超时信息", order_part.get("showExpiration") is True
and re.fullmatch(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", str(order_part.get("expirationTime"))) is not None,
order_part)
check("订单字段齐全", {"orderId", "orderNo", "payStatus", "orderStatus", "createTime"} <= set(order_part.keys()),
list(order_part.keys()))
methods = [m.get("method") for m in (info.get("paymentMethods") or [])]
check("支付方式含余额支付", "balance" in methods, methods)
check("收银台零 snake key", not snake_keys(info), snake_keys(info))
r = post("/api/cashier/orderPay", {"orderId": order_id, "client": "MP-WEIXIN"})
check("缺支付方式报错", r.get("status") == 500 and "请选择支付方式" in (r.get("message") or ""), r)
r = post("/api/cashier/orderPay", {"orderId": order_id, "method": "unknown", "client": "MP-WEIXIN"})
check("支付方式不可用报错", r.get("status") == 500 and "支付方式不可用" in (r.get("message") or ""), r)
db_exec(f"UPDATE yoshop_user SET balance=0 WHERE user_id={USER_ID}")
r = post("/api/cashier/orderPay", {"orderId": order_id, "method": "balance", "client": "MP-WEIXIN"})
check("余额不足报错", r.get("status") == 500 and "余额不足" in (r.get("message") or ""), r)
wechat_payment_id = db_exec(
"INSERT INTO yoshop_payment (client, method, is_must_template, template_id, is_enable, is_default, "
"others, store_id, create_time, update_time) VALUES ('MP-WEIXIN', 'wechat', 0, 0, 1, 0, '{}', 10001, "
"UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
created_payments.append(wechat_payment_id)
r = post("/api/cashier/orderPay", {"orderId": order_id, "method": "wechat", "client": "MP-WEIXIN"})
check("微信支付未接入有明确文案", r.get("status") == 500 and "尚未接入" in (r.get("message") or ""), r)
pay_money_before = float(db(f"SELECT pay_money FROM yoshop_user WHERE user_id={USER_ID}")[0][0])
db_exec(f"UPDATE yoshop_user SET balance=500 WHERE user_id={USER_ID}")
r = post("/api/cashier/orderPay", {"orderId": order_id, "method": "balance", "client": "MP-WEIXIN"})
check("余额支付成功", r.get("status") == 200 and r.get("message") == "恭喜您,订单已付款成功", r)
check("余额支付不返回第三方参数", field(r, "data", {}).get("payment") == [], r.get("data"))
row = db(f"SELECT pay_status, pay_method, trade_id, pay_time FROM yoshop_order WHERE order_id={order_id}")[0]
check("订单置为已付款", row[0] == 20 and row[1] == "balance" and row[3] > 0, row)
balance = float(db(f"SELECT balance FROM yoshop_user WHERE user_id={USER_ID}")[0][0])
check("余额扣减订单金额", abs(balance - 417.0) < 0.001, balance)
pay_money = float(db(f"SELECT pay_money FROM yoshop_user WHERE user_id={USER_ID}")[0][0])
check("累加用户总支付金额", abs(pay_money - pay_money_before - 83.0) < 0.001, pay_money)
logs = db(f"SELECT scene, money, `describe` FROM yoshop_user_balance_log WHERE user_id={USER_ID} "
f"AND `describe`='用户消费:{order_no}'")
check("写余额消费明细", len(logs) == 1 and logs[0][0] == 20 and float(logs[0][1]) == -83.0, logs)
sales = db(f"SELECT sales_actual FROM yoshop_goods WHERE goods_id={GOODS_ID}")[0][0]
check("累计商品销量", sales == backup["sales_actual"] + 1, (sales, backup["sales_actual"]))
r = post("/api/cashier/orderPay", {"orderId": order_id, "method": "balance", "client": "MP-WEIXIN"})
check("已支付订单不可重复支付", r.get("status") == 500 and "已支付" in (r.get("message") or ""), r)
r = get("/api/cashier/tradeQuery", {"method": "wechat", "client": "MP-WEIXIN"})
check("交易查询缺单号报错", r.get("status") == 500 and "商户订单号不能为空" in (r.get("message") or ""), r)
r = get("/api/cashier/tradeQuery", {"outTradeNo": "X1", "method": "wechat", "client": "MP-WEIXIN"})
check("交易查询失败仍返回成功响应",
r.get("status") == 200 and field(r, "data", {}).get("isPay") is False, r)
check("交易查询命中未接入文案", "尚未接入" in (r.get("message") or ""), r.get("message"))
r = get("/api/cashier/tradeQuery", {"outTradeNo": "X1", "method": "balance", "client": "MP-WEIXIN"})
check("非第三方交易查询返回未支付",
r.get("status") == 200 and field(r, "data", {}).get("isPay") is False
and "未支付" in (r.get("message") or ""), r)
# ---------------------------------------------------------------- 支付回调
print("\n[6] 支付回调")
text = raw_post("/api/notify/wechatV2", text="NOT-EXIST",
headers={"Content-Type": "text/xml"})
check("微信 V2 未知单号返回 FAIL", text == "FAIL", text)
text = raw_post("/api/notify/wechatV2", text="not-xml", headers={"Content-Type": "text/xml"})
check("微信 V2 报文异常返回 FAIL", text == "FAIL", text)
text = raw_post("/api/notify/wechatV3", text="{}", headers={"Content-Type": "application/json"})
check("微信 V3 未接入返回失败 JSON", '"code": "FAIL"' in text, text)
order2, order_no2 = make_order()
_, out2 = make_trade(order2, order_no2, method="wechat")
xml = (f""
f""
f"")
text = raw_post("/api/notify/wechatV2", text=xml, headers={"Content-Type": "text/xml"})
check("微信 V2 回调返回 SUCCESS", "SUCCESS" in text, text)
row = db(f"SELECT pay_status, pay_method FROM yoshop_order WHERE order_id={order2}")[0]
check("微信 V2 回调置订单已付款", row[0] == 20 and row[1] == "wechat", row)
trade = db(f"SELECT trade_state, trade_no FROM yoshop_payment_trade WHERE out_trade_no='{out2}'")[0]
check("交易记录置为已支付", trade[0] == 20 and trade[1] == "WX" + out2, trade)
text = raw_post("/api/notify/wechatV2", text=xml, headers={"Content-Type": "text/xml"})
check("重复回调幂等返回 SUCCESS", "SUCCESS" in text, text)
order3, order_no3 = make_order()
_, out3 = make_trade(order3, order_no3, method="alipay")
text = raw_post("/api/notify/alipay", data={"out_trade_no": out3, "trade_no": "ALI" + out3})
check("支付宝回调返回 success", text == "success", text)
row = db(f"SELECT pay_status, pay_method FROM yoshop_order WHERE order_id={order3}")[0]
check("支付宝回调置订单已付款", row[0] == 20 and row[1] == "alipay", row)
text = raw_post("/api/notify/alipay", data={"out_trade_no": "NOT-EXIST"})
check("支付宝未知单号返回 fail", text == "fail", text)
# 充值订单回调
recharge_no = "TESTR" + str(os.getpid())
recharge_id = db_exec(
"INSERT INTO yoshop_recharge_order (order_no, user_id, recharge_type, plan_id, pay_price, gift_money, "
"actual_money, pay_method, pay_status, pay_time, trade_id, platform, store_id, create_time, update_time) "
f"VALUES ('{recharge_no}', {USER_ID}, 10, 0, 100.00, 10.00, 110.00, '', 10, 0, 0, "
f"'MP-WEIXIN', 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
created_recharge_orders.append(recharge_id)
_, out4 = make_trade(recharge_id, recharge_no, method="alipay", order_type=100)
balance_before = float(db(f"SELECT balance FROM yoshop_user WHERE user_id={USER_ID}")[0][0])
text = raw_post("/api/notify/alipay", data={"out_trade_no": out4, "trade_no": "ALI" + out4})
check("充值订单回调返回 success", text == "success", text)
row = db(f"SELECT pay_status FROM yoshop_recharge_order WHERE order_id={recharge_id}")[0]
check("充值订单置为已支付", row[0] == 20, row)
balance = float(db(f"SELECT balance FROM yoshop_user WHERE user_id={USER_ID}")[0][0])
check("充值金额到账", abs(balance - balance_before - 110.0) < 0.001, (balance, balance_before))
logs = db(f"SELECT COUNT(*) FROM yoshop_user_balance_log WHERE user_id={USER_ID} "
f"AND `describe`='用户充值:{recharge_no}'")
check("写充值明细", logs[0][0] == 1, logs)
print(f"\n结果:PASS {passed} / FAIL {failed}")
return 1 if failed else 0
if __name__ == "__main__":
try:
code = main()
finally:
cleanup()
sys.exit(code)