241 lines
7.9 KiB
Python
241 lines
7.9 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
订单状态同步任务(兜底机制)
|
|||
|
|
|
|||
|
|
功能:
|
|||
|
|
1. 定时查询 'created' 状态的订单
|
|||
|
|
2. 调用微信支付接口查询真实状态
|
|||
|
|
3. 同步订单状态(paid / expired)
|
|||
|
|
4. 更新用户购买记录
|
|||
|
|
|
|||
|
|
运行方式:
|
|||
|
|
- 手动: python scripts/sync_order_status.py
|
|||
|
|
- 定时: crontab -e 添加 "*/5 * * * * python /path/to/sync_order_status.py"
|
|||
|
|
- Node.js: 使用 node-cron 定时调用
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
import os
|
|||
|
|
import json
|
|||
|
|
import time
|
|||
|
|
import hashlib
|
|||
|
|
import random
|
|||
|
|
import string
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import pymysql
|
|||
|
|
import requests
|
|||
|
|
except ImportError:
|
|||
|
|
print("[ERROR] 缺少依赖库,请安装:")
|
|||
|
|
print(" pip install pymysql requests")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 数据库配置
|
|||
|
|
DB = {
|
|||
|
|
"host": "56b4c23f6853c.gz.cdb.myqcloud.com",
|
|||
|
|
"port": 14413,
|
|||
|
|
"user": "cdb_outerroot",
|
|||
|
|
"password": "Zhiqun1984",
|
|||
|
|
"database": "soul_miniprogram",
|
|||
|
|
"charset": "utf8mb4",
|
|||
|
|
"cursorclass": pymysql.cursors.DictCursor,
|
|||
|
|
"connect_timeout": 15,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 微信支付配置(从环境变量或配置文件读取)
|
|||
|
|
WECHAT_PAY_CONFIG = {
|
|||
|
|
"appid": os.environ.get("WECHAT_APPID", "wxb8bbb2b10dec74aa"),
|
|||
|
|
"mch_id": os.environ.get("WECHAT_MCH_ID", "1318592501"),
|
|||
|
|
"api_key": os.environ.get("WECHAT_API_KEY", "YOUR_API_KEY_HERE"), # 需要配置真实的 API Key
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 订单超时时间(分钟)
|
|||
|
|
ORDER_TIMEOUT_MINUTES = 30
|
|||
|
|
|
|||
|
|
def log(message, level="INFO"):
|
|||
|
|
"""统一日志输出"""
|
|||
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
print(f"[{timestamp}] [{level}] {message}")
|
|||
|
|
|
|||
|
|
def generate_nonce_str(length=32):
|
|||
|
|
"""生成随机字符串"""
|
|||
|
|
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
|
|||
|
|
|
|||
|
|
def create_sign(params, api_key):
|
|||
|
|
"""生成微信支付签名"""
|
|||
|
|
# 1. 参数排序
|
|||
|
|
sorted_params = sorted(params.items())
|
|||
|
|
|
|||
|
|
# 2. 拼接字符串
|
|||
|
|
string_a = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
|
|||
|
|
string_sign_temp = f"{string_a}&key={api_key}"
|
|||
|
|
|
|||
|
|
# 3. MD5 加密并转大写
|
|||
|
|
sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()
|
|||
|
|
|
|||
|
|
return sign
|
|||
|
|
|
|||
|
|
def query_wechat_order_status(out_trade_no):
|
|||
|
|
"""
|
|||
|
|
查询微信支付订单状态
|
|||
|
|
文档: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_2
|
|||
|
|
"""
|
|||
|
|
url = "https://api.mch.weixin.qq.com/pay/orderquery"
|
|||
|
|
|
|||
|
|
params = {
|
|||
|
|
"appid": WECHAT_PAY_CONFIG["appid"],
|
|||
|
|
"mch_id": WECHAT_PAY_CONFIG["mch_id"],
|
|||
|
|
"out_trade_no": out_trade_no,
|
|||
|
|
"nonce_str": generate_nonce_str(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 生成签名
|
|||
|
|
params["sign"] = create_sign(params, WECHAT_PAY_CONFIG["api_key"])
|
|||
|
|
|
|||
|
|
# 构建 XML 请求体
|
|||
|
|
xml_data = "<xml>"
|
|||
|
|
for key, value in params.items():
|
|||
|
|
xml_data += f"<{key}>{value}</{key}>"
|
|||
|
|
xml_data += "</xml>"
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
response = requests.post(url, data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'}, timeout=10)
|
|||
|
|
|
|||
|
|
# 解析 XML 响应(简单处理,生产环境建议用 xml.etree.ElementTree)
|
|||
|
|
resp_text = response.text
|
|||
|
|
|
|||
|
|
# 提取关键字段
|
|||
|
|
if '<return_code><![CDATA[SUCCESS]]></return_code>' in resp_text:
|
|||
|
|
if '<trade_state><![CDATA[SUCCESS]]></trade_state>' in resp_text:
|
|||
|
|
return 'SUCCESS'
|
|||
|
|
elif '<trade_state><![CDATA[NOTPAY]]></trade_state>' in resp_text:
|
|||
|
|
return 'NOTPAY'
|
|||
|
|
elif '<trade_state><![CDATA[CLOSED]]></trade_state>' in resp_text:
|
|||
|
|
return 'CLOSED'
|
|||
|
|
elif '<trade_state><![CDATA[REFUND]]></trade_state>' in resp_text:
|
|||
|
|
return 'REFUND'
|
|||
|
|
else:
|
|||
|
|
return 'UNKNOWN'
|
|||
|
|
else:
|
|||
|
|
log(f"查询订单失败: {resp_text}", "WARN")
|
|||
|
|
return 'ERROR'
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
log(f"查询微信订单异常: {e}", "ERROR")
|
|||
|
|
return 'ERROR'
|
|||
|
|
|
|||
|
|
def sync_order_status():
|
|||
|
|
"""同步订单状态(主函数)"""
|
|||
|
|
log("========== 订单状态同步任务开始 ==========")
|
|||
|
|
|
|||
|
|
conn = pymysql.connect(**DB)
|
|||
|
|
cursor = conn.cursor()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 1. 查询所有 'created' 状态的订单(最近 2 小时内创建的)
|
|||
|
|
two_hours_ago = datetime.now() - timedelta(hours=2)
|
|||
|
|
|
|||
|
|
cursor.execute("""
|
|||
|
|
SELECT id, order_sn, user_id, product_type, product_id, amount, created_at
|
|||
|
|
FROM orders
|
|||
|
|
WHERE status = 'created' AND created_at >= %s
|
|||
|
|
ORDER BY created_at DESC
|
|||
|
|
""", (two_hours_ago,))
|
|||
|
|
|
|||
|
|
pending_orders = cursor.fetchall()
|
|||
|
|
|
|||
|
|
if not pending_orders:
|
|||
|
|
log("没有需要同步的订单")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
log(f"找到 {len(pending_orders)} 个待同步订单")
|
|||
|
|
|
|||
|
|
synced_count = 0
|
|||
|
|
expired_count = 0
|
|||
|
|
|
|||
|
|
for order in pending_orders:
|
|||
|
|
order_sn = order['order_sn']
|
|||
|
|
created_at = order['created_at']
|
|||
|
|
|
|||
|
|
# 2. 判断订单是否超时(超过 30 分钟)
|
|||
|
|
time_diff = datetime.now() - created_at
|
|||
|
|
|
|||
|
|
if time_diff > timedelta(minutes=ORDER_TIMEOUT_MINUTES):
|
|||
|
|
# 超时订单:标记为 expired
|
|||
|
|
log(f"订单 {order_sn} 超时 ({time_diff.seconds // 60} 分钟),标记为 expired")
|
|||
|
|
|
|||
|
|
cursor.execute("""
|
|||
|
|
UPDATE orders
|
|||
|
|
SET status = 'expired', updated_at = NOW()
|
|||
|
|
WHERE order_sn = %s
|
|||
|
|
""", (order_sn,))
|
|||
|
|
|
|||
|
|
expired_count += 1
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 3. 查询微信支付状态(跳过,因为需要真实 API Key)
|
|||
|
|
# 生产环境中取消下面的注释
|
|||
|
|
"""
|
|||
|
|
log(f"查询订单 {order_sn} 的微信支付状态...")
|
|||
|
|
|
|||
|
|
wechat_status = query_wechat_order_status(order_sn)
|
|||
|
|
|
|||
|
|
if wechat_status == 'SUCCESS':
|
|||
|
|
# 微信支付成功,更新本地订单为 paid
|
|||
|
|
log(f"订单 {order_sn} 微信支付成功,更新为 paid")
|
|||
|
|
|
|||
|
|
cursor.execute('''
|
|||
|
|
UPDATE orders
|
|||
|
|
SET status = 'paid', updated_at = NOW()
|
|||
|
|
WHERE order_sn = %s
|
|||
|
|
''', (order_sn,))
|
|||
|
|
|
|||
|
|
# 更新用户购买记录
|
|||
|
|
if order['product_type'] == 'fullbook':
|
|||
|
|
cursor.execute('''
|
|||
|
|
UPDATE users
|
|||
|
|
SET has_full_book = 1
|
|||
|
|
WHERE id = %s
|
|||
|
|
''', (order['user_id'],))
|
|||
|
|
|
|||
|
|
synced_count += 1
|
|||
|
|
|
|||
|
|
elif wechat_status == 'NOTPAY':
|
|||
|
|
log(f"订单 {order_sn} 尚未支付,保持 created 状态")
|
|||
|
|
|
|||
|
|
elif wechat_status == 'CLOSED':
|
|||
|
|
log(f"订单 {order_sn} 已关闭,标记为 cancelled")
|
|||
|
|
|
|||
|
|
cursor.execute('''
|
|||
|
|
UPDATE orders
|
|||
|
|
SET status = 'cancelled', updated_at = NOW()
|
|||
|
|
WHERE order_sn = %s
|
|||
|
|
''', (order_sn,))
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
log(f"订单 {order_sn} 查询失败或状态未知: {wechat_status}", "WARN")
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 测试环境:模拟查询(跳过微信接口)
|
|||
|
|
log(f"[TEST] 订单 {order_sn} 跳过微信查询(需配置 API Key)")
|
|||
|
|
|
|||
|
|
conn.commit()
|
|||
|
|
|
|||
|
|
log(f"同步完成: 同步 {synced_count} 个,超时 {expired_count} 个")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
conn.rollback()
|
|||
|
|
log(f"同步失败: {e}", "ERROR")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
finally:
|
|||
|
|
cursor.close()
|
|||
|
|
conn.close()
|
|||
|
|
log("========== 订单状态同步任务结束 ==========\n")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sync_order_status()
|