0722d7b0d5
- 福利钱包: 金币/现金余额、流水、兑换 (api/v1/wallet.py, crud/wallet.py, models/wallet.py, schemas/welfare.py) - 每日签到: 连续天数 + 档位奖励 (api/v1/signin.py, crud/signin.py, models/signin.py) - 任务系统: "打开消息提醒"等任务领奖 (api/v1/tasks.py, crud/task.py, models/task.py) - 省钱战绩: 省钱汇总/战绩/店铺菜品 (api/v1/savings.py, crud/savings.py, models/savings.py) - 激励广告发奖: 穿山甲服务端回调 + 发奖规则 + 限流 (api/v1/ad.py, core/pangle.py, core/rewards.py, core/ratelimit.py, crud/ad_reward.py, schemas/ad.py, docs/ad_reward_golive_checklist.md) - 微信提现: 商家转账到零钱 V3 (core/wxpay.py); user 表加微信 openid/nickname/avatar - DB 迁移: 8 个 alembic (welfare 表/cash_transaction/openid 唯一约束/savings 店铺菜品/savings_record/ad_reward/withdraw_order+openid/user 微信字段) - 运维脚本: reconcile_withdraws(对账) + reset_signin/reset_welfare + sim_pangle_callback(模拟穿山甲回调) - 测试: test_welfare / test_withdraw / test_ad_reward - 配置: .env.example + config.py 新增福利/广告/微信支付项; main.py 挂 5 个新路由 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
126 lines
4.8 KiB
Python
126 lines
4.8 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 =====
|
|
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"
|
|
# ===== 微信支付(商家转账到零钱 / 提现)=====
|
|
# 真实凭证放 .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
|
|
|
|
# ===== 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()
|