Files
competition-website/app/model/contestant.js
qubiaobiao 459f1adf6d 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)
2026-10-20 11:06:29 +08:00

154 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 选手模型 - Contestant
*
* 存储参赛选手的基本信息,包括身份认证、组别、成绩等。
* 选手通过身份证号和密码进行登录认证。
*/
module.exports = app => {
const { STRING, INTEGER, BOOLEAN, DATE } = app.Sequelize;
// 分数列统一走共享工厂(DECIMAL(6,2) + Number 兜底,见 app/lib/decimal_score.js)
const decimalScore = require('../lib/decimal_score')(app);
const Contestant = app.model.define('contestant', {
// 主键 ID,自增
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '选手唯一标识',
},
// 身份证号,唯一,长度 18,用于选手登录
id_number: {
type: STRING(18),
allowNull: false,
unique: true,
comment: '身份证号(登录账号)',
validate: {
len: [18, 18], // 严格校验 18 位身份证号
},
},
// 选手姓名
name: {
type: STRING(50),
allowNull: false,
comment: '选手姓名',
},
// 手机号码
phone: {
type: STRING(20),
allowNull: false,
comment: '手机号码',
},
// 参赛组别:zhongzhi(中职) / gaozhi(高职) / benke(本科) / jiaoshi(教师)
group: {
type: STRING(20),
allowNull: false,
comment: '参赛组别',
validate: {
isIn: [['zhongzhi', 'gaozhi', 'benke', 'jiaoshi']],
},
},
// 登录密码(存储明文,生产环境应使用 bcrypt 加密存储)
password: {
type: STRING(100),
allowNull: false,
defaultValue: '',
comment: '登录密码',
},
// 账号是否已激活(激活后才可以登录)
activated: {
type: BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '账号激活状态',
},
// 总分(管理员打分后更新,NULL 表示尚未评分)
score: decimalScore('score', '选手总分(exam_score + word_score)'),
// 卷面分(选择题+判断题自动计算)
exam_score: decimalScore('exam_score', '卷面考试成绩'),
// 综合大题 / Word 题分数(管理员手动打分,可含 2 位小数)
word_score: decimalScore('word_score', 'Word题/综合大题得分(可含 2 位小数)'),
// PPT分数(本科/教师组管理员手动打分)
ppt_score: decimalScore('ppt_score', 'PPT得分(本科/教师组,可含 2 位小数)'),
// 指导教师姓名(R176)。来源 = 报名表里 identity 为「指导教师」的行,
// 按 (group, 队伍编号) 归到同组选手身上;同组多人用「、」连接后冗余存一份。
// ⚠️ 语义上是**组级属性**,冗余到每位组员是为了让成绩公示表能直接按行取用,
// 不必每次再回查 aggregation。回填入口见 service/admin.js#backfillTeachers。
teacher_name: {
type: STRING(200),
allowNull: true,
defaultValue: null,
comment: '指导教师姓名(同组多人用、分隔)',
},
// 所属团队 ID(外键关联 teams 表)
team_id: {
type: INTEGER,
allowNull: true,
defaultValue: null,
comment: '所属团队ID',
},
// 测试数据标记:0=生产数据,1=测试数据(重置时仅重置测试数据)
test_flag: {
type: INTEGER,
allowNull: false,
defaultValue: 0,
comment: '测试数据标记:0=生产数据,1=测试数据',
},
// 记录创建时间
created_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '创建时间',
},
// 记录更新时间
updated_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '更新时间',
},
}, {
// 表名保持为 contestants(原样,不做复数变换)
tableName: 'contestants',
// 不自动生成 createdAt / updatedAt(我们手动定义 created_at / updated_at)
timestamps: false,
// 下划线命名
underscored: true,
// 索引
indexes: [
{
unique: true,
fields: ['id_number'],
},
{
fields: ['group'],
},
{
fields: ['team_id'],
},
],
});
Contestant.associate = () => {
app.model.Contestant.belongsTo(app.model.Team, {
foreignKey: 'team_id',
targetKey: 'team_uid',
as: 'team',
});
app.model.Contestant.hasMany(app.model.ExamSubmission, {
foreignKey: 'contestant_id',
as: 'examSubmissions',
});
app.model.Contestant.hasMany(app.model.WorkSubmission, {
foreignKey: 'contestant_id',
as: 'workSubmissions',
});
};
return Contestant;
};