diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 5ff2882..2083186 100644 Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/main.py b/app/main.py index fe8ccbc..f85b160 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,9 @@ import time import random import string import requests +from fastapi import Request, HTTPException +import logging +from pocketbase import PocketBase app = FastAPI() @@ -17,6 +20,44 @@ app.add_middleware( allow_headers=["*"], ) + +# ==================== PocketBase 配置 ==================== +PB_URL = "https://pocketbase.hackrobot.cn" +PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com" # 建议用 admin 账号 +PB_ADMIN_PASSWORD = "Xiao4669805@" # 安全起见,建议用环境变量 +# ======================================================== + +# 初始化 PocketBase 客户端(建议只初始化一次) +# pb_client = PocketBase(PB_URL) + + +# 登录 admin(启动时登录一次,token 有效期长) +# try: +# admin_data = pb_client.admins.auth_with_password(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD) +# print("PocketBase admin 登录成功") +# except Exception as e: +# print("PocketBase admin 登录失败:", e) + + +# 动态获取已登录的 PocketBase 客户端 +def get_pb_client(): + client = PocketBase(PB_URL) + try: + client.admins.auth_with_password(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD) + print("PocketBase admin 登录成功(动态)") + except Exception as e: + print("PocketBase admin 登录失败:", e) + raise e # 如果登录失败,直接抛异常,避免后续写入失败 + return client + + +# 配置日志(可选,记录到文件方便排查) +logging.basicConfig(filename='xorpay_notify.log', level=logging.INFO, + format='%(asctime)s - %(message)s') + +# 全局已处理订单(测试用,生产建议用 Redis 或数据库) +processed_orders = set() + # ==================== XorPay 配置 ==================== AID = "8220" # 你的 aid SECRET = "afcacd99570945f88de62624aaa3578e" # 你的 app secret @@ -47,9 +88,18 @@ async def payh5(item: dict): print('payh5 item:', item) total_fee_yuan = f"{item['total_fee'] / 100:.2f}" + # 新增:获取前端传来的 user_id + user_id = item.get('user_id', 'unknown') + pay_type = item.get('type', 'unknown').lower() # ← 新增 .lower(),统一小写 + # 获取 type + # pay_type = item.get('type', 'unknown') random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8)) - order_id = f"order_{int(time.time())}_{random_suffix}" + # order_id = f"order_{int(time.time())}_{random_suffix}" + timestamp = int(time.time()) + # order_id = f"{user_id}_order_{timestamp}_{random_suffix}" + # 推荐格式:user_{user_id}_{type}_order_{timestamp}_{random} + order_id = f"{user_id}_{pay_type}_order_{timestamp}_{random_suffix}" type_map = { "book": "购买书籍", @@ -59,7 +109,8 @@ async def payh5(item: dict): } name = type_map.get(item.get('type'), "商品支付") - notify_url = "https://www.hackrobot.cn/xorpay_notify" # 建议使用完整路径,避免冲突 + # notify_url = "https://www.hackrobot.cn/xorpay_notify" # 建议使用完整路径,避免冲突 + notify_url = "https://d25ce329c4d7.ngrok-free.app/xorpay_notify" # 建议使用完整路径,避免冲突 params = { "name": name, @@ -86,49 +137,125 @@ async def payh5(item: dict): "xorpay_url": f"https://xorpay.com/api/cashier/{AID}" } -# 异步通知回调:在支付成功后自动创建 memos 用户 +def create_memos_user(user_id: str, password: str = "123456"): + """ + 为指定 user_id 创建 memos 用户 + 支持兼容旧格式(带下划线),自动清理 + """ + try: + # 新增:清理用户名,确保无下划线,以字母开头 + if user_id.startswith("user_"): + cleaned_username = "user" + user_id.split("user_", 1)[1] # user_176... → user176... + elif "_" in user_id: + cleaned_username = user_id.replace("_", "") # 万一有其他下划线也去掉 + else: + cleaned_username = user_id # 已经是干净的,直接用 + + username = cleaned_username + print(f"正在为用户创建 memos 账号: {username}") + + memos_data = { + "username": username, + "password": password, + "role": "USER", + "state": "NORMAL" + } + memos_headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {MEMOS_TOKEN}" + } + + response = requests.post(MEMOS_API, json=memos_data, headers=memos_headers, timeout=10) + + if response.status_code in (200, 201): + result = response.json() + print(f"memos 用户创建成功: {username}") + logging.info(f"memos 用户创建成功: {username}") + return {"success": True, "data": result} + else: + error_text = response.text + print(f"memos 创建失败: {response.status_code} {error_text}") + logging.error(f"memos 创建失败 - username: {username}, status: {response.status_code}, response: {error_text}") + return {"success": False, "error": error_text} + + except Exception as e: + print(f"创建 memos 用户异常: {e}") + logging.error(f"创建 memos 用户异常 - username: {user_id}, error: {e}") + return {"success": False, "error": str(e)} + + +# 异步通知回调 @app.post("/xorpay_notify") -async def xorpay_notify(request: dict): - print("XorPay 异步通知:", request) +async def xorpay_notify(request: Request): + form_data = await request.form() + data = {key: value for key, value in form_data.items()} + + logging.info(f"XorPay 异步通知原始数据: {data}") + print("XorPay 异步通知:", data) - # 1. 验签(强烈推荐正式环境开启) - if not verify_xorpay_notify(request): - print("验签失败,可能为伪造通知") - return {"status": "fail", "msg": "sign error"} + order_id = data.get("order_id", "") + + # 幂等性检查 + if order_id in processed_orders: + print(f"订单 {order_id} 已处理过,跳过") + return "ok" - # 2. 检查支付状态(XorPay 成功通知时 status=1) - if request.get("status") != "1": - print("支付未成功") - return {"status": "ok"} # 仍返回 ok,避免重复通知 + # 1. 验签 + if not verify_xorpay_notify(data): + print("验签失败") + logging.error(f"验签失败: {data}") + raise HTTPException(status_code=400, detail="sign error") - # # 3. 这里可以根据你的业务逻辑,从 order_id 或其他参数提取用户名/密码等信息 - # # 假设前端在支付时传了自定义参数(如 order_id 中编码了用户名),或使用 return_url 携带信息 - # # 示例:简单生成一个用户(类似原 /create_user) - # timenew = str(int(time.time())) - # username = f"user_{timenew}" - # password = "123456" # 可以随机生成或从业务中获取 + # 2. 正确解析 user_id 和 pay_type + if "_order_" not in order_id: + raise HTTPException(status_code=400, detail="invalid order_id format") - # memos_data = { - # "username": username, - # "password": password, - # "role": "USER", - # "state": "NORMAL" - # } - # memos_headers = { - # "Content-Type": "application/json", - # "Authorization": f"Bearer {MEMOS_TOKEN}" - # } + prefix = order_id.split("_order_")[0] + + if "_" in prefix: + parts = prefix.rsplit("_", 1) # 从右边拆分一次 + user_id = parts[0] + pay_type = parts[1].lower() # ← 强制小写 + else: + user_id = prefix + pay_type = "unknown" - # try: - # memos_resp = requests.post(MEMOS_API, json=memos_data, headers=memos_headers) - # memos_result = memos_resp.json() - # print('memos 创建结果:', memos_result) - # # 这里可以记录订单与 memos 用户的关联(数据库等) - # except Exception as e: - # print("创建 memos 用户失败:", e) + print(f"支付成功,用户ID: {user_id},类型: {pay_type},订单: {order_id}") + + # 在解析完 pay_type 后,只有 vip 才调用 + if pay_type == "vip": + create_memos_user(user_id) # 直接传 user_id,密码默认 123456 + + # ------------------- 写入 PocketBase ------------------- + try: + # 动态获取已登录的客户端 + pb_client = get_pb_client() + + valid_types = ["vip", "book", "meetup", "video"] # 根据你实际允许的类型填 + if pay_type not in valid_types: + print(f"无效的 pay_type: {pay_type},强制设为 vip 或拒绝写入") + pay_type = "vip" # 或者直接 return "ok" 跳过写入 + + pb_data = { + "user_id": user_id, + "type": pay_type, + "order_id": order_id, + "amount": int(float(data.get("pay_price", 0)) * 100), + } + + record = pb_client.collection("payments").create(pb_data) + print(f"PocketBase 记录创建成功: {record.id}") + logging.info(f"支付记录写入 PocketBase 成功 - user_id: {user_id}, type: {pay_type}, order_id: {order_id}") + + processed_orders.add(order_id) + + except Exception as e: + print(f"PocketBase 写入失败: {e}") + logging.error(f"PocketBase 写入失败 - order_id: {order_id}, error: {e}") + processed_orders.add(order_id) # 防止重试风暴 + + return "ok" - # 返回 ok 表示通知成功接收 - return {"status": "ok"} # 保留原来的手动创建用户接口(如果还需要) class CreateUserRequest(BaseModel): diff --git a/requirements.txt b/requirements.txt index a70e8ac..0b72536 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ fastapi uvicorn sqlalchemy -pydantic \ No newline at end of file +pydantic +python-multipart \ No newline at end of file diff --git a/xorpay_notify.log b/xorpay_notify.log new file mode 100644 index 0000000..2e20784 --- /dev/null +++ b/xorpay_notify.log @@ -0,0 +1,26 @@ +2025-12-17 23:12:39,739 - XorPay 첽֪ͨԭʼ: {'aoid': 'ff6ed0b5470940ce965c201a3cf4265f', 'order_id': 'user_1766034344444_order_1766034744_f89afc82', 'detail': '{}', 'sign': 'e8343caedd3922785fa74552713252a0', 'pay_time': '2025-12-18 13:12:39.026849', 'pay_price': '0.10', 'more': ''} +2025-12-17 23:13:02,417 - XorPay 첽֪ͨԭʼ: {'aoid': '26a1087a84c945e2bb987ef9a2d0ea83', 'order_id': 'user_1766034344444_order_1766034638_96eca26d', 'detail': '{}', 'sign': '1d8cf284cbe35ad905a25291b0b74d52', 'pay_time': '2025-12-18 13:10:55', 'pay_price': '0.10', 'more': ''} +2025-12-17 23:24:01,958 - XorPay 첽֪ͨԭʼ: {'aoid': '2e9ff94264084a179c127124fd306254', 'order_id': 'user_1766034344444_order_1766034442_9c75a3fe', 'detail': '{}', 'sign': 'd9bd124d79dbf37705fa50776d5a0895', 'pay_time': '2025-12-18 13:07:36', 'pay_price': '0.10', 'more': ''} +2025-12-17 23:33:33,644 - XorPay 첽֪ͨԭʼ: {'aoid': 'cd904ae8d51f4b9186b56c1845e0d901', 'order_id': 'user_1766034344444_vip_order_1766035997_ea11ef8d', 'detail': '{}', 'sign': 'b92f269f8f5ffad2ec6a22a7ed745d34', 'pay_time': '2025-12-18 13:33:32.605326', 'pay_price': '0.10', 'more': ''} +2025-12-17 23:33:35,203 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK" +2025-12-17 23:33:35,234 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK" +2025-12-17 23:33:35,239 - ֧¼д PocketBase ɹ - user_id: user_1766034344444, type: vip, order_id: user_1766034344444_vip_order_1766035997_ea11ef8d +2025-12-18 01:43:22,250 - XorPay 첽֪ͨԭʼ: {'aoid': 'd7b3aeb1c7244aee9e9706c2489ed6d0', 'order_id': 'user_1766043770158_vip_order_1766043786_e44af2ae', 'detail': '{}', 'sign': '44b15e8c83a1c0210038766fb1c71164', 'pay_time': '2025-12-18 15:43:21.452098', 'pay_price': '0.10', 'more': ''} +2025-12-18 01:43:24,152 - memos ʧ - username: user_1766043770158, status: 400, response: {"code":3,"message":"invalid username: user_1766043770158","details":[]} +2025-12-18 01:43:25,870 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK" +2025-12-18 01:43:25,924 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK" +2025-12-18 01:43:25,926 - ֧¼д PocketBase ɹ - user_id: user_1766043770158, type: vip, order_id: user_1766043770158_vip_order_1766043786_e44af2ae +2025-12-18 01:46:45,905 - XorPay 첽֪ͨԭʼ: {'aoid': '703b2fa82a3d43698c3985884427936d', 'order_id': 'user1766043978189_vip_order_1766043991_c763fbc1', 'detail': '{}', 'sign': 'a90f22b750977427846075400fa91ab2', 'pay_time': '2025-12-18 15:46:45.139323', 'pay_price': '0.10', 'more': ''} +2025-12-18 01:46:47,450 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK" +2025-12-18 01:46:47,471 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 400 Bad Request" +2025-12-18 01:46:47,476 - PocketBase дʧ - order_id: user1766043978189_vip_order_1766043991_c763fbc1, error: Message: Response error. Status code:400 +URL: https://pocketbase.hackrobot.cn/api/collections/payments/records +Status: 400 +Data: {'data': {'type': {'code': 'validation_invalid_value', 'message': 'Invalid value unknown.', 'params': {'value': 'unknown'}}}, 'message': 'Failed to create record.', 'status': 400} +Is Abort: False +Original Error: N/A +2025-12-18 01:51:39,196 - XorPay 첽֪ͨԭʼ: {'aoid': '67f58f4a06194395a1e9365923132b42', 'order_id': 'user1766044269820_vip_order_1766044284_fd06ca4c', 'detail': '{}', 'sign': 'f8461c3ce764597621b12699d82bbd89', 'pay_time': '2025-12-18 15:51:38.453726', 'pay_price': '0.10', 'more': ''} +2025-12-18 01:51:40,720 - memos ûɹ: user1766044269820 +2025-12-18 01:51:42,279 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK" +2025-12-18 01:51:42,301 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK" +2025-12-18 01:51:42,303 - ֧¼д PocketBase ɹ - user_id: user1766044269820, type: vip, order_id: user1766044269820_vip_order_1766044284_fd06ca4c