feat(admin): R176-R180 公示表与新闻撤回 + 管理端多管理员权限(C 层)+ 交卷冻结

- R176 成绩公示表引擎(lib/publicitySheet.js + lib/assets/publicity-template.xlsx 官方模板基底)
  与指导教师回填(lib/teacherBackfill.js);赛项名称/编号改为配置项
- R177 新闻「发布 ↔ 撤回」互斥:撤回只置 is_draft=1,不删除数据
- R178 C 层 RBAC:lib/adminPermissions.js(13 权限点 / 70 条路由规则,唯一 SoT)
  + middleware/auth.js 三段判定(默认拒绝 · is_super 单点 · token_version)
  + controller/service/adminUser.js + lib/auditLog.js + model/adminAuditLog.js
- R178 B1 交卷冻结:service.exam.assertNotSubmitted(bizCode 4006 单一判据源)
- R178 B④ 提交口径拆分:formalSubmitted / submitted / answeredCount 三字段
- R178 成绩总览补小组编号:resolveTeamLabels(与选手管理同源,含兜底与三态)
- R181 生产迁移配套:admins +7 列、admin_audit_logs(scripts/migrate_r178_admin_rbac.py)
This commit is contained in:
2026-10-20 11:06:29 +08:00
parent 5392345078
commit 459f1adf6d
21 changed files with 2886 additions and 76 deletions

118
app/controller/adminUser.js Normal file
View File

@@ -0,0 +1,118 @@
'use strict';
/**
* 管理员账号管理控制器(R178 · C 层)
*
* 统一错误约定(与项目其它控制器一致):
* - 业务错误 = HTTP 200 + `code !== 0`(helper.error 不改 HTTP 状态)
* - service 通过 `bizCode` 表达期望错误码,这里原样翻译
* ⛔ 不要把业务错误包成 500(前端会当成系统故障,用户看到「服务器错误」)
*/
const Controller = require('egg').Controller;
const adminPermissions = require('../lib/adminPermissions');
class AdminUserController extends Controller {
/** 把 service 抛出的 bizCode 翻译成业务响应;非业务异常交给上层 catch */
_fail(err, fallbackMessage) {
const { ctx } = this;
if (err && err.bizCode) {
ctx.body = ctx.helper.error(err.bizCode, err.bizMessage || err.message);
return;
}
ctx.logger.error(`${fallbackMessage}:`, err);
ctx.body = ctx.helper.error(500, fallbackMessage);
}
/**
* 当前登录管理员的权限信息(自助,无需业务权限)
* GET /api/admin/me
*
* 前端用途:① 渲染侧边菜单;② 权限被他人改动后,下次进页面能感知并提示。
* 顺带返回权限点目录(catalog),避免前端硬编码一份权限清单(那是第二真相源)。
*/
async me() {
const { ctx } = this;
const admin = ctx.state.currentAdmin;
if (!admin) {
ctx.body = ctx.helper.error(401, '未登录或登录状态已失效');
return;
}
ctx.body = ctx.helper.success({
id: admin.id,
username: admin.username,
isSuper: Number(admin.is_super) === 1,
permissions: admin.permissions || [],
catalog: adminPermissions.PERMISSIONS,
}, '获取当前管理员信息成功');
}
/** GET /api/admin/admin-users */
async list() {
const { ctx, service } = this;
try {
const list = await service.adminUser.list();
ctx.body = ctx.helper.success({ list, total: list.length }, '获取管理员列表成功');
} catch (err) {
this._fail(err, '获取管理员列表失败');
}
}
/** POST /api/admin/admin-users */
async create() {
const { ctx, service } = this;
try {
const result = await service.adminUser.create(ctx.request.body || {});
ctx.body = ctx.helper.success(result, '管理员账号已创建');
} catch (err) {
this._fail(err, '创建管理员失败');
}
}
/** PUT /api/admin/admin-users/:id/permissions */
async updatePermissions() {
const { ctx, service } = this;
try {
const result = await service.adminUser.updatePermissions(ctx.params.id, ctx.request.body || {});
ctx.body = ctx.helper.success(result, '权限已更新');
} catch (err) {
this._fail(err, '更新权限失败');
}
}
/** PUT /api/admin/admin-users/:id/password */
async resetPassword() {
const { ctx, service } = this;
try {
const result = await service.adminUser.resetPassword(ctx.params.id, ctx.request.body || {});
ctx.body = ctx.helper.success(result, '口令已重置,该账号需重新登录');
} catch (err) {
this._fail(err, '重置口令失败');
}
}
/** PUT /api/admin/admin-users/:id/status */
async setStatus() {
const { ctx, service } = this;
try {
const result = await service.adminUser.setStatus(ctx.params.id, ctx.request.body || {});
const msg = Number(result.status) === 1 ? '账号已启用' : '账号已禁用,其在线会话已失效';
ctx.body = ctx.helper.success(result, msg);
} catch (err) {
this._fail(err, '修改账号状态失败');
}
}
/** DELETE /api/admin/admin-users/:id */
async remove() {
const { ctx, service } = this;
try {
const result = await service.adminUser.remove(ctx.params.id);
ctx.body = ctx.helper.success(result, '管理员账号已删除');
} catch (err) {
this._fail(err, '删除管理员失败');
}
}
}
module.exports = AdminUserController;