'data'
This commit is contained in:
@@ -43,8 +43,8 @@ MEMOS_TOKEN = os.getenv(
|
||||
"eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg",
|
||||
)
|
||||
|
||||
# 回调地址(用于生成 notify_url)
|
||||
BASE_URL = os.getenv("BASE_URL", "https://api.hackrobot.cn")
|
||||
# 回调地址(用于生成 notify_url)。salon 线上部署 api.nomadyt.com
|
||||
BASE_URL = os.getenv("BASE_URL", "https://api.nomadyt.com")
|
||||
|
||||
# MinIO 对象存储
|
||||
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minioweb.hackrobot.cn")
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- common: /submit_meetup_application、/create_user
|
||||
|
||||
本地调试:BASE_URL 需为 ZPAY 可访问地址(如 ngrok)
|
||||
线上部署:BASE_URL=https://api.hackrobot.cn(必须为 ZPAY 可公网访问的地址)
|
||||
线上部署:BASE_URL=https://api.nomadyt.com(必须为 ZPAY 可公网访问的地址)
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
@@ -108,7 +108,7 @@ def _check_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=%s 疑似内网地址,ZPAY 无法回调!线上部署请设置 BASE_URL 为公网地址,如 https://api.nomadyt.com",
|
||||
BASE_URL,
|
||||
)
|
||||
|
||||
|
||||
@@ -246,4 +246,5 @@ class ZPayProvider(PaymentProvider):
|
||||
status = int(result.get("status", 0))
|
||||
except (ValueError, TypeError):
|
||||
status = 0
|
||||
return {"paid": status == 1, "status": status}
|
||||
money = result.get("money", "")
|
||||
return {"paid": status == 1, "status": status, "money": money}
|
||||
|
||||
@@ -246,18 +246,67 @@ def _decode_pending_email(user_id: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _query_zpay_paid(order_id: str) -> bool:
|
||||
def _query_zpay_order(order_id: str) -> dict | None:
|
||||
"""查询 ZPAY 订单状态,返回 {paid, money}"""
|
||||
try:
|
||||
from ..payment import get_payment_provider
|
||||
from ..payment.zpay import ZPayProvider
|
||||
provider = get_payment_provider()
|
||||
if not isinstance(provider, ZPayProvider):
|
||||
return False
|
||||
return None
|
||||
result = provider.query_order_status(order_id, timeout=10)
|
||||
return bool(result.get("paid"))
|
||||
return result if result.get("paid") else None
|
||||
except Exception as e:
|
||||
logging.warning(f"salon complete-order zpay query failed: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _query_xorpay_paid(order_id: str) -> tuple[bool, str]:
|
||||
"""查询 XorPay 是否已支付,返回 (paid, pay_price)"""
|
||||
try:
|
||||
from ..payment import get_payment_provider
|
||||
from ..payment.xorpay import XorPayProvider
|
||||
provider = get_payment_provider()
|
||||
if not isinstance(provider, XorPayProvider):
|
||||
return False, ""
|
||||
result = provider.query_order_status(order_id, timeout=8)
|
||||
if result.get("paid"):
|
||||
return True, str(result.get("pay_price", "") or "")
|
||||
return False, ""
|
||||
except Exception as e:
|
||||
logging.warning(f"salon complete-order xorpay query failed: {e}")
|
||||
return False, ""
|
||||
|
||||
|
||||
def _update_solan_red_on_payment(token: str, user_id: str, order_id: str, amount_cents: int) -> None:
|
||||
"""支付成功后兜底更新 solanRed:order_id、amount、vip"""
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
try:
|
||||
r = requests.get(
|
||||
f"{base}/api/collections/solanRed/records",
|
||||
params={"filter": f'user_id = "{user_id}"', "sort": "-created", "perPage": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return
|
||||
data = r.json()
|
||||
items = data.get("items") or []
|
||||
if not items:
|
||||
return
|
||||
rec_id = items[0].get("id")
|
||||
if not rec_id:
|
||||
return
|
||||
patch = {"order_id": order_id, "amount": amount_cents, "vip": True}
|
||||
requests.patch(
|
||||
f"{base}/api/collections/solanRed/records/{rec_id}",
|
||||
json=patch,
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
logging.info(f"salon complete-order 兜底更新 solanRed - user_id: {user_id}, amount: {amount_cents}")
|
||||
except Exception as e:
|
||||
logging.warning(f"salon complete-order 兜底更新 solanRed 失败: {e}")
|
||||
|
||||
|
||||
@router.post("/complete-order")
|
||||
@@ -288,13 +337,46 @@ async def complete_order(item: dict):
|
||||
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
|
||||
if attempt < max_retries - 1:
|
||||
paid_via = None
|
||||
pay_price = ""
|
||||
zpay_result = _query_zpay_order(order_id)
|
||||
if zpay_result:
|
||||
paid_via = "zpay"
|
||||
pay_price = str(zpay_result.get("money", "") or zpay_result.get("pay_price", ""))
|
||||
if not paid_via:
|
||||
xorpay_paid, xorpay_price = _query_xorpay_paid(order_id)
|
||||
if xorpay_paid:
|
||||
paid_via = "xorpay"
|
||||
pay_price = xorpay_price
|
||||
if paid_via:
|
||||
from ..services.payment import handle_payment_success
|
||||
price = pay_price if pay_price and float(pay_price or 0) > 0 else "1"
|
||||
try:
|
||||
handle_payment_success(
|
||||
order_id,
|
||||
price,
|
||||
f"salon-{paid_via}-poll",
|
||||
use_memos=False,
|
||||
site_id=SITE_ID,
|
||||
)
|
||||
time.sleep(1)
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.warning(f"salon complete-order 手动触发支付处理失败: {e}")
|
||||
time.sleep(2)
|
||||
continue
|
||||
resp_preview = r.text[:200] if r.ok else f"status={r.status_code}"
|
||||
logging.warning(f"salon complete-order 未找到订单: order_id={order_id}, attempt={attempt + 1}, pb_resp={resp_preview}")
|
||||
raise HTTPException(status_code=400, detail="订单不存在或未支付成功")
|
||||
|
||||
# 兜底更新 solanRed:回调可能未到,确保 order_id、amount、vip 正确(按 user_id 查找,pending_ 用户记录存的是 pending_xxx)
|
||||
zpay_result = _query_zpay_order(order_id)
|
||||
money_yuan = float(zpay_result.get("money", 0) or 0) if zpay_result else 0
|
||||
amount_cents = int(money_yuan * 100) if money_yuan > 0 else 100
|
||||
lookup_user_id = user_id
|
||||
_update_solan_red_on_payment(token, lookup_user_id, order_id, amount_cents)
|
||||
|
||||
if user_id.startswith("pending_"):
|
||||
email = _decode_pending_email(user_id)
|
||||
if not email:
|
||||
@@ -314,7 +396,28 @@ async def complete_order(item: dict):
|
||||
"is_new": True,
|
||||
}
|
||||
|
||||
return {"ok": True, "token": None, "record": None, "is_new": False}
|
||||
# 非 pending 用户:返回 solanRed 记录供前端显示微信号
|
||||
record = None
|
||||
try:
|
||||
esc = lookup_user_id.replace("\\", "\\\\").replace('"', '\\"')
|
||||
r = requests.get(
|
||||
f"{base}/api/collections/solanRed/records",
|
||||
params={"filter": f'user_id = "{esc}"', "sort": "-created", "perPage": 1},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
items = r.json().get("items") or []
|
||||
if items:
|
||||
rec = items[0]
|
||||
record = {
|
||||
"wechatId": rec.get("wechatId"),
|
||||
"nickname": rec.get("nickname"),
|
||||
"media": rec.get("media"),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "token": None, "record": record, "is_new": False}
|
||||
|
||||
|
||||
@router.get("/vip/check")
|
||||
@@ -368,10 +471,15 @@ async def join_by_wechat(wechat_id: str = ""):
|
||||
record_dict = None
|
||||
if rec:
|
||||
record_dict = {
|
||||
"id": _get(rec, "id"),
|
||||
"user_id": _get(rec, "user_id"),
|
||||
"wechatId": _get(rec, "wechatId"),
|
||||
"nickname": _get(rec, "nickname"),
|
||||
"media": _get(rec, "media"),
|
||||
"amount": amount,
|
||||
"order_id": _get(rec, "order_id"),
|
||||
"vip": vip,
|
||||
"is_volunteer": bool(_get(rec, "is_volunteer")),
|
||||
}
|
||||
return {
|
||||
"hasRecord": has_record,
|
||||
@@ -387,6 +495,47 @@ async def join_by_wechat(wechat_id: str = ""):
|
||||
return {"hasRecord": False, "record": None, "isComplete": False, "isWechatFriend": False, "isApproved": False, "isRejected": False, "vip": False}
|
||||
|
||||
|
||||
@router.get("/join/volunteers")
|
||||
async def join_volunteers():
|
||||
"""获取志愿者列表 - solanRed 中 is_volunteer=true && is_approved=true"""
|
||||
token, err = _get_admin_token()
|
||||
if not token:
|
||||
return {"volunteer": None}
|
||||
base = (PB_URL or "").rstrip("/")
|
||||
if not base:
|
||||
return {"volunteer": None}
|
||||
try:
|
||||
r = requests.get(
|
||||
f"{base}/api/collections/solanRed/records",
|
||||
params={
|
||||
"filter": 'is_volunteer = true && is_approved = true',
|
||||
"sort": "-created",
|
||||
"perPage": 1,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return {"volunteer": None}
|
||||
data = r.json()
|
||||
items = data.get("items") or []
|
||||
volunteer = None
|
||||
if items:
|
||||
rec = items[0]
|
||||
nickname = str(rec.get("xiaohongshu_nickname") or rec.get("nickname") or "").strip() or "志愿者"
|
||||
media = str(rec.get("media") or "").strip() or None
|
||||
xiaohongshu_url = str(rec.get("xiaohongshu_url") or "").strip() or None
|
||||
volunteer = {
|
||||
"nickname": nickname,
|
||||
"avatar": media if media else None,
|
||||
"xiaohongshu_url": xiaohongshu_url if xiaohongshu_url else None,
|
||||
}
|
||||
return {"volunteer": volunteer}
|
||||
except Exception as e:
|
||||
logging.warning(f"salon join volunteers 查询失败: {e}")
|
||||
return {"volunteer": None}
|
||||
|
||||
|
||||
class XiaohongshuProfileRequest(BaseModel):
|
||||
url: str
|
||||
|
||||
@@ -1151,6 +1300,7 @@ async def submit_join(item: dict):
|
||||
# PocketBase type 仅接受 session-1/session-2 等,digital-nomad/volunteer 映射为空
|
||||
pb_type = raw_type if raw_type in ("session-1", "session-2") else ""
|
||||
|
||||
qualify = bool(item.get("qualify", False)) # 女性且小红书帖子>10 时自动通过
|
||||
pb_data = {
|
||||
"user_id": user_id,
|
||||
"nickname": item.get("nickname", ""),
|
||||
@@ -1169,6 +1319,7 @@ async def submit_join(item: dict):
|
||||
"xiaohongshu_url": item.get("xiaohongshu_url", ""),
|
||||
"is_volunteer": bool(item.get("is_volunteer", False)),
|
||||
"is_wechat_friend": bool(item.get("is_wechat_friend", False)),
|
||||
"is_approved": qualify, # 符合条件时自动通过审核
|
||||
"age_range": item.get("age_range", ""),
|
||||
"first_time": item.get("first_time", ""),
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ def handle_payment_success(
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
# meetup/salon 新用户:支付成功后才创建用户
|
||||
# meetup/salon 新用户:支付成功后才创建用户。solanRed 记录创建时用的是 pending_xxx,查找需用原值
|
||||
solan_lookup_user_id = user_id
|
||||
if pay_type in ("meetup", "salon") and user_id.startswith("pending_"):
|
||||
email = _decode_pending_email(user_id)
|
||||
if email:
|
||||
@@ -151,25 +152,30 @@ def handle_payment_success(
|
||||
solan_collection = None
|
||||
|
||||
if solan is not None:
|
||||
update_payload = {
|
||||
"order_id": order_id,
|
||||
"amount": amount_cents,
|
||||
}
|
||||
if pay_type == "salon":
|
||||
update_payload["vip"] = True
|
||||
elif pay_type == "meetup":
|
||||
update_payload["type"] = pay_type
|
||||
existing = solan.get_list(1, 1, {
|
||||
"filter": f'user_id = "{user_id}"',
|
||||
"filter": f'user_id = "{solan_lookup_user_id}"',
|
||||
"sort": "-created",
|
||||
})
|
||||
if existing.items:
|
||||
record_id = existing.items[0].id
|
||||
solan.update(record_id, {
|
||||
"type": pay_type,
|
||||
"order_id": order_id,
|
||||
"amount": amount_cents,
|
||||
})
|
||||
patch = dict(update_payload)
|
||||
if solan_lookup_user_id != user_id:
|
||||
patch["user_id"] = user_id
|
||||
solan.update(record_id, patch)
|
||||
logging.info(f"{solan_collection} 支付字段补齐成功 - user_id: {user_id}, record_id: {record_id}")
|
||||
else:
|
||||
solan.create({
|
||||
"user_id": user_id,
|
||||
"type": pay_type,
|
||||
"order_id": order_id,
|
||||
"amount": amount_cents,
|
||||
})
|
||||
create_payload = {"user_id": user_id, **update_payload}
|
||||
if pay_type == "salon":
|
||||
create_payload["type"] = ""
|
||||
solan.create(create_payload)
|
||||
logging.info(f"{solan_collection} 完整记录创建(兜底) - user_id: {user_id}")
|
||||
elif pay_type not in ("meetup", "salon"):
|
||||
existing = pb_client.collection("payments").get_list(1, 1, {
|
||||
|
||||
Reference in New Issue
Block a user