From a471487092043bfe1c914161fef7495cb504b3e1 Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Thu, 8 Oct 2026 11:22:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E4=BF=AE=E5=A4=8D=E9=80=89?= =?UTF-8?q?=E6=89=8B=E7=AE=A1=E7=90=86=E3=80=8C=E6=BF=80=E6=B4=BB=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E3=80=8D=E3=80=8C=E6=B3=A8=E5=86=8C=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E3=80=8D=E7=AD=9B=E9=80=89=E8=A2=AB=E9=9D=99=E9=BB=98=E4=B8=A2?= =?UTF-8?q?=E5=BC=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象(人工测试反馈):选手管理页「激活状态」下拉选择后列表无变化。 根因 —— 两层都在丢参且零报错: - controller/admin.js 只解构 group/keyword/page/pageSize, 前端已发出的 activated 从未透传给 service - service/admin.js 即使收到也未解构 activated,where 中根本没有该条件 - 同族缺陷:前端还发了 startDate/endDate(注册日期范围), controller 同样未转发 => 该筛选同样失效(此前无人报告) 修复: - controller:将 activated/startDate/endDate 整体透传给 service - service:解构 activated,并对 BOOLEAN 列做白名单归一化 前端 el-select 传的是字符串 'true'/'false',直接入 where 会被 MySQL 隐式转为 0,导致「已激活」错查出「未激活」(比不筛选更难察觉); 白名单之外的值(含 undefined/'')一律视为未指定忽略, 避免畸形入参被当作 false 而静默缩窄结果集 验证(scripts/verify_accounts_filter.py,9/9 PASS,真实 HTTP + 独立 DB 真值): A1 无筛选 total=26 == DB 26 A2 activated=true -> 10 == DB 已激活数 A3 activated=false -> 16 A4 分区性 10 + 16 == 26(关键判别器) D1 日期限定 DB 实际存在那天 -> 20 == DB 当日 D2 负向对照(范围早于最早记录)-> 0 变异自证:拆掉 where.activated 分支重跑 -> A2/A2b/A3/A3b/A4 全红 EXIT=1, 失败信息精确复现原现象(total=26 != 10);还原后 diff 空、md5 一致、无标记残留 同族扫描:全后端 ctx.query 仅 11 处,逐点核对 => 仅 getAccounts 一对丢参 部署(生产 back-end): 线上 md5 与本地逐文件一致(controller ad0e60f8 / service 93eb3dbb) MainPID 3341710 -> 3343212;8023 监听正常;err 级日志 -- No entries -- 路由探针:health 200 / admin-login 错误口令 HTTP200+code4004 / 不存在路由 404 / 未授权 accounts 401 --- app/controller/admin.js | 10 ++++++++-- app/service/admin.js | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/controller/admin.js b/app/controller/admin.js index 184f7c8..27600d2 100644 --- a/app/controller/admin.js +++ b/app/controller/admin.js @@ -111,8 +111,14 @@ class AdminController extends Controller { if (!this._ensureAdmin()) return; try { - const { group, keyword, page = 1, pageSize = 20 } = ctx.query; - const result = await service.admin.getAccounts({ group, keyword, page, pageSize }); + // ⚠️ 筛选条件必须**整体透传**给 service。此处曾只解构 group/keyword/page/pageSize, + // 导致前端已发出的 activated(激活状态)与 startDate/endDate(注册日期范围) + // 被静默丢弃 —— 选手管理页这两个筛选条件完全失效,且不报任何错 + // (2026-10-08 人工测试反馈「激活状态搜索条件无效」)。 + const { group, keyword, activated, startDate, endDate, page = 1, pageSize = 20 } = ctx.query; + const result = await service.admin.getAccounts({ + group, keyword, activated, startDate, endDate, page, pageSize, + }); ctx.body = ctx.helper.success(result, '获取账号列表成功'); } catch (err) { ctx.logger.error('获取账号列表失败:', err); diff --git a/app/service/admin.js b/app/service/admin.js index 39ed62e..d8fc323 100644 --- a/app/service/admin.js +++ b/app/service/admin.js @@ -109,12 +109,12 @@ class AdminService extends Service { /** * 获取分页账号列表 - * @param {Object} options - 查询选项 { group, keyword, page, pageSize, startDate, endDate } + * @param {Object} options - 查询选项 { group, keyword, activated, page, pageSize, startDate, endDate } * @returns {Promise} { list, total, page, pageSize } */ async getAccounts(options = {}) { const { ctx } = this; - const { group, keyword, startDate, endDate } = options; + const { group, keyword, activated, startDate, endDate } = options; const page = parseInt(options.page, 10) || 1; const pageSize = parseInt(options.pageSize, 10) || 20; const offset = (page - 1) * pageSize; @@ -124,6 +124,18 @@ class AdminService extends Service { if (group) { where.group = group; } + // 激活状态筛选(2026-10-08 修复:此前 controller 未透传、本函数也未解构该参数, + // ⇒ 选手管理页「激活状态」下拉完全失效且不报错)。 + // ⚠️ 前端 el-select 传的是字符串 'true'/'false',**不能直接塞进 where**: + // activated 是 BOOLEAN(TINYINT(1)) 列,MySQL 会把 'true' 隐式转成 0, + // 结果「已激活」错查出「未激活」。必须显式归一化为布尔。 + // 白名单之外的值(含 undefined/'' 等)一律视为「未指定」忽略, + // 避免畸形入参被当成 false 而静默缩窄结果集。 + if ([true, 1, '1', 'true'].includes(activated)) { + where.activated = true; + } else if ([false, 0, '0', 'false'].includes(activated)) { + where.activated = false; + } if (keyword) { where[ctx.model.Sequelize.Op.or] = [ { name: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } },