Commit Graph
43 Commits
Author SHA1 Message Date
Henry_Du 4fe807eb39 feat(排期): 新增批量发布全部草稿接口 publish-drafts
给小程序端「一键全部发布」用:一个事务把所有草稿翻成已发布并处理待处理重播申请,只清一次缓存、批量发订阅消息。避免前端逐条调用单条 publish 时每次都重建整张排期缓存导致的卡顿。
2026-10-24 13:39:13 +08:00
Henry_Du dc2565da34 fix(微信订阅): 字段名按微信真实模板校准,不再靠猜编号;phrase 只截断不补齐 2026-10-24 10:18:57 +08:00
Henry_Du c51b7c660a fix(微信订阅): 不再按本地记账拦截发送,一律交给微信裁决 2026-10-11 16:59:25 +08:00
Henry_Du 6f9def0ffd docs(微信订阅): 自测接口的提示指向新的模板登记表与体检接口 2026-10-11 16:12:28 +08:00
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
Henry_Du 09e7f53a1a feat(微信订阅): 新增一键体检接口 /api/admin/wechat/diagnose
线上排查「订阅消息没发出去」以前只能靠猜,因为没有查询额度的接口,四条跳过路径
也不写日志。这个 GET 接口(仅超管,浏览器直接打开即可)一次返回所有事实:

- access_token 能不能拿到、长度多少
- WECHAT_MINI_SUBSCRIBE_TEMPLATES / APPID / SECRET 是否注入、模板开关是否启用
- 当前账号有没有绑微信 openid(没有就永远发不出去)、哪些 userId 绑了微信
- 每个模板的 ID、字段映射,以及 buildTemplateData 算出的、真发出去会用的 data
- 加 ?send=1 时顺手给当前用户真发一条(两个模板各一次),把微信的 errcode
  原样返回:0=通、43101=没额度、47003=字段名不对、40003=openid 不对、40001=token 失效

用法:登录后台后打开 /api/admin/wechat/diagnose (或 ?send=1)
2026-10-11 15:37:39 +08:00
Henry_Du 81fd05b39a feat(微信订阅): 自测接口支持 dryRun(不消耗额度)与 sendToAll(区分没额度/字段错) 2026-10-11 12:04:03 +08:00
Henry_Du b64ee5cbc6 fix(排期): 发布按钮不再兼任删除,清空排期改为显式操作并加服务端守卫 2026-10-11 01:15:24 +08:00
Henry_Du 3eff710d5d feat(微信订阅): 接入歌曲排期模板并加固部署流程
订阅消息(小程序侧唯一能主动触达用户的通道):
- 新增 server/utils/wechatSubscribeFields.ts:按微信字段类型裁剪值(thing≤20、
  phrase 必须 5 个汉字、time/date/character_string 各自上限),空白压成一行,
  北京时间 time 由 playTime 的开始时间生成,解析不出退化为当天 00:00。
- wechatSubscribeService 内置 SONG_SELECTED 模板(站长新给的那个模板 ID),
  并转发上述纯函数;notificationService.createSongSelectedNotification 现在会带上
  playDate/playTime/sequence,三个排期接口(schedule / publish / bulk-publish)补传 sequence。
- 新增 test/wechatSubscribeFields.test.mjs(10 例,node --test 全绿)。
- test-subscribe 管理接口支持 vars 覆盖并返回真实发送值,方便站长核对字段名。

部署流程(Gitea Actions 容器里没有 buildx 插件,旧流程在此硬失败):
- 每个 job 各自安装/探测 docker-buildx(v0.19.3,走 gh-proxy 再退直连),
  不把 `buildx version` 当唯一依据;装不上时降级为普通 `docker build` 继续发布,
  不再因为缺插件整条流水线失败。

注意:SONG_SELECTED 的字段名(thing2/date4/time1/character_string3/phrase5)是按常见
命名推断的,需在公众平台后台核对;不一致时用环境变量覆盖,模板 ID 变更后老用户需重新授权。
2026-10-11 00:04:14 +08:00
Henry_Du ebf1c3a127 perf(auth): 登录去掉一次冗余查库 + 缓存 2FA 查询 + 修掉 session_key 进日志
登录实测约 232ms,其中 bcrypt 约 72ms、网络往返约 80ms 是不可省的计算/物理成本,
剩下的都是我们自己的开销。这次把能省的两处省掉(实测约 40ms,占登录耗时约 19%):

1. 去掉「数据库连接检查」的冗余预检
   原来每次登录先 await db.select().from(users).limit(1) 只为"测连接",
   紧接着又会真的查用户,连不上一样会报错 —— 纯粹多一次数据库往返。
   现在把那次真实查询包进 try,只有它失败才回报 503「数据库服务暂时不可用」,
   错误码与文案完全不变。

2. 缓存「这个账号有没有开 2FA」
   登录每次都查 userIdentities 找 TOTP,但绝大多数账号根本没开 2FA。
   新增 server/utils/totp-cache.ts:
     · 查到**没有** TOTP → 缓存 10 分钟,后续登录直接跳过查询
     · 查到**有** TOTP   → **不缓存**(安全优先:绝不能因为缓存而让刚开 2FA 的人被跳过验证)
   开启/关闭 2FA 的接口必须调用 invalidateTotpCache(userId),已在
   server/api/user/2fa/{enable,disable}.post.ts 接上 —— 否则会有最长 10 分钟的验证空窗。
   该缓存不参与密码校验,最坏情况只是多查一次库。

3. lastLogin / lastLoginIp 的 UPDATE 不再阻塞响应
   登录时间与登录 IP 是审计信息,晚写几十毫秒、甚至这次写失败,
   都不影响用户能否登录。改为不 await(保留 .catch,避免 unhandledRejection)。

4. 安全修复:微信登录不再把 session_key 打进日志
   server/api/auth/wechat/login.post.ts 原来打印整个 jscode2session 响应前 500 字,
   而该响应含 { session_key, openid, unionid }。
   session_key 是敏感凭据(可解密用户数据、配合其他接口换取手机号),
   长期落在服务端日志里等于凭据落盘。
   现在只打印:顶层键名、errcode/errmsg、以及 openid/session_key/unionid 的
   存在性与长度(不打印值);仅在"响应根本不是对象"时才打印截断原文
   (那种情况它不可能是合法凭据,且是排查被代理拦成 HTML 的唯一线索)。

已验证:
- 缓存逻辑用假 db 真跑过 5 个场景(命中缓存省下查询、失效后立刻重查、
  有 2FA 不缓存、多用户互不干扰、只失效指定用户)
- 4 个改动文件的相对 import 路径均存在;totp-cache 的导出与两处 import 对得上
- login.post.ts 抽走 TOTP 查询后已清理不再使用的 userIdentities / and 导入
- 文件编码 UTF-8 无 BOM、无乱码、括号平衡
2026-10-01 21:04:02 +08:00
Henry_Du 752a2eae87 feat(auth): 已登录账号绑定微信改为一键(不再要求填姓名密码)
站长反馈:登陆进去的账号绑定微信就不用再写账号信息了吧。

原来的绑定只有一个入口 POST /api/auth/bind,它是**未登录**场景用的:
微信扫出来的是个陌生身份,得先"姓名 + 密码"验证身份,才知道该绑到谁头上。
用户已经在账号页里登录了,再让他输一遍账号信息就很别扭。

新增 POST /api/auth/wechat/bind-self { bindToken }:
- 必须已登录(用 event.context.user 直接落库,不再要密码);
- bindToken 走原有 verifyBindingToken 校验;
- 该微信已经绑给**别人** → 409(不许抢绑);
- 已经绑给**自己** → 幂等返回成功(重复点不报错);
- 并发下唯一约束(23505)冲突也会复判一次,避免误报失败。

小程序端:
- utils/api.js 新增 bindWechatToSelf()(wx.login → wechat/login 拿 bindToken → 调新接口);
- 「账号管理 → 绑定微信」与首页那条绑定提示都改成一步完成,
  不再跳绑定页填账号密码(绑定页保留给未登录场景:如登录页发现微信未绑账号时去注册/绑定)。
2026-10-01 00:08:06 +08:00
Henry_Du cd6e235848 fix(users): 状态大小写不敏感 + 彻底修掉「无法编辑其它用户」
站长反馈仍然无法编辑其它用户。上一版只把**小程序端**的状态改成小写,
但如果小程序没重新编译(或用的还是旧版本),发上来的仍是 `ACTIVE`,
服务端 `['active','withdrawn','graduate'].includes(status)` 依旧不通过 → 400
「用户状态只能是 active, withdrawn 或 graduate」——编辑任何用户都会失败。

这版把容忍度放到**服务端**,与客户端版本解耦:

- 新增 server/utils/user-status.ts:
  · USER_STATUSES = ['active','withdrawn','graduate']
  · normalizeUserStatus() 大小写不敏感('ACTIVE' / ' Active ' → 'active'),认不出返回 null
  · USER_STATUS_RULE_MESSAGE 统一文案
- [id].put.ts:状态改用归一化值;另修正"自己只能改姓名"里的状态比较也忽略大小写
- [id]/status.put.ts、batch-status.put.ts:同样归一化;
  并修掉一个我自己引入的隐患(把归一化结果赋回 const 变量 → TypeError),
  batch-status 之前被误写成不存在的 statusesRaw,一并改正

效果:无论小程序是否重新编译,管理员都能编辑其它用户(含历史拼音账号只改年级)。
2026-09-30 23:49:40 +08:00
Henry_Du deb71875c1 fix(users): 自己可改显示名、超管可在编辑里提升超管(修「保存失败/禁止修改自己的账户」)
站长反馈:在 admin 模式下仍无法修改用户信息,弹「禁止在用户管理中修改自己的账户」;
另外角色下拉里没有「技术运维」。

1) 自己改自己:原来一律禁止 → 现在**只允许改姓名(显示名)**,其余(账号名/年级/班级/
   状态/角色/密码)只要动了就仍然拒绝。这样站长能在「用户管理」里把 admin 的显示名
   改成自己的名字(登录名仍是 admin,应急登录不受影响)。
   状态比较忽略大小写:库里是 active,客户端历史版本可能发 ACTIVE,不该因此判定"改了状态"。

2) 超管可在通用编辑接口里把别人提升为 SUPER_ADMIN(站长要求下拉里要有「技术运维」):
   新增 allowPromoteBySuper 例外 —— 仅当登录者是 SUPER_ADMIN、目标角色设为 SUPER_ADMIN、
   且目标不是初始账号(ID=1,本来就是超管)时放行;非超管仍被 canChangeRoleOf 拦住。
   与 POST /api/admin/users/{id}/promote-super 效果一致,两条路都能走。
2026-09-30 22:58:10 +08:00
Henry_Du dfb4891022 fix(users): 只校验"真的改过"的姓名/账号名,修回"管理员无法编辑其他用户"
站长反馈:管理员无法编辑其他用户信息了。

根因:「姓名/账号名必须纯中文」的校验是**每次编辑都重新做一遍**的,
所以历史账号(早期用拼音/英文建的、名字里带「·」或空格的)连"改个年级"
都会被这条规则拦死 —— 管理员等于没法维护这些账号。admin 账号之前也是同一个毛病。

改成与服务端同一口径的"只校验改动过的字段":
- 没改动 → 不校验(历史数据原样保留,不强迫改名);
- 改了 → 必须满足规则(纯中文;admin 账号的姓名例外);
- 主动把中文名改成拼音/带空格 → 仍然拦。

初始管理员(ID=1)的规则不变:登录名必须保持 admin、角色锁死、密码不在这里改。

(对应的小程序端同样改成"只校验改动字段":编辑时记下原始姓名/账号名,
提交时比对;新增时两个字段都校验。)
2026-09-30 22:02:14 +08:00
Henry_Du 792b3d4ef6 feat(users): 初始管理员可改显示名(登录名锁死 admin)+ 新增「提升为超级管理员」
站长诉求:想自定义 admin 账号的资料,但**必须保留 admin/admin123 作为应急登录**;
同时想用自己绑了微信的中文账号做日常管理。

按"哪些能改、哪些必须锁死"拆开处理:

1) server/api/admin/users/[id].put.ts —— 原来对 ID=1 **一律 403**(连显示名都改不了),
   现在:
   · 允许改:姓名(显示名)、年级、班级、状态;
   · 锁死:登录名必须保持 admin(那就是 admin/admin123 的应急通道,改了就登不进去);
   · 锁死:角色不能动(防止把自己降权导致没人能管站点);
   · 锁死:密码不在这里改(避免被其它管理员顺手重置;要改用它登录后去「设置」里改)。

2) 新增 POST /api/admin/users/{id}/promote-super —— 把某个账号提升为超级管理员。
   之所以单开接口而不是放开通用编辑接口的 role:原 `canChangeRoleOf()` 明确禁止提升为
   SUPER_ADMIN,两个前端的角色下拉也故意不给这一项。这里是**显式、可审计**的通道:
   仅 SUPER_ADMIN 可调、不能提升自己、已经是超管的会提示、成功后清该用户认证缓存。

前端(小程序)配套:用户管理操作菜单改成 {label, run} 数组(有权限时才出现"提升为超级管理员",
不再靠写死 tapIndex),点击有二次确认并列出会拿到的权限;编辑初始管理员时登录名输入框 disabled
并显示原因,姓名照常可改。
2026-09-30 21:42:59 +08:00
Henry_Du 5039ab3f79 feat(auth): 账号名必须是纯中文,唯一例外是管理员账号 admin
站长要求:账号名除了 admin 其它都必须是中文名。

原来「只能包含汉字」的校验散在 5 个写入路径上,而且**没有 admin 例外** ——
导致连 admin 账号自己都编辑不了(改任何字段都撞「姓名和用户名只能包含汉字」)。

- 新增 server/utils/username.ts:规则一处定义
  · isValidUsername:纯中文,或恰好是 admin
  · isValidPersonName:姓名纯中文,但 admin 账号的姓名不做限制
  · 统一提示文案(接口报错与客户端提示共用同一句)
- 5 个写入路径全部改用它,并删掉各自的 chineseRegex:
  admin/users 新建、admin/users 编辑、admin/users 批量导入、auth/register、user/profile
- 注册时明确拒绝 admin(系统保留账号)

已验证:两端规则对 9 个用例结论完全一致;5 处均无旧正则;
admin 可编辑,拼音名(zhangsan / zhang.san / 张三1 / 带空格)一律被拦。
2026-09-30 20:50:09 +08:00
Henry_Du cccaf36990 feat(brand): 站点品牌名统一为「宝一外校园点歌系统」
站长要求全站一致(小程序端已单独提交)。本次只改**用户可见文案与默认值**:

- 服务端默认值/兜底:system-settings-defaults、admin/system-settings(GET/POST)、
  site-config.get、utils/siteUtils —— 原来是 'VoiceHub' / '校园广播站点歌系统'
- 用户可见通知文案:MeoW 绑定验证码与标题、MeoW 测试通知、微信订阅测试通知
- 2FA 应用名(authenticator.keyuri)、WebAuthn rpName(出现在系统密钥弹窗里)
- 网页版界面:首页副标题、后台侧栏品牌名、登录/注册/找回/改密标题与文案、
  年度回顾(IntroSlide / ShareCard / 页面标题)、用户教程、通知发送预览、
  API 密钥与数据库导入说明、酷狗 sidecar 说明、打印页脚与歌单卡片页脚、Logo alt

**刻意不动**:localStorage/sessionStorage 的 `voicehub_*` 键、`voicehub:` 前缀的
STORAGE_KEY 与缓存/Redis 前缀、`window.voiceHubPlayer` 之类代码标识、MeoW 的
User-Agent、以及上游项目链接 `github.com/laoshuikaixue/VoiceHub`(那是对原项目的署名)。

注意:站点名**数据**(systemSettings.siteTitle)仍是你后台里设置的「宝一外校园点歌」,
代码只保证默认值与文案一致;要让全站显示完整名称,请在后台「站点配置 → 站点标题」
改成「宝一外校园点歌系统」。
2026-09-30 20:24:55 +08:00
Henry_Du 9d3edc2c5e feat(kugou): 凭证支持环境变量兜底(配一次永久用);补 deploy.yml 变量映射
站长问「能不能跳转到酷狗 App 登录」—— 平台不允许(小程序无法启动任意原生 App),
而且即使跳过去,App 内的登录态也只在那台手机上,不会回传给服务端。
所以真正的便利做法是「凭证配一次、长期复用」:

- kugouService.kuGouFetch:cookie/token 增加 KUGOU_ADMIN_COOKIE / KUGOU_ADMIN_TOKEN
  兜底读环境变量;优先级 显式传入 > 库里绑定 > 环境变量
  (库里绑定的优先,所以之后想临时换一份 Cookie 仍可在后台粘一次覆盖)
- status.get.ts:bound 增加 source: 'db' | 'env' | 'none',界面能说清凭证从哪来
- deploy.yml:KUGOU_ADMIN_COOKIE / KUGOU_ADMIN_TOKEN 三处齐全
  (step env: 映射 + ENV_PAYLOAD export + upsert_env_key),并用 ${VAR:-} 兜底 ——
  少任何一处都会像上次那样 unbound variable 直接把部署打挂

已用脚本校验:payload 占位符/参数 23/23 对应、远端脚本 30 个变量都有来源、
两个新变量三处齐全。
2026-09-30 19:16:55 +08:00
Henry_Du af50ff1d95 feat(kugou): 支持粘贴浏览器 Cookie 绑定酷狗(不再必须扫码)
站长要求去掉扫码:扫码必须拿另一台装了酷狗 App 的手机,很别扭。

技术上完全可行 —— kuGouFetch() 本来就是**无状态**的:它把库里
kugouAdminCookie 放进请求头、kugouAdminToken 放进 query 去调 sidecar。
扫码只是获取 cookie 的一种方式,不是同步的前置条件。

新增 POST /api/admin/kugou/bind-cookie {cookie, token?}:
- 写入前先用这份凭证探一次 /login/status,无效/过期的凭证**不落库**
  (避免出现看起来绑定了但同步全失败的假绑定)
- 有效则写入 systemSettings 的 kugouAdmin* 字段(与 qr-check 完全相同的落点)+ 刷新缓存
- 连不上 sidecar 时返回 502 并提示去「服务设置」检查酷狗 API 地址
- 只保存会话 Cookie,**不接收也不保存酷狗账号密码**

qr-create / qr-check 保留未删(网页版可能还在用)。
2026-09-30 19:09:26 +08:00
Henry_Du d26996350e feat(auth): 注册时可顺带绑定微信(微信登录发现未绑账号 → 引导注册)
- register.post.ts 接受可选 bindToken:注册成功后把该第三方身份插到新账号上
  · 绑不上**不影响注册**(账号已建好),返回 wechatBound=false + wechatBindError
  · 令牌无效/过期只记日志;唯一约束冲突按已绑定处理
  · 顺便把 token 返回给非浏览器客户端(小程序省掉一次重复登录)
2026-09-27 14:17:20 +08:00
Henry_Du a2ba15c3ca feat(notify): 微信订阅消息推送(驳回通知已对接)
- 新增 server/services/wechatSubscribeService.ts:access_token 缓存 + 模板配置 + 字段裁剪 + subscribe/send
  · 内置默认模板 SONG_REJECTED(模板 ID erAK**…,审核内容=thing6←歌名,驳回原因=thing4←驳回原因)
  · 环境变量 WECHAT_MINI_SUBSCRIBE_TEMPLATES 可按键覆盖默认模板
  · thing 字段按微信限制裁到 20 字(超长截 19 字 + 省略号),避免 47003
  · 发送永不抛异常;43101(用户没授权/没额度)只记 info
- 新增 server/api/user/wechat-subscribe-templates.get.ts:给小程序返回模板 ID(客户端要拿它去申请授权)
- 新增 server/api/admin/wechat/test-subscribe.post.ts:超管自测接口(token/openid/字段/微信原文一次看清)
- notificationService.ts:歌曲被选中/已播放/被驳回/系统通知 四处顺带发微信订阅消息
- .env.example + 部署流水线:WECHAT_MINI_SUBSCRIBE_TEMPLATES 可用仓库 Secret 下发
2026-09-27 13:55:55 +08:00
Henry_Du a080144a86 fix(wechat): jscode2session 需显式 responseType=json,否则 ofetch 不解析响应 2026-09-27 08:47:56 +08:00
dsh-agent 565d6e0fcb feat(api): 新增通用二维码接口 /api/qrcode
小程序没有任何 API 能直接唤起系统浏览器离开小程序打开网址,
唯一能真正离开小程序的官方途径是「长按图片 → 识别图中二维码」。
本接口把 URL 渲染成二维码图片(返回 base64 data URL),供小程序端
<img src> + show-menu-by-longpress 使用,让管理员能在微信内置浏览器里
打开完整网页版后台。

安全:需登录;只接受 http/https;长度上限 512。
2026-09-26 21:38:50 +08:00
Henry_Du 3614ac6350 fix(schedule): 修复排期重排接口因变量遮蔽而必然报错
sequence.post.ts 中 \const { schedules } = body\ 在 try 作用域内遮蔽了从
schema 导入的 Drizzle 表对象,导致 db.update(schedules) 实际传入的是请求体
数组而不是表,接口一被调用就抛错(网页版的拖拽排序同样受影响)。

改用 scheduleItems,并补注释说明该陷阱。
2026-09-26 15:12:07 +08:00
Henry_Du 39375c23f2 fix(open-api): schedules 的缓存/Redis 分支补齐 voteCount,消除分支间字段不一致
上一步只给数据库分支补了 voteCount,但实测该接口通常命中缓存分支,
导致 voteCount 依然缺失。抽出 buildVoteCountMap() 供三条分支共用,
保证无论缓存命中与否,输出结构一致。
2026-09-26 14:30:02 +08:00
Henry_Du 12dac95e1b feat(auth): 新增微信小程序登录(绑定策略:首次用学号密码绑定)
- 新增 POST /api/auth/wechat/login:用 wx.login 的 code 换 openid;已绑定直接签发 JWT 返回,未绑定返回 bindToken
- bind.post.ts:支持从请求体读取 bindToken(小程序不便维护 cookie),绑定成功后返回 token,供 Authorization: Bearer 使用
- auth-route-policy.ts:把新接口登记进免鉴权白名单,否则会被鉴权中间件拦截
- .env.example:新增 WECHAT_MINI_APPID / WECHAT_MINI_SECRET
2026-09-26 13:57:24 +08:00
Henry_Du a140ce8621 fix(open-api): 修复开放API两处缺陷并支持小程序取 token
- songs.get.ts: 引用未定义变量 songsWithDetails 导致 /api/open/songs 缓存未命中时必然 500,改为 formattedSongs;并让缓存分支兼容 requester 为字符串的情况,避免返回 { name: undefined }
- schedules.get.ts: 数据库分支补齐 sequence / played / song.voteCount,与此前只有缓存分支才返回这些字段的行为对齐(缺少 sequence 会让前端无法显示歌曲序号)
- login.post.ts: 响应体返回 token,便于小程序/App 通过 Authorization: Bearer 访问(不影响原有 httpOnly cookie 行为)
2026-09-26 13:48:27 +08:00
Henry_Du 381f3debf1 fix(admin+server): 酷狗 baseUrl 默认值移除 127.0.0.1 + 新增后台配置入口与推荐快捷地址
问题:容器内 SystemSettings.kugouApiBaseUrl 默认 http://127.0.0.1:3000 指向自己,sidecar 不可达。

修复:1) schema + migration 移除 kugouApiBaseUrl 默认值;kugouService + status API 兜底改空串(不再假设有 localhost),优先级 env(KUGOU_API_BASE) > DB。

2) 新增 POST /api/admin/kugou/settings:ADMIN+/SUPER 即时写 kugouApiBaseUrl 入库+刷新 CacheService。

3) KuGouImporter.client.vue 左栏新增 baseUrl 输入框(回车即存)+ 保存并测试按钮 + 两个推荐快捷按钮(容器宿主 http://172.19.0.1:3000、公网中继 https://bywmusic.bzw-hheb.top/kugou)+ 当前生效地址显式提示。

4) UI/Icon.vue 新增 server / globe / save 三个 SVG 图标与上述入口配套。
2026-09-19 13:40:59 +08:00
Henry_Du a0184fdeb0 feat(admin+server): 酷狗对接 - 扫码绑定管理员账号 / 按排期自动创建酷狗日歌单 + 同步歌曲
数据模型:SystemSettings 新增 kugouApiBaseUrl / kugouAdminCookie / kugouAdminToken / kugouAdminUser / kugouAdminBoundAt 5 字段;新建 KuGouPlaylistSync 同步历史表(12 字段 + playDate 倒序索引)。迁移脚本:app/drizzle/migrations/20260919143000_add_kugou_integration.sql

后端 Server:新增 server/services/kugouService.ts 封装 KuGouMusicApi sidecar 转发(自动拼 Cookie / Token header);新增 6 个 admin/kugou 路由:status / qr-create / qr-check / unbind / sync-schedule-playlists / sync-history。同步核心:逐日出非草稿排期 → kgHash 直取优先 → 失败用 cloudsearch/search 按标题+歌手三级召回 → /playlist/create + /playlist/tracks op=add 每 100 首分批;写同步历史(status、added/missing、歌单 URL)。

后台 UI:新建 Admin/KuGouImporter.client.vue 三栏布局——左栏 sidecar 健康卡+部署说明+扫码绑定/解绑弹窗;中栏多选排期日期+逐日出排期预览卡+按日同步;下栏最近 100 条同步历史(7 种状态 badge + 歌单链接跳转)。dashboard.vue L105 playlist-card 挂载点由 PlaylistCardPage 替换为 KuGouImporter;权限 permissions.js 5 个 ADMIN 族角色开放 playlist-card 页面访问(接口内部绑定/同步再强校验 ADMIN/SUPER_ADMIN)。

侧car要求:部署 KuGouMusicApi(参考文档 https://kugoumusicapi-docs.4everland.app),默认本机 http://127.0.0.1:3000,可通过 SystemSettings.kugouApiBaseUrl 或环境变量 KUGOU_API_BASE 覆盖。
2026-09-19 11:57:02 +08:00
Henry_Du 456f676929 fix(build): 彻底移除无用的网易云 API 模块,消除由于未安装模块引入导致的 OOM 和解析异常
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 3m2s
构建并部署 BYW VoiceHub / 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化) (push) Successful in 4m29s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Skipped
2026-09-13 13:54:41 +08:00
Henry_Du 7034bbaedb fix(build): 进一步优化打包内存,强制外置超大包,并将 PlaylistCardPage 设为客户端专属避免服务端静态分析
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 3m31s
构建并部署 BYW VoiceHub / 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化) (push) Successful in 4m52s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Skipped
2026-09-13 13:47:19 +08:00
Henry_Du a937f32e6e feat(theme): 移除首页主题切换按钮,恢复默认深色主题 ClassicDark
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m12s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Successful in 8m59s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Successful in 6s
- 首页 index.vue:删除主题切换按钮 DOM、useTheme 引入及 handleToggleTheme 处理函数
- nuxt.config.ts:defaultTheme ClassicLight → ClassicDark;enabledThemes 恢复为 4 主题(System/ClassicDark/ClassicLight/ModernLight);x-default-theme meta 和防闪烁脚本回退值同步改为 ClassicDark
- schema.ts / system-settings-defaults / site-config API / POST API / useSiteConfig:7 处主题默认配置全部从 ClassicLight/双浅色列表 同步改为 ClassicDark/四主题完整列表
2026-09-12 20:40:35 +08:00
Henry_Du a82ff6747e feat(site): 对齐参考站点UI与交互调整
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m8s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Successful in 9m11s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Successful in 7s
- 页脚:响应耗时文字统一为「响应耗时 XXXms」,版本链接改为 GitHub 官方仓库,默认 ICP 备案号设置为粤ICP备2025450093号
- 顶部栏:恢复主题切换按钮与语言按钮(语言暂为占位提示),放置于用户区左侧
- 主题:修复首屏内联脚本默认主题白名单校验,避免 localStorage 中残留的 light/dark/System 等非法值生效,首屏默认改为 ClassicLight(仅允许 ClassicLight/ModernLight)
- 登录页:在密码输入框下方恢复「忘记密码?」入口,新增配套的找回密码引导页 /forgot-password
- 默认配置:system-settings 兜底记录与 SYSTEM_SETTINGS_DEFAULTS 新增 icpNumber 默认值
2026-09-12 11:56:30 +08:00
Henry_Du 0c71328efd fix(nitro-import): permissions/user-avatar 引用路径统一改为 ~~/server/utils 别名,解决 Rollup UNRESOLVED_IMPORT
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m10s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Successful in 9m33s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Successful in 6s
2026-09-12 10:27:07 +08:00
Henry_Du d818819fff feat: 实现主题、黑名单、排程等14项功能迭代
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 1m24s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
- style(theme): 修复浅色模式不生效问题,为 ClassicLight 和 ModernLight 增加独立差异化 CSS 变量定义(主色/圆角/阴影/背景对比度)
- feat(blacklist): 后端放开 LANGUAGE/GENRE 类型校验;check API 新增语种(CJK/拉丁/假名/谚文/西里尔/阿拉伯)和曲风关键词匹配逻辑;前端 BlacklistManager UI 增加 语种/曲风 四类型切换、类型筛选、图标与标签颜色映射、输入提示
- fix(DataAnalysisPanel): 活跃用户列表容器加 max-h-[400px] + overflow-y-auto + 底部渐变遮罩;tooltip 加 max-height + truncate 防止超高;头像失败回退首字母
- feat(admin): 新增 user-avatar.ts 工具(OAuth→Gravatar→DiceBear 三级兜底);active-users/realtime/users 列表返回 avatarUrl;DataAnalysisPanel/UserManager 展示圆形头像;权限分级 ROLE_LEVELS,SUPER_ADMIN 全权限,ADMIN 仅可管理 ≤ USER 级用户的密码与角色,前端按钮联动禁用
- fix+style(schedule): ScheduleManager 标签页 sticky min-h 防抖、间距调优、表单分组、移动端 padding 统一
- feat(schedule): 自动排期新增「按投稿人数优先覆盖」策略(算法:requester + collaborators 总数降序),支持 CustomSelect 切换;新增 localStorage voicehub:schedule:auto-config 持久化 7 项配置(策略/时段/年级/排序/标签/搜索/学期)500ms debounce
- fix(schedule): songs/index.get 新增 usePool 参数,从 schedule_song_pool(status=PENDING,targetPlayDate IS NULL) 取歌并去重,排除已排期 songs.played=false;schedule.post 和 draft-bulk 分配后更新池状态为 ASSIGNED 避免重复分配
- feat(player): ScheduleManager 新增「构建播放队列」按钮,按 playDate→时段→sequence 排序后经 useAudioPlayer.playSong 整列载入并 toast 反馈
- fix(db): db.ts 检测 NETLIFY/VERCEL serverless 环境使用最小池 max=1 idle_timeout=1 keep_alive=false,Node Server 保持 max=10
- feat(site-config): site-config.get.ts 与 useSiteConfig.js 导出 statisticsCode 和 statisticsCodeEnabled;index.vue 挂载后解析 HTML 动态重插 script 元素确保第三方统计执行
- fix(schedule-printer): ScheduleItemPrint 封面 aspect-square object-cover;print-fix.css 打印时强制 48px×48px contain、去除圆角阴影
- fix(songRequest): 新增 server/utils/url.ts normalizeCoverUrl(trim/去零宽空格/相对协议补 https/非 http 丢弃/URL.parse 兜底);request.post/add.post/import.post 三处写入 cover/coverUrl 前统一规范化
2026-09-12 08:40:59 +08:00
Henry_Du d09d12828d feat(admin): 优化用户权限分级·统一头像·数据分析面板修复
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 55s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
- fix(DataAnalysisPanel): 活跃用户卡片固定 400px + overflow-y 独立滚动容器,底部渐变遮罩提示滚动;列表项 tooltip 增加 max-height 与 overflow 截断;新增圆形头像显示与 @error 回退首字母
- feat(admin): 新增 getUserAvatarUrl 统一头像策略链(OAuth identity→Gravatar→DiceBear 7.x initials),stats/realtime、active-users、users/index 三处接口返回 avatarUrl
- fix(admin): 建立 SUPER_ADMIN(100)>ADMIN(80)>USER(10) 分级常量与 canChangeRoleOf/canResetPasswordFor 等谓词;reset-password、[id].put、index.post 全面应用级别守卫;前端 UserManager 按钮按权限禁用、availableRoles 按操作者级别过滤 SUPER_ADMIN/ADMIN 等高角色
2026-09-12 08:36:45 +08:00
Henry_Du 153ee41ef0 feat(schedule)+fix(schedule)+feat(player): 排期管理5项功能 - 紧凑布局/投稿人优先算法/localStorage持久化/备选池修复/播放队列按钮
* fix+style(schedule): ScheduleManager 标签页紧凑布局,头部 min-height 防抖,内容区统间距与移动端适配
* feat(schedule): 新增自动排期策略「按投稿人数优先覆盖」,按 requester+collaborators 数排序分配好时段
* feat(schedule): localStorage 键 voicehub:schedule:auto-config 持久化策略/时段/学期/过滤等7项,onMounted 恢复,watch debounce 500ms 保存
* fix(schedule): songs/index.get.ts 新增 use_pool 查询参数,备选池过滤条件 status=PENDING + song.played=false + song未在任何排期中;ScheduleManager/draft-bulk 创建排期时将池记录 UPDATE 为 ASSIGNED
* feat(player): ScheduleManager 工具栏新增「构建播放队列」按钮,按 playDate+playTimeId+sequence 排序后调用 useAudioPlayer.playSong 加载播放列表,支持 confirm 确认与 toast 反馈
* refactor(useSongs): fetchSongs 增加第5参数 usePool,透传 use_pool=true 到 /api/songs
2026-09-12 08:33:25 +08:00
Henry_Du 0e60f15cfe feat(theme): sync defaultTheme=ClassicLight enabledThemes=[ClassicLight,ModernLight] across all 6 fallback locations
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 2m50s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 3s
2026-09-12 00:13:27 +08:00
Henry_Du cc142a58bc fix(system-settings): correctly balance fillDefaults arrow brackets L844 - 3 RPAREN after null (logic/filter/fromEntries) then RCURLY then outer RPAREN, IDE 0 diagnostics
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 4m12s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Failing after 4m58s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Skipped
2026-09-11 23:09:29 +08:00
Henry_Du 285dd24463 fix(system-settings): resolve index.post.ts L844 syntax error Expected ')' but found 'if' - fillDefaults arrow return/object literal close brackets order
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 1m33s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
2026-09-11 23:01:56 +08:00
Henry_Du 13b9b72170 fix(system-settings): 修复线上主题/排期/聚合登录等配置缺失,对新 DB 字段用 SYSTEM_SETTINGS_DEFAULTS 兜底 + 更新接口允许写入新字段 + 缩短 Redis 缓存 TTL
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 15m0s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 3s
- site-config.get.ts:filterPublicSettings 和 Redis/DB 命中分支全部先合并 SYSTEM_SETTINGS_DEFAULTS,解决老库无 schema 新字段导致 public 返回 defaultTheme=light、enabledThemes=light,dark 等旧值的问题
- admin/system-settings/index.post.ts:新增 ~40 个白名单字段(主题/排期可见范围/留言自动审查/OAuth 聚合/首次强制改密/投稿限制/卡密/注册审核/自动备份/统计代码等),支持管理面板保存生效;更新返回结果用 fillDefaults(DEFAULTS) 兜底
- admin/system-settings/index.ts (GET):返回 DB 记录前合并 SYSTEM_SETTINGS_DEFAULTS,避免管理后台 UI 拿不到新字段
- cacheService.ts:CACHE_TTL.SYSTEM_SETTINGS 3600s → 900s(60min→15min),防止 Redis 单机挂或旧脏值导致配置变更长时间不生效
2026-09-11 21:39:45 +08:00
Henry_Du bd8df1f026 feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
2026-09-06 17:03:20 +08:00
Henry_Du 4ce4649aaf 初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
2026-09-06 08:19:02 +08:00