diff --git a/backend/data/menus.json b/backend/data/menus.json index 7271139..860f3db 100644 --- a/backend/data/menus.json +++ b/backend/data/menus.json @@ -6,7 +6,7 @@ "component": "workbench/Index", "parentId": null, "sort": 1, - "permission": null + "permission": "workbench:read" }, { "name": "学校管理", @@ -14,8 +14,8 @@ "icon": "BankOutlined", "component": null, "parentId": null, - "sort": 5, - "permission": null, + "sort": 2, + "permission": "school:read", "children": [ { "name": "学校信息", @@ -67,70 +67,113 @@ } ] }, + { + "name": "赛事活动", + "path": "/activities", + "icon": "FlagOutlined", + "component": null, + "parentId": null, + "sort": 3, + "permission": "activity:read", + "children": [ + { + "name": "活动列表", + "path": "/activities", + "icon": "UnorderedListOutlined", + "component": "contests/Activities", + "sort": 1, + "permission": "activity:read" + }, + { + "name": "我的报名", + "path": "/activities/registrations", + "icon": "UserAddOutlined", + "component": "contests/registrations/Index", + "sort": 2, + "permission": "registration:read" + }, + { + "name": "我的作品", + "path": "/activities/works", + "icon": "FileTextOutlined", + "component": "contests/works/Index", + "sort": 3, + "permission": "work:read" + } + ] + }, { "name": "赛事管理", "path": "/contests", "icon": "TrophyOutlined", "component": null, "parentId": null, - "sort": 6, - "permission": null, + "sort": 4, + "permission": "contest:create", "children": [ - { - "name": "赛事活动", - "path": "/contests/activities", - "icon": "AppstoreOutlined", - "component": "contests/Activities", - "sort": 0, - "permission": "contest:read" - }, { "name": "赛事列表", "path": "/contests", "icon": "UnorderedListOutlined", "component": "contests/Index", "sort": 1, - "permission": "contest:read" + "permission": "contest:create" + }, + { + "name": "评委管理", + "path": "/contests/judges", + "icon": "SolutionOutlined", + "component": "contests/judges/Index", + "sort": 2, + "permission": "judge:read" }, { "name": "报名管理", "path": "/contests/registrations", "icon": "UserAddOutlined", "component": "contests/registrations/Index", - "sort": 2, - "permission": "contest:registration:read" + "sort": 3, + "permission": "registration:approve" }, { "name": "作品管理", "path": "/contests/works", "icon": "FileTextOutlined", "component": "contests/works/Index", - "sort": 3, - "permission": "contest:work:read" + "sort": 4, + "permission": "work:read" + }, + { + "name": "评审任务", + "path": "/contests/review-tasks", + "icon": "AuditOutlined", + "component": "contests/reviews/Tasks", + "sort": 5, + "permission": "review:read" }, { "name": "评审规则", "path": "/contests/reviews", "icon": "CheckCircleOutlined", "component": "contests/reviews/Index", - "sort": 4, - "permission": "contest:review:read" + "sort": 6, + "permission": "review-rule:read" }, { "name": "赛果发布", "path": "/contests/results", "icon": "TrophyOutlined", "component": "contests/results/Index", - "sort": 5, - "permission": "contest:result:read" + "sort": 7, + "permission": "contest:create" }, { "name": "通知管理", "path": "/contests/notices", "icon": "BellOutlined", "component": "contests/notices/Index", - "sort": 7, - "permission": "contest:notice:read" + "sort": 8, + "permission": "notice:create" } ] }, @@ -140,11 +183,11 @@ "icon": "FormOutlined", "component": null, "parentId": null, - "sort": 7, - "permission": null, + "sort": 5, + "permission": "homework:read", "children": [ { - "name": "作业管理", + "name": "作业列表", "path": "/homework", "icon": "FileTextOutlined", "component": "homework/Index", @@ -157,7 +200,7 @@ "icon": "EditOutlined", "component": "homework/Submissions", "sort": 2, - "permission": "homework:read" + "permission": "homework-submission:read" }, { "name": "评审规则", @@ -165,7 +208,7 @@ "icon": "CheckCircleOutlined", "component": "homework/ReviewRules", "sort": 3, - "permission": "homework:read" + "permission": "homework-review-rule:read" } ] }, @@ -176,7 +219,7 @@ "component": null, "parentId": null, "sort": 10, - "permission": null, + "permission": "user:read", "children": [ { "name": "用户管理", diff --git a/backend/data/permissions.json b/backend/data/permissions.json index 8cc961e..e04ae9a 100644 --- a/backend/data/permissions.json +++ b/backend/data/permissions.json @@ -1,4 +1,11 @@ [ + { + "code": "workbench:read", + "resource": "workbench", + "action": "read", + "name": "查看工作台", + "description": "允许查看工作台" + }, { "code": "user:create", "resource": "user", @@ -125,6 +132,517 @@ "name": "删除菜单", "description": "允许删除菜单" }, + { + "code": "tenant:create", + "resource": "tenant", + "action": "create", + "name": "创建租户", + "description": "允许创建租户" + }, + { + "code": "tenant:read", + "resource": "tenant", + "action": "read", + "name": "查看租户", + "description": "允许查看租户列表" + }, + { + "code": "tenant:update", + "resource": "tenant", + "action": "update", + "name": "更新租户", + "description": "允许更新租户信息" + }, + { + "code": "tenant:delete", + "resource": "tenant", + "action": "delete", + "name": "删除租户", + "description": "允许删除租户" + }, + { + "code": "school:create", + "resource": "school", + "action": "create", + "name": "创建学校", + "description": "允许创建学校信息" + }, + { + "code": "school:read", + "resource": "school", + "action": "read", + "name": "查看学校", + "description": "允许查看学校信息" + }, + { + "code": "school:update", + "resource": "school", + "action": "update", + "name": "更新学校", + "description": "允许更新学校信息" + }, + { + "code": "school:delete", + "resource": "school", + "action": "delete", + "name": "删除学校", + "description": "允许删除学校信息" + }, + { + "code": "department:create", + "resource": "department", + "action": "create", + "name": "创建部门", + "description": "允许创建部门" + }, + { + "code": "department:read", + "resource": "department", + "action": "read", + "name": "查看部门", + "description": "允许查看部门列表和详情" + }, + { + "code": "department:update", + "resource": "department", + "action": "update", + "name": "更新部门", + "description": "允许更新部门信息" + }, + { + "code": "department:delete", + "resource": "department", + "action": "delete", + "name": "删除部门", + "description": "允许删除部门" + }, + { + "code": "grade:create", + "resource": "grade", + "action": "create", + "name": "创建年级", + "description": "允许创建年级" + }, + { + "code": "grade:read", + "resource": "grade", + "action": "read", + "name": "查看年级", + "description": "允许查看年级列表和详情" + }, + { + "code": "grade:update", + "resource": "grade", + "action": "update", + "name": "更新年级", + "description": "允许更新年级信息" + }, + { + "code": "grade:delete", + "resource": "grade", + "action": "delete", + "name": "删除年级", + "description": "允许删除年级" + }, + { + "code": "class:create", + "resource": "class", + "action": "create", + "name": "创建班级", + "description": "允许创建班级" + }, + { + "code": "class:read", + "resource": "class", + "action": "read", + "name": "查看班级", + "description": "允许查看班级列表和详情" + }, + { + "code": "class:update", + "resource": "class", + "action": "update", + "name": "更新班级", + "description": "允许更新班级信息" + }, + { + "code": "class:delete", + "resource": "class", + "action": "delete", + "name": "删除班级", + "description": "允许删除班级" + }, + { + "code": "teacher:create", + "resource": "teacher", + "action": "create", + "name": "创建教师", + "description": "允许创建教师" + }, + { + "code": "teacher:read", + "resource": "teacher", + "action": "read", + "name": "查看教师", + "description": "允许查看教师列表和详情" + }, + { + "code": "teacher:update", + "resource": "teacher", + "action": "update", + "name": "更新教师", + "description": "允许更新教师信息" + }, + { + "code": "teacher:delete", + "resource": "teacher", + "action": "delete", + "name": "删除教师", + "description": "允许删除教师" + }, + { + "code": "student:create", + "resource": "student", + "action": "create", + "name": "创建学生", + "description": "允许创建学生" + }, + { + "code": "student:read", + "resource": "student", + "action": "read", + "name": "查看学生", + "description": "允许查看学生列表和详情" + }, + { + "code": "student:update", + "resource": "student", + "action": "update", + "name": "更新学生", + "description": "允许更新学生信息" + }, + { + "code": "student:delete", + "resource": "student", + "action": "delete", + "name": "删除学生", + "description": "允许删除学生" + }, + { + "code": "contest:create", + "resource": "contest", + "action": "create", + "name": "创建赛事", + "description": "允许创建赛事" + }, + { + "code": "contest:read", + "resource": "contest", + "action": "read", + "name": "查看赛事", + "description": "允许查看赛事列表和详情" + }, + { + "code": "contest:update", + "resource": "contest", + "action": "update", + "name": "更新赛事", + "description": "允许更新赛事信息" + }, + { + "code": "contest:delete", + "resource": "contest", + "action": "delete", + "name": "删除赛事", + "description": "允许删除赛事" + }, + { + "code": "contest:publish", + "resource": "contest", + "action": "publish", + "name": "发布赛事", + "description": "允许发布/取消发布赛事" + }, + { + "code": "contest:finish", + "resource": "contest", + "action": "finish", + "name": "结束赛事", + "description": "允许结束赛事" + }, + { + "code": "review-rule:create", + "resource": "review-rule", + "action": "create", + "name": "创建评审规则", + "description": "允许创建评审规则" + }, + { + "code": "review-rule:read", + "resource": "review-rule", + "action": "read", + "name": "查看评审规则", + "description": "允许查看评审规则" + }, + { + "code": "review-rule:update", + "resource": "review-rule", + "action": "update", + "name": "更新评审规则", + "description": "允许更新评审规则" + }, + { + "code": "review-rule:delete", + "resource": "review-rule", + "action": "delete", + "name": "删除评审规则", + "description": "允许删除评审规则" + }, + { + "code": "judge:create", + "resource": "judge", + "action": "create", + "name": "添加评委", + "description": "允许添加评委" + }, + { + "code": "judge:read", + "resource": "judge", + "action": "read", + "name": "查看评委", + "description": "允许查看评委列表" + }, + { + "code": "judge:update", + "resource": "judge", + "action": "update", + "name": "更新评委", + "description": "允许更新评委信息" + }, + { + "code": "judge:delete", + "resource": "judge", + "action": "delete", + "name": "删除评委", + "description": "允许删除评委" + }, + { + "code": "judge:assign", + "resource": "judge", + "action": "assign", + "name": "分配评委", + "description": "允许为赛事分配评委" + }, + { + "code": "registration:create", + "resource": "registration", + "action": "create", + "name": "创建报名", + "description": "允许报名赛事" + }, + { + "code": "registration:read", + "resource": "registration", + "action": "read", + "name": "查看报名", + "description": "允许查看报名记录" + }, + { + "code": "registration:update", + "resource": "registration", + "action": "update", + "name": "更新报名", + "description": "允许更新报名信息" + }, + { + "code": "registration:delete", + "resource": "registration", + "action": "delete", + "name": "取消报名", + "description": "允许取消报名" + }, + { + "code": "registration:approve", + "resource": "registration", + "action": "approve", + "name": "审核报名", + "description": "允许审核报名" + }, + { + "code": "work:create", + "resource": "work", + "action": "create", + "name": "上传作品", + "description": "允许上传参赛作品" + }, + { + "code": "work:read", + "resource": "work", + "action": "read", + "name": "查看作品", + "description": "允许查看参赛作品" + }, + { + "code": "work:update", + "resource": "work", + "action": "update", + "name": "更新作品", + "description": "允许更新作品信息" + }, + { + "code": "work:delete", + "resource": "work", + "action": "delete", + "name": "删除作品", + "description": "允许删除作品" + }, + { + "code": "work:submit", + "resource": "work", + "action": "submit", + "name": "提交作品", + "description": "允许提交作品" + }, + { + "code": "review:read", + "resource": "review", + "action": "read", + "name": "查看评审任务", + "description": "允许查看待评审作品" + }, + { + "code": "review:score", + "resource": "review", + "action": "score", + "name": "评审打分", + "description": "允许对作品打分" + }, + { + "code": "notice:create", + "resource": "notice", + "action": "create", + "name": "创建公告", + "description": "允许创建赛事公告" + }, + { + "code": "notice:read", + "resource": "notice", + "action": "read", + "name": "查看公告", + "description": "允许查看赛事公告" + }, + { + "code": "notice:update", + "resource": "notice", + "action": "update", + "name": "更新公告", + "description": "允许更新公告信息" + }, + { + "code": "notice:delete", + "resource": "notice", + "action": "delete", + "name": "删除公告", + "description": "允许删除公告" + }, + { + "code": "homework:create", + "resource": "homework", + "action": "create", + "name": "创建作业", + "description": "允许创建作业" + }, + { + "code": "homework:read", + "resource": "homework", + "action": "read", + "name": "查看作业", + "description": "允许查看作业列表" + }, + { + "code": "homework:update", + "resource": "homework", + "action": "update", + "name": "更新作业", + "description": "允许更新作业信息" + }, + { + "code": "homework:delete", + "resource": "homework", + "action": "delete", + "name": "删除作业", + "description": "允许删除作业" + }, + { + "code": "homework:publish", + "resource": "homework", + "action": "publish", + "name": "发布作业", + "description": "允许发布作业" + }, + { + "code": "homework-submission:create", + "resource": "homework-submission", + "action": "create", + "name": "提交作业", + "description": "允许提交作业" + }, + { + "code": "homework-submission:read", + "resource": "homework-submission", + "action": "read", + "name": "查看作业提交", + "description": "允许查看作业提交记录" + }, + { + "code": "homework-submission:update", + "resource": "homework-submission", + "action": "update", + "name": "更新作业提交", + "description": "允许更新提交的作业" + }, + { + "code": "homework-review-rule:create", + "resource": "homework-review-rule", + "action": "create", + "name": "创建作业评审规则", + "description": "允许创建作业评审规则" + }, + { + "code": "homework-review-rule:read", + "resource": "homework-review-rule", + "action": "read", + "name": "查看作业评审规则", + "description": "允许查看作业评审规则" + }, + { + "code": "homework-review-rule:update", + "resource": "homework-review-rule", + "action": "update", + "name": "更新作业评审规则", + "description": "允许更新作业评审规则" + }, + { + "code": "homework-review-rule:delete", + "resource": "homework-review-rule", + "action": "delete", + "name": "删除作业评审规则", + "description": "允许删除作业评审规则" + }, + { + "code": "homework-score:create", + "resource": "homework-score", + "action": "create", + "name": "作业评分", + "description": "允许对作业评分" + }, + { + "code": "homework-score:read", + "resource": "homework-score", + "action": "read", + "name": "查看作业评分", + "description": "允许查看作业评分" + }, { "code": "dict:create", "resource": "dict", @@ -196,423 +714,17 @@ "description": "允许删除系统日志" }, { - "code": "school:create", - "resource": "school", - "action": "create", - "name": "创建学校", - "description": "允许创建学校信息" - }, - { - "code": "school:read", - "resource": "school", + "code": "activity:read", + "resource": "activity", "action": "read", - "name": "查看学校", - "description": "允许查看学校信息" + "name": "查看赛事活动", + "description": "允许查看已发布的赛事活动" }, { - "code": "school:update", - "resource": "school", - "action": "update", - "name": "更新学校", - "description": "允许更新学校信息" - }, - { - "code": "school:delete", - "resource": "school", - "action": "delete", - "name": "删除学校", - "description": "允许删除学校信息" - }, - { - "code": "grade:create", - "resource": "grade", - "action": "create", - "name": "创建年级", - "description": "允许创建年级" - }, - { - "code": "grade:read", - "resource": "grade", - "action": "read", - "name": "查看年级", - "description": "允许查看年级列表和详情" - }, - { - "code": "grade:update", - "resource": "grade", - "action": "update", - "name": "更新年级", - "description": "允许更新年级信息" - }, - { - "code": "grade:delete", - "resource": "grade", - "action": "delete", - "name": "删除年级", - "description": "允许删除年级" - }, - { - "code": "class:create", - "resource": "class", - "action": "create", - "name": "创建班级", - "description": "允许创建班级" - }, - { - "code": "class:read", - "resource": "class", - "action": "read", - "name": "查看班级", - "description": "允许查看班级列表和详情" - }, - { - "code": "class:update", - "resource": "class", - "action": "update", - "name": "更新班级", - "description": "允许更新班级信息" - }, - { - "code": "class:delete", - "resource": "class", - "action": "delete", - "name": "删除班级", - "description": "允许删除班级" - }, - { - "code": "department:create", - "resource": "department", - "action": "create", - "name": "创建部门", - "description": "允许创建部门" - }, - { - "code": "department:read", - "resource": "department", - "action": "read", - "name": "查看部门", - "description": "允许查看部门列表和详情" - }, - { - "code": "department:update", - "resource": "department", - "action": "update", - "name": "更新部门", - "description": "允许更新部门信息" - }, - { - "code": "department:delete", - "resource": "department", - "action": "delete", - "name": "删除部门", - "description": "允许删除部门" - }, - { - "code": "teacher:create", - "resource": "teacher", - "action": "create", - "name": "创建教师", - "description": "允许创建教师" - }, - { - "code": "teacher:read", - "resource": "teacher", - "action": "read", - "name": "查看教师", - "description": "允许查看教师列表和详情" - }, - { - "code": "teacher:update", - "resource": "teacher", - "action": "update", - "name": "更新教师", - "description": "允许更新教师信息" - }, - { - "code": "teacher:delete", - "resource": "teacher", - "action": "delete", - "name": "删除教师", - "description": "允许删除教师" - }, - { - "code": "student:create", - "resource": "student", - "action": "create", - "name": "创建学生", - "description": "允许创建学生" - }, - { - "code": "student:read", - "resource": "student", - "action": "read", - "name": "查看学生", - "description": "允许查看学生列表和详情" - }, - { - "code": "student:update", - "resource": "student", - "action": "update", - "name": "更新学生", - "description": "允许更新学生信息" - }, - { - "code": "student:delete", - "resource": "student", - "action": "delete", - "name": "删除学生", - "description": "允许删除学生" - }, - { - "code": "contest:create", - "resource": "contest", - "action": "create", - "name": "创建比赛", - "description": "允许创建比赛" - }, - { - "code": "contest:read", - "resource": "contest", - "action": "read", - "name": "查看比赛", - "description": "允许查看比赛列表和详情" - }, - { - "code": "contest:update", - "resource": "contest", - "action": "update", - "name": "更新比赛", - "description": "允许更新比赛信息" - }, - { - "code": "contest:delete", - "resource": "contest", - "action": "delete", - "name": "删除比赛", - "description": "允许删除比赛" - }, - { - "code": "contest:publish", - "resource": "contest", - "action": "publish", - "name": "发布比赛", - "description": "允许发布比赛" - }, - { - "code": "contest:team:create", - "resource": "contest:team", - "action": "create", - "name": "创建团队", - "description": "允许创建比赛团队" - }, - { - "code": "contest:team:read", - "resource": "contest:team", - "action": "read", - "name": "查看团队", - "description": "允许查看团队列表和详情" - }, - { - "code": "contest:team:update", - "resource": "contest:team", - "action": "update", - "name": "更新团队", - "description": "允许更新团队信息" - }, - { - "code": "contest:team:delete", - "resource": "contest:team", - "action": "delete", - "name": "删除团队", - "description": "允许删除团队" - }, - { - "code": "contest:team:manage", - "resource": "contest:team", - "action": "manage", - "name": "管理团队成员", - "description": "允许管理团队成员" - }, - { - "code": "contest:review:create", - "resource": "contest:review", - "action": "create", - "name": "创建评审规则", - "description": "允许创建评审规则" - }, - { - "code": "contest:review:read", - "resource": "contest:review", - "action": "read", - "name": "查看评审", - "description": "允许查看评审规则和评审记录" - }, - { - "code": "contest:review:update", - "resource": "contest:review", - "action": "update", - "name": "更新评审规则", - "description": "允许更新评审规则" - }, - { - "code": "contest:review:delete", - "resource": "contest:review", - "action": "delete", - "name": "删除评审规则", - "description": "允许删除评审规则" - }, - { - "code": "contest:review:assign", - "resource": "contest:review", - "action": "assign", - "name": "分配评审任务", - "description": "允许分配评审任务给评委" - }, - { - "code": "contest:review:score", - "resource": "contest:review", - "action": "score", - "name": "评审打分", - "description": "允许对作品进行评审打分" - }, - { - "code": "contest:judge:create", - "resource": "contest:judge", - "action": "create", - "name": "添加评委", - "description": "允许添加比赛评委" - }, - { - "code": "contest:judge:read", - "resource": "contest:judge", - "action": "read", - "name": "查看评委", - "description": "允许查看评委列表" - }, - { - "code": "contest:judge:update", - "resource": "contest:judge", - "action": "update", - "name": "更新评委", - "description": "允许更新评委信息" - }, - { - "code": "contest:judge:delete", - "resource": "contest:judge", - "action": "delete", - "name": "删除评委", - "description": "允许删除评委" - }, - { - "code": "contest:work:create", - "resource": "contest:work", - "action": "create", - "name": "创建作品", - "description": "允许创建参赛作品" - }, - { - "code": "contest:work:read", - "resource": "contest:work", - "action": "read", - "name": "查看作品", - "description": "允许查看作品列表和详情" - }, - { - "code": "contest:work:update", - "resource": "contest:work", - "action": "update", - "name": "更新作品", - "description": "允许更新作品信息" - }, - { - "code": "contest:work:delete", - "resource": "contest:work", - "action": "delete", - "name": "删除作品", - "description": "允许删除作品" - }, - { - "code": "contest:work:submit", - "resource": "contest:work", - "action": "submit", - "name": "提交作品", - "description": "允许提交作品" - }, - { - "code": "contest:work:review", - "resource": "contest:work", - "action": "review", - "name": "审核作品", - "description": "允许审核作品状态" - }, - { - "code": "contest:registration:create", - "resource": "contest:registration", - "action": "create", - "name": "创建报名", - "description": "允许创建报名记录" - }, - { - "code": "contest:registration:read", - "resource": "contest:registration", - "action": "read", - "name": "查看报名", - "description": "允许查看报名列表和详情" - }, - { - "code": "contest:registration:update", - "resource": "contest:registration", - "action": "update", - "name": "更新报名", - "description": "允许更新报名信息" - }, - { - "code": "contest:registration:delete", - "resource": "contest:registration", - "action": "delete", - "name": "删除报名", - "description": "允许删除报名记录" - }, - { - "code": "contest:registration:approve", - "resource": "contest:registration", - "action": "approve", - "name": "审核报名", - "description": "允许审核报名(通过/拒绝)" - }, - { - "code": "contest:notice:create", - "resource": "contest:notice", - "action": "create", - "name": "创建公告", - "description": "允许创建比赛公告" - }, - { - "code": "contest:notice:read", - "resource": "contest:notice", - "action": "read", - "name": "查看公告", - "description": "允许查看公告列表和详情" - }, - { - "code": "contest:notice:update", - "resource": "contest:notice", - "action": "update", - "name": "更新公告", - "description": "允许更新公告信息" - }, - { - "code": "contest:notice:delete", - "resource": "contest:notice", - "action": "delete", - "name": "删除公告", - "description": "允许删除公告" - }, - { - "code": "contest:notice:publish", - "resource": "contest:notice", - "action": "publish", - "name": "发布公告", - "description": "允许发布公告" + "code": "activity:guidance", + "resource": "activity", + "action": "guidance", + "name": "指导学生", + "description": "允许指导学生参赛" } ] diff --git a/backend/package.json b/backend/package.json index bd3d6e3..49c90b2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,12 +31,17 @@ "init:admin:permissions": "ts-node scripts/init-admin-permissions.ts", "init:menus": "ts-node scripts/init-menus.ts", "init:super-tenant": "ts-node scripts/init-super-tenant.ts", + "init:linksea-tenant": "ts-node scripts/init-linksea-tenant.ts", "init:tenant-admin": "ts-node scripts/init-tenant-admin.ts", "init:tenant-admin:permissions": "ts-node scripts/init-tenant-admin.ts --permissions-only", "init:tenant-permissions": "ts-node scripts/init-tenant-permissions.ts", "init:tenant-menu-permissions": "ts-node scripts/init-tenant-menu-permissions.ts", "update:password": "ts-node scripts/update-password.ts", - "fix:invalid-datetime": "ts-node scripts/fix-invalid-datetime.ts" + "fix:invalid-datetime": "ts-node scripts/fix-invalid-datetime.ts", + "cleanup:tenant-permissions": "ts-node scripts/cleanup-tenant-permissions.ts", + "init:roles:super": "ts-node scripts/init-roles-permissions.ts --super", + "init:roles": "ts-node scripts/init-roles-permissions.ts", + "init:roles:all": "ts-node scripts/init-roles-permissions.ts --all" }, "dependencies": { "@nestjs/common": "^10.3.3", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 83a1601..0fbe119 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -57,17 +57,18 @@ model Tenant { /// 用户表 model User { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - username String /// 用户名(在租户内唯一) - password String /// 密码(加密存储) - nickname String /// 昵称 - email String? /// 邮箱(在租户内唯一,可选) - phone String? /// 联系方式/手机号 - gender String? /// 性别:male-男,female-女 - avatar String? /// 头像URL - status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 租户ID + username String /// 用户名(在租户内唯一) + password String /// 密码(加密存储) + nickname String /// 昵称 + email String? /// 邮箱(在租户内唯一,可选) + phone String? /// 联系方式/手机号 + gender String? /// 性别:male-男,female-女 + avatar String? /// 头像URL + organization String? /// 所属单位(用于评委等独立用户) + status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 creator Int? @map("creator") /// 创建人ID modifier Int? @map("modifier") /// 修改人ID createTime DateTime @default(now()) @map("create_time") /// 创建时间 diff --git a/backend/scripts/add-activity-list-menu.ts b/backend/scripts/add-activity-list-menu.ts deleted file mode 100644 index 9460444..0000000 --- a/backend/scripts/add-activity-list-menu.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 添加赛事活动下的赛事列表菜单...\n'); - - // 查找赛事活动父菜单 - const activityParent = await prisma.menu.findFirst({ - where: { path: '/student-activities' } - }); - - if (!activityParent) { - console.log('❌ 未找到赛事活动父菜单'); - return; - } - - // 检查是否已存在赛事列表子菜单 - const existing = await prisma.menu.findFirst({ - where: { path: '/student-activities/list' } - }); - - if (existing) { - console.log('⏭️ 赛事列表菜单已存在'); - } else { - // 创建赛事列表子菜单 - const listMenu = await prisma.menu.create({ - data: { - name: '赛事列表', - path: '/student-activities/list', - component: 'contests/Activities', - permission: 'contest:activity:read', - sort: 0, // 排在最前面 - parentId: activityParent.id, - validState: 1, - } - }); - console.log('✅ 已创建: 赛事列表'); - - // 分配给所有租户 - const tenants = await prisma.tenant.findMany({ select: { id: true } }); - for (const tenant of tenants) { - await prisma.tenantMenu.create({ - data: { tenantId: tenant.id, menuId: listMenu.id } - }); - } - console.log('✅ 已分配给所有租户'); - } - - // 更新其他子菜单的排序 - await prisma.menu.updateMany({ - where: { path: '/student-activities/guidance' }, - data: { sort: 1 } - }); - await prisma.menu.updateMany({ - where: { path: '/student-activities/review' }, - data: { sort: 2 } - }); - await prisma.menu.updateMany({ - where: { path: '/student-activities/comments' }, - data: { sort: 3 } - }); - - // 显示最终结构 - console.log('\n📋 赛事活动最终菜单结构:'); - const children = await prisma.menu.findMany({ - where: { parentId: activityParent.id }, - orderBy: { sort: 'asc' } - }); - - console.log(`- ${activityParent.name} (${activityParent.path})`); - children.forEach(c => { - console.log(` - ${c.name} (${c.path})`); - }); - - console.log('\n✅ 完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/add-homework-permissions.ts b/backend/scripts/add-homework-permissions.ts deleted file mode 100644 index 34c926d..0000000 --- a/backend/scripts/add-homework-permissions.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔍 检查现有 homework 权限...'); - - // 获取所有租户 - const tenants = await prisma.tenant.findMany({ - select: { id: true, name: true } - }); - console.log(`找到 ${tenants.length} 个租户`); - - // 定义作业管理权限 - const homeworkPermissions = [ - { code: 'homework:read', name: '查看作业', resource: 'homework', action: 'read' }, - { code: 'homework:create', name: '创建作业', resource: 'homework', action: 'create' }, - { code: 'homework:update', name: '编辑作业', resource: 'homework', action: 'update' }, - { code: 'homework:delete', name: '删除作业', resource: 'homework', action: 'delete' }, - ]; - - for (const tenant of tenants) { - console.log(`\n📝 为租户 "${tenant.name}" (ID: ${tenant.id}) 添加权限...`); - - for (const perm of homeworkPermissions) { - // 检查权限是否已存在 - const existing = await prisma.permission.findFirst({ - where: { - tenantId: tenant.id, - code: perm.code, - } - }); - - if (existing) { - console.log(` ⏭️ ${perm.name} (${perm.code}) 已存在`); - } else { - await prisma.permission.create({ - data: { - tenantId: tenant.id, - code: perm.code, - name: perm.name, - resource: perm.resource, - action: perm.action, - description: `作业管理 - ${perm.name}`, - } - }); - console.log(` ✅ ${perm.name} (${perm.code}) 已创建`); - } - } - - // 获取管理员角色并分配权限 - const adminRole = await prisma.role.findFirst({ - where: { - tenantId: tenant.id, - code: 'admin', - } - }); - - if (adminRole) { - console.log(`\n🔐 为管理员角色分配权限...`); - - const allHomeworkPerms = await prisma.permission.findMany({ - where: { - tenantId: tenant.id, - resource: 'homework', - } - }); - - for (const perm of allHomeworkPerms) { - const existing = await prisma.rolePermission.findFirst({ - where: { - roleId: adminRole.id, - permissionId: perm.id, - } - }); - - if (!existing) { - await prisma.rolePermission.create({ - data: { - roleId: adminRole.id, - permissionId: perm.id, - } - }); - console.log(` ✅ 已分配 ${perm.code} 给管理员角色`); - } else { - console.log(` ⏭️ ${perm.code} 已分配`); - } - } - } - } - - console.log('\n✅ 权限配置完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/add-missing-contest-menus.ts b/backend/scripts/add-missing-contest-menus.ts deleted file mode 100644 index 9f3f1e8..0000000 --- a/backend/scripts/add-missing-contest-menus.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 添加缺失的赛事管理菜单...\n'); - - // 查找赛事管理父菜单 - const contestMenu = await prisma.menu.findFirst({ - where: { name: '赛事管理', parentId: null } - }); - - if (!contestMenu) { - console.log('❌ 未找到赛事管理菜单'); - return; - } - - console.log('✅ 找到赛事管理菜单 ID:', contestMenu.id); - - // 要添加的菜单 - const menusToAdd = [ - { - name: '评委管理', - path: '/contests/judges', - component: 'contests/judges/Index', - permission: 'contest:judge:read', - sort: 35, // 在评审规则之后 - }, - { - name: '评审进度', - path: '/contests/reviews/progress', - component: 'contests/reviews/Progress', - permission: 'contest:review:read', - sort: 36, - }, - ]; - - for (const menuData of menusToAdd) { - // 检查是否已存在 - const existing = await prisma.menu.findFirst({ - where: { path: menuData.path } - }); - - if (existing) { - console.log(`⏭️ ${menuData.name} 已存在`); - continue; - } - - // 创建菜单 - const newMenu = await prisma.menu.create({ - data: { - ...menuData, - parentId: contestMenu.id, - validState: 1, - } - }); - console.log(`✅ 已创建: ${menuData.name} (ID: ${newMenu.id})`); - - // 分配给所有租户 - const tenants = await prisma.tenant.findMany({ select: { id: true } }); - for (const tenant of tenants) { - await prisma.tenantMenu.create({ - data: { tenantId: tenant.id, menuId: newMenu.id } - }); - } - console.log(` 已分配给 ${tenants.length} 个租户`); - } - - // 显示最终结构 - console.log('\n📋 赛事管理最终菜单结构:'); - const children = await prisma.menu.findMany({ - where: { parentId: contestMenu.id }, - orderBy: { sort: 'asc' } - }); - - console.log(`- ${contestMenu.name}`); - children.forEach(c => { - console.log(` - ${c.name} | ${c.path}`); - }); - - console.log('\n✅ 完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/add-student-homework-menu.ts b/backend/scripts/add-student-homework-menu.ts deleted file mode 100644 index 9799f7d..0000000 --- a/backend/scripts/add-student-homework-menu.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 添加学生作业菜单...\n'); - - // 获取所有租户 - const tenants = await prisma.tenant.findMany({ - select: { id: true, name: true } - }); - - for (const tenant of tenants) { - console.log(`\n📝 处理租户: ${tenant.name} (ID: ${tenant.id})`); - - // 1. 创建学生作业权限(如果不存在) - let studentHomeworkPerm = await prisma.permission.findFirst({ - where: { tenantId: tenant.id, code: 'homework:student:read' } - }); - - if (!studentHomeworkPerm) { - studentHomeworkPerm = await prisma.permission.create({ - data: { - tenantId: tenant.id, - code: 'homework:student:read', - name: '学生查看作业', - resource: 'homework:student', - action: 'read', - description: '学生查看作业列表', - } - }); - console.log(' ✅ 创建权限: homework:student:read'); - } - - // 2. 分配给学生角色 - const studentRole = await prisma.role.findFirst({ - where: { tenantId: tenant.id, code: 'student' } - }); - - if (studentRole) { - const existing = await prisma.rolePermission.findFirst({ - where: { roleId: studentRole.id, permissionId: studentHomeworkPerm.id } - }); - if (!existing) { - await prisma.rolePermission.create({ - data: { roleId: studentRole.id, permissionId: studentHomeworkPerm.id } - }); - console.log(' ✅ 已分配给学生角色'); - } - } - - // 3. 创建学生作业菜单 - // 先检查是否已存在 - const existingMenu = await prisma.menu.findFirst({ - where: { path: '/student-homework' } - }); - - if (!existingMenu) { - const studentHomeworkMenu = await prisma.menu.create({ - data: { - name: '我的作业', - path: '/student-homework', - component: 'homework/StudentList', - icon: 'BookOutlined', - permission: 'homework:student:read', - sort: 15, // 放在作业管理之前 - validState: 1, - } - }); - console.log(' ✅ 创建菜单: 我的作业'); - - // 4. 将菜单分配给租户 - await prisma.tenantMenu.create({ - data: { - tenantId: tenant.id, - menuId: studentHomeworkMenu.id, - } - }); - console.log(' ✅ 菜单已分配给租户'); - } else { - console.log(' ⏭️ 菜单已存在'); - - // 确保菜单已分配给租户 - const tenantMenu = await prisma.tenantMenu.findFirst({ - where: { tenantId: tenant.id, menuId: existingMenu.id } - }); - if (!tenantMenu) { - await prisma.tenantMenu.create({ - data: { - tenantId: tenant.id, - menuId: existingMenu.id, - } - }); - console.log(' ✅ 菜单已分配给租户'); - } - } - } - - // 5. 移除学生角色的 homework:read 权限(这个权限是给管理员的) - console.log('\n📝 更新权限配置...'); - const allStudentRoles = await prisma.role.findMany({ - where: { code: 'student' } - }); - - for (const studentRole of allStudentRoles) { - // 获取 homework:read 权限 - const homeworkReadPerm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: 'homework:read' } - }); - - if (homeworkReadPerm) { - await prisma.rolePermission.deleteMany({ - where: { roleId: studentRole.id, permissionId: homeworkReadPerm.id } - }); - console.log(` ✅ 移除学生角色(ID:${studentRole.id})的 homework:read 权限`); - } - } - - console.log('\n✅ 学生作业菜单配置完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/assign-homework-permissions.ts b/backend/scripts/assign-homework-permissions.ts deleted file mode 100644 index cfe7b5b..0000000 --- a/backend/scripts/assign-homework-permissions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔍 查询角色和权限...'); - - // 获取所有角色 - const roles = await prisma.role.findMany({ - select: { id: true, name: true, code: true, tenantId: true } - }); - console.log('角色:', roles); - - // 获取 homework 权限 - const homeworkPerms = await prisma.permission.findMany({ - where: { resource: 'homework' }, - select: { id: true, code: true, tenantId: true } - }); - console.log('Homework 权限:', homeworkPerms); - - // 为每个角色分配所有 homework 权限 - for (const role of roles) { - console.log(`\n🔐 为角色 "${role.name}" 分配权限...`); - - const permsForTenant = homeworkPerms.filter(p => p.tenantId === role.tenantId); - - for (const perm of permsForTenant) { - const existing = await prisma.rolePermission.findFirst({ - where: { - roleId: role.id, - permissionId: perm.id, - } - }); - - if (!existing) { - await prisma.rolePermission.create({ - data: { - roleId: role.id, - permissionId: perm.id, - } - }); - console.log(` ✅ 已分配 ${perm.code}`); - } else { - console.log(` ⏭️ ${perm.code} 已分配`); - } - } - } - - console.log('\n✅ 权限分配完成!请重新登录以刷新权限。'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/check-contest-menus.ts b/backend/scripts/check-contest-menus.ts deleted file mode 100644 index 333ed1b..0000000 --- a/backend/scripts/check-contest-menus.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - // 查找赛事管理菜单 - const contestMenu = await prisma.menu.findFirst({ - where: { name: '赛事管理' } - }); - - console.log('📋 赛事管理菜单:', contestMenu ? `ID ${contestMenu.id}` : '不存在'); - - if (contestMenu) { - const children = await prisma.menu.findMany({ - where: { parentId: contestMenu.id }, - orderBy: { sort: 'asc' } - }); - console.log('\n子菜单:'); - if (children.length === 0) { - console.log(' (无子菜单)'); - } - children.forEach(c => console.log(` - ${c.name} | ${c.path} | ${c.permission}`)); - } - - // 查找所有赛事相关菜单 - console.log('\n📋 所有赛事相关菜单:'); - const allContestMenus = await prisma.menu.findMany({ - where: { - OR: [ - { path: { contains: 'contest' } }, - { name: { contains: '赛事' } }, - { name: { contains: '赛果' } }, - { name: { contains: '报名' } }, - { name: { contains: '作品' } }, - ] - }, - orderBy: [{ parentId: 'asc' }, { sort: 'asc' }] - }); - - allContestMenus.forEach(m => { - const type = m.parentId ? ' (子)' : '(父)'; - console.log(`${type} ${m.name} | ${m.path} | parentId: ${m.parentId}`); - }); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/check-student-menus.ts b/backend/scripts/check-student-menus.ts deleted file mode 100644 index e7c75cb..0000000 --- a/backend/scripts/check-student-menus.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const studentRole = await prisma.role.findFirst({ where: { code: 'student' } }); - - if (!studentRole) { - console.log('未找到学生角色'); - return; - } - - // 获取学生权限 - const perms = await prisma.rolePermission.findMany({ - where: { roleId: studentRole.id }, - include: { permission: true } - }); - - const permCodes = perms.map(p => p.permission.code); - console.log('📋 学生权限:', permCodes.join(', ')); - - // 获取租户菜单 - const tenantMenus = await prisma.tenantMenu.findMany({ - where: { tenantId: studentRole.tenantId }, - include: { menu: true } - }); - - // 过滤出学生能看到的菜单 - const visibleMenus = tenantMenus - .filter(tm => tm.menu.permission && permCodes.includes(tm.menu.permission)) - .map(tm => ({ - name: tm.menu.name, - path: tm.menu.path, - permission: tm.menu.permission, - component: tm.menu.component, - })); - - console.log('\n📱 学生可见菜单:'); - visibleMenus.forEach(m => { - console.log(` - ${m.name}`); - console.log(` 路径: ${m.path}`); - console.log(` 权限: ${m.permission}`); - console.log(` 组件: ${m.component}`); - }); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/check-tenant-menus.ts b/backend/scripts/check-tenant-menus.ts deleted file mode 100644 index 0c4c477..0000000 --- a/backend/scripts/check-tenant-menus.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - // 获取所有赛事活动相关菜单 - const activityMenus = await prisma.menu.findMany({ - where: { path: { startsWith: '/student-activities' } }, - orderBy: { sort: 'asc' } - }); - - console.log('📋 赛事活动相关菜单:'); - activityMenus.forEach(m => { - console.log(` ID: ${m.id} | ${m.name} | ${m.path}`); - }); - - // 获取租户1的菜单分配 - console.log('\n📋 租户1的菜单分配:'); - const tenantMenus = await prisma.tenantMenu.findMany({ - where: { tenantId: 1 }, - include: { menu: true } - }); - - const assignedMenuIds = tenantMenus.map(tm => tm.menuId); - - // 检查哪些赛事活动菜单已分配 - console.log('\n赛事活动菜单分配状态:'); - activityMenus.forEach(m => { - const assigned = assignedMenuIds.includes(m.id); - console.log(` ${assigned ? '✅' : '❌'} ${m.name} (ID: ${m.id})`); - }); - - // 补充分配缺失的菜单 - console.log('\n📝 补充分配缺失的菜单...'); - for (const menu of activityMenus) { - if (!assignedMenuIds.includes(menu.id)) { - await prisma.tenantMenu.create({ - data: { tenantId: 1, menuId: menu.id } - }); - console.log(` ✅ 已分配: ${menu.name}`); - } - } - - console.log('\n✅ 完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/cleanup-tenant-permissions.ts b/backend/scripts/cleanup-tenant-permissions.ts new file mode 100644 index 0000000..2974afa --- /dev/null +++ b/backend/scripts/cleanup-tenant-permissions.ts @@ -0,0 +1,127 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +const nodeEnv = process.env.NODE_ENV || 'development'; +const envFile = `.env.${nodeEnv}`; +const backendDir = path.resolve(__dirname, '..'); +const envPath = path.resolve(backendDir, envFile); + +dotenv.config({ path: envPath }); + +if (!process.env.DATABASE_URL) { + dotenv.config({ path: path.resolve(backendDir, '.env') }); +} + +if (!process.env.DATABASE_URL) { + console.error('DATABASE_URL not found'); + process.exit(1); +} + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// 超级管理员专属权限(普通租户不应该有这些权限) +const superAdminOnlyPermissions = [ + 'tenant:create', + 'tenant:update', + 'tenant:delete', +]; + +async function cleanupTenantPermissions() { + try { + console.log('🚀 开始清理普通租户的超级管理员权限...\n'); + + // 1. 获取所有非超级租户 + const normalTenants = await prisma.tenant.findMany({ + where: { + isSuper: { not: 1 }, + validState: 1, + }, + }); + + console.log(`找到 ${normalTenants.length} 个普通租户\n`); + + for (const tenant of normalTenants) { + console.log(`处理租户: ${tenant.name} (${tenant.code})`); + + // 2. 找到该租户下的超级管理员专属权限 + const permissionsToRemove = await prisma.permission.findMany({ + where: { + tenantId: tenant.id, + code: { in: superAdminOnlyPermissions }, + }, + }); + + if (permissionsToRemove.length === 0) { + console.log(` ✓ 没有需要清理的权限\n`); + continue; + } + + const permissionIds = permissionsToRemove.map((p) => p.id); + console.log(` 找到 ${permissionsToRemove.length} 个需要清理的权限: ${permissionsToRemove.map((p) => p.code).join(', ')}`); + + // 3. 删除角色-权限关联 + const deletedRolePermissions = await prisma.rolePermission.deleteMany({ + where: { + permissionId: { in: permissionIds }, + }, + }); + console.log(` 删除了 ${deletedRolePermissions.count} 条角色-权限关联`); + + // 4. 删除权限记录 + const deletedPermissions = await prisma.permission.deleteMany({ + where: { + id: { in: permissionIds }, + }, + }); + console.log(` 删除了 ${deletedPermissions.count} 条权限记录\n`); + } + + // 5. 更新租户管理菜单权限 + console.log('更新租户管理菜单权限...'); + const tenantMenu = await prisma.menu.findFirst({ + where: { + name: '租户管理', + path: '/system/tenants', + }, + }); + + if (tenantMenu) { + if (tenantMenu.permission !== 'tenant:update') { + await prisma.menu.update({ + where: { id: tenantMenu.id }, + data: { permission: 'tenant:update' }, + }); + console.log(`✅ 菜单权限已更新为 tenant:update (原: ${tenantMenu.permission})`); + } else { + console.log('✅ 菜单权限已经是 tenant:update'); + } + } else { + console.log('⚠️ 未找到租户管理菜单'); + } + + console.log('\n✅ 清理完成!'); + console.log('\n说明:'); + console.log(' - 普通租户现在只有 tenant:read 权限(用于读取租户列表)'); + console.log(' - 租户管理菜单需要 tenant:update 权限才能看到'); + console.log(' - 只有超级租户才有 tenant:create/update/delete 权限'); + } catch (error) { + console.error('❌ 清理失败:', error); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +cleanupTenantPermissions() + .then(() => { + console.log('\n🎉 脚本执行完成!'); + process.exit(0); + }) + .catch((error) => { + console.error('\n💥 脚本执行失败:', error); + process.exit(1); + }); diff --git a/backend/scripts/debug-activity-menus.ts b/backend/scripts/debug-activity-menus.ts deleted file mode 100644 index 8673672..0000000 --- a/backend/scripts/debug-activity-menus.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const menus = await prisma.menu.findMany({ - where: { path: { startsWith: '/student-activities' } }, - orderBy: [{ parentId: 'asc' }, { sort: 'asc' }] - }); - - console.log('📋 赛事活动相关菜单:'); - menus.forEach(m => { - console.log(`\n name: ${m.name}`); - console.log(` path: ${m.path}`); - console.log(` component: ${m.component || '(无)'}`); - console.log(` parentId: ${m.parentId || '(无 - 顶级菜单)'}`); - console.log(` permission: ${m.permission}`); - }); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/debug-menus.ts b/backend/scripts/debug-menus.ts deleted file mode 100644 index 879a39e..0000000 --- a/backend/scripts/debug-menus.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const tenantId = 1; - - // 获取租户分配的菜单ID - const tenantMenus = await prisma.tenantMenu.findMany({ - where: { tenantId }, - }); - const menuIds = tenantMenus.map((tm) => tm.menuId); - - console.log('📋 租户分配的菜单IDs:', menuIds.length, '个'); - - // 获取租户分配的所有菜单(包括父菜单) - const allMenus = await prisma.menu.findMany({ - where: { - OR: [ - { id: { in: menuIds } }, - { children: { some: { id: { in: menuIds } } } }, - ], - validState: 1, - }, - orderBy: { - sort: 'asc', - }, - }); - - // 过滤出赛事活动相关的 - console.log('\n📋 赛事活动相关菜单 (从数据库查询):'); - const activityMenus = allMenus.filter(m => m.path?.startsWith('/student-activities')); - activityMenus.forEach(m => { - console.log(` ID: ${m.id} | ${m.name}`); - console.log(` path: ${m.path}`); - console.log(` component: ${m.component || '(null)'}`); - console.log(` parentId: ${m.parentId}`); - }); - - // 构建树形结构 - const buildTree = (menus: any[], parentId: number | null = null): any[] => { - return menus - .filter((menu) => menu.parentId === parentId) - .map((menu) => ({ - ...menu, - children: buildTree(menus, menu.id), - })); - }; - - const menuTree = buildTree(allMenus); - - // 找到赛事活动菜单树 - const activityTree = menuTree.find(m => m.path === '/student-activities'); - if (activityTree) { - console.log('\n📋 赛事活动菜单树 (模拟API返回):'); - const printTree = (menu: any, indent = '') => { - console.log(`${indent}${menu.name} (${menu.path})`); - console.log(`${indent} component: ${menu.component || '(null)'}`); - if (menu.children && menu.children.length > 0) { - menu.children.forEach((child: any) => printTree(child, indent + ' ')); - } - }; - printTree(activityTree); - } else { - console.log('\n❌ 未找到赛事活动菜单树'); - } - - // 模拟权限过滤 - const userPermissions = ['workbench:read', 'contest:activity:read', 'homework:student:read']; - - const filterMenus = (menus: any[]): any[] => { - return menus - .map((menu) => { - let filteredChildren: any[] = []; - if (menu.children && menu.children.length > 0) { - filteredChildren = filterMenus(menu.children); - } - - const hasPermissionField = menu.permission && menu.permission.trim() !== ''; - - if (hasPermissionField) { - if (!userPermissions.includes(menu.permission)) { - if (filteredChildren.length > 0) { - return { ...menu, children: filteredChildren }; - } - return null; - } - } - - if (!hasPermissionField) { - if (filteredChildren.length === 0 && (!menu.path || menu.path.trim() === '')) { - return null; - } - if (filteredChildren.length > 0) { - return { ...menu, children: filteredChildren }; - } - return null; - } - - const filtered = { ...menu }; - if (filteredChildren.length > 0) { - filtered.children = filteredChildren; - } - return filtered; - }) - .filter((menu) => menu !== null); - }; - - const filteredTree = filterMenus(menuTree); - const filteredActivityTree = filteredTree.find(m => m.path === '/student-activities'); - - if (filteredActivityTree) { - console.log('\n📋 权限过滤后的赛事活动菜单:'); - const printTree = (menu: any, indent = '') => { - console.log(`${indent}${menu.name} (${menu.path})`); - console.log(`${indent} component: ${menu.component || '(null)'}`); - if (menu.children && menu.children.length > 0) { - menu.children.forEach((child: any) => printTree(child, indent + ' ')); - } - }; - printTree(filteredActivityTree); - - console.log('\n📋 JSON格式 (检查component字段):'); - console.log(JSON.stringify(filteredActivityTree, null, 2)); - } -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/finalize-student-config.ts b/backend/scripts/finalize-student-config.ts deleted file mode 100644 index c088410..0000000 --- a/backend/scripts/finalize-student-config.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 最终配置学生权限...\n'); - - // 获取学生角色 - const studentRoles = await prisma.role.findMany({ - where: { code: 'student' } - }); - - for (const studentRole of studentRoles) { - console.log(`\n处理学生角色 ID: ${studentRole.id}`); - - // 学生最终需要的权限 - const neededPerms = [ - 'workbench:read', // 工作台 - 'contest:activity:read', // 赛事活动 - 'homework:student:read', // 我的作业菜单 - ]; - - // 确保所有需要的权限都已分配 - for (const permCode of neededPerms) { - const perm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: permCode } - }); - - if (perm) { - const existing = await prisma.rolePermission.findFirst({ - where: { roleId: studentRole.id, permissionId: perm.id } - }); - if (!existing) { - await prisma.rolePermission.create({ - data: { roleId: studentRole.id, permissionId: perm.id } - }); - console.log(` ✅ 已添加: ${permCode}`); - } else { - console.log(` ⏭️ 已存在: ${permCode}`); - } - } else { - console.log(` ❌ 权限不存在: ${permCode}`); - } - } - - // 移除不需要的权限 - const permsToRemove = ['homework:read']; - for (const permCode of permsToRemove) { - const perm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: permCode } - }); - if (perm) { - const deleted = await prisma.rolePermission.deleteMany({ - where: { roleId: studentRole.id, permissionId: perm.id } - }); - if (deleted.count > 0) { - console.log(` 🗑️ 已移除: ${permCode}`); - } - } - } - } - - // 更新学生作业菜单 - console.log('\n📝 更新学生作业菜单...'); - const studentMenu = await prisma.menu.findFirst({ - where: { path: '/student-homework' } - }); - - if (studentMenu) { - await prisma.menu.update({ - where: { id: studentMenu.id }, - data: { - permission: 'homework:student:read', - component: 'homework/StudentList', - } - }); - console.log(' ✅ 菜单已更新'); - } - - // 显示最终权限 - console.log('\n📋 学生角色最终权限:'); - for (const studentRole of studentRoles) { - const perms = await prisma.rolePermission.findMany({ - where: { roleId: studentRole.id }, - include: { permission: true } - }); - console.log(`\n 角色 ID ${studentRole.id}:`); - perms.forEach(p => console.log(` - ${p.permission.code}: ${p.permission.name}`)); - } - - console.log('\n✅ 配置完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/fix-duplicate-activity-menu.ts b/backend/scripts/fix-duplicate-activity-menu.ts deleted file mode 100644 index 1f70014..0000000 --- a/backend/scripts/fix-duplicate-activity-menu.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 修复重复的赛事活动菜单...\n'); - - // 查找原来的赛事活动菜单(在赛事管理下的,路径是 /contests/activities) - const oldActivityMenu = await prisma.menu.findFirst({ - where: { path: '/contests/activities' } - }); - - if (oldActivityMenu) { - // 把它改回 contest:read 权限,这样只有管理员能看到 - await prisma.menu.update({ - where: { id: oldActivityMenu.id }, - data: { permission: 'contest:read' } - }); - console.log('✅ 原赛事活动菜单权限更新为 contest:read'); - } - - // 验证最终结果 - console.log('\n📋 最终菜单配置:'); - const menus = await prisma.menu.findMany({ - where: { - OR: [ - { path: '/contests/activities' }, - { path: '/student-activities' }, - ] - }, - select: { name: true, path: true, permission: true, parentId: true } - }); - - menus.forEach(m => { - const type = m.parentId ? '(子菜单)' : '(一级菜单)'; - console.log(` - ${m.name} ${type}`); - console.log(` 路径: ${m.path}`); - console.log(` 权限: ${m.permission}`); - }); - - console.log('\n✅ 修复完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/fix-invalid-datetime.ts b/backend/scripts/fix-invalid-datetime.ts deleted file mode 100644 index cc66e4c..0000000 --- a/backend/scripts/fix-invalid-datetime.ts +++ /dev/null @@ -1,138 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck - -// 加载环境变量(必须在其他导入之前) -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -// 根据 NODE_ENV 加载对应的环境配置文件 -const nodeEnv = process.env.NODE_ENV || 'development'; -const envFile = `.env.${nodeEnv}`; -// scripts 目录的父目录就是 backend 目录 -const backendDir = path.resolve(__dirname, '..'); -const envPath = path.resolve(backendDir, envFile); - -// 尝试加载环境特定的配置文件 -dotenv.config({ path: envPath }); - -// 如果环境特定文件不存在,尝试加载默认的 .env 文件 -if (!process.env.DATABASE_URL) { - dotenv.config({ path: path.resolve(backendDir, '.env') }); -} - -// 验证必要的环境变量 -if (!process.env.DATABASE_URL) { - console.error('❌ 错误: 未找到 DATABASE_URL 环境变量'); - console.error(` 请确保存在以下文件之一:`); - console.error(` - ${envPath}`); - console.error(` - ${path.resolve(backendDir, '.env')}`); - console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`); - process.exit(1); -} - -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function fixInvalidDatetime() { - try { - console.log('🔧 开始修复无效的日期时间数据...\n'); - - // 获取当前时间作为默认值 - const now = new Date(); - - // 修复 users 表中的无效日期 - console.log('📋 修复 users 表中的 modify_time...'); - const nowStr = now.toISOString().slice(0, 19).replace('T', ' '); - const userResult = await prisma.$executeRawUnsafe(` - UPDATE \`users\` - SET modify_time = '${nowStr}' - WHERE modify_time = '0000-00-00 00:00:00' - OR modify_time < '1970-01-01 00:00:00' - OR YEAR(modify_time) = 0 - OR MONTH(modify_time) = 0 - OR DAY(modify_time) = 0 - `); - console.log(` ✅ 修复了 ${userResult} 条用户记录\n`); - - // 修复其他所有包含 modify_time 的表 - const tables = [ - 'tenants', - 'roles', - 'menus', - 'permissions', - 'dicts', - 'dict_items', - 'configs', - 'schools', - 'grades', - 'departments', - 'classes', - 'teachers', - 'students', - 'contests', - 'contest_teams', - 'contest_team_members', - 'contest_registrations', - 'contest_works', - 'contest_work_attachments', - 'contest_work_scores', - 'contest_work_judge_assignments', - 'contest_judges', - 'contest_notices', - 'contest_results', - ]; - - let totalFixed = userResult; - - for (const table of tables) { - try { - // 使用 Prisma 的原始 SQL,直接插入日期值 - const result = await prisma.$executeRawUnsafe(` - UPDATE \`${table}\` - SET modify_time = '${nowStr}' - WHERE modify_time = '0000-00-00 00:00:00' - OR modify_time < '1970-01-01 00:00:00' - OR YEAR(modify_time) = 0 - OR MONTH(modify_time) = 0 - OR DAY(modify_time) = 0 - `); - if (result > 0) { - console.log(` ✅ ${table}: 修复了 ${result} 条记录`); - totalFixed += result; - } - } catch (error: any) { - // 如果表不存在或没有 modify_time 字段,跳过 - if ( - error.code !== 'P2025' && - !error.message.includes('Unknown column') - ) { - console.log(` ⚠️ ${table}: ${error.message}`); - } - } - } - - console.log(`\n✅ 总共修复了 ${totalFixed} 条记录`); - console.log('\n💡 建议:检查 MySQL 的 sql_mode 设置,确保禁止无效日期'); - console.log(' 可以在 my.cnf 或 my.ini 中添加:'); - console.log( - ' sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"', - ); - } catch (error) { - console.error('\n💥 修复失败:', error); - throw error; - } finally { - await prisma.$disconnect(); - } -} - -// 执行修复 -fixInvalidDatetime() - .then(() => { - console.log('\n🎉 修复脚本执行完成!'); - process.exit(0); - }) - .catch((error) => { - console.error('\n💥 修复脚本执行失败:', error); - process.exit(1); - }); diff --git a/backend/scripts/fix-student-activity-menu.ts b/backend/scripts/fix-student-activity-menu.ts deleted file mode 100644 index 86f216b..0000000 --- a/backend/scripts/fix-student-activity-menu.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 修复学生赛事活动菜单...\n'); - - // 获取所有租户 - const tenants = await prisma.tenant.findMany({ - select: { id: true, name: true } - }); - - // 检查是否已存在独立的赛事活动菜单 - let studentActivityMenu = await prisma.menu.findFirst({ - where: { path: '/student-activities' } - }); - - if (!studentActivityMenu) { - // 创建独立的赛事活动菜单(一级菜单) - studentActivityMenu = await prisma.menu.create({ - data: { - name: '赛事活动', - path: '/student-activities', - component: 'contests/Activities', - icon: 'TrophyOutlined', - permission: 'contest:activity:read', - sort: 10, // 放在工作台之后 - parentId: null, // 一级菜单 - validState: 1, - } - }); - console.log('✅ 创建菜单: 赛事活动 (独立路由)'); - } else { - console.log('⏭️ 菜单已存在: 赛事活动'); - } - - // 为每个租户分配菜单 - for (const tenant of tenants) { - const existing = await prisma.tenantMenu.findFirst({ - where: { tenantId: tenant.id, menuId: studentActivityMenu.id } - }); - - if (!existing) { - await prisma.tenantMenu.create({ - data: { - tenantId: tenant.id, - menuId: studentActivityMenu.id, - } - }); - console.log(`✅ 菜单已分配给租户: ${tenant.name}`); - } - } - - // 更新原有的赛事活动菜单(在赛事管理下的)权限,确保只有管理员能看到 - const oldActivityMenu = await prisma.menu.findFirst({ - where: { - path: '/contests/activities', - parentId: { not: null } // 有父菜单的 - } - }); - - if (oldActivityMenu) { - // 保持原有权限 contest:read,这样管理员可以看到 - console.log('⏭️ 原赛事活动菜单保持不变(管理员使用)'); - } - - console.log('\n✅ 配置完成!'); - console.log('\n学生菜单结构:'); - console.log(' - 工作台'); - console.log(' - 赛事活动 (独立路由 /student-activities)'); - console.log(' - 我的作业 (独立路由 /student-homework)'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/fix-student-perm.ts b/backend/scripts/fix-student-perm.ts deleted file mode 100644 index cb081b1..0000000 --- a/backend/scripts/fix-student-perm.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const studentRole = await prisma.role.findFirst({ where: { code: 'student' } }); - if (!studentRole) { - console.log('未找到学生角色'); - return; - } - - // 需要的权限 - const neededPerms = ['workbench:read', 'homework:read', 'contest:activity:read']; - - for (const permCode of neededPerms) { - const perm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: permCode } - }); - - if (perm) { - const existing = await prisma.rolePermission.findFirst({ - where: { roleId: studentRole.id, permissionId: perm.id } - }); - if (!existing) { - await prisma.rolePermission.create({ - data: { roleId: studentRole.id, permissionId: perm.id } - }); - console.log(`✅ 已添加: ${permCode}`); - } else { - console.log(`⏭️ 已存在: ${permCode}`); - } - } else { - console.log(`❌ 权限不存在: ${permCode}`); - } - } - - // 显示最终权限 - const perms = await prisma.rolePermission.findMany({ - where: { roleId: studentRole.id }, - include: { permission: true } - }); - console.log('\n📋 学生最终权限:'); - perms.forEach(p => console.log(` - ${p.permission.code}: ${p.permission.name}`)); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/fix-workbench-permission.ts b/backend/scripts/fix-workbench-permission.ts deleted file mode 100644 index 0c0174c..0000000 --- a/backend/scripts/fix-workbench-permission.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 修复工作台权限配置...\n'); - - // 1. 获取所有租户 - const tenants = await prisma.tenant.findMany({ - select: { id: true, name: true } - }); - - for (const tenant of tenants) { - console.log(`📝 处理租户: ${tenant.name} (ID: ${tenant.id})`); - - // 2. 检查/创建 workbench:read 权限 - let perm = await prisma.permission.findFirst({ - where: { tenantId: tenant.id, code: 'workbench:read' } - }); - - if (!perm) { - perm = await prisma.permission.create({ - data: { - tenantId: tenant.id, - code: 'workbench:read', - name: '查看工作台', - resource: 'workbench', - action: 'read', - description: '访问工作台页面', - } - }); - console.log(' ✅ 创建权限: workbench:read'); - } else { - console.log(' ⏭️ 权限已存在: workbench:read'); - } - - // 3. 为该租户的所有角色分配此权限 - const roles = await prisma.role.findMany({ - where: { tenantId: tenant.id }, - select: { id: true, name: true, code: true } - }); - - for (const role of roles) { - const existing = await prisma.rolePermission.findFirst({ - where: { roleId: role.id, permissionId: perm.id } - }); - - if (!existing) { - await prisma.rolePermission.create({ - data: { roleId: role.id, permissionId: perm.id } - }); - console.log(` ✅ 分配给角色: ${role.name}`); - } else { - console.log(` ⏭️ 角色 ${role.name} 已有此权限`); - } - } - } - - // 4. 更新工作台菜单的权限配置 - console.log('\n🔧 更新工作台菜单权限配置...'); - const workbenchMenu = await prisma.menu.findFirst({ - where: { name: '工作台' } - }); - - if (workbenchMenu) { - if (!workbenchMenu.permission || workbenchMenu.permission.trim() === '') { - await prisma.menu.update({ - where: { id: workbenchMenu.id }, - data: { permission: 'workbench:read' } - }); - console.log('✅ 工作台菜单已设置 permission: workbench:read'); - } else { - console.log(`⏭️ 工作台菜单已有权限: ${workbenchMenu.permission}`); - } - } else { - console.log('⚠️ 未找到工作台菜单'); - } - - console.log('\n✅ 完成!请重新登录以查看更新后的菜单。'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/init-linksea-tenant.ts b/backend/scripts/init-linksea-tenant.ts new file mode 100644 index 0000000..3de46cd --- /dev/null +++ b/backend/scripts/init-linksea-tenant.ts @@ -0,0 +1,210 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +// 加载环境变量(必须在其他导入之前) +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +// 根据 NODE_ENV 加载对应的环境配置文件 +const nodeEnv = process.env.NODE_ENV || 'development'; +const envFile = `.env.${nodeEnv}`; +// scripts 目录的父目录就是 backend 目录 +const backendDir = path.resolve(__dirname, '..'); +const envPath = path.resolve(backendDir, envFile); + +// 尝试加载环境特定的配置文件 +dotenv.config({ path: envPath }); + +// 如果环境特定文件不存在,尝试加载默认的 .env 文件 +if (!process.env.DATABASE_URL) { + dotenv.config({ path: path.resolve(backendDir, '.env') }); +} + +// 验证必要的环境变量 +if (!process.env.DATABASE_URL) { + console.error('❌ 错误: 未找到 DATABASE_URL 环境变量'); + console.error(` 请确保存在以下文件之一:`); + console.error(` - ${envPath}`); + console.error(` - ${path.resolve(backendDir, '.env')}`); + console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`); + process.exit(1); +} + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🚀 开始创建 LinkSea 普通租户...\n'); + + const tenantCode = 'linksea'; + const menuNames = ['赛事管理', '系统管理']; + + // 1. 查找或创建租户 + console.log(`📋 步骤 1: 查找或创建租户 "${tenantCode}"...`); + let tenant = await prisma.tenant.findUnique({ + where: { code: tenantCode }, + }); + + if (!tenant) { + // 创建普通租户 + tenant = await prisma.tenant.create({ + data: { + name: 'LinkSea 租户', + code: tenantCode, + domain: tenantCode, + description: 'LinkSea 普通租户', + isSuper: 0, + validState: 1, + }, + }); + console.log(`✅ 租户创建成功: ${tenant.name} (${tenant.code})\n`); + } else { + if (tenant.validState !== 1) { + console.error(`❌ 错误: 租户 "${tenantCode}" 状态无效!`); + process.exit(1); + } + console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`); + } + + // 2. 查找指定的菜单(顶级菜单) + console.log(`📋 步骤 2: 查找菜单 "${menuNames.join('", "')}"...`); + const menus = await prisma.menu.findMany({ + where: { + name: { in: menuNames }, + parentId: null, // 只查找顶级菜单 + validState: 1, + }, + }); + + if (menus.length === 0) { + console.error(`❌ 错误: 未找到指定的菜单!`); + console.error(` 请确保菜单 "${menuNames.join('", "')}" 已初始化`); + console.error(` 运行: pnpm init:menus`); + process.exit(1); + } + + if (menus.length !== menuNames.length) { + const foundMenuNames = menus.map((m) => m.name); + const missingMenus = menuNames.filter( + (name) => !foundMenuNames.includes(name), + ); + console.warn(`⚠️ 警告: 部分菜单未找到: ${missingMenus.join(', ')}`); + console.log(` 找到的菜单: ${foundMenuNames.join(', ')}\n`); + } else { + console.log(`✅ 找到 ${menus.length} 个菜单:`); + menus.forEach((menu) => { + console.log(` ✓ ${menu.name}`); + }); + console.log(''); + } + + // 3. 递归获取菜单及其所有子菜单 + console.log(`📋 步骤 3: 获取菜单及其所有子菜单...`); + const menuIds = new Set(); + + // 递归函数:获取菜单及其所有子菜单的ID + async function getMenuAndChildrenIds(menuId: number) { + menuIds.add(menuId); + + // 获取所有子菜单 + const children = await prisma.menu.findMany({ + where: { + parentId: menuId, + validState: 1, + }, + }); + + // 递归获取子菜单的子菜单 + for (const child of children) { + await getMenuAndChildrenIds(child.id); + } + } + + // 为每个顶级菜单获取所有子菜单 + for (const menu of menus) { + await getMenuAndChildrenIds(menu.id); + } + + const menuIdArray = Array.from(menuIds); + console.log(`✅ 共找到 ${menuIdArray.length} 个菜单(包括子菜单)\n`); + + // 4. 获取租户已分配的菜单 + console.log(`📋 步骤 4: 检查租户已分配的菜单...`); + const existingTenantMenus = await prisma.tenantMenu.findMany({ + where: { + tenantId: tenant.id, + }, + select: { + menuId: true, + }, + }); + + const existingMenuIds = new Set(existingTenantMenus.map((tm) => tm.menuId)); + + // 5. 为租户分配菜单(只分配新的菜单) + console.log(`📋 步骤 5: 为租户分配菜单...`); + const menusToAdd = menuIdArray.filter((id) => !existingMenuIds.has(id)); + + if (menusToAdd.length === 0) { + console.log(`✅ 租户已拥有所有指定的菜单\n`); + } else { + let addedCount = 0; + const menuNamesToAdd: string[] = []; + + for (const menuId of menusToAdd) { + const menu = await prisma.menu.findUnique({ + where: { id: menuId }, + select: { name: true }, + }); + + await prisma.tenantMenu.create({ + data: { + tenantId: tenant.id, + menuId: menuId, + }, + }); + addedCount++; + if (menu) { + menuNamesToAdd.push(menu.name); + } + } + + console.log(`✅ 为租户添加了 ${addedCount} 个菜单:`); + menuNamesToAdd.forEach((name) => { + console.log(` ✓ ${name}`); + }); + console.log( + `\n✅ 租户现在拥有 ${menuIdArray.length} 个菜单(包括子菜单)\n`, + ); + } + + // 6. 验证结果 + console.log('📊 初始化结果:'); + console.log('========================================'); + console.log('租户信息:'); + console.log(` 租户编码: ${tenant.code}`); + console.log(` 租户名称: ${tenant.name}`); + console.log(` 租户类型: ${tenant.isSuper === 1 ? '超级租户' : '普通租户'}`); + console.log(` 访问链接: http://your-domain.com/?tenant=${tenant.code}`); + console.log('========================================'); + console.log('分配的菜单:'); + console.log(` 顶级菜单: ${menuNames.join(', ')}`); + console.log(` 菜单总数: ${menuIdArray.length} 个(包括子菜单)`); + console.log('========================================'); + console.log('\n💡 提示:'); + console.log(' 如需创建管理员账号,请运行: pnpm init:tenant-admin linksea'); + console.log('========================================'); +} + +main() + .then(() => { + console.log('\n🎉 LinkSea 租户创建脚本执行完成!'); + process.exit(0); + }) + .catch((error) => { + console.error('\n💥 LinkSea 租户创建脚本执行失败:', error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/scripts/init-menus.ts b/backend/scripts/init-menus.ts index dbb97e2..f5501b9 100644 --- a/backend/scripts/init-menus.ts +++ b/backend/scripts/init-menus.ts @@ -44,6 +44,9 @@ if (!fs.existsSync(menusFilePath)) { const menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8')); +// 超级租户可见的菜单名称 +const SUPER_TENANT_MENUS = ['工作台', '赛事活动', '赛事管理', '系统管理']; + async function initMenus() { try { console.log('🚀 开始初始化菜单数据...\n'); @@ -105,8 +108,10 @@ async function initMenus() { } // 清空现有菜单(重新初始化) - console.log('🗑️ 清空现有菜单...'); - // 先删除所有子菜单,再删除父菜单(避免外键约束问题) + console.log('🗑️ 清空现有菜单和租户菜单关联...'); + // 先删除租户菜单关联 + await prisma.tenantMenu.deleteMany({}); + // 再删除所有子菜单,再删除父菜单(避免外键约束问题) await prisma.menu.deleteMany({ where: { parentId: { @@ -163,7 +168,7 @@ async function initMenus() { printMenuTree(menu); }); - // 为所有现有租户分配新菜单 + // 为所有现有租户分配菜单(区分超级租户和普通租户) console.log(`\n📋 为所有租户分配菜单...`); const allTenants = await prisma.tenant.findMany({ where: { validState: 1 }, @@ -174,40 +179,46 @@ async function initMenus() { } else { console.log(` 找到 ${allTenants.length} 个租户\n`); - for (const tenant of allTenants) { - // 获取租户已分配的菜单 - const existingTenantMenus = await prisma.tenantMenu.findMany({ - where: { tenantId: tenant.id }, - select: { menuId: true }, - }); - - const existingMenuIds = new Set( - existingTenantMenus.map((tm) => tm.menuId), - ); - - // 为租户分配所有新菜单 - let addedMenuCount = 0; - for (const menu of allMenus) { - if (!existingMenuIds.has(menu.id)) { - await prisma.tenantMenu.create({ - data: { - tenantId: tenant.id, - menuId: menu.id, - }, - }); - addedMenuCount++; + // 获取超级租户专属菜单ID(工作台、赛事管理、系统管理及其子菜单) + const superTenantMenuIds = new Set(); + for (const menu of allMenus) { + // 顶级菜单 + if (!menu.parentId && SUPER_TENANT_MENUS.includes(menu.name)) { + superTenantMenuIds.add(menu.id); + } + // 子菜单(检查父菜单是否在超级租户菜单中) + if (menu.parentId) { + const parentMenu = allMenus.find(m => m.id === menu.parentId); + if (parentMenu && SUPER_TENANT_MENUS.includes(parentMenu.name)) { + superTenantMenuIds.add(menu.id); } } + } - if (addedMenuCount > 0) { - console.log( - ` ✓ 租户 "${tenant.name}" (${tenant.code}): 添加了 ${addedMenuCount} 个菜单`, - ); - } else { - console.log( - ` ✓ 租户 "${tenant.name}" (${tenant.code}): 已拥有所有菜单`, - ); + for (const tenant of allTenants) { + const isSuperTenant = tenant.isSuper === 1; + + // 确定要分配的菜单 + const menusToAssign = isSuperTenant + ? allMenus.filter(m => superTenantMenuIds.has(m.id)) + : allMenus; + + // 为租户分配菜单 + let addedMenuCount = 0; + for (const menu of menusToAssign) { + await prisma.tenantMenu.create({ + data: { + tenantId: tenant.id, + menuId: menu.id, + }, + }); + addedMenuCount++; } + + const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)'; + console.log( + ` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`, + ); } console.log(`\n✅ 菜单分配完成!`); } diff --git a/backend/scripts/init-roles-permissions.ts b/backend/scripts/init-roles-permissions.ts new file mode 100644 index 0000000..cd97e8b --- /dev/null +++ b/backend/scripts/init-roles-permissions.ts @@ -0,0 +1,561 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +const nodeEnv = process.env.NODE_ENV || 'development'; +const envFile = `.env.${nodeEnv}`; +const backendDir = path.resolve(__dirname, '..'); +const envPath = path.resolve(backendDir, envFile); + +dotenv.config({ path: envPath }); + +if (!process.env.DATABASE_URL) { + dotenv.config({ path: path.resolve(backendDir, '.env') }); +} + +if (!process.env.DATABASE_URL) { + console.error('DATABASE_URL not found'); + process.exit(1); +} + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// ============================================ +// 权限定义 +// ============================================ + +// 基础权限(所有角色共享的权限池) +const allPermissions = [ + // 工作台 + { code: 'workbench:read', resource: 'workbench', action: 'read', name: '查看工作台', description: '允许查看工作台' }, + + // 用户管理 + { code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' }, + { code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户列表和详情' }, + { code: 'user:update', resource: 'user', action: 'update', name: '更新用户', description: '允许更新用户信息' }, + { code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户', description: '允许删除用户' }, + + // 角色管理 + { code: 'role:create', resource: 'role', action: 'create', name: '创建角色', description: '允许创建新角色' }, + { code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色列表和详情' }, + { code: 'role:update', resource: 'role', action: 'update', name: '更新角色', description: '允许更新角色信息' }, + { code: 'role:delete', resource: 'role', action: 'delete', name: '删除角色', description: '允许删除角色' }, + { code: 'role:assign', resource: 'role', action: 'assign', name: '分配角色', description: '允许给用户分配角色' }, + + // 权限管理 + { code: 'permission:read', resource: 'permission', action: 'read', name: '查看权限', description: '允许查看权限列表' }, + + // 菜单管理 + { code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单列表' }, + + // 租户管理(超级租户专属) + { code: 'tenant:create', resource: 'tenant', action: 'create', name: '创建租户', description: '允许创建租户' }, + { code: 'tenant:read', resource: 'tenant', action: 'read', name: '查看租户', description: '允许查看租户列表' }, + { code: 'tenant:update', resource: 'tenant', action: 'update', name: '更新租户', description: '允许更新租户信息' }, + { code: 'tenant:delete', resource: 'tenant', action: 'delete', name: '删除租户', description: '允许删除租户' }, + + // 学校管理 + { code: 'school:create', resource: 'school', action: 'create', name: '创建学校', description: '允许创建学校信息' }, + { code: 'school:read', resource: 'school', action: 'read', name: '查看学校', description: '允许查看学校信息' }, + { code: 'school:update', resource: 'school', action: 'update', name: '更新学校', description: '允许更新学校信息' }, + { code: 'school:delete', resource: 'school', action: 'delete', name: '删除学校', description: '允许删除学校信息' }, + + // 部门管理 + { code: 'department:create', resource: 'department', action: 'create', name: '创建部门', description: '允许创建部门' }, + { code: 'department:read', resource: 'department', action: 'read', name: '查看部门', description: '允许查看部门列表' }, + { code: 'department:update', resource: 'department', action: 'update', name: '更新部门', description: '允许更新部门信息' }, + { code: 'department:delete', resource: 'department', action: 'delete', name: '删除部门', description: '允许删除部门' }, + + // 年级管理 + { code: 'grade:create', resource: 'grade', action: 'create', name: '创建年级', description: '允许创建年级' }, + { code: 'grade:read', resource: 'grade', action: 'read', name: '查看年级', description: '允许查看年级列表' }, + { code: 'grade:update', resource: 'grade', action: 'update', name: '更新年级', description: '允许更新年级信息' }, + { code: 'grade:delete', resource: 'grade', action: 'delete', name: '删除年级', description: '允许删除年级' }, + + // 班级管理 + { code: 'class:create', resource: 'class', action: 'create', name: '创建班级', description: '允许创建班级' }, + { code: 'class:read', resource: 'class', action: 'read', name: '查看班级', description: '允许查看班级列表' }, + { code: 'class:update', resource: 'class', action: 'update', name: '更新班级', description: '允许更新班级信息' }, + { code: 'class:delete', resource: 'class', action: 'delete', name: '删除班级', description: '允许删除班级' }, + + // 教师管理 + { code: 'teacher:create', resource: 'teacher', action: 'create', name: '创建教师', description: '允许创建教师' }, + { code: 'teacher:read', resource: 'teacher', action: 'read', name: '查看教师', description: '允许查看教师列表' }, + { code: 'teacher:update', resource: 'teacher', action: 'update', name: '更新教师', description: '允许更新教师信息' }, + { code: 'teacher:delete', resource: 'teacher', action: 'delete', name: '删除教师', description: '允许删除教师' }, + + // 学生管理 + { code: 'student:create', resource: 'student', action: 'create', name: '创建学生', description: '允许创建学生' }, + { code: 'student:read', resource: 'student', action: 'read', name: '查看学生', description: '允许查看学生列表' }, + { code: 'student:update', resource: 'student', action: 'update', name: '更新学生', description: '允许更新学生信息' }, + { code: 'student:delete', resource: 'student', action: 'delete', name: '删除学生', description: '允许删除学生' }, + + // 赛事管理(超级租户) + { code: 'contest:create', resource: 'contest', action: 'create', name: '创建赛事', description: '允许创建赛事' }, + { code: 'contest:read', resource: 'contest', action: 'read', name: '查看赛事', description: '允许查看赛事列表' }, + { code: 'contest:update', resource: 'contest', action: 'update', name: '更新赛事', description: '允许更新赛事信息' }, + { code: 'contest:delete', resource: 'contest', action: 'delete', name: '删除赛事', description: '允许删除赛事' }, + { code: 'contest:publish', resource: 'contest', action: 'publish', name: '发布赛事', description: '允许发布/取消发布赛事' }, + { code: 'contest:finish', resource: 'contest', action: 'finish', name: '结束赛事', description: '允许结束赛事' }, + + // 评审规则管理 + { code: 'review-rule:create', resource: 'review-rule', action: 'create', name: '创建评审规则', description: '允许创建评审规则' }, + { code: 'review-rule:read', resource: 'review-rule', action: 'read', name: '查看评审规则', description: '允许查看评审规则' }, + { code: 'review-rule:update', resource: 'review-rule', action: 'update', name: '更新评审规则', description: '允许更新评审规则' }, + { code: 'review-rule:delete', resource: 'review-rule', action: 'delete', name: '删除评审规则', description: '允许删除评审规则' }, + + // 评委管理 + { code: 'judge:create', resource: 'judge', action: 'create', name: '添加评委', description: '允许添加评委' }, + { code: 'judge:read', resource: 'judge', action: 'read', name: '查看评委', description: '允许查看评委列表' }, + { code: 'judge:update', resource: 'judge', action: 'update', name: '更新评委', description: '允许更新评委信息' }, + { code: 'judge:delete', resource: 'judge', action: 'delete', name: '删除评委', description: '允许删除评委' }, + { code: 'judge:assign', resource: 'judge', action: 'assign', name: '分配评委', description: '允许为赛事分配评委' }, + + // 赛事报名(学校端) + { code: 'registration:create', resource: 'registration', action: 'create', name: '创建报名', description: '允许报名赛事' }, + { code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' }, + { code: 'registration:update', resource: 'registration', action: 'update', name: '更新报名', description: '允许更新报名信息' }, + { code: 'registration:delete', resource: 'registration', action: 'delete', name: '取消报名', description: '允许取消报名' }, + { code: 'registration:approve', resource: 'registration', action: 'approve', name: '审核报名', description: '允许审核报名' }, + + // 参赛作品 + { code: 'work:create', resource: 'work', action: 'create', name: '上传作品', description: '允许上传参赛作品' }, + { code: 'work:read', resource: 'work', action: 'read', name: '查看作品', description: '允许查看参赛作品' }, + { code: 'work:update', resource: 'work', action: 'update', name: '更新作品', description: '允许更新作品信息' }, + { code: 'work:delete', resource: 'work', action: 'delete', name: '删除作品', description: '允许删除作品' }, + { code: 'work:submit', resource: 'work', action: 'submit', name: '提交作品', description: '允许提交作品' }, + + // 作品评审(评委端) + { code: 'review:read', resource: 'review', action: 'read', name: '查看评审任务', description: '允许查看待评审作品' }, + { code: 'review:score', resource: 'review', action: 'score', name: '评审打分', description: '允许对作品打分' }, + + // 赛事公告 + { code: 'notice:create', resource: 'notice', action: 'create', name: '创建公告', description: '允许创建赛事公告' }, + { code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看赛事公告' }, + { code: 'notice:update', resource: 'notice', action: 'update', name: '更新公告', description: '允许更新公告信息' }, + { code: 'notice:delete', resource: 'notice', action: 'delete', name: '删除公告', description: '允许删除公告' }, + + // 作业管理 + { code: 'homework:create', resource: 'homework', action: 'create', name: '创建作业', description: '允许创建作业' }, + { code: 'homework:read', resource: 'homework', action: 'read', name: '查看作业', description: '允许查看作业列表' }, + { code: 'homework:update', resource: 'homework', action: 'update', name: '更新作业', description: '允许更新作业信息' }, + { code: 'homework:delete', resource: 'homework', action: 'delete', name: '删除作业', description: '允许删除作业' }, + { code: 'homework:publish', resource: 'homework', action: 'publish', name: '发布作业', description: '允许发布作业' }, + + // 作业提交 + { code: 'homework-submission:create', resource: 'homework-submission', action: 'create', name: '提交作业', description: '允许提交作业' }, + { code: 'homework-submission:read', resource: 'homework-submission', action: 'read', name: '查看作业提交', description: '允许查看作业提交记录' }, + { code: 'homework-submission:update', resource: 'homework-submission', action: 'update', name: '更新作业提交', description: '允许更新提交的作业' }, + + // 作业评审规则 + { code: 'homework-review-rule:create', resource: 'homework-review-rule', action: 'create', name: '创建作业评审规则', description: '允许创建作业评审规则' }, + { code: 'homework-review-rule:read', resource: 'homework-review-rule', action: 'read', name: '查看作业评审规则', description: '允许查看作业评审规则' }, + { code: 'homework-review-rule:update', resource: 'homework-review-rule', action: 'update', name: '更新作业评审规则', description: '允许更新作业评审规则' }, + { code: 'homework-review-rule:delete', resource: 'homework-review-rule', action: 'delete', name: '删除作业评审规则', description: '允许删除作业评审规则' }, + + // 作业评分 + { code: 'homework-score:create', resource: 'homework-score', action: 'create', name: '作业评分', description: '允许对作业评分' }, + { code: 'homework-score:read', resource: 'homework-score', action: 'read', name: '查看作业评分', description: '允许查看作业评分' }, + + // 字典管理 + { code: 'dict:create', resource: 'dict', action: 'create', name: '创建字典', description: '允许创建新字典' }, + { code: 'dict:read', resource: 'dict', action: 'read', name: '查看字典', description: '允许查看字典列表和详情' }, + { code: 'dict:update', resource: 'dict', action: 'update', name: '更新字典', description: '允许更新字典信息' }, + { code: 'dict:delete', resource: 'dict', action: 'delete', name: '删除字典', description: '允许删除字典' }, + + // 系统配置 + { code: 'config:create', resource: 'config', action: 'create', name: '创建配置', description: '允许创建新配置' }, + { code: 'config:read', resource: 'config', action: 'read', name: '查看配置', description: '允许查看配置列表和详情' }, + { code: 'config:update', resource: 'config', action: 'update', name: '更新配置', description: '允许更新配置信息' }, + { code: 'config:delete', resource: 'config', action: 'delete', name: '删除配置', description: '允许删除配置' }, + + // 日志管理 + { code: 'log:read', resource: 'log', action: 'read', name: '查看日志', description: '允许查看系统日志' }, + { code: 'log:delete', resource: 'log', action: 'delete', name: '删除日志', description: '允许删除系统日志' }, + + // 赛事活动(学校端) + { code: 'activity:read', resource: 'activity', action: 'read', name: '查看赛事活动', description: '允许查看已发布的赛事活动' }, + { code: 'activity:guidance', resource: 'activity', action: 'guidance', name: '指导学生', description: '允许指导学生参赛' }, +]; + +// ============================================ +// 角色定义和权限映射 +// ============================================ + +// 超级租户角色 +const superTenantRoles = [ + { + code: 'super_admin', + name: '超级管理员', + description: '系统超级管理员,管理赛事和系统配置', + permissions: [ + // 工作台 + 'workbench:read', + // 系统管理 + 'user:create', 'user:read', 'user:update', 'user:delete', + 'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign', + 'permission:read', + 'menu:read', + 'tenant:create', 'tenant:read', 'tenant:update', 'tenant:delete', + 'dict:create', 'dict:read', 'dict:update', 'dict:delete', + 'config:create', 'config:read', 'config:update', 'config:delete', + 'log:read', 'log:delete', + // 赛事管理 + 'contest:create', 'contest:read', 'contest:update', 'contest:delete', 'contest:publish', 'contest:finish', + 'review-rule:create', 'review-rule:read', 'review-rule:update', 'review-rule:delete', + 'judge:create', 'judge:read', 'judge:update', 'judge:delete', 'judge:assign', + 'registration:read', 'registration:approve', + 'work:read', + 'notice:create', 'notice:read', 'notice:update', 'notice:delete', + ], + }, + { + code: 'judge', + name: '评委', + description: '赛事评委,可以评审作品', + permissions: [ + 'workbench:read', + 'activity:read', // 查看赛事活动 + 'work:read', // 查看待评审作品 + 'review:read', // 查看评审任务 + 'review:score', // 评审打分 + 'notice:read', // 查看公告 + ], + }, +]; + +// 普通租户(学校)角色 +const normalTenantRoles = [ + { + code: 'school_admin', + name: '学校管理员', + description: '学校管理员,管理学校信息、教师、学生等', + permissions: [ + 'workbench:read', + 'user:create', 'user:read', 'user:update', 'user:delete', + 'role:read', + 'permission:read', + 'menu:read', + // 学校管理 + 'school:create', 'school:read', 'school:update', 'school:delete', + 'department:create', 'department:read', 'department:update', 'department:delete', + 'grade:create', 'grade:read', 'grade:update', 'grade:delete', + 'class:create', 'class:read', 'class:update', 'class:delete', + 'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete', + 'student:create', 'student:read', 'student:update', 'student:delete', + // 赛事活动 + 'activity:read', + 'notice:read', + // 可以查看报名和作品 + 'registration:read', + 'work:read', + ], + }, + { + code: 'teacher', + name: '教师', + description: '教师角色,可以报名赛事、指导学生、管理作业', + permissions: [ + 'workbench:read', + // 查看基础信息 + 'grade:read', + 'class:read', + 'student:read', + // 赛事活动 + 'activity:read', // 查看赛事活动列表 + 'activity:guidance', // 指导学生参赛 + 'notice:read', // 查看赛事公告 + 'registration:create', 'registration:read', 'registration:update', 'registration:delete', // 报名管理 + 'work:create', 'work:read', 'work:update', 'work:submit', // 指导学生上传作品 + // 作业管理 + 'homework:create', 'homework:read', 'homework:update', 'homework:delete', 'homework:publish', + 'homework-submission:read', + 'homework-review-rule:create', 'homework-review-rule:read', 'homework-review-rule:update', 'homework-review-rule:delete', + 'homework-score:create', 'homework-score:read', + ], + }, + { + code: 'student', + name: '学生', + description: '学生角色,可以查看赛事、上传作品、提交作业', + permissions: [ + 'workbench:read', + // 赛事活动 + 'activity:read', // 查看赛事活动列表 + 'notice:read', // 查看赛事公告 + 'registration:read', // 查看自己的报名记录 + 'work:create', 'work:read', 'work:update', 'work:submit', // 上传/管理自己的作品 + // 作业 + 'homework:read', // 查看作业 + 'homework-submission:create', 'homework-submission:read', 'homework-submission:update', // 提交作业 + 'homework-score:read', // 查看自己的作业评分 + ], + }, +]; + +// ============================================ +// 初始化函数 +// ============================================ + +/** + * 为租户创建权限 + */ +async function createPermissions(tenantId: number, permissionCodes: string[]) { + const createdPermissions: { [code: string]: number } = {}; + + for (const code of permissionCodes) { + const permDef = allPermissions.find(p => p.code === code); + if (!permDef) { + console.log(` ⚠️ 权限定义不存在: ${code}`); + continue; + } + + // 检查是否已存在 + let permission = await prisma.permission.findFirst({ + where: { tenantId, code }, + }); + + if (!permission) { + permission = await prisma.permission.create({ + data: { + tenantId, + code: permDef.code, + resource: permDef.resource, + action: permDef.action, + name: permDef.name, + description: permDef.description, + validState: 1, + }, + }); + console.log(` ✓ 创建权限: ${code}`); + } + + createdPermissions[code] = permission.id; + } + + return createdPermissions; +} + +/** + * 为租户创建角色并分配权限 + */ +async function createRoleWithPermissions( + tenantId: number, + roleConfig: { code: string; name: string; description: string; permissions: string[] }, + permissionMap: { [code: string]: number } +) { + // 创建或获取角色 + let role = await prisma.role.findFirst({ + where: { tenantId, code: roleConfig.code }, + }); + + if (!role) { + role = await prisma.role.create({ + data: { + tenantId, + code: roleConfig.code, + name: roleConfig.name, + description: roleConfig.description, + validState: 1, + }, + }); + console.log(` ✓ 创建角色: ${roleConfig.name} (${roleConfig.code})`); + } else { + // 更新角色信息 + role = await prisma.role.update({ + where: { id: role.id }, + data: { + name: roleConfig.name, + description: roleConfig.description, + }, + }); + console.log(` ✓ 更新角色: ${roleConfig.name} (${roleConfig.code})`); + } + + // 分配权限 + const existingRolePermissions = await prisma.rolePermission.findMany({ + where: { roleId: role.id }, + select: { permissionId: true }, + }); + const existingPermissionIds = new Set(existingRolePermissions.map(rp => rp.permissionId)); + + let addedCount = 0; + for (const permCode of roleConfig.permissions) { + const permissionId = permissionMap[permCode]; + if (!permissionId) { + console.log(` ⚠️ 权限不存在: ${permCode}`); + continue; + } + + if (!existingPermissionIds.has(permissionId)) { + await prisma.rolePermission.create({ + data: { + roleId: role.id, + permissionId, + }, + }); + addedCount++; + } + } + + if (addedCount > 0) { + console.log(` 添加了 ${addedCount} 个权限`); + } + + return role; +} + +/** + * 初始化超级租户的角色和权限 + */ +async function initSuperTenantRoles() { + console.log('\n🚀 开始初始化超级租户角色和权限...\n'); + + // 查找超级租户 + const superTenant = await prisma.tenant.findFirst({ + where: { isSuper: 1, validState: 1 }, + }); + + if (!superTenant) { + console.error('❌ 超级租户不存在!请先运行 init:super-tenant'); + return; + } + + console.log(`找到超级租户: ${superTenant.name} (${superTenant.code})\n`); + + // 收集所有需要的权限码 + const allPermissionCodes = new Set(); + superTenantRoles.forEach(role => { + role.permissions.forEach(code => allPermissionCodes.add(code)); + }); + + // 创建权限 + console.log('📝 创建权限...'); + const permissionMap = await createPermissions(superTenant.id, Array.from(allPermissionCodes)); + console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`); + + // 创建角色 + console.log('👥 创建角色...'); + for (const roleConfig of superTenantRoles) { + await createRoleWithPermissions(superTenant.id, roleConfig, permissionMap); + } + + console.log('\n✅ 超级租户角色和权限初始化完成!'); +} + +/** + * 初始化普通租户的角色和权限 + */ +async function initNormalTenantRoles(tenantCode: string) { + console.log(`\n🚀 开始初始化租户 "${tenantCode}" 的角色和权限...\n`); + + // 查找租户 + const tenant = await prisma.tenant.findFirst({ + where: { code: tenantCode, validState: 1 }, + }); + + if (!tenant) { + console.error(`❌ 租户 "${tenantCode}" 不存在!`); + return; + } + + if (tenant.isSuper === 1) { + console.log('⚠️ 这是超级租户,请使用 --super 选项'); + return; + } + + console.log(`找到租户: ${tenant.name} (${tenant.code})\n`); + + // 收集所有需要的权限码 + const allPermissionCodes = new Set(); + normalTenantRoles.forEach(role => { + role.permissions.forEach(code => allPermissionCodes.add(code)); + }); + + // 创建权限 + console.log('📝 创建权限...'); + const permissionMap = await createPermissions(tenant.id, Array.from(allPermissionCodes)); + console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`); + + // 创建角色 + console.log('👥 创建角色...'); + for (const roleConfig of normalTenantRoles) { + await createRoleWithPermissions(tenant.id, roleConfig, permissionMap); + } + + // 输出角色信息 + console.log('\n📊 角色权限概览:'); + for (const roleConfig of normalTenantRoles) { + console.log(` ${roleConfig.name} (${roleConfig.code}): ${roleConfig.permissions.length} 个权限`); + } + + console.log(`\n✅ 租户 "${tenantCode}" 角色和权限初始化完成!`); +} + +/** + * 初始化所有普通租户的角色和权限 + */ +async function initAllNormalTenantRoles() { + console.log('\n🚀 开始初始化所有普通租户的角色和权限...\n'); + + // 查找所有普通租户 + const normalTenants = await prisma.tenant.findMany({ + where: { isSuper: { not: 1 }, validState: 1 }, + }); + + if (normalTenants.length === 0) { + console.log('⚠️ 没有找到普通租户'); + return; + } + + console.log(`找到 ${normalTenants.length} 个普通租户\n`); + + for (const tenant of normalTenants) { + await initNormalTenantRoles(tenant.code); + console.log(''); + } + + console.log('\n✅ 所有普通租户角色和权限初始化完成!'); +} + +// ============================================ +// 主函数 +// ============================================ + +async function main() { + const args = process.argv.slice(2); + const isSuper = args.includes('--super'); + const isAll = args.includes('--all'); + const tenantCode = args.find(arg => !arg.startsWith('--')); + + try { + if (isSuper) { + await initSuperTenantRoles(); + } else if (isAll) { + await initAllNormalTenantRoles(); + } else if (tenantCode) { + await initNormalTenantRoles(tenantCode); + } else { + console.log('使用方法:'); + console.log(' 初始化超级租户角色: ts-node scripts/init-roles-permissions.ts --super'); + console.log(' 初始化指定租户角色: ts-node scripts/init-roles-permissions.ts <租户编码>'); + console.log(' 初始化所有普通租户: ts-node scripts/init-roles-permissions.ts --all'); + process.exit(1); + } + } finally { + await prisma.$disconnect(); + } +} + +main() + .then(() => { + console.log('\n🎉 脚本执行完成!'); + process.exit(0); + }) + .catch((error) => { + console.error('\n💥 脚本执行失败:', error); + process.exit(1); + }); diff --git a/backend/scripts/init-super-tenant.ts b/backend/scripts/init-super-tenant.ts index 2b281ad..5165adf 100644 --- a/backend/scripts/init-super-tenant.ts +++ b/backend/scripts/init-super-tenant.ts @@ -230,7 +230,7 @@ async function main() { icon: 'TeamOutlined', component: 'system/tenants/Index', parentId: systemMenu.id, - permission: 'tenant:read', + permission: 'tenant:update', // 只有超级租户才有此权限,普通租户只有 tenant:read sort: 7, validState: 1, }, diff --git a/backend/scripts/init-tenant-admin.ts b/backend/scripts/init-tenant-admin.ts index 08caae6..8f0b4c0 100644 --- a/backend/scripts/init-tenant-admin.ts +++ b/backend/scripts/init-tenant-admin.ts @@ -254,6 +254,563 @@ const permissions = [ name: '修改用户密码', description: '允许修改用户密码', }, + + // 学校管理权限 + { + code: 'school:create', + resource: 'school', + action: 'create', + name: '创建学校', + description: '允许创建学校信息', + }, + { + code: 'school:read', + resource: 'school', + action: 'read', + name: '查看学校', + description: '允许查看学校信息', + }, + { + code: 'school:update', + resource: 'school', + action: 'update', + name: '更新学校', + description: '允许更新学校信息', + }, + { + code: 'school:delete', + resource: 'school', + action: 'delete', + name: '删除学校', + description: '允许删除学校信息', + }, + { + code: 'department:create', + resource: 'department', + action: 'create', + name: '创建部门', + description: '允许创建部门', + }, + { + code: 'department:read', + resource: 'department', + action: 'read', + name: '查看部门', + description: '允许查看部门列表和详情', + }, + { + code: 'department:update', + resource: 'department', + action: 'update', + name: '更新部门', + description: '允许更新部门信息', + }, + { + code: 'department:delete', + resource: 'department', + action: 'delete', + name: '删除部门', + description: '允许删除部门', + }, + { + code: 'grade:create', + resource: 'grade', + action: 'create', + name: '创建年级', + description: '允许创建年级', + }, + { + code: 'grade:read', + resource: 'grade', + action: 'read', + name: '查看年级', + description: '允许查看年级列表和详情', + }, + { + code: 'grade:update', + resource: 'grade', + action: 'update', + name: '更新年级', + description: '允许更新年级信息', + }, + { + code: 'grade:delete', + resource: 'grade', + action: 'delete', + name: '删除年级', + description: '允许删除年级', + }, + { + code: 'class:create', + resource: 'class', + action: 'create', + name: '创建班级', + description: '允许创建班级', + }, + { + code: 'class:read', + resource: 'class', + action: 'read', + name: '查看班级', + description: '允许查看班级列表和详情', + }, + { + code: 'class:update', + resource: 'class', + action: 'update', + name: '更新班级', + description: '允许更新班级信息', + }, + { + code: 'class:delete', + resource: 'class', + action: 'delete', + name: '删除班级', + description: '允许删除班级', + }, + { + code: 'teacher:create', + resource: 'teacher', + action: 'create', + name: '创建教师', + description: '允许创建教师', + }, + { + code: 'teacher:read', + resource: 'teacher', + action: 'read', + name: '查看教师', + description: '允许查看教师列表和详情', + }, + { + code: 'teacher:update', + resource: 'teacher', + action: 'update', + name: '更新教师', + description: '允许更新教师信息', + }, + { + code: 'teacher:delete', + resource: 'teacher', + action: 'delete', + name: '删除教师', + description: '允许删除教师', + }, + { + code: 'student:create', + resource: 'student', + action: 'create', + name: '创建学生', + description: '允许创建学生', + }, + { + code: 'student:read', + resource: 'student', + action: 'read', + name: '查看学生', + description: '允许查看学生列表和详情', + }, + { + code: 'student:update', + resource: 'student', + action: 'update', + name: '更新学生', + description: '允许更新学生信息', + }, + { + code: 'student:delete', + resource: 'student', + action: 'delete', + name: '删除学生', + description: '允许删除学生', + }, + + // 赛事管理权限 + { + code: 'contest:create', + resource: 'contest', + action: 'create', + name: '创建比赛', + description: '允许创建比赛', + }, + { + code: 'contest:read', + resource: 'contest', + action: 'read', + name: '查看比赛', + description: '允许查看比赛列表和详情', + }, + { + code: 'contest:update', + resource: 'contest', + action: 'update', + name: '更新比赛', + description: '允许更新比赛信息', + }, + { + code: 'contest:delete', + resource: 'contest', + action: 'delete', + name: '删除比赛', + description: '允许删除比赛', + }, + { + code: 'contest:publish', + resource: 'contest', + action: 'publish', + name: '发布比赛', + description: '允许发布比赛', + }, + { + code: 'contest:team:create', + resource: 'contest:team', + action: 'create', + name: '创建团队', + description: '允许创建比赛团队', + }, + { + code: 'contest:team:read', + resource: 'contest:team', + action: 'read', + name: '查看团队', + description: '允许查看团队列表和详情', + }, + { + code: 'contest:team:update', + resource: 'contest:team', + action: 'update', + name: '更新团队', + description: '允许更新团队信息', + }, + { + code: 'contest:team:delete', + resource: 'contest:team', + action: 'delete', + name: '删除团队', + description: '允许删除团队', + }, + { + code: 'contest:team:manage', + resource: 'contest:team', + action: 'manage', + name: '管理团队成员', + description: '允许管理团队成员', + }, + { + code: 'contest:review:create', + resource: 'contest:review', + action: 'create', + name: '创建评审规则', + description: '允许创建评审规则', + }, + { + code: 'contest:review:read', + resource: 'contest:review', + action: 'read', + name: '查看评审', + description: '允许查看评审规则和评审记录', + }, + { + code: 'contest:review:update', + resource: 'contest:review', + action: 'update', + name: '更新评审规则', + description: '允许更新评审规则', + }, + { + code: 'contest:review:delete', + resource: 'contest:review', + action: 'delete', + name: '删除评审规则', + description: '允许删除评审规则', + }, + { + code: 'contest:review:assign', + resource: 'contest:review', + action: 'assign', + name: '分配评审任务', + description: '允许分配评审任务给评委', + }, + { + code: 'contest:review:score', + resource: 'contest:review', + action: 'score', + name: '评审打分', + description: '允许对作品进行评审打分', + }, + { + code: 'contest:judge:create', + resource: 'contest:judge', + action: 'create', + name: '添加评委', + description: '允许添加比赛评委', + }, + { + code: 'contest:judge:read', + resource: 'contest:judge', + action: 'read', + name: '查看评委', + description: '允许查看评委列表', + }, + { + code: 'contest:judge:update', + resource: 'contest:judge', + action: 'update', + name: '更新评委', + description: '允许更新评委信息', + }, + { + code: 'contest:judge:delete', + resource: 'contest:judge', + action: 'delete', + name: '删除评委', + description: '允许删除评委', + }, + { + code: 'contest:work:create', + resource: 'contest:work', + action: 'create', + name: '创建作品', + description: '允许创建参赛作品', + }, + { + code: 'contest:work:read', + resource: 'contest:work', + action: 'read', + name: '查看作品', + description: '允许查看作品列表和详情', + }, + { + code: 'contest:work:update', + resource: 'contest:work', + action: 'update', + name: '更新作品', + description: '允许更新作品信息', + }, + { + code: 'contest:work:delete', + resource: 'contest:work', + action: 'delete', + name: '删除作品', + description: '允许删除作品', + }, + { + code: 'contest:work:submit', + resource: 'contest:work', + action: 'submit', + name: '提交作品', + description: '允许提交作品', + }, + { + code: 'contest:work:review', + resource: 'contest:work', + action: 'review', + name: '审核作品', + description: '允许审核作品状态', + }, + { + code: 'contest:registration:create', + resource: 'contest:registration', + action: 'create', + name: '创建报名', + description: '允许创建报名记录', + }, + { + code: 'contest:registration:read', + resource: 'contest:registration', + action: 'read', + name: '查看报名', + description: '允许查看报名列表和详情', + }, + { + code: 'contest:registration:update', + resource: 'contest:registration', + action: 'update', + name: '更新报名', + description: '允许更新报名信息', + }, + { + code: 'contest:registration:delete', + resource: 'contest:registration', + action: 'delete', + name: '删除报名', + description: '允许删除报名记录', + }, + { + code: 'contest:registration:approve', + resource: 'contest:registration', + action: 'approve', + name: '审核报名', + description: '允许审核报名(通过/拒绝)', + }, + { + code: 'contest:notice:create', + resource: 'contest:notice', + action: 'create', + name: '创建公告', + description: '允许创建比赛公告', + }, + { + code: 'contest:notice:read', + resource: 'contest:notice', + action: 'read', + name: '查看公告', + description: '允许查看公告列表和详情', + }, + { + code: 'contest:notice:update', + resource: 'contest:notice', + action: 'update', + name: '更新公告', + description: '允许更新公告信息', + }, + { + code: 'contest:notice:delete', + resource: 'contest:notice', + action: 'delete', + name: '删除公告', + description: '允许删除公告', + }, + { + code: 'contest:notice:publish', + resource: 'contest:notice', + action: 'publish', + name: '发布公告', + description: '允许发布公告', + }, + + // 作业管理权限 + { + code: 'homework:create', + resource: 'homework', + action: 'create', + name: '创建作业', + description: '允许创建作业', + }, + { + code: 'homework:read', + resource: 'homework', + action: 'read', + name: '查看作业', + description: '允许查看作业列表和详情', + }, + { + code: 'homework:update', + resource: 'homework', + action: 'update', + name: '更新作业', + description: '允许更新作业信息', + }, + { + code: 'homework:delete', + resource: 'homework', + action: 'delete', + name: '删除作业', + description: '允许删除作业', + }, + + // 租户查看权限(所有租户都可以查看租户列表,用于发布赛事选择公开范围等) + { + code: 'tenant:read', + resource: 'tenant', + action: 'read', + name: '查看租户', + description: '允许查看租户列表和详情', + }, + + // 评委管理权限(简化前缀) + { + code: 'judge:create', + resource: 'judge', + action: 'create', + name: '添加评委', + description: '允许添加评委', + }, + { + code: 'judge:read', + resource: 'judge', + action: 'read', + name: '查看评委', + description: '允许查看评委列表和详情', + }, + { + code: 'judge:update', + resource: 'judge', + action: 'update', + name: '更新评委', + description: '允许更新评委信息', + }, + { + code: 'judge:delete', + resource: 'judge', + action: 'delete', + name: '删除评委', + description: '允许删除评委', + }, + + // 公告管理权限(简化前缀) + { + code: 'notice:create', + resource: 'notice', + action: 'create', + name: '创建公告', + description: '允许创建公告', + }, + { + code: 'notice:read', + resource: 'notice', + action: 'read', + name: '查看公告', + description: '允许查看公告列表和详情', + }, + { + code: 'notice:update', + resource: 'notice', + action: 'update', + name: '更新公告', + description: '允许更新公告信息', + }, + { + code: 'notice:delete', + resource: 'notice', + action: 'delete', + name: '删除公告', + description: '允许删除公告', + }, + + // 作品管理权限(简化前缀) + { + code: 'work:read', + resource: 'work', + action: 'read', + name: '查看作品', + description: '允许查看参赛作品', + }, +]; + +// 超级管理员专属权限(只有超级租户才能拥有) +const superAdminPermissions = [ + { + code: 'tenant:create', + resource: 'tenant', + action: 'create', + name: '创建租户', + description: '允许创建租户(仅超级管理员)', + }, + { + code: 'tenant:update', + resource: 'tenant', + action: 'update', + name: '更新租户', + description: '允许更新租户信息(仅超级管理员)', + }, + { + code: 'tenant:delete', + resource: 'tenant', + action: 'delete', + name: '删除租户', + description: '允许删除租户(仅超级管理员)', + }, ]; /** @@ -280,7 +837,8 @@ async function initTenantAdminPermissionsOnly(tenantCode: string) { process.exit(1); } - console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`); + const isSuperTenant = tenant.isSuper === 1; + console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})${isSuperTenant ? ' [超级租户]' : ''}\n`); // 2. 检查 admin 角色是否存在 console.log(`👤 步骤 2: 检查 admin 角色是否存在...`); @@ -301,10 +859,15 @@ async function initTenantAdminPermissionsOnly(tenantCode: string) { console.log(`✅ 找到 admin 角色: ${adminRole.name} (${adminRole.code})\n`); // 3. 初始化租户权限(如果不存在则创建) - console.log(`📝 步骤 3: 初始化租户权限...`); + // 超级租户拥有所有权限,普通租户只拥有基础权限 + const allPermissions = isSuperTenant + ? [...permissions, ...superAdminPermissions] + : permissions; + + console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`); const createdPermissions = []; - for (const perm of permissions) { + for (const perm of allPermissions) { // 检查权限是否已存在 const existingPermission = await prisma.permission.findFirst({ where: { @@ -448,7 +1011,8 @@ async function initTenantAdmin(tenantCode: string) { process.exit(1); } - console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`); + const isSuperTenant = tenant.isSuper === 1; + console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})${isSuperTenant ? ' [超级租户]' : ''}\n`); // 2. 检查是否已存在 admin 用户 console.log(`👤 步骤 2: 检查 admin 用户是否已存在...`); @@ -468,10 +1032,15 @@ async function initTenantAdmin(tenantCode: string) { } // 3. 初始化租户权限(如果不存在则创建) - console.log(`📝 步骤 3: 初始化租户权限...`); + // 超级租户拥有所有权限,普通租户只拥有基础权限 + const allPermissions = isSuperTenant + ? [...permissions, ...superAdminPermissions] + : permissions; + + console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`); const createdPermissions = []; - for (const perm of permissions) { + for (const perm of allPermissions) { // 检查权限是否已存在 const existingPermission = await prisma.permission.findFirst({ where: { diff --git a/backend/scripts/move-student-homework-menu.ts b/backend/scripts/move-student-homework-menu.ts deleted file mode 100644 index ababff5..0000000 --- a/backend/scripts/move-student-homework-menu.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 把"我的作业"移到"作业管理"下...\n'); - - // 1. 查找作业管理父菜单 - const homeworkParent = await prisma.menu.findFirst({ - where: { name: '作业管理', parentId: null } - }); - - if (!homeworkParent) { - console.log('❌ 未找到作业管理菜单'); - return; - } - console.log('✅ 找到作业管理菜单 ID:', homeworkParent.id); - - // 2. 查找我的作业菜单 - const studentHomework = await prisma.menu.findFirst({ - where: { path: '/student-homework' } - }); - - if (!studentHomework) { - console.log('❌ 未找到我的作业菜单'); - return; - } - console.log('✅ 找到我的作业菜单 ID:', studentHomework.id); - - // 3. 更新我的作业菜单,设置为作业管理的子菜单 - await prisma.menu.update({ - where: { id: studentHomework.id }, - data: { - parentId: homeworkParent.id, - path: '/homework/student', - sort: 0, // 排在最前面 - } - }); - console.log('✅ 已更新我的作业为作业管理的子菜单'); - - // 4. 显示最终作业管理菜单结构 - console.log('\n📋 作业管理最终菜单结构:'); - const children = await prisma.menu.findMany({ - where: { parentId: homeworkParent.id }, - orderBy: { sort: 'asc' } - }); - - console.log(`- ${homeworkParent.name} (${homeworkParent.path})`); - children.forEach(c => { - console.log(` - ${c.name} (${c.path}) | component: ${c.component || '(null)'} | permission: ${c.permission}`); - }); - - console.log('\n✅ 完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/query-contest-submit-time.ts b/backend/scripts/query-contest-submit-time.ts deleted file mode 100644 index 950c11b..0000000 --- a/backend/scripts/query-contest-submit-time.ts +++ /dev/null @@ -1,102 +0,0 @@ -// 加载环境变量(必须在其他导入之前) -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -// 根据 NODE_ENV 加载对应的环境配置文件 -const nodeEnv = process.env.NODE_ENV || 'development'; -const envFile = `.env.${nodeEnv}`; -// scripts 目录的父目录就是 backend 目录 -const backendDir = path.resolve(__dirname, '..'); -const envPath = path.resolve(backendDir, envFile); - -// 尝试加载环境特定的配置文件 -dotenv.config({ path: envPath }); - -// 如果环境特定文件不存在,尝试加载默认的 .env 文件 -if (!process.env.DATABASE_URL) { - dotenv.config({ path: path.resolve(backendDir, '.env') }); -} - -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function queryContestSubmitTime() { - try { - const contestName = '3D打印作品大赛2'; - - const contest = await prisma.contest.findFirst({ - where: { - contestName: { - contains: contestName, - }, - validState: 1, - }, - select: { - id: true, - contestName: true, - submitStartTime: true, - submitEndTime: true, - submitRule: true, - startTime: true, - endTime: true, - }, - }); - - if (!contest) { - console.log(`未找到名为"${contestName}"的比赛`); - - // 尝试查找包含"3D打印"的比赛 - const contests = await prisma.contest.findMany({ - where: { - contestName: { - contains: '3D打印', - }, - validState: 1, - }, - select: { - id: true, - contestName: true, - submitStartTime: true, - submitEndTime: true, - }, - orderBy: { - createTime: 'desc', - }, - }); - - if (contests.length > 0) { - console.log('\n找到以下包含"3D打印"的比赛:'); - contests.forEach((c) => { - console.log(`\n比赛名称: ${c.contestName}`); - console.log(`作品提交开始时间: ${c.submitStartTime}`); - console.log(`作品提交结束时间: ${c.submitEndTime}`); - }); - } else { - console.log('未找到任何包含"3D打印"的比赛'); - } - return; - } - - console.log('='.repeat(60)); - console.log('比赛信息'); - console.log('='.repeat(60)); - console.log(`比赛ID: ${contest.id}`); - console.log(`比赛名称: ${contest.contestName}`); - console.log(`\n作品提交时间:`); - console.log(` 开始时间: ${contest.submitStartTime}`); - console.log(` 结束时间: ${contest.submitEndTime}`); - console.log(` 提交规则: ${contest.submitRule === 'once' ? '单次提交' : '可多次提交'}`); - console.log(`\n比赛时间:`); - console.log(` 开始时间: ${contest.startTime}`); - console.log(` 结束时间: ${contest.endTime}`); - console.log('='.repeat(60)); - } catch (error) { - console.error('查询失败:', error); - } finally { - await prisma.$disconnect(); - } -} - -queryContestSubmitTime(); - diff --git a/backend/scripts/rename-activity-list.ts b/backend/scripts/rename-activity-list.ts deleted file mode 100644 index bd61bd4..0000000 --- a/backend/scripts/rename-activity-list.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const result = await prisma.menu.updateMany({ - where: { path: '/student-activities/list' }, - data: { name: '活动列表' } - }); - console.log('✅ 已更新', result.count, '条记录'); - - // 验证 - const menu = await prisma.menu.findFirst({ - where: { path: '/student-activities/list' } - }); - if (menu) { - console.log('更新后:', menu.name, '|', menu.path); - } -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/show-contest-menus.ts b/backend/scripts/show-contest-menus.ts deleted file mode 100644 index 642ce8b..0000000 --- a/backend/scripts/show-contest-menus.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - const contestMenu = await prisma.menu.findFirst({ - where: { name: '赛事管理', parentId: null } - }); - - if (!contestMenu) { - console.log('未找到赛事管理菜单'); - return; - } - - const children = await prisma.menu.findMany({ - where: { parentId: contestMenu.id }, - orderBy: { sort: 'asc' } - }); - - console.log('📋 赛事管理菜单结构:'); - console.log(`- ${contestMenu.name} (ID: ${contestMenu.id}, path: ${contestMenu.path})`); - children.forEach(c => { - console.log(` - ${c.name}`); - console.log(` path: ${c.path}`); - console.log(` component: ${c.component}`); - console.log(` permission: ${c.permission}`); - }); - - // 检查是否有评委管理和评审进度 - console.log('\n📋 检查缺失的菜单:'); - const judgeMenu = children.find(c => c.name === '评委管理'); - const reviewProgressMenu = children.find(c => c.name === '评审进度'); - - console.log('评委管理:', judgeMenu ? '存在' : '❌ 不存在'); - console.log('评审进度:', reviewProgressMenu ? '存在' : '❌ 不存在'); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/show-homework-menus.ts b/backend/scripts/show-homework-menus.ts deleted file mode 100644 index dbcdd18..0000000 --- a/backend/scripts/show-homework-menus.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - // 作业管理菜单 - const homeworkParent = await prisma.menu.findFirst({ - where: { name: '作业管理', parentId: null } - }); - - if (!homeworkParent) { - console.log('未找到作业管理菜单'); - return; - } - - const children = await prisma.menu.findMany({ - where: { parentId: homeworkParent.id }, - orderBy: { sort: 'asc' } - }); - - console.log('📋 作业管理菜单结构:'); - console.log(`- ${homeworkParent.name} (${homeworkParent.path})`); - children.forEach(c => { - console.log(` - ${c.name}`); - console.log(` path: ${c.path}`); - console.log(` component: ${c.component}`); - console.log(` permission: ${c.permission}`); - }); -} - -main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/backend/scripts/update-activity-menus.ts b/backend/scripts/update-activity-menus.ts deleted file mode 100644 index 3f30b08..0000000 --- a/backend/scripts/update-activity-menus.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 更新赛事活动菜单结构...\n'); - - // 1. 删除赛事管理下的赛事活动菜单 - console.log('📝 步骤1: 移除赛事管理下的赛事活动菜单'); - const oldActivityMenu = await prisma.menu.findFirst({ - where: { path: '/contests/activities' } - }); - - if (oldActivityMenu) { - // 先删除租户菜单关联 - await prisma.tenantMenu.deleteMany({ - where: { menuId: oldActivityMenu.id } - }); - // 再删除菜单 - await prisma.menu.delete({ - where: { id: oldActivityMenu.id } - }); - console.log(' ✅ 已删除旧的赛事活动菜单'); - } else { - console.log(' ⏭️ 旧菜单不存在,跳过'); - } - - // 2. 更新独立的赛事活动菜单(改为父菜单,不需要 component) - console.log('\n📝 步骤2: 更新赛事活动为父级菜单'); - let activityParent = await prisma.menu.findFirst({ - where: { path: '/student-activities' } - }); - - if (activityParent) { - await prisma.menu.update({ - where: { id: activityParent.id }, - data: { - component: null, // 父菜单不需要组件 - } - }); - console.log(' ✅ 已更新为父级菜单'); - } else { - // 如果不存在,创建它 - activityParent = await prisma.menu.create({ - data: { - name: '赛事活动', - path: '/student-activities', - component: null, - icon: 'TrophyOutlined', - permission: 'contest:activity:read', - sort: 10, - parentId: null, - validState: 1, - } - }); - console.log(' ✅ 已创建父级菜单'); - } - - // 3. 创建三个子菜单 - console.log('\n📝 步骤3: 创建子菜单'); - - const subMenus = [ - { - name: '我的指导', - path: '/student-activities/guidance', - component: 'activities/Guidance', - permission: 'contest:activity:read', - sort: 1, - }, - { - name: '评审作品', - path: '/student-activities/review', - component: 'activities/Review', - permission: 'contest:activity:read', - sort: 2, - }, - { - name: '预设评语', - path: '/student-activities/comments', - component: 'activities/Comments', - permission: 'contest:activity:read', - sort: 3, - }, - ]; - - for (const menuData of subMenus) { - const existing = await prisma.menu.findFirst({ - where: { path: menuData.path } - }); - - if (existing) { - console.log(` ⏭️ ${menuData.name} 已存在`); - } else { - await prisma.menu.create({ - data: { - ...menuData, - parentId: activityParent.id, - validState: 1, - } - }); - console.log(` ✅ 已创建: ${menuData.name}`); - } - } - - // 4. 为租户分配新菜单 - console.log('\n📝 步骤4: 分配菜单给租户'); - const tenants = await prisma.tenant.findMany({ - select: { id: true, name: true } - }); - - const allMenus = await prisma.menu.findMany({ - where: { - OR: [ - { id: activityParent.id }, - { parentId: activityParent.id } - ] - } - }); - - for (const tenant of tenants) { - for (const menu of allMenus) { - const existing = await prisma.tenantMenu.findFirst({ - where: { tenantId: tenant.id, menuId: menu.id } - }); - if (!existing) { - await prisma.tenantMenu.create({ - data: { tenantId: tenant.id, menuId: menu.id } - }); - } - } - console.log(` ✅ 已分配给租户: ${tenant.name}`); - } - - // 5. 显示最终菜单结构 - console.log('\n📋 最终菜单结构:'); - const finalMenus = await prisma.menu.findMany({ - where: { - OR: [ - { id: activityParent.id }, - { parentId: activityParent.id } - ] - }, - orderBy: { sort: 'asc' } - }); - - finalMenus.forEach(m => { - const prefix = m.parentId ? ' ' : ''; - console.log(`${prefix}- ${m.name} (${m.path})`); - }); - - console.log('\n✅ 菜单更新完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/update-password.ts b/backend/scripts/update-password.ts deleted file mode 100644 index 407e335..0000000 --- a/backend/scripts/update-password.ts +++ /dev/null @@ -1,120 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -// 加载环境变量(必须在其他导入之前) -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -// 根据 NODE_ENV 加载对应的环境配置文件 -const nodeEnv = process.env.NODE_ENV || 'development'; -const envFile = `.env.${nodeEnv}`; -// scripts 目录的父目录就是 backend 目录 -const backendDir = path.resolve(__dirname, '..'); -const envPath = path.resolve(backendDir, envFile); - -// 尝试加载环境特定的配置文件 -dotenv.config({ path: envPath }); - -// 如果环境特定文件不存在,尝试加载默认的 .env 文件 -if (!process.env.DATABASE_URL) { - dotenv.config({ path: path.resolve(backendDir, '.env') }); -} - -// 验证必要的环境变量 -if (!process.env.DATABASE_URL) { - console.error('❌ 错误: 未找到 DATABASE_URL 环境变量'); - console.error(` 请确保存在以下文件之一:`); - console.error(` - ${envPath}`); - console.error(` - ${path.resolve(backendDir, '.env')}`); - console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`); - process.exit(1); -} - -import { PrismaClient } from '@prisma/client'; -import * as bcrypt from 'bcrypt'; - -const prisma = new PrismaClient(); - -async function updatePassword() { - try { - const tenantCode = 'super'; - const username = 'admin'; - const newPassword = process.argv[2] || 'cms@admin'; // 支持命令行参数传入新密码 - - console.log(`🔐 开始修改租户 "${tenantCode}" 的 admin 用户密码...\n`); - - // 1. 查找租户 - console.log(`📋 步骤 1: 查找租户 "${tenantCode}"...`); - const tenant = await prisma.tenant.findUnique({ - where: { code: tenantCode }, - }); - - if (!tenant) { - console.error(`❌ 错误: 租户 "${tenantCode}" 不存在!`); - process.exit(1); - } - - console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`); - - // 2. 查找用户 - console.log(`👤 步骤 2: 查找用户 "${username}"...`); - const existingUser = await prisma.user.findFirst({ - where: { - tenantId: tenant.id, - username: username, - }, - }); - - if (!existingUser) { - console.error( - `❌ 错误: 租户 "${tenantCode}" 下不存在用户 "${username}"!`, - ); - console.error(` 请先创建该用户`); - process.exit(1); - } - - console.log( - `✅ 找到用户: ${existingUser.username} (${existingUser.nickname})\n`, - ); - - // 3. 加密新密码 - console.log(`🔒 步骤 3: 加密新密码...`); - const hashedPassword = await bcrypt.hash(newPassword, 10); - console.log(`✅ 密码加密完成\n`); - - // 4. 更新密码 - console.log(`💾 步骤 4: 更新用户密码...`); - const updatedUser = await prisma.user.update({ - where: { id: existingUser.id }, - data: { - password: hashedPassword, - }, - }); - - console.log(`✅ 密码修改成功!\n`); - console.log(`📊 更新结果:`); - console.log(` 租户名称: ${tenant.name}`); - console.log(` 租户编码: ${tenant.code}`); - console.log(` 用户ID: ${updatedUser.id}`); - console.log(` 用户名: ${updatedUser.username}`); - console.log(` 昵称: ${updatedUser.nickname}`); - console.log(` 新密码: ${newPassword}`); - console.log(` 修改时间: ${updatedUser.modifyTime}\n`); - } catch (error) { - console.error('❌ 修改密码时发生错误:'); - console.error(error); - process.exit(1); - } finally { - await prisma.$disconnect(); - } -} - -// 执行脚本 -updatePassword() - .then(() => { - console.log('🎉 密码修改脚本执行完成!'); - process.exit(0); - }) - .catch((error) => { - console.error('\n💥 密码修改脚本执行失败:', error); - process.exit(1); - }); diff --git a/backend/scripts/update-student-permissions.ts b/backend/scripts/update-student-permissions.ts deleted file mode 100644 index 34da22c..0000000 --- a/backend/scripts/update-student-permissions.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🔧 更新学生角色权限...\n'); - - // 获取学生角色 - const studentRole = await prisma.role.findFirst({ - where: { code: 'student' } - }); - - if (!studentRole) { - console.log('❌ 未找到学生角色'); - return; - } - - console.log(`找到学生角色: ${studentRole.name} (ID: ${studentRole.id})`); - - // 需要移除的权限 - const permissionsToRemove = [ - 'contest:read', // 赛事列表权限 - 'contest:work:read', // 作品管理权限 - 'contest:work:update', // 作品更新权限 - 'work:submit', // 提交作品权限 - 'homework:update', // 作业更新权限 - 'homework:delete', // 作业删除权限 - ]; - - // 需要保留/添加的权限 - const permissionsToKeep = [ - 'workbench:read', // 工作台 - 'contest:activity:read', // 赛事活动(新权限) - 'homework:read', // 查看作业 - ]; - - // 移除不需要的权限 - console.log('\n📝 移除不需要的权限...'); - for (const permCode of permissionsToRemove) { - const perm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: permCode } - }); - - if (perm) { - const deleted = await prisma.rolePermission.deleteMany({ - where: { roleId: studentRole.id, permissionId: perm.id } - }); - if (deleted.count > 0) { - console.log(` ✅ 已移除: ${permCode}`); - } else { - console.log(` ⏭️ ${permCode} 未分配,跳过`); - } - } - } - - // 创建赛事活动权限(如果不存在) - console.log('\n📝 创建/分配赛事活动权限...'); - let activityPerm = await prisma.permission.findFirst({ - where: { tenantId: studentRole.tenantId, code: 'contest:activity:read' } - }); - - if (!activityPerm) { - activityPerm = await prisma.permission.create({ - data: { - tenantId: studentRole.tenantId, - code: 'contest:activity:read', - name: '查看赛事活动', - resource: 'contest:activity', - action: 'read', - description: '学生查看赛事活动列表', - } - }); - console.log(' ✅ 创建权限: contest:activity:read'); - } - - // 分配赛事活动权限给学生 - const existingActivityPerm = await prisma.rolePermission.findFirst({ - where: { roleId: studentRole.id, permissionId: activityPerm.id } - }); - if (!existingActivityPerm) { - await prisma.rolePermission.create({ - data: { roleId: studentRole.id, permissionId: activityPerm.id } - }); - console.log(' ✅ 已分配给学生角色: contest:activity:read'); - } - - // 更新赛事活动菜单的权限 - console.log('\n📝 更新菜单权限配置...'); - const activityMenu = await prisma.menu.findFirst({ - where: { name: '赛事活动' } - }); - if (activityMenu) { - await prisma.menu.update({ - where: { id: activityMenu.id }, - data: { permission: 'contest:activity:read' } - }); - console.log(' ✅ 赛事活动菜单权限更新为: contest:activity:read'); - } - - // 显示最终的学生权限 - console.log('\n📋 学生角色最终权限:'); - const finalPerms = await prisma.rolePermission.findMany({ - where: { roleId: studentRole.id }, - include: { permission: true } - }); - finalPerms.forEach(rp => { - console.log(` - ${rp.permission.code}: ${rp.permission.name}`); - }); - - console.log('\n✅ 学生权限更新完成!'); -} - -main() - .catch(console.error) - .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/update-tenant-menu-permission.ts b/backend/scripts/update-tenant-menu-permission.ts new file mode 100644 index 0000000..06c787f --- /dev/null +++ b/backend/scripts/update-tenant-menu-permission.ts @@ -0,0 +1,78 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +const nodeEnv = process.env.NODE_ENV || 'development'; +const envFile = `.env.${nodeEnv}`; +const backendDir = path.resolve(__dirname, '..'); +const envPath = path.resolve(backendDir, envFile); + +dotenv.config({ path: envPath }); + +if (!process.env.DATABASE_URL) { + dotenv.config({ path: path.resolve(backendDir, '.env') }); +} + +if (!process.env.DATABASE_URL) { + console.error('DATABASE_URL not found'); + process.exit(1); +} + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function updateTenantMenuPermission() { + try { + console.log('🚀 开始更新租户管理菜单权限...\n'); + + // 查找租户管理菜单 + const tenantMenu = await prisma.menu.findFirst({ + where: { + name: '租户管理', + path: '/system/tenants', + }, + }); + + if (!tenantMenu) { + console.log('❌ 租户管理菜单不存在'); + return; + } + + console.log(`找到租户管理菜单: ID=${tenantMenu.id}, 当前权限=${tenantMenu.permission}`); + + if (tenantMenu.permission === 'tenant:update') { + console.log('✅ 菜单权限已经是 tenant:update,无需更新'); + return; + } + + // 更新菜单权限 + await prisma.menu.update({ + where: { id: tenantMenu.id }, + data: { + permission: 'tenant:update', + }, + }); + + console.log('✅ 菜单权限已更新为 tenant:update'); + console.log('\n说明:'); + console.log(' - 普通租户只有 tenant:read 权限,可以读取租户列表(用于发布赛事选择公开范围)'); + console.log(' - 只有超级租户才有 tenant:update 权限,才能看到租户管理菜单'); + } catch (error) { + console.error('❌ 更新失败:', error); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +updateTenantMenuPermission() + .then(() => { + console.log('\n🎉 脚本执行完成!'); + process.exit(0); + }) + .catch((error) => { + console.error('\n💥 脚本执行失败:', error); + process.exit(1); + }); diff --git a/backend/src/contests/contests/contests.service.ts b/backend/src/contests/contests/contests.service.ts index 58e326e..2d8f03e 100644 --- a/backend/src/contests/contests/contests.service.ts +++ b/backend/src/contests/contests/contests.service.ts @@ -17,6 +17,11 @@ export class ContestsService { * 检查比赛是否对租户可见 */ private isContestVisibleToTenant(contest: any, tenantId: number): boolean { + // 如果赛事未发布,对租户不可见 + if (contest.contestState !== 'published') { + return false; + } + // 如果contestTenants为null,表示所有租户可见 if (!contest.contestTenants) { return true; @@ -173,6 +178,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, @@ -232,6 +238,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, @@ -239,28 +246,60 @@ export class ContestsService { this.prisma.contest.count({ where }), ]); - // 如果指定了租户ID,进行应用层过滤 + // 如果指定了租户ID,进行应用层过滤(超级租户不过滤,可以看到所有赛事) let filteredList = allList; let filteredTotal = allTotal; if (tenantId) { - filteredList = allList.filter((contest) => - this.isContestVisibleToTenant(contest, tenantId), - ); - // 重新计算总数(简化处理,实际应该用原生SQL) - filteredTotal = filteredList.length; - // 限制返回数量 - filteredList = filteredList.slice(0, pageSize); + // 检查是否为超级租户 + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { isSuper: true }, + }); + const isSuperTenant = tenant?.isSuper === 1; + + // 超级租户可以看到所有赛事,普通租户只能看到已发布且在公开范围内的赛事 + if (!isSuperTenant) { + filteredList = allList.filter((contest) => + this.isContestVisibleToTenant(contest, tenantId), + ); + // 重新计算总数(简化处理,实际应该用原生SQL) + filteredTotal = filteredList.length; + // 限制返回数量 + filteredList = filteredList.slice(0, pageSize); + } } + // 解析 contestTenants JSON 字符串为数组 + const parsedList = filteredList.map((contest) => ({ + ...contest, + contestTenants: this.parseContestTenants(contest.contestTenants), + })); + return { - list: filteredList, + list: parsedList, total: filteredTotal, page, pageSize, }; } + /** + * 解析 contestTenants JSON 字符串为数组 + */ + private parseContestTenants(contestTenants: any): number[] | null { + if (!contestTenants) { + return null; + } + try { + return Array.isArray(contestTenants) + ? contestTenants + : JSON.parse(contestTenants as string); + } catch { + return null; + } + } + /** * 获取我参与的赛事列表 * @param role 角色:student-学生报名的赛事, teacher-教师指导的赛事, judge-评委评审的赛事 @@ -389,6 +428,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, @@ -437,12 +477,24 @@ export class ContestsService { throw new NotFoundException('比赛不存在'); } - // 租户过滤:检查比赛是否对租户可见 - if (tenantId && !this.isContestVisibleToTenant(contest, tenantId)) { - throw new NotFoundException('比赛不存在或无权访问'); + // 租户过滤:检查比赛是否对租户可见(超级租户可以查看所有赛事) + if (tenantId) { + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { isSuper: true }, + }); + const isSuperTenant = tenant?.isSuper === 1; + + if (!isSuperTenant && !this.isContestVisibleToTenant(contest, tenantId)) { + throw new NotFoundException('比赛不存在或无权访问'); + } } - return contest; + // 解析 contestTenants JSON 字符串为数组 + return { + ...contest, + contestTenants: this.parseContestTenants(contest.contestTenants), + }; } async update( @@ -655,6 +707,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, @@ -779,6 +832,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, @@ -822,6 +876,7 @@ export class ContestsService { registrations: true, works: true, teams: true, + judges: true, }, }, }, diff --git a/backend/src/contests/works/dto/query-work.dto.ts b/backend/src/contests/works/dto/query-work.dto.ts index 67edcad..bae3cbc 100644 --- a/backend/src/contests/works/dto/query-work.dto.ts +++ b/backend/src/contests/works/dto/query-work.dto.ts @@ -40,5 +40,13 @@ export class QueryWorkDto { @IsString() @IsOptional() title?: string; + + @IsString() + @IsOptional() + workNo?: string; + + @IsString() + @IsOptional() + username?: string; } diff --git a/backend/src/contests/works/works.service.ts b/backend/src/contests/works/works.service.ts index 80521b2..ea52258 100644 --- a/backend/src/contests/works/works.service.ts +++ b/backend/src/contests/works/works.service.ts @@ -172,6 +172,8 @@ export class WorksService { registrationId, status, title, + workNo, + username, } = queryDto; const skip = (page - 1) * pageSize; @@ -202,6 +204,31 @@ export class WorksService { }; } + if (workNo) { + where.workNo = { + contains: workNo, + }; + } + + if (username) { + where.OR = [ + { + submitterAccountNo: { + contains: username, + }, + }, + { + registration: { + user: { + username: { + contains: username, + }, + }, + }, + }, + ]; + } + const [list, total] = await Promise.all([ this.prisma.contestWork.findMany({ where, @@ -215,6 +242,12 @@ export class WorksService { select: { id: true, contestName: true, + reviewRuleId: true, + reviewRule: { + select: { + judgeCount: true, + }, + }, }, }, registration: { @@ -226,9 +259,33 @@ export class WorksService { nickname: true, }, }, + team: { + select: { + id: true, + teamName: true, + }, + }, }, }, attachments: true, + scores: { + where: { validState: 1 }, + select: { + id: true, + totalScore: true, + }, + }, + assignments: { + include: { + judge: { + select: { + id: true, + username: true, + nickname: true, + }, + }, + }, + }, _count: { select: { scores: true, @@ -240,8 +297,32 @@ export class WorksService { this.prisma.contestWork.count({ where }), ]); + // 计算评审统计数据 + const enrichedList = list.map((work) => { + const reviewedCount = work._count?.scores || 0; + const totalJudgesCount = + work.contest?.reviewRule?.judgeCount || work._count?.assignments || 0; + + // 计算平均分 + let averageScore: number | null = null; + if (work.scores && work.scores.length > 0) { + const totalScore = work.scores.reduce( + (sum, score) => sum + Number(score.totalScore || 0), + 0, + ); + averageScore = totalScore / work.scores.length; + } + + return { + ...work, + reviewedCount, + totalJudgesCount, + averageScore, + }; + }); + return { - list, + list: enrichedList, total, page, pageSize, diff --git a/backend/src/judges-management/dto/create-judge.dto.ts b/backend/src/judges-management/dto/create-judge.dto.ts index c485206..c671f54 100644 --- a/backend/src/judges-management/dto/create-judge.dto.ts +++ b/backend/src/judges-management/dto/create-judge.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsEnum, IsInt } from 'class-validator'; +import { IsString, IsOptional, IsEnum } from 'class-validator'; import { Gender, UserStatus } from '../../users/dto/create-user.dto'; export class CreateJudgeDto { @@ -8,8 +8,8 @@ export class CreateJudgeDto { @IsEnum(Gender) gender: Gender; - @IsInt() - tenantId: number; + @IsString() + organization: string; @IsString() phone: string; diff --git a/backend/src/judges-management/dto/query-judge.dto.ts b/backend/src/judges-management/dto/query-judge.dto.ts index 4079b33..67fde82 100644 --- a/backend/src/judges-management/dto/query-judge.dto.ts +++ b/backend/src/judges-management/dto/query-judge.dto.ts @@ -13,10 +13,9 @@ export class QueryJudgeDto { @Type(() => Number) pageSize?: number = 10; - @IsInt() + @IsString() @IsOptional() - @Type(() => Number) - tenantId?: number; + organization?: string; @IsString() @IsOptional() diff --git a/backend/src/judges-management/dto/update-judge.dto.ts b/backend/src/judges-management/dto/update-judge.dto.ts index 056a3e6..1a5e90b 100644 --- a/backend/src/judges-management/dto/update-judge.dto.ts +++ b/backend/src/judges-management/dto/update-judge.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsEnum, IsInt } from 'class-validator'; +import { IsString, IsOptional, IsEnum } from 'class-validator'; import { Gender, UserStatus } from '../../users/dto/create-user.dto'; export class UpdateJudgeDto { @@ -10,9 +10,9 @@ export class UpdateJudgeDto { @IsOptional() gender?: Gender; - @IsInt() + @IsString() @IsOptional() - tenantId?: number; + organization?: string; @IsString() @IsOptional() diff --git a/backend/src/judges-management/judges-management.service.ts b/backend/src/judges-management/judges-management.service.ts index 39f6463..41c5fe3 100644 --- a/backend/src/judges-management/judges-management.service.ts +++ b/backend/src/judges-management/judges-management.service.ts @@ -15,6 +15,24 @@ const JUDGE_ROLE_CODE = 'judge'; export class JudgesManagementService { constructor(private prisma: PrismaService) {} + /** + * 获取超级租户ID(评委统一存储在超级租户下) + */ + private async getSuperTenantId(): Promise { + const superTenant = await this.prisma.tenant.findFirst({ + where: { + isSuper: 1, + validState: 1, + }, + }); + + if (!superTenant) { + throw new BadRequestException('系统未配置超级租户'); + } + + return superTenant.id; + } + /** * 确保评委角色存在,如果不存在则创建 */ @@ -44,10 +62,13 @@ export class JudgesManagementService { * 创建评委 */ async create(createJudgeDto: CreateJudgeDto, creatorId?: number) { - const { tenantId, password, ...userData } = createJudgeDto; + const { organization, password, ...userData } = createJudgeDto; + + // 评委统一存储在超级租户下 + const superTenantId = await this.getSuperTenantId(); // 确保评委角色存在 - const judgeRoleId = await this.ensureJudgeRole(tenantId); + const judgeRoleId = await this.ensureJudgeRole(superTenantId); // 生成用户名(如果没有提供) let username = createJudgeDto.username; @@ -56,11 +77,11 @@ export class JudgesManagementService { username = createJudgeDto.phone; } - // 检查用户名是否已存在 + // 检查用户名是否已存在(在超级租户下) const existingUser = await this.prisma.user.findFirst({ where: { username, - tenantId, + tenantId: superTenantId, }, }); @@ -76,7 +97,8 @@ export class JudgesManagementService { data: { ...userData, username, - tenantId, + tenantId: superTenantId, + organization, // 使用独立的所属单位字段 password: hashedPassword, status: createJudgeDto.status || 'enabled', creator: creatorId, @@ -87,12 +109,6 @@ export class JudgesManagementService { }, }, include: { - tenant: { - select: { - id: true, - name: true, - }, - }, roles: { include: { role: true, @@ -108,7 +124,7 @@ export class JudgesManagementService { * 查询评委列表 */ async findAll(queryDto: QueryJudgeDto) { - const { page = 1, pageSize = 10, tenantId, nickname, username, status } = queryDto; + const { page = 1, pageSize = 10, organization, nickname, username, status } = queryDto; const skip = (page - 1) * pageSize; // 构建查询条件:必须有评委角色 @@ -123,8 +139,10 @@ export class JudgesManagementService { }, }; - if (tenantId) { - where.tenantId = tenantId; + if (organization) { + where.organization = { + contains: organization, + }; } if (nickname) { @@ -149,12 +167,6 @@ export class JudgesManagementService { skip, take: pageSize, include: { - tenant: { - select: { - id: true, - name: true, - }, - }, roles: { include: { role: true, @@ -210,12 +222,6 @@ export class JudgesManagementService { }, }, include: { - tenant: { - select: { - id: true, - name: true, - }, - }, roles: { include: { role: true, @@ -264,19 +270,10 @@ export class JudgesManagementService { data.modifier = modifierId; } - // 移除 tenantId,不允许修改租户 - delete data.tenantId; - return this.prisma.user.update({ where: { id }, data, include: { - tenant: { - select: { - id: true, - name: true, - }, - }, roles: { include: { role: true, @@ -329,14 +326,6 @@ export class JudgesManagementService { data: { status, }, - include: { - tenant: { - select: { - id: true, - name: true, - }, - }, - }, }); } diff --git a/backend/src/menus/menus.service.ts b/backend/src/menus/menus.service.ts index 09baf24..28435c4 100644 --- a/backend/src/menus/menus.service.ts +++ b/backend/src/menus/menus.service.ts @@ -71,30 +71,7 @@ export class MenusService { * @returns 过滤后的菜单树 */ async findUserMenus(userId: number, tenantId: number) { - // 检查是否为超级租户 - const tenant = await this.prisma.tenant.findUnique({ - where: { id: tenantId }, - select: { isSuper: true }, - }); - const isSuperTenant = tenant?.isSuper === 1; - - // 获取用户信息和角色 - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - include: { - roles: { - include: { - role: true, - }, - }, - }, - }); - - // 检查用户是否有超级管理员角色 - const userRoles = user?.roles?.map((ur: any) => ur.role.code) || []; - const isSuperAdmin = userRoles.includes('super_admin'); - - // 获取用户的所有权限(即使是超级租户,普通用户也需要权限检查) + // 获取用户的所有权限 const userPermissions = await this.authService.getUserPermissions(userId); // 获取租户分配的菜单ID @@ -135,19 +112,10 @@ export class MenusService { const menuTree = buildTree(allMenus); // 过滤菜单:只有用户拥有菜单对应权限的菜单才会显示 - // 超级管理员显示所有菜单,普通用户根据权限过滤 + // 所有用户都根据权限过滤菜单 const filterMenus = (menus: any[]): any[] => { return menus .map((menu) => { - // 超级管理员显示所有菜单 - if (isSuperAdmin) { - const filtered = { ...menu }; - if (menu.children && menu.children.length > 0) { - filtered.children = filterMenus(menu.children); - } - return filtered; - } - // 先递归处理子菜单 let filteredChildren: any[] = []; if (menu.children && menu.children.length > 0) { diff --git a/backend/tmpclaude-0d23-cwd b/backend/tmpclaude-0d23-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-0d23-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-16b9-cwd b/backend/tmpclaude-16b9-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-16b9-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-3238-cwd b/backend/tmpclaude-3238-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-3238-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-9916-cwd b/backend/tmpclaude-9916-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-9916-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-bac0-cwd b/backend/tmpclaude-bac0-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-bac0-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-cd6c-cwd b/backend/tmpclaude-cd6c-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-cd6c-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/backend/tmpclaude-dcee-cwd b/backend/tmpclaude-dcee-cwd new file mode 100644 index 0000000..283c91e --- /dev/null +++ b/backend/tmpclaude-dcee-cwd @@ -0,0 +1 @@ +/c/Users/82788/Desktop/work/competition-management-system/backend diff --git a/frontend/src/api/contests.ts b/frontend/src/api/contests.ts index 7c7e866..371ba82 100644 --- a/frontend/src/api/contests.ts +++ b/frontend/src/api/contests.ts @@ -329,10 +329,24 @@ export interface ContestWork { contest?: Contest; registration?: ContestRegistration; attachments?: ContestWorkAttachment[]; + assignments?: Array<{ + id: number; + judgeId: number; + status: string; + judge?: { + id: number; + username: string; + nickname: string; + }; + }>; _count?: { scores: number; assignments: number; }; + // 评审统计字段(由后端计算返回) + reviewedCount?: number; + totalJudgesCount?: number; + averageScore?: number | null; } export interface ContestWorkAttachment { @@ -365,6 +379,8 @@ export interface QueryWorkParams extends PaginationParams { registrationId?: number; status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted"; title?: string; + workNo?: string; + username?: string; } // ==================== 评审相关类型 ==================== @@ -1063,6 +1079,17 @@ export const reviewsApi = { >(`/contests/reviews/work/${workId}/final-score`); return response; }, + + // 替换评委 + replaceJudge: async ( + assignmentId: number, + newJudgeId: number + ): Promise => { + await request.post( + `/contests/reviews/replace-judge`, + { assignmentId, newJudgeId } + ); + }, }; // 公告管理 diff --git a/frontend/src/api/judges-management.ts b/frontend/src/api/judges-management.ts index 5fc2ba0..91dd1ec 100644 --- a/frontend/src/api/judges-management.ts +++ b/frontend/src/api/judges-management.ts @@ -10,16 +10,12 @@ export interface Judge { gender?: 'male' | 'female'; avatar?: string; status?: 'enabled' | 'disabled'; - tenantId?: number; + organization?: string; validState?: number; creator?: number; modifier?: number; createTime?: string; modifyTime?: string; - tenant?: { - id: number; - name: string; - }; roles?: Array<{ id: number; role: { @@ -39,7 +35,7 @@ export interface Judge { } export interface QueryJudgeParams extends PaginationParams { - tenantId?: number; + organization?: string; nickname?: string; username?: string; status?: 'enabled' | 'disabled'; @@ -48,7 +44,7 @@ export interface QueryJudgeParams extends PaginationParams { export interface CreateJudgeForm { nickname: string; gender: 'male' | 'female'; - tenantId: number; + organization: string; phone: string; password: string; username?: string; @@ -60,6 +56,7 @@ export interface CreateJudgeForm { export interface UpdateJudgeForm { nickname?: string; gender?: 'male' | 'female'; + organization?: string; phone?: string; password?: string; username?: string; @@ -153,3 +150,4 @@ export const judgesManagementApi = { }; + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 2fb982c..d1f8d51 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -110,6 +110,28 @@ const baseRoutes: RouteRecordRaw[] = [ permissions: ["contest:registration:read"], }, }, + // 评审进度详情路由 + { + path: "contests/reviews/:id/progress", + name: "ReviewProgressDetail", + component: () => import("@/views/contests/reviews/ProgressDetail.vue"), + meta: { + title: "评审进度详情", + requiresAuth: true, + permissions: ["review:read"], + }, + }, + // 参赛作品详情列表路由 + { + path: "contests/works/:id/list", + name: "WorksDetail", + component: () => import("@/views/contests/works/WorksDetail.vue"), + meta: { + title: "参赛作品详情", + requiresAuth: true, + permissions: ["work:read"], + }, + }, // 作业提交记录路由 { path: "homework/submissions", diff --git a/frontend/src/utils/menu.ts b/frontend/src/utils/menu.ts index e5fab67..32cf716 100644 --- a/frontend/src/utils/menu.ts +++ b/frontend/src/utils/menu.ts @@ -30,9 +30,14 @@ const componentMap: Record Promise> = { "contests/registrations/Index": () => import("@/views/contests/registrations/Index.vue"), "contests/works/Index": () => import("@/views/contests/works/Index.vue"), + "contests/works/WorksDetail": () => + import("@/views/contests/works/WorksDetail.vue"), "contests/reviews/Index": () => import("@/views/contests/reviews/Index.vue"), + "contests/reviews/Tasks": () => import("@/views/contests/reviews/Tasks.vue"), "contests/reviews/Progress": () => import("@/views/contests/reviews/Progress.vue"), + "contests/reviews/ProgressDetail": () => + import("@/views/contests/reviews/ProgressDetail.vue"), "contests/judges/Index": () => import("@/views/contests/judges/Index.vue"), "contests/results/Index": () => import("@/views/contests/results/Index.vue"), "contests/notices/Index": () => import("@/views/contests/notices/Index.vue"), diff --git a/frontend/src/views/contests/Index.vue b/frontend/src/views/contests/Index.vue index faa92ae..e5282b7 100644 --- a/frontend/src/views/contests/Index.vue +++ b/frontend/src/views/contests/Index.vue @@ -1,7 +1,7 @@