4d444b7c43
Reviewed-on: #4
143 lines
5.7 KiB
Python
143 lines
5.7 KiB
Python
"""全局配置。
|
|
|
|
用 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()
|