Files
competition-website/app/model/account.js
qubiaobiao 23604f2718 feat(exam/auth): 登录闸门 + 交卷稳定性加固(R4-R8)
R4 登录/交卷背压闸门
- app/middleware/concurrency_guard.js: 新增。超限即 429 + Retry-After(立即拒绝式,
  不排队;实测 429 P50=34ms)。RETRY_AFTER 收敛为单一有依据常量 = '2'
  (≈1.8 个满载窗口;实测 bcryptjs 1129ms / 原生 265ms 每窗口)。
  配额:LOGIN_MAX=16 / SUBMIT_MAX=24 / HEAVY_REQ_MAX=20,各 *REQ_MAX 覆盖。
- config/config.default.js: 闸门配额与连接池容量配置;DB dialect 锁 mysql。

R7 顺序红线 + R8 Retry-After 同源 + 原生 bcrypt
- 3 处硬编码 '2' 收敛为单一常量;补"勿因换实现而删闸门"红线注释。
- bcryptjs -> bcrypt@6(N-API,Windows/Linux 均免编译;drop-in,存量哈希 $2b$10$ 兼容)。
- app/model/account.js / app/service/auth.js: 同步哈希实现注释(D4 全族 5 处陈旧
  SHA256 注释修正)。

答题链路与综合大题
- app/controller/exam.js: 中职/高职综合大题接口(GET big-question / POST big-answer,
  逐问 2000 字上限校验);uploadWord 对中职/高职封堵(400)。
- app/service/exam.js: 抽题快照 question_snapshot 落库、答案归集键修正。
- app/controller/auth.js / app/service/auth.js: toClientUser() 唯一出口,
  未发布态按组剔除 score,exam_score/word_score/ppt_score 任何情况下不下发。
- app/router.js: 新增大题与管理端路由。
- app/extend/helper.js: 下载文件名 RFC5987 编码(禁双重编码)。
- app.js: 池容量断言按 env 区分单/多 worker(防 dev 假日志)。

验证:ci-gates.sh 9/9 全绿;verify_login_guard.py 28/28(含 D 组变异 9/9);
真实登录/抽题/交卷端到端 code=0;前端构建 BUILD_RC=0。
2026-09-22 09:16:55 +08:00

168 lines
3.5 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';
/**
* 账号模型 - Account
*
* 存储参赛者账号的全部信息,包括个人资料、联系方式和密码等。
* 表名:accounts
*/
module.exports = app => {
const { STRING, INTEGER, BOOLEAN, DATE } = app.Sequelize;
const Account = app.model.define('account', {
// 主键,自增
uid: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '账号唯一标识',
},
// 角色:contestant / admin
role: {
type: STRING(20),
allowNull: false,
defaultValue: 'contestant',
comment: '账号角色',
},
// 中文姓名
name_cn: {
type: STRING(50),
allowNull: false,
comment: '中文姓名',
},
// 英文姓名
name_en: {
type: STRING(100),
allowNull: true,
defaultValue: null,
comment: '英文姓名',
},
// 性别:male / female
gender: {
type: STRING(10),
allowNull: true,
defaultValue: null,
comment: '性别',
},
// 手机号码
phone: {
type: STRING(20),
allowNull: true,
defaultValue: null,
comment: '手机号码',
},
// 电子邮箱
email: {
type: STRING(100),
allowNull: true,
defaultValue: null,
comment: '电子邮箱',
},
// 身份证号,唯一
id_number: {
type: STRING(18),
allowNull: false,
unique: true,
comment: '身份证号',
},
// 登录密码(bcrypt 加盐哈希存储;兼容存量 SHA256 旧哈希,登录成功后透明升级)
password: {
type: STRING(100),
allowNull: false,
comment: '登录密码',
},
// 密码盐值
salt: {
type: STRING(32),
allowNull: false,
comment: '密码盐值',
},
// 职位
position: {
type: STRING(100),
allowNull: true,
defaultValue: null,
comment: '职位',
},
// 专业
major: {
type: STRING(100),
allowNull: true,
defaultValue: null,
comment: '专业',
},
// 国家
country: {
type: STRING(50),
allowNull: true,
defaultValue: null,
comment: '国家',
},
// 地区
region: {
type: STRING(50),
allowNull: true,
defaultValue: null,
comment: '地区',
},
// 机构/单位(中文)
organization_cn: {
type: STRING(200),
allowNull: true,
defaultValue: null,
comment: '机构/单位(中文)',
},
// 机构/单位(英文)
organization_en: {
type: STRING(200),
allowNull: true,
defaultValue: null,
comment: '机构/单位(英文)',
},
// 账号是否已激活
activated: {
type: BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '账号激活状态',
},
// 总分
score: {
type: INTEGER,
allowNull: true,
defaultValue: null,
comment: '账号总分',
},
// 记录创建时间
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: '更新时间',
},
}, {
tableName: 'accounts',
timestamps: false,
underscored: true,
indexes: [
{
unique: true,
fields: ['id_number'],
},
{
fields: ['role'],
},
],
});
return Account;
};