fix(admin): 修复选手管理「激活状态」「注册日期」筛选被静默丢弃

现象(人工测试反馈):选手管理页「激活状态」下拉选择后列表无变化。

根因 —— 两层都在丢参且零报错:
  - 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
This commit is contained in:
2026-10-08 11:22:18 +08:00
parent 15c9ab6f91
commit a471487092
2 changed files with 22 additions and 4 deletions

View File

@@ -111,8 +111,14 @@ class AdminController extends Controller {
if (!this._ensureAdmin()) return; if (!this._ensureAdmin()) return;
try { try {
const { group, keyword, page = 1, pageSize = 20 } = ctx.query; // ⚠️ 筛选条件必须**整体透传**给 service。此处曾只解构 group/keyword/page/pageSize,
const result = await service.admin.getAccounts({ 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, '获取账号列表成功'); ctx.body = ctx.helper.success(result, '获取账号列表成功');
} catch (err) { } catch (err) {
ctx.logger.error('获取账号列表失败:', err); ctx.logger.error('获取账号列表失败:', err);

View File

@@ -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<Object>} { list, total, page, pageSize } * @returns {Promise<Object>} { list, total, page, pageSize }
*/ */
async getAccounts(options = {}) { async getAccounts(options = {}) {
const { ctx } = this; const { ctx } = this;
const { group, keyword, startDate, endDate } = options; const { group, keyword, activated, startDate, endDate } = options;
const page = parseInt(options.page, 10) || 1; const page = parseInt(options.page, 10) || 1;
const pageSize = parseInt(options.pageSize, 10) || 20; const pageSize = parseInt(options.pageSize, 10) || 20;
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
@@ -124,6 +124,18 @@ class AdminService extends Service {
if (group) { if (group) {
where.group = 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) { if (keyword) {
where[ctx.model.Sequelize.Op.or] = [ where[ctx.model.Sequelize.Op.or] = [
{ name: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } }, { name: { [ctx.model.Sequelize.Op.like]: `%${keyword}%` } },