From 29ce734ffaf9cdad902a9d49c3850a067290e39d Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Fri, 9 Oct 2026 15:53:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20getTeams=E3=80=8C=E5=B7=B2?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E3=80=8D=E5=8F=A3=E5=BE=84=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E4=B8=BA=E3=80=8C=E8=87=B3=E5=B0=91=E4=B8=80=E5=90=8D=E6=88=90?= =?UTF-8?q?=E5=91=98=E6=BF=80=E6=B4=BB=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R90续三:需求方确认该筛选的设计目的是「确认小组是否已有人激活可以参赛」, 故 partial(部分激活)应归入已激活侧查看,旧口径「全员激活」与之不符。 - 行字段 activated:activatedCount === memberCount → activatedCount > 0 - 新增 allActivated 字段保留严格「全员激活」语义(不丢信息) - summary 新增筛选后三态分布 filteredActivated / filteredPartial / filteredInactive - 筛选逻辑零改动:语义收敛在行字段 activated 上,筛选仍按该字段判定 - 同步修正方法头 JSDoc 与筛选/summary 段注释 预期副作用:选中「已有成员激活」时 filteredMemberActivated 恒等于全集 memberActivated(inactive 组贡献 0)⇒ 原先「选手列表激活 4 人 / 小组视图 3 人」 的差额(= partial 组内已激活成员)不再出现。 生产已部署并复验:线上 33 项断言 ALL_PASS,activated=true 由 2 组变为 3 组 (2 全员 + 1 部分)、成员激活 3 → 4;服务器侧 MySQL 只读交叉复核一致。 --- app/service/admin.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/app/service/admin.js b/app/service/admin.js index 573c7e7..880d0ee 100644 --- a/app/service/admin.js +++ b/app/service/admin.js @@ -260,9 +260,11 @@ class AdminService extends Service { * 同一选手若两处都有归属,**只计一次**(以 aggregation 为准,避免重复计数)。 * - **学校名称**取自 `aggregation.organization_cn`。同一小组理论上同校; * 若出现多校(异常数据),并列展示并置 `schoolConflict=true` 供排查,**不静默取一个**。 - * - **激活判据**:`activated` = 组内**全部**成员 `contestants.activated=1`。 - * 三个细分状态:`activated`(全激活)/ `partial`(部分激活)/ `inactive`(全未激活)。 - * ⚠️ `partial` 在「已激活/未激活」二选一筛选里归入**未激活**(因为尚未全员可登录)。 + * - **三态判定**(字段 `status`):`activated`(全员激活)/ `partial`(部分激活)/ `inactive`(全未激活)。 + * - **🔴「已激活」语义(R90 续三修正)**:行字段 `activated` = 组内**至少 1 名**成员已激活 + * —— 设计目的即为「确认该小组是否已有人激活、**可以参赛**」,故 `partial` 归入**已激活**侧, + * 仅 `inactive` 归入**未激活**侧(旧口径「全员激活」与产品语义不符,已修正)。 + * 严格「全员激活」语义另存于 `allActivated`;前端表格按 `status` 渲染三态标签,显示不受影响。 * - **测试数据口径**:默认 `includeTest=false` ⇒ 只统计 `test_flag=0` 的成员; * 某小组若成员全为测试数据,则该小组整体不出现(这正是「生产口径」想要的)。 * @@ -270,6 +272,7 @@ class AdminService extends Service { * @returns {Promise} { list, summary, filteredTotal, filters } * summary 为**筛选前全集**口径,含 total/activated/partial/inactive/memberTotal/memberActivated * 以及 R90 增补的 memberActivatedInPartial / filteredTotal / filteredMemberTotal / filteredMemberActivated + * 与 R90 续三增补的 filteredActivated / filteredPartial / filteredInactive(筛选后三态分布) */ async getTeams(options = {}) { const { ctx } = this; @@ -366,7 +369,12 @@ class AdminService extends Service { schoolConflict: b.schools.size > 1, memberCount, activatedCount, - activated: memberCount > 0 && activatedCount === memberCount, + // 🔴 R90 续三:「已激活」= 组内**至少 1 名**成员已激活(= 可以参赛),partial 归入已激活侧。 + // 旧口径「全员激活」与产品语义(确认小组是否已有人激活可参赛)不符,已修正。 + // 筛选逻辑随之无需改动(下方 list 过滤仍按本字段判定)。 + activated: memberCount > 0 && activatedCount > 0, + // 严格「全员激活」语义(前端表格按 status 渲染三态标签,本项供需严格判定的场景) + allActivated: memberCount > 0 && activatedCount === memberCount, status, }); } @@ -390,6 +398,8 @@ class AdminService extends Service { }); // 筛选(与 getAccounts 的 activated 归一化同理:前端 el-select 传字符串) + // 🔴 R90 续三口径:activated=true ⇒「至少 1 名成员已激活」= activated 组 + partial 组(可参赛); + // activated=false ⇒ 仅 inactive 组(无任何成员激活)。判据统一挂在行字段 activated 上。 let list = all; if ([true, 1, '1', 'true'].includes(activated)) { list = all.filter(t => t.activated === true); @@ -399,9 +409,12 @@ class AdminService extends Service { // R90:给前端「双口径」汇总用(人工测试反馈:筛选后小组数 3 与成员激活数 4 看似矛盾)。 // 既有 6 个字段语义一律不变(仍为**筛选前全集**口径),仅新增下列字段: - // · memberActivatedInPartial —— 「部分激活」组内已激活的成员数(∅=恒为全集口径)。 - // 它是「成员激活总数 − 筛选出的已激活组成员数」差额的来源,正是消除歧义的关键量。 + // · memberActivatedInPartial —— 「部分激活」组内已激活的成员数(恒为全集口径)。 // · filteredTotal / filteredMemberTotal / filteredMemberActivated —— **当前筛选后**口径。 + // R90 续三:口径修正为「activated = 至少 1 名成员激活」后,选中「已激活」时 partial 组一并纳入 + // ⇒ filteredMemberActivated 恒等于全集 memberActivated(inactive 组贡献 0), + // 「选手列表激活人数」与「已激活小组成员数」不再有差额。 + // 另补 filteredActivated / filteredPartial / filteredInactive,供前端展示筛选后三态分布。 const allSummary = summarize(all); const filteredSummary = summarize(list); @@ -415,6 +428,10 @@ class AdminService extends Service { filteredTotal: filteredSummary.total, filteredMemberTotal: filteredSummary.memberTotal, filteredMemberActivated: filteredSummary.memberActivated, + // R90 续三:筛选后三态分布(选中「已激活」时 = 全员激活 + 部分激活) + filteredActivated: filteredSummary.activated, + filteredPartial: filteredSummary.partial, + filteredInactive: filteredSummary.inactive, }, // summary 作用于**筛选前**的全集(给总览用);filteredTotal 说明筛选后条数 filteredTotal: list.length,