's'
This commit is contained in:
384
app/main.py
384
app/main.py
@@ -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|wxpay,ZPAY 使用;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
|
||||
|
||||
Reference in New Issue
Block a user