feat(admin): 新增选手信息编辑接口 PUT /api/admin/contestants/:id(R108)

此前 contestants 表只有 Excel 导入一个写入口 —— 证件号/姓名/手机号录错后
改不了也删不掉。本提交新增受控编辑通道。

service.updateContestant(id, payload):
  · 白名单仅 name / phone / id_number;白名单外字段(group / 成绩类 /
    password / test_flag / team_id / activated)显式 400 拒绝,非静默忽略
    (静默忽略会让管理员误以为改成功)
  · id_number 判据复用 lib/idCard.js(与导入通道同源):长度 != 18 拒绝;
    校验位不符 / 含非法字符 => 放行 + warnings[](生产库现存正式考生中
    即有 4 条校验位不符,硬拦截会造成合法数据改不动)
  · 唯一性冲突 => 409
  · 改号连带重算口令:导入时 password = sha256(证号后 6 位),而证号即登录
    账号 => 不同步重算则学生「账号对、口令错」(UI 上看不出来)。4 分支:
    测试数据 / 已激活 / 已是 bcrypt => 不动 + 告警;未激活派生值 => 重算
  · 同值提交(含仅大小写差异)不发 UPDATE
  · 审计日志证号脱敏、口令只记「是否重算」

controller.updateContestant:映射 400/404/409。
router:新增 PUT /api/admin/contestants/:id(adminAuth)。破坏面定级 =
非破坏性(白名单编辑 + 可逆 + 带审计)=> 未登记 DESTRUCTIVE_ENDPOINTS.json。

判据:scripts/verify_update_contestant.mjs(42 判据 + 4 变异自证)。
行号漂移纪律:controller/admin.js 被 3 个脚本按行号引用(:33/:1556/:1570)
=> 新增一律追加文件末尾,改后 3/3 未漂移。
This commit is contained in:
2026-10-12 17:20:41 +08:00
parent f87ed306d2
commit 706ff2507e
3 changed files with 308 additions and 15 deletions

View File

@@ -205,7 +205,14 @@ class AdminController extends Controller {
// 非参赛选手行按业务规则主动忽略(报名表含报名人/指导教师/领队),在提示中单列以免被误读为失败
const ignoredTip = result.ignored ? `,忽略非参赛选手 ${result.ignored} 条` : '';
ctx.body = ctx.helper.success(result, `导入完成:成功 ${result.imported} 条,跳过 ${result.skipped} 条${ignoredTip}`);
// R107:18 位但校验位不符 / 含非法字符的行仍会导入,但需人工复核 —— 摘要里给出条数,
// 明细随 data.warnings 一并回传(前端在「导入结果」对话框展示)。
const warnTip = (result.warnings && result.warnings.length)
? `,${result.warnings.length} 行需人工复核` : '';
ctx.body = ctx.helper.success(
result,
`导入完成:成功 ${result.imported} 条,跳过 ${result.skipped} 条${ignoredTip}${warnTip}`
);
} catch (err) {
ctx.logger.error('Excel 导入失败:', err);
const msg = (err && err.message) ? err.message : '未知错误';
@@ -2219,6 +2226,49 @@ class AdminController extends Controller {
ctx.body = ctx.helper.error(500, '获取考生全卷明细失败');
}
}
// ==================== 选手信息编辑(R108 新增) ====================
/**
* 编辑已入库选手的身份字段
* PUT /api/admin/contestants/:id
*
* Body: `{ name?, phone?, id_number? }` —— 仅这三项可改,其余字段一律拒绝(400)。
* ⚠️ **可改 / 不可改的完整口径与理由**(组别为何只读、成绩字段为何禁止、改号后
* 口令如何同步)是 `service/admin.js#updateContestant` 的方法头注释 —— 那是唯一真相源,
* 此处不复制,以免两处口径漂移。
*
* 状态码映射:NOT_FOUND → 404;DUPLICATE_ID(身份证号被他人占用)→ 409;
* BAD_ID / FIELD_NOT_ALLOWED / INVALID → 400。
*/
async updateContestant() {
const { ctx, service } = this;
if (!this._ensureAdmin()) return;
try {
const cid = parseInt(ctx.params.id, 10);
if (isNaN(cid)) {
ctx.body = ctx.helper.error(400, '选手 ID 非法');
return;
}
const result = await service.admin.updateContestant(cid, ctx.request.body || {});
if (!result.ok) {
const status = result.code === 'NOT_FOUND' ? 404
: (result.code === 'DUPLICATE_ID' ? 409 : 400);
ctx.body = ctx.helper.error(status, result.message);
return;
}
ctx.body = ctx.helper.success(
result,
result.changed.length === 0 ? '未检测到需要修改的内容' : '选手信息已更新'
);
} catch (err) {
ctx.logger.error('编辑选手信息失败:', err);
ctx.body = ctx.helper.error(500, '编辑选手信息失败');
}
}
}
module.exports = AdminController;