diff --git a/app/main.py b/app/main.py index 07ea39b..3cfa8c6 100755 --- a/app/main.py +++ b/app/main.py @@ -23,22 +23,10 @@ app.add_middleware( # ==================== PocketBase 配置 ==================== PB_URL = "https://pocketbase.hackrobot.cn" -PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com" # 建议用 admin 账号 -PB_ADMIN_PASSWORD = "Xiao4669805@" # 安全起见,建议用环境变量 +PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com" +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) @@ -47,27 +35,27 @@ def get_pb_client(): print("PocketBase admin 登录成功(动态)") except Exception as e: print("PocketBase admin 登录失败:", e) - raise 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 +AID = "8220" +SECRET = "afcacd99570945f88de62624aaa3578e" # ==================================================== -# memos 配置(从原代码恢复) +# memos 配置(保留原始) MEMOS_API = "https://qun.hackrobot.cn/api/v1/users" MEMOS_TOKEN = "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg" -# 官方推荐签名方式(严格无分隔符拼接) +# 官方推荐签名方式 def xorpay_sign(name: str, pay_type: str, price: str, order_id: str, notify_url: str) -> str: raw = name + pay_type + price + order_id + notify_url + SECRET return hashlib.md5(raw.encode('utf-8')).hexdigest().lower() @@ -83,34 +71,70 @@ def verify_xorpay_notify(data: dict) -> bool: calculated_sign = hashlib.md5(raw.encode('utf-8')).hexdigest().lower() return calculated_sign == received_sign +def create_memos_user(user_id: str, password: str = "123456"): + try: + if user_id.startswith("user_"): + cleaned_username = "user" + user_id.split("user_", 1)[1] + 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): # ← 修复:status_code + 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("/payh5") 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') + pay_type = item.get('type', 'unknown').lower() random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8)) - # 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": "购买书籍", - "meetup": "活动报名", + "meetup": "数字游民社区报名", "video": "视频解锁", "vip": "VIP会员充值", } - name = type_map.get(item.get('type'), "商品支付") + name = type_map.get(pay_type, "商品支付") # ← 修复:用 pay_type 匹配(小写一致) - # notify_url = "https://www.hackrobot.cn/xorpay_notify" # 建议使用完整路径,避免冲突 - notify_url = "https://api.hackrobot.cn/xorpay_notify" # 建议使用完整路径,避免冲突 + notify_url = "https://api.hackrobot.cn/xorpay_notify" params = { "name": name, @@ -130,61 +154,53 @@ async def payh5(item: dict): print("生成的参数:", params) - # 返回参数字典,前端用 form POST 到 xorpay 收银台 return { "status": "ok", "xorpay_params": params, "xorpay_url": f"https://xorpay.com/api/cashier/{AID}" } -def create_memos_user(user_id: str, password: str = "123456"): - """ - 为指定 user_id 创建 memos 用户 - 支持兼容旧格式(带下划线),自动清理 - """ +# 新增:接收前端提交的 meetup 申请表单(存申请字段,支付字段留空) +@app.post("/submit_meetup_application") +async def submit_meetup_application(item: dict): + print('submit_meetup_application item:', item) + + user_id = item.get('user_id') + if not user_id: + raise HTTPException(status_code=400, detail="missing user_id") + 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 # 已经是干净的,直接用 + pb_client = get_pb_client() + solan = pb_client.collection("solan") - 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}" + pb_data = { + "user_id": user_id, + "nickname": item.get("nickname", ""), + "occupation": item.get("occupation", ""), + "reason": item.get("reason", ""), + "wechatId": item.get("wechatId", ""), + "gender": item.get("gender", ""), + "education": item.get("education", ""), + "gradYear": item.get("gradYear", ""), + "phone": item.get("phone", ""), + "calculated_age": item.get("calculated_age", 0), + # 支付字段留空,等待回调填充 + "type": "", + "order_id": "", + "amount": 0, } - 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} + record = solan.create(pb_data) + logging.info(f"meetup 申请表单提交成功(待支付) - user_id: {user_id}, record_id: {record.id}") + return {"status": "ok", "record_id": record.id} except Exception as e: - print(f"创建 memos 用户异常: {e}") - logging.error(f"创建 memos 用户异常 - username: {user_id}, error: {e}") - return {"success": False, "error": str(e)} - + print(f"meetup 申请提交失败: {e}") + logging.error(f"meetup 申请提交失败 - user_id: {user_id}, error: {e}") + raise HTTPException(status_code=500, detail="submit failed") + + -# 异步通知回调 @app.post("/xorpay_notify") async def xorpay_notify(request: Request): form_data = await request.form() @@ -195,69 +211,87 @@ async def xorpay_notify(request: Request): order_id = data.get("order_id", "") - # 幂等性检查 if order_id in processed_orders: print(f"订单 {order_id} 已处理过,跳过") return "ok" - # 1. 验签 if not verify_xorpay_notify(data): print("验签失败") logging.error(f"验签失败: {data}") raise HTTPException(status_code=400, detail="sign error") - # 2. 正确解析 user_id 和 pay_type if "_order_" not in order_id: raise HTTPException(status_code=400, detail="invalid order_id format") prefix = order_id.split("_order_")[0] if "_" in prefix: - parts = prefix.rsplit("_", 1) # 从右边拆分一次 + parts = prefix.rsplit("_", 1) user_id = parts[0] - pay_type = parts[1].lower() # ← 强制小写 + pay_type = parts[1].lower() else: user_id = prefix pay_type = "unknown" print(f"支付成功,用户ID: {user_id},类型: {pay_type},订单: {order_id}") - # 在解析完 pay_type 后,只有 vip 才调用 if pay_type == "vip": - create_memos_user(user_id) # 直接传 user_id,密码默认 123456 + create_memos_user(user_id) + + if pay_type == "meetup": + logging.info(f"meetup 类型支付成功(独立逻辑),不创建 memos 用户 - user_id: {user_id}") - # ------------------- 写入 PocketBase ------------------- try: - # 动态获取已登录的客户端 pb_client = get_pb_client() - valid_types = ["vip", "book", "meetup", "video"] # 根据你实际允许的类型填 + 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" 跳过写入 + pay_type = "vip" - pb_data = { - "user_id": user_id, - "type": pay_type, - "order_id": order_id, - "amount": int(float(data.get("pay_price", 0)) * 100), - } + amount_cents = 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}") + if pay_type == "meetup": + solan = pb_client.collection("solan") + # 查找该 user_id 的待支付记录 + existing = solan.list(filter=f'user_id="{user_id}"', per_page=1) + if existing.items: + record_id = existing.items[0].id + # PATCH 填充支付字段 + solan.update(record_id, { + "type": "meetup", + "order_id": order_id, + "amount": amount_cents, + }) + logging.info(f"meetup 支付字段补齐成功 - user_id: {user_id}, record_id: {record_id}") + else: + # 兜底:如果没找到申请记录,直接创建完整记录 + solan.create({ + "user_id": user_id, + "type": "meetup", + "order_id": order_id, + "amount": amount_cents, + }) + logging.info(f"meetup 完整记录创建(兜底) - user_id: {user_id}") + else: + # 其他类型仍写入 payments + pb_client.collection("payments").create({ + "user_id": user_id, + "type": pay_type, + "order_id": order_id, + "amount": amount_cents, + }) 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) # 防止重试风暴 + print(f"PocketBase 操作失败: {e}") + logging.error(f"PocketBase 操作失败 - order_id: {order_id}, error: {e}") + processed_orders.add(order_id) return "ok" -# 保留原来的手动创建用户接口(如果还需要) class CreateUserRequest(BaseModel): username: str = None password: str = None