Files
BYW-VoiceHub/.env.example
T
Henry_Du d1b643bd93 refactor(微信订阅): 重写订阅消息链路,每一步失败都可见
把「微信订阅消息」这套代码按验收标准推翻重写:用户能自己看懂为什么收不到,
任何一步失败都必须看得见。对外函数名不变,notificationService / test-subscribe
等调用点无需改动。

新增 server/utils/wechatSubscribeTemplates.ts(纯函数,零依赖):
  - 模板登记表唯一来源(内置 SONG_SELECTED / SONG_REJECTED),服务层不再自己写死
  - parseSubscribeTemplates 如实报告环境变量覆盖情况:坏 JSON 只报告不抛、
    只给 id 时沿用内置字段映射、缺 id 的项记进 unknownKeys 并忽略
  - findEmptyFields / findEmptyDataFields 发出前自查空字段(微信空值整条 47003)
  - explainWechatErrcode 把 43101/47003/40003/40037/40001/45009 翻成人话

重写 server/services/wechatSubscribeService.ts:
  - sendWechatSubscribe 返回 SendOutcome,用 stage 明确「卡在哪一步」:
    no-template / disabled-by-user / no-wechat-openid / no-access-token /
    empty-field / no-grant / rejected-no-quota / rejected-bad-field / rejected-other
  - 每一次返回都写一行日志,不再有静默跳过(这是「发不出去全靠猜」的根因)
  - inspectWechatSubscribe 只读体检:不发消息、不消耗额度,返回逐项 verdict + actions
  - grantWechatSubscribe 依靠新增的 WechatSubscribeGrant 表记账:微信没有查询
    剩余授权的接口,只能自己数(用户点允许 +1、发送成功 -1)
  - 记账表不可用时失败开放(fail-open):额度预检关闭、照常发送,避免通知整体哑掉
  - access_token 显式 responseType: json 并做字符串兜底解析

新增接口:
  - GET  /api/user/wechat-subscribe-status   小程序显示「还能收到几条 / 为什么收不到」
  - POST /api/user/wechat-subscribe-grant    小程序授权成功后上报 accept 的模板
  - GET  /api/admin/wechat/diagnose          重写:人话结论 + actions,?send=1 真发一次

其他:新增 WechatSubscribeGrant 表(schema + 手写迁移)、.env.example 说明 JSON 覆盖
写法、.gitattributes 补 *.sql eol=lf。

测试:新增 test/wechatSubscribeTemplates.test.mjs(13 个用例,可直接 import .ts,
不依赖 Nuxt 别名),npm test 里订阅相关 25/25 通过。
2026-10-11 16:02:33 +08:00

144 lines
7.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库连接地址(必填)
# 格式:postgresql://用户名:密码@主机:端口/数据库名?sslmode=require
# 示例:postgresql://myuser:mypassword@localhost:5432/voicehub?sslmode=require
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
# JWT 认证密钥(必填)
# 请使用强随机字符串,建议至少 32 位
JWT_SECRET="your-very-secure-jwt-secret-key"
# 受信的 CDN/反向代理真实 IP 头(可选)
# 多个头用逗号分隔;未配置时只使用 TCP 连接地址,避免客户端伪造转发头。
# 示例:EdgeOne 使用 eo-connecting-ip,Cloudflare 使用 cf-connecting-ip
# TRUSTED_CLIENT_IP_HEADERS=eo-connecting-ip
# WebAuthn / Passkey 配置(可选)
# 依赖方 ID (RP ID),必须是你域名的一个有效域名
# 规则:必须是请求域名的精确匹配或父域名
#
# 示例:
# - 请求域名为 voicehub.example.com,RP ID 可设为 voicehub.example.com 或 example.com
# - 不配置时,默认使用当前请求的 hostname(适合开发/预览环境)
# - 生产环境建议配置为你的主域名,如:voicehub.example.com
WEBAUTHN_RP_ID=
# 允许的 Origin 列表(可选)
# WebAuthn 要求精确匹配 Origin,此配置用于支持多域名部署
# 多个域名用逗号分隔,必须包含 https:// 前缀
#
# 示例:
# - 单域名:https://voicehub.example.com
# - 多域名(Vercel 预览 + 生产):https://voicehub.example.com,https://voicehub-xxx.vercel.app
# - 二级域名:https://voicehub.example.com,https://sub.voicehub.example.com
#
# 不配置时:默认信任请求的 Origin(适合开发环境)
# 生产环境:强烈建议配置,以防止恶意站点伪造 Origin 攻击
WEBAUTHN_ORIGIN=
# 应用运行环境(可选)
# development: 开发环境
# production: 生产环境
NODE_ENV=development
# Sentry 配置(可选)
# 错误监控与性能追踪,配置后系统将自动上报运行时异常
#
# SENTRY_DSN 客户端(浏览器)使用的 DSN(公开可暴露)
# 示例:https://publicKey@o0.ingest.sentry.io/0
# SENTRY_DSN=
#
# SENTRY_SERVER_DSN 服务端(Node)使用的 DSN(保密,不应暴露给浏览器)
# 如未单独配置,服务端会自动降级使用 SENTRY_DSN
# SENTRY_SERVER_DSN=
#
# 采样率配置(0~1 的小数,默认 1.0 表示 100%)
# 错误采样率
# SENTRY_ERROR_SAMPLE_RATE=1.0
# 性能追踪(Traces)采样率
# SENTRY_TRACES_SAMPLE_RATE=1.0
# 浏览器会话回放(Replay)采样率,需同时设置下面两项才会生效
# 生产环境建议分别设为 0.1 和 1.0,即 10% 正常用户 + 100% 错误用户进行回放
# SENTRY_REPLAY_SESSION_SAMPLE_RATE=
# SENTRY_REPLAY_ON_ERROR_SAMPLE_RATE=
#
# 项目版本号,用于区分不同版本的错误;未填时自动使用 package.json 的 version
# SENTRY_RELEASE=
#
# 环境标签,默认继承 NODE_ENV
# SENTRY_ENVIRONMENT=
# Redis 缓存服务(可选,暂不推荐)
# 如果设置了 REDIS_URL,系统将自动启用 Redis 缓存功能
# 格式:redis://用户名:密码@主机:端口
# REDIS_URL="redis://default:password@host:port"
# Docker Compose 自带的 Redis 容器(compose 自带 redis 服务,仅在启用自带 redis 时用)
# - 留空(或空字符串):不设置 Redis 密码,内网部署常用(默认)
# - 填写强密码:Redis 会启用 requirepass,连接时 REDIS_URL 里也要带上相同密码
REDIS_PASSWORD=
# Redis 键命名空间,多个 VoiceHub 环境共用 Redis 时应分别设置
# REDIS_KEY_PREFIX="voicehub:v2:"
# OAuth 配置(可选)
# 启用相应的 OAuth 提供商需要填写对应的 Client ID 和 Client Secret
#
# 注意:OAuth 运行时配置已经迁移到管理员后台;以下变量仅用于兼容旧部署和后台一键导入。
# 导入完成后,系统以数据库中保存的配置为准。
# OAuth 回调地址(必填,如果启用 OAuth)
# 注意:[provider] 是占位符,实际使用时需要根据具体情况填写
OAUTH_REDIRECT_URI="http://localhost:3000/api/auth/[provider]/callback"
# 用于加密 OAuth state 的密钥(必填,如果启用 OAuth)
OAUTH_STATE_SECRET=your-secure-random-string-for-oauth-state
# GitHub OAuth
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Casdoor OAuth
CASDOOR_CLIENT_ID=
CASDOOR_CLIENT_SECRET=
CASDOOR_ENDPOINT=
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# 聚合登陆 OAuth(如 auxx.cn 等聚合登录服务,支持微信/QQ 等通道
AGGREGATE_OAUTH_APP_ID=
AGGREGATE_OAUTH_APP_KEY=
# 支持单值、逗号分隔或 JSON 数组,例如:qq,wx,sina,baidu
# 可用值:qq,wx,alipay,sina,baidu,douyin,huawei,xiaomi,gitee,gitea,bilibili,kuaishou
AGGREGATE_OAUTH_LOGIN_TYPE=wx
# 聚合登录服务基地址(https,不带 /oauth/... 路径),例如:https://www.auxx.cn
AGGREGATE_OAUTH_ENDPOINT=
# 微信小程序登录(可选)
# 用于小程序端 wx.login() 的 code 换取 openid,对应接口 POST /api/auth/wechat/login
# AppID / AppSecret 在微信公众平台「开发管理 → 开发设置 → 开发者ID」中获取
# 注意:这与上方聚合登录的 wx 通道不是一回事——小程序 openid 按 AppID 维度生成,两者不通用
WECHAT_MINI_APPID=
WECHAT_MINI_SECRET=
# 微信「订阅消息」模板(可选:**留空也能用**,代码里内置了站长选好的模板)
# 内置模板(唯一来源:server/utils/wechatSubscribeTemplates.ts 的 BUILTIN_TEMPLATES):
# SONG_SELECTED「歌曲排期通知」—— 管理员把歌排进排期/发布排期时发
# thing2→songTitle, date4→playDate, time1→playTime, character_string3→sequence, phrase5→status
# SONG_REJECTED「审核驳回通知」—— 投稿被驳回时发
# thing6→songTitle, thing4→reason
# 下面这条环境变量**只用来覆盖内置映射**(比如站长在公众平台选了别的模板或字段名)。
# ⚠️ 必须是合法 JSON;写了坏值(例如只填一个光秃秃的模板 ID)服务端会**忽略它**并在启动日志里
# 打一行「不是合法 JSON,已忽略(仍用内置默认模板)」,属于可恢复状态,但请在体检接口里确认。
# {"SONG_SELECTED":{"id":"模板ID"}} ← 只换 ID,沿用内置字段映射
# {"SONG_REJECTED":{"id":"模板ID","fields":{"thing6":"songTitle"}}} ← 同时换字段映射
# {"SONG_PLAYED":{"id":"模板ID","fields":{"thing1":"songTitle"}}} ← 也可以补一张内置表里没有的模板
# fields 的键是模板里的字段名(thing2/date4/time1/character_string3/phrase5,公众平台选用模板时能看到),
# 值是变量名(可用:songTitle / message / time / playDate / playTime / sequence / status / reason)。
# ⚠️ 平台限制:一次性订阅=一次授权只能发一条;只有绑定了微信的账号才有 openid;
# 模板 ID 变了老用户要**重新点一次「开启微信通知」**才有额度(旧授权按模板 ID 记账,直接作废)。
# 发不出去时先看 GET /api/admin/wechat/diagnose(超管,只读不消耗额度):
# 它会把「拿不拿到 token / 有没有绑微信 / 通知开关开没开 / 还剩几条授权」逐项说清。
WECHAT_MINI_SUBSCRIBE_TEMPLATES=