From 5306e8b5ccc0284836e530b757474053d0c9acb0 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 26 Mar 2026 02:35:13 -0500 Subject: [PATCH] 's' --- app/config.py | 2 +- app/main.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 605 -> 603 bytes app/payment/__pycache__/base.cpython-312.pyc | Bin 2584 -> 2582 bytes .../__pycache__/factory.cpython-312.pyc | Bin 2900 -> 2898 bytes .../__pycache__/xorpay.cpython-312.pyc | Bin 5733 -> 5731 bytes app/payment/__pycache__/zpay.cpython-312.pyc | Bin 11518 -> 11711 bytes app/payment/zpay.py | 3 +- .../__pycache__/__init__.cpython-312.pyc | Bin 720 -> 718 bytes .../__pycache__/_digital_base.cpython-312.pyc | Bin 16160 -> 17313 bytes .../__pycache__/cnomadcna.cpython-312.pyc | Bin 378 -> 376 bytes .../__pycache__/common.cpython-312.pyc | Bin 3914 -> 3912 bytes .../__pycache__/digital.cpython-312.pyc | Bin 360 -> 358 bytes .../__pycache__/legacy.cpython-312.pyc | Bin 382 -> 380 bytes .../__pycache__/nomadlms.cpython-312.pyc | Bin 351 -> 349 bytes .../__pycache__/nomadvip.cpython-312.pyc | Bin 35723 -> 35721 bytes app/routers/_digital_base.py | 74 +-- app/routers/meetup.py | 1 + app/routers/salon.py | 4 + app/routers/salon_meetup.py | 502 ++++++++++++++++++ app/sdk/__pycache__/__init__.cpython-312.pyc | Bin 626 -> 624 bytes app/sdk/__pycache__/memos.cpython-312.pyc | Bin 3848 -> 3846 bytes app/sdk/__pycache__/minio.cpython-312.pyc | Bin 4215 -> 4213 bytes .../__pycache__/pocketbase.cpython-312.pyc | Bin 2905 -> 3131 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 513 -> 511 bytes app/sdk/pocketbase.py | 4 +- .../__pycache__/__init__.cpython-312.pyc | Bin 531 -> 529 bytes .../__pycache__/memos.cpython-312.pyc | Bin 1034 -> 1032 bytes .../__pycache__/payment.cpython-312.pyc | Bin 8795 -> 9676 bytes app/services/__pycache__/pb.cpython-312.pyc | Bin 1161 -> 987 bytes app/services/payment.py | 29 +- app/services/pb.py | 9 +- 32 files changed, 583 insertions(+), 47 deletions(-) create mode 100644 app/routers/salon.py create mode 100644 app/routers/salon_meetup.py diff --git a/app/config.py b/app/config.py index 23c7dcf..b71b47b 100644 --- a/app/config.py +++ b/app/config.py @@ -43,7 +43,7 @@ MEMOS_TOKEN = os.getenv( "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiaGFja3JvYm90IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NzQwNTE0OX0.Idn7kBlxE-CoSOXwWuZ1tHGIRKHAIeDyXSafGS5OHsg", ) -# 回调地址(用于生成 notify_url) +# 回调地址(用于生成 notify_url)。线上 api.nomadyt.com 时需设置 BASE_URL=https://api.nomadyt.com BASE_URL = os.getenv("BASE_URL", "https://api.hackrobot.cn") # MinIO 对象存储 diff --git a/app/main.py b/app/main.py index 8b3dc59..a197b5b 100755 --- a/app/main.py +++ b/app/main.py @@ -31,6 +31,7 @@ from .routers import ( common_router, meetup_router, ) +from .routers.salon_meetup import router as salon_meetup_router app = FastAPI( title="PayJS API", @@ -165,6 +166,7 @@ app.include_router(nomadlms_router) app.include_router(legacy_router) app.include_router(common_router) app.include_router(meetup_router) +app.include_router(salon_meetup_router) @app.get("/") diff --git a/app/payment/__pycache__/__init__.cpython-312.pyc b/app/payment/__pycache__/__init__.cpython-312.pyc index 9f5317ef497bff72f29805e20ea05595691c297f..751e5ff57a298f7934cba94a89dbd7cac3946b97 100644 GIT binary patch delta 60 zcmcc1a+`(sG%qg~0}y1N-<+wrk@r5MqPDYDOhHk8R%&udOhIC0R&ioMW=vv10f>{E MnpYAtnV-oI0AZsP_y7O^ delta 62 zcmcc3a+ihoG%qg~0}vG2Z^;zc$a|krSy#Wjyc~p!^7Bjd3lb}{iW3Vm^%DyUK%Csv Lyb}G%yi9%oZ*>&F diff --git a/app/payment/__pycache__/base.cpython-312.pyc b/app/payment/__pycache__/base.cpython-312.pyc index 8bd3013ac4ab7bf375f98fd2978fe56d8a36e711..fd323d0b27f1cc3b99aa8872bf4680acfc9ef53c 100644 GIT binary patch delta 61 zcmbOsGEIc{G%qg~0}$jZug$F9$h&|^QQg@prl2T4D>b9RNh@6w&|y delta 63 zcmbOxGDC#-G%qg~0}vG2Z^?An$h&|^SyR8fyc~p!^7Bjd3lb}{iW3Vm^%DyUK%Csv Myb}G*=a@X$0c)KVCIA2c diff --git a/app/payment/__pycache__/factory.cpython-312.pyc b/app/payment/__pycache__/factory.cpython-312.pyc index 103f59f050c6de20c0c00d6c86fd18da419faa1d..24308778c28f66366a546dd0e80d1c5cd5c0c23a 100644 GIT binary patch delta 61 zcmca2c1eu)G%qg~0}zy*-<%n+kvEoEQOnsXrl2T4D>blb$Q#S7tfOCEUJk-V`S~UK1&Nhe#fb%(`iTVvAWm*- MUWxwZZsz0c0CS}kVE_OC diff --git a/app/payment/__pycache__/xorpay.cpython-312.pyc b/app/payment/__pycache__/xorpay.cpython-312.pyc index 955ed8c08ab672d046aa2debfbe8bb304a6680b2..bb4b95a29219cf513d2c0a17e75b8e0b488035b7 100644 GIT binary patch delta 61 zcmaE=^H_)XG%qg~0}y!b+?*M{k(ZZQQPbHfrl2T4D>bb(lcKt_RZKxqepYI7NlZavWma)wL1s*1K>>)9 zo0?Y=vssflfQ|9i<|h+2C&#NuI0_&MF)=cv z2-b4eaHMc$YcMd>aMW-X@s{vIv;g^>FtsT{bC`pHYC-hmGSz&>wUc+KYVvbLGW7J7 zGgL4~GL$oF3Qv|(dn{Yz2Gm~E1tL^{geG&*1Q6Q?EIOG>J%EvGat8eMopfl<1ZY;%$UeH!Tu`)m{BAM^dbOr Cq=y0k delta 317 zcmdlV{V$U5G%qg~0}vG2Z^;x@*~s^UNm*0Byu2KQi}LeJ^a~Ozvx*Z7GW8P+3P7CP z)Vvb?&FahnY>dA)S8$q0F=lOEtGtVmmm`HMTZ4h2h9iZ44%6f(s`-qilhxET`8gsP zdcw*XDwrb~${95UCikd4=G0^=ngF!4$Y*kodH^Hm>|I}1rDEDvcjR+nXE@9t?IOo;Sees>h54`!ud_Y# zVM|63+nyCj9$^=Bk!C(3#Rwwh1YL}nj~Ft#7)@r@+RylV^I@&KjEcgn<|~RnFaW6? v!C>ly%8B9&A>k(~PU?LD2~R$%Go3MFa;&buU^FwM`2_o~3?RBl5a>_<2Y_D5 diff --git a/app/payment/zpay.py b/app/payment/zpay.py index 6978457..39b35d4 100644 --- a/app/payment/zpay.py +++ b/app/payment/zpay.py @@ -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") or result.get("pay_price") or "" + return {"paid": status == 1, "status": status, "pay_price": str(money) if money else ""} diff --git a/app/routers/__pycache__/__init__.cpython-312.pyc b/app/routers/__pycache__/__init__.cpython-312.pyc index 1848da243771a58eb071101a4603e09982cf66b9..62186f9d551d713d691843336472fbeda8dde5ab 100644 GIT binary patch delta 60 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 Bd_DjG delta 3664 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 T>qqNyJhkt@O-U(Mu?YVLB8z-C diff --git a/app/routers/__pycache__/cnomadcna.cpython-312.pyc b/app/routers/__pycache__/cnomadcna.cpython-312.pyc index 43305c6882fa8d2fd3f36a44af92b1f809de7665..e204276351d351569d94ee385018f47449529729 100644 GIT binary patch delta 58 zcmeyx^n;1}G%qg~0}zOmZraGr$*8E~Y!y>bl%JKFToO}|SeaFvSdbZ$SWpmClwVqs MT2ve}*__cC01LenLjV8( delta 60 zcmeyt^oxo6G%qg~0}vG2Z`sJr$*8QSUtV4g!bSP{CHe)4m087!1)2JZ1qJ#=`K2YP LMaBA)O&Og5JBkz5 diff --git a/app/routers/__pycache__/common.cpython-312.pyc b/app/routers/__pycache__/common.cpython-312.pyc index 8592782e8446d1e40abf95742da938c0e69e2729..f6ebe17b2ca2cfed62e43a4f13cb0c17c32ed85e 100644 GIT binary patch delta 59 zcmX>lcS4T)G%qg~0}#AExp^b^Ja$D*XRDZkqWrAXhcS?@?G%qg~0}vG2Z`sH_k6l??zr4I0gp2a?OY{p8E3=9d3o`W+3kvj$@=Hrn Ni;DF(pJK0L2LNK$6+-|3 diff --git a/app/routers/__pycache__/digital.cpython-312.pyc b/app/routers/__pycache__/digital.cpython-312.pyc index eb292c012086b3c52ac76b621c4b114d23ffd11f..f9cd87415ded1f2413f98cd92ac4e23f673c5c10 100644 GIT binary patch delta 57 zcmaFC^o)u7G%qg~0}u$7ZkovbQ&G#=DyE<)KPxr4B&Hy-GOIYTATuVhpdh9wzqBN^ Ls5oY_I-?^19W@iz delta 59 zcmaFH^n!`|G%qg~0}vG2Z<)ybQ&~s9yu2KQi}LeJ^a~Ozvx*Z7GW8P+3iONeOG{FV LiuEU}GCBePPOlVg diff --git a/app/routers/__pycache__/legacy.cpython-312.pyc b/app/routers/__pycache__/legacy.cpython-312.pyc index e36aa3a6fcb69e13f716215b75694774f782e19e..34c4906edd5ce76e183c1025ce999831ee38ad17 100644 GIT binary patch delta 58 zcmeyz^oNQ2G%qg~0}y0AT)&Z9m{C#F*(#=>C_gJTxg@3_u`;VTu^=-hv7jKPD8IBM MwWv5|vLmA{059tl6#xJL delta 60 zcmeyv^pA=AG%qg~0}vG2Z`sH#%&4rbUtV4g!bSP{CHe)4m087!1)2JZ1qJ#=`K2YP LMaBA)?HO$WKB*Jw diff --git a/app/routers/__pycache__/nomadlms.cpython-312.pyc b/app/routers/__pycache__/nomadlms.cpython-312.pyc index 4b25e44813ca817e20f673832c0ced1affb7599b..07b217057c44127f1a2e8d5faa96599a2cea80cb 100644 GIT binary patch delta 57 zcmcc5beDRZKxqepYI7NlZavWma)wL1s*1K|xGWerZW+ LQE|*<1x9-SINTI= delta 59 zcmcc1bf1a)G%qg~0}vG2Z<)ybNm*CFyu2KQi}LeJ^a~Ozvx*Z7GW8P+3iONeOG{FV LiuEVUG1>zFNsJUs diff --git a/app/routers/__pycache__/nomadvip.cpython-312.pyc b/app/routers/__pycache__/nomadvip.cpython-312.pyc index 1fe4d2b639a323a0756aef901f5510bccabe9ff8..d5bd854d2b7fa437f2950325c094a9e728ba92ad 100644 GIT binary patch delta 61 zcmeC4&eS=biTgA!FBbz4Ts*&dBX=E}qPDYDOhHk8R%&udOhIC0R&ioMW=vv1K}=D8 PX-R5Pam?m5Z1-CLeC!t9 delta 63 zcmeC2&eT1fiTgA!FBbz46xwgu$X&;#tgByMUJk-V`S~UK1&Nhe#fb%(`iTVv`bGJr PC8 dict | None: + 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) + return None - 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) + def _query_xorpay() -> dict | None: + try: + xorpay = get_payment_provider_by_name("xorpay") + r = xorpay.query_order_status(order_id, timeout=6) + if r.get("paid"): + return {"paid": True, "source": "xorpay", "status": r.get("status", "")} + except Exception as error: + _safe_log(logging.WARNING, "%s order_status XorPay failed: %s", project_name, error) + return None - 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) + def _query_zpay() -> dict | None: + try: + zpay = get_payment_provider_by_name("zpay") + if isinstance(zpay, ZPayProvider): + r = zpay.query_order_status(order_id, timeout=6) + if r.get("paid"): + return {"paid": True, "source": "zpay", "status": r.get("status", "")} + except Exception as error: + _safe_log(logging.WARNING, "%s order_status ZPAY failed: %s", project_name, error) + return None + # 并行查询 PB、ZPAY、XorPay,任一返回 paid 即立即返回(PC 扫码用 ZPAY,微信用 XorPay) + pb_task = asyncio.to_thread(_query_pb) + xorpay_task = asyncio.to_thread(_query_xorpay) + zpay_task = asyncio.to_thread(_query_zpay) + results = await asyncio.gather(pb_task, xorpay_task, zpay_task) + for r in results: + if r: + return r return {"paid": False} @router.post("/xorpay_notify") diff --git a/app/routers/meetup.py b/app/routers/meetup.py index 6bd2aba..de1fe8f 100644 --- a/app/routers/meetup.py +++ b/app/routers/meetup.py @@ -193,6 +193,7 @@ async def ensure_user(item: EnsureUserRequest): "email": email, "password": DEFAULT_PASSWORD, "passwordConfirm": DEFAULT_PASSWORD, + "live_allowed": True, }, headers={ "Content-Type": "application/json", diff --git a/app/routers/salon.py b/app/routers/salon.py new file mode 100644 index 0000000..2c7d95e --- /dev/null +++ b/app/routers/salon.py @@ -0,0 +1,4 @@ +"""salon 项目接口:克隆 cnomadcna 的支付与 meetup 流程,使用 site_id=salon""" +from ._digital_base import create_digital_router + +router = create_digital_router(prefix="/salon", project_name="salon", site_id="salon") diff --git a/app/routers/salon_meetup.py b/app/routers/salon_meetup.py new file mode 100644 index 0000000..6c3e08e --- /dev/null +++ b/app/routers/salon_meetup.py @@ -0,0 +1,502 @@ +""" +salon 加入流程:check-user、ensure-user、complete-order +克隆 meetup 逻辑,使用 site_id=salon +""" +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 + +SITE_ID = "salon" +router = APIRouter(prefix="/api/salon", tags=["salon"]) +DEFAULT_PASSWORD = "12345678" + +_SALON_ADMIN_TOKEN_CACHE: tuple[str, float] | None = None +_TOKEN_CACHE_TTL = 300 + + +def _get_admin_token() -> tuple[str | None, str | None]: + global _SALON_ADMIN_TOKEN_CACHE + now = time.time() + if _SALON_ADMIN_TOKEN_CACHE and _SALON_ADMIN_TOKEN_CACHE[1] > now: + return _SALON_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("/") + 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: + _SALON_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, token: str | None = None) -> bool: + if not token: + token, _ = _get_admin_token() + if not token: + return False + try: + 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): + 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, 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"salon check-user error: {e}") + raise HTTPException(status_code=500, detail="操作失败") + + +@router.post("/ensure-user") +async def ensure_user(item: EnsureUserRequest): + 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, + "live_allowed": True, + }, + 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"salon ensure-user create failed: status={create_r.status_code}, msg={msg}") + 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"salon 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: + 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"salon complete-order zpay query failed: {e}") + return False + + +@router.post("/complete-order") +async def complete_order(item: dict): + 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 != "salon": + raise HTTPException(status_code=400, detail="订单格式无效") + + 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}" && site_id = "{SITE_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: + paid_via = None + pay_price = "" + if _query_zpay_paid(order_id): + paid_via = "zpay" + try: + from ..payment.zpay import ZPayProvider + from ..payment import get_payment_provider + p = get_payment_provider() + if isinstance(p, ZPayProvider): + r = p.query_order_status(order_id, timeout=8) + if r.get("paid"): + pay_price = str(r.get("pay_price", "") or r.get("money", "")) + except Exception: + pass + if not paid_via: + try: + from ..payment.xorpay import XorPayProvider + from ..payment import get_payment_provider + p = get_payment_provider() + if isinstance(p, XorPayProvider): + r = p.query_order_status(order_id, timeout=8) + if r.get("paid"): + paid_via = "xorpay" + pay_price = str(r.get("pay_price", "")) + except Exception: + pass + if paid_via: + from ..services.payment import handle_payment_success + # pay_price 为空时用默认茶歇费 1 元 + 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}") + if paid_via: + 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="订单不存在或未支付成功") + + 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, + } + + return {"ok": True, "token": None, "record": None, "is_new": False} + + +@router.get("/vip/check") +async def vip_check(user_id: str = ""): + """检查用户是否为 salon VIP""" + if not user_id: + return {"vip": False} + token, _ = _get_admin_token() + return {"vip": _check_site_vip(user_id, token)} + + +@router.get("/join/by-wechat") +async def join_by_wechat(wechat_id: str = ""): + """按微信号查询 solanRed 记录,供 salon 前端提交后刷新状态""" + if not wechat_id or not wechat_id.strip(): + return {"hasRecord": False, "record": None, "isComplete": False, "isWechatFriend": False, "isApproved": False, "isRejected": False, "vip": False} + try: + from ..services import get_pb_client + pb_client = get_pb_client() + solan = pb_client.collection("solanRed") + safe_wechat = wechat_id.strip().replace("\\", "\\\\").replace('"', '\\"') + existing = solan.get_list(1, 1, {"filter": f'wechatId = "{safe_wechat}"', "sort": "-created"}) + if not existing.items: + return {"hasRecord": False, "record": None, "isComplete": False, "isWechatFriend": False, "isApproved": False, "isRejected": False, "vip": False} + rec = existing.items[0] + amount = rec.get("amount") or 0 + is_complete = bool(rec.get("order_id")) and (amount or 0) > 0 + return { + "hasRecord": True, + "record": rec, + "isComplete": is_complete, + "isWechatFriend": bool(rec.get("is_wechat_friend")), + "isApproved": bool(rec.get("is_approved")), + "isRejected": bool(rec.get("is_rejected")), + "vip": bool(rec.get("vip")), + } + except Exception as e: + logging.warning("salon join/by-wechat error: %s", e) + return {"hasRecord": False, "record": None, "isComplete": False, "isWechatFriend": False, "isApproved": False, "isRejected": False, "vip": False} + + +@router.get("/join/status") +async def join_status(user_id: str = ""): + """检查用户是否已提交 salon 加入申请""" + if not user_id: + return {"hasRecord": False, "isComplete": False} + try: + from ..services import get_pb_client + pb_client = get_pb_client() + solan = pb_client.collection("solanRed") + existing = solan.get_list(1, 1, {"filter": f'user_id = "{user_id}"', "sort": "-created"}) + has_record = len(existing.items) > 0 + rec = existing.items[0] if existing.items else None + is_complete = bool(rec and rec.get("order_id") and rec.get("amount", 0) > 0) + return {"hasRecord": has_record, "isComplete": is_complete} + except Exception: + return {"hasRecord": False, "isComplete": False} + + +def _validate_xiaohongshu_url(url: str) -> dict | None: + """校验小红书主页 URL 并尝试爬取用户名/头像/粉丝数/性别。返回 None 表示校验失败。""" + url = (url or "").strip() + if not url: + return None + if "xiaohongshu.com" not in url.lower(): + return None + if not url.startswith("http://") and not url.startswith("https://"): + return None + try: + r = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}) + if r.status_code != 200: + return None + html = r.text + nickname = "" + avatar = "" + followers = 0 + gender = "" + import re + if '"nickname"' in html or '"user"' in html: + m = re.search(r'"nickname"\s*:\s*"([^"]+)"', html) + if m: + nickname = m.group(1) + m = re.search(r'"avatar"\s*:\s*"([^"]+)"', html) + if m: + avatar = m.group(1).replace("\\u002F", "/") + m = re.search(r'"follows"\s*:\s*(\d+)', html) + if m: + followers = int(m.group(1)) + if not nickname and '"desc"' in html: + m = re.search(r'"desc"\s*:\s*"([^"]+)"', html) + if m: + nickname = m.group(1)[:20] + return {"nickname": nickname or "用户", "avatar": avatar, "followers": followers, "gender": gender, "url": url} + except Exception as e: + logging.warning(f"xiaohongshu crawl failed: {e}") + return {"nickname": "用户", "avatar": "", "followers": 0, "gender": "", "url": url} + + +@router.post("/join") +async def submit_join(item: dict): + """salon 加入社区报名表单,写入 solanRed 集合。需提供 xiaohongshu_url,提交前校验并爬取信息。""" + user_id = item.get("user_id") + if not user_id: + raise HTTPException(status_code=400, detail="missing user_id") + + xiaohongshu_url = (item.get("xiaohongshu_url") or "").strip() + if not xiaohongshu_url: + raise HTTPException(status_code=400, detail="请填写小红书主页地址") + + profile = _validate_xiaohongshu_url(xiaohongshu_url) + if not profile: + raise HTTPException(status_code=400, detail="小红书主页地址无效或无法访问,请检查后重试") + + try: + from ..services import get_pb_client + pb_client = get_pb_client() + solan = pb_client.collection("solanRed") + wechat_id = (item.get("wechatId") or "").strip() + if wechat_id: + safe_wechat = wechat_id.replace("\\", "\\\\").replace('"', '\\"') + existing = solan.get_list(1, 1, {"filter": f'wechatId = "{safe_wechat}"'}) + if existing.items: + raise HTTPException(status_code=400, detail="该微信号已报名,请勿重复提交") + pb_data = { + "user_id": user_id, + "nickname": item.get("nickname", ""), + "occupation": item.get("occupation", ""), + "reason": item.get("reason", ""), + "wechatId": item.get("wechatId", ""), + "gender": profile.get("gender", "") or item.get("gender", ""), + "education": item.get("education", ""), + "gradYear": item.get("gradYear", ""), + "phone": item.get("phone", ""), + "single": item.get("single", ""), + "media": item.get("media", ""), + "calculated_age": item.get("calculated_age", 0), + "type": item.get("type", ""), + "order_id": "", + "amount": 0, + "xiaohongshu_url": xiaohongshu_url, + "xiaohongshu_nickname": profile.get("nickname", ""), + "xiaohongshu_avatar": profile.get("avatar", ""), + "xiaohongshu_followers": profile.get("followers", 0), + } + record = solan.create(pb_data) + logging.info(f"salon 申请表单提交成功 - user_id: {user_id}, record_id: {record.id}") + return {"ok": True, "user_id": user_id, "record_id": record.id} + except Exception as e: + logging.error(f"salon join 提交失败: {e}") + raise HTTPException(status_code=500, detail="submit failed") diff --git a/app/sdk/__pycache__/__init__.cpython-312.pyc b/app/sdk/__pycache__/__init__.cpython-312.pyc index ecf6064160e748ec04c0c00f15b129943356891e..64d5f8c6104ed2d3bf326d587f4d64bc0c334870 100644 GIT binary patch delta 54 zcmeyw@_~i>G%qg~0}vQKTECI|FQcrwvsFw%QGQlxa!E`Ym?(X&CAQh00imwn>TW+Gs&trTg4O<yYC<&CAQh00f2hTQ+j5Gs&yzmzS4=a8Z7KiGD$1Wma)wL8g9UL4kg8O1A!H If2Qp`0Nbe&CIA2c diff --git a/app/sdk/__pycache__/minio.cpython-312.pyc b/app/sdk/__pycache__/minio.cpython-312.pyc index 9c2d77606416070dff9001174260db39191769ad..26baa0994b05177e81af317f08267ee3d692ec73 100644 GIT binary patch delta 55 zcmeya@Ku5PG%qg~0}xz#w0+T9smz36BGad delta 57 zcmeyW@LhrXG%qg~0}vG2Z`sJL%_6U+UtV4g!bSP{CHe)4m087!1)2JZ1qJ%WDcSm) IgISb#01WsNe*gdg diff --git a/app/sdk/__pycache__/pocketbase.cpython-312.pyc b/app/sdk/__pycache__/pocketbase.cpython-312.pyc index 73352627b5f0b9495bad36885cfa96f97b1436f0..29462967d0d84f1e81a0b5b0df2d75a5509be3b5 100644 GIT binary patch delta 737 zcmca9wp)VlG%qg~0}#~d?8vm|*vKc(B&+Fc6;n`@pOut>^nN^%vkQtL$P!Lm` zk{vVIhpCoHi*fR4CTWqH6s8o$9L8G45>}8AAW$>efLU}hJF~VZ2Sfmw%;KIL%dC-5 z!;r;?EDT|We_`F9unmFj<1dP&gCFYG8Q4&EId+X>);F z{x*-)JT_io(j#ZXXadHgn4Qm;YCYF-Qg47~8keob_0BV#539}^^r6!i7 zYO+Ec2~t`F3IeFg$@y#!jH;VAupMQTP(}!YSV};`Pm>L3Q!j_2j0%va$&O?Q+@>P+ z$)`9{8TBR`aL#8f5(BE6e3R2y8srm*6PPpeN^Y^FWG0ssse%NJC#!RL@vyVX%<%fm z0Hi($O|Iq2Wi*@onoC{E3Z#M`M1Z{ci^C>2KczG$)vm~PvJSTbuPr10Ck7w^RssOC CV73eZ delta 544 zcmdljaZ`-%G%qg~0}vG2Z^>N4zL8I!NnTsOyu2KQi}LeJ^a~Ozvx*Z7GW8P+3iOLp zvh^o>G1ao(0&1B)S%F!cuOXy_8Dszm)G$m=;1HS2%dD-<1`$CfvpA7?5Oxhi7T087 zW{t_+%-oZem_^xw88jJ7)hGXC)?(LWFX9HOn%v4_z@y1n!~O>#&f4N30+hJLm7JfGlbT$TnV+Z01lCz33ewI5Boq{ic!5-r zIFP842*^*)PAzdtEKUt}@zz6Vlb`I&cEef%sOA<+No7H5krYUeG>`xq2-13sEx9N) zu_RTK6=FU}X^|v|fT)}t#_qtVym>nNQAP=QgfNID2PFJ7*^0y`mvAb|C;)jtuY$Z+ z1U3Y2Q<3uIjhv~BI+Mk@=5yWRh>y=p%uS7tpS+vPSQ_L_h&z}w^Ga^9q+}+S6e)rP z^(S+3d-1Tc%FOWk%mAc5@J){A&Sf;7e3n~X${eJEA4Gt{;1`EYZhlH>PO4pz)nq;% P1zu}L{!a`*0;~i8{knB% diff --git a/app/sdk/payment/__pycache__/__init__.cpython-312.pyc b/app/sdk/payment/__pycache__/__init__.cpython-312.pyc index 218827e0936012e4b3f218bcac11fe2f8d57905c..c74dc619d2ffe2137de1e29173bf4c5cb34ae820 100644 GIT binary patch delta 62 zcmZo<`OnOKnwOW00SK%gt>4Jq!KkY5Y!y>bl%JKFToO}|SeaFvSdbZ$SWpmCoRSR^ O$W6^FiJ82M(H#JGZ58?e delta 64 zcmey*+{nUxnwOW00SF51w`}C@U{p8MFE1|#;iCNf68(b2%B PocketBase: - """获取已认证的 PocketBase 客户端""" + """获取已认证的 PocketBase 客户端(首次登录时认证,后续复用)""" if self._client is None: + import logging self._client = PocketBase(self.url) self._client.admins.auth_with_password(self.admin_email, self.admin_password) + logging.getLogger(__name__).info("PocketBase admin 登录成功") return self._client def collection(self, name: str): diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc index 89f9b902875c5a4f12c971e5aa966d00a15a2b31..08b55070a9635b46996f5fbbf9701ddb717a55b1 100644 GIT binary patch delta 59 zcmbQtGLeP*G%qg~0}ymQTECHdE~Ao;vsFw%QGQlxa!E`TVFVpP&}wu&hz%FjwoE{Q2ftjsD-EXa&WEGUR6PAw|S OOinG1+5C}Fk_iAPU=&0E delta 62 zcmeC+=;Gi$&CAQh00f2hTQ+hZVpP%AFE1|#;iCNf68(b2%Bi{nE+Gp6uSTb diff --git a/app/services/__pycache__/payment.cpython-312.pyc b/app/services/__pycache__/payment.cpython-312.pyc index d6c03064d7825454a1545fced85ce3220893f139..8464f9ffcb7c4d751934e0d8170854a941f1da29 100644 GIT binary patch delta 1787 zcmaJ>S!@$W7~b)Ey}r-&eXSF6IAB7eKtw`FNgyeOl5mD2#Wfi#I2hYqC!ioxiiFw% z4-NfW6^VrM2%;55ekc{{0~JE4K*Xelx(10ug{o2?j2el)RO-xzq@t?2(){zye||gv z%-mmnYn}0?!JuWx_s-+%2~*`|V+C8Wu(PEr)=)unGKp2zHav&Igedj_^Bzwj6VY!NLTK?btz5I_C-`5J~TX7kmim z!L45Ibq_KZ74#1n<}jb*B_YRSkDIK9Eb!c7BV~`O!^jIqR1P&s>B}jHeAK`qtcQn$ zmme&eciAmrC7jZT#*#jnI1rDz}(G7j3`t@xTuB4)! zid9s|jvW+L5)o#l*U3~;W@5v8rL_cRSt3MHlw4$%R?LYv^V23JQuWgHMtER$E3E4L z3-d9g&8e_XZkG#=T7q_cj-xHUh=?co5HrYN101nNbjCRy>E+kf2+G4X!HLZ9mo;Ru zU<>F3iGV%KVn@Q^AEDsGQ z`v=BZXmf;75S9vIBtlg1%l{n074jJ-f1N!N6AH1@NulC4N5(OAYFIvI?vN zgS`?}L4!SHqk9&5xog!qGu=Cg+czU4d}H_WHMn+;BOjJI)`7-R&(`3DbX)jEFLnDJ z`%wh$J9d$C+UyLW`uq;(L$sm9Wn;;7YGeWFp^U^|FJFCn_T;lqF8y-#^s^7go}Tzz zjEE!Twnr1VMGSM&HsV@R?n$TjNn0u84kU0qE%gx$8{G@6%Lq86jtofg*l-+^GusRw zxZ}FFh+5K9u^V2vTLam2GM3sTJIrc{X{3$_nFf62uJV#o>-{H++Lz4RInOfG3@be1 zD~Sg^@9Q?shs5De&p8#ni_&U1>upoDP}m5Mz3%EY6w=48#KtfV3$xTVQSD|bUfuxF z*Nt|;Uf)uL;jC|+?hvu@4IVGJ~G>>u^6l_&IaH8D%Fj|PYI;@dRM^#v*aG4R4m(SHG>X#$(WtbV zZs1BNiY;xO1eQ$EXe^aV566ZR=~R|DyhgR$@JJ>Zm+0#((YI2P8%19t`E(_vL(miQ zpkpu;$~vo%`JAoHkmk delta 1371 zcmYjQTW=dh6yEXLyI!wv>-9Bue2Y`ZN{k64MQO>cj$A5RkQNFpL0H?YDNY;P%sMqK zPC^iZA|V7i;)N!nyr5R)fy(@VNJv${k=i^|s@#Vn#A_=TB9#{;W;P9CrTOMNw>f9$ zoY~IbpRu=XHVZ?(_a=WT+rl^PaW;|8G#U-MaJ5>`)Rc=0no=uglv*vLmGFh~d`Zi+ zPOuFFnu1?VU%S^?l~eg9y25N2RP(w)6@IY55*HK1a{~;6BTXa3%)`hEJ6ue(>AGJ8 zM%RXn%O=$hy?hKgV3Lo(1RsM{{&d9EWUer(yuqq&)kBW-?k6ZCa=Z_okO&b1=CPpv zEOWy^|A1kZ`6iFuOX>VarZuWq?Kt*Xwpw|oj2B^3xb7JwJ}I`t)fB8OYWNu<;tZ@@Z1`Ka=EkXJi8P780uG+4& zXuW}mm3*oVSUHVu!m#W^b~q--B!}uyolO(DV~MoQrONPu+=ue;E%C6zs?QI%>_lx=>b2 zRVV}#u2CXiGN} zFcceM?>gro8E+akldT`)S@inP$(exy$80%|JeT(~ZG(phZNE@p*>=pIpJ3Z#CaRe* z5zRKs628p|g&}s^;V&fF?WBqDl&`}Vi)wYgSj1Uc-#8pg92!4DXva}3DwRsLuGGub zibfnJsn%3qs+})k`uZ__e^|c@I^cS9&f^AbCVJ2s+)rq}1ae-LyA0W`$=B-Fd;X%d z`!baJlT%fM?DykyOqE4-hUHWq5vp08bRKz_b-M=1Q BTi^fy diff --git a/app/services/__pycache__/pb.cpython-312.pyc b/app/services/__pycache__/pb.cpython-312.pyc index 66a33e9ace0dba7617ff9f955d18c978686653d3..e9372ea0aa531ca16a911451ed98ac28642b75bd 100644 GIT binary patch delta 491 zcmeC=yv@#gnwOW00SK1s?8wZV$SWBV0OU+(2xUlNh+;@#Okv7l%w>vV%4LpX&Si;W zVFZaW=dk9oMX>?dEQ#!?Y$+^N>PO4pzBv1n6pkhNH@qw9a!R#qYT(c0JPwgu>b%7 delta 579 zcmZ8eL2DC16n<}Zvzy(xFVhHEMMJE|j2K3mSd;3qAtMlASWQK3{smKB z;poIrDX|hvreswHUxGB3X<;XBwp#hJm3BGzHnS`x7!h2 z$7$(JYo+R0O9h(j8jAg8D9VM`p@15~-~|DgDH zJcKp821T+w5!@oB`Q;ohz3P;gSnHzYvAjj=b|b&{{O!)Wr@NiaokuTa~Zx$b9xgz`0!Ib`uLQ8X`rQ^ zENm?FwVXfmRa76&KA8QK(m#ungTx_!&Q0>#;=xchP3n|QGn6AQyHN>B!c-)^DA}Hp n-8$1(3cfVR7ks&s+d9%ya(yW5OK}|Ayg3A(hUpj{{xi~V^Ej0q diff --git a/app/services/payment.py b/app/services/payment.py index e1af7e2..29b07dc 100644 --- a/app/services/payment.py +++ b/app/services/payment.py @@ -46,6 +46,7 @@ def _create_user_by_email(email: str) -> str | None: "email": email, "password": DEFAULT_PASSWORD, "passwordConfirm": DEFAULT_PASSWORD, + "live_allowed": True, }, headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"}, timeout=10, @@ -131,8 +132,10 @@ def handle_payment_success( if pay_type == "meetup": logging.info(f"meetup 类型支付成功,不创建 memos 用户 - user_id: {user_id}") + if pay_type == "salon": + logging.info(f"salon 类型支付成功 - user_id: {user_id}") - valid_types = ["vip", "book", "meetup", "video"] + valid_types = ["vip", "book", "meetup", "video", "salon"] if pay_type not in valid_types: pay_type = "vip" @@ -162,6 +165,26 @@ def handle_payment_success( "amount": amount_cents, }) logging.info(f"meetup 完整记录创建(兜底) - user_id: {user_id}") + elif pay_type == "salon": + solan = pb_client.collection("solanRed") + 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, { + "order_id": order_id, + "amount": amount_cents, + }) + logging.info(f"salon 支付字段补齐成功 - user_id: {user_id}, record_id: {record_id}") + else: + solan.create({ + "user_id": user_id, + "order_id": order_id, + "amount": amount_cents, + }) + logging.info(f"salon 完整记录创建(兜底) - user_id: {user_id}") else: existing = pb_client.collection("payments").get_list(1, 1, { "filter": f'order_id = "{order_id}"', @@ -177,8 +200,8 @@ def handle_payment_success( } pb_client.collection("payments").create(payload) - # 写入 site_vip(站点独立 VIP)。meetup 类型统一写 site_id=meetup - effective_site_id = "meetup" if pay_type == "meetup" else site_id + # 写入 site_vip(站点独立 VIP)。meetup 写 site_id=meetup,salon 写 site_id=salon + effective_site_id = "meetup" if pay_type == "meetup" else ("salon" if pay_type == "salon" else site_id) if effective_site_id: try: site_vip = pb_client.collection("site_vip") diff --git a/app/services/pb.py b/app/services/pb.py index 0772f0b..507e23f 100644 --- a/app/services/pb.py +++ b/app/services/pb.py @@ -1,10 +1,7 @@ """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 @@ -20,8 +17,6 @@ def _get_pb_sdk() -> PocketBaseSDK: def get_pb_client(): - """获取已认证的 PocketBase 客户端""" + """获取已认证的 PocketBase 客户端(复用已登录实例,不重复打日志)""" sdk = _get_pb_sdk() - client = sdk.get_client() - logger.info("PocketBase admin 登录成功") - return client + return sdk.get_client()