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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user