From 3eff710d5daba3e6a4a359f7c637d8f5e81805b6 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Sun, 11 Oct 2026 00:04:14 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(=E5=BE=AE=E4=BF=A1=E8=AE=A2?= =?UTF-8?q?=E9=98=85):=20=E6=8E=A5=E5=85=A5=E6=AD=8C=E6=9B=B2=E6=8E=92?= =?UTF-8?q?=E6=9C=9F=E6=A8=A1=E6=9D=BF=E5=B9=B6=E5=8A=A0=E5=9B=BA=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 订阅消息(小程序侧唯一能主动触达用户的通道): - 新增 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 变更后老用户需重新授权。 --- .env.example | 17 +- .gitea/workflows/deploy.yml | 195 +++++++++++++++--- app/assets/css/theme-protection.css | 9 - app/components/Admin/ApiKeyManager.vue | 2 +- app/components/Admin/DatabaseManager.vue | 2 +- app/components/Admin/KuGouImporter.client.vue | 2 +- app/components/Admin/NotificationSender.vue | 2 +- .../Admin/PlaylistCardGenerator.vue | 2 +- app/components/Admin/PlaylistCardPage.vue.old | Bin 0 -> 24 bytes app/components/Admin/SchedulePrinter.vue | 4 +- app/components/Admin/Sidebar.vue | 4 +- app/components/Admin/SiteConfigManager.vue | 2 +- app/components/Auth/LoginForm.vue | 4 +- app/components/Auth/RegisterForm.vue | 2 +- app/components/SiteFooter.vue | 13 +- app/components/UI/HeroIllustration.vue | 42 ---- app/components/UI/MobileBottomNav.vue | 40 ---- app/components/UserTutorial.vue | 4 +- app/components/year-review/IntroSlide.vue | 2 +- app/components/year-review/ShareCard.vue | 2 +- app/layouts/default.vue | 4 +- old.vue | Bin 0 -> 24 bytes old_playlist.vue | 114 ++++++++++ server/api/admin/schedule.post.ts | 4 +- .../api/admin/schedule/bulk-publish.post.ts | 6 +- server/api/admin/schedule/publish.post.ts | 4 +- .../api/admin/wechat/test-subscribe.post.ts | 46 ++++- server/services/notificationService.ts | 22 +- server/services/wechatSubscribeService.ts | 72 ++++--- server/utils/wechatSubscribeFields.ts | 103 +++++++++ test/wechatSubscribeFields.test.mjs | 123 +++++++++++ 31 files changed, 659 insertions(+), 189 deletions(-) create mode 100644 app/components/Admin/PlaylistCardPage.vue.old delete mode 100644 app/components/UI/HeroIllustration.vue delete mode 100644 app/components/UI/MobileBottomNav.vue create mode 100644 old.vue create mode 100644 old_playlist.vue create mode 100644 server/utils/wechatSubscribeFields.ts create mode 100644 test/wechatSubscribeFields.test.mjs diff --git a/.env.example b/.env.example index 4ed5092..80972c5 100644 --- a/.env.example +++ b/.env.example @@ -122,11 +122,16 @@ AGGREGATE_OAUTH_ENDPOINT= WECHAT_MINI_APPID= WECHAT_MINI_SECRET= -# 微信「订阅消息」模板(可选,但**想让通知发到微信里就必须配**) -# 公众平台「功能 → 订阅消息 → 选用模板」拿到模板 ID 后,按下面的 JSON 填: -# {"SONG_PLAYED":{"id":"模板ID","fields":{"thing1":"songTitle","time2":"time","thing3":"message"}}} +# 微信「订阅消息」模板(可选:**留空也能用**,代码里内置了站长选好的模板) +# 内置模板(见 server/services/wechatSubscribeService.ts 的 DEFAULT_TEMPLATES): +# SONG_SELECTED「歌曲排期」—— 管理员把歌排进排期/发布排期时发 +# SONG_REJECTED「歌曲驳回」—— 投稿被驳回时发 +# 下面这条环境变量**只用来覆盖内置映射**(比如站长在公众平台选了别的字段名): +# {"SONG_SELECTED":{"id":"模板ID","fields":{"thing2":"songTitle","time1":"playTime"}}} # 键是通知类型(SONG_SELECTED / SONG_PLAYED / SONG_REJECTED / SYSTEM_NOTICE), -# fields 的键是模板里的字段名(thing1/time2/... 选用模板时能看到),值是变量名 -# (内置变量:songTitle / message / time)。不配就等于关闭微信推送,其它渠道照常。 -# ⚠️ 平台限制:一次性订阅=一次授权只能发一条;且只有绑定了微信的账号才有 openid。 +# fields 的键是模板里的字段名(thing2/date4/time1/character_string3/phrase5,选用模板时能看到), +# 值是变量名(内置变量:songTitle / message / time / playDate / playTime / sequence / status)。 +# 同键只给 id 不给 fields 时,会沿用内置的字段映射(不会退化成 thing1/time2/thing3)。 +# ⚠️ 平台限制:一次性订阅=一次授权只能发一条;只有绑定了微信的账号才有 openid; +# 模板 ID 变了老用户要**重新点一次「开启微信通知」**才有额度。 WECHAT_MINI_SUBSCRIBE_TEMPLATES= diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 32ef27b..003c791 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -519,7 +519,63 @@ jobs: echo "环境准备完成" cat /etc/os-release 2>/dev/null | head -4 || true docker --version - docker buildx version || echo "buildx 未安装" + docker buildx version 2>&1 | head -1 || echo "buildx 子命令不可用" + + - name: 确保 Docker Buildx 插件可用(缺失则装官方插件,否则回退 docker build) + run: | + set -euxo pipefail + + # 症状:`docker buildx create --name xxx` 报 + # unknown flag: --name / See 'docker --help' + # 打出的是 docker 的总帮助 → 说明 `buildx` 子命令没落到真插件上,docker CLI 把 + # `--name` 当自己的顶层 flag 解析了。这里先探一次并按需装官方插件,不能等 build-image + # 作业才炸(各 job 是独立容器,装了也得在该 job 里再装一次)。 + ensure_buildx() { + # 不用 `docker buildx version` 当唯一依据:这种“子命令没落到真插件上”的坏状态里 + # 它仍可能返回 0。优先相信插件文件在位,其次才信命令。 + if [ -x "${HOME}/.docker/cli-plugins/docker-buildx" ] \ + || [ -x /usr/local/lib/docker/cli-plugins/docker-buildx ] \ + || { docker buildx version >/dev/null 2>&1 && docker buildx create --help 2>/dev/null | grep -q -- '--name'; }; then + echo "[buildx] 插件可用:$(docker buildx version 2>&1 | head -1)" + return 0 + fi + echo "[buildx] 未发现可用插件,从官方 release 安装" + ARCH="$(uname -m)" + case "${ARCH}" in + x86_64|amd64) BUILDX_ARCH=amd64 ;; + aarch64|arm64) BUILDX_ARCH=arm64 ;; + *) echo "[buildx] 不支持的架构 ${ARCH},跳过安装"; return 1 ;; + esac + BUILDX_VER="v0.19.3" + BUILDX_BIN="buildx-${BUILDX_VER}.linux-${BUILDX_ARCH}" + BUILDX_DIR="${HOME}/.docker/cli-plugins" + mkdir -p "${BUILDX_DIR}" + if curl -fsSL --retry 3 --connect-timeout 20 -o "${BUILDX_DIR}/docker-buildx" \ + "${GH_PROXY_PREFIX}https://github.com/docker/buildx/releases/download/${BUILDX_VER}/${BUILDX_BIN}" \ + || curl -fsSL --retry 3 --connect-timeout 20 -o "${BUILDX_DIR}/docker-buildx" \ + "https://github.com/docker/buildx/releases/download/${BUILDX_VER}/${BUILDX_BIN}"; then + chmod +x "${BUILDX_DIR}/docker-buildx" + echo "[buildx] 已安装到 ${BUILDX_DIR}/docker-buildx" + else + rm -f "${BUILDX_DIR}/docker-buildx" + echo "[buildx] 下载失败(GitHub 与 ${GH_PROXY_PREFIX} 都不通)" + fi + if ! docker buildx version >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + echo "[buildx] 当前 docker 不认 ~/.docker/cli-plugins,改写系统插件目录" + sudo mkdir -p /usr/local/lib/docker/cli-plugins + sudo cp -f "${BUILDX_DIR}/docker-buildx" /usr/local/lib/docker/cli-plugins/docker-buildx 2>/dev/null || true + sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx 2>/dev/null || true + fi + [ -x "${BUILDX_DIR}/docker-buildx" ] || [ -x /usr/local/lib/docker/cli-plugins/docker-buildx ] + } + + if ensure_buildx && docker buildx create --help 2>/dev/null | grep -q -- '--name'; then + echo "[buildx] 自检通过(create 支持 --name)" + echo "BUILDX_INSTALLED=yes" >> "$GITHUB_ENV" + else + echo "[buildx] 自检未通过,镜像构建作业将自动回退 docker build(不阻塞部署)" + echo "BUILDX_INSTALLED=no" >> "$GITHUB_ENV" + fi - name: 检出代码 run: | @@ -669,7 +725,61 @@ jobs: BRANCH="${GITHUB_REF_NAME:-main}" git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" . - - name: 初始化 Docker Buildx builder (为本作业重新创建) + - name: 确保 Docker Buildx 插件可用(本作业容器内独立安装) + run: | + set -euxo pipefail + + # 每个 job 跑在独立容器里,meta-warmup 装的插件这里看不到,必须自己再确保一次。 + # 症状回顾:`docker buildx create --name xxx` 报 unknown flag: --name(打出的是 docker + # 总帮助)→ buildx 子命令没落到真插件上。装完仍不可用则不报错,交给后面的 docker build 兜底。 + # 判定同 meta-warmup:优先看插件文件是否在位,不把 `docker buildx version` 当唯一依据。 + if [ -x "${HOME}/.docker/cli-plugins/docker-buildx" ] \ + || [ -x /usr/local/lib/docker/cli-plugins/docker-buildx ] \ + || { docker buildx version >/dev/null 2>&1 && docker buildx create --help 2>/dev/null | grep -q -- '--name'; }; then + echo "[buildx] 插件已可用:$(docker buildx version 2>&1 | head -1)" + echo "BUILDX_USABLE=yes" >> "$GITHUB_ENV" + exit 0 + fi + echo "[buildx] 未发现可用插件,从官方 release 安装" + ARCH="$(uname -m)" + case "${ARCH}" in + x86_64|amd64) BUILDX_ARCH=amd64 ;; + aarch64|arm64) BUILDX_ARCH=arm64 ;; + *) echo "[buildx] 不支持的架构 ${ARCH}"; echo "BUILDX_USABLE=no" >> "$GITHUB_ENV"; exit 0 ;; + esac + BUILDX_VER="v0.19.3" + BUILDX_BIN="buildx-${BUILDX_VER}.linux-${BUILDX_ARCH}" + BUILDX_DIR="${HOME}/.docker/cli-plugins" + mkdir -p "${BUILDX_DIR}" + if curl -fsSL --retry 3 --connect-timeout 20 -o "${BUILDX_DIR}/docker-buildx" \ + "${GH_PROXY_PREFIX}https://github.com/docker/buildx/releases/download/${BUILDX_VER}/${BUILDX_BIN}" \ + || curl -fsSL --retry 3 --connect-timeout 20 -o "${BUILDX_DIR}/docker-buildx" \ + "https://github.com/docker/buildx/releases/download/${BUILDX_VER}/${BUILDX_BIN}"; then + chmod +x "${BUILDX_DIR}/docker-buildx" + echo "[buildx] 已安装到 ${BUILDX_DIR}/docker-buildx" + else + rm -f "${BUILDX_DIR}/docker-buildx" + echo "[buildx] 下载失败(GitHub 与 ${GH_PROXY_PREFIX} 都不通)" + fi + if ! docker buildx version >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + sudo mkdir -p /usr/local/lib/docker/cli-plugins + sudo cp -f "${BUILDX_DIR}/docker-buildx" /usr/local/lib/docker/cli-plugins/docker-buildx 2>/dev/null || true + sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx 2>/dev/null || true + fi + if [ -x "${BUILDX_DIR}/docker-buildx" ] || [ -x /usr/local/lib/docker/cli-plugins/docker-buildx ]; then + if docker buildx create --help 2>/dev/null | grep -q -- '--name'; then + echo "[buildx] 自检通过" + echo "BUILDX_USABLE=yes" >> "$GITHUB_ENV" + else + echo "[buildx] 插件在位但 create 仍不认识 --name,改用 docker build 兜底" + echo "BUILDX_USABLE=no" >> "$GITHUB_ENV" + fi + else + echo "[buildx] 安装未成功,改用 docker build 兜底" + echo "BUILDX_USABLE=no" >> "$GITHUB_ENV" + fi + + - name: 初始化 Docker Buildx builder (为本作业重新创建,不可用则回退 docker build) env: REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }} @@ -708,17 +818,35 @@ jobs: done [ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像"; exit 1; } printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" -u "${REGISTRY_USER}" --password-stdin + + # buildx 插件不可用时(`docker buildx create --name` 报 unknown flag: --name, + # 说明子命令没落到真插件上),不再硬失败,转用普通 docker build 继续部署。 + if [ "${BUILDX_USABLE:-no}" != "yes" ] \ + || ! docker buildx version >/dev/null 2>&1 \ + || ! docker buildx create --help 2>/dev/null | grep -q -- '--name'; then + echo "[buildx] 本作业不可用,跳过 builder 初始化,将由 docker build 回退构建" + echo "BUILDX_USABLE=no" >> "$GITHUB_ENV" + rm -f "${BKK_CFG}" + exit 0 + fi + docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true - docker buildx create \ - --name "${BUILDER_NAME}" \ - --driver docker-container \ - --use \ - --config "${BKK_CFG}" \ - --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \ - --driver-opt network=host \ - --driver-opt "image=${BUILDKIT_IMAGE}" + if docker buildx create \ + --name "${BUILDER_NAME}" \ + --driver docker-container \ + --use \ + --config "${BKK_CFG}" \ + --buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \ + --driver-opt network=host \ + --driver-opt "image=${BUILDKIT_IMAGE}" \ + && docker buildx inspect --bootstrap; then + echo "[buildx] builder ${BUILDER_NAME} 就绪,使用 buildx 构建(含分层缓存)" + echo "BUILDX_USABLE=yes" >> "$GITHUB_ENV" + else + echo "[buildx] builder 创建或启动失败,回退 docker build(不使用分层缓存)" + echo "BUILDX_USABLE=no" >> "$GITHUB_ENV" + fi rm -f "${BKK_CFG}" - docker buildx inspect --bootstrap - name: 复用 meta-warmup 结果(与 Preflight 并发省 ~5min) run: | @@ -777,18 +905,39 @@ jobs: echo "使用 BuildKit 分层缓存加速(默认不写 --no-cache)" fi - docker buildx build \ - --file ./Dockerfile \ - --platform linux/amd64 \ - --load \ - --provenance=false \ - "${BUILD_ARGS[@]}" \ - --tag "${IMG_FULL}" \ - --tag "${IMG_LATEST}" \ - --label "org.opencontainers.image.source=${OCS_SOURCE}" \ - --label "org.opencontainers.image.revision=${OCS_REVISION}" \ - --label "org.opencontainers.image.created=${OCS_CREATED}" \ - . + # BUILDX_USABLE 由「初始化 Docker Buildx builder」步骤写入 GITHUB_ENV: + # yes → buildx(分层缓存 + --load 打进本地 daemon) + # no → 普通 docker build 兜底(无分层缓存;依赖 daemon.json 的 registry-mirrors + # 或 Dockerfile 里的 DOCKER_MIRROR build-arg 拉基础镜像) + # 注意:--provenance 只有 buildx 认,回退时必须去掉。 + if [ "${BUILDX_USABLE:-yes}" = "yes" ]; then + echo "[build] 使用 docker buildx build" + docker buildx build \ + --file ./Dockerfile \ + --platform linux/amd64 \ + --load \ + --provenance=false \ + "${BUILD_ARGS[@]}" \ + --tag "${IMG_FULL}" \ + --tag "${IMG_LATEST}" \ + --label "org.opencontainers.image.source=${OCS_SOURCE}" \ + --label "org.opencontainers.image.revision=${OCS_REVISION}" \ + --label "org.opencontainers.image.created=${OCS_CREATED}" \ + . + else + echo "[build] buildx 不可用,回退 docker build(生效的 mirror 由 daemon.json 决定)" + docker info 2>/dev/null | sed -n '/Registry Mirrors/,/^$/p' || true + docker build \ + --file ./Dockerfile \ + --platform linux/amd64 \ + "${BUILD_ARGS[@]}" \ + --tag "${IMG_FULL}" \ + --tag "${IMG_LATEST}" \ + --label "org.opencontainers.image.source=${OCS_SOURCE}" \ + --label "org.opencontainers.image.revision=${OCS_REVISION}" \ + --label "org.opencontainers.image.created=${OCS_CREATED}" \ + . + fi docker image inspect "${IMG_FULL}" >/dev/null docker image inspect "${IMG_LATEST}" >/dev/null diff --git a/app/assets/css/theme-protection.css b/app/assets/css/theme-protection.css index d8801cb..d009d9a 100644 --- a/app/assets/css/theme-protection.css +++ b/app/assets/css/theme-protection.css @@ -21,15 +21,6 @@ color-scheme: light; } -/* ===== 前台页面强制浅色 color-scheme(防止系统深色干扰)===== */ -body[data-frontend="true"]:not(.admin-body), -body[data-frontend="true"]:not(.admin-body) input, -body[data-frontend="true"]:not(.admin-body) textarea, -body[data-frontend="true"]:not(.admin-body) select, -body[data-frontend="true"]:not(.admin-body) button { - color-scheme: light; -} - /* 保护表单元素外观,但不强锁 color-scheme = none(none 会导致控件 fallback 到浅色) */ input[type='text'], input[type='email'], diff --git a/app/components/Admin/ApiKeyManager.vue b/app/components/Admin/ApiKeyManager.vue index d892b3a..cbb201c 100644 --- a/app/components/Admin/ApiKeyManager.vue +++ b/app/components/Admin/ApiKeyManager.vue @@ -75,7 +75,7 @@

暂无API密钥

- 您还没有创建任何访问密钥。创建密钥后,您可以安全地将 VoiceHub 集成到第三方应用程序中。 + 您还没有创建任何访问密钥。创建密钥后,您可以安全地将宝一外校园点歌系统集成到第三方应用程序中。