This commit is contained in:
eric
2025-12-18 01:54:34 -06:00
parent dcc0d77571
commit 9523d311f8
4 changed files with 193 additions and 39 deletions

Binary file not shown.

View File

@@ -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):

View File

@@ -1,4 +1,5 @@
fastapi
uvicorn
sqlalchemy
pydantic
pydantic
python-multipart

26
xorpay_notify.log Normal file
View File

@@ -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