From 64e61454ab6aca3c04f06cb670c51214fa51c42e Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Mar 2026 18:47:19 +0800 Subject: [PATCH] feat: refresh innovation platform demo --- .gitignore | 4 + 01-需求分析文档.md | 288 +++++++++ 02-数据库设计文档.md | 477 +++++++++++++++ README.md | 45 ++ backend/Dockerfile | 5 + backend/pom.xml | 92 +++ .../InnovationPlatformApplication.java | 16 + .../platform/common/PageResult.java | 28 + .../innovation/platform/common/Result.java | 51 ++ .../platform/config/SaTokenConfig.java | 25 + .../platform/controller/AuthController.java | 44 ++ .../controller/ProjectController.java | 69 +++ .../innovation/platform/dto/LoginRequest.java | 13 + .../platform/dto/LoginResponse.java | 22 + .../platform/dto/ProjectQueryRequest.java | 28 + .../platform/dto/ProjectRequest.java | 90 +++ .../platform/dto/ProjectResponse.java | 116 ++++ .../platform/dto/ProjectStatsResponse.java | 37 ++ .../platform/dto/RegisterRequest.java | 29 + .../platform/entity/Achievement.java | 30 + .../entity/AchievementAttachment.java | 23 + .../platform/entity/BaseEntity.java | 30 + .../innovation/platform/entity/Project.java | 30 + .../platform/entity/ProjectAttachment.java | 24 + .../platform/entity/ProjectMember.java | 22 + .../innovation/platform/entity/Review.java | 24 + .../platform/entity/ReviewScoreItem.java | 22 + .../innovation/platform/entity/StuInfo.java | 23 + .../innovation/platform/entity/SysConfig.java | 18 + .../innovation/platform/entity/SysLog.java | 25 + .../innovation/platform/entity/SysUser.java | 23 + .../platform/entity/TeacherInfo.java | 19 + .../exception/GlobalExceptionHandler.java | 46 ++ .../platform/mapper/ProjectMapper.java | 12 + .../platform/mapper/SysUserMapper.java | 12 + .../platform/service/ProjectService.java | 25 + .../platform/service/SysUserService.java | 37 ++ .../service/impl/ProjectServiceImpl.java | 194 ++++++ .../service/impl/SysUserServiceImpl.java | 86 +++ backend/src/main/resources/application.yml | 28 + backend/src/main/resources/data.sql | 201 +++++++ backend/src/main/resources/schema.sql | 133 +++++ backend/src/main/resources/static/index.html | 562 ++++++++++++++++++ docker-compose.yml | 37 ++ 44 files changed, 3165 insertions(+) create mode 100644 .gitignore create mode 100644 01-需求分析文档.md create mode 100644 02-数据库设计文档.md create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/innovation/platform/InnovationPlatformApplication.java create mode 100644 backend/src/main/java/com/innovation/platform/common/PageResult.java create mode 100644 backend/src/main/java/com/innovation/platform/common/Result.java create mode 100644 backend/src/main/java/com/innovation/platform/config/SaTokenConfig.java create mode 100644 backend/src/main/java/com/innovation/platform/controller/AuthController.java create mode 100644 backend/src/main/java/com/innovation/platform/controller/ProjectController.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/LoginRequest.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/LoginResponse.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/ProjectQueryRequest.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/ProjectRequest.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/ProjectResponse.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/ProjectStatsResponse.java create mode 100644 backend/src/main/java/com/innovation/platform/dto/RegisterRequest.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/Achievement.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/AchievementAttachment.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/BaseEntity.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/Project.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/ProjectAttachment.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/ProjectMember.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/Review.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/ReviewScoreItem.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/StuInfo.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/SysConfig.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/SysLog.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/SysUser.java create mode 100644 backend/src/main/java/com/innovation/platform/entity/TeacherInfo.java create mode 100644 backend/src/main/java/com/innovation/platform/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/innovation/platform/mapper/ProjectMapper.java create mode 100644 backend/src/main/java/com/innovation/platform/mapper/SysUserMapper.java create mode 100644 backend/src/main/java/com/innovation/platform/service/ProjectService.java create mode 100644 backend/src/main/java/com/innovation/platform/service/SysUserService.java create mode 100644 backend/src/main/java/com/innovation/platform/service/impl/ProjectServiceImpl.java create mode 100644 backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/data.sql create mode 100644 backend/src/main/resources/schema.sql create mode 100644 backend/src/main/resources/static/index.html create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01d3be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +backend/target/ +.idea/ +*.iml +.DS_Store diff --git a/01-需求分析文档.md b/01-需求分析文档.md new file mode 100644 index 0000000..d147aea --- /dev/null +++ b/01-需求分析文档.md @@ -0,0 +1,288 @@ +# 高校创新创业项目孵化平台 - 需求分析文档 + +## 一、用户角色分析 + +### 1.1 角色定义 + +| 角色 | 描述 | 核心职责 | +|------|------|----------| +| 教师 | 项目指导教师和评审专家 | 项目指导、评审打分、意见反馈 | +| 管理员 | 系统运维和业务管理 | 用户管理、项目管理、规则配置、数据统计 | + +### 1.2 角色权限矩阵 + +| 功能模块 | 学生 | 教师 | 管理员 | +|----------|------|------|--------| +| 用户注册/登录 | ✅ | ✅ | ✅ | +| 个人信息管理 | ✅ | ✅ | ✅ | +| 项目申报 | ✅ | ❌ | ✅ | +| 项目查看(自己) | ✅ | ❌ | ✅ | +| 项目查看(全部) | ❌ | ✅(分配) | ✅ | +| 项目评审 | ❌ | ✅ | ✅ | +| 成果提交 | ✅ | ❌ | ✅ | +| 规则配置 | ❌ | ❌ | ✅ | +| 用户管理 | ❌ | ❌ | ✅ | +| 数据统计 | ❌ | ❌ | ✅ | + +### 1.3 用户属性详细设计 + +#### 学生属性 +- 学号(唯一标识) +- 姓名 +- 性别 +- 学院 +- 专业 +- 年级 +- 班级 +- 联系电话 +- 邮箱 +- 指导教师ID + +#### 教师属性 +- 工号(唯一标识) +- 姓名 +- 性别 +- 学院 +- 职称 +- 联系电话 +- 邮箱 +- 研究方向 + +#### 管理员属性 +- 管理员账号 +- 姓名 +- 权限范围 +- 联系方式 + +--- + +## 二、功能模块拆分 + +### 2.1 模块总览图(文字描述) + +``` +高校创新创业项目孵化平台 +├── 用户中心模块 +│ ├── 用户注册 +│ ├── 用户登录/登出 +│ ├── 个人信息管理 +│ └── 密码修改/重置 +│ +├── 项目管理模块 +│ ├── 项目申报 +│ ├── 项目查询 +│ ├── 项目修改 +│ ├── 项目进度跟踪 +│ └── 项目归档 +│ +├── 评审管理模块 +│ ├── 评审任务分配 +│ ├── 在线评审 +│ ├── 评审意见填写 +│ └── 评审结果查询 +│ +├── 成果管理模块 +│ ├── 成果录入 +│ ├── 成果附件上传 +│ ├── 成果审核 +│ └── 成果统计 +│ +│ +├── 数据统计模块 +│ ├── 项目统计 +│ ├── 成果统计 +│ └── 可视化报表 +│ +└── 系统管理模块 + ├── 用户管理 + ├── 角色权限管理 + ├── 系统配置 + └── 操作日志 +``` + +### 2.2 各模块功能详细说明 + +#### 2.2.1 用户中心模块 + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| +| 用户注册 | 学生/教师自助注册 | 账号、密码、身份信息 | 注册结果 | 全部 | +| 用户登录 | 账号密码登录,支持记住密码 | 账号、密码 | Token、用户信息 | 全部 | +| 个人信息管理 | 修改个人信息 | 修改字段 | 修改结果 | 全部 | +| 密码修改 | 修改登录密码 | 原密码、新密码 | 修改结果 | 全部 | + +#### 2.2.2 项目管理模块 + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| +| 项目申报 | 提交新项目申请 | 项目名称、类型、描述、成员、预算 | 申报结果 | 学生 | +| 项目查询 | 按条件查询项目 | 查询条件 | 项目列表 | 全部 | +| 项目修改 | 修改项目信息 | 修改内容 | 修改结果 | 学生(自己的) | +| 项目进度跟踪 | 更新项目进度 | 进度信息、附件 | 更新结果 | 学生 | +| 项目归档 | 项目结题后归档 | 项目ID | 归档结果 | 管理员 | + +#### 2.2.3 评审管理模块 + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| +| 评审任务分配 | 分配评审专家 | 项目ID、教师ID | 分配结果 | 管理员 | +| 在线评审 | 填写评审意见 | 评审表单 | 提交结果 | 教师 | +| 评审意见填写 | 详细评审意见 | 意见内容、评分 | 保存结果 | 教师 | +| 评审结果查询 | 查看评审结果 | 项目ID | 评审详情 | 学生/教师 | + +#### 2.2.4 成果管理模块 + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| +| 成果录入 | 录入项目成果 | 成果类型、描述、附件 | 录入结果 | 学生 | +| 成果附件上传 | 上传证明材料 | 文件 | 文件URL | 学生 | +| 成果审核 | 审核成果真实性 | 审核意见 | 审核结果 | 教师/管理员 | +| 成果统计 | 统计成果数量 | 统计条件 | 统计报表 | 管理员 | + + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| + +#### 2.2.6 数据统计模块 + +| 功能点 | 功能描述 | 输入 | 输出 | 角色 | +|--------|----------|------|------|------| +| 项目统计 | 项目数量、状态分布 | 时间范围 | 统计数据 | 管理员 | +| 成果统计 | 成果类型、级别分布 | 时间范围 | 统计数据 | 管理员 | +| 可视化报表 | 图表展示统计结果 | 数据源 | ECharts图表 | 管理员 | + +--- + +## 三、业务流程描述 + +### 3.1 项目申报流程 + +``` +1. 学生登录系统 +2. 进入项目申报页面 +3. 填写项目基本信息: + - 项目名称 + - 项目类型(创新训练/创业训练/创业实践) + - 项目级别(校级/省级/国家级) + - 项目简介 + - 研究计划 + - 预期成果 + - 经费预算 +4. 添加项目成员(可多人协作) +5. 选择指导教师 +6. 上传附件材料(项目计划书等) +7. 提交申报 +8. 系统生成项目编号 +9. 项目状态变更为"待初审" +``` + +### 3.2 项目评审流程 + +``` +初审阶段: +1. 管理员查看待初审项目列表 +2. 管理员分配初审专家(1-3人) +3. 系统发送评审通知给专家 +4. 专家登录系统查看评审任务 +5. 专家在线评审: + - 查看项目详情 + - 查看附件材料 + - 填写评审意见 + - 给出评分 + - 选择通过/不通过/修改后通过 +6. 系统汇总评审意见 +7. 若通过,项目状态变更为"初审通过" +8. 若不通过,项目状态变更为"初审不通过",学生可修改后重新提交 + +中期检查阶段: +1. 管理员发起中期检查 +2. 学生填写中期检查报告 +3. 上传阶段性成果材料 +4. 指导教师评审中期报告 +5. 系统记录中期检查结果 + +结题验收阶段: +1. 学生提交结题申请 +2. 上传最终成果材料 +3. 管理员分配验收专家 +4. 专家评审验收材料 +5. 系统记录验收结果 +6. 项目状态变更为"已结题" +``` + + +``` +2. 系统读取项目信息: + - 项目级别 + - 项目成员排名 + - 结题评价等级 +7. 若有异议,可提交申诉 +8. 管理员审核申诉 +``` + +### 3.4 成果管理流程 + +``` +1. 项目进行中/结题后,学生录入成果 +2. 选择成果类型: + - 学术论文 + - 发明专利 + - 实用新型专利 + - 软件著作权 + - 竞赛获奖 + - 创业实践成果 + - 其他 +3. 填写成果详情: + - 成果名称 + - 发表/获得时间 + - 发表/颁发机构 + - 成果描述 +4. 上传证明材料(证书、论文等) +5. 提交审核 +6. 指导教师/管理员审核真实性 +7. 审核通过后,成果状态变更为"已认证" +``` + +--- + +## 四、非功能性需求 + +### 4.1 性能需求 +- 系统响应时间 < 2秒 +- 支持500并发用户 +- 数据库查询优化,索引设计合理 + +### 4.2 安全需求 +- 用户密码加密存储(BCrypt) +- 使用Sa-Token进行会话管理和权限控制 +- 敏感操作需二次确认 +- 操作日志记录 + +### 4.3 可用性需求 +- 界面简洁直观,符合用户习惯 +- 提供操作提示和帮助文档 +- 错误信息友好明确 + +### 4.4 兼容性需求 +- 支持主流浏览器(Chrome、Firefox、Edge、Safari) +- 响应式设计,支持移动端访问 + +--- + +## 五、需求优先级 + +| 优先级 | 模块 | 说明 | +|--------|------|------| +| P0 | 用户中心 | 基础功能,必须优先实现 | +| P0 | 项目管理-申报 | 核心业务入口 | +| P0 | 项目管理-查询 | 基础功能 | +| P1 | 评审管理 | 核心业务流程 | +| P1 | 成果管理 | 核心业务 | +| P2 | 数据统计 | 增值功能 | +| P2 | 系统管理 | 管理功能 | + +--- + +*文档版本: v1.0* +*创建日期: 2026-03-01* +*作者: PMClaw* \ No newline at end of file diff --git a/02-数据库设计文档.md b/02-数据库设计文档.md new file mode 100644 index 0000000..9032fec --- /dev/null +++ b/02-数据库设计文档.md @@ -0,0 +1,477 @@ +# 高校创新创业项目孵化平台 - 数据库设计文档 + +## 一、数据库概述 + +### 1.1 设计原则 +- 遵循第三范式,减少数据冗余 +- 合理设置索引,优化查询性能 +- 使用软删除,保留历史数据 +- 统一字段命名规范(下划线命名法) + +### 1.2 公共字段说明 + +所有表都包含以下公共字段: + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | BIGINT | 主键,自增 | +| create_time | DATETIME | 创建时间 | +| update_time | DATETIME | 更新时间 | +| create_by | BIGINT | 创建人ID | +| update_by | BIGINT | 更新人ID | +| deleted | TINYINT | 逻辑删除标识(0-未删除,1-已删除) | + +--- + +## 二、用户相关表 + +### 2.1 用户表 (sys_user) + +```sql +CREATE TABLE `sys_user` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', + `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', + `gender` TINYINT DEFAULT 0 COMMENT '性别(0-未知,1-男,2-女)', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-禁用,1-启用)', + `role_type` TINYINT NOT NULL COMMENT '角色类型(1-学生,2-教师,3-管理员)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `create_by` BIGINT DEFAULT NULL COMMENT '创建人', + `update_by` BIGINT DEFAULT NULL COMMENT '更新人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_role_type` (`role_type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; +``` + +### 2.2 学生信息表 (stu_info) + +```sql +CREATE TABLE `stu_info` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `student_no` VARCHAR(20) NOT NULL COMMENT '学号', + `college` VARCHAR(100) NOT NULL COMMENT '学院', + `major` VARCHAR(100) NOT NULL COMMENT '专业', + `grade` VARCHAR(10) NOT NULL COMMENT '年级', + `class_name` VARCHAR(50) DEFAULT NULL COMMENT '班级', + `advisor_id` BIGINT DEFAULT NULL COMMENT '指导教师ID', + `total_credit` DECIMAL(5,1) DEFAULT 0.0 COMMENT '累计创新学分', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_student_no` (`student_no`), + UNIQUE KEY `uk_user_id` (`user_id`), + KEY `idx_college` (`college`), + KEY `idx_grade` (`grade`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; +``` + +### 2.3 教师信息表 (teacher_info) + +```sql +CREATE TABLE `teacher_info` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `teacher_no` VARCHAR(20) NOT NULL COMMENT '工号', + `college` VARCHAR(100) NOT NULL COMMENT '学院', + `title` VARCHAR(50) DEFAULT NULL COMMENT '职称', + `research_field` VARCHAR(255) DEFAULT NULL COMMENT '研究方向', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_teacher_no` (`teacher_no`), + UNIQUE KEY `uk_user_id` (`user_id`), + KEY `idx_college` (`college`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师信息表'; +``` + +--- + +## 三、项目相关表 + +### 3.1 项目表 (project) + +```sql +CREATE TABLE `project` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '项目ID', + `project_no` VARCHAR(30) NOT NULL COMMENT '项目编号', + `project_name` VARCHAR(200) NOT NULL COMMENT '项目名称', + `project_type` TINYINT NOT NULL COMMENT '项目类型(1-创新训练,2-创业训练,3-创业实践)', + `project_level` TINYINT NOT NULL COMMENT '项目级别(1-校级,2-省级,3-国家级)', + `leader_id` BIGINT NOT NULL COMMENT '负责人ID', + `advisor_id` BIGINT NOT NULL COMMENT '指导教师ID', + `description` TEXT COMMENT '项目简介', + `research_plan` TEXT COMMENT '研究计划', + `expected_result` TEXT COMMENT '预期成果', + `budget` DECIMAL(10,2) DEFAULT 0.00 COMMENT '经费预算', + `status` TINYINT DEFAULT 1 COMMENT '状态(1-待初审,2-初审中,3-初审通过,4-初审不通过,5-中期检查中,6-中期通过,7-中期不通过,8-结题验收中,9-已结题,10-已归档)', + `start_time` DATE DEFAULT NULL COMMENT '立项时间', + `end_time` DATE DEFAULT NULL COMMENT '结题时间', + `college` VARCHAR(100) DEFAULT NULL COMMENT '所属学院', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `create_by` BIGINT DEFAULT NULL COMMENT '创建人', + `update_by` BIGINT DEFAULT NULL COMMENT '更新人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_project_no` (`project_no`), + KEY `idx_leader_id` (`leader_id`), + KEY `idx_advisor_id` (`advisor_id`), + KEY `idx_status` (`status`), + KEY `idx_project_level` (`project_level`), + KEY `idx_college` (`college`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目表'; +``` + +### 3.2 项目成员表 (project_member) + +```sql +CREATE TABLE `project_member` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `project_id` BIGINT NOT NULL COMMENT '项目ID', + `user_id` BIGINT NOT NULL COMMENT '成员ID', + `member_order` INT DEFAULT 1 COMMENT '成员排名(影响学分分配)', + `role` TINYINT DEFAULT 1 COMMENT '角色(1-成员,2-负责人)', + `join_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_project_user` (`project_id`, `user_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目成员表'; +``` + +### 3.3 项目附件表 (project_attachment) + +```sql +CREATE TABLE `project_attachment` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `project_id` BIGINT NOT NULL COMMENT '项目ID', + `file_name` VARCHAR(255) NOT NULL COMMENT '文件名', + `file_path` VARCHAR(500) NOT NULL COMMENT '文件路径', + `file_size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)', + `file_type` VARCHAR(50) DEFAULT NULL COMMENT '文件类型', + `attachment_type` TINYINT DEFAULT 1 COMMENT '附件类型(1-申报材料,2-中期材料,3-结题材料)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `create_by` BIGINT DEFAULT NULL COMMENT '上传人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_project_id` (`project_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目附件表'; +``` + +--- + +## 四、评审相关表 + +### 4.1 评审表 (review) + +```sql +CREATE TABLE `review` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '评审ID', + `project_id` BIGINT NOT NULL COMMENT '项目ID', + `reviewer_id` BIGINT NOT NULL COMMENT '评审人ID', + `review_type` TINYINT NOT NULL COMMENT '评审类型(1-初审,2-中期检查,3-结题验收)', + `score` DECIMAL(5,1) DEFAULT NULL COMMENT '评审分数(0-100)', + `opinion` TEXT COMMENT '评审意见', + `result` TINYINT DEFAULT NULL COMMENT '评审结果(1-通过,2-不通过,3-修改后通过)', + `status` TINYINT DEFAULT 1 COMMENT '状态(1-待评审,2-已评审)', + `review_time` DATETIME DEFAULT NULL COMMENT '评审时间', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_reviewer_id` (`reviewer_id`), + KEY `idx_review_type` (`review_type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审表'; +``` + +### 4.2 评审评分项表 (review_score_item) + +```sql +CREATE TABLE `review_score_item` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `review_id` BIGINT NOT NULL COMMENT '评审ID', + `item_name` VARCHAR(100) NOT NULL COMMENT '评分项名称', + `item_score` DECIMAL(5,1) NOT NULL COMMENT '该项分数', + `max_score` DECIMAL(5,1) NOT NULL COMMENT '该项满分', + `item_comment` VARCHAR(500) DEFAULT NULL COMMENT '该项评语', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_review_id` (`review_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审评分项表'; +``` + +--- + +## 五、成果相关表 + +### 5.1 成果表 (achievement) + +```sql +CREATE TABLE `achievement` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '成果ID', + `project_id` BIGINT NOT NULL COMMENT '项目ID', + `achievement_type` TINYINT NOT NULL COMMENT '成果类型(1-学术论文,2-发明专利,3-实用新型专利,4-软件著作权,5-竞赛获奖,6-创业实践,7-其他)', + `achievement_name` VARCHAR(200) NOT NULL COMMENT '成果名称', + `achievement_level` TINYINT DEFAULT NULL COMMENT '成果级别(1-校级,2-市级,3-省级,4-国家级,5-国际级)', + `author_names` VARCHAR(500) DEFAULT NULL COMMENT '作者/获奖人姓名', + `publish_time` DATE DEFAULT NULL COMMENT '发表/获得时间', + `publish_org` VARCHAR(200) DEFAULT NULL COMMENT '发表/颁发机构', + `description` TEXT COMMENT '成果描述', + `credit` DECIMAL(5,1) DEFAULT 0.0 COMMENT '认定学分', + `status` TINYINT DEFAULT 1 COMMENT '状态(1-待审核,2-已认证,3-审核不通过)', + `auditor_id` BIGINT DEFAULT NULL COMMENT '审核人ID', + `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间', + `audit_opinion` VARCHAR(500) DEFAULT NULL COMMENT '审核意见', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `create_by` BIGINT DEFAULT NULL COMMENT '创建人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_achievement_type` (`achievement_type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成果表'; +``` + +### 5.2 成果附件表 (achievement_attachment) + +```sql +CREATE TABLE `achievement_attachment` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID', + `achievement_id` BIGINT NOT NULL COMMENT '成果ID', + `file_name` VARCHAR(255) NOT NULL COMMENT '文件名', + `file_path` VARCHAR(500) NOT NULL COMMENT '文件路径', + `file_size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)', + `file_type` VARCHAR(50) DEFAULT NULL COMMENT '文件类型', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `create_by` BIGINT DEFAULT NULL COMMENT '上传人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_achievement_id` (`achievement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成果附件表'; +``` + +--- + +## 六、学分相关表 + +### 6.1 学分规则表 (credit_rule) + +```sql +CREATE TABLE `credit_rule` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `rule_name` VARCHAR(100) NOT NULL COMMENT '规则名称', + `rule_type` TINYINT NOT NULL COMMENT '规则类型(1-项目级别,2-成果类型)', + `target_type` TINYINT NOT NULL COMMENT '目标类型(与项目级别或成果类型对应)', + `target_level` TINYINT DEFAULT NULL COMMENT '目标级别(用于成果级别细分)', + `base_credit` DECIMAL(5,1) NOT NULL COMMENT '基础学分', + `leader_coefficient` DECIMAL(3,2) DEFAULT 1.00 COMMENT '负责人系数', + `member_coefficient` DECIMAL(3,2) DEFAULT 0.50 COMMENT '成员系数', + `description` VARCHAR(500) DEFAULT NULL COMMENT '规则说明', + `status` TINYINT DEFAULT 1 COMMENT '状态(0-禁用,1-启用)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `create_by` BIGINT DEFAULT NULL COMMENT '创建人', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_rule_type` (`rule_type`), + KEY `idx_target_type` (`target_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学分规则表'; +``` + +### 6.2 学分明细表 (credit_detail) + +```sql +CREATE TABLE `credit_detail` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '明细ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `project_id` BIGINT DEFAULT NULL COMMENT '项目ID', + `achievement_id` BIGINT DEFAULT NULL COMMENT '成果ID', + `credit_source` TINYINT NOT NULL COMMENT '学分来源(1-项目结题,2-成果认证)', + `credit` DECIMAL(5,1) NOT NULL COMMENT '获得学分', + `coefficient` DECIMAL(3,2) DEFAULT 1.00 COMMENT '分配系数', + `rule_id` BIGINT DEFAULT NULL COMMENT '适用规则ID', + `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', + `status` TINYINT DEFAULT 1 COMMENT '状态(1-正常,2-申诉中,3-已调整)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_achievement_id` (`achievement_id`), + KEY `idx_credit_source` (`credit_source`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学分明细表'; +``` + +### 6.3 学分申诉表 (credit_appeal) + +```sql +CREATE TABLE `credit_appeal` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '申诉ID', + `credit_detail_id` BIGINT NOT NULL COMMENT '学分明细ID', + `user_id` BIGINT NOT NULL COMMENT '申诉人ID', + `appeal_reason` TEXT NOT NULL COMMENT '申诉原因', + `appeal_evidence` VARCHAR(500) DEFAULT NULL COMMENT '申诉证据(附件路径)', + `status` TINYINT DEFAULT 1 COMMENT '状态(1-待处理,2-已通过,3-已驳回)', + `handler_id` BIGINT DEFAULT NULL COMMENT '处理人ID', + `handle_time` DATETIME DEFAULT NULL COMMENT '处理时间', + `handle_result` TEXT COMMENT '处理结果', + `adjusted_credit` DECIMAL(5,1) DEFAULT NULL COMMENT '调整后学分', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + KEY `idx_credit_detail_id` (`credit_detail_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学分申诉表'; +``` + +--- + +## 七、系统管理表 + +### 7.1 操作日志表 (sys_log) + +```sql +CREATE TABLE `sys_log` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `user_id` BIGINT DEFAULT NULL COMMENT '操作用户ID', + `username` VARCHAR(50) DEFAULT NULL COMMENT '操作用户名', + `operation` VARCHAR(100) NOT NULL COMMENT '操作名称', + `method` VARCHAR(200) DEFAULT NULL COMMENT '请求方法', + `params` TEXT COMMENT '请求参数', + `ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址', + `time` BIGINT DEFAULT NULL COMMENT '执行时长(毫秒)', + `result` TINYINT DEFAULT 1 COMMENT '执行结果(1-成功,0-失败)', + `error_msg` TEXT COMMENT '错误信息', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表'; +``` + +### 7.2 系统配置表 (sys_config) + +```sql +CREATE TABLE `sys_config` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '配置ID', + `config_key` VARCHAR(100) NOT NULL COMMENT '配置键', + `config_value` VARCHAR(500) NOT NULL COMMENT '配置值', + `config_name` VARCHAR(100) NOT NULL COMMENT '配置名称', + `description` VARCHAR(255) DEFAULT NULL COMMENT '配置说明', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标识', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_config_key` (`config_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表'; +``` + +--- + +## 八、数据库关系图(文字描述) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户子系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ sys_user (用户表) │ +│ │ │ +│ ├──1:1──> stu_info (学生信息表) │ +│ └──1:1──> teacher_info (教师信息表) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 项目子系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ project (项目表) │ +│ │ │ +│ ├──1:N──> project_member (项目成员表) │ +│ ├──1:N──> project_attachment (项目附件表) │ +│ ├──1:N──> review (评审表) │ +│ ├──1:N──> achievement (成果表) │ +│ └──1:N──> credit_detail (学分明细表) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 评审子系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ review (评审表) │ +│ │ │ +│ └──1:N──> review_score_item (评审评分项表) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 成果子系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ achievement (成果表) │ +│ │ │ +│ ├──1:N──> achievement_attachment (成果附件表) │ +│ └──1:1──> credit_detail (学分明细表) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 学分子系统 │ +├─────────────────────────────────────────────────────────────────┤ +│ credit_rule (学分规则表) │ +│ │ │ +│ └──1:N──> credit_detail (学分明细表) │ +│ │ │ +│ └──1:N──> credit_appeal (学分申诉表) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 九、初始化数据 + +### 9.1 学分规则初始化数据 + +```sql +-- 项目级别学分规则 +INSERT INTO credit_rule (rule_name, rule_type, target_type, base_credit, leader_coefficient, member_coefficient, description) VALUES +('校级项目', 1, 1, 1.0, 1.00, 0.50, '校级创新创业项目基础学分'), +('省级项目', 1, 2, 2.0, 1.00, 0.50, '省级创新创业项目基础学分'), +('国家级项目', 1, 3, 3.0, 1.00, 0.50, '国家级创新创业项目基础学分'); + +-- 成果类型学分规则 +INSERT INTO credit_rule (rule_name, rule_type, target_type, target_level, base_credit, description) VALUES +('学术论文-核心期刊', 2, 1, 3, 1.5, '发表核心期刊论文'), +('学术论文-SCI/EI', 2, 1, 4, 2.0, '发表SCI/EI论文'), +('发明专利', 2, 2, NULL, 2.0, '获得发明专利授权'), +('实用新型专利', 2, 3, NULL, 1.0, '获得实用新型专利授权'), +('软件著作权', 2, 4, NULL, 0.5, '获得软件著作权登记'), +('竞赛获奖-省级', 2, 5, 3, 1.0, '省级竞赛获奖'), +('竞赛获奖-国家级', 2, 5, 4, 2.0, '国家级竞赛获奖'); +``` + +### 9.2 管理员账号初始化 + +```sql +-- 默认管理员账号(密码: admin123,实际使用BCrypt加密) +INSERT INTO sys_user (username, password, real_name, gender, role_type, status) VALUES +('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', '系统管理员', 1, 3, 1); +``` + +--- + +*文档版本: v1.0* +*创建日期: 2026-03-01* +*作者: PMClaw* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e355f7 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Innovation Platform + +Innovation Platform is a Spring Boot based demo for university innovation and entrepreneurship project management. + +## What is included + +- Authentication endpoints powered by Sa-Token +- Project CRUD APIs with pagination +- Project dashboard statistics endpoint +- Static admin dashboard prototype at `backend/src/main/resources/static/index.html` +- Example SQL schema and seed data + +## Demo accounts + +- `admin / admin123` +- `teacher001 / admin123` +- `student001 / admin123` + +## Notable API routes + +- `POST /api/auth/login` +- `POST /api/auth/register` +- `POST /api/auth/logout` +- `GET /api/projects` +- `GET /api/projects/stats` +- `GET /api/projects/{id}` +- `POST /api/projects` +- `PUT /api/projects/{id}` +- `DELETE /api/projects/{id}` + +## Dashboard prototype + +When the backend is running, open `/index.html` to use the lightweight dashboard prototype. It can: + +- request a login token +- load project statistics +- query recent projects with keyword, type, level, and status filters +- visualize status and level breakdowns +- view leader and advisor names directly in project rows + +## Notes + +- This repository snapshot was recovered from an archive, so local build and runtime validation depend on the target machine having Java and Maven available. +- The current environment used for editing did not include a working Maven installation, so changes were verified statically only. +- The SQL bootstrap files were refreshed into a clean UTF-8 seed set and made idempotent with `ON DUPLICATE KEY UPDATE`. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..990dddb --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jdk-alpine +WORKDIR /app +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..dc28c09 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + + com.innovation + innovation-platform + 1.0.0 + innovation-platform + University innovation and entrepreneurship project management demo + + + 17 + 3.5.5 + 1.37.0 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + com.mysql + mysql-connector-j + runtime + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + org.projectlombok + lombok + true + + + cn.hutool + hutool-all + 5.8.25 + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.4.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/backend/src/main/java/com/innovation/platform/InnovationPlatformApplication.java b/backend/src/main/java/com/innovation/platform/InnovationPlatformApplication.java new file mode 100644 index 0000000..cdc86b9 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/InnovationPlatformApplication.java @@ -0,0 +1,16 @@ +package com.innovation.platform; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 高校创新创业项目孵化平台启动类 + */ +@SpringBootApplication +@MapperScan("com.innovation.platform.mapper") +public class InnovationPlatformApplication { + public static void main(String[] args) { + SpringApplication.run(InnovationPlatformApplication.class, args); + } +} diff --git a/backend/src/main/java/com/innovation/platform/common/PageResult.java b/backend/src/main/java/com/innovation/platform/common/PageResult.java new file mode 100644 index 0000000..e6b70dd --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/common/PageResult.java @@ -0,0 +1,28 @@ +package com.innovation.platform.common; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; +import java.io.Serializable; +import java.util.List; + +/** + * 分页结果 + */ +@Data +public class PageResult implements Serializable { + private List records; + private Long total; + private Long size; + private Long current; + private Long pages; + + public static PageResult of(IPage page) { + PageResult result = new PageResult<>(); + result.setRecords(page.getRecords()); + result.setTotal(page.getTotal()); + result.setSize(page.getSize()); + result.setCurrent(page.getCurrent()); + result.setPages(page.getPages()); + return result; + } +} diff --git a/backend/src/main/java/com/innovation/platform/common/Result.java b/backend/src/main/java/com/innovation/platform/common/Result.java new file mode 100644 index 0000000..eec3029 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/common/Result.java @@ -0,0 +1,51 @@ +package com.innovation.platform.common; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class Result implements Serializable { + private Integer code; + private String message; + private T data; + private Long timestamp; + + public Result() { + this.timestamp = System.currentTimeMillis(); + } + + public static Result success() { + Result result = new Result<>(); + result.setCode(200); + result.setMessage("Success"); + return result; + } + + public static Result success(T data) { + Result result = success(); + result.setData(data); + return result; + } + + public static Result success(String message, T data) { + Result result = success(); + result.setMessage(message); + result.setData(data); + return result; + } + + public static Result error(String message) { + Result result = new Result<>(); + result.setCode(500); + result.setMessage(message); + return result; + } + + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } +} diff --git a/backend/src/main/java/com/innovation/platform/config/SaTokenConfig.java b/backend/src/main/java/com/innovation/platform/config/SaTokenConfig.java new file mode 100644 index 0000000..594626d --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/config/SaTokenConfig.java @@ -0,0 +1,25 @@ +package com.innovation.platform.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Sa-Token 配置类 + */ +@Configuration +public class SaTokenConfig implements WebMvcConfigurer { + + /** + * 跨域配置 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/backend/src/main/java/com/innovation/platform/controller/AuthController.java b/backend/src/main/java/com/innovation/platform/controller/AuthController.java new file mode 100644 index 0000000..62a7420 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/controller/AuthController.java @@ -0,0 +1,44 @@ +package com.innovation.platform.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.innovation.platform.common.Result; +import com.innovation.platform.dto.LoginRequest; +import com.innovation.platform.dto.LoginResponse; +import com.innovation.platform.dto.RegisterRequest; +import com.innovation.platform.service.SysUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Authentication", description = "Login, registration, and logout endpoints") +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final SysUserService sysUserService; + + @SaIgnore + @Operation(summary = "User login") + @PostMapping("/login") + public Result login(@Valid @RequestBody LoginRequest request) { + return Result.success("Login succeeded.", sysUserService.login(request)); + } + + @SaIgnore + @Operation(summary = "User registration") + @PostMapping("/register") + public Result register(@Valid @RequestBody RegisterRequest request) { + sysUserService.register(request); + return Result.success("Registration succeeded.", null); + } + + @Operation(summary = "User logout") + @PostMapping("/logout") + public Result logout() { + sysUserService.logout(); + return Result.success("Logout succeeded.", null); + } +} diff --git a/backend/src/main/java/com/innovation/platform/controller/ProjectController.java b/backend/src/main/java/com/innovation/platform/controller/ProjectController.java new file mode 100644 index 0000000..e2d3ce8 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/controller/ProjectController.java @@ -0,0 +1,69 @@ +package com.innovation.platform.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.innovation.platform.common.PageResult; +import com.innovation.platform.common.Result; +import com.innovation.platform.dto.ProjectQueryRequest; +import com.innovation.platform.dto.ProjectRequest; +import com.innovation.platform.dto.ProjectResponse; +import com.innovation.platform.dto.ProjectStatsResponse; +import com.innovation.platform.service.ProjectService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Project management", description = "Project CRUD, filtering, and dashboard statistics") +@RestController +@RequestMapping("/api/projects") +@RequiredArgsConstructor +@SaCheckLogin +public class ProjectController { + + private final ProjectService projectService; + + @Operation(summary = "List projects with pagination and keyword search") + @GetMapping + public Result> page(ProjectQueryRequest request) { + IPage page = projectService.page(request); + return Result.success(PageResult.of(page)); + } + + @Operation(summary = "Get project dashboard statistics") + @GetMapping("/stats") + public Result stats() { + return Result.success(projectService.stats()); + } + + @Operation(summary = "Get project detail by id") + @GetMapping("/{id}") + public Result getById(@Parameter(description = "Project id") @PathVariable Long id) { + return Result.success(projectService.getById(id)); + } + + @Operation(summary = "Create a project") + @PostMapping + public Result create(@Valid @RequestBody ProjectRequest request) { + return Result.success("Project created successfully.", projectService.create(request)); + } + + @Operation(summary = "Update a project") + @PutMapping("/{id}") + public Result update( + @Parameter(description = "Project id") @PathVariable Long id, + @Valid @RequestBody ProjectRequest request + ) { + projectService.update(id, request); + return Result.success("Project updated successfully.", null); + } + + @Operation(summary = "Delete a project") + @DeleteMapping("/{id}") + public Result delete(@Parameter(description = "Project id") @PathVariable Long id) { + projectService.delete(id); + return Result.success("Project deleted successfully.", null); + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/LoginRequest.java b/backend/src/main/java/com/innovation/platform/dto/LoginRequest.java new file mode 100644 index 0000000..b038394 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/LoginRequest.java @@ -0,0 +1,13 @@ +package com.innovation.platform.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class LoginRequest { + @NotBlank(message = "Username is required.") + private String username; + + @NotBlank(message = "Password is required.") + private String password; +} diff --git a/backend/src/main/java/com/innovation/platform/dto/LoginResponse.java b/backend/src/main/java/com/innovation/platform/dto/LoginResponse.java new file mode 100644 index 0000000..e7b147a --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/LoginResponse.java @@ -0,0 +1,22 @@ +package com.innovation.platform.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录响应 DTO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginResponse { + private String token; + private Long userId; + private String username; + private String realName; + private String avatar; + private Integer roleType; +} diff --git a/backend/src/main/java/com/innovation/platform/dto/ProjectQueryRequest.java b/backend/src/main/java/com/innovation/platform/dto/ProjectQueryRequest.java new file mode 100644 index 0000000..6ccf190 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/ProjectQueryRequest.java @@ -0,0 +1,28 @@ +package com.innovation.platform.dto; + +import lombok.Data; + +@Data +public class ProjectQueryRequest { + private String keyword; + private String projectName; + private Integer projectType; + private Integer projectLevel; + private Integer status; + private Long leaderId; + private Long advisorId; + private String college; + private Integer current = 1; + private Integer size = 10; + + public Integer getCurrent() { + return current == null || current < 1 ? 1 : current; + } + + public Integer getSize() { + if (size == null || size < 1) { + return 10; + } + return Math.min(size, 100); + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/ProjectRequest.java b/backend/src/main/java/com/innovation/platform/dto/ProjectRequest.java new file mode 100644 index 0000000..2437859 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/ProjectRequest.java @@ -0,0 +1,90 @@ +package com.innovation.platform.dto; + +import com.innovation.platform.entity.Project; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ProjectRequest { + @NotBlank(message = "Project number is required.") + @Size(max = 30, message = "Project number must stay within 30 characters.") + private String projectNo; + + @NotBlank(message = "Project name is required.") + @Size(max = 200, message = "Project name must stay within 200 characters.") + private String projectName; + + @NotNull(message = "Project type is required.") + private Integer projectType; + + @NotNull(message = "Project level is required.") + private Integer projectLevel; + + @NotNull(message = "Leader id is required.") + private Long leaderId; + + @NotNull(message = "Advisor id is required.") + private Long advisorId; + + @Size(max = 4000, message = "Description must stay within 4000 characters.") + private String description; + + @Size(max = 4000, message = "Research plan must stay within 4000 characters.") + private String researchPlan; + + @Size(max = 4000, message = "Expected result must stay within 4000 characters.") + private String expectedResult; + + @Pattern( + regexp = "^$|^\\d+(\\.\\d{1,2})?$", + message = "Budget must be a number with up to two decimal places." + ) + private String budget; + + private Integer status; + private LocalDate startTime; + private LocalDate endTime; + + @Size(max = 100, message = "College must stay within 100 characters.") + private String college; + + @AssertTrue(message = "End date must be on or after the start date.") + public boolean isDateRangeValid() { + if (startTime == null || endTime == null) { + return true; + } + return !endTime.isBefore(startTime); + } + + public Project toEntity() { + Project project = new Project(); + project.setProjectNo(clean(projectNo)); + project.setProjectName(clean(projectName)); + project.setProjectType(projectType); + project.setProjectLevel(projectLevel); + project.setLeaderId(leaderId); + project.setAdvisorId(advisorId); + project.setDescription(clean(description)); + project.setResearchPlan(clean(researchPlan)); + project.setExpectedResult(clean(expectedResult)); + if (budget != null && !budget.isBlank()) { + project.setBudget(new BigDecimal(budget.trim())); + } + project.setStatus(status); + project.setStartTime(startTime); + project.setEndTime(endTime); + project.setCollege(clean(college)); + return project; + } + + private String clean(String value) { + return value == null ? null : value.trim(); + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/ProjectResponse.java b/backend/src/main/java/com/innovation/platform/dto/ProjectResponse.java new file mode 100644 index 0000000..40b16a0 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/ProjectResponse.java @@ -0,0 +1,116 @@ +package com.innovation.platform.dto; + +import com.innovation.platform.entity.Project; +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +@Data +@Builder +public class ProjectResponse { + private Long id; + private String projectNo; + private String projectName; + private Integer projectType; + private String projectTypeLabel; + private Integer projectLevel; + private String projectLevelLabel; + private Long leaderId; + private String leaderName; + private Long advisorId; + private String advisorName; + private String description; + private String researchPlan; + private String expectedResult; + private BigDecimal budget; + private Integer status; + private String statusLabel; + private LocalDate startTime; + private LocalDate endTime; + private Long durationDays; + private String college; + private LocalDateTime createTime; + private LocalDateTime updateTime; + + public static ProjectResponse fromEntity(Project project) { + return fromEntity(project, Map.of()); + } + + public static ProjectResponse fromEntity(Project project, Map userNames) { + return ProjectResponse.builder() + .id(project.getId()) + .projectNo(project.getProjectNo()) + .projectName(project.getProjectName()) + .projectType(project.getProjectType()) + .projectTypeLabel(projectTypeLabel(project.getProjectType())) + .projectLevel(project.getProjectLevel()) + .projectLevelLabel(projectLevelLabel(project.getProjectLevel())) + .leaderId(project.getLeaderId()) + .leaderName(userNames.get(project.getLeaderId())) + .advisorId(project.getAdvisorId()) + .advisorName(userNames.get(project.getAdvisorId())) + .description(project.getDescription()) + .researchPlan(project.getResearchPlan()) + .expectedResult(project.getExpectedResult()) + .budget(project.getBudget()) + .status(project.getStatus()) + .statusLabel(statusLabel(project.getStatus())) + .startTime(project.getStartTime()) + .endTime(project.getEndTime()) + .durationDays(durationDays(project.getStartTime(), project.getEndTime())) + .college(project.getCollege()) + .createTime(project.getCreateTime()) + .updateTime(project.getUpdateTime()) + .build(); + } + + private static String projectTypeLabel(Integer value) { + if (value == null) { + return "Unknown"; + } + return switch (value) { + case 1 -> "Research"; + case 2 -> "Startup"; + case 3 -> "Competition"; + default -> "Other"; + }; + } + + private static String projectLevelLabel(Integer value) { + if (value == null) { + return "Unknown"; + } + return switch (value) { + case 1 -> "School"; + case 2 -> "Provincial"; + case 3 -> "National"; + default -> "Other"; + }; + } + + private static String statusLabel(Integer value) { + if (value == null) { + return "Unknown"; + } + return switch (value) { + case 0 -> "Draft"; + case 1 -> "Pending review"; + case 2 -> "In progress"; + case 3 -> "Completed"; + case 4 -> "Rejected"; + default -> "Other"; + }; + } + + private static Long durationDays(LocalDate start, LocalDate end) { + if (start == null || end == null || end.isBefore(start)) { + return null; + } + return ChronoUnit.DAYS.between(start, end) + 1; + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/ProjectStatsResponse.java b/backend/src/main/java/com/innovation/platform/dto/ProjectStatsResponse.java new file mode 100644 index 0000000..217841b --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/ProjectStatsResponse.java @@ -0,0 +1,37 @@ +package com.innovation.platform.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProjectStatsResponse { + private long totalProjects; + private long draftProjects; + private long pendingProjects; + private long activeProjects; + private long completedProjects; + private long rejectedProjects; + private long nationalProjects; + private BigDecimal totalBudget; + private BigDecimal averageBudget; + private List statusBreakdown; + private List levelBreakdown; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Bucket { + private String key; + private String label; + private long value; + } +} diff --git a/backend/src/main/java/com/innovation/platform/dto/RegisterRequest.java b/backend/src/main/java/com/innovation/platform/dto/RegisterRequest.java new file mode 100644 index 0000000..3b121a2 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/dto/RegisterRequest.java @@ -0,0 +1,29 @@ +package com.innovation.platform.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Data +public class RegisterRequest { + @NotBlank(message = "Username is required.") + private String username; + + @NotBlank(message = "Password is required.") + private String password; + + @NotBlank(message = "Real name is required.") + private String realName; + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "Phone number format is invalid.") + private String phone; + + @Pattern( + regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + message = "Email format is invalid." + ) + private String email; + + private Integer gender; + private Integer roleType; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/Achievement.java b/backend/src/main/java/com/innovation/platform/entity/Achievement.java new file mode 100644 index 0000000..8bb9a47 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/Achievement.java @@ -0,0 +1,30 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 成果实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("achievement") +public class Achievement extends BaseEntity { + private Long projectId; + private Integer achievementType; + private String achievementName; + private Integer achievementLevel; + private String authorNames; + private LocalDate publishTime; + private String publishOrg; + private String description; + private BigDecimal credit; + private Integer status; + private Long auditorId; + private LocalDateTime auditTime; + private String auditOpinion; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/AchievementAttachment.java b/backend/src/main/java/com/innovation/platform/entity/AchievementAttachment.java new file mode 100644 index 0000000..a7449cf --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/AchievementAttachment.java @@ -0,0 +1,23 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 成果附件实体 + */ +@Data +@TableName("achievement_attachment") +public class AchievementAttachment implements Serializable { + private Long id; + private Long achievementId; + private String fileName; + private String filePath; + private Long fileSize; + private String fileType; + private LocalDateTime createTime; + private Long createBy; + private Integer deleted; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/BaseEntity.java b/backend/src/main/java/com/innovation/platform/entity/BaseEntity.java new file mode 100644 index 0000000..422ec62 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/BaseEntity.java @@ -0,0 +1,30 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础实体类 + */ +@Data +public class BaseEntity implements Serializable { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableField(fill = FieldFill.INSERT) + private Long createBy; + + @TableField(fill = FieldFill.UPDATE) + private Long updateBy; + + @TableLogic + private Integer deleted; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/Project.java b/backend/src/main/java/com/innovation/platform/entity/Project.java new file mode 100644 index 0000000..e4bb058 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/Project.java @@ -0,0 +1,30 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 项目实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("project") +public class Project extends BaseEntity { + private String projectNo; + private String projectName; + private Integer projectType; + private Integer projectLevel; + private Long leaderId; + private Long advisorId; + private String description; + private String researchPlan; + private String expectedResult; + private BigDecimal budget; + private Integer status; + private LocalDate startTime; + private LocalDate endTime; + private String college; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/ProjectAttachment.java b/backend/src/main/java/com/innovation/platform/entity/ProjectAttachment.java new file mode 100644 index 0000000..e4b4f06 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/ProjectAttachment.java @@ -0,0 +1,24 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 项目附件实体 + */ +@Data +@TableName("project_attachment") +public class ProjectAttachment implements Serializable { + private Long id; + private Long projectId; + private String fileName; + private String filePath; + private Long fileSize; + private String fileType; + private Integer attachmentType; + private LocalDateTime createTime; + private Long createBy; + private Integer deleted; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/ProjectMember.java b/backend/src/main/java/com/innovation/platform/entity/ProjectMember.java new file mode 100644 index 0000000..37f3044 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/ProjectMember.java @@ -0,0 +1,22 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 项目成员实体 + */ +@Data +@TableName("project_member") +public class ProjectMember implements Serializable { + private Long id; + private Long projectId; + private Long userId; + private Integer memberOrder; + private Integer role; + private LocalDateTime joinTime; + private LocalDateTime createTime; + private Integer deleted; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/Review.java b/backend/src/main/java/com/innovation/platform/entity/Review.java new file mode 100644 index 0000000..49a898a --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/Review.java @@ -0,0 +1,24 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 评审实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("review") +public class Review extends BaseEntity { + private Long projectId; + private Long reviewerId; + private Integer reviewType; + private BigDecimal score; + private String opinion; + private Integer result; + private Integer status; + private LocalDateTime reviewTime; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/ReviewScoreItem.java b/backend/src/main/java/com/innovation/platform/entity/ReviewScoreItem.java new file mode 100644 index 0000000..a795200 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/ReviewScoreItem.java @@ -0,0 +1,22 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 评审评分项实体 + */ +@Data +@TableName("review_score_item") +public class ReviewScoreItem implements Serializable { + private Long id; + private Long reviewId; + private String itemName; + private BigDecimal itemScore; + private BigDecimal maxScore; + private String itemComment; + private LocalDateTime createTime; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/StuInfo.java b/backend/src/main/java/com/innovation/platform/entity/StuInfo.java new file mode 100644 index 0000000..c161aeb --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/StuInfo.java @@ -0,0 +1,23 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.math.BigDecimal; + +/** + * 学生信息实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("stu_info") +public class StuInfo extends BaseEntity { + private Long userId; + private String studentNo; + private String college; + private String major; + private String grade; + private String className; + private Long advisorId; + private BigDecimal totalCredit; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/SysConfig.java b/backend/src/main/java/com/innovation/platform/entity/SysConfig.java new file mode 100644 index 0000000..25c4087 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/SysConfig.java @@ -0,0 +1,18 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统配置实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_config") +public class SysConfig extends BaseEntity { + private String configKey; + private String configValue; + private String configName; + private String description; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/SysLog.java b/backend/src/main/java/com/innovation/platform/entity/SysLog.java new file mode 100644 index 0000000..a64fbb4 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/SysLog.java @@ -0,0 +1,25 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 系统日志实体 + */ +@Data +@TableName("sys_log") +public class SysLog implements Serializable { + private Long id; + private Long userId; + private String username; + private String operation; + private String method; + private String params; + private String ip; + private Long time; + private Integer result; + private String errorMsg; + private LocalDateTime createTime; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/SysUser.java b/backend/src/main/java/com/innovation/platform/entity/SysUser.java new file mode 100644 index 0000000..dea753c --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/SysUser.java @@ -0,0 +1,23 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_user") +public class SysUser extends BaseEntity { + private String username; + private String password; + private String realName; + private Integer gender; + private String phone; + private String email; + private String avatar; + private Integer status; + private Integer roleType; +} diff --git a/backend/src/main/java/com/innovation/platform/entity/TeacherInfo.java b/backend/src/main/java/com/innovation/platform/entity/TeacherInfo.java new file mode 100644 index 0000000..2f7d518 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/entity/TeacherInfo.java @@ -0,0 +1,19 @@ +package com.innovation.platform.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 教师信息实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("teacher_info") +public class TeacherInfo extends BaseEntity { + private Long userId; + private String teacherNo; + private String college; + private String title; + private String researchField; +} diff --git a/backend/src/main/java/com/innovation/platform/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/innovation/platform/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ac2bd6e --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/exception/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.innovation.platform.exception; + +import com.innovation.platform.common.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(RuntimeException.class) + public Result handleRuntimeException(RuntimeException e) { + log.error("Business exception: {}", e.getMessage(), e); + return Result.error(400, e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("Validation failed: {}", message); + return Result.error(400, message); + } + + @ExceptionHandler(BindException.class) + public Result handleBindException(BindException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("Binding failed: {}", message); + return Result.error(400, message); + } + + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + log.error("Unexpected system exception: {}", e.getMessage(), e); + return Result.error(500, "Unexpected system error. Please try again later."); + } +} diff --git a/backend/src/main/java/com/innovation/platform/mapper/ProjectMapper.java b/backend/src/main/java/com/innovation/platform/mapper/ProjectMapper.java new file mode 100644 index 0000000..e10eb2c --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/mapper/ProjectMapper.java @@ -0,0 +1,12 @@ +package com.innovation.platform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.innovation.platform.entity.Project; +import org.apache.ibatis.annotations.Mapper; + +/** + * 项目 Mapper 接口 + */ +@Mapper +public interface ProjectMapper extends BaseMapper { +} diff --git a/backend/src/main/java/com/innovation/platform/mapper/SysUserMapper.java b/backend/src/main/java/com/innovation/platform/mapper/SysUserMapper.java new file mode 100644 index 0000000..e7040dd --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/mapper/SysUserMapper.java @@ -0,0 +1,12 @@ +package com.innovation.platform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.innovation.platform.entity.SysUser; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户 Mapper 接口 + */ +@Mapper +public interface SysUserMapper extends BaseMapper { +} diff --git a/backend/src/main/java/com/innovation/platform/service/ProjectService.java b/backend/src/main/java/com/innovation/platform/service/ProjectService.java new file mode 100644 index 0000000..e470bf0 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/service/ProjectService.java @@ -0,0 +1,25 @@ +package com.innovation.platform.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.innovation.platform.dto.ProjectQueryRequest; +import com.innovation.platform.dto.ProjectRequest; +import com.innovation.platform.dto.ProjectResponse; +import com.innovation.platform.dto.ProjectStatsResponse; +import com.innovation.platform.entity.Project; + +public interface ProjectService { + + IPage page(ProjectQueryRequest request); + + ProjectResponse getById(Long id); + + Long create(ProjectRequest request); + + void update(Long id, ProjectRequest request); + + void delete(Long id); + + Project getEntityById(Long id); + + ProjectStatsResponse stats(); +} diff --git a/backend/src/main/java/com/innovation/platform/service/SysUserService.java b/backend/src/main/java/com/innovation/platform/service/SysUserService.java new file mode 100644 index 0000000..ddc1bcf --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/service/SysUserService.java @@ -0,0 +1,37 @@ +package com.innovation.platform.service; + +import com.innovation.platform.dto.LoginRequest; +import com.innovation.platform.dto.LoginResponse; +import com.innovation.platform.dto.RegisterRequest; +import com.innovation.platform.entity.SysUser; + +/** + * 用户服务接口 + */ +public interface SysUserService { + + /** + * 用户登录 + */ + LoginResponse login(LoginRequest request); + + /** + * 用户注册 + */ + void register(RegisterRequest request); + + /** + * 用户登出 + */ + void logout(); + + /** + * 根据用户名查询用户 + */ + SysUser getByUsername(String username); + + /** + * 根据ID查询用户 + */ + SysUser getById(Long id); +} diff --git a/backend/src/main/java/com/innovation/platform/service/impl/ProjectServiceImpl.java b/backend/src/main/java/com/innovation/platform/service/impl/ProjectServiceImpl.java new file mode 100644 index 0000000..b74763f --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/service/impl/ProjectServiceImpl.java @@ -0,0 +1,194 @@ +package com.innovation.platform.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.innovation.platform.dto.ProjectQueryRequest; +import com.innovation.platform.dto.ProjectRequest; +import com.innovation.platform.dto.ProjectResponse; +import com.innovation.platform.dto.ProjectStatsResponse; +import com.innovation.platform.entity.Project; +import com.innovation.platform.entity.SysUser; +import com.innovation.platform.mapper.ProjectMapper; +import com.innovation.platform.mapper.SysUserMapper; +import com.innovation.platform.service.ProjectService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +@RequiredArgsConstructor +public class ProjectServiceImpl implements ProjectService { + + private final ProjectMapper projectMapper; + private final SysUserMapper sysUserMapper; + + @Override + public IPage page(ProjectQueryRequest request) { + Page page = new Page<>(request.getCurrent(), request.getSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + String keyword = clean(request.getKeyword()); + wrapper.and(StringUtils.hasText(keyword), nested -> nested + .like(Project::getProjectNo, keyword) + .or() + .like(Project::getProjectName, keyword) + .or() + .like(Project::getDescription, keyword) + .or() + .like(Project::getCollege, keyword)) + .like(StringUtils.hasText(request.getProjectName()), Project::getProjectName, clean(request.getProjectName())) + .eq(request.getProjectType() != null, Project::getProjectType, request.getProjectType()) + .eq(request.getProjectLevel() != null, Project::getProjectLevel, request.getProjectLevel()) + .eq(request.getStatus() != null, Project::getStatus, request.getStatus()) + .eq(request.getLeaderId() != null, Project::getLeaderId, request.getLeaderId()) + .eq(request.getAdvisorId() != null, Project::getAdvisorId, request.getAdvisorId()) + .eq(StringUtils.hasText(request.getCollege()), Project::getCollege, clean(request.getCollege())) + .orderByDesc(Project::getCreateTime); + + IPage projectPage = projectMapper.selectPage(page, wrapper); + Map userNames = loadUserNames(projectPage.getRecords()); + return projectPage.convert(project -> ProjectResponse.fromEntity(project, userNames)); + } + + @Override + public ProjectResponse getById(Long id) { + Project project = projectMapper.selectById(id); + if (project == null) { + throw new RuntimeException("Project does not exist."); + } + return ProjectResponse.fromEntity(project, loadUserNames(List.of(project))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ProjectRequest request) { + Project project = request.toEntity(); + project.setStatus(project.getStatus() != null ? project.getStatus() : 0); + + Long userId = StpUtil.getLoginIdAsLong(); + project.setCreateBy(userId); + project.setUpdateBy(userId); + + projectMapper.insert(project); + return project.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Long id, ProjectRequest request) { + Project existingProject = projectMapper.selectById(id); + if (existingProject == null) { + throw new RuntimeException("Project does not exist."); + } + + Project project = request.toEntity(); + project.setId(id); + + Long userId = StpUtil.getLoginIdAsLong(); + project.setUpdateBy(userId); + project.setCreateBy(existingProject.getCreateBy()); + project.setCreateTime(existingProject.getCreateTime()); + + projectMapper.updateById(project); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + Project project = projectMapper.selectById(id); + if (project == null) { + throw new RuntimeException("Project does not exist."); + } + projectMapper.deleteById(id); + } + + @Override + public Project getEntityById(Long id) { + return projectMapper.selectById(id); + } + + @Override + public ProjectStatsResponse stats() { + List projects = projectMapper.selectList(new LambdaQueryWrapper().orderByDesc(Project::getCreateTime)); + + long total = projects.size(); + long draft = projects.stream().filter(item -> statusEquals(item, 0)).count(); + long pending = projects.stream().filter(item -> statusEquals(item, 1)).count(); + long active = projects.stream().filter(item -> statusEquals(item, 2)).count(); + long completed = projects.stream().filter(item -> statusEquals(item, 3)).count(); + long rejected = projects.stream().filter(item -> statusEquals(item, 4)).count(); + long national = projects.stream().filter(item -> levelEquals(item, 3)).count(); + + BigDecimal totalBudget = projects.stream() + .map(Project::getBudget) + .filter(value -> value != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal averageBudget = total == 0 + ? BigDecimal.ZERO + : totalBudget.divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP); + + return ProjectStatsResponse.builder() + .totalProjects(total) + .draftProjects(draft) + .pendingProjects(pending) + .activeProjects(active) + .completedProjects(completed) + .rejectedProjects(rejected) + .nationalProjects(national) + .totalBudget(totalBudget) + .averageBudget(averageBudget) + .statusBreakdown(List.of( + ProjectStatsResponse.Bucket.builder().key("draft").label("Draft").value(draft).build(), + ProjectStatsResponse.Bucket.builder().key("pending").label("Pending review").value(pending).build(), + ProjectStatsResponse.Bucket.builder().key("active").label("In progress").value(active).build(), + ProjectStatsResponse.Bucket.builder().key("completed").label("Completed").value(completed).build(), + ProjectStatsResponse.Bucket.builder().key("rejected").label("Rejected").value(rejected).build() + )) + .levelBreakdown(List.of( + ProjectStatsResponse.Bucket.builder().key("school").label("School").value(projects.stream().filter(item -> levelEquals(item, 1)).count()).build(), + ProjectStatsResponse.Bucket.builder().key("provincial").label("Provincial").value(projects.stream().filter(item -> levelEquals(item, 2)).count()).build(), + ProjectStatsResponse.Bucket.builder().key("national").label("National").value(national).build() + )) + .build(); + } + + private boolean statusEquals(Project project, int status) { + return project.getStatus() != null && project.getStatus() == status; + } + + private boolean levelEquals(Project project, int level) { + return project.getProjectLevel() != null && project.getProjectLevel() == level; + } + + private String clean(String value) { + return value == null ? null : value.trim(); + } + + private Map loadUserNames(List projects) { + List userIds = projects.stream() + .flatMap(project -> Stream.of(project.getLeaderId(), project.getAdvisorId())) + .filter(Objects::nonNull) + .distinct() + .toList(); + + if (userIds.isEmpty()) { + return Collections.emptyMap(); + } + + return sysUserMapper.selectBatchIds(userIds).stream() + .collect(Collectors.toMap(SysUser::getId, SysUser::getRealName)); + } +} diff --git a/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java b/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..a031446 --- /dev/null +++ b/backend/src/main/java/com/innovation/platform/service/impl/SysUserServiceImpl.java @@ -0,0 +1,86 @@ +package com.innovation.platform.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.crypto.digest.BCrypt; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.innovation.platform.dto.LoginRequest; +import com.innovation.platform.dto.LoginResponse; +import com.innovation.platform.dto.RegisterRequest; +import com.innovation.platform.entity.SysUser; +import com.innovation.platform.mapper.SysUserMapper; +import com.innovation.platform.service.SysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SysUserServiceImpl implements SysUserService { + + private final SysUserMapper sysUserMapper; + + @Override + public LoginResponse login(LoginRequest request) { + SysUser user = getByUsername(request.getUsername()); + if (user == null) { + throw new RuntimeException("Invalid username or password."); + } + + if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) { + throw new RuntimeException("Invalid username or password."); + } + + if (user.getStatus() != null && user.getStatus() == 0) { + throw new RuntimeException("This account is disabled."); + } + + StpUtil.login(user.getId()); + + return LoginResponse.builder() + .token(StpUtil.getTokenValue()) + .userId(user.getId()) + .username(user.getUsername()) + .realName(user.getRealName()) + .avatar(user.getAvatar()) + .roleType(user.getRoleType()) + .build(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void register(RegisterRequest request) { + if (getByUsername(request.getUsername()) != null) { + throw new RuntimeException("Username already exists."); + } + + SysUser user = new SysUser(); + user.setUsername(request.getUsername()); + user.setPassword(BCrypt.hashpw(request.getPassword())); + user.setRealName(request.getRealName()); + user.setPhone(request.getPhone()); + user.setEmail(request.getEmail()); + user.setGender(request.getGender()); + user.setRoleType(request.getRoleType() != null ? request.getRoleType() : 0); + user.setStatus(1); + + sysUserMapper.insert(user); + } + + @Override + public void logout() { + StpUtil.logout(); + } + + @Override + public SysUser getByUsername(String username) { + return sysUserMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysUser::getUsername, username) + ); + } + + @Override + public SysUser getById(Long id) { + return sysUserMapper.selectById(id); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..da681fe --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 8080 + +spring: + datasource: + url: jdbc:mysql://mysql:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai + username: innovation + password: innovation123 + driver-class-name: com.mysql.cj.jdbc.Driver + sql: + init: + mode: always + schema-locations: classpath:schema.sql + data-locations: classpath:data.sql + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + configuration: + map-underscore-to-camel-case: true + +sa-token: + token-name: satoken + timeout: 86400 + active-timeout: -1 + is-concurrent: true + is-share: true + token-style: uuid + is-log: false diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql new file mode 100644 index 0000000..7f33310 --- /dev/null +++ b/backend/src/main/resources/data.sql @@ -0,0 +1,201 @@ +-- Seed users +INSERT INTO sys_user (id, username, password, real_name, gender, role_type, status) VALUES + (1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'System Admin', 1, 3, 1), + (2, 'teacher001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Alice Chen', 2, 2, 1), + (3, 'teacher002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Michael Zhao', 1, 2, 1), + (4, 'student001', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Olivia Lin', 2, 1, 1), + (5, 'student002', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Ethan Wu', 1, 1, 1), + (6, 'student003', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9hQIu', 'Sophia Xu', 2, 1, 1) +ON DUPLICATE KEY UPDATE + password = VALUES(password), + real_name = VALUES(real_name), + gender = VALUES(gender), + role_type = VALUES(role_type), + status = VALUES(status); + +-- Teacher profiles +INSERT INTO teacher_info (id, user_id, teacher_no, college, title, research_field) VALUES + (1, 2, 'T2024001', 'School of Computer Science', 'Associate Professor', 'Artificial Intelligence'), + (2, 3, 'T2024002', 'School of Management', 'Professor', 'Digital Entrepreneurship') +ON DUPLICATE KEY UPDATE + college = VALUES(college), + title = VALUES(title), + research_field = VALUES(research_field); + +-- Student profiles +INSERT INTO stu_info (id, user_id, student_no, college, major, grade, class_name, advisor_id) VALUES + (1, 4, 'S2021001', 'School of Computer Science', 'Software Engineering', '2021', 'SE2101', 2), + (2, 5, 'S2021002', 'School of Management', 'Business Analytics', '2021', 'BA2101', 3), + (3, 6, 'S2022001', 'School of Information Engineering', 'Data Science', '2022', 'DS2201', 2) +ON DUPLICATE KEY UPDATE + college = VALUES(college), + major = VALUES(major), + grade = VALUES(grade), + class_name = VALUES(class_name), + advisor_id = VALUES(advisor_id); + +-- Demo projects +INSERT INTO project ( + id, + project_no, + project_name, + project_type, + project_level, + leader_id, + advisor_id, + description, + research_plan, + expected_result, + budget, + status, + start_time, + end_time, + college, + create_by, + update_by +) VALUES + ( + 1, + 'PRJ2024-001', + 'Smart Campus Assistant', + 1, + 2, + 4, + 2, + 'Build a multimodal assistant for campus services and policy guidance.', + 'Deliver a knowledge base, chat workflow, and pilot deployment in the service center.', + 'Working prototype, evaluation report, and student operation handbook.', + 18000.00, + 2, + '2024-03-01', + '2024-12-31', + 'School of Computer Science', + 4, + 4 + ), + ( + 2, + 'PRJ2024-002', + 'Green Logistics Lab', + 2, + 1, + 5, + 3, + 'Design a campus scale reverse logistics service for reusable packaging.', + 'Run a semester pilot with collection points, route optimization, and merchant onboarding.', + 'Service blueprint, mini-program prototype, and impact dashboard.', + 9500.00, + 1, + '2024-04-15', + '2024-11-30', + 'School of Management', + 5, + 5 + ), + ( + 3, + 'PRJ2024-003', + 'Medical Image Triage Toolkit', + 1, + 3, + 6, + 2, + 'Explore lightweight triage assistance for common medical imaging scenarios.', + 'Prepare datasets, benchmark candidate models, and validate interpretability outputs.', + 'Research paper draft, benchmark scripts, and explainability report.', + 32000.00, + 3, + '2024-01-10', + '2025-01-15', + 'School of Information Engineering', + 6, + 6 + ), + ( + 4, + 'PRJ2024-004', + 'AR Heritage Guide', + 3, + 1, + 4, + 3, + 'Create an augmented reality campus heritage route for new students and visitors.', + 'Complete scene design, oral history collection, and route testing before launch.', + 'Interactive AR route, narration scripts, and event showcase materials.', + 12000.00, + 0, + '2024-09-01', + '2025-03-01', + 'School of Design', + 4, + 4 + ), + ( + 5, + 'PRJ2024-005', + 'Agri IoT Monitoring Network', + 1, + 2, + 5, + 2, + 'Prototype low-cost monitoring stations for greenhouse temperature and soil conditions.', + 'Build device kits, integrate dashboards, and verify long-run data stability.', + 'Sensor kit, field report, and maintenance SOP.', + 21000.00, + 4, + '2024-02-20', + '2024-10-20', + 'School of Agronomy', + 5, + 5 + ), + ( + 6, + 'PRJ2024-006', + 'Inclusive Career Coach', + 2, + 3, + 6, + 3, + 'Build an inclusive career guidance service for first-generation college students.', + 'Interview student cohorts, refine service flows, and launch a recommendation prototype.', + 'Coaching toolkit, matching engine prototype, and adoption report.', + 28500.00, + 2, + '2024-05-01', + '2025-02-28', + 'School of Public Affairs', + 6, + 6 + ) +ON DUPLICATE KEY UPDATE + project_name = VALUES(project_name), + project_type = VALUES(project_type), + project_level = VALUES(project_level), + leader_id = VALUES(leader_id), + advisor_id = VALUES(advisor_id), + description = VALUES(description), + research_plan = VALUES(research_plan), + expected_result = VALUES(expected_result), + budget = VALUES(budget), + status = VALUES(status), + start_time = VALUES(start_time), + end_time = VALUES(end_time), + college = VALUES(college), + update_by = VALUES(update_by); + +-- Project members +INSERT INTO project_member (id, project_id, user_id, member_order, role) VALUES + (1, 1, 4, 1, 2), + (2, 1, 5, 2, 1), + (3, 2, 5, 1, 2), + (4, 2, 6, 2, 1), + (5, 3, 6, 1, 2), + (6, 3, 4, 2, 1), + (7, 4, 4, 1, 2), + (8, 5, 5, 1, 2), + (9, 6, 6, 1, 2), + (10, 6, 5, 2, 1) +ON DUPLICATE KEY UPDATE + member_order = VALUES(member_order), + role = VALUES(role); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql new file mode 100644 index 0000000..96a1f82 --- /dev/null +++ b/backend/src/main/resources/schema.sql @@ -0,0 +1,133 @@ +-- Core user table +CREATE TABLE IF NOT EXISTS sys_user ( + id BIGINT NOT NULL AUTO_INCREMENT, + username VARCHAR(50) NOT NULL, + password VARCHAR(100) NOT NULL, + real_name VARCHAR(50) NOT NULL, + gender TINYINT DEFAULT 0, + phone VARCHAR(20) DEFAULT NULL, + email VARCHAR(100) DEFAULT NULL, + avatar VARCHAR(255) DEFAULT NULL, + status TINYINT DEFAULT 1, + role_type TINYINT NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + create_by BIGINT DEFAULT NULL, + update_by BIGINT DEFAULT NULL, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_username (username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Student profile table +CREATE TABLE IF NOT EXISTS stu_info ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + student_no VARCHAR(20) NOT NULL, + college VARCHAR(100) NOT NULL, + major VARCHAR(100) NOT NULL, + grade VARCHAR(10) NOT NULL, + class_name VARCHAR(50) DEFAULT NULL, + advisor_id BIGINT DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_student_no (student_no), + UNIQUE KEY uk_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Teacher profile table +CREATE TABLE IF NOT EXISTS teacher_info ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + teacher_no VARCHAR(20) NOT NULL, + college VARCHAR(100) NOT NULL, + title VARCHAR(50) DEFAULT NULL, + research_field VARCHAR(255) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_teacher_no (teacher_no), + UNIQUE KEY uk_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Project table +CREATE TABLE IF NOT EXISTS project ( + id BIGINT NOT NULL AUTO_INCREMENT, + project_no VARCHAR(30) NOT NULL, + project_name VARCHAR(200) NOT NULL, + project_type TINYINT NOT NULL, + project_level TINYINT NOT NULL, + leader_id BIGINT NOT NULL, + advisor_id BIGINT NOT NULL, + description TEXT, + research_plan TEXT, + expected_result TEXT, + budget DECIMAL(10,2) DEFAULT 0.00, + status TINYINT DEFAULT 1, + start_time DATE DEFAULT NULL, + end_time DATE DEFAULT NULL, + college VARCHAR(100) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + create_by BIGINT DEFAULT NULL, + update_by BIGINT DEFAULT NULL, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_project_no (project_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Project member table +CREATE TABLE IF NOT EXISTS project_member ( + id BIGINT NOT NULL AUTO_INCREMENT, + project_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + member_order INT DEFAULT 1, + role TINYINT DEFAULT 1, + join_time DATETIME DEFAULT CURRENT_TIMESTAMP, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY uk_project_user (project_id, user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Achievement table +CREATE TABLE IF NOT EXISTS achievement ( + id BIGINT NOT NULL AUTO_INCREMENT, + project_id BIGINT NOT NULL, + achievement_type TINYINT NOT NULL, + achievement_name VARCHAR(200) NOT NULL, + achievement_level TINYINT DEFAULT NULL, + author_names VARCHAR(500) DEFAULT NULL, + publish_time DATE DEFAULT NULL, + publish_org VARCHAR(200) DEFAULT NULL, + description TEXT, + status TINYINT DEFAULT 1, + auditor_id BIGINT DEFAULT NULL, + audit_time DATETIME DEFAULT NULL, + audit_opinion VARCHAR(500) DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + create_by BIGINT DEFAULT NULL, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Review table +CREATE TABLE IF NOT EXISTS review ( + id BIGINT NOT NULL AUTO_INCREMENT, + project_id BIGINT NOT NULL, + reviewer_id BIGINT NOT NULL, + review_type TINYINT NOT NULL, + score DECIMAL(5,1) DEFAULT NULL, + opinion TEXT, + result TINYINT DEFAULT NULL, + status TINYINT DEFAULT 1, + review_time DATETIME DEFAULT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted TINYINT DEFAULT 0, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html new file mode 100644 index 0000000..9a7ff4f --- /dev/null +++ b/backend/src/main/resources/static/index.html @@ -0,0 +1,562 @@ + + + + + + Innovation Platform Dashboard + + + +
+
+
+
+
Innovation Platform
+

Project dashboard for university innovation programs

+

This static page is served by Spring Boot and talks directly to the project APIs. It helps demo authentication, project search, and the new statistics endpoint without requiring a separate frontend build.

+
+
+
+ Use satoken header + Supports quick login + Reads stats and project pages +
+
+ Seed accounts share the bundled demo password: +
admin / admin123 +
teacher001 / admin123 +
student001 / admin123 +
+
+
+
+ +
+ + +
+
+
Overview
+

Program health snapshot

+
+
Total projects-
+
Draft-
+
Pending review-
+
In progress-
+
Completed-
+
Rejected-
+
+
+
National level-
+
Total budget-
+
Average budget-
+
+
+ +
+
Breakdown
+

Status and level distribution

+
+
+
+
+
+ +
+
Projects
+

Recent project records

+
+ + + + + + + + + + + + + + + + +
ProjectLeadTypeStatusLevelTimelineCollegeBudget
No data loaded yet.
+
+
+
+
+
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..617965d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' +services: + mysql: + image: mysql:8.0 + container_name: innovation-mysql + environment: + MYSQL_ROOT_PASSWORD: root123456 + MYSQL_DATABASE: innovation_platform + MYSQL_USER: innovation + MYSQL_PASSWORD: innovation123 + ports: + - "127.0.0.1:3307:3306" + volumes: + - mysql-data:/var/lib/mysql + networks: + - innovation-network + + backend: + build: ./backend + container_name: innovation-backend + depends_on: + - mysql + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/innovation_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai + SPRING_DATASOURCE_USERNAME: innovation + SPRING_DATASOURCE_PASSWORD: innovation123 + ports: + - "8080:8080" + networks: + - innovation-network + +networks: + innovation-network: + driver: bridge + +volumes: + mysql-data: