ed76820e97
读完了两份 spec + 分派器,这分支的全貌清晰了。下面是可直接粘进 Gitea PR 的 MR 说明(标题 + 正文): 建议标题 feat(sms): 短信验证码可切换多 provider(极光/阿里云/创蓝),默认极光零改动 MR 正文(复制以下 markdown): ## 概述 把原单文件 `app/integrations/sms.py` 重构为**可切换 provider 包** `app/integrations/sms/`,在保留极光(默认、行为零改动)的基础上,新增两家验证码短信 provider: - **阿里云号码认证 dypns**(Mode A:阿里云生成/存储/校验验证码,核验免费) - **创蓝云智 253**(Mode B:本服务自管码,httpx 直连 + HMAC 签名) Provider 由 `SMS_PROVIDER` 按调用实时选择,默认 `jiguang`。短信=花钱 + 登录关键路径,故新 provider **opt-in、可灰度、秒级回退**,默认路径零变更。 ## 为什么 现有极光路径本地内存存码(多 worker 不共享,已是技术债),且单一供应商无法灰度/切换。引入 provider 抽象后:阿里云托管码可消除存码债,创蓝作为备选降低单点依赖,三家随配置切换与回退。 ## 改动内容 **架构(`app/integrations/sms/`)** | 文件 | 说明 | |---|---| | `__init__.py` | 对外仍暴露 `send_code/verify_code/SmsError`(auth 导入不变);按 `SMS_PROVIDER` **每次调用**分派;未知值回退 `jiguang` | | `base.py` | `SmsError`(status_code→HTTP) + provider 无关的 `mock_verify` | | `jiguang.py` | 原 `sms.py` 逻辑**原样迁入**,行为零改动(git 识别为 rename) | | `aliyun.py` | 新增,Mode A:`SendSmsVerifyCode` + `CheckSmsVerifyCode`,惰性加载 SDK | | `chuanglan.py` | 新增,Mode B:自管码 + `tpl/send` + HMAC 签名 | **两种验证码模式** - Mode A(阿里云):不本地存码,阿里云 `##code##` 托管生成+校验;本地仅留 per-phone 失败计数防爆破。 - Mode B(极光/创蓝):`secrets` 生成 N 位 → 进程内存 → 供应商只下发;本地一次性校验 + 失败 N 次作废。创蓝**复制**极光存码机器(不重构极光,零回归风险)。 **配置(`config.py` + `.env.example`)** - `SMS_PROVIDER = jiguang | aliyun | chuanglan`(默认 jiguang) - `ALIYUN_SMS_*`(AK/签名/模板/方案名/时长…) + `aliyun_sms_configured` 门控 - `CHUANGLAN_SMS_*`(账号/密码/模板/签名/endpoint…) + `chuanglan_sms_configured` 门控 - 复用现有 `SMS_MOCK / SMS_CODE_LENGTH / SMS_CODE_TTL_SEC / SMS_SEND_INTERVAL_SEC / SMS_MAX_VERIFY_ATTEMPTS` - 切到某 provider 却未配齐 → `send_code` 抛 `SmsError(503)`,不静默 **auth.py(最小改动)** - `verify_code` 现在可能抛 `SmsError`(阿里云降级 503)→ `sms_login`、`wechat_bind_phone_sms` 两处各包 `try/except SmsError → HTTPException`,与 `send_code` 现有写法一致。 **依赖** - `pyproject.toml` 增 `alibabacloud_dypnsapi20170525`(仅阿里云 provider 惰性 import;jiguang/chuanglan 不加载)。创蓝零新依赖(httpx + 标准库)。 **测试** - 新增 `test_sms_aliyun.py` / `test_sms_chuanglan.py`(均 monkeypatch 网络接缝,不发真短信) + `test_sms_dispatch.py`(分派/回退)。 - `test_auth.py` 相应更新。 - 现有测试走 `SMS_MOCK=true` 在分派层短路,不受影响。 **文档** - 设计 spec:`docs/superpowers/specs/2026-07-25-aliyun-sms-verify-design.md`、`2026-07-26-chuanglan-sms-verify-design.md` - 接口调研:`docs/integrations/aliyun/*`、`docs/integrations/chuanglan/tpl-send.md`、`docs/integrations/sms.md` ## 兼容性 & 回退 - **默认 `SMS_PROVIDER=jiguang`,线上行为与现状完全一致**;不改极光逻辑、不动 API 层频控与测试账号短路。 - 切阿里云/创蓝仅改环境变量,出问题秒切回极光;未知 `SMS_PROVIDER` 一律回退极光,防误配打挂登录。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #188
178 lines
9.7 KiB
Bash
178 lines
9.7 KiB
Bash
# ===== 环境 =====
|
|
# dev / prod。dev 模式下若 JG_PRIVATE_KEY_PATH 指向的文件不存在,jverify-login 接口会拒绝调用
|
|
# (而不是 import 时炸进程),并提示用 mock 模式跑通其他链路。
|
|
APP_ENV=dev
|
|
APP_NAME=shaguabijia-app-server
|
|
APP_DEBUG=true
|
|
|
|
# ===== 数据库 =====
|
|
# SQLite 本地文件路径。生产环境用 /opt/shaguabijia-app-server/data.db
|
|
DATABASE_URL=sqlite:///./data/app.db
|
|
|
|
# ===== JWT =====
|
|
# 生产部署务必改成随机长字符串,可用:python -c "import secrets; print(secrets.token_urlsafe(64))"
|
|
JWT_SECRET_KEY=
|
|
JWT_ALGORITHM=HS256
|
|
# access token 有效期(分钟),默认 2 小时
|
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=120
|
|
# refresh token 有效期(天),默认 30 天
|
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
|
|
|
# ===== 极光一键登录 =====
|
|
# 控制台 → 应用设置 → 应用信息 拿到
|
|
JG_APP_KEY=
|
|
JG_MASTER_SECRET=
|
|
# RSA 私钥路径(本地开发可指到项目里的 secrets/jverify_rsa_private.pem,生产用 /opt 下绝对路径)
|
|
JG_PRIVATE_KEY_PATH=./secrets/jverify_rsa_private.pem
|
|
JG_VERIFY_ENDPOINT=https://api.verification.jpush.cn/v1/web/loginTokenVerify
|
|
JG_REQUEST_TIMEOUT_SEC=15
|
|
|
|
# ===== 无障碍保护存活监控(pull 后置检测;本期不接推送)=====
|
|
HEARTBEAT_MONITOR_ENABLED=true
|
|
HEARTBEAT_TIMEOUT_MINUTES=60
|
|
HEARTBEAT_SCAN_INTERVAL_SEC=60
|
|
|
|
# ===== 短信 (mock 模式) =====
|
|
# mock = true 时,任意 6 位数字均通过,且 /sms/send 不真发短信(只 log)。生产改 false。
|
|
SMS_MOCK=true
|
|
SMS_CODE_TTL_SEC=300
|
|
SMS_SEND_INTERVAL_SEC=60
|
|
|
|
# ===== 短信提供商(可切换:jiguang 默认 / aliyun 阿里云号码认证 / chuanglan 创蓝云智)=====
|
|
# jiguang :本服务生成验证码,极光 REST 只负责下发,本地内存校验(复用上面极光 JG_* 凭证)。
|
|
# aliyun :阿里云 dypns 号码认证,阿里云生成+下发+校验(Mode A,核验免费);缺凭证时 /sms/* 返 503。
|
|
# 需在阿里云号码认证控制台开通「融合认证」,并使用系统赠送签名 + 赠送模板。
|
|
# chuanglan:创蓝云智(253)模板短信,本服务生成码、创蓝只下发、本地校验(Mode B,与极光同);缺凭证 503。
|
|
# 用 YZM 前缀验证码账号;服务器出网 IP 需在创蓝控制台加白名单(否则 117)。见 docs/integrations/chuanglan/tpl-send.md。
|
|
SMS_PROVIDER=jiguang
|
|
ALIYUN_SMS_ACCESS_KEY_ID=
|
|
ALIYUN_SMS_ACCESS_KEY_SECRET=
|
|
ALIYUN_SMS_SIGN_NAME=
|
|
ALIYUN_SMS_TEMPLATE_CODE=
|
|
# 方案名:留空=默认方案;若填,发码与校验须一致(本服务已共用同一配置项,不会不匹配)。
|
|
ALIYUN_SMS_SCHEME_NAME=
|
|
ALIYUN_SMS_ENDPOINT=dypnsapi.aliyuncs.com
|
|
ALIYUN_SMS_CODE_LENGTH=6
|
|
ALIYUN_SMS_VALID_TIME_SEC=300
|
|
ALIYUN_SMS_INTERVAL_SEC=60
|
|
ALIYUN_SMS_TIMEOUT_SEC=15
|
|
# --- 创蓝云智(253)---
|
|
CHUANGLAN_SMS_ACCOUNT=
|
|
CHUANGLAN_SMS_PASSWORD=
|
|
CHUANGLAN_SMS_TEMPLATE_ID=1022457679
|
|
# 短信签名文案【品牌】;模板已关联签名则留空。
|
|
CHUANGLAN_SMS_SIGNATURE=
|
|
CHUANGLAN_SMS_ENDPOINT=https://smssh.253.com/msg/sms/v2/tpl/send
|
|
CHUANGLAN_SMS_TIMEOUT_SEC=10
|
|
|
|
# ===== 测试账号(release 包全流程联调用)=====
|
|
# 配一个固定测试手机号,专供无 SIM 卡 / 不走一键登录时打通全流程:该号登录【免短信验证码】
|
|
# (real 模式下也跳过校验)、每次登录【都重走新手引导】,并有【每日登录上限】防被人猜到号后脚本刷。
|
|
# 逻辑见 app/core/test_account.py,与其他业务解耦。
|
|
# ⚠️ 留空 = 关闭整功能(生产默认);要启用才填号(如 11111111111)。改完重启生效,随时可清空停用。
|
|
TEST_ACCOUNT_PHONE=
|
|
# 该测试号每日最多登录次数,当日超过即拒绝(429),次日归零。
|
|
TEST_ACCOUNT_DAILY_LIMIT=500
|
|
|
|
# ===== 美团联盟 CPS =====
|
|
# 美团联盟后台 → 媒体管理 拿到的 appkey 和密钥
|
|
MT_CPS_APP_KEY=
|
|
MT_CPS_APP_SECRET=
|
|
# 默认渠道追踪标识(sid),用于区分不同 app 的 CPS 数据
|
|
MT_CPS_DEFAULT_SID=sgbjia
|
|
# 美团调用走的代理。⚠️ 本机/内网开发直连美团会 SSL EOF,必须填本地代理(如 http://127.0.0.1:7897);
|
|
# 线上国内服务器留空(=直连)。留空且本机直连失败时 /feed、/coupons、/top-sales 会返回空。
|
|
MT_CPS_PROXY=
|
|
|
|
# ===== 京东联盟 CPS =====
|
|
# 京东联盟/京东宙斯开放平台创建应用后填写。AUTH_KEY 是工具商授权 key,自有应用可留空。
|
|
JD_UNION_APP_KEY=
|
|
JD_UNION_APP_SECRET=
|
|
JD_UNION_SITE_ID=
|
|
JD_UNION_AUTH_KEY=
|
|
|
|
# ===== Pricebot 上游 (领券/比价业务透传目标) =====
|
|
# 客户端调本服务的 /api/v1/coupon/step 等,我们透传到 pricebot-backend。
|
|
# 本地开发用 localhost:8000。生产部署改成内网地址(如 http://pricebot.internal:8000)。
|
|
PRICEBOT_BASE_URL=http://localhost:8000
|
|
# 【多实例】单机多进程部署时,填逗号分隔的实例列表(端口与 pricebot 集群对齐),透传层按
|
|
# trace_id 一致性 hash 选实例 → 同一比价所有帧落同一进程(进程内维护 state,无需 Redis)。
|
|
# 留空 = 单实例(用上面的 PRICEBOT_BASE_URL)。详见 pricebot-backend/docs/并发部署设计.md
|
|
# PRICEBOT_INSTANCES=http://127.0.0.1:8001,http://127.0.0.1:8002,http://127.0.0.1:8003,http://127.0.0.1:8004,http://127.0.0.1:8005,http://127.0.0.1:8006
|
|
# 领券单帧最多 wait 6s,加网络往返,30s 兜底
|
|
PRICEBOT_REQUEST_TIMEOUT_SEC=30
|
|
# 比价(intent/recognize + price/step)透传超时:大上下文 LLM + 逐帧 LLM,给 60s
|
|
PRICEBOT_COMPARE_TIMEOUT_SEC=60
|
|
|
|
# ===== 内部端点 (server→server, pricebot 上报价格观测) =====
|
|
# pricebot 比价 done 后把各平台到手价 POST 到本服务 /internal/price-observation 落库
|
|
# (price_observation 表,比价资产沉淀层),靠共享密钥头 X-Internal-Secret 校验。
|
|
# 必须与 pricebot 侧的 INTERNAL_API_SECRET **同值**;留空 = 内部写端点关闭(返 503)。
|
|
# 启用前两边都填同一高熵串:python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|
INTERNAL_API_SECRET=
|
|
|
|
# ===== CORS =====
|
|
# 逗号分隔,生产留空(只让 app 调,不开放 web)。本地开发可加 http://localhost:5173 之类
|
|
CORS_ALLOW_ORIGINS=
|
|
|
|
# ===== 微信支付(商家转账到零钱 / 提现)=====
|
|
# appid/secret 来自微信开放平台移动应用;mch/序列号/公钥ID 来自微信支付商户平台。
|
|
# 证书 .pem 放 secrets/(已 gitignore)。
|
|
WECHAT_APP_ID=wxxxxxxxxxxxxxxxxx
|
|
WECHAT_APP_SECRET=your_app_secret
|
|
WXPAY_MCH_ID=your_mch_id
|
|
WXPAY_MCH_SERIAL_NO=your_cert_serial_no
|
|
WXPAY_MCH_PRIVATE_KEY_PATH=./secrets/apiclient_key.pem
|
|
WXPAY_PUBLIC_KEY_ID=PUB_KEY_ID_xxxxxxxx
|
|
WXPAY_PUBLIC_KEY_PATH=./secrets/pub_key.pem
|
|
WXPAY_TRANSFER_SCENE_ID=1000
|
|
WXPAY_AUTH_NOTIFY_URL=
|
|
WITHDRAW_AUTO_RECONCILE_ENABLED=false
|
|
WITHDRAW_AUTO_RECONCILE_INTERVAL_SEC=300
|
|
WITHDRAW_AUTO_RECONCILE_OLDER_THAN_MINUTES=15
|
|
# 0 点自动兑金币开关(deploy/daily-exchange.timer 触发的脚本读它;false=脚本 no-op)。默认 true。
|
|
AUTO_EXCHANGE_ENABLED=true
|
|
|
|
# ===== 穿山甲激励视频(服务端发奖回调)=====
|
|
# 看完激励视频后穿山甲服务器 S2S 回调本服务发金币(客户端不参与发奖)。
|
|
# 穿山甲"奖励校验密钥"(m-key),验签用,从 GroMore 后台各广告位取到后填这里。
|
|
# 配齐(任一非空)并把 ENABLED=true 后,/api/v1/ad/pangle-callback 才受理回调(否则 503)。
|
|
# 每个激励位 m-key 不同但共用同一回调 URL → 各位分开一行配(留空的忽略);验签逐个试、任一通过即接受。
|
|
PANGLE_CALLBACK_ENABLED=false
|
|
# 测试应用 激励位 104099649
|
|
PANGLE_REWARD_SECRET_TEST=
|
|
# 测试应用 专属激励位 104127529
|
|
PANGLE_REWARD_SECRET_TEST_DEDICATED=
|
|
# 正式应用 激励位 104099389
|
|
PANGLE_REWARD_SECRET_PROD=
|
|
# (旧用法,仍兼容:单个或逗号分隔的多个 m-key,会与上面三个合并去重)
|
|
PANGLE_REWARD_SECRET=
|
|
# ⚠️ 仅本地联调:true 时开放 POST /api/v1/ad/test-grant,让 debug 客户端看完广告直接发奖,
|
|
# 验证"看广告→金币到账"全链路(未部署公网、穿山甲 S2S 打不到本地时用)。生产必须 false(绕过反作弊)。
|
|
AD_REWARD_TEST_GRANT_ENABLED=false
|
|
|
|
# ===== 穿山甲 GroMore 数据 API(按天拉收益报表,供后台广告收益报表的「穿山甲后台收益」)=====
|
|
# ⚠️ 与上面发奖回调的 m-key 是【两套不同凭证】:这三样在穿山甲后台「接入中心 → GroMore-API →
|
|
# 聚合数据报告 API」文档页领取。只读拉取 GroMore 天级 revenue(预估)/ api_revenue(收益Api),
|
|
# 不参与发奖。三样齐全才生效;留空 = scripts/sync_pangle_revenue 直接 no-op。
|
|
# 子账号(role_id≠user_id)需主账号在「角色管理」授予「查看全部数据」权限,否则查不到收益(接口 118);
|
|
# role_id 填成 = user_id 即查主账号数据。同步:线上每天 ~10:30 由 timer 跑 python -m scripts.sync_pangle_revenue。
|
|
PANGLE_REPORT_USER_ID=0
|
|
PANGLE_REPORT_ROLE_ID=0
|
|
PANGLE_REPORT_SECURITY_KEY=
|
|
# GroMore AppId(报表 site_id 维度)→ 应用环境;默认取现网两个应用,按需覆盖。
|
|
PANGLE_REPORT_SITE_ID_PROD=5830519
|
|
PANGLE_REPORT_SITE_ID_TEST=5832303
|
|
|
|
# ===== 客户端运行日志上报(POST /api/v1/applog/batch)=====
|
|
# 客户端批量上报的 App 运行日志逐条落到独立滚动文件 logs/app-client.log,供阿里云 Logtail
|
|
# 采进【独立 SLS logstore】(与服务日志 app-server.log 分开;滚动机制相同,trace_id 可跨层检索)。
|
|
# 全部有默认值,不填即用默认(定义见 app/core/client_log.py 与 app/api/v1/applog.py)。
|
|
# CLIENT_LOG_FILE=logs/app-client.log # 落盘路径
|
|
# CLIENT_LOG_MAX_BYTES=20971520 # 单文件 20MB 滚动
|
|
# CLIENT_LOG_BACKUP_COUNT=10 # 保留 10 个 → ~200MB 缓冲(给 Logtail 断线留余量)
|
|
# CLIENT_LOG_SERVICE_NAME=app-client # 输出行 service 字段
|
|
# APPLOG_MAX_BATCH=500 # 单批最大条数(超 → 422;导入期常量,改需重启)
|
|
# APPLOG_MAX_BODY_BYTES=2097152 # 请求体上限 2MB(超 → 413;运行期可调)
|
|
# APPLOG_MAX_MSG_BYTES=8192 # 单条 msg 超此字节数截断
|