"""全局配置。 用 pydantic-settings 从环境变量 + .env 文件加载。所有可调参数集中在这里, 业务代码通过 `from app.core.config import settings` 引用,不再读 os.environ。 """ from __future__ import annotations from functools import lru_cache from pathlib import Path from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=str(_PROJECT_ROOT / ".env"), env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # ===== 环境 ===== APP_ENV: Literal["dev", "prod"] = "dev" APP_NAME: str = "shaguabijia-app-server" APP_DEBUG: bool = True # 接口限流开关(测试里关掉,避免内存计数跨用例累加误伤) RATE_LIMIT_ENABLED: bool = True # ===== 数据库 ===== DATABASE_URL: str = "sqlite:///./data/app.db" # ===== JWT ===== JWT_SECRET_KEY: str = "change-me" JWT_ALGORITHM: str = "HS256" JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 120 JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 30 # ===== 极光 ===== JG_APP_KEY: str = "" JG_MASTER_SECRET: str = "" JG_PRIVATE_KEY_PATH: str = "./secrets/jverify_rsa_private.pem" JG_VERIFY_ENDPOINT: str = "https://api.verification.jpush.cn/v1/web/loginTokenVerify" JG_REQUEST_TIMEOUT_SEC: int = 15 # ===== 短信 ===== SMS_MOCK: bool = True SMS_CODE_TTL_SEC: int = 300 SMS_SEND_INTERVAL_SEC: int = 60 # ===== 美团联盟 CPS ===== # 未配置时所有 /api/v1/meituan/* 接口 200 返空(优雅降级),不影响登录/领券等其他业务。 MT_CPS_APP_KEY: str = "" MT_CPS_APP_SECRET: str = "" MT_CPS_HOST: str = "https://media.meituan.com" MT_CPS_TIMEOUT_SEC: int = 15 MT_CPS_DEFAULT_SID: str = "sgbjia" @property def mt_cps_configured(self) -> bool: """美团 CPS 凭证齐全(缺则接口返空,而非 502)。""" return bool(self.MT_CPS_APP_KEY and self.MT_CPS_APP_SECRET) # ===== 微信支付(商家转账到零钱 / 提现)===== # 真实凭证放 .env(已 gitignore),证书 .pem 放 secrets/。WECHAT_APP_ID 同时用于 # 微信登录(code 换 openid)与转账,必须与 App 端开放平台 appid 一致。 WECHAT_APP_ID: str = "" # 开放平台移动应用 appid(wx 开头) WECHAT_APP_SECRET: str = "" # 开放平台 AppSecret,code 换 openid 用 WXPAY_MCH_ID: str = "" # 微信支付商户号 WXPAY_MCH_SERIAL_NO: str = "" # 商户 API 证书序列号 WXPAY_MCH_PRIVATE_KEY_PATH: str = "./secrets/apiclient_key.pem" # 商户 API 私钥 WXPAY_PUBLIC_KEY_ID: str = "" # 微信支付平台公钥 ID(PUB_KEY_ID_...) WXPAY_PUBLIC_KEY_PATH: str = "./secrets/pub_key.pem" # 微信支付平台公钥 WXPAY_TRANSFER_SCENE_ID: str = "1000" # 转账场景 ID(1000=现金营销) WXPAY_REQUEST_TIMEOUT_SEC: int = 10 @property def wxpay_configured(self) -> bool: """凭证是否齐全(缺则提现接口拒绝调用,而非启动时炸)。""" return bool( self.WECHAT_APP_ID and self.WXPAY_MCH_ID and self.WXPAY_MCH_SERIAL_NO and self.WXPAY_PUBLIC_KEY_ID ) # ===== 穿山甲激励视频(服务端发奖回调)===== # 看完激励视频后穿山甲服务器回调本服务发金币(S2S,客户端被破解也刷不到)。 # PANGLE_REWARD_SECRET 是穿山甲后台配置的"奖励校验密钥",验签用,从后台取到后填 .env。 PANGLE_CALLBACK_ENABLED: bool = False PANGLE_REWARD_SECRET: str = "" # ⚠️ 仅本地联调:打开后开放 POST /api/v1/ad/test-grant,让(已登录的)客户端在没部署公网、 # 穿山甲 S2S 回调打不到本地时,直接触发一次发奖,验证"看广告→金币到账"全链路。 # 它让客户端能自助发奖 = 绕过反作弊,**生产必须保持 False**(默认 False;只在本地 .env 设 true)。 AD_REWARD_TEST_GRANT_ENABLED: bool = False @property def pangle_callback_configured(self) -> bool: """回调开关打开且验签密钥已配,才接受发奖回调。""" return bool(self.PANGLE_CALLBACK_ENABLED and self.PANGLE_REWARD_SECRET) # ===== Pricebot 上游 (领券业务透传目标) ===== # pricebot-backend 默认跑在 8000。/api/v1/coupon/step 会透传到这里的 /api/coupon/step PRICEBOT_BASE_URL: str = "http://localhost:8000" # 领券一帧最多 wait 6s,加网络往返,timeout 给 30s 比较稳 PRICEBOT_REQUEST_TIMEOUT_SEC: int = 30 # 比价(intent/recognize + price/step)透传超时:意图识别是大上下文 LLM、 # price/step 每帧也是 LLM,可能 >30s;对齐客户端 agent ApiClient 的 60s 读超时。 PRICEBOT_COMPARE_TIMEOUT_SEC: int = 60 # ===== 媒体文件(用户头像上传)===== # 落盘根目录(data/ 已 gitignore,上传不进库);对外经 StaticFiles 挂在 MEDIA_URL_PREFIX。 # 生产可改由 nginx 直接 serve MEDIA_ROOT,绕过应用进程。 MEDIA_ROOT: str = "./data/media" MEDIA_URL_PREFIX: str = "/media" AVATAR_MAX_BYTES: int = 5 * 1024 * 1024 # 头像最大 5MB # ===== CORS ===== CORS_ALLOW_ORIGINS: str = "" @property def cors_origins_list(self) -> list[str]: if not self.CORS_ALLOW_ORIGINS.strip(): return [] return [o.strip() for o in self.CORS_ALLOW_ORIGINS.split(",") if o.strip()] @property def is_prod(self) -> bool: return self.APP_ENV == "prod" @lru_cache(maxsize=1) def get_settings() -> Settings: return Settings() settings = get_settings()