Files
gitlab-instance-0a899031_pa…/app/main.py
2026-01-29 11:10:19 -06:00

315 lines
10 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import hashlib
import time
import random
import string
import requests
from fastapi import Request, HTTPException
import logging
from pocketbase import PocketBase
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==================== PocketBase 配置 ====================
PB_URL = "https://pocketbase.hackrobot.cn"
PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com"
PB_ADMIN_PASSWORD = "Xiao4669805@"
# ========================================================
# 动态获取已登录的 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')
# 全局已处理订单(保留原始)
processed_orders = set()
# ==================== XorPay 配置 ====================
AID = "8220"
SECRET = "afcacd99570945f88de62624aaa3578e"
# ====================================================
# 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()
# 回调验签函数
def verify_xorpay_notify(data: dict) -> bool:
received_sign = data.get("sign", "")
aoid = data.get("aoid", "")
order_id = data.get("order_id", "")
pay_price = data.get("pay_price", "")
pay_time = data.get("pay_time", "")
raw = aoid + order_id + pay_price + pay_time + SECRET
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 = item.get('user_id', 'unknown')
pay_type = item.get('type', 'unknown').lower()
random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8))
timestamp = int(time.time())
order_id = f"{user_id}_{pay_type}_order_{timestamp}_{random_suffix}"
type_map = {
"book": "购买书籍",
"meetup": "数字游民社区报名",
"video": "视频解锁",
"vip": "VIP会员充值",
}
name = type_map.get(pay_type, "商品支付") # ← 修复:用 pay_type 匹配(小写一致)
notify_url = "https://api.hackrobot.cn/xorpay_notify"
params = {
"name": name,
"pay_type": "jsapi",
"price": total_fee_yuan,
"order_id": order_id,
"notify_url": notify_url,
}
params["sign"] = xorpay_sign(
params["name"],
params["pay_type"],
params["price"],
params["order_id"],
params["notify_url"]
)
print("生成的参数:", params)
return {
"status": "ok",
"xorpay_params": params,
"xorpay_url": f"https://xorpay.com/api/cashier/{AID}"
}
# 新增:接收前端提交的 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:
pb_client = get_pb_client()
solan = pb_client.collection("solan")
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,
}
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"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()
data = {key: value for key, value in form_data.items()}
logging.info(f"XorPay 异步通知原始数据: {data}")
print("XorPay 异步通知:", data)
order_id = data.get("order_id", "")
if order_id in processed_orders:
print(f"订单 {order_id} 已处理过,跳过")
return "ok"
if not verify_xorpay_notify(data):
print("验签失败")
logging.error(f"验签失败: {data}")
raise HTTPException(status_code=400, detail="sign error")
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)
user_id = parts[0]
pay_type = parts[1].lower()
else:
user_id = prefix
pay_type = "unknown"
print(f"支付成功用户ID: {user_id},类型: {pay_type},订单: {order_id}")
if pay_type == "vip":
create_memos_user(user_id)
if pay_type == "meetup":
logging.info(f"meetup 类型支付成功(独立逻辑),不创建 memos 用户 - user_id: {user_id}")
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"
amount_cents = int(float(data.get("pay_price", 0)) * 100)
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)
return "ok"
class CreateUserRequest(BaseModel):
username: str = None
password: str = None
@app.post("/create_user")
async def create_user(item: CreateUserRequest):
timenew = str(int(time.time()))
memos_data = {
"username": item.username or f"user{timenew}",
"password": item.password or "123456",
"role": "USER",
"state": "NORMAL"
}
memos_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {MEMOS_TOKEN}"
}
memos_resp = requests.post(MEMOS_API, json=memos_data, headers=memos_headers)
memos_result = memos_resp.json()
print('memos_result-', memos_result)
return memos_result