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:
@@ -3,56 +3,66 @@
|
||||
/**
|
||||
* 竞赛信息模型 - CompetitionInfo
|
||||
*
|
||||
* 存储竞赛的基本信息,包括竞赛名称和代码。
|
||||
* 表名:competition_info
|
||||
*
|
||||
* 🔴 2026-10-19(R176)修正:原模型声明的是三列结构
|
||||
* (`competition_uid` / `competition_name` / `competition_code`),
|
||||
* 但**运行库实际是 key-value 结构**:
|
||||
* `id` / `info_key` / `info_value` / `created_at` / `updated_at`(实测 SHOW CREATE TABLE)
|
||||
* 模型与表不一致的后果:
|
||||
* ① `findOne()` 默认按模型声明选列 ⇒ `Unknown column 'competition_uid'` 直接 500;
|
||||
* ② `create()` 会带上 `competition_name`(allowNull:false)⇒ 必然失败。
|
||||
* 旧代码之所以没暴露,是因为唯一的使用点(导入时取 competition_uid)
|
||||
* 包在 try/catch 里,失败后静默回落到默认值 1 —— 而 `info_key='competition_uid'`
|
||||
* 的值恰好也是 1,所以"看起来一直是对的"。
|
||||
*
|
||||
* ⚠️ 因此本表**不能按列访问**,必须走 `info_key` 取值。
|
||||
* 公示表要用的两个键是 `competition_item_name` / `competition_item_code`。
|
||||
*/
|
||||
|
||||
module.exports = app => {
|
||||
const { STRING, INTEGER, DATE } = app.Sequelize;
|
||||
const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;
|
||||
|
||||
const CompetitionInfo = app.model.define('competition_info', {
|
||||
// 主键,自增
|
||||
competition_uid: {
|
||||
// 主键,自增(运行库真实主键是 id,不是 competition_uid)
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '竞赛唯一标识',
|
||||
comment: '自增主键',
|
||||
},
|
||||
// 竞赛名称
|
||||
competition_name: {
|
||||
type: STRING(200),
|
||||
allowNull: false,
|
||||
comment: '竞赛名称',
|
||||
},
|
||||
// 竞赛代码(唯一标识码)
|
||||
competition_code: {
|
||||
// 配置键名(如 competition_name / competition_code / competition_item_name)
|
||||
// ⚠️ 运行库**没有**给 info_key 建唯一索引 ⇒ 写侧必须"先查再写",不能依赖 upsert
|
||||
info_key: {
|
||||
type: STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '竞赛代码',
|
||||
comment: '配置键',
|
||||
},
|
||||
// 配置值(统一按字符串存,数字也以文本形式落库)
|
||||
info_value: {
|
||||
type: TEXT,
|
||||
allowNull: true,
|
||||
comment: '配置值',
|
||||
},
|
||||
// 记录创建时间
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
allowNull: true,
|
||||
comment: '创建时间',
|
||||
},
|
||||
// 记录更新时间
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
allowNull: true,
|
||||
comment: '更新时间',
|
||||
},
|
||||
}, {
|
||||
tableName: 'competition_info',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
// 索引仅作 Sequelize 元数据声明(sync 时用);⛔ 本项目的表由 SQL 手工维护,
|
||||
// 不要指望这里加 unique 会同步到运行库
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['competition_code'],
|
||||
fields: ['info_key'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user