This commit is contained in:
eric
2026-03-08 01:24:59 -06:00
parent c291e95a39
commit f725104c8a
19 changed files with 645 additions and 172 deletions

View File

@@ -1,15 +1,30 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import hashlib
import time
"""
支付 API 主入口
支持多支付渠道XorPay、ZPAY通过 PAYMENT_PROVIDER 环境变量切换
"""
import logging
import random
import string
import time
from typing import Dict, Any
import requests
from fastapi import Request, HTTPException
import logging
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from pocketbase import PocketBase
from .config import (
PAYMENT_PROVIDER,
BASE_URL,
PB_URL,
PB_ADMIN_EMAIL,
PB_ADMIN_PASSWORD,
MEMOS_API,
MEMOS_TOKEN,
)
from .payment import get_payment_provider
app = FastAPI()
app.add_middleware(
@@ -20,57 +35,30 @@ app.add_middleware(
allow_headers=["*"],
)
# ==================== PocketBase 配置 ====================
PB_URL = "https://pocketbase.hackrobot.cn"
PB_ADMIN_EMAIL = "xiaoshuang.eric@gmail.com"
PB_ADMIN_PASSWORD = "Xiao4669805@"
# ========================================================
# 日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(message)s",
handlers=[
logging.FileHandler("payment_notify.log"),
logging.StreamHandler(),
],
)
# 已处理订单(防重复)
processed_orders: set[str] = set()
def get_pb_client():
client = PocketBase(PB_URL)
try:
client.admins.auth_with_password(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD)
print("PocketBase admin 登录成功(动态)")
print("PocketBase admin 登录成功")
except Exception as e:
print("PocketBase admin 登录失败:", e)
raise e
return client
# 配置日志(添加控制台输出,便于调试)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(message)s',
handlers=[
logging.FileHandler('xorpay_notify.log'),
logging.StreamHandler() # 新增:同时输出到控制台
]
)
# 全局已处理订单
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:
@@ -88,11 +76,11 @@ def create_memos_user(user_id: str, password: str = "123456"):
"username": username,
"password": password,
"role": "USER",
"state": "NORMAL"
"state": "NORMAL",
}
memos_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {MEMOS_TOKEN}"
"Authorization": f"Bearer {MEMOS_TOKEN}",
}
response = requests.post(MEMOS_API, json=memos_data, headers=memos_headers, timeout=10)
@@ -113,15 +101,89 @@ def create_memos_user(user_id: str, password: str = "123456"):
logging.error(f"创建 memos 用户异常 - username: {user_id}, error: {e}")
return {"success": False, "error": str(e)}
def _parse_order_id(order_id: str) -> tuple[str, str]:
"""从 order_id 解析 user_id 和 pay_type"""
if "_order_" not in order_id:
return "", "unknown"
prefix = order_id.split("_order_")[0]
if "_" in prefix:
parts = prefix.rsplit("_", 1)
return parts[0], parts[1].lower()
return prefix, "unknown"
def _handle_payment_success(order_id: str, pay_price: str, provider_name: str):
"""支付成功后的统一业务处理"""
user_id, pay_type = _parse_order_id(order_id)
if not user_id:
return
print(f"支付成功 [{provider_name}] 用户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}")
valid_types = ["vip", "book", "meetup", "video"]
if pay_type not in valid_types:
pay_type = "vip"
amount_cents = int(float(pay_price or 0) * 100)
try:
pb_client = get_pb_client()
if pay_type == "meetup":
solan = pb_client.collection("solan")
existing = solan.get_list(1, 1, {
"filter": f'user_id = "{user_id}"',
"sort": "-created",
})
if existing.items:
record_id = existing.items[0].id
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:
pb_client.collection("payments").create({
"user_id": user_id,
"type": pay_type,
"order_id": order_id,
"amount": amount_cents,
})
except Exception as e:
print(f"PocketBase 操作失败: {e}")
logging.error(f"PocketBase 操作失败 - order_id: {order_id}, error: {e}")
finally:
processed_orders.add(order_id)
# ==================== 支付接口 ====================
@app.post("/payh5")
async def payh5(item: dict):
print('payh5 item:', item)
"""创建 H5 支付订单(根据 PAYMENT_PROVIDER 使用对应渠道)"""
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()
user_id = item.get("user_id", "unknown")
pay_type = item.get("type", "unknown").lower()
random_suffix = ''.join(random.choices(string.hexdigits.lower(), k=8))
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}"
@@ -133,37 +195,108 @@ async def payh5(item: dict):
}
name = type_map.get(pay_type, "商品支付")
notify_url = "https://api.hackrobot.cn/xorpay_notify"
provider = get_payment_provider()
notify_path = "/xorpay_notify" if provider.name == "xorpay" else "/zpay_notify"
notify_url = f"{BASE_URL.rstrip('/')}{notify_path}"
params = {
"name": name,
"pay_type": "jsapi",
"price": total_fee_yuan,
"order_id": order_id,
"notify_url": notify_url,
}
# 支付方式:前端可传 channel=alipay|wxpayZPAY 使用XorPay 固定 jsapi
channel = item.get("channel", "alipay")
params["sign"] = xorpay_sign(
params["name"],
params["pay_type"],
params["price"],
params["order_id"],
params["notify_url"]
result = provider.create_order(
order_id=order_id,
name=name,
total_fee_yuan=total_fee_yuan,
pay_type=channel,
notify_url=notify_url,
return_url=item.get("return_url"),
client_ip=item.get("client_ip"),
)
print("生成的参数:", params)
# 兼容旧版前端:保留 xorpay_params / xorpay_url 字段名
if provider.name == "xorpay":
return {
"status": "ok",
"xorpay_params": result.get("params", {}),
"xorpay_url": result.get("pay_url", ""),
"provider": provider.name,
}
return {
"status": "ok",
"xorpay_params": params,
"xorpay_url": f"https://xorpay.com/api/cashier/{AID}"
"params": result.get("params", {}),
"pay_url": result.get("pay_url", ""),
"provider": provider.name,
**{k: v for k, v in result.items() if k not in ("status", "params", "pay_url", "provider")},
}
@app.post("/xorpay_notify")
async def xorpay_notify(request: Request):
"""XorPay 异步通知POST form"""
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
logging.info(f"XorPay 异步通知: {data}")
order_id = data.get("order_id", "")
if order_id in processed_orders:
return "ok"
provider = get_payment_provider()
if provider.name != "xorpay":
raise HTTPException(status_code=400, detail="xorpay_notify only for xorpay")
if not provider.verify_notify(data):
logging.error(f"XorPay 验签失败: {data}")
raise HTTPException(status_code=400, detail="sign error")
parsed = provider.parse_notify(data)
if parsed.get("trade_status") != "success":
return "ok"
_handle_payment_success(order_id, parsed.get("pay_price", data.get("pay_price", "")), "xorpay")
return provider.success_response()
@app.get("/zpay_notify")
@app.post("/zpay_notify")
async def zpay_notify(request: Request):
"""ZPAY 异步通知GET 或 POST"""
if request.method == "GET":
data = dict(request.query_params)
else:
form_data = await request.form()
data = {k: v for k, v in form_data.items()}
logging.info(f"ZPAY 异步通知: {data}")
order_id = data.get("out_trade_no", "")
if order_id in processed_orders:
return "success"
provider = get_payment_provider()
if provider.name != "zpay":
raise HTTPException(status_code=400, detail="zpay_notify only for zpay")
if not provider.verify_notify(data):
logging.error(f"ZPAY 验签失败: {data}")
raise HTTPException(status_code=400, detail="sign error")
parsed = provider.parse_notify(data)
if parsed.get("trade_status") != "success":
return "success"
_handle_payment_success(order_id, parsed.get("pay_price", data.get("money", "")), "zpay")
return provider.success_response()
# ==================== 其他接口 ====================
@app.post("/submit_meetup_application")
async def submit_meetup_application(item: dict):
print('submit_meetup_application item:', item)
print("submit_meetup_application item:", item)
user_id = item.get('user_id')
user_id = item.get("user_id")
if not user_id:
raise HTTPException(status_code=400, detail="missing user_id")
@@ -196,100 +329,11 @@ async def submit_meetup_application(item: dict):
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")
# 修复 filter 语法:添加空格和 '=' 操作符PocketBase 要求)
# 同时添加 sort 以取最新记录(防止同一 user_id 多条)
existing = solan.get_list(1, 1, {
"filter": f'user_id = "{user_id}"',
"sort": "-created" # 按创建时间倒序,取最新一条
})
if existing.items:
record_id = existing.items[0].id
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:
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
username: str | None = None
password: str | None = None
@app.post("/create_user")
async def create_user(item: CreateUserRequest):
@@ -298,13 +342,13 @@ async def create_user(item: CreateUserRequest):
"username": item.username or f"user{timenew}",
"password": item.password or "123456",
"role": "USER",
"state": "NORMAL"
"state": "NORMAL",
}
memos_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {MEMOS_TOKEN}"
"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
print("memos_result-", memos_result)
return memos_result