| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- # -*- 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="<xml><out_trade_no>NOT-EXIST</out_trade_no></xml>",
- 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"<xml><out_trade_no><![CDATA[{out2}]]></out_trade_no>"
- f"<transaction_id><![CDATA[WX{out2}]]></transaction_id>"
- f"<return_code><![CDATA[SUCCESS]]></return_code></xml>")
- 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)
|