's'
This commit is contained in:
230
app/main.py
230
app/main.py
@@ -23,22 +23,10 @@ app.add_middleware(
|
|||||||
|
|
||||||
# ==================== PocketBase 配置 ====================
|
# ==================== PocketBase 配置 ====================
|
||||||
PB_URL = "https://pocketbase.hackrobot.cn"
|
PB_URL = "https://pocketbase.hackrobot.cn"
|
||||||
PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com" # 建议用 admin 账号
|
PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com"
|
||||||
PB_ADMIN_PASSWORD = "Xiao4669805@" # 安全起见,建议用环境变量
|
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 客户端
|
# 动态获取已登录的 PocketBase 客户端
|
||||||
def get_pb_client():
|
def get_pb_client():
|
||||||
client = PocketBase(PB_URL)
|
client = PocketBase(PB_URL)
|
||||||
@@ -47,27 +35,27 @@ def get_pb_client():
|
|||||||
print("PocketBase admin 登录成功(动态)")
|
print("PocketBase admin 登录成功(动态)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("PocketBase admin 登录失败:", e)
|
print("PocketBase admin 登录失败:", e)
|
||||||
raise e # 如果登录失败,直接抛异常,避免后续写入失败
|
raise e
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
# 配置日志(可选,记录到文件方便排查)
|
# 配置日志
|
||||||
logging.basicConfig(filename='xorpay_notify.log', level=logging.INFO,
|
logging.basicConfig(filename='xorpay_notify.log', level=logging.INFO,
|
||||||
format='%(asctime)s - %(message)s')
|
format='%(asctime)s - %(message)s')
|
||||||
|
|
||||||
# 全局已处理订单(测试用,生产建议用 Redis 或数据库)
|
# 全局已处理订单(保留原始)
|
||||||
processed_orders = set()
|
processed_orders = set()
|
||||||
|
|
||||||
# ==================== XorPay 配置 ====================
|
# ==================== XorPay 配置 ====================
|
||||||
AID = "8220" # 你的 aid
|
AID = "8220"
|
||||||
SECRET = "afcacd99570945f88de62624aaa3578e" # 你的 app secret
|
SECRET = "afcacd99570945f88de62624aaa3578e"
|
||||||
# ====================================================
|
# ====================================================
|
||||||
|
|
||||||
# memos 配置(从原代码恢复)
|
# memos 配置(保留原始)
|
||||||
MEMOS_API = "https://qun.hackrobot.cn/api/v1/users"
|
MEMOS_API = "https://qun.hackrobot.cn/api/v1/users"
|
||||||
MEMOS_TOKEN = "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg"
|
MEMOS_TOKEN = "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg"
|
||||||
|
|
||||||
# 官方推荐签名方式(严格无分隔符拼接)
|
# 官方推荐签名方式
|
||||||
def xorpay_sign(name: str, pay_type: str, price: str, order_id: str, notify_url: str) -> str:
|
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
|
raw = name + pay_type + price + order_id + notify_url + SECRET
|
||||||
return hashlib.md5(raw.encode('utf-8')).hexdigest().lower()
|
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()
|
calculated_sign = hashlib.md5(raw.encode('utf-8')).hexdigest().lower()
|
||||||
return calculated_sign == received_sign
|
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")
|
@app.post("/payh5")
|
||||||
async def payh5(item: dict):
|
async def payh5(item: dict):
|
||||||
print('payh5 item:', item)
|
print('payh5 item:', item)
|
||||||
|
|
||||||
total_fee_yuan = f"{item['total_fee'] / 100:.2f}"
|
total_fee_yuan = f"{item['total_fee'] / 100:.2f}"
|
||||||
# 新增:获取前端传来的 user_id
|
|
||||||
user_id = item.get('user_id', 'unknown')
|
user_id = item.get('user_id', 'unknown')
|
||||||
pay_type = item.get('type', 'unknown').lower() # ← 新增 .lower(),统一小写
|
pay_type = item.get('type', 'unknown').lower()
|
||||||
# 获取 type
|
|
||||||
# pay_type = item.get('type', 'unknown')
|
|
||||||
|
|
||||||
random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8))
|
random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8))
|
||||||
# order_id = f"order_{int(time.time())}_{random_suffix}"
|
|
||||||
timestamp = int(time.time())
|
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}"
|
order_id = f"{user_id}_{pay_type}_order_{timestamp}_{random_suffix}"
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
"book": "购买书籍",
|
"book": "购买书籍",
|
||||||
"meetup": "活动报名",
|
"meetup": "数字游民社区报名",
|
||||||
"video": "视频解锁",
|
"video": "视频解锁",
|
||||||
"vip": "VIP会员充值",
|
"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 = {
|
params = {
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -130,61 +154,53 @@ async def payh5(item: dict):
|
|||||||
|
|
||||||
print("生成的参数:", params)
|
print("生成的参数:", params)
|
||||||
|
|
||||||
# 返回参数字典,前端用 form POST 到 xorpay 收银台
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"xorpay_params": params,
|
"xorpay_params": params,
|
||||||
"xorpay_url": f"https://xorpay.com/api/cashier/{AID}"
|
"xorpay_url": f"https://xorpay.com/api/cashier/{AID}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_memos_user(user_id: str, password: str = "123456"):
|
# 新增:接收前端提交的 meetup 申请表单(存申请字段,支付字段留空)
|
||||||
"""
|
@app.post("/submit_meetup_application")
|
||||||
为指定 user_id 创建 memos 用户
|
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:
|
try:
|
||||||
# 新增:清理用户名,确保无下划线,以字母开头
|
pb_client = get_pb_client()
|
||||||
if user_id.startswith("user_"):
|
solan = pb_client.collection("solan")
|
||||||
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
|
pb_data = {
|
||||||
print(f"正在为用户创建 memos 账号: {username}")
|
"user_id": user_id,
|
||||||
|
"nickname": item.get("nickname", ""),
|
||||||
memos_data = {
|
"occupation": item.get("occupation", ""),
|
||||||
"username": username,
|
"reason": item.get("reason", ""),
|
||||||
"password": password,
|
"wechatId": item.get("wechatId", ""),
|
||||||
"role": "USER",
|
"gender": item.get("gender", ""),
|
||||||
"state": "NORMAL"
|
"education": item.get("education", ""),
|
||||||
}
|
"gradYear": item.get("gradYear", ""),
|
||||||
memos_headers = {
|
"phone": item.get("phone", ""),
|
||||||
"Content-Type": "application/json",
|
"calculated_age": item.get("calculated_age", 0),
|
||||||
"Authorization": f"Bearer {MEMOS_TOKEN}"
|
# 支付字段留空,等待回调填充
|
||||||
|
"type": "",
|
||||||
|
"order_id": "",
|
||||||
|
"amount": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(MEMOS_API, json=memos_data, headers=memos_headers, timeout=10)
|
record = solan.create(pb_data)
|
||||||
|
logging.info(f"meetup 申请表单提交成功(待支付) - user_id: {user_id}, record_id: {record.id}")
|
||||||
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}
|
|
||||||
|
|
||||||
|
return {"status": "ok", "record_id": record.id}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"创建 memos 用户异常: {e}")
|
print(f"meetup 申请提交失败: {e}")
|
||||||
logging.error(f"创建 memos 用户异常 - username: {user_id}, error: {e}")
|
logging.error(f"meetup 申请提交失败 - user_id: {user_id}, error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
raise HTTPException(status_code=500, detail="submit failed")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 异步通知回调
|
|
||||||
@app.post("/xorpay_notify")
|
@app.post("/xorpay_notify")
|
||||||
async def xorpay_notify(request: Request):
|
async def xorpay_notify(request: Request):
|
||||||
form_data = await request.form()
|
form_data = await request.form()
|
||||||
@@ -195,69 +211,87 @@ async def xorpay_notify(request: Request):
|
|||||||
|
|
||||||
order_id = data.get("order_id", "")
|
order_id = data.get("order_id", "")
|
||||||
|
|
||||||
# 幂等性检查
|
|
||||||
if order_id in processed_orders:
|
if order_id in processed_orders:
|
||||||
print(f"订单 {order_id} 已处理过,跳过")
|
print(f"订单 {order_id} 已处理过,跳过")
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
# 1. 验签
|
|
||||||
if not verify_xorpay_notify(data):
|
if not verify_xorpay_notify(data):
|
||||||
print("验签失败")
|
print("验签失败")
|
||||||
logging.error(f"验签失败: {data}")
|
logging.error(f"验签失败: {data}")
|
||||||
raise HTTPException(status_code=400, detail="sign error")
|
raise HTTPException(status_code=400, detail="sign error")
|
||||||
|
|
||||||
# 2. 正确解析 user_id 和 pay_type
|
|
||||||
if "_order_" not in order_id:
|
if "_order_" not in order_id:
|
||||||
raise HTTPException(status_code=400, detail="invalid order_id format")
|
raise HTTPException(status_code=400, detail="invalid order_id format")
|
||||||
|
|
||||||
prefix = order_id.split("_order_")[0]
|
prefix = order_id.split("_order_")[0]
|
||||||
|
|
||||||
if "_" in prefix:
|
if "_" in prefix:
|
||||||
parts = prefix.rsplit("_", 1) # 从右边拆分一次
|
parts = prefix.rsplit("_", 1)
|
||||||
user_id = parts[0]
|
user_id = parts[0]
|
||||||
pay_type = parts[1].lower() # ← 强制小写
|
pay_type = parts[1].lower()
|
||||||
else:
|
else:
|
||||||
user_id = prefix
|
user_id = prefix
|
||||||
pay_type = "unknown"
|
pay_type = "unknown"
|
||||||
|
|
||||||
print(f"支付成功,用户ID: {user_id},类型: {pay_type},订单: {order_id}")
|
print(f"支付成功,用户ID: {user_id},类型: {pay_type},订单: {order_id}")
|
||||||
|
|
||||||
# 在解析完 pay_type 后,只有 vip 才调用
|
|
||||||
if 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:
|
try:
|
||||||
# 动态获取已登录的客户端
|
|
||||||
pb_client = get_pb_client()
|
pb_client = get_pb_client()
|
||||||
|
|
||||||
valid_types = ["vip", "book", "meetup", "video"] # 根据你实际允许的类型填
|
valid_types = ["vip", "book", "meetup", "video"]
|
||||||
if pay_type not in valid_types:
|
if pay_type not in valid_types:
|
||||||
print(f"无效的 pay_type: {pay_type},强制设为 vip 或拒绝写入")
|
print(f"无效的 pay_type: {pay_type},强制设为 vip 或拒绝写入")
|
||||||
pay_type = "vip" # 或者直接 return "ok" 跳过写入
|
pay_type = "vip"
|
||||||
|
|
||||||
pb_data = {
|
amount_cents = int(float(data.get("pay_price", 0)) * 100)
|
||||||
"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)
|
if pay_type == "meetup":
|
||||||
print(f"PocketBase 记录创建成功: {record.id}")
|
solan = pb_client.collection("solan")
|
||||||
logging.info(f"支付记录写入 PocketBase 成功 - user_id: {user_id}, type: {pay_type}, order_id: {order_id}")
|
# 查找该 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)
|
processed_orders.add(order_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"PocketBase 写入失败: {e}")
|
print(f"PocketBase 操作失败: {e}")
|
||||||
logging.error(f"PocketBase 写入失败 - order_id: {order_id}, error: {e}")
|
logging.error(f"PocketBase 操作失败 - order_id: {order_id}, error: {e}")
|
||||||
processed_orders.add(order_id) # 防止重试风暴
|
processed_orders.add(order_id)
|
||||||
|
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
# 保留原来的手动创建用户接口(如果还需要)
|
|
||||||
class CreateUserRequest(BaseModel):
|
class CreateUserRequest(BaseModel):
|
||||||
username: str = None
|
username: str = None
|
||||||
password: str = None
|
password: str = None
|
||||||
|
|||||||
Reference in New Issue
Block a user