Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e9c863941 |
35
.env.example
35
.env.example
@@ -1,35 +0,0 @@
|
||||
# ========== 本地调试 ==========
|
||||
# PORT: 默认 8007,与前端 PAYMENT_API_URL 一致
|
||||
# BASE_URL 需为 ZPAY 可访问的回调地址。本地调试时 ZPAY 无法回调 localhost/内网,
|
||||
# 可用 ngrok 等隧道:BASE_URL=https://xxx.ngrok.io
|
||||
# 或直接使用生产地址测试回调(需 payjsapi 已部署到公网)
|
||||
PORT=8007
|
||||
BASE_URL=https://api.hackrobot.cn
|
||||
|
||||
# 支付渠道:zpay | xorpay
|
||||
PAYMENT_PROVIDER=zpay
|
||||
|
||||
# XorPay
|
||||
XORPAY_AID=8220
|
||||
XORPAY_SECRET=your_secret
|
||||
|
||||
# ZPAY(网关: https://zpayz.cn/)
|
||||
ZPAY_PID=2025121809351743
|
||||
ZPAY_KEY=tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89
|
||||
|
||||
# PocketBase(check-user、ensure-user、支付回调、nomadvip/digital/cnomadcna 共用)
|
||||
PB_URL=https://pocketbase.hackrobot.cn
|
||||
PB_ADMIN_EMAIL=xiaoshuang.eric@gmail.com
|
||||
PB_ADMIN_PASSWORD=Xiao4669805@
|
||||
|
||||
# Memos(nomadvip 支付成功后创建账号,与 payjsapi 同源)
|
||||
MEMOS_API=https://qun.hackrobot.cn/api/v1/users
|
||||
MEMOS_TOKEN=eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg
|
||||
|
||||
# MinIO 对象存储(可选)
|
||||
MINIO_ENDPOINT=minioweb.hackrobot.cn
|
||||
MINIO_PORT=443
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_BUCKET=hackrobot
|
||||
MINIO_UPLOAD_PREFIX=joins
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,13 +0,0 @@
|
||||
# 日志文件(不产生,避免 git 本地/线上不一致)
|
||||
*.log
|
||||
payment_notify.log
|
||||
xorpay_notify.log
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
venv/
|
||||
.venv/
|
||||
.env
|
||||
.env.local
|
||||
92
README.md
Executable file → Normal file
92
README.md
Executable file → Normal file
@@ -1,96 +1,6 @@
|
||||
# PayJS API
|
||||
# androidh5api
|
||||
|
||||
支付 API 服务,支持多支付渠道(XorPay、ZPAY),按项目模块化拆分。
|
||||
|
||||
## 路由与项目对应
|
||||
|
||||
| 项目 | 路由前缀 | 说明 |
|
||||
|------|----------|------|
|
||||
| nomadvip | `/nomadvip/*` | 固定 ZPAY,独立实例 |
|
||||
| digital | `/digital/*` | 使用 PAYMENT_PROVIDER |
|
||||
| cnomadcna | `/cnomadcna/*` | 使用 PAYMENT_PROVIDER |
|
||||
| nomadlms | `/nomadlms/*` | 使用 PAYMENT_PROVIDER |
|
||||
| legacy | `/payh5`、`/zpay_notify` 等 | 向后兼容 |
|
||||
|
||||
各项目业务互不影响,可单独配置。
|
||||
|
||||
## 环境
|
||||
|
||||
本地调试:`BASE_URL` 需为 ZPAY 可访问地址(如 ngrok 暴露 8700 端口)
|
||||
线上部署:`BASE_URL=https://api.hackrobot.cn`
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── config.py # 配置(环境变量)
|
||||
├── main.py # FastAPI 入口,挂载路由
|
||||
├── sdk/ # SDK 组件化封装
|
||||
│ ├── payment/ # 支付网关(ZPAY/XorPay)
|
||||
│ ├── pocketbase.py # PocketBase 数据库
|
||||
│ ├── minio.py # MinIO 对象存储
|
||||
│ └── memos.py # Memos 用户(仅 nomadvip 使用)
|
||||
├── services/ # 共享业务逻辑(基于 SDK)
|
||||
│ ├── pb.py # PocketBase 客户端
|
||||
│ ├── memos.py # Memos 用户创建
|
||||
│ ├── minio.py # MinIO 上传
|
||||
│ └── payment.py # 支付成功处理
|
||||
├── routers/ # 项目路由
|
||||
│ ├── nomadvip.py # /nomadvip/*
|
||||
│ ├── digital.py # /digital/*
|
||||
│ ├── cnomadcna.py # /cnomadcna/*
|
||||
│ ├── nomadlms.py # /nomadlms/*
|
||||
│ ├── legacy.py # /payh5 等(向后兼容)
|
||||
│ └── common.py # /submit_meetup_application、/create_user
|
||||
└── payment/
|
||||
├── base.py # 支付抽象基类
|
||||
├── xorpay.py # XorPay 实现
|
||||
├── zpay.py # ZPAY 实现(易支付兼容)
|
||||
└── factory.py # 渠道工厂
|
||||
```
|
||||
|
||||
## 支付渠道
|
||||
|
||||
| 渠道 | 环境变量 | 说明 |
|
||||
|------|----------|------|
|
||||
| XorPay | `PAYMENT_PROVIDER=xorpay` | 默认 |
|
||||
| ZPAY | `PAYMENT_PROVIDER=zpay` | 需配置 `ZPAY_PID`、`ZPAY_KEY` |
|
||||
|
||||
切换渠道:设置环境变量 `PAYMENT_PROVIDER=xorpay` 或 `zpay`,参考 `.env.example`。
|
||||
|
||||
## 本地调试
|
||||
|
||||
```bash
|
||||
# 复制配置
|
||||
cp .env.example .env
|
||||
|
||||
# 本地调试时 BASE_URL 需 ZPAY 可访问,可用 ngrok:
|
||||
# ngrok http 8700 -> 将 BASE_URL 设为 ngrok 提供的 https 地址
|
||||
|
||||
python run.py
|
||||
# 或 uvicorn app.main:app --host 0.0.0.0 --port 8700 --reload
|
||||
```
|
||||
|
||||
## 线上部署
|
||||
|
||||
域名:`https://api.hackrobot.cn`
|
||||
|
||||
```bash
|
||||
# .env 中设置
|
||||
BASE_URL=https://api.hackrobot.cn
|
||||
PAYMENT_PROVIDER=zpay
|
||||
# 其他 ZPAY、PocketBase、Memos 配置见 .env.example
|
||||
```
|
||||
|
||||
## 线上「支付失败」排查
|
||||
|
||||
本地正常、Ubuntu 部署后显示支付失败,常见原因:
|
||||
|
||||
1. **BASE_URL 配置错误**:必须为 ZPAY 可公网访问的地址。错误示例:`http://127.0.0.1:8700`、`http://localhost`。正确:`https://api.hackrobot.cn`(域名需解析到本机,nginx 反向代理 8700 端口)。
|
||||
|
||||
2. **服务器无法访问 zpayz.cn**:防火墙、DNS 或网络限制。访问 `https://你的域名/health?check_zpay=1` 自检,若 `zpayz_reachable: false` 则需开放出站或检查网络。
|
||||
|
||||
3. **启动时控制台有 BASE_URL 警告**:说明配置了内网地址,需修改 `.env` 中的 `BASE_URL` 为公网地址后重启。
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
python3 run.py
|
||||
0
app/__init__.py
Executable file → Normal file
0
app/__init__.py
Executable file → Normal file
BIN
app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/__pycache__/crud.cpython-310.pyc
Normal file
BIN
app/__pycache__/crud.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/crud.cpython-311.pyc
Normal file
BIN
app/__pycache__/crud.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-310.pyc
Normal file
BIN
app/__pycache__/database.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-311.pyc
Normal file
BIN
app/__pycache__/database.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-310.pyc
Normal file
BIN
app/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-311.pyc
Normal file
BIN
app/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/__pycache__/models.cpython-310.pyc
Normal file
BIN
app/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/models.cpython-311.pyc
Normal file
BIN
app/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schemas.cpython-310.pyc
Normal file
BIN
app/__pycache__/schemas.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schemas.cpython-311.pyc
Normal file
BIN
app/__pycache__/schemas.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/tgMesaage.cpython-310.pyc
Normal file
BIN
app/__pycache__/tgMesaage.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/tgMessage.cpython-310.pyc
Normal file
BIN
app/__pycache__/tgMessage.cpython-310.pyc
Normal file
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
"""
|
||||
应用配置
|
||||
支付渠道可通过 PAYMENT_PROVIDER 切换:xorpay | zpay
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 强制从 payjsapi 项目根目录加载 .env、.env.local,避免 cwd 不同导致加载失败
|
||||
_root = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(_root / ".env")
|
||||
load_dotenv(_root / ".env.local", override=True)
|
||||
|
||||
PaymentChannel = Literal["xorpay", "zpay"]
|
||||
|
||||
# 当前使用的支付渠道(zpay 替代 xorpay,payjs2 接口业务保留)
|
||||
PAYMENT_PROVIDER: PaymentChannel = os.getenv("PAYMENT_PROVIDER", "zpay").lower()
|
||||
|
||||
# XorPay 配置
|
||||
XORPAY_AID = os.getenv("XORPAY_AID", "8220")
|
||||
XORPAY_SECRET = os.getenv("XORPAY_SECRET", "afcacd99570945f88de62624aaa3578e")
|
||||
XORPAY_CASHIER_URL = os.getenv("XORPAY_CASHIER_URL", "https://xorpay.com/api/cashier")
|
||||
|
||||
# ZPAY 配置(网关: https://zpayz.cn/)
|
||||
ZPAY_PID = os.getenv("ZPAY_PID", "2025121809351743")
|
||||
ZPAY_KEY = os.getenv("ZPAY_KEY", "tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89")
|
||||
|
||||
# PocketBase(支持 PB_ADMIN_* 或 POCKETBASE_*,空值时用默认)
|
||||
PB_URL = os.getenv("PB_URL") or os.getenv("POCKETBASE_URL") or "https://pocketbase.hackrobot.cn"
|
||||
PB_ADMIN_EMAIL = (
|
||||
os.getenv("PB_ADMIN_EMAIL") or os.getenv("POCKETBASE_EMAIL") or "xiaoshuang.eric@gmail.com"
|
||||
)
|
||||
PB_ADMIN_PASSWORD = (
|
||||
os.getenv("PB_ADMIN_PASSWORD") or os.getenv("POCKETBASE_PASSWORD") or "Xiao4669805@"
|
||||
)
|
||||
|
||||
# Memos
|
||||
MEMOS_API = os.getenv("MEMOS_API", "https://qun.hackrobot.cn/api/v1/users")
|
||||
MEMOS_TOKEN = os.getenv(
|
||||
"MEMOS_TOKEN",
|
||||
"eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg",
|
||||
)
|
||||
|
||||
# 回调地址(用于生成 notify_url)
|
||||
BASE_URL = os.getenv("BASE_URL", "https://api.hackrobot.cn")
|
||||
|
||||
# MinIO 对象存储
|
||||
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minioweb.hackrobot.cn")
|
||||
MINIO_PORT = int(os.getenv("MINIO_PORT", "443"))
|
||||
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "")
|
||||
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "")
|
||||
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "hackrobot")
|
||||
MINIO_USE_SSL = os.getenv("MINIO_USE_SSL", "true").lower() == "true"
|
||||
_minio_ep = os.getenv("MINIO_ENDPOINT", "minioweb.hackrobot.cn")
|
||||
_minio_bucket = os.getenv("MINIO_BUCKET", "hackrobot")
|
||||
MINIO_PUBLIC_URL = os.getenv("MINIO_PUBLIC_URL", f"https://{_minio_ep}/{_minio_bucket}")
|
||||
MINIO_UPLOAD_PREFIX = os.getenv("MINIO_UPLOAD_PREFIX", "joins")
|
||||
235
app/main.py
Executable file → Normal file
235
app/main.py
Executable file → Normal file
@@ -1,168 +1,91 @@
|
||||
"""
|
||||
支付 API 主入口
|
||||
支持多支付渠道(XorPay、ZPAY),通过 PAYMENT_PROVIDER 环境变量切换
|
||||
|
||||
路由模块化,按项目拆分:
|
||||
- nomadvip: /nomadvip/* - 固定 ZPAY
|
||||
- digital: /digital/* - 使用 PAYMENT_PROVIDER
|
||||
- cnomadcna: /cnomadcna/* - 使用 PAYMENT_PROVIDER
|
||||
- nomadlms: /nomadlms/* - 使用 PAYMENT_PROVIDER
|
||||
- legacy: /payh5、/zpay_notify 等(向后兼容)
|
||||
- common: /submit_meetup_application、/create_user
|
||||
|
||||
本地调试:BASE_URL 需为 ZPAY 可访问地址(如 ngrok)
|
||||
线上部署:BASE_URL=https://api.hackrobot.cn(必须为 ZPAY 可公网访问的地址)
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import requests
|
||||
|
||||
from .config import BASE_URL
|
||||
from .routers import (
|
||||
nomadvip_router,
|
||||
digital_router,
|
||||
cnomadcna_router,
|
||||
nomadlms_router,
|
||||
legacy_router,
|
||||
common_router,
|
||||
meetup_router,
|
||||
)
|
||||
import hashlib
|
||||
from urllib.parse import urlencode, unquote
|
||||
import time
|
||||
"""
|
||||
JSAPI 支付
|
||||
"""
|
||||
key = "zA2hBU6pv6CIBzkW" # 填写通信密钥
|
||||
mchid = "1561724891" # 特写商户号
|
||||
|
||||
app = FastAPI(
|
||||
title="PayJS API",
|
||||
description="nomadvip / digital / cnomadcna / nomadlms 支付接口",
|
||||
version="2.0",
|
||||
)
|
||||
|
||||
# 构造签名函数
|
||||
def sign(attributes):
|
||||
attributes_new = {k: attributes[k] for k in sorted(attributes.keys())}
|
||||
return (hashlib.md5((unquote(urlencode(attributes_new)) + "&key=" +
|
||||
key).encode(encoding="utf-8")).hexdigest().upper())
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
event: str
|
||||
EventGroupMsg: None = None
|
||||
prirobot_wxidce: None = None
|
||||
type: None = None
|
||||
|
||||
|
||||
# event
|
||||
# "":"EventGroupMsg",//事件标示(当前值为群消息事件)
|
||||
# "robot_wxid":"wxid_5hxa04j4z6pg22",//机器人wxid
|
||||
# "robot_name":"",//机器人昵称,一般为空
|
||||
# "type":1,//1/文本消息 3/图片消息 34/语音消息 42/名片消息 43/视频 47/动态表情 48/地理位置 49/分享链接 2000/转账 2001/红包 2002/小程序 2003/群邀请
|
||||
# "from_wxid":"18900134932@chatroom",//群id,群消息事件才有
|
||||
# "from_name":"微群测",//群名字
|
||||
# "final_from_wxid":"sundreamer",//发该消息的用户微信id
|
||||
# "final_from_name":"遗忘悠剑o",//微信昵称
|
||||
# "to_wxid":"wxid_5hxa04j4z6pg22",//接收消息的人id,(一般是机器人收到了,也有可能是机器人发出的消息,别人收到了,那就是别人)
|
||||
# "msg":"图片https://b3logfile.com/bing/20201024.jpg",//消息内容(string/array) 使用时候根据不同的事件标示来定义这个值,字符串类型或者数据类型
|
||||
# "money":0.01 //金额,只有"EventReceivedTransfer"事件才有该参数
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
|
||||
# 允许所有来源跨域访问(不推荐用于生产环境)
|
||||
origins = ["*"]
|
||||
|
||||
# 设置CORS(跨域资源共享)策略
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_origins=["*"], # 允许所有来源
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # 允许所有方法
|
||||
allow_headers=["*"], # 允许所有请求头
|
||||
)
|
||||
|
||||
|
||||
class _SafeStderrProxy:
|
||||
def write(self, message: str) -> int:
|
||||
try:
|
||||
return sys.stderr.write(message)
|
||||
except (OSError, ValueError):
|
||||
return 0
|
||||
|
||||
def flush(self) -> None:
|
||||
try:
|
||||
sys.stderr.flush()
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _safe_log(level: int, message: str, *args, **kwargs) -> None:
|
||||
try:
|
||||
logging.log(level, message, *args, **kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""捕获未处理异常,记录日志后返回 500(HTTPException 交由 FastAPI 默认处理)"""
|
||||
if isinstance(exc, HTTPException):
|
||||
raise exc
|
||||
_safe_log(logging.ERROR, "未处理异常: %s - %s", request.url.path, exc, exc_info=True)
|
||||
return JSONResponse(status_code=500, content={"detail": str(exc)})
|
||||
|
||||
logging.raiseExceptions = False
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
handlers=[logging.StreamHandler(_SafeStderrProxy())],
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
def _safe_stderr_write(message: str) -> None:
|
||||
try:
|
||||
sys.stderr.write(message)
|
||||
sys.stderr.flush()
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _should_trace_request(path: str) -> bool:
|
||||
return any(
|
||||
segment in path for segment in ("/nomadvip", "/digital", "/cnomadcna", "/nomadlms")
|
||||
)
|
||||
|
||||
# 启动时校验 BASE_URL(线上部署必须为公网可访问地址,否则 ZPAY 无法回调)
|
||||
def _check_base_url():
|
||||
url = (BASE_URL or "").strip().lower()
|
||||
if not url:
|
||||
logging.warning("BASE_URL 未设置,将使用默认值。线上部署请务必设置 BASE_URL 为公网地址")
|
||||
return
|
||||
if "localhost" in url or "127.0.0.1" in url or url.startswith("http://192.168.") or url.startswith("http://10."):
|
||||
logging.warning(
|
||||
"BASE_URL=%s 疑似内网地址,ZPAY 无法回调!线上部署请设置 BASE_URL 为公网地址,如 https://api.hackrobot.cn",
|
||||
BASE_URL,
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
_check_base_url()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health(check_zpay: str = ""):
|
||||
"""健康检查。check_zpay=1 时额外检测 zpayz.cn 连通性"""
|
||||
out = {"status": "ok", "service": "payjsapi", "base_url": BASE_URL}
|
||||
if str(check_zpay).lower() in ("1", "true", "yes"):
|
||||
try:
|
||||
r = requests.get("https://zpayz.cn", timeout=5)
|
||||
out["zpayz_reachable"] = r.status_code in (200, 301, 302, 404)
|
||||
except Exception as e:
|
||||
out["zpayz_reachable"] = False
|
||||
out["zpayz_error"] = str(e)
|
||||
return out
|
||||
|
||||
|
||||
# 请求日志中间件:每个请求都打印到 stderr,确保终端可见
|
||||
@app.middleware("http")
|
||||
async def log_requests(request, call_next):
|
||||
path = request.url.path
|
||||
if _should_trace_request(path):
|
||||
_safe_stderr_write(f"[payjsapi] >>> {request.method} {path}\n")
|
||||
resp = await call_next(request)
|
||||
if _should_trace_request(path):
|
||||
_safe_stderr_write(f"[payjsapi] <<< {request.method} {path} {resp.status_code}\n")
|
||||
return resp
|
||||
|
||||
# 挂载路由
|
||||
app.include_router(nomadvip_router)
|
||||
app.include_router(digital_router)
|
||||
app.include_router(cnomadcna_router)
|
||||
app.include_router(nomadlms_router)
|
||||
app.include_router(legacy_router)
|
||||
app.include_router(common_router)
|
||||
app.include_router(meetup_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "payjsapi",
|
||||
"routes": {
|
||||
"nomadvip": "/nomadvip/payh5, /nomadvip/payh5/redirect, /nomadvip/check_vip, /nomadvip/check_vip_by_email, /nomadvip/order_status, /nomadvip/zpay_order_status, /nomadvip/zpay_notify, /nomadvip/xorpay_notify",
|
||||
"digital": "/digital/payh5, /digital/payh5/redirect, /digital/zpay_order_status, /digital/zpay_notify, /digital/xorpay_notify",
|
||||
"cnomadcna": "/cnomadcna/payh5, ...",
|
||||
"nomadlms": "/nomadlms/payh5, ...",
|
||||
"legacy": "/payh5, /payh5/redirect, /zpay_notify, /xorpay_notify, /zpay_order_status",
|
||||
"common": "/submit_meetup_application, /create_user",
|
||||
},
|
||||
return JSONResponse(content={"message": "Hello World"})
|
||||
|
||||
|
||||
@app.post("/payh5")
|
||||
async def payh5(item: dict):
|
||||
print('item', item)
|
||||
# 时间戳
|
||||
timenew = str(int(time.time()))
|
||||
order = {
|
||||
"mchid": mchid,
|
||||
"body": timenew, # 订单标题
|
||||
"out_trade_no": timenew, # 订单号
|
||||
"total_fee": item['total_fee'], # 金额,单位:分
|
||||
# "openid": "o7LFAwWu3OhSrs49-1x5cSlFm0D8", # 通过openid接口获取到的openid
|
||||
# "notify_url": ""
|
||||
# "callback_url":'https://aiimg.hackrobot.cn/api'
|
||||
}
|
||||
|
||||
order["callback_url"] = item['callback_url']
|
||||
order["openid"] = item['openid']
|
||||
order["sign"] = sign(order)
|
||||
|
||||
request_url = "https://payjs.cn/api/jsapi"
|
||||
# 返回结果中包含`jsapi`字段,该字段的值即是前端发起时所需的6个支付参数
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
response = requests.post(request_url, data=order, headers=headers)
|
||||
if response:
|
||||
print(response.json())
|
||||
return response.json()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
支付模块
|
||||
支持多支付渠道,可通过配置切换
|
||||
"""
|
||||
from .base import PaymentProvider
|
||||
from .xorpay import XorPayProvider
|
||||
from .zpay import ZPayProvider
|
||||
from .factory import (
|
||||
get_payment_provider,
|
||||
get_payment_provider_by_name,
|
||||
reset_provider,
|
||||
resolve_provider,
|
||||
resolve_channel,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PaymentProvider",
|
||||
"XorPayProvider",
|
||||
"ZPayProvider",
|
||||
"get_payment_provider",
|
||||
"get_payment_provider_by_name",
|
||||
"reset_provider",
|
||||
"resolve_provider",
|
||||
"resolve_channel",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
"""
|
||||
支付渠道抽象基类
|
||||
定义统一的支付接口,便于切换不同支付渠道(XorPay、ZPAY 等)
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class PaymentProvider(ABC):
|
||||
"""支付渠道抽象基类"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""支付渠道名称"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建支付订单
|
||||
:return: 包含支付跳转信息等,格式由各渠道自定义
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
||||
"""验证异步通知签名"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
解析异步通知,提取统一字段
|
||||
:return: {
|
||||
"order_id": str, # 商户订单号
|
||||
"trade_no": str, # 渠道订单号
|
||||
"pay_price": str, # 实付金额(元)
|
||||
"pay_time": str, # 支付时间
|
||||
"trade_status": str, # 支付状态
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def success_response(self) -> str:
|
||||
"""异步通知成功时需返回的字符串(如 'ok' 或 'success')"""
|
||||
pass
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
支付渠道工厂
|
||||
根据配置返回对应的支付 Provider
|
||||
默认使用 zpay 替代 xorpay
|
||||
"""
|
||||
from ..config import (
|
||||
PAYMENT_PROVIDER,
|
||||
XORPAY_AID,
|
||||
XORPAY_SECRET,
|
||||
XORPAY_CASHIER_URL,
|
||||
ZPAY_PID,
|
||||
ZPAY_KEY,
|
||||
)
|
||||
from .base import PaymentProvider
|
||||
from .xorpay import XorPayProvider
|
||||
from .zpay import ZPayProvider
|
||||
|
||||
_provider_instance: PaymentProvider | None = None
|
||||
|
||||
|
||||
def get_payment_provider() -> PaymentProvider:
|
||||
"""获取当前配置的支付渠道实例(单例)"""
|
||||
global _provider_instance
|
||||
if _provider_instance is not None:
|
||||
return _provider_instance
|
||||
if PAYMENT_PROVIDER == "zpay":
|
||||
_provider_instance = ZPayProvider(pid=ZPAY_PID, key=ZPAY_KEY)
|
||||
else:
|
||||
_provider_instance = XorPayProvider(
|
||||
aid=XORPAY_AID,
|
||||
secret=XORPAY_SECRET,
|
||||
cashier_url=XORPAY_CASHIER_URL,
|
||||
)
|
||||
return _provider_instance
|
||||
|
||||
|
||||
def get_payment_provider_by_name(name: str) -> PaymentProvider:
|
||||
"""按名称获取 Provider(用于 per-request 覆盖)"""
|
||||
if name == "xorpay":
|
||||
return XorPayProvider(
|
||||
aid=XORPAY_AID,
|
||||
secret=XORPAY_SECRET,
|
||||
cashier_url=XORPAY_CASHIER_URL,
|
||||
)
|
||||
return ZPayProvider(pid=ZPAY_PID, key=ZPAY_KEY)
|
||||
|
||||
|
||||
def reset_provider():
|
||||
"""重置 Provider(用于测试或切换渠道)"""
|
||||
global _provider_instance
|
||||
_provider_instance = None
|
||||
|
||||
|
||||
def resolve_provider(
|
||||
device: str | None = None,
|
||||
req_provider: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> PaymentProvider:
|
||||
"""
|
||||
根据设备解析支付渠道:
|
||||
- 微信内(device=wechat 或 UA 含 MicroMessenger) → xorpay
|
||||
- PC/手机浏览器 → zpay
|
||||
显式传入的 provider 优先。
|
||||
"""
|
||||
req = (req_provider or "").strip().lower()
|
||||
if req in ("xorpay", "zpay"):
|
||||
return get_payment_provider_by_name(req)
|
||||
dev = (device or "pc").lower()
|
||||
if dev == "wechat":
|
||||
return get_payment_provider_by_name("xorpay")
|
||||
ua = (user_agent or "").lower()
|
||||
if "micromessenger" in ua:
|
||||
return get_payment_provider_by_name("xorpay")
|
||||
return get_payment_provider_by_name("zpay")
|
||||
|
||||
|
||||
def resolve_channel(item_channel: str | None) -> str:
|
||||
"""支付方式固定为微信支付,支付宝暂不提供"""
|
||||
return "wxpay"
|
||||
@@ -1,108 +0,0 @@
|
||||
"""
|
||||
XorPay 支付渠道实现
|
||||
文档: https://xorpay.com
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .base import PaymentProvider
|
||||
|
||||
|
||||
class XorPayProvider(PaymentProvider):
|
||||
"""XorPay 支付"""
|
||||
|
||||
def __init__(self, aid: str, secret: str, cashier_url: str = "https://xorpay.com/api/cashier"):
|
||||
self.aid = aid
|
||||
self.secret = secret
|
||||
self.cashier_url = cashier_url.rstrip("/")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "xorpay"
|
||||
|
||||
def _sign(self, name: str, pay_type: str, price: str, order_id: str, notify_url: str) -> str:
|
||||
raw = name + pay_type + price + order_id + notify_url + self.secret
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
|
||||
def _query2_sign(self, order_id: str) -> str:
|
||||
raw = order_id + self.secret
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
# XorPay 使用 jsapi 表示 H5/JSAPI 支付
|
||||
channel_pay_type = "jsapi"
|
||||
params = {
|
||||
"name": name,
|
||||
"pay_type": channel_pay_type,
|
||||
"price": total_fee_yuan,
|
||||
"order_id": order_id,
|
||||
"notify_url": notify_url,
|
||||
}
|
||||
params["sign"] = self._sign(
|
||||
params["name"],
|
||||
params["pay_type"],
|
||||
params["price"],
|
||||
params["order_id"],
|
||||
params["notify_url"],
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": self.name,
|
||||
"params": params,
|
||||
"pay_url": f"{self.cashier_url}/{self.aid}",
|
||||
}
|
||||
|
||||
def verify_notify(self, data: Dict[str, Any]) -> 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 + self.secret
|
||||
calculated = hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
return calculated == received_sign
|
||||
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"order_id": data.get("order_id", ""),
|
||||
"trade_no": data.get("aoid", ""),
|
||||
"pay_price": data.get("pay_price", ""),
|
||||
"pay_time": data.get("pay_time", ""),
|
||||
"trade_status": "success", # XorPay 验签通过即视为成功
|
||||
}
|
||||
|
||||
def success_response(self) -> str:
|
||||
return "ok"
|
||||
|
||||
def query_order_status(self, order_id: str, timeout: int = 10) -> Dict[str, Any]:
|
||||
url = f"https://xorpay.com/api/query2/{self.aid}"
|
||||
params = {
|
||||
"order_id": order_id,
|
||||
"sign": self._query2_sign(order_id),
|
||||
}
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json() or {}
|
||||
status = str(data.get("status", "")).strip().lower()
|
||||
return {
|
||||
"paid": status in ("payed", "success"),
|
||||
"status": status,
|
||||
"pay_price": data.get("pay_price") or data.get("price") or "",
|
||||
"raw": data,
|
||||
}
|
||||
except Exception as error:
|
||||
return {"paid": False, "error": str(error)}
|
||||
@@ -1,207 +0,0 @@
|
||||
"""
|
||||
ZPAY 支付渠道实现
|
||||
文档: https://z-pay.cn/doc.html
|
||||
支持:页面跳转支付、API 接口支付、异步通知验签
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from .base import PaymentProvider
|
||||
|
||||
|
||||
class ZPayProvider(PaymentProvider):
|
||||
"""ZPAY 支付(易支付兼容接口)"""
|
||||
|
||||
# API 地址
|
||||
SUBMIT_URL = "https://zpayz.cn/submit.php" # 页面跳转
|
||||
MAPI_URL = "https://zpayz.cn/mapi.php" # API 接口
|
||||
|
||||
def __init__(self, pid: str, key: str):
|
||||
self.pid = pid
|
||||
self.key = key
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "zpay"
|
||||
|
||||
def _md5_sign(self, params: Dict[str, Any], exclude: Optional[set] = None) -> str:
|
||||
"""
|
||||
ZPAY MD5 签名算法:
|
||||
1. 参数按 ASCII 排序,sign/sign_type/空值不参与
|
||||
2. 拼接为 a=b&c=d
|
||||
3. sign = md5(拼接串 + KEY),小写
|
||||
"""
|
||||
exclude = exclude or {"sign", "sign_type"}
|
||||
filtered = {k: v for k, v in params.items() if k not in exclude and v is not None and v != ""}
|
||||
sorted_keys = sorted(filtered.keys())
|
||||
parts = [f"{k}={filtered[k]}" for k in sorted_keys]
|
||||
raw = "&".join(parts) + self.key
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest().lower()
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: Optional[str] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
# ZPAY: alipay / wxpay(pay_type 可为业务类型或支付方式,此处按支付方式解析)
|
||||
channel_type = "wxpay" if str(pay_type).lower() in ("wx", "wechat", "wxpay") else "alipay"
|
||||
params = {
|
||||
"pid": self.pid,
|
||||
"type": channel_type,
|
||||
"out_trade_no": order_id,
|
||||
"notify_url": notify_url,
|
||||
"name": name,
|
||||
"money": total_fee_yuan,
|
||||
"return_url": return_url or notify_url,
|
||||
}
|
||||
if client_ip:
|
||||
params["clientip"] = client_ip
|
||||
if extra and extra.get("param"):
|
||||
params["param"] = str(extra["param"])
|
||||
params["sign_type"] = "MD5"
|
||||
params["sign"] = self._md5_sign(params)
|
||||
|
||||
# 页面跳转方式:返回 form 提交 URL 和参数
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": self.name,
|
||||
"params": params,
|
||||
"pay_url": self.SUBMIT_URL,
|
||||
"submit_method": "POST",
|
||||
}
|
||||
|
||||
def create_order_api(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
name: str,
|
||||
total_fee_yuan: str,
|
||||
pay_type: str,
|
||||
notify_url: str,
|
||||
return_url: Optional[str] = None,
|
||||
client_ip: str = "127.0.0.1",
|
||||
device: str = "pc",
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
ZPAY API 接口支付(返回 payurl/qrcode 等)
|
||||
适用于需要二维码或 H5 跳转的场景
|
||||
"""
|
||||
channel_type = "wxpay" if pay_type.lower() in ("wx", "wechat", "wxpay") else "alipay"
|
||||
params = {
|
||||
"pid": self.pid,
|
||||
"type": channel_type,
|
||||
"out_trade_no": order_id,
|
||||
"notify_url": notify_url,
|
||||
"name": name,
|
||||
"money": total_fee_yuan,
|
||||
"clientip": client_ip or "127.0.0.1",
|
||||
"device": device,
|
||||
}
|
||||
if return_url:
|
||||
params["return_url"] = return_url
|
||||
if extra and extra.get("param"):
|
||||
params["param"] = str(extra["param"])
|
||||
params["sign_type"] = "MD5"
|
||||
params["sign"] = self._md5_sign(params)
|
||||
|
||||
try:
|
||||
resp = requests.post(self.MAPI_URL, data=params, timeout=15)
|
||||
except requests.RequestException as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"provider": self.name,
|
||||
"msg": f"连接 zpayz.cn 失败: {e}。请检查服务器网络、防火墙或 DNS。",
|
||||
}
|
||||
|
||||
try:
|
||||
result = resp.json()
|
||||
except Exception:
|
||||
result = {}
|
||||
# ZPAY 可能返回二次编码的 JSON(body 为 JSON 字符串),需再次解析
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except Exception:
|
||||
result = {}
|
||||
if not isinstance(result, dict):
|
||||
result = {}
|
||||
# 部分环境下 ZPAY 将成功数据放在 msg 中(msg 为 JSON 字符串)
|
||||
msg_val = result.get("msg")
|
||||
if isinstance(msg_val, str) and msg_val.strip().startswith("{"):
|
||||
try:
|
||||
inner = json.loads(msg_val)
|
||||
if isinstance(inner, dict) and int(float(inner.get("code", 0))) == 1:
|
||||
result = inner
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
code = int(float(result.get("code", 0)))
|
||||
except (TypeError, ValueError):
|
||||
code = 0
|
||||
if code == 1:
|
||||
return {
|
||||
"status": "ok",
|
||||
"provider": self.name,
|
||||
"payurl": result.get("payurl"),
|
||||
"payurl2": result.get("payurl2"), # 微信 H5,手机浏览器打开可唤醒微信
|
||||
"qrcode": result.get("qrcode"),
|
||||
"img": result.get("img"),
|
||||
"O_id": result.get("O_id"),
|
||||
"trade_no": result.get("trade_no"),
|
||||
}
|
||||
return {
|
||||
"status": "error",
|
||||
"provider": self.name,
|
||||
"msg": result.get("msg", resp.text),
|
||||
}
|
||||
|
||||
def verify_notify(self, data: Dict[str, Any]) -> bool:
|
||||
received = data.get("sign", "")
|
||||
calculated = self._md5_sign(data)
|
||||
return calculated == received
|
||||
|
||||
def parse_notify(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# ZPAY 通知:trade_status 为 TRADE_SUCCESS 表示成功
|
||||
status = "success" if data.get("trade_status") == "TRADE_SUCCESS" else "pending"
|
||||
return {
|
||||
"order_id": data.get("out_trade_no", ""),
|
||||
"trade_no": data.get("trade_no", ""),
|
||||
"pay_price": data.get("money", ""),
|
||||
"pay_time": "", # ZPAY 通知未明确返回
|
||||
"trade_status": status,
|
||||
}
|
||||
|
||||
def success_response(self) -> str:
|
||||
return "success"
|
||||
|
||||
def query_order_status(self, out_trade_no: str, timeout: int = 15) -> Dict[str, Any]:
|
||||
"""查询 ZPAY 订单支付状态(供 nomadvip/digital/cnomadcna/nomadlms 共用)"""
|
||||
url = f"https://zpayz.cn/api.php?act=order&pid={self.pid}&key={self.key}&out_trade_no={out_trade_no}"
|
||||
try:
|
||||
resp = requests.get(url, timeout=timeout)
|
||||
result = resp.json() if resp.ok else {}
|
||||
except Exception as e:
|
||||
return {"paid": False, "error": str(e)}
|
||||
try:
|
||||
code = result.get("code", 0)
|
||||
code = int(code) if isinstance(code, (int, float)) or str(code).isdigit() else 0
|
||||
except (ValueError, TypeError):
|
||||
code = 0
|
||||
if code != 1:
|
||||
return {"paid": False, "msg": result.get("msg", "查询失败")}
|
||||
try:
|
||||
status = int(result.get("status", 0))
|
||||
except (ValueError, TypeError):
|
||||
status = 0
|
||||
return {"paid": status == 1, "status": status}
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
项目路由模块
|
||||
- nomadvip: /nomadvip/*
|
||||
- digital: /digital/*
|
||||
- cnomadcna: /cnomadcna/*
|
||||
- nomadlms: /nomadlms/*
|
||||
- legacy: /payh5, /zpay_notify 等(向后兼容 digital/cnomadcna)
|
||||
"""
|
||||
from .nomadvip import router as nomadvip_router
|
||||
from .digital import router as digital_router
|
||||
from .cnomadcna import router as cnomadcna_router
|
||||
from .nomadlms import router as nomadlms_router
|
||||
from .legacy import router as legacy_router
|
||||
from .common import router as common_router
|
||||
from .meetup import router as meetup_router
|
||||
|
||||
__all__ = [
|
||||
"nomadvip_router",
|
||||
"digital_router",
|
||||
"cnomadcna_router",
|
||||
"nomadlms_router",
|
||||
"legacy_router",
|
||||
"common_router",
|
||||
"meetup_router",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,304 +0,0 @@
|
||||
"""
|
||||
digital/cnomadcna/nomadlms 共享的支付逻辑
|
||||
根据 prefix 生成对应 notify_path,业务互不影响
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
|
||||
from ..config import BASE_URL
|
||||
from ..payment import (
|
||||
get_payment_provider,
|
||||
get_payment_provider_by_name,
|
||||
resolve_provider,
|
||||
resolve_channel,
|
||||
)
|
||||
from ..payment.zpay import ZPayProvider
|
||||
from ..services import handle_payment_success, processed_orders
|
||||
|
||||
|
||||
def _safe_log(level: int, message: str, *args, **kwargs) -> None:
|
||||
try:
|
||||
logging.log(level, message, *args, **kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def create_digital_router(prefix: str, project_name: str, site_id: str | None = None) -> APIRouter:
|
||||
"""
|
||||
创建 digital/cnomadcna/nomadlms 的支付路由
|
||||
prefix: 如 "/digital", "/cnomadcna", "/nomadlms" 或 ""(legacy)
|
||||
project_name: 用于日志,如 "digital", "cnomadcna", "nomadlms", "legacy"
|
||||
site_id: 站点标识,用于 site_vip(digital/meetup/vip)
|
||||
"""
|
||||
router = APIRouter(prefix=prefix, tags=[project_name])
|
||||
|
||||
def _notify_path(path: str) -> str:
|
||||
base = BASE_URL.rstrip("/")
|
||||
if prefix:
|
||||
return f"{base}{prefix}{path}"
|
||||
return f"{base}{path}"
|
||||
|
||||
@router.post("/payh5")
|
||||
async def payh5(item: dict, request: Request):
|
||||
"""创建 H5 支付订单。默认微信支付,PC/手机用 zpay,微信内用 xorpay,支付宝暂不提供"""
|
||||
req_provider = (item.get("provider") or "").strip().lower() or None
|
||||
device = (item.get("device") or "pc").lower()
|
||||
ua = (request.headers.get("user-agent") or "").strip()
|
||||
provider = resolve_provider(device, req_provider, ua)
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = (item.get("type") or "unknown").lower()
|
||||
_safe_log(
|
||||
logging.INFO,
|
||||
"[payjsapi] %s/payh5 user_id=%s type=%s provider=%s channel=%s",
|
||||
project_name,
|
||||
user_id,
|
||||
pay_type,
|
||||
provider.name,
|
||||
channel,
|
||||
)
|
||||
try:
|
||||
total_fee = int(item.get("total_fee", 80))
|
||||
except (TypeError, ValueError):
|
||||
total_fee = 80
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
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, "商品支付")
|
||||
notify_path = "/xorpay_notify" if provider.name == "xorpay" else "/zpay_notify"
|
||||
notify_url = _notify_path(notify_path)
|
||||
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"),
|
||||
)
|
||||
_safe_log(
|
||||
logging.INFO,
|
||||
"[payjsapi] %s/payh5 created order_id=%s provider=%s notify_url=%s return_url=%s",
|
||||
project_name,
|
||||
order_id,
|
||||
provider.name,
|
||||
notify_url,
|
||||
item.get("return_url") or "",
|
||||
)
|
||||
if provider.name == "xorpay":
|
||||
return {
|
||||
"status": "ok",
|
||||
"order_id": order_id,
|
||||
"xorpay_params": result.get("params", {}),
|
||||
"xorpay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
}
|
||||
params = result.get("params", {})
|
||||
params_str = {k: str(v) for k, v in params.items() if v is not None and str(v).strip() != ""}
|
||||
return {
|
||||
"status": "ok",
|
||||
"params": params_str,
|
||||
"pay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
"submit_method": "POST",
|
||||
}
|
||||
|
||||
@router.post("/payh5/redirect")
|
||||
async def payh5_redirect(item: dict, request: Request):
|
||||
"""ZPAY mapi 接口,返回 302 或二维码。PC/手机用 zpay,默认微信支付,支付宝暂不提供"""
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
_safe_log(
|
||||
logging.INFO,
|
||||
"[payjsapi] %s/payh5/redirect user_id=%s type=%s device=%s channel=%s",
|
||||
project_name,
|
||||
item.get("user_id", ""),
|
||||
item.get("type", ""),
|
||||
item.get("device", "pc"),
|
||||
channel,
|
||||
)
|
||||
try:
|
||||
total_fee = float(item.get("total_fee", 80))
|
||||
except (TypeError, ValueError):
|
||||
total_fee = 80.0
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = item.get("type", "unknown").lower()
|
||||
return_url = item.get("return_url")
|
||||
device = item.get("device", "pc")
|
||||
client_ip = item.get("client_ip") or (request.client.host if request.client else "127.0.0.1")
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
if forwarded:
|
||||
client_ip = forwarded.split(",")[0].strip()
|
||||
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, "商品支付")
|
||||
notify_url = _notify_path("/zpay_notify")
|
||||
result = provider.create_order_api(
|
||||
order_id=order_id,
|
||||
name=name,
|
||||
total_fee_yuan=total_fee_yuan,
|
||||
pay_type=channel,
|
||||
notify_url=notify_url,
|
||||
return_url=return_url,
|
||||
client_ip=client_ip,
|
||||
device=device,
|
||||
)
|
||||
if result.get("status") != "ok":
|
||||
raise HTTPException(status_code=500, detail=result.get("msg", "ZPAY mapi 创建订单失败"))
|
||||
_safe_log(
|
||||
logging.INFO,
|
||||
"[payjsapi] %s/payh5/redirect created order_id=%s device=%s channel=%s",
|
||||
project_name,
|
||||
order_id,
|
||||
device,
|
||||
channel,
|
||||
)
|
||||
payurl = result.get("payurl")
|
||||
payurl2 = result.get("payurl2")
|
||||
img = result.get("img")
|
||||
if device == "pc" and img:
|
||||
return JSONResponse(status_code=200, content={
|
||||
"status": "ok", "qrcode_mode": True, "order_id": order_id,
|
||||
"img": img, "payurl": payurl, "return_url": return_url, "channel": channel,
|
||||
})
|
||||
if device == "wechat" and channel.lower() in ("wxpay", "wx", "wechat"):
|
||||
return JSONResponse(status_code=200, content={"use_form": True, "order_id": order_id})
|
||||
headers = {"X-Pay-Order-Id": order_id}
|
||||
if device == "h5" and channel.lower() in ("wxpay", "wx", "wechat") and payurl2:
|
||||
return RedirectResponse(url=payurl2, status_code=302, headers=headers)
|
||||
if payurl:
|
||||
return RedirectResponse(url=payurl, status_code=302, headers=headers)
|
||||
raise HTTPException(status_code=500, detail="ZPAY 未返回支付链接")
|
||||
|
||||
@router.get("/zpay_order_status")
|
||||
async def zpay_order_status(out_trade_no: str):
|
||||
"""查询 ZPAY 订单支付状态"""
|
||||
provider = get_payment_provider()
|
||||
if not isinstance(provider, ZPayProvider):
|
||||
raise HTTPException(status_code=400, detail="仅 zpay 渠道支持")
|
||||
result = provider.query_order_status(out_trade_no, timeout=15)
|
||||
if result.get("error"):
|
||||
_safe_log(logging.WARNING, "%s ZPAY 订单查询失败: %s", project_name, result.get("error"))
|
||||
return result
|
||||
|
||||
@router.get("/order_status")
|
||||
async def order_status(order_id: str = "", out_trade_no: str = ""):
|
||||
order_id = (order_id or out_trade_no or "").strip()
|
||||
if not order_id:
|
||||
return {"paid": False, "error": "missing order_id"}
|
||||
|
||||
try:
|
||||
from ..services.pb import get_pb_client
|
||||
|
||||
pb = get_pb_client()
|
||||
records = pb.collection("payments").get_list(
|
||||
1, 1, {"filter": f'order_id = "{order_id}"'}
|
||||
)
|
||||
if records.items:
|
||||
return {"paid": True, "source": "pocketbase"}
|
||||
except Exception as error:
|
||||
_safe_log(logging.WARNING, "%s order_status PocketBase failed: %s", project_name, error)
|
||||
|
||||
try:
|
||||
xorpay = get_payment_provider_by_name("xorpay")
|
||||
xorpay_result = xorpay.query_order_status(order_id, timeout=8)
|
||||
if xorpay_result.get("paid"):
|
||||
return {
|
||||
"paid": True,
|
||||
"source": "xorpay",
|
||||
"status": xorpay_result.get("status", ""),
|
||||
}
|
||||
except Exception as error:
|
||||
_safe_log(logging.WARNING, "%s order_status XorPay failed: %s", project_name, error)
|
||||
|
||||
try:
|
||||
zpay = get_payment_provider_by_name("zpay")
|
||||
if isinstance(zpay, ZPayProvider):
|
||||
zpay_result = zpay.query_order_status(order_id, timeout=8)
|
||||
if zpay_result.get("paid"):
|
||||
return {
|
||||
"paid": True,
|
||||
"source": "zpay",
|
||||
"status": zpay_result.get("status", ""),
|
||||
}
|
||||
except Exception as error:
|
||||
_safe_log(logging.WARNING, "%s order_status ZPAY failed: %s", project_name, error)
|
||||
|
||||
return {"paid": False}
|
||||
|
||||
@router.post("/xorpay_notify")
|
||||
async def xorpay_notify(request: Request):
|
||||
"""XorPay 异步通知"""
|
||||
form_data = await request.form()
|
||||
data = {k: v for k, v in form_data.items()}
|
||||
_safe_log(logging.INFO, "%s XorPay 异步通知: %s", project_name, data)
|
||||
order_id = data.get("order_id", "")
|
||||
if order_id in processed_orders:
|
||||
return "ok"
|
||||
provider = get_payment_provider_by_name("xorpay")
|
||||
if not provider.verify_notify(data):
|
||||
_safe_log(logging.ERROR, "%s XorPay 验签失败: %s", project_name, 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", "")),
|
||||
f"{project_name}-xorpay",
|
||||
use_memos=False,
|
||||
site_id=site_id,
|
||||
)
|
||||
return provider.success_response()
|
||||
|
||||
@router.get("/zpay_notify")
|
||||
@router.post("/zpay_notify")
|
||||
async def zpay_notify(request: Request):
|
||||
"""ZPAY 异步通知"""
|
||||
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()}
|
||||
_safe_log(logging.INFO, "%s ZPAY 异步通知: %s", project_name, data)
|
||||
order_id = data.get("out_trade_no", "")
|
||||
if order_id in processed_orders:
|
||||
return "success"
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
if not provider.verify_notify(data):
|
||||
_safe_log(logging.ERROR, "%s ZPAY 验签失败: %s", project_name, 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", "")),
|
||||
f"{project_name}-zpay",
|
||||
use_memos=False,
|
||||
site_id=site_id,
|
||||
)
|
||||
return provider.success_response()
|
||||
|
||||
return router
|
||||
@@ -1,4 +0,0 @@
|
||||
"""cnomadcna 项目支付接口:/cnomadcna/*"""
|
||||
from ._digital_base import create_digital_router
|
||||
|
||||
router = create_digital_router(prefix="/cnomadcna", project_name="cnomadcna", site_id="meetup")
|
||||
@@ -1,65 +0,0 @@
|
||||
"""共享接口:meetup 申请、create_user 等"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..services import get_pb_client
|
||||
from ..sdk.memos import MemosSDK
|
||||
from ..config import MEMOS_API, MEMOS_TOKEN
|
||||
|
||||
router = APIRouter(tags=["common"])
|
||||
|
||||
|
||||
@router.post("/submit_meetup_application")
|
||||
async def submit_meetup_application(item: dict):
|
||||
"""数字游民社区报名表单"""
|
||||
logging.info("submit_meetup_application item: %s", 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:
|
||||
logging.error("meetup 申请提交失败: %s", e)
|
||||
logging.error(f"meetup 申请提交失败 - user_id: {user_id}, error: {e}")
|
||||
raise HTTPException(status_code=500, detail="submit failed")
|
||||
|
||||
|
||||
class CreateUserRequest(BaseModel):
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
@router.post("/create_user")
|
||||
async def create_user(item: CreateUserRequest):
|
||||
"""创建 Memos 用户(基于 MemosSDK)"""
|
||||
sdk = MemosSDK(api_url=MEMOS_API, token=MEMOS_TOKEN, enabled=bool(MEMOS_API and MEMOS_TOKEN))
|
||||
if not sdk.enabled:
|
||||
return {"error": "Memos 未配置"}
|
||||
timenew = str(int(time.time()))
|
||||
username = item.username or f"user{timenew}"
|
||||
password = item.password or "12345678"
|
||||
result = sdk.create_user_by_username(username=username, password=password)
|
||||
logging.info("memos_result- %s", result)
|
||||
return result
|
||||
@@ -1,4 +0,0 @@
|
||||
"""digital 项目支付接口:/digital/*"""
|
||||
from ._digital_base import create_digital_router
|
||||
|
||||
router = create_digital_router(prefix="/digital", project_name="digital", site_id="digital")
|
||||
@@ -1,4 +0,0 @@
|
||||
"""向后兼容:/payh5、/zpay_notify 等(digital/cnomadcna/nomadlms 旧版调用)"""
|
||||
from ._digital_base import create_digital_router
|
||||
|
||||
router = create_digital_router(prefix="", project_name="legacy")
|
||||
@@ -1,328 +0,0 @@
|
||||
"""
|
||||
meetup 加入流程:check-user、ensure-user、complete-order
|
||||
供 cnomadcna 等前端调用,当 /api/* 代理到 payjsapi 时使用
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..config import PB_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD
|
||||
|
||||
router = APIRouter(prefix="/api/meetup", tags=["meetup"])
|
||||
DEFAULT_PASSWORD = "12345678" # PocketBase 默认要求至少 8 位
|
||||
|
||||
# Admin token 缓存,避免每次请求都向 PocketBase 认证(PB 远程时易超时)
|
||||
_ADMIN_TOKEN_CACHE: tuple[str, float] | None = None
|
||||
_TOKEN_CACHE_TTL = 300 # 5 分钟
|
||||
|
||||
|
||||
def _get_admin_token() -> tuple[str | None, str | None]:
|
||||
"""返回 (token, error_detail)。token 为 None 时 error_detail 为失败原因。带 5 分钟缓存。"""
|
||||
global _ADMIN_TOKEN_CACHE
|
||||
now = time.time()
|
||||
if _ADMIN_TOKEN_CACHE and _ADMIN_TOKEN_CACHE[1] > now:
|
||||
return _ADMIN_TOKEN_CACHE[0], None
|
||||
|
||||
if not PB_ADMIN_EMAIL or not PB_ADMIN_PASSWORD:
|
||||
return None, "PB_ADMIN_EMAIL 或 PB_ADMIN_PASSWORD 未配置"
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
# PocketBase v0.23+ 使用 _superusers,旧版用 admins
|
||||
for path in ["/api/collections/_superusers/auth-with-password", "/api/admins/auth-with-password"]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{base}{path}",
|
||||
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||
timeout=4,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
token = data.get("token")
|
||||
if token:
|
||||
_ADMIN_TOKEN_CACHE = (token, now + _TOKEN_CACHE_TTL)
|
||||
return token, None
|
||||
if r.status_code == 404:
|
||||
continue # 尝试下一个路径
|
||||
try:
|
||||
msg = r.json().get("message", r.text[:100])
|
||||
except Exception:
|
||||
msg = r.text[:100]
|
||||
logging.warning(f"PocketBase admin auth failed: status={r.status_code}, msg={msg}")
|
||||
return None, f"PocketBase 管理员认证失败: {msg}"
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logging.error(f"PocketBase 连接失败: {e}")
|
||||
return None, f"无法连接 {PB_URL},请检查网络或 PB_URL"
|
||||
except Exception as e:
|
||||
logging.warning(f"PocketBase auth try {path}: {e}")
|
||||
return None, "PocketBase 管理员认证失败: 请确认 PB_URL 及管理员账号密码正确"
|
||||
|
||||
|
||||
class CheckUserRequest(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class EnsureUserRequest(BaseModel):
|
||||
email: str
|
||||
password: str | None = None
|
||||
|
||||
|
||||
def _check_site_vip(user_id: str, site_id: str = "meetup", token: str | None = None) -> bool:
|
||||
"""检查用户是否有有效 site_vip(未过期)。可传入 token 避免重复认证。"""
|
||||
if not token:
|
||||
token, _ = _get_admin_token()
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
r = requests.get(
|
||||
f"{PB_URL}/api/collections/site_vip/records",
|
||||
params={
|
||||
"filter": f'user_id = "{user_id}" && site_id = "{site_id}"',
|
||||
"perPage": 1,
|
||||
"sort": "-expires_at",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=4,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return False
|
||||
data = r.json()
|
||||
items = data.get("items") or []
|
||||
if not items:
|
||||
return False
|
||||
rec = items[0]
|
||||
exp = rec.get("expires_at")
|
||||
if not exp:
|
||||
return True
|
||||
try:
|
||||
from datetime import datetime as _dt, timezone
|
||||
|
||||
exp_dt = _dt.fromisoformat(exp.replace("Z", "+00:00"))
|
||||
now = _dt.now(timezone.utc)
|
||||
if exp_dt.tzinfo is None:
|
||||
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
|
||||
return exp_dt > now
|
||||
except Exception:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/check-user")
|
||||
async def check_user(item: CheckUserRequest):
|
||||
"""检查邮箱是否已在 users 表存在,以及 VIP 状态(新/老用户判断)"""
|
||||
email = (item.email or "").strip()
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="请输入邮箱")
|
||||
|
||||
token, err = _get_admin_token()
|
||||
if not token:
|
||||
raise HTTPException(status_code=500, detail=f"服务配置错误:{err}")
|
||||
|
||||
try:
|
||||
filter_val = f'email="{email}"'
|
||||
r = requests.get(
|
||||
f"{PB_URL}/api/collections/users/records",
|
||||
params={"filter": filter_val, "perPage": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=4,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise HTTPException(status_code=500, detail="查询失败")
|
||||
data = r.json()
|
||||
items = data.get("items") or []
|
||||
exists = len(items) > 0
|
||||
user_id = items[0].get("id") if items else None
|
||||
vip = _check_site_vip(user_id, "meetup", token) if user_id else False
|
||||
return {"ok": True, "exists": exists, "vip": vip, "user_id": user_id}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"check-user error: {e}")
|
||||
raise HTTPException(status_code=500, detail="操作失败")
|
||||
|
||||
|
||||
@router.post("/ensure-user")
|
||||
async def ensure_user(item: EnsureUserRequest):
|
||||
"""确保用户存在:老用户验证密码,新用户用默认密码 12345678 创建"""
|
||||
email = (item.email or "").strip()
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="请输入邮箱")
|
||||
|
||||
password = (item.password or "").strip() or DEFAULT_PASSWORD
|
||||
|
||||
try:
|
||||
# 尝试用密码登录
|
||||
login_r = requests.post(
|
||||
f"{PB_URL}/api/collections/users/auth-with-password",
|
||||
json={"identity": email, "password": password},
|
||||
timeout=10,
|
||||
)
|
||||
if login_r.status_code == 200:
|
||||
data = login_r.json()
|
||||
return {
|
||||
"ok": True,
|
||||
"user_id": data.get("record", {}).get("id"),
|
||||
"token": data.get("token"),
|
||||
"record": data.get("record"),
|
||||
"is_new": False,
|
||||
}
|
||||
|
||||
try:
|
||||
err_data = login_r.json()
|
||||
except Exception:
|
||||
err_data = {}
|
||||
is_not_found = login_r.status_code == 400 and (
|
||||
"Invalid" in str(err_data.get("message", ""))
|
||||
or "identity" in str(err_data.get("message", "")).lower()
|
||||
)
|
||||
if not is_not_found and item.password:
|
||||
raise HTTPException(status_code=400, detail="密码错误")
|
||||
|
||||
# 新用户:创建
|
||||
token, err = _get_admin_token()
|
||||
if not token:
|
||||
raise HTTPException(status_code=500, detail=f"服务配置错误:{err}")
|
||||
|
||||
create_r = requests.post(
|
||||
f"{PB_URL}/api/collections/users/records",
|
||||
json={
|
||||
"email": email,
|
||||
"password": DEFAULT_PASSWORD,
|
||||
"passwordConfirm": DEFAULT_PASSWORD,
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if create_r.status_code != 200:
|
||||
try:
|
||||
create_err = create_r.json()
|
||||
except Exception:
|
||||
create_err = {}
|
||||
data = create_err.get("data", {})
|
||||
if (
|
||||
create_r.status_code == 400
|
||||
and "already" in str(data.get("email", {}).get("message", "")).lower()
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="该邮箱已注册,请输入密码登录")
|
||||
msg = create_err.get("message", "创建账号失败")
|
||||
logging.warning(f"ensure-user create failed: status={create_r.status_code}, msg={msg}, data={data}")
|
||||
raise HTTPException(status_code=500, detail=msg)
|
||||
|
||||
# 登录新用户
|
||||
final_r = requests.post(
|
||||
f"{PB_URL}/api/collections/users/auth-with-password",
|
||||
json={"identity": email, "password": DEFAULT_PASSWORD},
|
||||
timeout=10,
|
||||
)
|
||||
if final_r.status_code != 200:
|
||||
raise HTTPException(status_code=500, detail="登录失败")
|
||||
|
||||
auth_data = final_r.json()
|
||||
return {
|
||||
"ok": True,
|
||||
"user_id": auth_data.get("record", {}).get("id"),
|
||||
"token": auth_data.get("token"),
|
||||
"record": auth_data.get("record"),
|
||||
"is_new": True,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"ensure-user error: {e}")
|
||||
raise HTTPException(status_code=500, detail="操作失败")
|
||||
|
||||
|
||||
def _decode_pending_email(user_id: str) -> str | None:
|
||||
if not user_id.startswith("pending_"):
|
||||
return None
|
||||
try:
|
||||
return bytes.fromhex(user_id[8:]).decode("utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _query_zpay_paid(order_id: str) -> bool:
|
||||
"""查询 ZPAY 订单是否已支付"""
|
||||
try:
|
||||
from ..payment import get_payment_provider
|
||||
from ..payment.zpay import ZPayProvider
|
||||
provider = get_payment_provider()
|
||||
if not isinstance(provider, ZPayProvider):
|
||||
return False
|
||||
result = provider.query_order_status(order_id, timeout=10)
|
||||
return bool(result.get("paid"))
|
||||
except Exception as e:
|
||||
logging.warning(f"complete-order zpay query failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/complete-order")
|
||||
async def complete_order(item: dict):
|
||||
"""
|
||||
支付成功后完成订单:验证订单、为新用户同步 session。
|
||||
支持 pending_ 订单(新用户)和已存在用户订单(user_id 为 PB id)。
|
||||
异步通知可能晚于用户回跳,故在 site_vip 未找到时轮询重试(ZPAY 已确认支付时)。
|
||||
"""
|
||||
order_id = (item.get("order_id") or "").strip()
|
||||
if not order_id:
|
||||
raise HTTPException(status_code=400, detail="缺少 order_id")
|
||||
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
token, err = _get_admin_token()
|
||||
if not token:
|
||||
raise HTTPException(status_code=500, detail=f"服务配置错误:{err}")
|
||||
|
||||
from ..services.payment import parse_order_id
|
||||
|
||||
user_id, pay_type = parse_order_id(order_id)
|
||||
if not user_id or pay_type != "meetup":
|
||||
raise HTTPException(status_code=400, detail="订单格式无效")
|
||||
|
||||
# 验证订单:site_vip 中需存在该 order_id(异步通知可能晚于用户回跳,ZPAY 已支付时轮询重试)
|
||||
max_retries = 4
|
||||
for attempt in range(max_retries):
|
||||
r = requests.get(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
params={"filter": f'order_id = "{order_id}"', "perPage": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
total = (r.json().get("totalItems") or 0) if r.status_code == 200 else 0
|
||||
if total > 0:
|
||||
break
|
||||
if attempt < max_retries - 1 and _query_zpay_paid(order_id):
|
||||
time.sleep(2)
|
||||
continue
|
||||
resp_preview = r.text[:200] if r.ok else f"status={r.status_code}"
|
||||
logging.warning(f"complete-order 未找到订单: order_id={order_id}, attempt={attempt + 1}, pb_resp={resp_preview}")
|
||||
raise HTTPException(status_code=400, detail="订单不存在或未支付成功")
|
||||
|
||||
# pending_ 新用户:用邮箱+默认密码登录返回 token
|
||||
if user_id.startswith("pending_"):
|
||||
email = _decode_pending_email(user_id)
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="订单格式无效")
|
||||
login_r = requests.post(
|
||||
f"{base}/api/collections/users/auth-with-password",
|
||||
json={"identity": email, "password": DEFAULT_PASSWORD},
|
||||
timeout=10,
|
||||
)
|
||||
if login_r.status_code != 200:
|
||||
raise HTTPException(status_code=500, detail="登录失败,请稍后重试")
|
||||
auth_data = login_r.json()
|
||||
return {
|
||||
"ok": True,
|
||||
"token": auth_data.get("token"),
|
||||
"record": auth_data.get("record"),
|
||||
"is_new": True,
|
||||
}
|
||||
|
||||
# 已存在用户(PB user_id):订单已验证,返回 ok,前端已有 session 则无需 token
|
||||
return {"ok": True, "token": None, "record": None, "is_new": False}
|
||||
@@ -1,4 +0,0 @@
|
||||
"""nomadlms 项目支付接口:/nomadlms/*"""
|
||||
from ._digital_base import create_digital_router
|
||||
|
||||
router = create_digital_router(prefix="/nomadlms", project_name="nomadlms")
|
||||
@@ -1,883 +0,0 @@
|
||||
"""nomadvip 项目支付接口:/nomadvip/*,支持 xorpay / zpay。"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
|
||||
from ..config import BASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, PB_URL
|
||||
from ..payment import (
|
||||
get_payment_provider,
|
||||
get_payment_provider_by_name,
|
||||
resolve_provider,
|
||||
resolve_channel,
|
||||
)
|
||||
from ..payment.zpay import ZPayProvider
|
||||
from ..services import handle_payment_success, processed_orders
|
||||
|
||||
router = APIRouter(prefix="/nomadvip", tags=["nomadvip"])
|
||||
|
||||
|
||||
def _get_notify_path(provider_name: str) -> str:
|
||||
if provider_name == "xorpay":
|
||||
return f"{BASE_URL.rstrip('/')}/nomadvip/xorpay_notify"
|
||||
return f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify"
|
||||
|
||||
|
||||
@router.post("/payh5")
|
||||
async def nomadvip_payh5(item: dict, request: Request):
|
||||
"""创建支付订单。微信内用 xorpay,PC/手机用 zpay,默认微信支付,支付宝暂不提供"""
|
||||
req_provider = (item.get("provider") or "").strip().lower() or None
|
||||
device = (item.get("device") or "pc").lower()
|
||||
ua = (request.headers.get("user-agent") or "").strip()
|
||||
provider = resolve_provider(device, req_provider, ua)
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
logging.info(
|
||||
"[payjsapi] payh5 user_id=%s type=%s provider=%s channel=%s",
|
||||
item.get("user_id", ""),
|
||||
item.get("type", ""),
|
||||
provider.name,
|
||||
channel,
|
||||
)
|
||||
|
||||
total_fee = int(item.get("total_fee", 880))
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = (item.get("type") or "vip").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, "VIP会员充值")
|
||||
notify_url = _get_notify_path(provider.name)
|
||||
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"),
|
||||
)
|
||||
logging.info(
|
||||
"[payjsapi] payh5 created order_id=%s provider=%s notify_url=%s return_url=%s",
|
||||
order_id,
|
||||
provider.name,
|
||||
notify_url,
|
||||
item.get("return_url") or "",
|
||||
)
|
||||
|
||||
if provider.name == "xorpay":
|
||||
return {
|
||||
"status": "ok",
|
||||
"order_id": order_id,
|
||||
"xorpay_params": result.get("params", {}),
|
||||
"xorpay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
}
|
||||
|
||||
params = result.get("params", {})
|
||||
params_str = {
|
||||
k: str(v) for k, v in params.items() if v is not None and str(v).strip() != ""
|
||||
}
|
||||
return {
|
||||
"status": "ok",
|
||||
"params": params_str,
|
||||
"pay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
"submit_method": "POST",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/payh5/redirect")
|
||||
async def nomadvip_payh5_redirect(item: dict, request: Request):
|
||||
"""ZPAY mapi 接口,PC/手机用 zpay,默认微信支付,支付宝暂不提供"""
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
|
||||
logging.info("nomadvip payh5/redirect request: item=%s", item)
|
||||
try:
|
||||
total_fee = float(item.get("total_fee", 880))
|
||||
except (TypeError, ValueError):
|
||||
logging.warning(
|
||||
"nomadvip payh5/redirect invalid total_fee=%s", item.get("total_fee")
|
||||
)
|
||||
total_fee = 880.0
|
||||
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = (item.get("type") or "vip").lower()
|
||||
return_url = item.get("return_url")
|
||||
device = item.get("device", "pc")
|
||||
client_ip = item.get("client_ip") or (
|
||||
request.client.host if request.client else "127.0.0.1"
|
||||
)
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
if forwarded:
|
||||
client_ip = forwarded.split(",")[0].strip()
|
||||
|
||||
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}"
|
||||
|
||||
# 追加 order_id,供 paid 页在 cookie 丢失时恢复 user_id。
|
||||
if return_url and "order_id=" not in return_url and "out_trade_no=" not in return_url:
|
||||
sep = "&" if "?" in return_url else "?"
|
||||
return_url = f"{return_url}{sep}order_id={order_id}"
|
||||
|
||||
type_map = {
|
||||
"book": "购买书籍",
|
||||
"meetup": "数字游民社区报名",
|
||||
"video": "视频解锁",
|
||||
"vip": "VIP会员充值",
|
||||
}
|
||||
name = type_map.get(pay_type, "VIP会员充值")
|
||||
notify_url = f"{BASE_URL.rstrip('/')}/nomadvip/zpay_notify"
|
||||
|
||||
result = provider.create_order_api(
|
||||
order_id=order_id,
|
||||
name=name,
|
||||
total_fee_yuan=total_fee_yuan,
|
||||
pay_type=channel,
|
||||
notify_url=notify_url,
|
||||
return_url=return_url,
|
||||
client_ip=client_ip,
|
||||
device=device,
|
||||
)
|
||||
if result.get("status") != "ok":
|
||||
err_msg = result.get("msg", "ZPAY mapi 创建订单失败")
|
||||
logging.error(
|
||||
"nomadvip payh5/redirect ZPAY create failed: %s result=%s",
|
||||
err_msg,
|
||||
result,
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=err_msg)
|
||||
|
||||
payurl = result.get("payurl")
|
||||
payurl2 = result.get("payurl2")
|
||||
img = result.get("img")
|
||||
if device == "pc" and img:
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
content={
|
||||
"status": "ok",
|
||||
"qrcode_mode": True,
|
||||
"order_id": order_id,
|
||||
"img": img,
|
||||
"payurl": payurl,
|
||||
"return_url": return_url,
|
||||
"channel": channel,
|
||||
},
|
||||
)
|
||||
if device == "wechat" and channel.lower() in ("wxpay", "wx", "wechat"):
|
||||
return JSONResponse(
|
||||
status_code=200, content={"use_form": True, "order_id": order_id}
|
||||
)
|
||||
|
||||
headers = {"X-Pay-Order-Id": order_id}
|
||||
if device == "h5" and channel.lower() in ("wxpay", "wx", "wechat") and payurl2:
|
||||
return RedirectResponse(url=payurl2, status_code=302, headers=headers)
|
||||
if payurl:
|
||||
return RedirectResponse(url=payurl, status_code=302, headers=headers)
|
||||
|
||||
logging.error(
|
||||
"nomadvip payh5/redirect ZPAY missing pay url device=%s channel=%s keys=%s",
|
||||
device,
|
||||
channel,
|
||||
list(result.keys()),
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="ZPAY 未返回支付链接")
|
||||
|
||||
|
||||
@router.get("/zpay_order_status")
|
||||
async def nomadvip_zpay_order_status(out_trade_no: str):
|
||||
"""查询 ZPAY 订单支付状态。"""
|
||||
provider = get_payment_provider()
|
||||
if not isinstance(provider, ZPayProvider):
|
||||
raise HTTPException(status_code=400, detail="zpay_order_status 仅支持 zpay")
|
||||
result = provider.query_order_status(out_trade_no, timeout=15)
|
||||
if result.get("error"):
|
||||
logging.warning(
|
||||
"nomadvip ZPAY order status failed: %s", result.get("error")
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _get_linked_email_for_user(pb, user_id: str) -> str | None:
|
||||
"""按 user_id 查询 vip_email_link 关联邮箱。"""
|
||||
try:
|
||||
link_rec = pb.collection("vip_email_link").get_list(
|
||||
1, 1, {"filter": f'anonymous_user_id = "{user_id}"'}
|
||||
)
|
||||
if link_rec.items:
|
||||
return link_rec.items[0].get("email") or ""
|
||||
|
||||
if user_id and user_id.startswith("user") and user_id[4:].isdigit():
|
||||
link_rec = pb.collection("vip_email_link").get_list(
|
||||
1, 1, {"filter": f'user_id = "{user_id}"'}
|
||||
)
|
||||
if link_rec.items:
|
||||
return link_rec.items[0].get("email") or ""
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _uid_has_vip(pb, uid: str, site_id: str = "vip") -> bool:
|
||||
if not uid:
|
||||
return False
|
||||
|
||||
sv = pb.collection("site_vip").get_list(
|
||||
1, 1, {"filter": f'user_id = "{uid}" && site_id = "{site_id}"'}
|
||||
)
|
||||
if sv.items:
|
||||
rec = sv.items[0]
|
||||
expires = rec.get("expires_at") or ""
|
||||
if not expires:
|
||||
return True
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
exp_dt = datetime.fromisoformat(expires.replace("Z", "+00:00"))
|
||||
now = datetime.now(exp_dt.tzinfo) if exp_dt.tzinfo else datetime.now()
|
||||
return exp_dt.timestamp() >= now.timestamp()
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
pm = pb.collection("payments").get_list(
|
||||
1,
|
||||
1,
|
||||
{"filter": f'user_id = "{uid}" && type = "vip"', "sort": "-created"},
|
||||
)
|
||||
return bool(pm.items)
|
||||
|
||||
|
||||
@router.get("/check_vip")
|
||||
async def nomadvip_check_vip(user_id: str = ""):
|
||||
"""Return vip status for a user_id."""
|
||||
user_id = (user_id or "").strip()
|
||||
logging.info("[payjsapi] check_vip user_id=%s", user_id or "(empty)")
|
||||
if not user_id:
|
||||
return {"vip": False, "error": "missing user_id"}
|
||||
|
||||
try:
|
||||
from ..services.pb import get_pb_client
|
||||
|
||||
pb = get_pb_client()
|
||||
if not _uid_has_vip(pb, user_id):
|
||||
logging.info("[payjsapi] check_vip miss user_id=%s vip=False", user_id)
|
||||
return {"vip": False}
|
||||
|
||||
email = _get_linked_email_for_user(pb, user_id)
|
||||
if not email:
|
||||
admin_token = _get_pb_admin_token()
|
||||
if admin_token and user_id.startswith("user") and user_id[4:].isdigit():
|
||||
linked_member = _pb_find_linked_member_from_order(admin_token, user_id)
|
||||
if linked_member:
|
||||
linked_user_id, linked_email = linked_member
|
||||
email = linked_email
|
||||
_pb_upsert_email_link(admin_token, linked_email, linked_user_id, user_id)
|
||||
|
||||
logging.info(
|
||||
"[payjsapi] check_vip hit user_id=%s vip=True email=%s",
|
||||
user_id,
|
||||
email or "(none)",
|
||||
)
|
||||
return {"vip": True, "email": email}
|
||||
except Exception as error:
|
||||
logging.warning("[payjsapi] check_vip failed: %s", error)
|
||||
return {"vip": False, "error": str(error)}
|
||||
|
||||
def _get_pb_admin_token() -> str | None:
|
||||
"""获取 PocketBase 管理员 token。"""
|
||||
if not PB_ADMIN_EMAIL or not PB_ADMIN_PASSWORD:
|
||||
return None
|
||||
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
for path in [
|
||||
"/api/collections/_superusers/auth-with-password",
|
||||
"/api/admins/auth-with-password",
|
||||
]:
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{base}{path}",
|
||||
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json().get("token")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _pb_escape(value: str) -> str:
|
||||
return str(value).replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def _pb_list_email_links(admin_token: str, email: str) -> list[dict]:
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
headers = {"Authorization": f"Bearer {admin_token}"}
|
||||
|
||||
for candidate in [email, email.lower()]:
|
||||
safe_email = _pb_escape(candidate)
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/vip_email_link/records",
|
||||
params={"filter": f'email = "{safe_email}"', "perPage": 1},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
continue
|
||||
|
||||
items = (response.json() or {}).get("items") or []
|
||||
if items:
|
||||
return items
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _pb_upsert_email_link(
|
||||
admin_token: str,
|
||||
email: str,
|
||||
pb_user_id: str,
|
||||
anonymous_user_id: str | None = None,
|
||||
):
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
headers = {
|
||||
"Authorization": f"Bearer {admin_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
items = _pb_list_email_links(admin_token, email)
|
||||
payload = {"email": email.lower(), "user_id": pb_user_id}
|
||||
if anonymous_user_id:
|
||||
payload["anonymous_user_id"] = anonymous_user_id
|
||||
|
||||
if items:
|
||||
record_id = items[0].get("id")
|
||||
if record_id:
|
||||
requests.patch(
|
||||
f"{base}/api/collections/vip_email_link/records/{record_id}",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
return
|
||||
|
||||
requests.post(
|
||||
f"{base}/api/collections/vip_email_link/records",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
def _pb_move_site_vip(
|
||||
admin_token: str,
|
||||
from_user_id: str,
|
||||
to_user_id: str,
|
||||
site_id: str = "vip",
|
||||
):
|
||||
if not from_user_id or not to_user_id or from_user_id == to_user_id:
|
||||
return
|
||||
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
safe_from_user_id = _pb_escape(from_user_id)
|
||||
safe_site_id = _pb_escape(site_id)
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
params={
|
||||
"filter": f'user_id = "{safe_from_user_id}" && site_id = "{safe_site_id}"',
|
||||
"perPage": 500,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {admin_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
items = (response.json() or {}).get("items") or []
|
||||
for item in items:
|
||||
record_id = item.get("id")
|
||||
if not record_id:
|
||||
continue
|
||||
requests.patch(
|
||||
f"{base}/api/collections/site_vip/records/{record_id}",
|
||||
json={"user_id": to_user_id},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
def _pb_latest_vip_payment(admin_token: str, user_id: str) -> dict | None:
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
safe_user_id = _pb_escape(user_id)
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/payments/records",
|
||||
params={
|
||||
"filter": f'user_id = "{safe_user_id}" && type = "vip"',
|
||||
"perPage": 1,
|
||||
"sort": "-created",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
items = (response.json() or {}).get("items") or []
|
||||
return items[0] if items else None
|
||||
|
||||
|
||||
def _pb_list_site_vip_by_order(
|
||||
admin_token: str,
|
||||
order_id: str,
|
||||
site_id: str = "vip",
|
||||
) -> list[dict]:
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
safe_order_id = _pb_escape(order_id)
|
||||
safe_site_id = _pb_escape(site_id)
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
params={
|
||||
"filter": f'order_id = "{safe_order_id}" && site_id = "{safe_site_id}"',
|
||||
"perPage": 500,
|
||||
"sort": "-created",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return []
|
||||
|
||||
return (response.json() or {}).get("items") or []
|
||||
|
||||
|
||||
def _pb_get_user_email(admin_token: str, user_id: str) -> str | None:
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/users/records/{user_id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
email = (response.json() or {}).get("email") or ""
|
||||
return email.strip().lower() or None
|
||||
|
||||
|
||||
def _pb_find_linked_member_from_order(
|
||||
admin_token: str,
|
||||
anonymous_user_id: str,
|
||||
) -> tuple[str, str] | None:
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
safe_user_id = _pb_escape(anonymous_user_id)
|
||||
response = requests.get(
|
||||
f"{base}/api/collections/payments/records",
|
||||
params={
|
||||
"filter": f'user_id = "{safe_user_id}" && type = "vip"',
|
||||
"perPage": 20,
|
||||
"sort": "-created",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
items = (response.json() or {}).get("items") or []
|
||||
seen_orders = set()
|
||||
for item in items:
|
||||
order_id = (item.get("order_id") or "").strip()
|
||||
if not order_id or order_id in seen_orders:
|
||||
continue
|
||||
seen_orders.add(order_id)
|
||||
|
||||
for site_vip in _pb_list_site_vip_by_order(admin_token, order_id):
|
||||
linked_user_id = (site_vip.get("user_id") or "").strip()
|
||||
if (
|
||||
not linked_user_id
|
||||
or linked_user_id == anonymous_user_id
|
||||
or (linked_user_id.startswith("user") and linked_user_id[4:].isdigit())
|
||||
):
|
||||
continue
|
||||
|
||||
email = _pb_get_user_email(admin_token, linked_user_id)
|
||||
if email:
|
||||
return linked_user_id, email
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _pb_ensure_site_vip(
|
||||
admin_token: str,
|
||||
from_user_id: str,
|
||||
to_user_id: str,
|
||||
site_id: str = "vip",
|
||||
):
|
||||
if not from_user_id or not to_user_id or from_user_id == to_user_id:
|
||||
return
|
||||
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
safe_from_user_id = _pb_escape(from_user_id)
|
||||
safe_to_user_id = _pb_escape(to_user_id)
|
||||
safe_site_id = _pb_escape(site_id)
|
||||
source_records = requests.get(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
params={
|
||||
"filter": f'user_id = "{safe_from_user_id}" && site_id = "{safe_site_id}"',
|
||||
"perPage": 500,
|
||||
"sort": "-created",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
to_records = requests.get(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
params={
|
||||
"filter": f'user_id = "{safe_to_user_id}" && site_id = "{safe_site_id}"',
|
||||
"perPage": 500,
|
||||
"sort": "-created",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
payment = _pb_latest_vip_payment(admin_token, from_user_id)
|
||||
order_id = (payment or {}).get("order_id") or ""
|
||||
order_records = (
|
||||
_pb_list_site_vip_by_order(admin_token, order_id, site_id=site_id)
|
||||
if order_id
|
||||
else []
|
||||
)
|
||||
source_items = (source_records.json() or {}).get("items") or [] if source_records.status_code == 200 else []
|
||||
target_items = (to_records.json() or {}).get("items") or [] if to_records.status_code == 200 else []
|
||||
canonical = (target_items or source_items or order_records or [None])[0]
|
||||
headers = {
|
||||
"Authorization": f"Bearer {admin_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
if canonical and canonical.get("id"):
|
||||
patch_body = {"user_id": to_user_id, "site_id": site_id}
|
||||
next_order_id = order_id or (canonical.get("order_id") or "").strip()
|
||||
if next_order_id:
|
||||
patch_body["order_id"] = next_order_id
|
||||
requests.patch(
|
||||
f"{base}/api/collections/site_vip/records/{canonical['id']}",
|
||||
json=patch_body,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
duplicate_ids = set()
|
||||
for record in [*target_items, *source_items, *order_records]:
|
||||
record_id = record.get("id")
|
||||
if record_id and record_id != canonical["id"]:
|
||||
duplicate_ids.add(record_id)
|
||||
for record_id in duplicate_ids:
|
||||
requests.delete(
|
||||
f"{base}/api/collections/site_vip/records/{record_id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
return
|
||||
|
||||
if not order_id:
|
||||
return
|
||||
|
||||
requests.post(
|
||||
f"{base}/api/collections/site_vip/records",
|
||||
json={"user_id": to_user_id, "site_id": site_id, "order_id": order_id},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/check_vip_by_email")
|
||||
async def nomadvip_check_vip_by_email(item: dict):
|
||||
"""Recover VIP by email/password, optionally with anonymous user_id."""
|
||||
email = (item.get("email") or "").strip().lower()
|
||||
password = (item.get("password") or "").strip()
|
||||
anonymous_user_id = (
|
||||
item.get("user_id") or item.get("anonymousUserId") or ""
|
||||
).strip()
|
||||
logging.info(
|
||||
"[payjsapi] check_vip_by_email email=%s user_id=%s",
|
||||
email or "(empty)",
|
||||
anonymous_user_id or "(empty)",
|
||||
)
|
||||
|
||||
if not email or not password:
|
||||
return {"vip": False, "error": "missing email or password"}
|
||||
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
login_res = requests.post(
|
||||
f"{base}/api/collections/users/auth-with-password",
|
||||
json={"identity": email, "password": password},
|
||||
timeout=10,
|
||||
)
|
||||
if login_res.status_code != 200:
|
||||
try:
|
||||
error = login_res.json() or {}
|
||||
except Exception:
|
||||
error = {}
|
||||
logging.info(
|
||||
"[payjsapi] check_vip_by_email login failed status=%s message=%s",
|
||||
login_res.status_code,
|
||||
error.get("message", ""),
|
||||
)
|
||||
return {
|
||||
"vip": False,
|
||||
"error": error.get("message") or "invalid email or password",
|
||||
}
|
||||
|
||||
login_data = login_res.json() or {}
|
||||
token = login_data.get("token")
|
||||
record = login_data.get("record") or {}
|
||||
pb_user_id = (record.get("id") or "").strip()
|
||||
if not token or not pb_user_id:
|
||||
return {"vip": False, "error": "login failed"}
|
||||
|
||||
admin_token = _get_pb_admin_token()
|
||||
if not admin_token:
|
||||
return {"vip": False, "error": "PocketBase admin auth failed"}
|
||||
|
||||
try:
|
||||
from ..services.pb import get_pb_client
|
||||
|
||||
pb = get_pb_client()
|
||||
matched_user_id = None
|
||||
recovered_anonymous_user_id = (
|
||||
anonymous_user_id
|
||||
if anonymous_user_id.startswith("user") and anonymous_user_id[4:].isdigit()
|
||||
else None
|
||||
)
|
||||
recovery_source = None
|
||||
|
||||
if recovered_anonymous_user_id and _uid_has_vip(pb, recovered_anonymous_user_id):
|
||||
matched_user_id = recovered_anonymous_user_id
|
||||
recovery_source = "request_user_id"
|
||||
else:
|
||||
items = _pb_list_email_links(admin_token, email)
|
||||
logging.info(
|
||||
"[payjsapi] check_vip_by_email vip_email_link email=%s count=%s",
|
||||
email,
|
||||
len(items),
|
||||
)
|
||||
for link_record in items:
|
||||
linked_ids = []
|
||||
linked_pb_user_id = (link_record.get("user_id") or "").strip()
|
||||
linked_anonymous_user_id = (
|
||||
link_record.get("anonymous_user_id") or ""
|
||||
).strip()
|
||||
if linked_pb_user_id:
|
||||
linked_ids.append(linked_pb_user_id)
|
||||
if linked_anonymous_user_id and linked_anonymous_user_id not in linked_ids:
|
||||
linked_ids.append(linked_anonymous_user_id)
|
||||
|
||||
for linked_uid in linked_ids:
|
||||
if not _uid_has_vip(pb, linked_uid):
|
||||
continue
|
||||
matched_user_id = linked_uid
|
||||
if linked_anonymous_user_id.startswith("user") and linked_anonymous_user_id[4:].isdigit():
|
||||
recovered_anonymous_user_id = linked_anonymous_user_id
|
||||
elif linked_uid.startswith("user") and linked_uid[4:].isdigit():
|
||||
recovered_anonymous_user_id = linked_uid
|
||||
recovery_source = "vip_email_link"
|
||||
break
|
||||
|
||||
if matched_user_id:
|
||||
break
|
||||
|
||||
if not matched_user_id and _uid_has_vip(pb, pb_user_id):
|
||||
matched_user_id = pb_user_id
|
||||
recovered_anonymous_user_id = None
|
||||
recovery_source = "pb_user_id"
|
||||
|
||||
if not matched_user_id:
|
||||
logging.info("[payjsapi] check_vip_by_email no vip match for email=%s", email)
|
||||
return {
|
||||
"vip": False,
|
||||
"error": "no linked vip record found; provide the paid user_id if this is a historical anonymous order",
|
||||
}
|
||||
|
||||
if recovered_anonymous_user_id:
|
||||
_pb_ensure_site_vip(admin_token, recovered_anonymous_user_id, pb_user_id)
|
||||
_pb_upsert_email_link(
|
||||
admin_token,
|
||||
email,
|
||||
pb_user_id,
|
||||
recovered_anonymous_user_id,
|
||||
)
|
||||
else:
|
||||
_pb_upsert_email_link(admin_token, email, pb_user_id)
|
||||
|
||||
logging.info(
|
||||
"[payjsapi] check_vip_by_email recovered by %s matched_user_id=%s effectiveUserId=%s anonymousUserId=%s",
|
||||
recovery_source or "unknown",
|
||||
matched_user_id,
|
||||
pb_user_id,
|
||||
recovered_anonymous_user_id or "(none)",
|
||||
)
|
||||
return {
|
||||
"vip": True,
|
||||
"token": token,
|
||||
"record": record,
|
||||
"effectiveUserId": pb_user_id,
|
||||
"anonymousUserId": recovered_anonymous_user_id,
|
||||
}
|
||||
except Exception as error:
|
||||
logging.warning("[payjsapi] check_vip_by_email failed: %s", error)
|
||||
return {"vip": False, "error": str(error)}
|
||||
|
||||
@router.get("/order_status")
|
||||
async def nomadvip_order_status(order_id: str = "", out_trade_no: str = ""):
|
||||
"""查询订单支付状态,优先查 ZPAY,失败时回退 PocketBase。"""
|
||||
order_id = (order_id or out_trade_no or "").strip()
|
||||
logging.info("[payjsapi] order_status order_id=%s", order_id or "(空)")
|
||||
if not order_id:
|
||||
return {"paid": False, "error": "缺少 order_id 或 out_trade_no"}
|
||||
|
||||
try:
|
||||
from ..services.pb import get_pb_client
|
||||
|
||||
pb = get_pb_client()
|
||||
records = pb.collection("payments").get_list(
|
||||
1, 1, {"filter": f'order_id = "{order_id}"'}
|
||||
)
|
||||
paid = len(records.items) > 0
|
||||
logging.info("[payjsapi] order_status PocketBase paid=%s", paid)
|
||||
if paid:
|
||||
return {"paid": True, "source": "pocketbase"}
|
||||
except Exception as error:
|
||||
logging.warning("[payjsapi] order_status PocketBase failed: %s", error)
|
||||
|
||||
xorpay = get_payment_provider_by_name("xorpay")
|
||||
xorpay_result = xorpay.query_order_status(order_id, timeout=8)
|
||||
if xorpay_result.get("error"):
|
||||
logging.warning(
|
||||
"[payjsapi] order_status XorPay query failed order_id=%s error=%s",
|
||||
order_id,
|
||||
xorpay_result.get("error"),
|
||||
)
|
||||
else:
|
||||
logging.info(
|
||||
"[payjsapi] order_status XorPay status=%s paid=%s",
|
||||
xorpay_result.get("status", ""),
|
||||
xorpay_result.get("paid", False),
|
||||
)
|
||||
if xorpay_result.get("paid"):
|
||||
return {
|
||||
"paid": True,
|
||||
"source": "xorpay",
|
||||
"status": xorpay_result.get("status"),
|
||||
"pay_price": xorpay_result.get("pay_price") or "",
|
||||
}
|
||||
|
||||
zpay = get_payment_provider_by_name("zpay")
|
||||
if isinstance(zpay, ZPayProvider):
|
||||
result = zpay.query_order_status(order_id, timeout=8)
|
||||
if not result.get("error") and result.get("paid"):
|
||||
logging.info("[payjsapi] order_status ZPAY paid=True")
|
||||
return result
|
||||
|
||||
return {"paid": False}
|
||||
|
||||
|
||||
@router.post("/xorpay_notify")
|
||||
async def nomadvip_xorpay_notify(request: Request):
|
||||
"""XorPay 异步通知。"""
|
||||
form_data = await request.form()
|
||||
data = {k: v for k, v in form_data.items()}
|
||||
logging.info("nomadvip XorPay notify: %s", data)
|
||||
order_id = data.get("order_id", "")
|
||||
if order_id in processed_orders:
|
||||
logging.info("nomadvip order already processed, skip: %s", order_id)
|
||||
return "ok"
|
||||
|
||||
provider = get_payment_provider_by_name("xorpay")
|
||||
if not provider.verify_notify(data):
|
||||
logging.error("nomadvip XorPay verify failed: %s", data)
|
||||
raise HTTPException(status_code=400, detail="sign error")
|
||||
|
||||
parsed = provider.parse_notify(data)
|
||||
if parsed.get("trade_status") != "success":
|
||||
return "ok"
|
||||
|
||||
pay_price = parsed.get("pay_price", data.get("pay_price", ""))
|
||||
try:
|
||||
handle_payment_success(
|
||||
order_id,
|
||||
pay_price,
|
||||
"nomadvip-xorpay",
|
||||
use_memos=True,
|
||||
site_id="vip",
|
||||
)
|
||||
logging.info("nomadvip business done: order_id=%s", order_id)
|
||||
except Exception as error:
|
||||
logging.error(
|
||||
"nomadvip PocketBase/Memos failed: %s", error, exc_info=True
|
||||
)
|
||||
raise
|
||||
return provider.success_response()
|
||||
|
||||
|
||||
@router.get("/zpay_notify")
|
||||
@router.post("/zpay_notify")
|
||||
async def nomadvip_zpay_notify(request: Request):
|
||||
"""ZPAY 异步通知。"""
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
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("nomadvip ZPAY notify: %s", data)
|
||||
order_id = data.get("out_trade_no", "")
|
||||
if order_id in processed_orders:
|
||||
logging.info("nomadvip order already processed, skip: %s", order_id)
|
||||
return "success"
|
||||
|
||||
if not provider.verify_notify(data):
|
||||
logging.error("nomadvip ZPAY verify failed: %s", data)
|
||||
raise HTTPException(status_code=400, detail="sign error")
|
||||
|
||||
parsed = provider.parse_notify(data)
|
||||
trade_status = parsed.get("trade_status") or data.get("trade_status", "")
|
||||
if trade_status not in ("success", "TRADE_SUCCESS"):
|
||||
logging.info("nomadvip non-success trade status, skip: %s", trade_status)
|
||||
return "success"
|
||||
|
||||
pay_price = parsed.get("pay_price") or data.get("money", "")
|
||||
try:
|
||||
handle_payment_success(
|
||||
order_id,
|
||||
pay_price,
|
||||
"nomadvip-zpay",
|
||||
use_memos=True,
|
||||
site_id="vip",
|
||||
)
|
||||
logging.info("nomadvip business done: order_id=%s", order_id)
|
||||
except Exception as error:
|
||||
logging.error(
|
||||
"nomadvip PocketBase/Memos failed: %s", error, exc_info=True
|
||||
)
|
||||
raise
|
||||
return provider.success_response()
|
||||
@@ -1,274 +0,0 @@
|
||||
"""wxH5 项目支付接口:/wxh5/*,支持 xorpay/zpay,支付成功后同步 Supabase wxgzh_vip_users"""
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
|
||||
from ..config import BASE_URL
|
||||
from ..payment import (
|
||||
get_payment_provider,
|
||||
get_payment_provider_by_name,
|
||||
resolve_provider,
|
||||
resolve_channel,
|
||||
)
|
||||
from ..payment.zpay import ZPayProvider
|
||||
from ..services import handle_payment_success, processed_orders
|
||||
|
||||
router = APIRouter(prefix="/wxh5", tags=["wxh5"])
|
||||
|
||||
# 套餐金额(分)与有效期映射
|
||||
PLAN_MAP = {
|
||||
"month": (2900, 30), # 月卡 29 元,30 天
|
||||
"year": (19900, 365), # 年卡 199 元,365 天
|
||||
"lifetime": (39900, 36500), # 终身 399 元,100 年
|
||||
}
|
||||
|
||||
|
||||
def _infer_plan_from_amount(amount_cents: int) -> str:
|
||||
"""根据支付金额(分)推断套餐"""
|
||||
if amount_cents >= 39900:
|
||||
return "lifetime"
|
||||
if amount_cents >= 19900:
|
||||
return "year"
|
||||
return "month"
|
||||
|
||||
|
||||
def _get_notify_path(provider_name: str) -> str:
|
||||
base = BASE_URL.rstrip("/")
|
||||
return f"{base}/wxh5/xorpay_notify" if provider_name == "xorpay" else f"{base}/wxh5/zpay_notify"
|
||||
|
||||
|
||||
def _sync_wxh5_vip_to_supabase(user_id: str, plan: str):
|
||||
"""支付成功后同步 VIP 到 Supabase wxgzh_vip_users"""
|
||||
try:
|
||||
from supabase import create_client
|
||||
from ..config import SUPABASE_URL, SUPABASE_SERVICE_KEY
|
||||
url = SUPABASE_URL
|
||||
key = SUPABASE_SERVICE_KEY
|
||||
if not url or not key:
|
||||
logging.warning("wxh5: SUPABASE_URL/SUPABASE_SERVICE_KEY 未配置,跳过 Supabase 同步")
|
||||
return
|
||||
days = PLAN_MAP.get(plan, PLAN_MAP["year"])[1]
|
||||
expire_at = (datetime.utcnow() + timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
client = create_client(url, key)
|
||||
# upsert: 存在则更新 expire_at,不存在则插入
|
||||
client.table("wxgzh_vip_users").upsert(
|
||||
{"user_id": user_id, "expire_at": expire_at},
|
||||
on_conflict="user_id",
|
||||
).execute()
|
||||
logging.info(f"wxh5 Supabase VIP 同步成功: user_id={user_id}, plan={plan}")
|
||||
except Exception as e:
|
||||
logging.error(f"wxh5 Supabase 同步失败: {e}", exc_info=True)
|
||||
|
||||
|
||||
@router.post("/payh5")
|
||||
async def wxh5_payh5(item: dict, request: Request):
|
||||
"""wxH5 专用:创建支付订单。微信内用 xorpay,PC/手机用 zpay,默认微信支付,支付宝暂不提供"""
|
||||
req_provider = (item.get("provider") or "").strip().lower() or None
|
||||
device = (item.get("device") or "pc").lower()
|
||||
ua = (request.headers.get("user-agent") or "").strip()
|
||||
provider = resolve_provider(device, req_provider, ua)
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
plan = (item.get("plan") or "year").lower()
|
||||
total_fee = PLAN_MAP.get(plan, PLAN_MAP["year"])[0]
|
||||
total_fee = int(item.get("total_fee", total_fee))
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = (item.get("type") or "vip").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}"
|
||||
return_url = item.get("return_url", "")
|
||||
if return_url and "order_id=" not in return_url:
|
||||
sep = "&" if "?" in return_url else "?"
|
||||
return_url = f"{return_url}{sep}order_id={order_id}"
|
||||
type_map = {"book": "购买书籍", "meetup": "社区报名", "video": "视频解锁", "vip": "VIP会员充值"}
|
||||
name = type_map.get(pay_type, "VIP会员充值")
|
||||
notify_url = _get_notify_path(provider.name)
|
||||
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=return_url,
|
||||
client_ip=item.get("client_ip"),
|
||||
)
|
||||
if provider.name == "xorpay":
|
||||
return {
|
||||
"status": "ok",
|
||||
"xorpay_params": result.get("params", {}),
|
||||
"xorpay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
"order_id": order_id,
|
||||
}
|
||||
params = result.get("params", {})
|
||||
params_str = {k: str(v) for k, v in params.items() if v is not None and str(v).strip() != ""}
|
||||
return {
|
||||
"status": "ok",
|
||||
"params": params_str,
|
||||
"pay_url": result.get("pay_url", ""),
|
||||
"provider": provider.name,
|
||||
"submit_method": "POST",
|
||||
"order_id": order_id,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/payh5/redirect")
|
||||
async def wxh5_payh5_redirect(item: dict, request: Request):
|
||||
"""wxH5 专用:ZPAY mapi 接口,PC/手机用 zpay,默认微信支付,支付宝暂不提供"""
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
channel = resolve_channel(item.get("channel"))
|
||||
plan = (item.get("plan") or "year").lower()
|
||||
total_fee = PLAN_MAP.get(plan, PLAN_MAP["year"])[0]
|
||||
try:
|
||||
total_fee = int(item.get("total_fee", total_fee))
|
||||
except (TypeError, ValueError):
|
||||
total_fee = PLAN_MAP["year"][0]
|
||||
total_fee_yuan = f"{total_fee / 100:.2f}"
|
||||
user_id = item.get("user_id", "unknown")
|
||||
pay_type = (item.get("type") or "vip").lower()
|
||||
device = item.get("device", "pc")
|
||||
client_ip = item.get("client_ip") or (request.client.host if request.client else "127.0.0.1")
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
if forwarded:
|
||||
client_ip = forwarded.split(",")[0].strip()
|
||||
random_suffix = "".join(random.choices(string.hexdigits.lower(), k=8))
|
||||
order_id = f"{user_id}_{pay_type}_order_{int(time.time())}_{random_suffix}"
|
||||
return_url_raw = item.get("return_url", "")
|
||||
if return_url_raw and "order_id=" not in return_url_raw:
|
||||
sep = "&" if "?" in return_url_raw else "?"
|
||||
return_url = f"{return_url_raw}{sep}order_id={order_id}"
|
||||
else:
|
||||
return_url = return_url_raw
|
||||
type_map = {"book": "购买书籍", "meetup": "社区报名", "video": "视频解锁", "vip": "VIP会员充值"}
|
||||
name = type_map.get(pay_type, "VIP会员充值")
|
||||
notify_url = f"{BASE_URL.rstrip('/')}/wxh5/zpay_notify"
|
||||
result = provider.create_order_api(
|
||||
order_id=order_id,
|
||||
name=name,
|
||||
total_fee_yuan=total_fee_yuan,
|
||||
pay_type=channel,
|
||||
notify_url=notify_url,
|
||||
return_url=return_url,
|
||||
client_ip=client_ip,
|
||||
device=device,
|
||||
)
|
||||
if result.get("status") != "ok":
|
||||
err_msg = result.get("msg", "ZPAY mapi 创建订单失败")
|
||||
raise HTTPException(status_code=500, detail=err_msg)
|
||||
payurl = result.get("payurl")
|
||||
payurl2 = result.get("payurl2")
|
||||
img = result.get("img")
|
||||
if device == "pc" and img:
|
||||
return JSONResponse(status_code=200, content={
|
||||
"status": "ok", "qrcode_mode": True, "order_id": order_id,
|
||||
"img": img, "payurl": payurl, "return_url": return_url, "channel": channel,
|
||||
})
|
||||
if device == "wechat" and channel.lower() in ("wxpay", "wx", "wechat"):
|
||||
return JSONResponse(status_code=200, content={"use_form": True, "order_id": order_id})
|
||||
headers = {"X-Pay-Order-Id": order_id}
|
||||
prefer_json = str(item.get("prefer_json", "")).lower() in ("1", "true", "yes")
|
||||
if prefer_json:
|
||||
target = payurl2 if (device == "h5" and channel.lower() in ("wxpay", "wx", "wechat")) else payurl
|
||||
if target:
|
||||
return JSONResponse(status_code=200, content={
|
||||
"status": "ok", "redirect_url": target, "order_id": order_id, "provider": "zpay",
|
||||
})
|
||||
if device == "h5" and channel.lower() in ("wxpay", "wx", "wechat") and payurl2:
|
||||
return RedirectResponse(url=payurl2, status_code=302, headers=headers)
|
||||
if payurl:
|
||||
return RedirectResponse(url=payurl, status_code=302, headers=headers)
|
||||
raise HTTPException(status_code=500, detail="ZPAY 未返回支付链接")
|
||||
|
||||
|
||||
def _parse_wxh5_user_id(order_id: str) -> str:
|
||||
"""从 order_id 解析 user_id,格式: user_id_vip_order_ts"""
|
||||
if "_order_" not in order_id:
|
||||
return ""
|
||||
prefix = order_id.split("_order_")[0]
|
||||
if "_" in prefix:
|
||||
return prefix.rsplit("_", 1)[0]
|
||||
return prefix
|
||||
|
||||
|
||||
def _handle_wxh5_payment_success(order_id: str, pay_price: str, provider_name: str):
|
||||
"""wxH5 支付成功:PocketBase + Supabase"""
|
||||
user_id = _parse_wxh5_user_id(order_id)
|
||||
if not user_id:
|
||||
return
|
||||
handle_payment_success(order_id, pay_price, provider_name, use_memos=False)
|
||||
amount_cents = int(float(pay_price or 0) * 100)
|
||||
plan = _infer_plan_from_amount(amount_cents)
|
||||
_sync_wxh5_vip_to_supabase(user_id, plan)
|
||||
|
||||
|
||||
@router.get("/order_status")
|
||||
async def wxh5_order_status(order_id: str):
|
||||
"""wxH5 通用:查询订单支付状态"""
|
||||
zpay = get_payment_provider_by_name("zpay")
|
||||
if isinstance(zpay, ZPayProvider):
|
||||
result = zpay.query_order_status(order_id, timeout=15)
|
||||
if not result.get("error") and result.get("paid"):
|
||||
return result
|
||||
try:
|
||||
from ..services.pb import get_pb_client
|
||||
pb = get_pb_client()
|
||||
records = pb.collection("payments").get_list(1, 1, {"filter": f'order_id = "{order_id}"'})
|
||||
return {"paid": len(records.items) > 0}
|
||||
except Exception as e:
|
||||
logging.warning(f"wxh5 PocketBase 查询失败: {e}")
|
||||
return {"paid": False, "error": str(e)}
|
||||
|
||||
|
||||
@router.post("/xorpay_notify")
|
||||
async def wxh5_xorpay_notify(request: Request):
|
||||
"""wxH5 XorPay 异步通知"""
|
||||
form_data = await request.form()
|
||||
data = {k: v for k, v in form_data.items()}
|
||||
order_id = data.get("order_id", "")
|
||||
if order_id in processed_orders:
|
||||
return "ok"
|
||||
provider = get_payment_provider_by_name("xorpay")
|
||||
if not provider.verify_notify(data):
|
||||
raise HTTPException(status_code=400, detail="sign error")
|
||||
parsed = provider.parse_notify(data)
|
||||
if parsed.get("trade_status") != "success":
|
||||
return "ok"
|
||||
try:
|
||||
_handle_wxh5_payment_success(order_id, parsed.get("pay_price", ""), "wxh5-xorpay")
|
||||
except Exception as e:
|
||||
logging.error(f"wxh5 业务处理异常: {e}", exc_info=True)
|
||||
raise
|
||||
return provider.success_response()
|
||||
|
||||
|
||||
@router.get("/zpay_notify")
|
||||
@router.post("/zpay_notify")
|
||||
async def wxh5_zpay_notify(request: Request):
|
||||
"""wxH5 ZPAY 异步通知"""
|
||||
provider = get_payment_provider_by_name("zpay")
|
||||
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()}
|
||||
order_id = data.get("out_trade_no", "")
|
||||
if order_id in processed_orders:
|
||||
return "success"
|
||||
if not provider.verify_notify(data):
|
||||
raise HTTPException(status_code=400, detail="sign error")
|
||||
parsed = provider.parse_notify(data)
|
||||
trade_status = parsed.get("trade_status") or data.get("trade_status", "")
|
||||
if trade_status not in ("success", "TRADE_SUCCESS"):
|
||||
return "success"
|
||||
try:
|
||||
_handle_wxh5_payment_success(order_id, parsed.get("pay_price") or data.get("money", ""), "wxh5-zpay")
|
||||
except Exception as e:
|
||||
logging.error(f"wxh5 业务处理异常: {e}", exc_info=True)
|
||||
raise
|
||||
return provider.success_response()
|
||||
@@ -1,20 +0,0 @@
|
||||
"""
|
||||
PayJS API SDK 模块
|
||||
- payment: 支付网关(ZPAY/XorPay)
|
||||
- pocketbase: PocketBase 数据库
|
||||
- minio: MinIO 对象存储
|
||||
- memos: Memos 用户(仅 nomadvip 使用)
|
||||
"""
|
||||
from .payment import get_payment_provider, ZPayProvider, XorPayProvider
|
||||
from .pocketbase import PocketBaseSDK
|
||||
from .minio import MinioSDK
|
||||
from .memos import MemosSDK
|
||||
|
||||
__all__ = [
|
||||
"get_payment_provider",
|
||||
"ZPayProvider",
|
||||
"XorPayProvider",
|
||||
"PocketBaseSDK",
|
||||
"MinioSDK",
|
||||
"MemosSDK",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,95 +0,0 @@
|
||||
"""
|
||||
Memos SDK
|
||||
用户创建等操作,仅 nomadvip 使用
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class MemosSDK:
|
||||
"""Memos 用户管理 SDK"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_url: str,
|
||||
token: str,
|
||||
enabled: bool = True,
|
||||
):
|
||||
self.api_url = api_url.rstrip("/")
|
||||
self.token = token
|
||||
self.enabled = enabled and bool(api_url and token)
|
||||
|
||||
def create_user(
|
||||
self,
|
||||
user_id: str,
|
||||
password: str = "12345678",
|
||||
) -> dict:
|
||||
"""
|
||||
创建 Memos 用户
|
||||
:param user_id: PocketBase 用户 ID,如 user1766043770158
|
||||
:return: {"success": bool, "data"?: dict, "error"?: str}
|
||||
"""
|
||||
if not self.enabled:
|
||||
return {"success": False, "error": "Memos 未启用"}
|
||||
|
||||
try:
|
||||
if user_id.startswith("user_"):
|
||||
cleaned = "user" + user_id.split("user_", 1)[1]
|
||||
elif "_" in user_id:
|
||||
cleaned = user_id.replace("_", "")
|
||||
else:
|
||||
cleaned = user_id
|
||||
|
||||
username = cleaned
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"role": "USER",
|
||||
"state": "NORMAL",
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
}
|
||||
resp = requests.post(
|
||||
self.api_url,
|
||||
json=data,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
if resp.status_code in (200, 201):
|
||||
logging.info(f"memos 用户创建成功: {username}")
|
||||
return {"success": True, "data": resp.json()}
|
||||
else:
|
||||
logging.error(f"memos 创建失败 - username: {username}, status: {resp.status_code}, response: {resp.text}")
|
||||
return {"success": False, "error": resp.text}
|
||||
except Exception as e:
|
||||
logging.error(f"创建 memos 用户异常 - user_id: {user_id}, error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def create_user_by_username(
|
||||
self,
|
||||
username: str,
|
||||
password: str = "12345678",
|
||||
) -> dict:
|
||||
"""按用户名直接创建(用于 /create_user 接口)"""
|
||||
if not self.enabled:
|
||||
return {"success": False, "error": "Memos 未启用"}
|
||||
try:
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"role": "USER",
|
||||
"state": "NORMAL",
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
}
|
||||
resp = requests.post(self.api_url, json=data, headers=headers, timeout=10)
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
logging.error(f"创建 memos 用户异常 - username: {username}, error: {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -1,92 +0,0 @@
|
||||
"""
|
||||
MinIO SDK
|
||||
对象存储封装,支持配置注入
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from minio import Minio
|
||||
MINIO_AVAILABLE = True
|
||||
except ImportError:
|
||||
MINIO_AVAILABLE = False
|
||||
Minio = None
|
||||
|
||||
|
||||
class MinioSDK:
|
||||
"""MinIO 对象存储 SDK"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
end_point: str,
|
||||
access_key: str,
|
||||
secret_key: str,
|
||||
bucket: str = "hackrobot",
|
||||
use_ssl: bool = True,
|
||||
port: Optional[int] = None,
|
||||
region: str = "china",
|
||||
public_url: Optional[str] = None,
|
||||
upload_prefix: str = "joins",
|
||||
):
|
||||
if not MINIO_AVAILABLE:
|
||||
raise ImportError("minio 未安装,请执行: pip install minio")
|
||||
self.end_point = end_point
|
||||
self.port = port or (443 if use_ssl else 80)
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.bucket = bucket
|
||||
self.use_ssl = use_ssl
|
||||
self.region = region
|
||||
self.public_url = public_url or f"https://{end_point}/{bucket}"
|
||||
self.upload_prefix = upload_prefix.rstrip("/")
|
||||
self._client: Optional[Minio] = None
|
||||
|
||||
def get_client(self) -> "Minio":
|
||||
"""获取 MinIO 客户端"""
|
||||
if self._client is None:
|
||||
endpoint = f"{self.end_point}:{self.port}" if self.port not in (80, 443) else self.end_point
|
||||
self._client = Minio(
|
||||
endpoint,
|
||||
access_key=self.access_key,
|
||||
secret_key=self.secret_key,
|
||||
secure=self.use_ssl,
|
||||
region=self.region,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def generate_object_key(self, ext: str) -> str:
|
||||
"""生成上传用的对象键"""
|
||||
ext = ext.lstrip(".")
|
||||
suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
return f"{self.upload_prefix}/{int(time.time() * 1000)}_{suffix}.{ext}"
|
||||
|
||||
def upload_buffer(
|
||||
self,
|
||||
data: bytes,
|
||||
object_key: str,
|
||||
content_type: str = "application/octet-stream",
|
||||
) -> dict:
|
||||
"""
|
||||
上传文件到 MinIO
|
||||
:return: {"url": str, "object_key": str}
|
||||
"""
|
||||
client = self.get_client()
|
||||
from io import BytesIO
|
||||
stream = BytesIO(data)
|
||||
client.put_object(
|
||||
self.bucket,
|
||||
object_key,
|
||||
stream,
|
||||
len(data),
|
||||
content_type=content_type,
|
||||
)
|
||||
url = f"{self.public_url.rstrip('/')}/{object_key}"
|
||||
return {"url": url, "object_key": object_key}
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""是否启用(需配置 access_key/secret_key)"""
|
||||
return bool(self.end_point and self.access_key and self.secret_key)
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
支付网关 SDK
|
||||
支持 ZPAY、XorPay,通过配置切换
|
||||
"""
|
||||
from app.payment.base import PaymentProvider
|
||||
from app.payment.zpay import ZPayProvider
|
||||
from app.payment.xorpay import XorPayProvider
|
||||
from app.payment.factory import get_payment_provider
|
||||
|
||||
__all__ = [
|
||||
"PaymentProvider",
|
||||
"ZPayProvider",
|
||||
"XorPayProvider",
|
||||
"get_payment_provider",
|
||||
]
|
||||
Binary file not shown.
@@ -1,69 +0,0 @@
|
||||
"""
|
||||
PocketBase SDK
|
||||
数据库操作封装,支持配置注入
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from pocketbase import PocketBase
|
||||
|
||||
|
||||
class PocketBaseSDK:
|
||||
"""PocketBase 客户端 SDK"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
admin_email: str,
|
||||
admin_password: str,
|
||||
):
|
||||
self.url = url
|
||||
self.admin_email = admin_email
|
||||
self.admin_password = admin_password
|
||||
self._client: Optional[PocketBase] = None
|
||||
|
||||
def get_client(self) -> PocketBase:
|
||||
"""获取已认证的 PocketBase 客户端"""
|
||||
if self._client is None:
|
||||
self._client = PocketBase(self.url)
|
||||
self._client.admins.auth_with_password(self.admin_email, self.admin_password)
|
||||
return self._client
|
||||
|
||||
def collection(self, name: str):
|
||||
"""获取集合"""
|
||||
return self.get_client().collection(name)
|
||||
|
||||
def create_payment(self, user_id: str, pay_type: str, order_id: str, amount: int) -> dict:
|
||||
"""创建支付记录"""
|
||||
return self.collection("payments").create({
|
||||
"user_id": user_id,
|
||||
"type": pay_type,
|
||||
"order_id": order_id,
|
||||
"amount": amount,
|
||||
})
|
||||
|
||||
def update_meetup_payment(
|
||||
self,
|
||||
user_id: str,
|
||||
order_id: str,
|
||||
amount: int,
|
||||
) -> None:
|
||||
"""更新或创建 meetup 支付记录"""
|
||||
solan = self.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,
|
||||
})
|
||||
else:
|
||||
solan.create({
|
||||
"user_id": user_id,
|
||||
"type": "meetup",
|
||||
"order_id": order_id,
|
||||
"amount": amount,
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
共享业务逻辑,基于 SDK 封装
|
||||
- PocketBase、Memos 通过 app.sdk 使用
|
||||
- 仅 nomadvip 涉及 Memos
|
||||
"""
|
||||
from .pb import get_pb_client
|
||||
from .memos import create_memos_user
|
||||
from .payment import handle_payment_success, parse_order_id, processed_orders
|
||||
|
||||
__all__ = [
|
||||
"get_pb_client",
|
||||
"create_memos_user",
|
||||
"handle_payment_success",
|
||||
"parse_order_id",
|
||||
"processed_orders",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
"""Memos 用户创建(基于 SDK,仅 nomadvip 使用)"""
|
||||
from app.config import MEMOS_API, MEMOS_TOKEN
|
||||
from app.sdk.memos import MemosSDK
|
||||
|
||||
_memos_sdk: MemosSDK | None = None
|
||||
|
||||
|
||||
def _get_memos_sdk() -> MemosSDK:
|
||||
global _memos_sdk
|
||||
if _memos_sdk is None:
|
||||
_memos_sdk = MemosSDK(
|
||||
api_url=MEMOS_API,
|
||||
token=MEMOS_TOKEN,
|
||||
enabled=bool(MEMOS_API and MEMOS_TOKEN),
|
||||
)
|
||||
return _memos_sdk
|
||||
|
||||
|
||||
def create_memos_user(user_id: str, password: str = "12345678"):
|
||||
"""创建 Memos 用户"""
|
||||
return _get_memos_sdk().create_user(user_id=user_id, password=password)
|
||||
@@ -1,51 +0,0 @@
|
||||
"""MinIO 对象存储(基于 SDK)"""
|
||||
from app.config import (
|
||||
MINIO_ENDPOINT,
|
||||
MINIO_PORT,
|
||||
MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY,
|
||||
MINIO_BUCKET,
|
||||
MINIO_USE_SSL,
|
||||
MINIO_PUBLIC_URL,
|
||||
MINIO_UPLOAD_PREFIX,
|
||||
)
|
||||
from app.sdk.minio import MinioSDK
|
||||
|
||||
_minio_sdk: MinioSDK | None = None
|
||||
|
||||
|
||||
def _get_minio_sdk() -> MinioSDK:
|
||||
global _minio_sdk
|
||||
if _minio_sdk is None:
|
||||
_minio_sdk = MinioSDK(
|
||||
end_point=MINIO_ENDPOINT,
|
||||
access_key=MINIO_ACCESS_KEY,
|
||||
secret_key=MINIO_SECRET_KEY,
|
||||
bucket=MINIO_BUCKET,
|
||||
use_ssl=MINIO_USE_SSL,
|
||||
port=MINIO_PORT,
|
||||
public_url=MINIO_PUBLIC_URL,
|
||||
upload_prefix=MINIO_UPLOAD_PREFIX,
|
||||
)
|
||||
return _minio_sdk
|
||||
|
||||
|
||||
def get_minio_client():
|
||||
"""获取 MinIO 客户端(未配置时返回 None)"""
|
||||
sdk = _get_minio_sdk()
|
||||
if not sdk.enabled:
|
||||
return None
|
||||
return sdk.get_client()
|
||||
|
||||
|
||||
def upload_buffer(data: bytes, object_key: str, content_type: str = "application/octet-stream"):
|
||||
"""上传文件到 MinIO(未配置时抛出异常)"""
|
||||
sdk = _get_minio_sdk()
|
||||
if not sdk.enabled:
|
||||
raise RuntimeError("MinIO 未配置,请设置 MINIO_ACCESS_KEY、MINIO_SECRET_KEY")
|
||||
return sdk.upload_buffer(data, object_key, content_type)
|
||||
|
||||
|
||||
def generate_object_key(ext: str) -> str:
|
||||
"""生成上传用的对象键"""
|
||||
return _get_minio_sdk().generate_object_key(ext)
|
||||
@@ -1,208 +0,0 @@
|
||||
"""支付成功后的统一业务处理(基于 SDK)"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from ..config import PB_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD
|
||||
from .pb import get_pb_client
|
||||
from .memos import create_memos_user
|
||||
|
||||
# 已处理订单(防重复)
|
||||
processed_orders: set[str] = set()
|
||||
DEFAULT_PASSWORD = "12345678"
|
||||
|
||||
|
||||
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 _decode_pending_email(user_id: str) -> str | None:
|
||||
"""从 pending_hex 格式解析邮箱"""
|
||||
if not user_id.startswith("pending_"):
|
||||
return None
|
||||
hex_part = user_id[8:] # 去掉 "pending_"
|
||||
try:
|
||||
return bytes.fromhex(hex_part).decode("utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _create_user_by_email(email: str) -> str | None:
|
||||
"""用管理员创建用户,返回 user_id。若已存在则返回已有用户 id"""
|
||||
token, _ = _get_admin_token()
|
||||
if not token:
|
||||
return None
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
r = requests.post(
|
||||
f"{base}/api/collections/users/records",
|
||||
json={
|
||||
"email": email,
|
||||
"password": DEFAULT_PASSWORD,
|
||||
"passwordConfirm": DEFAULT_PASSWORD,
|
||||
},
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("id")
|
||||
if r.status_code == 400:
|
||||
try:
|
||||
err = r.json()
|
||||
if "already" in str(err.get("data", {}).get("email", {}).get("message", "")).lower():
|
||||
# 用户已存在,查询获取 id
|
||||
get_r = requests.get(
|
||||
f"{base}/api/collections/users/records",
|
||||
params={"filter": f'email="{email}"', "perPage": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if get_r.status_code == 200:
|
||||
items = get_r.json().get("items", [])
|
||||
if items:
|
||||
return items[0].get("id")
|
||||
except Exception:
|
||||
pass
|
||||
logging.warning(f"create user failed: {r.status_code} {r.text[:200]}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_admin_token() -> tuple[str | None, str | None]:
|
||||
"""获取 PocketBase 管理员 token"""
|
||||
if not PB_ADMIN_EMAIL or not PB_ADMIN_PASSWORD:
|
||||
return None, "未配置"
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
for path in ["/api/collections/_superusers/auth-with-password", "/api/admins/auth-with-password"]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{base}{path}",
|
||||
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||
timeout=10,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("token"), None
|
||||
if r.status_code == 404:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None, "认证失败"
|
||||
|
||||
|
||||
def handle_payment_success(
|
||||
order_id: str,
|
||||
pay_price: str,
|
||||
provider_name: str,
|
||||
*,
|
||||
use_memos: bool = False,
|
||||
site_id: str | None = None,
|
||||
):
|
||||
"""
|
||||
支付成功后的统一业务处理
|
||||
:param use_memos: 是否创建 Memos 用户,仅 nomadvip 为 True
|
||||
:param site_id: 站点标识(digital/meetup/vip),用于写入 site_vip
|
||||
"""
|
||||
user_id, pay_type = parse_order_id(order_id)
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
# meetup 新用户:支付成功后才创建用户
|
||||
if pay_type == "meetup" and user_id.startswith("pending_"):
|
||||
email = _decode_pending_email(user_id)
|
||||
if email:
|
||||
real_id = _create_user_by_email(email)
|
||||
if real_id:
|
||||
user_id = real_id
|
||||
logging.info(f"meetup 支付成功,已创建用户: {email}")
|
||||
else:
|
||||
logging.error(f"meetup 支付成功但创建用户失败: {email}")
|
||||
return
|
||||
|
||||
logging.info("[payjsapi] 支付成功 provider=%s user_id=%s pay_type=%s site_id=%s order_id=%s",
|
||||
provider_name, user_id, pay_type, site_id, order_id)
|
||||
|
||||
if pay_type == "vip" and use_memos:
|
||||
create_memos_user(user_id, password="12345678")
|
||||
|
||||
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:
|
||||
existing = pb_client.collection("payments").get_list(1, 1, {
|
||||
"filter": f'order_id = "{order_id}"',
|
||||
})
|
||||
if not existing.items:
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"type": pay_type,
|
||||
"order_id": order_id,
|
||||
"amount": amount_cents,
|
||||
"total_fee": amount_cents,
|
||||
"channel": "wxpay",
|
||||
}
|
||||
pb_client.collection("payments").create(payload)
|
||||
|
||||
# 写入 site_vip(站点独立 VIP)。meetup 类型统一写 site_id=meetup
|
||||
effective_site_id = "meetup" if pay_type == "meetup" else site_id
|
||||
if effective_site_id:
|
||||
try:
|
||||
site_vip = pb_client.collection("site_vip")
|
||||
existing_vip = site_vip.get_list(1, 1, {
|
||||
"filter": f'user_id = "{user_id}" && site_id = "{effective_site_id}"',
|
||||
})
|
||||
if existing_vip.items:
|
||||
site_vip.update(existing_vip.items[0].id, {
|
||||
"order_id": order_id,
|
||||
"expires_at": "",
|
||||
})
|
||||
logging.info(f"site_vip 更新成功 - user_id: {user_id}, site_id: {effective_site_id}")
|
||||
else:
|
||||
site_vip.create({
|
||||
"user_id": user_id,
|
||||
"site_id": effective_site_id,
|
||||
"order_id": order_id,
|
||||
"expires_at": "",
|
||||
})
|
||||
logging.info(f"site_vip 创建成功 - user_id: {user_id}, site_id: {effective_site_id}")
|
||||
except Exception as e:
|
||||
logging.warning(f"site_vip 写入失败(不影响主流程): {e}")
|
||||
|
||||
processed_orders.add(order_id)
|
||||
except Exception as e:
|
||||
logging.error("PocketBase 操作失败 order_id=%s error=%s", order_id, e)
|
||||
raise
|
||||
@@ -1,27 +0,0 @@
|
||||
"""PocketBase 客户端(基于 SDK)"""
|
||||
import logging
|
||||
|
||||
from app.config import PB_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD
|
||||
from app.sdk.pocketbase import PocketBaseSDK
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_pb_sdk: PocketBaseSDK | None = None
|
||||
|
||||
|
||||
def _get_pb_sdk() -> PocketBaseSDK:
|
||||
global _pb_sdk
|
||||
if _pb_sdk is None:
|
||||
_pb_sdk = PocketBaseSDK(
|
||||
url=PB_URL,
|
||||
admin_email=PB_ADMIN_EMAIL,
|
||||
admin_password=PB_ADMIN_PASSWORD,
|
||||
)
|
||||
return _pb_sdk
|
||||
|
||||
|
||||
def get_pb_client():
|
||||
"""获取已认证的 PocketBase 客户端"""
|
||||
sdk = _get_pb_sdk()
|
||||
client = sdk.get_client()
|
||||
logger.info("PocketBase admin 登录成功")
|
||||
return client
|
||||
@@ -1,17 +0,0 @@
|
||||
2026-03-08 03:04:04,172 - ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-08 06:25:51,919 - ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-08 06:28:42,212 - ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-08 06:31:18,405 - ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-08 06:39:41,805 - ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-08 06:40:22,362 - ZPAY 땐데꿴璂呵겨: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||
2026-03-10 05:01:08,564 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773135567826', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'pc', 'client_ip': '192.168.41.222'}
|
||||
2026-03-10 05:02:23,523 - nomadvip ZPAY 땐데꿴璂呵겨: HTTPSConnectionPool(host='zpayz.cn', port=443): Read timed out. (read timeout=10)
|
||||
2026-03-10 05:02:46,901 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:03:26,325 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:04:54,606 - nomadvip ZPAY 땐데꿴璂呵겨: ('Connection aborted.', ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host', None, 10054, None))
|
||||
2026-03-10 05:06:39,639 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:07:03,035 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:07:37,981 - nomadvip ZPAY 땐데꿴璂呵겨: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
|
||||
2026-03-10 05:08:31,484 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:15:55,977 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773136912337', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://192.168.41.222:3000/paid', 'device': 'wechat', 'client_ip': '192.168.41.34'}
|
||||
2026-03-10 05:22:16,735 - nomadvip payh5/redirect 헝헹: item={'user_id': 'user1773131395284', 'total_fee': 18800, 'type': 'vip', 'channel': 'wxpay', 'return_url': 'http://localhost:3000/paid', 'device': 'pc', 'client_ip': '127.0.0.1'}
|
||||
7
requirements.txt
Executable file → Normal file
7
requirements.txt
Executable file → Normal file
@@ -1,9 +1,4 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
python-dotenv
|
||||
sqlalchemy
|
||||
pydantic
|
||||
python-multipart
|
||||
pocketbase
|
||||
requests
|
||||
minio
|
||||
pydantic
|
||||
@@ -1,4 +0,0 @@
|
||||
@echo off
|
||||
REM 强制无缓冲输出,确保日志立即显示在终端
|
||||
set PYTHONUNBUFFERED=1
|
||||
python -u run.py
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 强制无缓冲输出,确保日志立即显示在终端
|
||||
export PYTHONUNBUFFERED=1
|
||||
exec python -u run.py
|
||||
31
run.py
Executable file → Normal file
31
run.py
Executable file → Normal file
@@ -1,31 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("PYTHONUNBUFFERED", "1")
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(line_buffering=True)
|
||||
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", "8007"))
|
||||
reload_enabled = os.getenv("UVICORN_RELOAD", "0").strip().lower() in ("1", "true", "yes")
|
||||
try:
|
||||
sys.stderr.write(f"\n[payjsapi] startup http://0.0.0.0:{port}\n")
|
||||
sys.stderr.flush()
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
reload=reload_enabled,
|
||||
log_level="info",
|
||||
)
|
||||
|
||||
# uvicorn.run("app.main:app", host="0.0.0.0", port=8700, reload=True)
|
||||
uvicorn.run("app.main:app", host="127.0.0.1", port=8700, reload=True)
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
/usr/bin/python3
|
||||
@@ -1 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
lib
|
||||
@@ -1,5 +0,0 @@
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.12.3
|
||||
executable = /usr/bin/python3.12
|
||||
command = /usr/bin/python3 -m venv /www/wwwroot/payjsapi/venv
|
||||
@@ -1,343 +0,0 @@
|
||||
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
|
||||
2025-12-18 16:11:56,651 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '2c599bd51b364a6ca6938a9359ee905b', 'order_id': 'user1766044768982_vip_order_1766045500_fa88ad9e', 'detail': '{}', 'sign': 'ec59d2938230a0335284ed5678c44ed6', 'pay_time': '2025-12-18 16:11:55.527091', 'pay_price': '0.10', 'more': ''}
|
||||
2025-12-18 16:11:56,869 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766044768982
|
||||
2025-12-18 16:11:57,099 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:11:57,129 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:11:57,129 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766044768982, type: vip, order_id: user1766044768982_vip_order_1766045500_fa88ad9e
|
||||
2025-12-18 16:12:01,273 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '8e17ee8d8b1246a1b6bc7e46e861021c', 'order_id': 'user1766044768982_vip_order_1766045413_39b3cf6b', 'detail': '{}', 'sign': '920993c639746192fa75a0586a988cf3', 'pay_time': '2025-12-18 16:10:27', 'pay_price': '0.10', 'more': ''}
|
||||
2025-12-18 16:12:01,399 - memos 鍒涘缓澶辫触 - username: user1766044768982, status: 500, response: {"code":13,"message":"failed to create user: constraint failed: UNIQUE constraint failed: user.username (2067)","details":[]}
|
||||
2025-12-18 16:12:01,594 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:12:01,601 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:12:01,601 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766044768982, type: vip, order_id: user1766044768982_vip_order_1766045413_39b3cf6b
|
||||
2025-12-18 16:14:54,444 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '317faf5478a049908b2ea1aa4050c6e7', 'order_id': 'user1766045681042_vip_order_1766045683_ea6ca46a', 'detail': '{}', 'sign': '780d3c2f6e2ed277b3e2c562f343515e', 'pay_time': '2025-12-18 16:14:54.411898', 'pay_price': '0.10', 'more': ''}
|
||||
2025-12-18 16:14:54,611 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766045681042
|
||||
2025-12-18 16:14:55,672 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:14:55,674 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-18 16:14:55,675 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766045681042, type: vip, order_id: user1766045681042_vip_order_1766045683_ea6ca46a
|
||||
2025-12-18 18:12:18,576 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '5f62f32ad26a44b888b5d0e13706f46f', 'order_id': 'user1766046553880_vip_order_1766052718_4ab52abd', 'detail': '{}', 'sign': 'd4cb94c6a3ca4e96809d9cda85d7c20d', 'pay_time': '2025-12-18 18:12:18.408242', 'pay_price': '188.00', 'more': ''}
|
||||
2025-12-18 18:12:19,329 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766046553880
|
||||
2025-12-18 18:12:19,686 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-18 18:12:19,718 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-18 18:12:19,730 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766046553880, type: vip, order_id: user1766046553880_vip_order_1766052718_4ab52abd
|
||||
2025-12-19 18:40:53,356 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '057a4319f16d4036a3dda94ea65412ab', 'order_id': 'user1766140776331_vip_order_1766140804_d9eaf9bc', 'detail': '{}', 'sign': '085ab9d3c1ceb2e7e9cf17f54499c90e', 'pay_time': '2025-12-19 18:40:53.132699', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-19 18:40:54,204 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766140776331
|
||||
2025-12-19 18:40:54,602 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-19 18:40:54,643 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-19 18:40:54,659 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766140776331, type: vip, order_id: user1766140776331_vip_order_1766140804_d9eaf9bc
|
||||
2025-12-20 01:25:01,990 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '610aa1b9b7ab48f6a2793b07e4528868', 'order_id': 'user1766165011994_vip_order_1766165079_abeb6f2b', 'detail': '{}', 'sign': 'f82a03ea098007b9a93a12e02a5290a6', 'pay_time': '2025-12-20 01:25:01.789169', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-20 01:25:02,625 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766165011994
|
||||
2025-12-20 01:25:02,927 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-20 01:25:03,284 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-20 01:25:03,288 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766165011994, type: vip, order_id: user1766165011994_vip_order_1766165079_abeb6f2b
|
||||
2025-12-20 15:17:44,923 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '8eb32676f2fa4bf2a77c94d4409063e8', 'order_id': 'user1766213338235_vip_order_1766215049_2ba092ac', 'detail': '{}', 'sign': '15a15a8155619cf84ec6ee305e5b7c27', 'pay_time': '2025-12-20 15:17:44.685799', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-20 15:17:45,768 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766213338235
|
||||
2025-12-20 15:17:46,148 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-20 15:17:46,182 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-20 15:17:46,193 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766213338235, type: vip, order_id: user1766213338235_vip_order_1766215049_2ba092ac
|
||||
2025-12-20 16:26:09,306 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '5442ce403ae14e1dab141f909b4bb579', 'order_id': 'user1766046581515_vip_order_1766219155_ba76bab4', 'detail': '{}', 'sign': '7ca539655f764cac6392bd07fd4c1072', 'pay_time': '2025-12-20 16:26:09.136153', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-20 16:26:09,618 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766046581515
|
||||
2025-12-20 16:26:09,782 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-20 16:26:09,795 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-20 16:26:09,796 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766046581515, type: vip, order_id: user1766046581515_vip_order_1766219155_ba76bab4
|
||||
2025-12-20 17:54:32,400 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '343e5832e9654c2c82157e70880cb660', 'order_id': 'user1766224455635_vip_order_1766224460_cffae307', 'detail': '{}', 'sign': 'c04a150a905e88cbb75fe6992ee4d58d', 'pay_time': '2025-12-20 17:54:31.254708', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-20 17:54:33,178 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766224455635
|
||||
2025-12-20 17:54:33,554 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-20 17:54:33,562 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-20 17:54:33,562 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766224455635, type: vip, order_id: user1766224455635_vip_order_1766224460_cffae307
|
||||
2025-12-21 18:25:02,824 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '1fec5389b5d94c91b9f164c0b27f39bf', 'order_id': 'user1766296152138_vip_order_1766312684_25e8ea6c', 'detail': '{}', 'sign': '12e1e532adccd0b8fe7dbb87c10eabdf', 'pay_time': '2025-12-21 18:25:02.595185', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-21 18:25:03,850 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766296152138
|
||||
2025-12-21 18:25:04,215 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-21 18:25:04,255 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-21 18:25:04,270 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766296152138, type: vip, order_id: user1766296152138_vip_order_1766312684_25e8ea6c
|
||||
2025-12-21 19:16:25,590 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '8be679ab67ba40e6abbacbc745d7b2bd', 'order_id': 'user1766296261505_vip_order_1766315769_c0268b55', 'detail': '{}', 'sign': 'a9684bc176ae31afa4142bb8edb52b93', 'pay_time': '2025-12-21 19:16:25.434751', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-21 19:16:26,170 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766296261505
|
||||
2025-12-21 19:16:26,333 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-21 19:16:26,341 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-21 19:16:26,341 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766296261505, type: vip, order_id: user1766296261505_vip_order_1766315769_c0268b55
|
||||
2025-12-21 19:16:51,387 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '6c0f4be3b017404eba1fde3e2fec0033', 'order_id': 'user1766315791228_vip_order_1766315797_e1bcdb8d', 'detail': '{}', 'sign': 'b554cda9c1f6a4466e7ee56007fe8443', 'pay_time': '2025-12-21 19:16:51.353245', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-21 19:16:51,555 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766315791228
|
||||
2025-12-21 19:16:51,717 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-21 19:16:51,722 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-21 19:16:51,722 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766315791228, type: vip, order_id: user1766315791228_vip_order_1766315797_e1bcdb8d
|
||||
2025-12-21 23:13:35,029 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '91bdcf5e525341ce81a9e064c234d47c', 'order_id': 'user1766329986866_vip_order_1766329997_fcda92f7', 'detail': '{}', 'sign': '80dfb5f134bc17d00936f8fb57ee1143', 'pay_time': '2025-12-21 23:13:34.878451', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-21 23:13:35,231 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766329986866
|
||||
2025-12-21 23:13:35,519 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-21 23:13:35,525 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-21 23:13:35,526 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766329986866, type: vip, order_id: user1766329986866_vip_order_1766329997_fcda92f7
|
||||
2025-12-22 22:28:55,382 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '2b64f275a603458e9570851293a277ac', 'order_id': 'user1766413713868_vip_order_1766413715_e1ba5dc4', 'detail': '{}', 'sign': '28a8e18854866b478e66afa4b343786e', 'pay_time': '2025-12-22 22:28:55.154387', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-22 22:28:56,142 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766413713868
|
||||
2025-12-22 22:28:56,448 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-22 22:28:56,478 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-22 22:28:56,488 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766413713868, type: vip, order_id: user1766413713868_vip_order_1766413715_e1ba5dc4
|
||||
2025-12-23 10:27:30,132 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'bee93191cdbf4a30902c7fa4de36140a', 'order_id': 'user1766456742992_vip_order_1766456837_90bb94eb', 'detail': '{}', 'sign': '47ea9e6e940c5d1cb99926b7a4eebf68', 'pay_time': '2025-12-23 10:27:29.914796', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-23 10:27:31,276 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766456742992
|
||||
2025-12-23 10:27:31,838 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-23 10:27:31,880 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-23 10:27:31,895 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766456742992, type: vip, order_id: user1766456742992_vip_order_1766456837_90bb94eb
|
||||
2025-12-23 16:42:42,232 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'bcf3c2ce47d740e797f0096fcbc9d823', 'order_id': 'user1766479338813_vip_order_1766479345_caa1a9e8', 'detail': '{}', 'sign': 'fd49eb134791f746001ad44c44832c17', 'pay_time': '2025-12-23 16:42:42.041901', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-23 16:42:42,776 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766479338813
|
||||
2025-12-23 16:42:43,062 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-23 16:42:43,586 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-23 16:42:43,590 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766479338813, type: vip, order_id: user1766479338813_vip_order_1766479345_caa1a9e8
|
||||
2025-12-23 19:26:03,240 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '7ed0d27bde004d3a935b53b49702b743', 'order_id': 'user1766489119849_vip_order_1766489127_3aabed43', 'detail': '{}', 'sign': '8fd4c65d638c13a3ef961298964c5d9c', 'pay_time': '2025-12-23 19:26:03.071519', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-23 19:26:03,572 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766489119849
|
||||
2025-12-23 19:26:03,737 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-23 19:26:03,746 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-23 19:26:03,746 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766489119849, type: vip, order_id: user1766489119849_vip_order_1766489127_3aabed43
|
||||
2025-12-24 07:58:44,464 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '2d353f37d4a34fa182f5fad493c1e8e3', 'order_id': 'user1766534296167_vip_order_1766534297_534b3d4b', 'detail': '{}', 'sign': '4e69abfb04e09e714f099bb65973e782', 'pay_time': '2025-12-24 07:58:44.271470', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-24 07:58:45,381 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766534296167
|
||||
2025-12-24 07:58:45,715 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-24 07:58:45,753 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-24 07:58:45,769 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766534296167, type: vip, order_id: user1766534296167_vip_order_1766534297_534b3d4b
|
||||
2025-12-24 08:00:03,544 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'fff28d3bf4504062b1fe56b89c532440', 'order_id': 'user1766534381284_vip_order_1766534384_f1bdf215', 'detail': '{}', 'sign': '6e55110ff04fc5c1c68f82d463d3beae', 'pay_time': '2025-12-24 08:00:03.509869', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-24 08:00:03,872 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766534381284
|
||||
2025-12-24 08:00:04,158 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-24 08:00:04,166 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-24 08:00:04,166 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766534381284, type: vip, order_id: user1766534381284_vip_order_1766534384_f1bdf215
|
||||
2025-12-24 12:38:45,444 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '1abe7c217dea4c119921735f2d9d7395', 'order_id': 'user1766551004577_vip_order_1766551115_03736ef8', 'detail': '{}', 'sign': 'bf63ffd12cf05ab189b459ee6d83df3c', 'pay_time': '2025-12-24 12:38:45.259828', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-24 12:38:46,088 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766551004577
|
||||
2025-12-24 12:38:46,365 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-24 12:38:46,393 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-24 12:38:46,404 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766551004577, type: vip, order_id: user1766551004577_vip_order_1766551115_03736ef8
|
||||
2025-12-25 14:34:56,058 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'e63e23aa882a4945807f5e3cdbc77bff', 'order_id': 'user1766644478831_vip_order_1766644480_aafd9b52', 'detail': '{}', 'sign': '45f9603ed57b1dc86741ca3c47dd7617', 'pay_time': '2025-12-25 14:34:55.845371', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 14:34:56,872 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766644478831
|
||||
2025-12-25 14:34:57,203 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:34:57,693 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:34:57,708 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766644478831, type: vip, order_id: user1766644478831_vip_order_1766644480_aafd9b52
|
||||
2025-12-25 14:39:27,820 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '259af2bb60e3463e9cabc83c3944b238', 'order_id': 'user1766644709180_vip_order_1766644722_eae4ff2a', 'detail': '{}', 'sign': '09331deda262c3bca0f04b9eb092c335', 'pay_time': '2025-12-25 14:39:27.641197', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 14:39:27,989 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766644709180
|
||||
2025-12-25 14:39:28,150 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:39:28,156 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:39:28,157 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766644709180, type: vip, order_id: user1766644709180_vip_order_1766644722_eae4ff2a
|
||||
2025-12-25 14:45:00,011 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '53612e51bdc94ad5b90086091f684e1a', 'order_id': 'user1766645074947_vip_order_1766645082_cc1cd20d', 'detail': '{}', 'sign': '90920b8ff308e49dbe8f6d3489126fad', 'pay_time': '2025-12-25 14:44:59.925734', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 14:45:00,253 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766645074947
|
||||
2025-12-25 14:45:00,451 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:45:00,458 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:45:00,458 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766645074947, type: vip, order_id: user1766645074947_vip_order_1766645082_cc1cd20d
|
||||
2025-12-25 14:53:50,486 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '0c3481595d2244f49b988b02f6fb79b1', 'order_id': 'user1766645607019_vip_order_1766645617_7d2c2f0e', 'detail': '{}', 'sign': 'b97ca09f294f4caa628abc4d63311c83', 'pay_time': '2025-12-25 14:53:50.331570', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 14:53:50,658 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766645607019
|
||||
2025-12-25 14:53:50,822 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:53:50,828 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 14:53:50,828 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766645607019, type: vip, order_id: user1766645607019_vip_order_1766645617_7d2c2f0e
|
||||
2025-12-25 15:26:02,951 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'c7002f2ab8d34fd981606bd9fe0051a2', 'order_id': 'user1766102687534_vip_order_1766647552_9f9feed4', 'detail': '{}', 'sign': '7eaf3442d110d2af3ef6adf2eb11189b', 'pay_time': '2025-12-25 15:26:02.782398', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 15:26:03,549 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766102687534
|
||||
2025-12-25 15:26:03,819 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 15:26:03,834 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 15:26:03,836 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766102687534, type: vip, order_id: user1766102687534_vip_order_1766647552_9f9feed4
|
||||
2025-12-25 15:28:52,615 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'f210a5394ad84429b91d43ebee8dc48a', 'order_id': 'user1766647507325_vip_order_1766647720_bec3fa2a', 'detail': '{}', 'sign': '1c5fced854dc61daf40f0a7f485ed25e', 'pay_time': '2025-12-25 15:28:52.584563', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 15:28:52,792 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766647507325
|
||||
2025-12-25 15:28:52,954 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 15:28:53,029 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 15:28:53,029 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766647507325, type: vip, order_id: user1766647507325_vip_order_1766647720_bec3fa2a
|
||||
2025-12-25 16:08:32,724 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '9c8c1d5f84e447db9413e6ba8079e079', 'order_id': 'user1766650018794_vip_order_1766650105_99ab8550', 'detail': '{}', 'sign': '1d8f5c3b0bb4c7d4e7e3249b34b7c14b', 'pay_time': '2025-12-25 16:08:32.590930', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 16:08:32,895 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766650018794
|
||||
2025-12-25 16:08:33,058 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:08:33,065 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:08:33,065 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766650018794, type: vip, order_id: user1766650018794_vip_order_1766650105_99ab8550
|
||||
2025-12-25 16:08:49,855 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '057e5d84a56b4a5f9022ed60fed63584', 'order_id': 'user1766648960557_vip_order_1766650099_f3ffcbaf', 'detail': '{}', 'sign': 'ebc9dbf1b61a6ea2d2ba744572ec1784', 'pay_time': '2025-12-25 16:08:49.823710', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 16:08:50,024 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766648960557
|
||||
2025-12-25 16:08:50,190 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:08:50,193 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:08:50,193 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766648960557, type: vip, order_id: user1766648960557_vip_order_1766650099_f3ffcbaf
|
||||
2025-12-25 16:53:15,082 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '7c233317484b44f8b12f64752c9ba765', 'order_id': 'user1766642873343_vip_order_1766652778_2ddc93d9', 'detail': '{}', 'sign': '5befe13745a772364edfd851bcf9d83c', 'pay_time': '2025-12-25 16:53:14.927339', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 16:53:15,336 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766642873343
|
||||
2025-12-25 16:53:15,497 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:53:15,505 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 16:53:15,505 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766642873343, type: vip, order_id: user1766642873343_vip_order_1766652778_2ddc93d9
|
||||
2025-12-25 17:21:30,633 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '724bf0ff5b0c4ba886047a8f577e6a49', 'order_id': 'user1766425649141_vip_order_1766654472_e3a31ef2', 'detail': '{}', 'sign': '930b26b77e50d7b8677bc4b07a8ccefd', 'pay_time': '2025-12-25 17:21:30.479462', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 17:21:30,826 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766425649141
|
||||
2025-12-25 17:21:30,988 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 17:21:30,995 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 17:21:30,995 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766425649141, type: vip, order_id: user1766425649141_vip_order_1766654472_e3a31ef2
|
||||
2025-12-25 18:48:30,408 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '46adec50b319426db93f5efec4108336', 'order_id': 'user1766659694256_vip_order_1766659696_f8319dd5', 'detail': '{}', 'sign': '494b5f48f3410d8185060404620d558e', 'pay_time': '2025-12-25 18:48:30.276586', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 18:48:30,585 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766659694256
|
||||
2025-12-25 18:48:30,745 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 18:48:30,759 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 18:48:30,760 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766659694256, type: vip, order_id: user1766659694256_vip_order_1766659696_f8319dd5
|
||||
2025-12-25 23:43:41,286 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '73f41bf5555b46fba22a3ef5ab78a2f2', 'order_id': 'user1766677403431_vip_order_1766677408_51f35a7d', 'detail': '{}', 'sign': '9cb826f31fc24872d64a32ac4da10878', 'pay_time': '2025-12-25 23:43:41.104124', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-25 23:43:42,040 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766677403431
|
||||
2025-12-25 23:43:42,743 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-25 23:43:42,752 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-25 23:43:42,752 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766677403431, type: vip, order_id: user1766677403431_vip_order_1766677408_51f35a7d
|
||||
2025-12-26 01:28:00,596 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'afc37fdbf5394e5390f5daf25965bc30', 'order_id': 'user1766683323953_vip_order_1766683672_dffcccce', 'detail': '{}', 'sign': '7da90f4aa550ff781ba2eb29e816808e', 'pay_time': '2025-12-26 01:28:00.421039', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-26 01:28:00,817 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766683323953
|
||||
2025-12-26 01:28:00,984 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-26 01:28:00,995 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-26 01:28:00,996 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766683323953, type: vip, order_id: user1766683323953_vip_order_1766683672_dffcccce
|
||||
2025-12-26 07:32:19,663 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '4c809d871d84418da6336f2653a54a09', 'order_id': 'user1766705393451_vip_order_1766705520_54a049e8', 'detail': '{}', 'sign': '09a46bd70ca5692d59f07c4e1a97df89', 'pay_time': '2025-12-26 07:32:19.440263', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-26 07:32:20,466 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766705393451
|
||||
2025-12-26 07:32:20,766 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-26 07:32:20,799 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-26 07:32:20,806 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766705393451, type: vip, order_id: user1766705393451_vip_order_1766705520_54a049e8
|
||||
2025-12-26 09:51:47,523 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '5fd5db6752c048768d42099f40bcf94d', 'order_id': 'user1766713881133_vip_order_1766713892_63fcc777', 'detail': '{}', 'sign': 'bc66dbdf9e2050e8d16e210240c0d2fd', 'pay_time': '2025-12-26 09:51:47.392578', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-26 09:51:47,791 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766713881133
|
||||
2025-12-26 09:51:47,952 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-26 09:51:47,962 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-26 09:51:47,962 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766713881133, type: vip, order_id: user1766713881133_vip_order_1766713892_63fcc777
|
||||
2025-12-26 20:42:12,077 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '3d81365773924cf28e6633f8736c49e9', 'order_id': 'user1766456732682_vip_order_1766752919_c01e5c7c', 'detail': '{}', 'sign': '9c791d646e77a3c23d940ff75bbac2d5', 'pay_time': '2025-12-26 20:42:11.851603', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-26 20:42:12,726 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766456732682
|
||||
2025-12-26 20:42:13,088 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-26 20:42:13,127 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-26 20:42:13,684 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766456732682, type: vip, order_id: user1766456732682_vip_order_1766752919_c01e5c7c
|
||||
2025-12-26 21:59:52,133 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '1bc7e794e2504a0f8262f6ed08c26b89', 'order_id': 'user1766757398274_vip_order_1766757411_adac75bc', 'detail': '{}', 'sign': '88b0802f3651fdbf0ebcf36f5e93cd35', 'pay_time': '2025-12-26 21:59:51.964255', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-26 21:59:52,342 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766757398274
|
||||
2025-12-26 21:59:52,508 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-26 21:59:52,516 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-26 21:59:52,516 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766757398274, type: vip, order_id: user1766757398274_vip_order_1766757411_adac75bc
|
||||
2025-12-27 00:35:29,599 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'f035e2d41a8d4cdb952202b8a4670766', 'order_id': 'user1766766911300_vip_order_1766766916_05b79fe5', 'detail': '{}', 'sign': 'b8888a23e0282872c293a741572256aa', 'pay_time': '2025-12-27 00:35:29.446434', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-27 00:35:29,780 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766766911300
|
||||
2025-12-27 00:35:29,943 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-27 00:35:29,951 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-27 00:35:29,951 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766766911300, type: vip, order_id: user1766766911300_vip_order_1766766916_05b79fe5
|
||||
2025-12-27 22:06:29,961 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'd5fbb42c040844f69d1f13f4f0402a59', 'order_id': 'user1766760554294_vip_order_1766844380_dba8623e', 'detail': '{}', 'sign': '55635e22369cffcfb8156d9f1ee8697e', 'pay_time': '2025-12-27 22:06:29.721581', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-27 22:06:31,066 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766760554294
|
||||
2025-12-27 22:06:31,425 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-27 22:06:31,465 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-27 22:06:31,479 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766760554294, type: vip, order_id: user1766760554294_vip_order_1766844380_dba8623e
|
||||
2025-12-27 22:36:00,006 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'edb9213e2ac343bbb7de697092b7ec39', 'order_id': 'user1766092372361_vip_order_1766846141_d4d4ad0b', 'detail': '{}', 'sign': '084d88a8d918b9b1654327cf2cb895b5', 'pay_time': '2025-12-27 22:35:59.824034', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-27 22:36:00,291 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766092372361
|
||||
2025-12-27 22:36:00,457 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-27 22:36:00,464 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-27 22:36:00,464 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766092372361, type: vip, order_id: user1766092372361_vip_order_1766846141_d4d4ad0b
|
||||
2025-12-28 13:30:47,717 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'a3f3d369653c425887f0ab9b30a0a50c', 'order_id': 'user1766864758860_vip_order_1766899835_ecf39390', 'detail': '{}', 'sign': '4eca166a31f41ba4c72a4f9779d3054b', 'pay_time': '2025-12-28 13:30:47.532714', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 13:30:48,327 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766864758860
|
||||
2025-12-28 13:30:48,647 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:30:48,679 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:30:48,692 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766864758860, type: vip, order_id: user1766864758860_vip_order_1766899835_ecf39390
|
||||
2025-12-28 13:38:58,457 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '938ad71940a745e48644d931e411b1df', 'order_id': 'user1766855547122_vip_order_1766900323_ce3c2fbb', 'detail': '{}', 'sign': 'd6339a30dcc819df47e59f13fdf0bfb9', 'pay_time': '2025-12-28 13:38:58.266814', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 13:38:58,632 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766855547122
|
||||
2025-12-28 13:38:59,039 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:38:59,046 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:38:59,047 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766855547122, type: vip, order_id: user1766855547122_vip_order_1766900323_ce3c2fbb
|
||||
2025-12-28 13:40:17,029 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '81ef015f219845018093968d4eb29f3a', 'order_id': 'user1766900390848_vip_order_1766900392_aeaec3ba', 'detail': '{}', 'sign': '92780969a7d80b7c26b305a56943e055', 'pay_time': '2025-12-28 13:40:16.951540', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 13:40:17,211 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766900390848
|
||||
2025-12-28 13:40:17,376 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:40:17,379 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 13:40:17,380 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766900390848, type: vip, order_id: user1766900390848_vip_order_1766900392_aeaec3ba
|
||||
2025-12-28 15:47:45,741 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '8b996cf1517946a09433c32293723077', 'order_id': 'user1766895362623_vip_order_1766908051_7fc3fffc', 'detail': '{}', 'sign': 'd4d84928ac5c1b82302b17666b7ad13b', 'pay_time': '2025-12-28 15:47:45.590088', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 15:47:45,937 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766895362623
|
||||
2025-12-28 15:47:46,097 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 15:47:46,104 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 15:47:46,105 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766895362623, type: vip, order_id: user1766895362623_vip_order_1766908051_7fc3fffc
|
||||
2025-12-28 16:11:17,866 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '0d6f2a49b60a4d8eb7f5fc63b8497753', 'order_id': 'user1766909461315_vip_order_1766909463_62e1d6ac', 'detail': '{}', 'sign': '40f4acfc429d6e92f7977c0eac62c2bf', 'pay_time': '2025-12-28 16:11:17.733786', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 16:11:18,043 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766909461315
|
||||
2025-12-28 16:11:18,203 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 16:11:18,209 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 16:11:18,221 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766909461315, type: vip, order_id: user1766909461315_vip_order_1766909463_62e1d6ac
|
||||
2025-12-28 17:14:41,638 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'b11f8da725a347d5a733cc99e18846c4', 'order_id': 'user1766913040041_vip_order_1766913270_515b268c', 'detail': '{}', 'sign': 'f1531397c0aa48aadf659f8c83d27c16', 'pay_time': '2025-12-28 17:14:41.488840', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 17:14:41,810 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766913040041
|
||||
2025-12-28 17:14:41,970 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 17:14:41,980 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 17:14:41,981 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766913040041, type: vip, order_id: user1766913040041_vip_order_1766913270_515b268c
|
||||
2025-12-28 19:46:27,708 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '4cc4c0ad58394214bc085b377c3764b3', 'order_id': 'user1766425383848_vip_order_1766922377_e877eadc', 'detail': '{}', 'sign': 'c912c15710540aa8d71bfd6e3b77f4db', 'pay_time': '2025-12-28 19:46:27.518697', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-28 19:46:28,494 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766425383848
|
||||
2025-12-28 19:46:28,806 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-28 19:46:28,839 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-28 19:46:28,849 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766425383848, type: vip, order_id: user1766425383848_vip_order_1766922377_e877eadc
|
||||
2025-12-29 01:14:10,684 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '9a56e0a6795f42db8f26d5a024cc325d', 'order_id': 'user1766942020218_vip_order_1766942033_afdebf35', 'detail': '{}', 'sign': 'a7da24a07ac770e65a6d01a3e93ea019', 'pay_time': '2025-12-29 01:14:10.012262', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 01:14:11,561 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766942020218
|
||||
2025-12-29 01:14:11,942 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 01:14:11,987 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 01:14:12,005 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766942020218, type: vip, order_id: user1766942020218_vip_order_1766942033_afdebf35
|
||||
2025-12-29 02:55:46,502 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '48b759a62a464d56b3fb7147cf169176', 'order_id': 'user1766948108700_vip_order_1766948128_ede4556f', 'detail': '{}', 'sign': '0530aff0e4d17a12e231182e0fd62dfc', 'pay_time': '2025-12-29 02:55:46.368873', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 02:55:46,783 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766948108700
|
||||
2025-12-29 02:55:46,949 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 02:55:46,959 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 02:55:46,959 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766948108700, type: vip, order_id: user1766948108700_vip_order_1766948128_ede4556f
|
||||
2025-12-29 04:40:39,020 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'edac62a960f140369ceb717aeaba54a8', 'order_id': 'user1766954408526_vip_order_1766954427_b77e70bc', 'detail': '{}', 'sign': '2ca0969fef96e146055bb895bff89ab5', 'pay_time': '2025-12-29 04:40:38.871646', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 04:40:39,202 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766954408526
|
||||
2025-12-29 04:40:39,398 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 04:40:40,110 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 04:40:40,110 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766954408526, type: vip, order_id: user1766954408526_vip_order_1766954427_b77e70bc
|
||||
2025-12-29 11:22:44,233 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'e7872fdb149a4d0a9e9a377020ed3846', 'order_id': 'user1766978502076_vip_order_1766978555_addb54db', 'detail': '{}', 'sign': '12d19286d98654b69fb00b9e904a02b9', 'pay_time': '2025-12-29 11:22:44.052519', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 11:22:44,449 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766978502076
|
||||
2025-12-29 11:22:44,625 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 11:22:44,631 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 11:22:44,632 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766978502076, type: vip, order_id: user1766978502076_vip_order_1766978555_addb54db
|
||||
2025-12-29 15:18:01,222 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '62b15f12f36a4120a3ad45bd914419b8', 'order_id': 'user1766992589670_vip_order_1766992668_7feb4dd2', 'detail': '{}', 'sign': 'fb13c58d8ff4f23d9a3b08d15a0cd195', 'pay_time': '2025-12-29 15:18:01.035286', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 15:18:01,880 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1766992589670
|
||||
2025-12-29 15:18:02,184 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 15:18:02,214 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 15:18:02,227 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766992589670, type: vip, order_id: user1766992589670_vip_order_1766992668_7feb4dd2
|
||||
2025-12-29 22:45:30,783 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '673cf3034e584a1c937ab040d5918a3f', 'order_id': 'user1767019507607_vip_order_1767019509_bbfd26ab', 'detail': '{}', 'sign': '53fcdc75340722de59efc461e459cdfb', 'pay_time': '2025-12-29 22:45:30.577358', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-29 22:45:31,541 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767019507607
|
||||
2025-12-29 22:45:31,886 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-29 22:45:31,927 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-29 22:45:31,940 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767019507607, type: vip, order_id: user1767019507607_vip_order_1767019509_bbfd26ab
|
||||
2025-12-30 00:13:14,904 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'dac2bf04f45f4b96a0ce780711dc347d', 'order_id': 'user1767024766316_vip_order_1767024779_4de6dbba', 'detail': '{}', 'sign': 'fe599535e6e61db00bf22f75de029412', 'pay_time': '2025-12-30 00:13:14.770161', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 00:13:15,165 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767024766316
|
||||
2025-12-30 00:13:15,331 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 00:13:15,340 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 00:13:15,340 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767024766316, type: vip, order_id: user1767024766316_vip_order_1767024779_4de6dbba
|
||||
2025-12-30 00:51:51,344 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '209f74a9af7e4906a9e41017bbf3335c', 'order_id': 'user1767026896002_vip_order_1767027100_7dd417ff', 'detail': '{}', 'sign': 'c200f4067b09999a83bb6f9f3bc2b273', 'pay_time': '2025-12-30 00:51:51.188914', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 00:51:51,632 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767026896002
|
||||
2025-12-30 00:51:51,794 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 00:51:51,801 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 00:51:52,016 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767026896002, type: vip, order_id: user1767026896002_vip_order_1767027100_7dd417ff
|
||||
2025-12-30 06:00:15,795 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '7fe465367cdd43518d9bf290d9ea2632', 'order_id': 'user1767045585676_vip_order_1767045592_b3b24c97', 'detail': '{}', 'sign': '94f00384c52bd50dc599435aa3bfda94', 'pay_time': '2025-12-30 06:00:15.608307', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 06:00:16,643 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767045585676
|
||||
2025-12-30 06:00:16,996 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 06:00:17,033 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 06:00:17,045 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767045585676, type: vip, order_id: user1767045585676_vip_order_1767045592_b3b24c97
|
||||
2025-12-30 09:30:38,452 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'cdefff04e7404a03b384ee0c1ea2d6cc', 'order_id': 'user1767058197797_vip_order_1767058229_dfe82b3b', 'detail': '{}', 'sign': 'df23601fd2ceaa0e6b5cea929bdfea13', 'pay_time': '2025-12-30 09:30:38.275481', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 09:30:39,283 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767058197797
|
||||
2025-12-30 09:30:39,766 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 09:30:39,787 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 09:30:39,794 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767058197797, type: vip, order_id: user1767058197797_vip_order_1767058229_dfe82b3b
|
||||
2025-12-30 17:52:33,431 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '7881a501072a48818c8a14358af722dc', 'order_id': 'user1767088318302_vip_order_1767088329_a1770bfe', 'detail': '{}', 'sign': '7003335fd5562a6dab259aac90278dde', 'pay_time': '2025-12-30 17:52:33.219326', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 17:52:34,128 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767088318302
|
||||
2025-12-30 17:52:34,486 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 17:52:34,523 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 17:52:34,535 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767088318302, type: vip, order_id: user1767088318302_vip_order_1767088329_a1770bfe
|
||||
2025-12-30 20:31:31,918 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '85715ed961084e229188b712798f0516', 'order_id': 'user1767097859349_vip_order_1767097861_ccb0eddb', 'detail': '{}', 'sign': 'cfb1be415e68fd55912eb881391a8e2a', 'pay_time': '2025-12-30 20:31:31.753658', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-30 20:31:32,212 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767097859349
|
||||
2025-12-30 20:31:32,476 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-30 20:31:32,492 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-30 20:31:32,492 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767097859349, type: vip, order_id: user1767097859349_vip_order_1767097861_ccb0eddb
|
||||
2025-12-31 18:19:05,989 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '3f6b397d7a484255807b359914d040a3', 'order_id': 'user1767176330550_vip_order_1767176333_479ae9ee', 'detail': '{}', 'sign': '80ac79b3a392da436c7be8f939368099', 'pay_time': '2025-12-31 18:19:05.779904', 'pay_price': '88.00', 'more': ''}
|
||||
2025-12-31 18:19:06,866 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767176330550
|
||||
2025-12-31 18:19:07,287 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-31 18:19:07,348 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-31 18:19:07,944 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767176330550, type: vip, order_id: user1767176330550_vip_order_1767176333_479ae9ee
|
||||
2025-12-31 18:55:07,924 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'f306334a8b6447da8ce14db7bff78213', 'order_id': 'user1766044269820_book_order_1767178489_cdec2cc8', 'detail': '{}', 'sign': '1e3260c0f37039c2a51830d421441d84', 'pay_time': '2025-12-31 18:55:07.732277', 'pay_price': '0.88', 'more': ''}
|
||||
2025-12-31 18:55:08,742 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-31 18:55:08,770 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-31 18:55:08,778 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766044269820, type: book, order_id: user1766044269820_book_order_1767178489_cdec2cc8
|
||||
2025-12-31 19:00:41,588 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '2a6b73ec5b2d41ee9c0a2bb41be31177', 'order_id': 'user1766044269820_book_order_1767178823_d55ea2fa', 'detail': '{}', 'sign': '91d2e0d1ce37043981d523105cf10f89', 'pay_time': '2025-12-31 19:00:41.487375', 'pay_price': '0.88', 'more': ''}
|
||||
2025-12-31 19:00:41,780 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-31 19:00:41,787 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-31 19:00:41,787 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1766044269820, type: book, order_id: user1766044269820_book_order_1767178823_d55ea2fa
|
||||
2025-12-31 19:16:08,040 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': '2fa4787db82b4bd480d6974d44159240', 'order_id': 'user1767179744807_book_order_1767179752_58cb8e8f', 'detail': '{}', 'sign': '3b75920b471f741e64badad48cd65b9f', 'pay_time': '2025-12-31 19:16:07.906529', 'pay_price': '0.88', 'more': ''}
|
||||
2025-12-31 19:16:08,239 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2025-12-31 19:16:08,246 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2025-12-31 19:16:08,246 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767179744807, type: book, order_id: user1767179744807_book_order_1767179752_58cb8e8f
|
||||
2026-01-01 10:47:13,495 - XorPay 寮傛<E5AFAE>閫氱煡鍘熷<E98D98>鏁版嵁: {'aoid': 'ae1043fc189a46e2866e7a2ccaa87c94', 'order_id': 'user1767235545946_vip_order_1767235604_abfead81', 'detail': '{}', 'sign': 'bdd2d201ad80016b17a8e28cac971929', 'pay_time': '2026-01-01 10:47:13.251717', 'pay_price': '288.00', 'more': ''}
|
||||
2026-01-01 10:47:15,341 - memos 鐢ㄦ埛鍒涘缓鎴愬姛: user1767235545946
|
||||
2026-01-01 10:47:15,839 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/_superusers/auth-with-password "HTTP/1.1 200 OK"
|
||||
2026-01-01 10:47:15,888 - HTTP Request: POST https://pocketbase.hackrobot.cn/api/collections/payments/records "HTTP/1.1 200 OK"
|
||||
2026-01-01 10:47:15,890 - 鏀<>粯璁板綍鍐欏叆 PocketBase 鎴愬姛 - user_id: user1767235545946, type: vip, order_id: user1767235545946_vip_order_1767235604_abfead81
|
||||
@@ -1,44 +0,0 @@
|
||||
# 手机微信支付调试说明
|
||||
|
||||
前端(vipgroup 等)与 payjsapi 同机时,支付请求由服务端发起,应使用 `127.0.0.1:8700`。
|
||||
|
||||
## 1. 同机部署(vipgroup + payjsapi 都在本机)
|
||||
|
||||
**无需配置**。默认使用 `http://127.0.0.1:8700`。
|
||||
|
||||
若 `.env.local` 中有 `PAYMENT_API_URL=http://0.0.0.0:8700`,请删除或改为:
|
||||
```bash
|
||||
PAYMENT_API_URL=http://127.0.0.1:8700
|
||||
```
|
||||
(0.0.0.0 无法作为连接目标,会导致立即失败)
|
||||
|
||||
## 2. 确认 payjsapi 已启动
|
||||
|
||||
```bash
|
||||
cd C:\project\payjsapi
|
||||
python run.py
|
||||
```
|
||||
|
||||
应监听 `0.0.0.0:8700`,可从局域网访问。
|
||||
|
||||
## 3. 确认防火墙放行 8700 端口
|
||||
|
||||
Windows 防火墙可能阻止手机访问 8700。若 vipgroup 与 payjsapi 同机,请求是服务端内部调用,一般无问题。
|
||||
|
||||
## 4. 查看控制台日志
|
||||
|
||||
支付失败时,vipgroup 的 Next.js 控制台会输出:
|
||||
|
||||
```
|
||||
[pay] 请求失败: ... apiUrl: http://xxx:8700 requestHost: xxx
|
||||
```
|
||||
|
||||
- 若 `apiUrl` 为 `http://0.0.0.0:8700` → 在 `.env.local` 中设置 `PAYMENT_API_URL=http://192.168.41.222:8700`
|
||||
- 若 `apiUrl` 正确但仍失败 → 检查 payjsapi 是否运行、网络是否可达
|
||||
|
||||
## 5. vipgroup 需包含的配置逻辑
|
||||
|
||||
若 vipgroup 与 lms/nomadlms 结构不同,需确保:
|
||||
|
||||
1. 调用 `/api/pay` 或直接调用 payjsapi 时,使用正确的 `PAYMENT_API_URL`
|
||||
2. 本地开发时 `PAYMENT_API_URL` 指向 `http://{你的局域网IP}:8700`
|
||||
Reference in New Issue
Block a user