chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。 已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
This commit is contained in:
125
database/migrations/001-create-accounts.js
Normal file
125
database/migrations/001-create-accounts.js
Normal file
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('accounts', {
|
||||
uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '用户唯一标识',
|
||||
},
|
||||
role: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: false,
|
||||
defaultValue: 'student',
|
||||
comment: '角色:student/teacher/judge/admin',
|
||||
},
|
||||
name_cn: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
comment: '中文姓名',
|
||||
},
|
||||
name_en: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: true,
|
||||
comment: '英文姓名/拼音',
|
||||
},
|
||||
gender: {
|
||||
type: Sequelize.STRING(8),
|
||||
allowNull: true,
|
||||
comment: '性别:男/女',
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: true,
|
||||
comment: '手机号码',
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: true,
|
||||
comment: '电子邮箱',
|
||||
},
|
||||
id_number: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '身份证号(唯一)',
|
||||
},
|
||||
password: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: false,
|
||||
comment: '密码(加密存储)',
|
||||
},
|
||||
salt: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
comment: '密码盐值',
|
||||
},
|
||||
position: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '职务/职称',
|
||||
},
|
||||
major: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: true,
|
||||
comment: '专业',
|
||||
},
|
||||
country: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '国家',
|
||||
},
|
||||
region: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '地区/省份',
|
||||
},
|
||||
organization_cn: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '所属单位(中文)',
|
||||
},
|
||||
organization_en: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '所属单位(英文)',
|
||||
},
|
||||
activated: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '激活状态:0未激活 1已激活',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '总分/积分',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('accounts', ['role'], { name: 'idx_role' }),
|
||||
queryInterface.addIndex('accounts', ['phone'], { name: 'idx_phone' }),
|
||||
queryInterface.addIndex('accounts', ['name_cn'], { name: 'idx_name_cn' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('accounts');
|
||||
},
|
||||
};
|
||||
105
database/migrations/001-create-contestants.js
Normal file
105
database/migrations/001-create-contestants.js
Normal file
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:创建选手表(contestants)
|
||||
* 包含选手基本信息、身份认证、组别和成绩字段
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 执行迁移:创建 contestants 表
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import('sequelize').Sequelize} Sequelize
|
||||
*/
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER, STRING, BOOLEAN, DATE } = Sequelize;
|
||||
|
||||
await queryInterface.createTable('contestants', {
|
||||
// 主键 ID,自增
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '选手唯一标识',
|
||||
},
|
||||
// 身份证号,唯一,18位
|
||||
id_number: {
|
||||
type: STRING(18),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '身份证号',
|
||||
},
|
||||
// 选手姓名
|
||||
name: {
|
||||
type: STRING(50),
|
||||
allowNull: false,
|
||||
comment: '选手姓名',
|
||||
},
|
||||
// 手机号码
|
||||
phone: {
|
||||
type: STRING(20),
|
||||
allowNull: false,
|
||||
comment: '手机号码',
|
||||
},
|
||||
// 参赛组别
|
||||
group: {
|
||||
type: STRING(20),
|
||||
allowNull: false,
|
||||
comment: '参赛组别(zhongzhi/gaozhi/benke/jiaoshi)',
|
||||
},
|
||||
// 登录密码
|
||||
password: {
|
||||
type: STRING(100),
|
||||
allowNull: false,
|
||||
defaultValue: '',
|
||||
comment: '登录密码',
|
||||
},
|
||||
// 账号激活状态
|
||||
activated: {
|
||||
type: BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: '账号激活状态',
|
||||
},
|
||||
// 总分
|
||||
score: {
|
||||
type: INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '选手总分',
|
||||
},
|
||||
// 创建时间
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
// 更新时间
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
|
||||
// 为 id_number 创建唯一索引
|
||||
await queryInterface.addIndex('contestants', ['id_number'], {
|
||||
unique: true,
|
||||
name: 'idx_contestants_id_number',
|
||||
});
|
||||
|
||||
// 为 group 创建索引
|
||||
await queryInterface.addIndex('contestants', ['group'], {
|
||||
name: 'idx_contestants_group',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 回滚迁移:删除 contestants 表
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
*/
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('contestants');
|
||||
},
|
||||
};
|
||||
61
database/migrations/002-create-admins.js
Normal file
61
database/migrations/002-create-admins.js
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:创建管理员表(admins)
|
||||
* 存储后台管理员账号信息
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 执行迁移:创建 admins 表
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
* @param {import('sequelize').Sequelize} Sequelize
|
||||
*/
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER, STRING, DATE } = Sequelize;
|
||||
|
||||
await queryInterface.createTable('admins', {
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '管理员唯一标识',
|
||||
},
|
||||
username: {
|
||||
type: STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '管理员用户名',
|
||||
},
|
||||
password: {
|
||||
type: STRING(100),
|
||||
allowNull: false,
|
||||
comment: '管理员密码',
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('admins', ['username'], {
|
||||
unique: true,
|
||||
name: 'idx_admins_username',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 回滚迁移:删除 admins 表
|
||||
* @param {import('sequelize').QueryInterface} queryInterface
|
||||
*/
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('admins');
|
||||
},
|
||||
};
|
||||
56
database/migrations/002-create-teams.js
Normal file
56
database/migrations/002-create-teams.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('teams', {
|
||||
team_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
// 注意:运行库中 teams 表真实自增主键为 id,team_uid 为非自增普通列,
|
||||
// 由应用层(importFromExcel 显式 MAX+1)赋值。故此处不声明 autoIncrement,
|
||||
// 使迁移与运行库一致,避免新建库时 team_uid 被设为自增而与运行库行为背离。
|
||||
allowNull: false,
|
||||
comment: '团队唯一标识(由应用层显式生成,非 DB 自增)',
|
||||
},
|
||||
group_name: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
comment: '组别名称',
|
||||
},
|
||||
team_id: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '团队编号(唯一)',
|
||||
},
|
||||
team_number: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: true,
|
||||
comment: '团队序号',
|
||||
},
|
||||
address: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '地址',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return queryInterface.addIndex('teams', ['group_name'], { name: 'idx_group_name' });
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('teams');
|
||||
},
|
||||
};
|
||||
42
database/migrations/003-create-competition-info.js
Normal file
42
database/migrations/003-create-competition-info.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('competition_info', {
|
||||
competition_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '赛事唯一标识',
|
||||
},
|
||||
competition_name: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: false,
|
||||
comment: '赛事名称',
|
||||
},
|
||||
competition_code: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '赛事编码(唯一)',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('competition_info');
|
||||
},
|
||||
};
|
||||
94
database/migrations/004-create-aggregation.js
Normal file
94
database/migrations/004-create-aggregation.js
Normal file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('aggregation', {
|
||||
aggregation_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '报名记录唯一标识',
|
||||
},
|
||||
competition_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '赛事ID(关联competition_info)',
|
||||
references: { model: 'competition_info', key: 'competition_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
team_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '团队ID(关联teams)',
|
||||
references: { model: 'teams', key: 'team_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
account_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '用户ID(关联accounts)',
|
||||
references: { model: 'accounts', key: 'uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
identity: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: true,
|
||||
comment: '身份标识',
|
||||
},
|
||||
is_registrant: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否为主要报名人:0否 1是',
|
||||
},
|
||||
registration_form: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: '报名表文件路径',
|
||||
},
|
||||
stamped_form: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: '盖章报名表文件路径',
|
||||
},
|
||||
audit_status: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending',
|
||||
comment: '审核状态:pending/approved/rejected',
|
||||
},
|
||||
reject_reason: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: '驳回原因',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('aggregation', ['competition_uid'], { name: 'idx_competition_uid' }),
|
||||
queryInterface.addIndex('aggregation', ['team_uid'], { name: 'idx_team_uid' }),
|
||||
queryInterface.addIndex('aggregation', ['account_uid'], { name: 'idx_account_uid' }),
|
||||
queryInterface.addIndex('aggregation', ['audit_status'], { name: 'idx_audit_status' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('aggregation');
|
||||
},
|
||||
};
|
||||
69
database/migrations/004-create-exam-questions.js
Normal file
69
database/migrations/004-create-exam-questions.js
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:创建试卷题目表(exam_questions)
|
||||
* 存储各组别的试卷题目
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER, STRING, TEXT, DATE } = Sequelize;
|
||||
|
||||
await queryInterface.createTable('exam_questions', {
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目唯一标识',
|
||||
},
|
||||
group: {
|
||||
type: STRING(20),
|
||||
allowNull: false,
|
||||
comment: '题目所属组别',
|
||||
},
|
||||
title: {
|
||||
type: STRING(200),
|
||||
allowNull: false,
|
||||
comment: '题目标题',
|
||||
},
|
||||
content: {
|
||||
type: TEXT,
|
||||
allowNull: false,
|
||||
comment: '题目内容描述',
|
||||
},
|
||||
score: {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
comment: '题目分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序序号',
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('exam_questions', ['group'], {
|
||||
name: 'idx_exam_questions_group',
|
||||
});
|
||||
await queryInterface.addIndex('exam_questions', ['group', 'sort_order'], {
|
||||
name: 'idx_exam_questions_group_sort',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('exam_questions');
|
||||
},
|
||||
};
|
||||
67
database/migrations/005-create-exam-submissions.js
Normal file
67
database/migrations/005-create-exam-submissions.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:创建答题记录表(exam_submissions)
|
||||
* 存储选手的试卷答题记录
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER, TEXT, DATE } = Sequelize;
|
||||
|
||||
await queryInterface.createTable('exam_submissions', {
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '答题记录唯一标识',
|
||||
},
|
||||
contestant_id: {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
comment: '选手 ID',
|
||||
references: {
|
||||
model: 'contestants',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
answers: {
|
||||
type: TEXT,
|
||||
allowNull: false,
|
||||
comment: '答题内容(JSON)',
|
||||
},
|
||||
submit_time: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '提交时间',
|
||||
},
|
||||
score: {
|
||||
type: INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '试卷得分',
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('exam_submissions', ['contestant_id'], {
|
||||
name: 'idx_exam_submissions_contestant_id',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('exam_submissions');
|
||||
},
|
||||
};
|
||||
68
database/migrations/005-create-news.js
Normal file
68
database/migrations/005-create-news.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('news', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '新闻ID',
|
||||
},
|
||||
title: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '新闻标题',
|
||||
},
|
||||
content: {
|
||||
type: Sequelize.TEXT('long'),
|
||||
allowNull: true,
|
||||
comment: '新闻内容(富文本)',
|
||||
},
|
||||
wx_link: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: '微信链接',
|
||||
},
|
||||
publish_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '发布时间',
|
||||
},
|
||||
pinned: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否置顶:0否 1是',
|
||||
},
|
||||
group_name: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '目标组别',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('news', ['publish_time'], { name: 'idx_publish_time' }),
|
||||
queryInterface.addIndex('news', ['pinned', 'publish_time'], { name: 'idx_pinned_publish_time' }),
|
||||
queryInterface.addIndex('news', ['group_name'], { name: 'idx_group_name' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('news');
|
||||
},
|
||||
};
|
||||
58
database/migrations/006-create-carousel.js
Normal file
58
database/migrations/006-create-carousel.js
Normal file
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('carousel', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '轮播图ID',
|
||||
},
|
||||
image_url: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: false,
|
||||
comment: '图片URL',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序序号',
|
||||
},
|
||||
link_url: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: '点击跳转链接',
|
||||
},
|
||||
enabled: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '是否启用:0禁用 1启用',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('carousel', ['sort_order'], { name: 'idx_sort_order' }),
|
||||
queryInterface.addIndex('carousel', ['enabled'], { name: 'idx_enabled' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('carousel');
|
||||
},
|
||||
};
|
||||
85
database/migrations/006-create-work-submissions.js
Normal file
85
database/migrations/006-create-work-submissions.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:创建作品提交表(work_submissions)
|
||||
* 存储选手上传的作品文件信息
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER, STRING, DATE } = Sequelize;
|
||||
|
||||
await queryInterface.createTable('work_submissions', {
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '作品提交唯一标识',
|
||||
},
|
||||
contestant_id: {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
comment: '选手 ID',
|
||||
references: {
|
||||
model: 'contestants',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
file_type: {
|
||||
type: STRING(20),
|
||||
allowNull: false,
|
||||
comment: '文件类型(plan/ppt)',
|
||||
},
|
||||
file_name: {
|
||||
type: STRING(200),
|
||||
allowNull: false,
|
||||
comment: '原始文件名',
|
||||
},
|
||||
file_path: {
|
||||
type: STRING(500),
|
||||
allowNull: false,
|
||||
comment: '文件存储路径',
|
||||
},
|
||||
file_size: {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
comment: '文件大小(字节)',
|
||||
},
|
||||
submit_time: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '提交时间',
|
||||
},
|
||||
score: {
|
||||
type: INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '作品得分',
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('work_submissions', ['contestant_id'], {
|
||||
name: 'idx_work_submissions_contestant_id',
|
||||
});
|
||||
await queryInterface.addIndex('work_submissions', ['file_type'], {
|
||||
name: 'idx_work_submissions_file_type',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('work_submissions');
|
||||
},
|
||||
};
|
||||
69
database/migrations/007-create-zhongzhi-questions.js
Normal file
69
database/migrations/007-create-zhongzhi-questions.js
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('zhongzhi_questions', {
|
||||
question_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目唯一标识',
|
||||
},
|
||||
question: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
comment: '题目内容',
|
||||
},
|
||||
answer: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: false,
|
||||
comment: '正确答案',
|
||||
},
|
||||
question_type: {
|
||||
type: Sequelize.STRING(16),
|
||||
allowNull: false,
|
||||
defaultValue: 'choice',
|
||||
comment: '题目类型:choice选择题 judge判断题',
|
||||
},
|
||||
options: {
|
||||
type: Sequelize.JSON,
|
||||
allowNull: true,
|
||||
comment: '选项(JSON格式)',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '题目分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序序号',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('zhongzhi_questions', ['question_type'], { name: 'idx_question_type' }),
|
||||
queryInterface.addIndex('zhongzhi_questions', ['sort_order'], { name: 'idx_sort_order' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('zhongzhi_questions');
|
||||
},
|
||||
};
|
||||
69
database/migrations/008-create-gaozhi-questions.js
Normal file
69
database/migrations/008-create-gaozhi-questions.js
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('gaozhi_questions', {
|
||||
question_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目唯一标识',
|
||||
},
|
||||
question: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
comment: '题目内容',
|
||||
},
|
||||
answer: {
|
||||
type: Sequelize.STRING(32),
|
||||
allowNull: false,
|
||||
comment: '正确答案',
|
||||
},
|
||||
question_type: {
|
||||
type: Sequelize.STRING(16),
|
||||
allowNull: false,
|
||||
defaultValue: 'choice',
|
||||
comment: '题目类型:choice选择题 judge判断题',
|
||||
},
|
||||
options: {
|
||||
type: Sequelize.JSON,
|
||||
allowNull: true,
|
||||
comment: '选项(JSON格式)',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '题目分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序序号',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('gaozhi_questions', ['question_type'], { name: 'idx_question_type' }),
|
||||
queryInterface.addIndex('gaozhi_questions', ['sort_order'], { name: 'idx_sort_order' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('gaozhi_questions');
|
||||
},
|
||||
};
|
||||
82
database/migrations/009-create-zhongzhi-submissions.js
Normal file
82
database/migrations/009-create-zhongzhi-submissions.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('zhongzhi_submissions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '提交记录ID',
|
||||
},
|
||||
aggregation_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '报名记录ID(关联aggregation)',
|
||||
references: { model: 'aggregation', key: 'aggregation_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
question_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '题目ID(关联zhongzhi_questions)',
|
||||
references: { model: 'zhongzhi_questions', key: 'question_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
answer_data: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '提交的答案',
|
||||
},
|
||||
is_correct: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: true,
|
||||
comment: '是否正确:0错误 1正确',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '得分',
|
||||
},
|
||||
word_file_path: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: 'Word文件路径',
|
||||
},
|
||||
word_file_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: 'Word文件原始名称',
|
||||
},
|
||||
word_file_md5: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: 'Word文件MD5哈希值',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('zhongzhi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }),
|
||||
queryInterface.addIndex('zhongzhi_submissions', ['question_uid'], { name: 'idx_question_uid' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('zhongzhi_submissions');
|
||||
},
|
||||
};
|
||||
82
database/migrations/010-create-gaozhi-submissions.js
Normal file
82
database/migrations/010-create-gaozhi-submissions.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('gaozhi_submissions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '提交记录ID',
|
||||
},
|
||||
aggregation_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '报名记录ID(关联aggregation)',
|
||||
references: { model: 'aggregation', key: 'aggregation_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
question_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '题目ID(关联gaozhi_questions)',
|
||||
references: { model: 'gaozhi_questions', key: 'question_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
answer_data: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '提交的答案',
|
||||
},
|
||||
is_correct: {
|
||||
type: Sequelize.TINYINT,
|
||||
allowNull: true,
|
||||
comment: '是否正确:0错误 1正确',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '得分',
|
||||
},
|
||||
word_file_path: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: 'Word文件路径',
|
||||
},
|
||||
word_file_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
comment: 'Word文件原始名称',
|
||||
},
|
||||
word_file_md5: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: 'Word文件MD5哈希值',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('gaozhi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' }),
|
||||
queryInterface.addIndex('gaozhi_submissions', ['question_uid'], { name: 'idx_question_uid' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('gaozhi_submissions');
|
||||
},
|
||||
};
|
||||
77
database/migrations/011-create-benke-submissions.js
Normal file
77
database/migrations/011-create-benke-submissions.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('benke_submissions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '提交记录ID',
|
||||
},
|
||||
aggregation_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '报名记录ID(关联aggregation)',
|
||||
references: { model: 'aggregation', key: 'aggregation_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
work_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '作品名称',
|
||||
},
|
||||
file_path: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: false,
|
||||
comment: '文件存储路径',
|
||||
},
|
||||
file_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '文件原始名称',
|
||||
},
|
||||
file_size: {
|
||||
type: Sequelize.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '文件大小(字节)',
|
||||
},
|
||||
file_md5: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '文件MD5哈希值',
|
||||
},
|
||||
upload_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '上传时间',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '评分',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return queryInterface.addIndex('benke_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' });
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('benke_submissions');
|
||||
},
|
||||
};
|
||||
77
database/migrations/012-create-jiaoshi-submissions.js
Normal file
77
database/migrations/012-create-jiaoshi-submissions.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('jiaoshi_submissions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '提交记录ID',
|
||||
},
|
||||
aggregation_uid: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '报名记录ID(关联aggregation)',
|
||||
references: { model: 'aggregation', key: 'aggregation_uid' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
work_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '作品名称',
|
||||
},
|
||||
file_path: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: false,
|
||||
comment: '文件存储路径',
|
||||
},
|
||||
file_name: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
comment: '文件原始名称',
|
||||
},
|
||||
file_size: {
|
||||
type: Sequelize.INTEGER.UNSIGNED,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '文件大小(字节)',
|
||||
},
|
||||
file_md5: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: true,
|
||||
comment: '文件MD5哈希值',
|
||||
},
|
||||
upload_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '上传时间',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '评分',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return queryInterface.addIndex('jiaoshi_submissions', ['aggregation_uid'], { name: 'idx_aggregation_uid' });
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('jiaoshi_submissions');
|
||||
},
|
||||
};
|
||||
57
database/migrations/013-create-competition-times.js
Normal file
57
database/migrations/013-create-competition-times.js
Normal file
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('competition_times', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '配置ID',
|
||||
},
|
||||
group_name: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '组别名称(唯一)',
|
||||
},
|
||||
start_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '比赛开始时间',
|
||||
},
|
||||
end_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '比赛结束时间',
|
||||
},
|
||||
deadline: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
comment: '报名/提交截止时间',
|
||||
},
|
||||
word_template: {
|
||||
type: Sequelize.STRING(512),
|
||||
allowNull: true,
|
||||
comment: 'Word模板文件路径',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('competition_times');
|
||||
},
|
||||
};
|
||||
42
database/migrations/014-create-system-configs.js
Normal file
42
database/migrations/014-create-system-configs.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('system_configs', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '配置ID',
|
||||
},
|
||||
config_key: {
|
||||
type: Sequelize.STRING(128),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '配置键名(唯一)',
|
||||
},
|
||||
config_value: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '配置值',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('system_configs');
|
||||
},
|
||||
};
|
||||
68
database/migrations/015-create-choice-questions.js
Normal file
68
database/migrations/015-create-choice-questions.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('choice_questions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目ID',
|
||||
},
|
||||
group: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '组别:zhongzhi/gaozhi',
|
||||
},
|
||||
question: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
comment: '题目内容',
|
||||
},
|
||||
options: {
|
||||
type: Sequelize.JSON,
|
||||
allowNull: false,
|
||||
comment: '选项列表 [{ key: "A", text: "..." }, ...]',
|
||||
},
|
||||
answer: {
|
||||
type: Sequelize.STRING(10),
|
||||
allowNull: false,
|
||||
comment: '正确答案(如 A / AB / ABCD)',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 5,
|
||||
comment: '分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('choice_questions', ['group'], { name: 'idx_choice_group' }),
|
||||
queryInterface.addIndex('choice_questions', ['sort_order'], { name: 'idx_choice_sort' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('choice_questions');
|
||||
},
|
||||
};
|
||||
63
database/migrations/016-create-judge-questions.js
Normal file
63
database/migrations/016-create-judge-questions.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('judge_questions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目ID',
|
||||
},
|
||||
group: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '组别:zhongzhi/gaozhi',
|
||||
},
|
||||
question: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
comment: '题目内容',
|
||||
},
|
||||
answer: {
|
||||
type: Sequelize.STRING(10),
|
||||
allowNull: false,
|
||||
comment: '正确答案:true 或 false',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 5,
|
||||
comment: '分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('judge_questions', ['group'], { name: 'idx_judge_group' }),
|
||||
queryInterface.addIndex('judge_questions', ['sort_order'], { name: 'idx_judge_sort' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('judge_questions');
|
||||
},
|
||||
};
|
||||
68
database/migrations/017-create-word-questions.js
Normal file
68
database/migrations/017-create-word-questions.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('word_questions', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '题目ID',
|
||||
},
|
||||
group: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '组别:zhongzhi/gaozhi',
|
||||
},
|
||||
title: {
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: false,
|
||||
comment: '题目标题',
|
||||
},
|
||||
description: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '题目描述/操作要求',
|
||||
},
|
||||
score: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 30,
|
||||
comment: '分值',
|
||||
},
|
||||
sort_order: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序',
|
||||
},
|
||||
template_path: {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: true,
|
||||
comment: 'Word 模板文件路径',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间',
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '更新时间',
|
||||
},
|
||||
}).then(() => {
|
||||
return Promise.all([
|
||||
queryInterface.addIndex('word_questions', ['group'], { name: 'idx_word_group' }),
|
||||
queryInterface.addIndex('word_questions', ['sort_order'], { name: 'idx_word_sort' }),
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.dropTable('word_questions');
|
||||
},
|
||||
};
|
||||
23
database/migrations/018-add-test-flag-to-contestants.js
Normal file
23
database/migrations/018-add-test-flag-to-contestants.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移文件:为 contestants 表添加 test_flag 字段
|
||||
* 用于区分测试数据和生产数据,防止重置操作误删生产数据
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const { INTEGER } = Sequelize;
|
||||
|
||||
await queryInterface.addColumn('contestants', 'test_flag', {
|
||||
type: INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '测试数据标记:0=生产数据,1=测试数据',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeColumn('contestants', 'test_flag');
|
||||
},
|
||||
};
|
||||
17
database/migrations/019-add-title-to-carousel.js
Normal file
17
database/migrations/019-add-title-to-carousel.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('carousel', 'title', {
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '轮播图标题',
|
||||
after: 'image_url',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('carousel', 'title');
|
||||
},
|
||||
};
|
||||
40
database/migrations/020-add-question-type-to-submissions.js
Normal file
40
database/migrations/020-add-question-type-to-submissions.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移 020:为 zhongzhi_submissions / gaozhi_submissions 增加 question_type 列
|
||||
*
|
||||
* 背景(Bug #11 / B11):选择题(choice_questions)与判断题(judge_questions)的 id
|
||||
* 在不同表中数值重叠(如 choice#5 与 judge#5 均为 5),原 saveAnswers 仅按
|
||||
* (aggregation_uid, question_uid) 查找并 upsert,导致同 id 的选择题与判断题被
|
||||
* 折叠成一条记录,考生作答数少于抽题数、部分答案静默丢失。
|
||||
*
|
||||
* 修复:新增 question_type 列(choice/judge),与 question_uid 联合唯一标识一道题;
|
||||
* service.saveAnswers 的查找/写入键改为 (aggregation_uid, question_uid, question_type);
|
||||
* autoScore 判分不受影响(其本就按 question_type 选模型 + 用原始 id 作主键)。
|
||||
*/
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const addCol = async (table) => {
|
||||
await queryInterface.addColumn(table, 'question_type', {
|
||||
type: Sequelize.STRING(16),
|
||||
allowNull: false,
|
||||
defaultValue: '',
|
||||
comment: '题型:choice/judge,与 question_uid 联合区分题目',
|
||||
});
|
||||
await queryInterface.addIndex(
|
||||
table,
|
||||
['aggregation_uid', 'question_uid', 'question_type'],
|
||||
{ name: `idx_${table}_agg_qtype` }
|
||||
);
|
||||
};
|
||||
await addCol('zhongzhi_submissions');
|
||||
await addCol('gaozhi_submissions');
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('zhongzhi_submissions', 'idx_zhongzhi_submissions_agg_qtype');
|
||||
await queryInterface.removeColumn('zhongzhi_submissions', 'question_type');
|
||||
await queryInterface.removeIndex('gaozhi_submissions', 'idx_gaozhi_submissions_agg_qtype');
|
||||
await queryInterface.removeColumn('gaozhi_submissions', 'question_type');
|
||||
},
|
||||
};
|
||||
99
database/migrations/021-unique-submission-indexes.js
Normal file
99
database/migrations/021-unique-submission-indexes.js
Normal file
@@ -0,0 +1,99 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 迁移 021:为答题提交表增加唯一约束,杜绝并发作答重复落库(Bug #12 / B12)
|
||||
*
|
||||
* 背景(B12):原 saveAnswers 使用「先 findOne 再 create」的非原子写法,且
|
||||
* (aggregation_uid, question_uid, question_type) 仅为普通索引。同一用户并发提交
|
||||
* 时,多个请求都 find 不到记录,于是各自 create,导致同一题被插入多行
|
||||
* (实测:3 路并发产生 57 行 / 20 题,exam_submissions 出现 3 条同选手记录)。
|
||||
* 重复行会污染 answeredCount、判分明细与成绩恢复,属数据一致性缺陷。
|
||||
*
|
||||
* 修复:
|
||||
* 1) zhongzhi_submissions / gaozhi_submissions 的复合索引升级为唯一索引
|
||||
* uk_{table}_submission (aggregation_uid, question_uid, question_type);
|
||||
* service.saveAnswers 改用 upsert(conflictFields=[...]),由 MySQL 的
|
||||
* INSERT ... ON DUPLICATE KEY UPDATE 在并发下原子折叠为单行更新。
|
||||
* 2) exam_submissions 的 contestant_id 索引升级为唯一索引
|
||||
* uk_exam_submission_contestant,配合 service 中并发安全的
|
||||
* _ensureExamSubmissionSafe,保证每选手仅一条提交记录。
|
||||
*
|
||||
* 注意:up/down 均先去重(保留最小 id)再加减索引,确保幂等且不会因历史脏数据失败。
|
||||
*/
|
||||
module.exports = {
|
||||
up: async (queryInterface) => {
|
||||
// ---- 去重:同 (aggregation_uid, question_uid, question_type) 仅留最小 id ----
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE t FROM zhongzhi_submissions t
|
||||
WHERE t.id NOT IN (
|
||||
SELECT min_id FROM (
|
||||
SELECT MIN(id) AS min_id FROM zhongzhi_submissions
|
||||
GROUP BY aggregation_uid, question_uid, question_type
|
||||
) AS keep
|
||||
)
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE t FROM gaozhi_submissions t
|
||||
WHERE t.id NOT IN (
|
||||
SELECT min_id FROM (
|
||||
SELECT MIN(id) AS min_id FROM gaozhi_submissions
|
||||
GROUP BY aggregation_uid, question_uid, question_type
|
||||
) AS keep
|
||||
)
|
||||
`);
|
||||
// exam_submissions:每 contestant_id 仅留最小 id
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE t FROM exam_submissions t
|
||||
WHERE t.id NOT IN (
|
||||
SELECT min_id FROM (
|
||||
SELECT MIN(id) AS min_id FROM exam_submissions
|
||||
GROUP BY contestant_id
|
||||
) AS keep
|
||||
)
|
||||
`);
|
||||
|
||||
// ---- zhongzhi ----
|
||||
await queryInterface.removeIndex('zhongzhi_submissions', 'idx_zhongzhi_submissions_agg_qtype').catch(() => {});
|
||||
await queryInterface.addIndex('zhongzhi_submissions', {
|
||||
name: 'uk_zhongzhi_submission',
|
||||
unique: true,
|
||||
fields: ['aggregation_uid', 'question_uid', 'question_type'],
|
||||
}).catch(() => {});
|
||||
|
||||
// ---- gaozhi ----
|
||||
await queryInterface.removeIndex('gaozhi_submissions', 'idx_gaozhi_submissions_agg_qtype').catch(() => {});
|
||||
await queryInterface.addIndex('gaozhi_submissions', {
|
||||
name: 'uk_gaozhi_submission',
|
||||
unique: true,
|
||||
fields: ['aggregation_uid', 'question_uid', 'question_type'],
|
||||
}).catch(() => {});
|
||||
|
||||
// ---- exam_submissions ----
|
||||
await queryInterface.removeIndex('exam_submissions', 'idx_exam_submissions_contestant_id').catch(() => {});
|
||||
await queryInterface.addIndex('exam_submissions', {
|
||||
name: 'uk_exam_submission_contestant',
|
||||
unique: true,
|
||||
fields: ['contestant_id'],
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeIndex('zhongzhi_submissions', 'uk_zhongzhi_submission').catch(() => {});
|
||||
await queryInterface.addIndex('zhongzhi_submissions', {
|
||||
name: 'idx_zhongzhi_submissions_agg_qtype',
|
||||
fields: ['aggregation_uid', 'question_uid', 'question_type'],
|
||||
});
|
||||
|
||||
await queryInterface.removeIndex('gaozhi_submissions', 'uk_gaozhi_submission').catch(() => {});
|
||||
await queryInterface.addIndex('gaozhi_submissions', {
|
||||
name: 'idx_gaozhi_submissions_agg_qtype',
|
||||
fields: ['aggregation_uid', 'question_uid', 'question_type'],
|
||||
});
|
||||
|
||||
await queryInterface.removeIndex('exam_submissions', 'uk_exam_submission_contestant').catch(() => {});
|
||||
await queryInterface.addIndex('exam_submissions', {
|
||||
name: 'idx_exam_submissions_contestant_id',
|
||||
fields: ['contestant_id'],
|
||||
});
|
||||
},
|
||||
};
|
||||
56
database/migrations/022-add-content-button-to-carousel.js
Normal file
56
database/migrations/022-add-content-button-to-carousel.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 为 carousel 表新增富文本正文与可配置按钮字段,并放宽 image_url 允许为空
|
||||
* (支持"渐变底图回退"的固定页迁移)。
|
||||
*
|
||||
* 新增:
|
||||
* - content TEXT 富文本正文(HTML,由 wangEditor 生成,前端 v-html 渲染)
|
||||
* - button_text VARCHAR(100) 按钮文字(为空则不显示按钮)
|
||||
* - button_link VARCHAR(500) 按钮链接(内部路由如 /activate,或外部 http(s):// 链接)
|
||||
* 变更:
|
||||
* - image_url 由 NOT NULL 放宽为 允许 NULL(无底图时前端回退 CSS 渐变)
|
||||
*/
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('carousel', 'content', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '富文本正文(HTML)',
|
||||
after: 'title',
|
||||
});
|
||||
await queryInterface.addColumn('carousel', 'button_text', {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '按钮文字(为空则不显示按钮)',
|
||||
after: 'content',
|
||||
});
|
||||
await queryInterface.addColumn('carousel', 'button_link', {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '按钮链接(内部路由/外部URL)',
|
||||
after: 'button_text',
|
||||
});
|
||||
// 放宽 image_url 允许为空,支持渐变底图回退
|
||||
await queryInterface.changeColumn('carousel', 'image_url', {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: '图片 URL 地址(可为空时前端回退渐变)',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('carousel', 'content');
|
||||
await queryInterface.removeColumn('carousel', 'button_text');
|
||||
await queryInterface.removeColumn('carousel', 'button_link');
|
||||
await queryInterface.changeColumn('carousel', 'image_url', {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: false,
|
||||
comment: '图片 URL 地址',
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user