diff --git a/.dockerignore b/.dockerignore index e5e39aa..a8c2f35 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -# T1:构建镜像严禁带入的敏感/私有/无关内容 +# 构建镜像严禁带入的敏感/私有/无关内容 LICENSE README.md .gitignore @@ -15,7 +15,7 @@ _smoke_test_downloader.py _test_podcast_api.py 部署文档.md 功能介绍.md -# T1 新增:真实凭据、用户数据库、密钥、日志、交接文档、运维脚本与审查工作目录 +# 真实凭据、用户数据库、密钥、日志、交接文档、运维脚本与审查工作目录 cookie.txt user_cookie.txt # Cookie 备份(qr_login._backup_cookie 产生,含真实 MUSIC_U): @@ -25,14 +25,14 @@ user_cookie.*.txt data/ data/backups/ *.log -# 第 97 条(P3):轮转件 music_api.log.1/2/3 不匹配 *.log——含 IP/UA 的历史日志 -# 曾随 _mkpkg 同类问题外泄(第 55 条),docker 轨道同步堵上 +# 轮转件 music_api.log.1/2/3 不匹配 *.log——含 IP/UA 的历史日志 +# 曾随 _mkpkg 同类问题外泄,docker 轨道同步堵上 music_api.log* *.log.* 交接文档.md 修复验收台账_F01-F117.md 待修问题清单_三轮汇总.md -# P2-9:补齐后产生的审计清单与内部引导文件(含内部路径/运维细节) +# 补齐后产生的审计清单与内部引导文件(含内部路径/运维细节) 待修问题清单_第四轮深挖.md 待修问题清单_第五轮深挖.md 新会话引导.md diff --git a/.env.example b/.env.example index 16ef0ed..02b1568 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -# 复制为 .env 后按需修改(F59:不提供 .env 时 docker-compose 使用默认 TZ,可直接运行) +# 复制为 .env 后按需修改(不提供 .env 时 docker-compose 使用默认 TZ,可直接运行) TZ=Asia/Shanghai # HTTPS 反代(Caddy/Nginx 等)上线后取消注释:会话 Cookie 仅经 HTTPS 传输 -# (纯 HTTP 部署不要开启,否则浏览器拒收 Cookie;详见交接文档 F11) +# (纯 HTTP 部署不要开启,否则浏览器拒收 Cookie) # ENABLE_SECURE_COOKIE=1 -# 第 100 条:跨域来源控制——默认 `*`(历史口径);自用收敛同源可改为实际域名 +# 跨域来源控制——默认 `*`(历史口径);自用收敛同源可改为实际域名 # CORS_ORIGINS=https://qf.linxingran.top diff --git a/.gitignore b/.gitignore index b4dce09..00ec01d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ __pycache__ *.pyo *.pyd -# P3-41:运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理, +# 运行时敏感数据与私有文档——公开包买家若将本项目纳入 git 管理, # 以下内容绝不能进版本库(用户数据库/网易云 Cookie/日志/审计文档) data/ cookie.txt diff --git a/Dockerfile b/Dockerfile index 6942f66..65b7853 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -# T7:基础镜像升级到 Python 3.12(3.9 已于 2025-10 EOL);alpine + ffmpeg 组合保持不变 +# 基础镜像 Python 3.12(3.9 已于 2025-10 EOL);alpine + ffmpeg 组合保持不变 FROM python:3.12-alpine3.21 WORKDIR /app # 使用阿里云 apk 镜像源加速安装 ffmpeg -# 第 97 条(P3):补 tzdata——alpine 无 zoneinfo,ENV TZ=Asia/Shanghai 不装它不生效 +# 补 tzdata——alpine 无 zoneinfo,ENV TZ=Asia/Shanghai 不装它不生效 # (容器内日志/展示本地时间差 8 小时;计费/会话全部 epoch 秒不受影响) RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories && \ apk add --no-cache tzdata ffmpeg diff --git a/core/config.py b/core/config.py index 342a9b9..077583c 100644 --- a/core/config.py +++ b/core/config.py @@ -11,7 +11,7 @@ from dataclasses import dataclass # 站点统一版本号(/health、/api/info、前端 CHANGELOG 全部以此为准,别处不得硬编码) APP_VERSION = '2.4.0' -# 交付结果保留期(/D-1:服务端与前端「领取」入口统一 35 分钟——前端入口保留 35 分钟, +# 交付结果保留期(服务端与前端「领取」入口统一 35 分钟——前端入口保留 35 分钟, # 原 30 分钟会在最后 5 分钟让领取必 404)。终态任务记录、任务目录、下载缓存均按此回收。 RESULT_RETENTION_SECONDS = 35 * 60 diff --git a/docker-compose.yml b/docker-compose.yml index 5344aef..3216f88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,12 @@ services: build: context: . dockerfile: Dockerfile - # F59:.env 缺失时不再启动失败(required 属性 compose v2.24+ 支持;老版本可删掉该行) + # .env 缺失时不再启动失败(required 属性 compose v2.24+ 支持;老版本可删掉该行) env_file: - path: .env required: false environment: - - TZ=Asia/Shanghai # 未提供 .env 时的默认时区(F59:文档"默认即可运行"现在成立) + - TZ=Asia/Shanghai # 未提供 .env 时的默认时区("默认即可运行"现在成立) - ENABLE_SECURE_COOKIE=${ENABLE_SECURE_COOKIE:-} # HTTPS 反代上线后设 1(见 .env.example) ports: - "5001:5000" # 对外 5001 端口(容器内服务监听 5000) diff --git a/entrypoint.sh b/entrypoint.sh index fdb7e56..c90c3d3 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/sh -# F59:exec 让 python 成为 PID 1 直接接收 SIGTERM,容器停止不再经 shell 转发而超时 -# 第 93 条(P2-4):compose 挂载 ./user_cookie.txt 时若宿主不存在该文件,Docker 会 +# exec 让 python 成为 PID 1 直接接收 SIGTERM,容器停止不再经 shell 转发而超时 +# compose 挂载 ./user_cookie.txt 时若宿主不存在该文件,Docker 会 # 在启动前创建同名**目录**并挂载——容器内 touch 只对无卷 docker run 有效。 -# 第 97 条(P3):检测到同名目录直接报错退出(fail-fast),不再静默带病运行 +# 检测到同名目录直接报错退出(fail-fast),不再静默带病运行 for f in user_cookie.txt cookie.txt; do if [ -d "/app/$f" ]; then echo "FATAL: /app/$f 是目录(宿主机缺文件时 Docker 挂载会建同名目录)。请在宿主机先 touch 该文件再启动。" >&2 diff --git a/services/_shared.py b/services/_shared.py index cbaa7f8..64ddf4a 100644 --- a/services/_shared.py +++ b/services/_shared.py @@ -69,7 +69,7 @@ _TASKS_SWEEP_LAST = {'ts': 0.0} # 上次 tasks/ 孤儿目录补扫时间 def _sweep_rate_buckets() -> None: - """限流桶周期清扫(O-5):把超过 10 分钟无新请求的桶移出,防止低流量长期运行下 + """限流桶周期清扫:把超过 10 分钟无新请求的桶移出,防止低流量长期运行下 缓慢累积(原实现只在"新建第 8001 个桶"时才触发清理,时机太晚)""" now = time.time() with _RATE_LOCK: diff --git a/services/tasks.py b/services/tasks.py index d036326..a6cc75c 100644 --- a/services/tasks.py +++ b/services/tasks.py @@ -79,7 +79,7 @@ def _task_monitor_loop() -> None: - 终态任务按 completed_at 保留 35 分钟(与前端「领取」入口对齐) - 运行中/排队/暂停任务超过 4 小时仍未结束 → 置取消(cancel Event 一并置位) - 下载缓存按年龄清扫(配套,同为 35 分钟口径) - - 顺带清理限流桶中的过期项(O-5:不再只依赖"新建桶时"的清理时机) + - 顺带清理限流桶中的过期项(不再只依赖"新建桶时"的清理时机) """ while True: time.sleep(HEARTBEAT_INTERVAL) @@ -167,7 +167,7 @@ def _task_monitor_loop() -> None: auth_system.ledger_settle(tid, 0, 'timeout') except Exception: pass - # ---- 下载缓存清扫(保留期同交付口径 35 分钟)+ 限流桶过期项顺手清理(O-5)---- + # ---- 下载缓存清扫(保留期同交付口径 35 分钟)+ 限流桶过期项顺手清理 ---- _sweep_download_cache(RESULT_RETENTION_SECONDS) _sweep_rate_buckets() # 账本周期兜底(原仅启动执行一次)---- diff --git a/static/site_auth.js b/static/site_auth.js index 26e22e8..75b3dbc 100644 --- a/static/site_auth.js +++ b/static/site_auth.js @@ -5,7 +5,7 @@ (function () { 'use strict'; - /* P2-11:Promise.finally 老内核 polyfill(与 index.html 同一份标准实现—— + /* Promise.finally 老内核 polyfill(与 index.html 同一份标准实现—— index.html 的脚本先加载,这里再兜底一次确保独立引用时也安全) */ if (!Promise.prototype.finally) { Promise.prototype.finally = function (cb) { @@ -71,7 +71,7 @@ return r.json().then(function (d) { if (timer) clearTimeout(timer); return d; }, function (e) { if (timer) clearTimeout(timer); throw e; }); }).catch(function () { - // F9:默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection + // 默认失败兜底——网络断/超时/非 JSON 响应不再产生 unhandledrejection // 噪音(喂 fe_error 上报通道),统一转为标准失败对象走调用方既有的 toast if (timer) clearTimeout(timer); return { status: 0, success: false, message: '网络异常,请检查连接后重试' }; @@ -92,9 +92,9 @@ /* ---------------- 设备标识 ---------------- */ function getDeviceId() { - /* F-7(第 79 条):与 index.html 的 getDeviceId 统一为同一键/同一值—— + /* 与 index.html 的 getDeviceId 统一为同一键/同一值—— 注册指纹与下载任务/心跳的设备号不再各走一套; - 内存缓存兜底 localStorage 不可写时同一页面内保持稳定(F108 同款)。 */ + 内存缓存兜底 localStorage 不可写时同一页面内保持稳定。 */ if (window.__siteDeviceIdCache) return window.__siteDeviceIdCache; var KEY = 'netease_device_id'; var d = ''; @@ -111,8 +111,8 @@ function getDeviceId() { var user = null; var durationText = ''; var ann = null; - var smtpOk = false; // F-8.2:fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开) - var registerCredits = 3; // F-6.1:注册赠送次数由 /auth/me 下发,文案不再硬编码 + var smtpOk = false; // fail-closed——/auth/me 返回前不显示注册入口(防止未配置 SMTP 的窗口期误开) + var registerCredits = 3; // 注册赠送次数由 /auth/me 下发,文案不再硬编码 /* ---------------- 弹窗 ---------------- */ function openModal(id) { var m = $('#' + id); if (m) m.classList.add('show'); } @@ -129,9 +129,9 @@ function getDeviceId() { ['goto-register', 'goto-register2'].forEach(function (id) { var a = $('#' + id); if (a) a.hidden = !showRegister; }); - // F-8.1:两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个 + // 两个弹窗(登录/注册)里的试用提示都要受管——原 querySelector 只命中第一个 $$('.auth-trial-box').forEach(function (box) { box.hidden = !showRegister; }); - $$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; }); // F-6.1 + $$('.reg-gift-num').forEach(function (el) { el.textContent = registerCredits; }); var guest = $('#auth-guest-area'), uarea = $('#auth-user-area'); if (guest && uarea) { if (user) { @@ -163,12 +163,12 @@ function getDeviceId() { durationText = d.duration_text || ''; ann = d.announcement || null; smtpOk = !!d.smtp_configured; - // F-6.1:注册赠送次数动态下发(/auth/me),文案不再硬编码 + // 注册赠送次数动态下发(/auth/me),文案不再硬编码 if (typeof d.register_credits === 'number') registerCredits = d.register_credits; renderAuthArea(); renderUserCenter(); maybeShowAnnouncement(); - // A1:确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框) + // 确认登录后再恢复下载任务(游客调任务接口会 401 误弹登录框) if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} } @@ -190,14 +190,14 @@ function getDeviceId() { showExpiredModal(); return; } - // F53:会话失效时清掉内存里的旧用户状态再引导登录 + // 会话失效时清掉内存里的旧用户状态再引导登录 //(原实现保留旧 user,页面继续显示"已登录"却处处 401) user = null; durationText = ''; renderAuthArea(); renderUserCenter(); authToastOnce(msg || '登录已失效,请重新登录'); - // 第 87 条:登录弹窗已打开且正处于找回/两步验证视图时,不复位视图—— + // 登录弹窗已打开且正处于找回/两步验证视图时,不复位视图—— // 未登录期间后台任务轮询/心跳会持续吃 401 反复进入本函数,若每次都把 // 弹窗拽回登录视图,管理员正在输入的验证码会被打断、票据被清空 var _lm = document.getElementById('login-modal'); @@ -209,7 +209,7 @@ function getDeviceId() { hideForgotView(); hideAdmin2faView(); } - // 第 98 条:会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱), + // 会话失效弹登录窗前,先关掉可能叠开的注册弹窗(双遮罩错乱), // 已 show 则短路——401 风暴(1.5s 轮询)期间不反复重开 var _reg = document.getElementById('register-modal'); if (_reg && _reg.classList.contains('show')) closeModal('register-modal'); @@ -265,14 +265,14 @@ function getDeviceId() { durationText = d.duration_text || ''; renderAuthArea(); renderUserCenter(); - // A1:登录成功后立即恢复本设备的进行中任务 + // 登录成功后立即恢复本设备的进行中任务 if (user && typeof window.SITE_TASKS_BOOTSTRAP === 'function') { try { window.SITE_TASKS_BOOTSTRAP(); } catch (e) {} } } /* ---------------- 登录 / 注册弹窗 ---------------- */ - // 第 81 条:注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开 + // 注册唯一入口——导航按钮与登录弹窗两处链接(全站仅有的 3 条打开 // 路径)统一经此:未同意协议 → cookieGate('register') 弹「隐私与协议设置」 // 注册模式(16 秒强制阅读计时)并中止;已同意 → 直接进注册弹窗。 // 提交侧 doRegister 另有 fail-closed 兜底闸,程序化打开也绕不过。 @@ -294,7 +294,7 @@ function getDeviceId() { ['login-close', 'register-close', 'announce-close'].forEach(function (id) { var b = $('#' + id); if (b) b.addEventListener('click', function () { - if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 第 87 条:关闭侧复位 + if (id.indexOf('login') === 0 && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位 closeModal(id.indexOf('login') === 0 ? 'login-modal' : id.indexOf('register') === 0 ? 'register-modal' : 'announce-modal'); }); }); @@ -302,7 +302,7 @@ function getDeviceId() { var m = $('#' + p[0]); if (m) m.addEventListener('click', function (e) { if (e.target === m) { - if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 第 87 条:关闭侧复位 + if (p[0] === 'login-modal' && window.resetAuthViews) window.resetAuthViews(); // 关闭侧复位 closeModal(p[0]); } }); @@ -312,12 +312,12 @@ function getDeviceId() { if (g2) g2.addEventListener('click', function () { closeModal('login-modal'); openRegisterFlow(); }); var gl1 = $('#goto-login'); if (gl1) gl1.addEventListener('click', function () { closeModal('register-modal'); hideForgotView(); hideAdmin2faView(); openModal('login-modal'); }); - // 第 86 条:找回密码入口/返回(F5.1 只交付 DOM 漏了绑定) + // 找回密码入口/返回(只交付 DOM 漏了绑定) var gf = $('#goto-forgot'); if (gf) gf.addEventListener('click', showForgotView); var fbl = $('#forgot-back-login'); if (fbl) fbl.addEventListener('click', hideForgotView); - // 第 86 条:管理员两步验证视图的返回链接 + // 管理员两步验证视图的返回链接 var a2b = $('#a2fa-back'); if (a2b) a2b.addEventListener('click', hideAdmin2faView); @@ -334,7 +334,7 @@ function getDeviceId() { api('/auth/login', { username: username, password: password }) .then(function (data) { if (data.status !== 200) throw new Error(data.message || '登录失败'); - // 第 86 条:管理员密码验证通过——转两步验证视图(服务端未建会话) + // 管理员密码验证通过——转两步验证视图(服务端未建会话) if (data.data && data.data.admin_2fa) { showAdmin2faView(data.data.ticket, data.data.email_masked); toast('密码验证通过,请完成邮箱验证', 'success'); @@ -358,7 +358,7 @@ function getDeviceId() { /* ---------------- 注册 ---------------- */ var _REG_EMAIL_RE = /^[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}$/; var _REG_NAME_RE = /^[A-Za-z0-9_\u4e00-\u9fa5]{3,20}$/; - // F5.1:注册弹窗内常驻错误行(长文案 2.6s toast 看不完) + // 注册弹窗内常驻错误行(长文案 2.6s toast 看不完) function regShowError(msg) { toast(msg, 'error'); var line = $('#reg-error-line'); @@ -391,8 +391,8 @@ function getDeviceId() { } /* ---------------- 找回密码 ---------------- */ - // 第 84 条:F5.1 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。 - // 后端 /auth/forgot_password(第 79 条)已就绪:验证码原子消费、防枚举 + // 只交付了找回视图 DOM 未实现 JS——此处补齐全链路。 + // 后端 /auth/forgot_password 已就绪:验证码原子消费、防枚举 // 统一文案、login_seq 踢全会话、5 次/分/IP 限流;发码复用 /auth/send_code。 function forgotShowError(msg) { toast(msg, 'error'); @@ -470,7 +470,7 @@ function getDeviceId() { .finally(function () { btn.disabled = false; btn.textContent = '重置密码'; }); } - /* ---------------- 管理员两步验证(第 86 条) ---------------- + /* ---------------- 管理员两步验证 ---------------- * Step1 用户名+密码 → 服务端签发票据(不建会话);本视图完成 * Step2 绑定邮箱比对(触发发码)与 Step3 验证码登录。普通用户不经过此流程。 */ var _a2faTicket = ''; @@ -495,7 +495,7 @@ function getDeviceId() { _a2faTicket = ticket || ''; var mask = $('#a2fa-email-mask'); if (mask) mask.textContent = emailMask || '-'; - // 第 87 条:清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交) + // 清掉上次会话残留的输入(错码/旧邮箱不再被原样重复提交) var ae = $('#a2fa-email'), ac = $('#a2fa-code'); if (ae) ae.value = ''; if (ac) ac.value = ''; @@ -515,7 +515,7 @@ function getDeviceId() { a2faClearError(); a2faStopTimer(); } - // 第 87 条:关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用, + // 关闭侧统一复位——登录弹窗的 X/遮罩/Escape/doLogout 关闭路径都调用, // 不再只依赖"打开侧纪律";同时供 index.html 的 Escape 通用分支使用 window.resetAuthViews = function () { hideForgotView(); hideAdmin2faView(); }; function doA2faSendCode() { @@ -527,7 +527,7 @@ function getDeviceId() { api('/auth/admin_login/email', { ticket: _a2faTicket, email: email }) .then(function (data) { if (data.status === 401) { - // 第 87 条:先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示 + // 先复位视图再提示——错误行随视图隐藏,此处 toast 是唯一可达提示 hideAdmin2faView(); a2faShowError(data.message || '请重新登录'); return; @@ -558,7 +558,7 @@ function getDeviceId() { api('/auth/admin_login/verify', { ticket: _a2faTicket, code: code }) .then(function (data) { if (data.status === 401) { - // 第 87 条:先复位视图再提示(同上,错误行随视图隐藏) + // 先复位视图再提示(同上,错误行随视图隐藏) hideAdmin2faView(); a2faShowError(data.message || '请重新登录'); return; @@ -582,7 +582,7 @@ function getDeviceId() { var email = ($('#reg-email') || {}).value || ''; var code = ($('#reg-code') || {}).value || ''; regClearError(); - // 第 81 条:提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化 + // 提交侧兜底闸(入口已统一走 openRegisterFlow,此处防任何程序化 // 打开路径绕过)。fail-closed:协议闸缺失视为未同意。注册语境只查协议同意 // (功能性 Cookie 不拦注册,拦解析/下载时另有提示);未同意时闸内已自动 // 弹「隐私与协议设置」注册模式并 toast,此处不再重复提示 @@ -591,7 +591,7 @@ function getDeviceId() { return; } if (!window.SITE_COOKIE_GATE('register')) return; - // F1.1:前端即时校验(口径与后端一致,省一次网络往返) + // 前端即时校验(口径与后端一致,省一次网络往返) if (!username || !password || !email || !code) { regShowError('请填写完整注册信息'); return; } if (!_REG_NAME_RE.test(username)) { regShowError('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'); return; } if (!_REG_EMAIL_RE.test(email.trim())) { regShowError('邮箱格式不正确'); return; } @@ -619,10 +619,10 @@ function getDeviceId() { api('/auth/logout', {}).then(function () { user = null; durationText = ''; renderAuthArea(); - renderUserCenter(); // 第 83 条:同步个人中心 gate/content 显隐,与其他登出路径一致 - if (window.resetAuthViews) window.resetAuthViews(); // 第 87 条:登出复位登录弹窗视图 + renderUserCenter(); // 同步个人中心 gate/content 显隐,与其他登出路径一致 + if (window.resetAuthViews) window.resetAuthViews(); // 登出复位登录弹窗视图 closeModal('register-modal'); closeModal('login-modal'); - // 第 102 条(U15 拍板):退出连带清除个人网易云登录(index.html 注册的钩子: + // 退出连带清除个人网易云登录(index.html 注册的钩子: // 清 localStorage 的个人 Cookie/解析账号选择并复位账号栏) if (typeof window.SITE_ON_LOGOUT === 'function') { try { window.SITE_ON_LOGOUT(); } catch (e) {} @@ -680,7 +680,7 @@ function getDeviceId() { .then(function (data) { if (data.status !== 200) throw new Error(data.message || '兑换失败'); durationText = (data.data && data.data.duration_text) || durationText; - // F69:同步更新内存余额(批量下载预检读的是 user.duration_seconds, + // 同步更新内存余额(批量下载预检读的是 user.duration_seconds, // 原实现只改显示文字,预检仍用充值前的旧值) if (user && data.data && typeof data.data.duration_seconds === 'number') { user.duration_seconds = data.data.duration_seconds; @@ -700,7 +700,7 @@ function getDeviceId() { /* ---------------- 公告 ---------------- */ function maybeShowAnnouncement(force) { - // F81:手动"查看公告"(force=true)无条件展示; + // 手动"查看公告"(force=true)无条件展示; // 自动弹出仍按 popup_mode 与 once 已读版本判断 if (!force && (!ann || !ann.content || ann.popup_mode === 'off')) return; if (!force && ann && ann.popup_mode === 'once') { @@ -717,7 +717,7 @@ function getDeviceId() { var t = $('#announce-title'), c = $('#announce-content'); if (t) t.textContent = ann.title || '系统公告'; if (c) c.innerHTML = String(ann.content).split('\n').map(function (l) { return escapeHtml(l); }).join('
'); - // 第 98 条:首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱); + // 首访 Cookie 协议窗显示中不叠加公告弹窗(双遮罩错乱); // 跳过时不记已读,下次进入页面再弹 var _cm = document.getElementById('cookie-modal'); if (_cm && _cm.classList.contains('show')) return; @@ -747,7 +747,7 @@ function getDeviceId() { function uaBindSide() { $$('#admin-side .admin-side-item[data-view]').forEach(function (item) { - // 第 97 条(P3):侧栏项补键盘语义(F109 同型——tabindex/role + Enter/空格激活) + // 侧栏项补键盘语义(同型——tabindex/role + Enter/空格激活) item.setAttribute('tabindex', '0'); item.setAttribute('role', 'button'); item.addEventListener('click', function () { @@ -767,7 +767,7 @@ function getDeviceId() { }); var lo = $('#ua-logout'); if (lo) lo.addEventListener('click', doLogout); - // 第 97 条(P3):返回/退出两个无 data-view 的侧栏项同样补键盘语义 + // 返回/退出两个无 data-view 的侧栏项同样补键盘语义 [back, lo].forEach(function (el) { if (!el) return; el.setAttribute('tabindex', '0'); @@ -776,7 +776,7 @@ function getDeviceId() { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } }); }); - /* ---- 播放 API 密钥(第 78 条) ---- */ + /* ---- 播放 API 密钥 ---- */ var pkShow = $('#ua-playkey-show'); var pkInput = $('#ua-playkey-value'); function pkCopyToClipboard(text) { @@ -891,12 +891,12 @@ function getDeviceId() { var isAdmin = u.role === 'admin'; var disabled = (u.status === 0); var banned = (u.status === 2); - // 第 97 条(P3):数值字段同过转义(纵深防御——后端口径若变为字符串 + // 数值字段同过转义(纵深防御——后端口径若变为字符串 // 不再形成管理后台存储型 XSS 面,与同表 username/email 口径对齐) var uid = escapeHtml(String(u.id)); var dur = escapeHtml(String(u.duration_seconds)); var cred = u.duration_seconds < 0 ? '不限' : (dur + ' 次'); - var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // R5-79:IP 优先显示 + var dev = [u.register_ip, u.trial_device].filter(Boolean).join(' · ') || '-'; // IP 优先显示 if (u.last_province) dev += ' · ' + u.last_province; return '' + '' + uid + '' + @@ -925,7 +925,7 @@ function getDeviceId() { .then(function (d) { toast(d.message || (d.status === 200 ? '已修改' : '修改失败'), d.status === 200 ? 'success' : 'error'); if (d.status === 200) uaLoadUsers(); }); }); }); - // T3:管理员自助改密(需验证旧密码;改完其他设备全部下线) + // 管理员自助改密(需验证旧密码;改完其他设备全部下线) $$('[data-uself]', body).forEach(function (b) { b.addEventListener('click', function () { var oldpwd = prompt('请输入当前密码:', ''); @@ -990,7 +990,7 @@ function getDeviceId() { } /* ---- 全局 Cookie ---- */ - var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // T2:轮询代次令牌(删除 Cookie 后作废旧轮询) + var uaQrTimer = null, uaUnikey = '', uaQrGen = 0; // 轮询代次令牌(删除 Cookie 后作废旧轮询) function uaLoadCookieStatus() { api('/admin/cookie/status', {}).then(function (data) { var el = $('#ua-cookie-status'); @@ -1013,13 +1013,13 @@ function getDeviceId() { } if (tip) tip.textContent = '请使用网易云音乐手机 APP 扫码'; if (uaQrTimer) clearInterval(uaQrTimer); - uaQrGen++; // T2:新轮询代次 + uaQrGen++; // 新轮询代次 uaQrTimer = setInterval(uaQrPoll, 3000); }); } function uaQrPoll() { if (!uaUnikey) { if (uaQrTimer) clearInterval(uaQrTimer); return; } - var myGen = uaQrGen; // T2:本次轮询所属代次 + var myGen = uaQrGen; // 本次轮询所属代次 api('/admin/qr/check', { unikey: uaUnikey }).then(function (data) { if (myGen !== uaQrGen) return; // 期间已删除 Cookie/重开扫码,丢弃迟到响应 var tip = $('#ua-qr-tip'); @@ -1045,7 +1045,7 @@ function getDeviceId() { function uaCookieDelete() { if (!window.confirm('确定删除全局 Cookie?删除后需重新扫码登录。')) return; api('/admin/cookie/delete', {}).then(function (data) { - // T2:作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回 + // 作废在途扫码轮询——迟到的 803 成功响应不得把刚删除的 Cookie 写回 if (uaQrTimer) clearInterval(uaQrTimer); uaUnikey = ''; uaQrGen++; @@ -1099,7 +1099,7 @@ function getDeviceId() { var total = (data.data && data.data.total) || 0; var pages = (data.data && data.data.pages) || 1; uaLogPage = (data.data && data.data.page) || 1; - window._uaLogPages = pages; // P3-43:next 按钮按真实页数封顶 + window._uaLogPages = pages; // next 按钮按真实页数封顶 var el; if ((el = $('#ua-log-page'))) el.textContent = uaLogPage; if ((el = $('#ua-log-pages'))) el.textContent = pages; @@ -1147,7 +1147,7 @@ function getDeviceId() { uaLoadCards(0); }); } - // 第 81 条:卡密管理操作列(禁用/启用/删除)+ 分页(F85 后端已支持, + // 卡密管理操作列(禁用/启用/删除)+ 分页(后端已支持, // 不分页则超出最新 100 张的旧卡密在界面上永远管理不到) var _uaCardPage = 0; var _uaCardTotal = 0; @@ -1312,7 +1312,7 @@ function getDeviceId() { uaLoadDb(); }) .catch(function (err) { - // 第 97 条(P2):全站唯一绕过统一请求封装的调用此前无失败兜底—— + // 全站唯一绕过统一请求封装的调用此前无失败兜底—— // 网络失败/非 JSON 响应时管理员毫无感知还制造 unhandledrejection 噪音 toast((err && err.message) || '备份上传失败,请检查网络后重试', 'error'); }); @@ -1323,7 +1323,7 @@ function getDeviceId() { bindAuthModal(); var s1 = $('#login-submit'); if (s1) s1.addEventListener('click', doLogin); var s2 = $('#login-password'); if (s2) s2.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); }); - // F-3.1:回车提交覆盖全部输入框(登录用户名框 / 注册四框) + // 回车提交覆盖全部输入框(登录用户名框 / 注册四框) var s2u = $('#login-username'); if (s2u) s2u.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLogin(); }); var s3 = $('#reg-submit'); if (s3) s3.addEventListener('click', doRegister); var s4 = $('#reg-send-code'); if (s4) s4.addEventListener('click', sendCode); @@ -1331,7 +1331,7 @@ function getDeviceId() { var el = $(sel); if (el) el.addEventListener('keydown', function (e) { if (e.key === 'Enter') doRegister(); }); }); - // F-4.1:密码明文切换(登录/注册/找回) + // 密码明文切换(登录/注册/找回) [['#login-pwd-eye', '#login-password'], ['#reg-pwd-eye', '#reg-password'], ['#forgot-pwd-eye', '#forgot-password']].forEach(function (pair) { var eye = $(pair[0]), input = $(pair[1]); if (!eye || !input) return; @@ -1341,7 +1341,7 @@ function getDeviceId() { eye.textContent = show ? '隐藏' : '显示'; }); }); - // F-2.1:发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发 + // 发码后修改邮箱 → 提前终止倒计时并恢复按钮,提示需重发 var regEmail = $('#reg-email'); if (regEmail) regEmail.addEventListener('input', function () { var btn = $('#reg-send-code'); @@ -1351,7 +1351,7 @@ function getDeviceId() { toast('邮箱已修改,请重新发送验证码', 'info'); } }); - // 第 84 条:找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互) + // 找回视图——发码/提交/回车/改邮箱终止倒计时(对齐注册交互) var fsc = $('#forgot-send-code'); if (fsc) fsc.addEventListener('click', doForgotSendCode); var fsub = $('#forgot-submit'); if (fsub) fsub.addEventListener('click', doForgotReset); ['#forgot-email', '#forgot-code', '#forgot-password'].forEach(function (sel) { @@ -1367,7 +1367,7 @@ function getDeviceId() { toast('邮箱已修改,请重新发送验证码', 'info'); } }); - // 第 86 条:管理员两步验证——发码/验证提交/回车提交 + // 管理员两步验证——发码/验证提交/回车提交 var asc = $('#a2fa-send'); if (asc) asc.addEventListener('click', doA2faSendCode); var asb = $('#a2fa-submit'); if (asb) asb.addEventListener('click', doA2faVerify); ['#a2fa-email', '#a2fa-code'].forEach(function (sel) { @@ -1386,7 +1386,7 @@ function getDeviceId() { else { var t = $('#tab-usercenter'); if (t) t.click(); } }); var ucRedeem = $('#uc-card-redeem'); if (ucRedeem) ucRedeem.addEventListener('click', doRedeem); - var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // F81:手动查看强制展示 + var ucAnn = $('#uc-ann-view'); if (ucAnn) ucAnn.addEventListener('click', function () { maybeShowAnnouncement(true); }); // 手动查看强制展示 var ucLogout = $('#uc-logout'); if (ucLogout) ucLogout.addEventListener('click', doLogout); uaBindSide(); @@ -1400,7 +1400,7 @@ function getDeviceId() { var b8 = $('#ua-log-tab-ok'); if (b8) b8.addEventListener('click', function () { uaLogsType = 'success'; uaLogPage = 1; uaLoadLogs(); }); var b9 = $('#ua-log-tab-fail'); if (b9) b9.addEventListener('click', function () { uaLogsType = 'fail'; uaLogPage = 1; uaLoadLogs(); }); var blogPrev = $('#ua-log-prev'); if (blogPrev) blogPrev.addEventListener('click', function () { if (uaLogPage > 1) { uaLogPage--; uaLoadLogs(); } }); - // P3-43:next 补边界(原实现可翻到空页——前端无 pages 时放宽为 999 防呆) + // next 补边界(原实现可翻到空页——前端无 pages 时放宽为 999 防呆) var blogNext = $('#ua-log-next'); if (blogNext) blogNext.addEventListener('click', function () { var maxP = window._uaLogPages || 999; if (uaLogPage < maxP) { uaLogPage++; uaLoadLogs(); } }); var blogClear = $('#ua-log-clear'); if (blogClear) blogClear.addEventListener('click', uaClearLogs); var b10 = $('#ua-card-gen'); if (b10) b10.addEventListener('click', uaGenCards); diff --git a/templates/index.html b/templates/index.html index 4669ee1..7bd7a16 100644 --- a/templates/index.html +++ b/templates/index.html @@ -30,7 +30,7 @@ --color-shadow-heavy: 0 2px 4px rgba(15, 23, 42, 0.03), 0 12px 32px -8px rgba(15, 23, 42, 0.12), 0 32px 64px -24px rgba(15, 23, 42, 0.10); --player-bg: linear-gradient(135deg, #2a2a35, #1c1c26); --player-text: #ffffff; - --vip-green: #52c41a; /* 第 100 条(C2):--vip-gold/--vip-red 死变量已删 */ + --vip-green: #52c41a; /* --vip-gold/--vip-red 死变量已删 */ --color-bg-header: rgba(255, 255, 255, 0.82); } html.dark { @@ -263,7 +263,7 @@ border-radius: 50%; animation: spin 0.8s linear infinite; } - /* 第 100 条(C2):`.btn-default-loading .spinner` 死规则已删(setBtnLoading + /* `.btn-default-loading .spinner` 死规则已删(setBtnLoading 走 .btn .spinner 且全部用于彩色按钮,本选择器从未被命中) */ @keyframes spin { to { transform: rotate(360deg); } } @@ -517,7 +517,7 @@ } .vip-vip { background: linear-gradient(90deg, #f7ba2a, #ea5455); color: #fff; } .vip-svip { background: linear-gradient(90deg, #c8161d, #8f0f14); color: #fff; } - /* 第 100 条(C2):.vip-musician 死规则已删(前端不产生该类名) */ + /* .vip-musician 死规则已删(前端不产生该类名) */ .vip-pkg { background: rgba(82, 196, 26, 0.15); color: var(--vip-green); border: 1px solid rgba(82,196,26,0.4); } .comment-meta { font-size: 12px; color: var(--color-text-tertiary); } .comment-content { font-size: 14px; margin-top: 4px; word-break: break-word; } @@ -624,7 +624,7 @@ .about-card:hover { transform: translateY(-2px); box-shadow: var(--color-shadow); } .about-card-title { font-weight: 600; font-size: 14px; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; } .about-card-desc { font-size: 12.5px; color: var(--color-text-secondary); line-height: 1.7; } - /* 第 100 条(C2):.about-author/.about-qq 等 7 条死规则已删(作者卡片在第 2 条 + /* .about-author/.about-qq 等 7 条死规则已删(作者卡片在 改名时整体移除,样式残留且曾误导 v7 美化层给死类追加规则) */ /* ============ 设置抽屉 ============ */ @@ -787,7 +787,7 @@ border: 1px dashed var(--color-border); border-radius: 10px; padding: 12px; text-align: center; font-size: 13px; color: var(--color-text-secondary); } - /* 第 100 条(C2):.auth-tabs 两条死规则已删(登录/注册弹窗早已改为四视图切换) */ + /* .auth-tabs 两条死规则已删(登录/注册弹窗早已改为四视图切换) */ /* ============ 美化层(视觉与交互打磨,全部基于现有变量体系) ============ */ html { scroll-behavior: smooth; } @@ -892,7 +892,7 @@ } .uc-card-title { font-size: 12px; color: var(--color-text-tertiary); margin-bottom: 6px; } .uc-card-value { font-size: 17px; font-weight: 600; word-break: break-all; } - /* 第 100 条(C2):.uc-card-value.danger 死规则已删 */ + /* .uc-card-value.danger 死规则已删 */ .uc-section { margin-top: 16px; } .admin-layout { display: flex; gap: 16px; align-items: flex-start; } .admin-side { @@ -924,8 +924,8 @@ font-size: 12px; border: 1px solid var(--color-border); color: var(--color-text-secondary); } .tag-pill.ok { border-color: var(--color-success); color: var(--color-success); } - .log-list { max-height: 420px; overflow-y: auto; } /* ua-log-list(管理后台日志页)在用,第 100 条复核保留 */ - /* 第 100 条(C2):.tag-pill.used/.log-item/.log-item .log-time 三条死规则已删 + .log-list { max-height: 420px; overflow-y: auto; } /* ua-log-list(管理后台日志页)在用,复核保留 */ + /* .tag-pill.used/.log-item/.log-item .log-time 三条死规则已删 (管理后台日志走 site-table 渲染,log-item 类从未产生) */ .code-box { background: var(--color-fill); border-radius: 8px; padding: 10px 12px; @@ -1003,10 +1003,10 @@ [hidden] { display: none !important; } - /* 第 84 条:登录弹窗找回密码视图模式——隐藏登录字段只留找回表单 */ + /* 登录弹窗找回密码视图模式——隐藏登录字段只留找回表单 */ .auth-form.forgot-mode > *:not(#forgot-view) { display: none; } - /* 第 86 条:登录弹窗管理员两步验证视图模式——隐藏登录字段只留 2FA 表单 */ + /* 登录弹窗管理员两步验证视图模式——隐藏登录字段只留 2FA 表单 */ .auth-form.admin2fa-mode > *:not(#admin2fa-view) { display: none; } @media (max-width: 768px) { @@ -1348,7 +1348,7 @@ @media (pointer: coarse) { .btn-sm { min-height: 34px; } .icon-btn { min-height: 38px; min-width: 38px; } - .btn-mini { height: 34px; min-height: 34px; padding: 0 10px; } /* R5-8:触屏统一 34px 触控标准 */ + .btn-mini { height: 34px; min-height: 34px; padding: 0 10px; } /* 触屏统一 34px 触控标准 */ .player-ctrl-sm { width: 36px; height: 36px; } .lyric-line { padding: 8px 10px; } .history-row .tag { padding: 4px 10px; } @@ -1358,7 +1358,7 @@ /* ---------- 小屏手机(≤400px,iPhone SE / 小屏安卓) ---------- */ @media (max-width: 400px) { - /* R5-P3(第 79 条):开放邮箱注册后游客顶栏多出「注册」按钮,小屏压缩防横向溢出: + /* 开放邮箱注册后游客顶栏多出「注册」按钮,小屏压缩防横向溢出: 注册/登录按钮缩 padding,图标按钮缩 padding 并收紧间距/标题字号 */ #btn-open-register { padding: 0 8px; font-size: 12px; } #btn-open-login { padding: 0 10px; } @@ -1442,7 +1442,7 @@ .brand-sub { display: none; } .main-container { padding: 12px 16px; } .lyric-box { max-height: 180px; } /* 横屏矮视口:垂直空间稀缺,不随字号放大 */ - /* 弹窗横屏 92dvh 已移至 v6 视口块之后(第 100 条 C1:修复同特异性级联覆盖) */ + /* 弹窗横屏 92dvh 已移至 v6 视口块之后(修复同特异性级联覆盖) */ } /* ============================================================ @@ -1450,12 +1450,12 @@ ============================================================ */ /* ---------- iOS 动态视口(地址栏收缩时 100vh 偏大) ---------- - 第 100 条(C1):此块必须位于 v5 横屏 92dvh 规则之前——同特异性按源序 + 此块必须位于 v5 横屏 92dvh 规则之前——同特异性按源序 后者胜,v6 初版写在横屏块之后曾把"横屏 92dvh"静默覆盖成 80dvh */ body { min-height: 100vh; min-height: 100dvh; } .modal { max-height: 80vh; max-height: 80dvh; } @media (max-width: 480px) { .modal { max-height: 86vh; max-height: 86dvh; } } /* 恢复 v5 手机端高度 */ - @media (max-height: 520px) and (orientation: landscape) { .modal { max-height: 92vh; max-height: 92dvh; } } /* 横屏最高(第 100 条:移到 80dvh 之后修复级联覆盖) */ + @media (max-height: 520px) and (orientation: landscape) { .modal { max-height: 92vh; max-height: 92dvh; } } /* 横屏最高(移到 80dvh 之后修复级联覆盖) */ /* ---------- 播放中:歌名旁均衡器律动(纯伪元素,零 DOM) ---------- */ .player.playing .player-name::before, @@ -1575,7 +1575,7 @@ .admin-view.active { animation: rowIn 0.22s var(--ease-out); } .stat-list-item { border-radius: 8px; transition: background 0.15s; } .stat-list-item:hover { background: var(--color-bg-hover); } - /* 第 100 条(C2):.log-item 两条死规则(含本层追加的 hover)已随基座一并删除 */ + /* .log-item 两条死规则(含本层追加的 hover)已随基座一并删除 */ /* ---------- 防移动端误触下拉刷新 ---------- */ html { overscroll-behavior-y: none; } @@ -1648,8 +1648,8 @@ /* 弹窗/抽屉打开时隐藏实时排队与任务面板浮窗(状态类挂在 上,避免观察器自反馈) */ html.overlay-open .queue-float, html.overlay-open .task-panel { opacity: 0 !important; pointer-events: none !important; } - /* R5-3:字号弹层打开期间同样隐藏两浮窗(弹层贴顶时不再被排队悬浮窗压住)—— - 由 setLyricFsPop 直接挂/摘 html.fs-pop-open 类,不经 MutationObserver(第 53 条活锁教训) */ + /* 字号弹层打开期间同样隐藏两浮窗(弹层贴顶时不再被排队悬浮窗压住)—— + 由 setLyricFsPop 直接挂/摘 html.fs-pop-open 类,不经 MutationObserver(活锁教训) */ html.fs-pop-open .queue-float, html.fs-pop-open .task-panel { opacity: 0 !important; pointer-events: none !important; } .webview-guide { @@ -1698,7 +1698,7 @@ ============================================================ */ /* ---------- 超小屏(≤359px:手表浏览器 / 折叠屏外屏) ---------- - 第 100 条(C3):v10 两批次叠加曾让 gap:3px/font-size:14px 被本块后写 + v10 两批次叠加曾让 gap:3px/font-size:14px 被本块后写 (gap:6px/display:none)静默作废——已合并为最终生效值,消除改前不生效陷阱 */ @media (max-width: 359px) { #btn-open-register { padding: 0 6px; } @@ -2228,7 +2228,7 @@
U
- +
-
@@ -2379,7 +2379,7 @@ 加载中...
- +
@@ -2933,7 +2933,7 @@
@@ -2962,7 +2962,7 @@
还没有账号?立即注册 · 忘记密码?
🎁 新用户?注册账号 注册即送 3 次免费下载次数
- + - +