feat: refresh innovation platform demo

This commit is contained in:
Codex
2026-03-18 18:47:19 +08:00
commit 64e61454ab
44 changed files with 3165 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
backend/target/
.idea/
*.iml
.DS_Store

288
01-需求分析文档.md Normal file
View File

@@ -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*

477
02-数据库设计文档.md Normal file
View File

@@ -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*

45
README.md Normal file
View File

@@ -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`.

5
backend/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

92
backend/pom.xml Normal file
View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<groupId>com.innovation</groupId>
<artifactId>innovation-platform</artifactId>
<version>1.0.0</version>
<name>innovation-platform</name>
<description>University innovation and entrepreneurship project management demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<sa-token.version>1.37.0</sa-token.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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);
}
}

View File

@@ -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<T> implements Serializable {
private List<T> records;
private Long total;
private Long size;
private Long current;
private Long pages;
public static <T> PageResult<T> of(IPage<T> page) {
PageResult<T> 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;
}
}

View File

@@ -0,0 +1,51 @@
package com.innovation.platform.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result<T> implements Serializable {
private Integer code;
private String message;
private T data;
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("Success");
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = success();
result.setData(data);
return result;
}
public static <T> Result<T> success(String message, T data) {
Result<T> result = success();
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}

View File

@@ -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);
}
}

View File

@@ -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<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
return Result.success("Login succeeded.", sysUserService.login(request));
}
@SaIgnore
@Operation(summary = "User registration")
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody RegisterRequest request) {
sysUserService.register(request);
return Result.success("Registration succeeded.", null);
}
@Operation(summary = "User logout")
@PostMapping("/logout")
public Result<Void> logout() {
sysUserService.logout();
return Result.success("Logout succeeded.", null);
}
}

View File

@@ -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<PageResult<ProjectResponse>> page(ProjectQueryRequest request) {
IPage<ProjectResponse> page = projectService.page(request);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get project dashboard statistics")
@GetMapping("/stats")
public Result<ProjectStatsResponse> stats() {
return Result.success(projectService.stats());
}
@Operation(summary = "Get project detail by id")
@GetMapping("/{id}")
public Result<ProjectResponse> getById(@Parameter(description = "Project id") @PathVariable Long id) {
return Result.success(projectService.getById(id));
}
@Operation(summary = "Create a project")
@PostMapping
public Result<Long> create(@Valid @RequestBody ProjectRequest request) {
return Result.success("Project created successfully.", projectService.create(request));
}
@Operation(summary = "Update a project")
@PutMapping("/{id}")
public Result<Void> 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<Void> delete(@Parameter(description = "Project id") @PathVariable Long id) {
projectService.delete(id);
return Result.success("Project deleted successfully.", null);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<Long, String> 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;
}
}

View File

@@ -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<Bucket> statusBreakdown;
private List<Bucket> levelBreakdown;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Bucket {
private String key;
private String label;
private long value;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Void> handleRuntimeException(RuntimeException e) {
log.error("Business exception: {}", e.getMessage(), e);
return Result.error(400, e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> 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<Void> 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<Void> handleException(Exception e) {
log.error("Unexpected system exception: {}", e.getMessage(), e);
return Result.error(500, "Unexpected system error. Please try again later.");
}
}

View File

@@ -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<Project> {
}

View File

@@ -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<SysUser> {
}

View File

@@ -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<ProjectResponse> 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();
}

View File

@@ -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);
}

View File

@@ -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<ProjectResponse> page(ProjectQueryRequest request) {
Page<Project> page = new Page<>(request.getCurrent(), request.getSize());
LambdaQueryWrapper<Project> 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<Project> projectPage = projectMapper.selectPage(page, wrapper);
Map<Long, String> 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<Project> projects = projectMapper.selectList(new LambdaQueryWrapper<Project>().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<Long, String> loadUserNames(List<Project> projects) {
List<Long> 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));
}
}

View File

@@ -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<SysUser>()
.eq(SysUser::getUsername, username)
);
}
@Override
public SysUser getById(Long id) {
return sysUserMapper.selectById(id);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Innovation Platform Dashboard</title>
<style>
:root {
--bg: #f3f6fb;
--panel: rgba(255,255,255,0.94);
--line: #d9e2ec;
--text: #102033;
--muted: #5a6c81;
--brand: #1768c8;
--accent: #0f9d77;
--warn: #f08c2b;
--shadow: 0 18px 40px rgba(16, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
background:
radial-gradient(circle at top right, rgba(23, 104, 200, 0.14), transparent 28%),
radial-gradient(circle at bottom left, rgba(15, 157, 119, 0.12), transparent 24%),
var(--bg);
}
button, input, select { font: inherit; }
.shell { max-width: 1460px; margin: 0 auto; padding: 24px; }
.hero, .card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero { padding: 28px; margin-bottom: 18px; }
.eyebrow {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 800;
color: var(--brand);
}
h1, h2, h3 { margin: 10px 0 12px; }
p { margin: 0; color: var(--muted); line-height: 1.85; }
.hero-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 18px;
align-items: start;
}
.hero-actions, .inline-actions, .chip-list {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn, .btn-soft {
border: 0;
border-radius: 999px;
padding: 12px 18px;
cursor: pointer;
font-weight: 700;
}
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: #fff; }
.btn-soft { background: #eaf3ff; color: var(--brand); }
.layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
.card { padding: 22px; }
.field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}
label { font-size: 13px; font-weight: 700; color: #21364d; }
input, select {
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: transparent;
color: var(--text);
outline: none;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 16px;
}
.stat {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.5);
}
.stat span { display: block; font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.stat strong { font-size: 26px; }
.sub-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.list, .table-wrap {
border: 1px solid var(--line);
border-radius: 18px;
overflow: hidden;
background: rgba(255,255,255,0.5);
}
.list-item {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.list-item:last-child { border-bottom: 0; }
table { width: 100%; border-collapse: collapse; }
th, td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
th { background: #f7fbff; color: #29425a; font-size: 13px; }
td { color: var(--muted); }
.pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: #edf5ff;
color: var(--brand);
font-size: 12px;
font-weight: 700;
}
.notice {
margin-top: 14px;
padding: 14px;
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.55);
color: var(--muted);
line-height: 1.8;
}
.status {
margin-top: 14px;
padding: 14px;
border-radius: 16px;
border: 1px solid #d8e5f4;
background: #f7fbff;
color: #35506b;
line-height: 1.8;
min-height: 58px;
}
.status.error {
background: #fff3f3;
border-color: #f0d0d0;
color: #b94242;
}
.status.success {
background: #eefbf6;
border-color: #d3f0e5;
color: #177658;
}
@media (max-width: 1180px) {
.layout, .hero-grid, .sub-grid { grid-template-columns: 1fr; }
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.shell { padding: 14px; }
.field-grid, .stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell">
<section class="hero">
<div class="hero-grid">
<div>
<div class="eyebrow">Innovation Platform</div>
<h1>Project dashboard for university innovation programs</h1>
<p>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.</p>
</div>
<div>
<div class="chip-list">
<span class="pill">Use satoken header</span>
<span class="pill">Supports quick login</span>
<span class="pill">Reads stats and project pages</span>
</div>
<div class="notice">
Seed accounts share the bundled demo password:
<br />admin / admin123
<br />teacher001 / admin123
<br />student001 / admin123
</div>
</div>
</div>
</section>
<div class="layout">
<aside class="sidebar">
<section class="card">
<div class="eyebrow">Connection</div>
<h2>Login and token helper</h2>
<div class="field-grid">
<div class="field">
<label for="baseUrl">API base URL</label>
<input id="baseUrl" value="" placeholder="http://localhost:8080" />
</div>
<div class="field">
<label for="token">satoken header value</label>
<input id="token" value="" placeholder="Paste token here" />
</div>
<div class="field">
<label for="accountPreset">Demo account</label>
<select id="accountPreset">
<option value="admin">Admin</option>
<option value="teacher001">Teacher</option>
<option value="student001">Student</option>
</select>
</div>
<div class="field">
<label for="username">Username</label>
<input id="username" value="admin" />
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" type="password" value="admin123" />
</div>
</div>
<div class="inline-actions" style="margin-top: 14px;">
<button class="btn" type="button" id="loginBtn">Request token</button>
<button class="btn-soft" type="button" id="statsBtn">Load stats</button>
</div>
<div class="status" id="statusBox">Ready. Request a token or paste one manually, then load dashboard data.</div>
</section>
<section class="card">
<div class="eyebrow">Filters</div>
<h2>Project query</h2>
<div class="field-grid">
<div class="field">
<label for="keyword">Keyword</label>
<input id="keyword" placeholder="AI, campus, PRJ2024..." />
</div>
<div class="field">
<label for="projectType">Project type</label>
<select id="projectType">
<option value="">All</option>
<option value="1">Research</option>
<option value="2">Startup</option>
<option value="3">Competition</option>
</select>
</div>
<div class="field">
<label for="projectLevel">Project level</label>
<select id="projectLevel">
<option value="">All</option>
<option value="1">School</option>
<option value="2">Provincial</option>
<option value="3">National</option>
</select>
</div>
<div class="field">
<label for="status">Status</label>
<select id="status">
<option value="">All</option>
<option value="0">Draft</option>
<option value="1">Pending review</option>
<option value="2">In progress</option>
<option value="3">Completed</option>
<option value="4">Rejected</option>
</select>
</div>
</div>
<div class="inline-actions" style="margin-top: 14px;">
<button class="btn" type="button" id="projectsBtn">Load projects</button>
<button class="btn-soft" type="button" id="resetBtn">Reset filters</button>
</div>
</section>
</aside>
<main class="content">
<section class="card">
<div class="eyebrow">Overview</div>
<h2>Program health snapshot</h2>
<div class="stats">
<div class="stat"><span>Total projects</span><strong id="totalProjects">-</strong></div>
<div class="stat"><span>Draft</span><strong id="draftProjects">-</strong></div>
<div class="stat"><span>Pending review</span><strong id="pendingProjects">-</strong></div>
<div class="stat"><span>In progress</span><strong id="activeProjects">-</strong></div>
<div class="stat"><span>Completed</span><strong id="completedProjects">-</strong></div>
<div class="stat"><span>Rejected</span><strong id="rejectedProjects">-</strong></div>
</div>
<div class="stats">
<div class="stat"><span>National level</span><strong id="nationalProjects">-</strong></div>
<div class="stat"><span>Total budget</span><strong id="totalBudget">-</strong></div>
<div class="stat"><span>Average budget</span><strong id="averageBudget">-</strong></div>
</div>
</section>
<section class="card">
<div class="eyebrow">Breakdown</div>
<h2>Status and level distribution</h2>
<div class="sub-grid">
<div class="list" id="statusList"></div>
<div class="list" id="levelList"></div>
</div>
</section>
<section class="card">
<div class="eyebrow">Projects</div>
<h2>Recent project records</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Project</th>
<th>Lead</th>
<th>Type</th>
<th>Status</th>
<th>Level</th>
<th>Timeline</th>
<th>College</th>
<th>Budget</th>
</tr>
</thead>
<tbody id="projectTable">
<tr><td colspan="8">No data loaded yet.</td></tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
</div>
<script>
const refs = {
baseUrl: document.getElementById('baseUrl'),
token: document.getElementById('token'),
accountPreset: document.getElementById('accountPreset'),
username: document.getElementById('username'),
password: document.getElementById('password'),
keyword: document.getElementById('keyword'),
projectType: document.getElementById('projectType'),
projectLevel: document.getElementById('projectLevel'),
status: document.getElementById('status'),
statusBox: document.getElementById('statusBox'),
projectTable: document.getElementById('projectTable'),
statusList: document.getElementById('statusList'),
levelList: document.getElementById('levelList')
};
const statRefs = {
totalProjects: document.getElementById('totalProjects'),
draftProjects: document.getElementById('draftProjects'),
pendingProjects: document.getElementById('pendingProjects'),
activeProjects: document.getElementById('activeProjects'),
completedProjects: document.getElementById('completedProjects'),
rejectedProjects: document.getElementById('rejectedProjects'),
nationalProjects: document.getElementById('nationalProjects'),
totalBudget: document.getElementById('totalBudget'),
averageBudget: document.getElementById('averageBudget')
};
const accountPresets = {
admin: { username: 'admin', password: 'admin123' },
teacher001: { username: 'teacher001', password: 'admin123' },
student001: { username: 'student001', password: 'admin123' }
};
refs.baseUrl.value = window.location.origin || 'http://localhost:8080';
applyPresetAccount();
document.getElementById('loginBtn').addEventListener('click', login);
document.getElementById('statsBtn').addEventListener('click', loadStats);
document.getElementById('projectsBtn').addEventListener('click', loadProjects);
refs.accountPreset.addEventListener('change', applyPresetAccount);
document.getElementById('resetBtn').addEventListener('click', () => {
refs.keyword.value = '';
refs.projectType.value = '';
refs.projectLevel.value = '';
refs.status.value = '';
loadProjects();
});
async function login() {
setStatus('Requesting login token...');
try {
const response = await fetch(buildUrl('/api/auth/login'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: refs.username.value.trim(),
password: refs.password.value
})
});
const payload = await response.json();
if (payload.code !== 200) {
throw new Error(payload.message || 'Login failed.');
}
refs.token.value = payload.data.token || '';
setStatus('Login succeeded. Token stored in the satoken field.', 'success');
await loadStats();
await loadProjects();
} catch (error) {
setStatus(error.message, 'error');
}
}
async function loadStats() {
setStatus('Loading project statistics...');
try {
const payload = await apiFetch('/api/projects/stats');
const stats = payload.data;
statRefs.totalProjects.textContent = stats.totalProjects ?? '-';
statRefs.draftProjects.textContent = stats.draftProjects ?? '-';
statRefs.pendingProjects.textContent = stats.pendingProjects ?? '-';
statRefs.activeProjects.textContent = stats.activeProjects ?? '-';
statRefs.completedProjects.textContent = stats.completedProjects ?? '-';
statRefs.rejectedProjects.textContent = stats.rejectedProjects ?? '-';
statRefs.nationalProjects.textContent = stats.nationalProjects ?? '-';
statRefs.totalBudget.textContent = formatMoney(stats.totalBudget);
statRefs.averageBudget.textContent = formatMoney(stats.averageBudget);
renderBucketList(refs.statusList, stats.statusBreakdown || []);
renderBucketList(refs.levelList, stats.levelBreakdown || []);
setStatus('Statistics loaded successfully.', 'success');
} catch (error) {
setStatus(error.message, 'error');
}
}
async function loadProjects() {
setStatus('Loading project records...');
const params = new URLSearchParams();
if (refs.keyword.value.trim()) params.set('keyword', refs.keyword.value.trim());
if (refs.projectType.value) params.set('projectType', refs.projectType.value);
if (refs.projectLevel.value) params.set('projectLevel', refs.projectLevel.value);
if (refs.status.value) params.set('status', refs.status.value);
params.set('size', '8');
try {
const payload = await apiFetch('/api/projects?' + params.toString());
const page = payload.data || {};
const records = page.records || [];
if (!records.length) {
refs.projectTable.innerHTML = '<tr><td colspan="8">No matching projects found.</td></tr>';
} else {
refs.projectTable.innerHTML = records.map((item) => `
<tr>
<td>
<strong>${escapeHtml(item.projectName || 'Unnamed project')}</strong><br />
<span>${escapeHtml(item.projectNo || '-')}</span>
</td>
<td>
<strong>${escapeHtml(item.leaderName || 'Unassigned')}</strong><br />
<span>Advisor: ${escapeHtml(item.advisorName || '-')}</span>
</td>
<td>${escapeHtml(item.projectTypeLabel || '-')}</td>
<td>${escapeHtml(item.statusLabel || '-')}</td>
<td>${escapeHtml(item.projectLevelLabel || '-')}</td>
<td>${formatDuration(item.startTime, item.endTime, item.durationDays)}</td>
<td>${escapeHtml(item.college || '-')}</td>
<td>${formatMoney(item.budget)}</td>
</tr>
`).join('');
}
setStatus(`Loaded ${records.length} project record(s).`, 'success');
} catch (error) {
setStatus(error.message, 'error');
}
}
async function apiFetch(path) {
const headers = {};
const token = refs.token.value.trim();
if (token) {
headers.satoken = token;
}
const response = await fetch(buildUrl(path), { headers });
const payload = await response.json();
if (payload.code !== 200) {
throw new Error(payload.message || 'Request failed.');
}
return payload;
}
function renderBucketList(target, items) {
if (!items.length) {
target.innerHTML = '<div class="list-item"><span>No data</span><strong>-</strong></div>';
return;
}
target.innerHTML = items.map((item) => `
<div class="list-item">
<span>${escapeHtml(item.label || item.key || 'Item')}</span>
<strong>${item.value ?? 0}</strong>
</div>
`).join('');
}
function buildUrl(path) {
const base = refs.baseUrl.value.trim().replace(/\/$/, '');
return (base || 'http://localhost:8080') + path;
}
function formatMoney(value) {
if (value === null || value === undefined || value === '') {
return '-';
}
const num = Number(value);
if (Number.isNaN(num)) {
return escapeHtml(String(value));
}
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatDuration(start, end, durationDays) {
const parts = [];
if (start || end) {
parts.push(`${escapeHtml(start || '?')} to ${escapeHtml(end || '?')}`);
}
if (durationDays) {
parts.push(`${durationDays} day(s)`);
}
return parts.length ? parts.join('<br />') : '-';
}
function applyPresetAccount() {
const preset = accountPresets[refs.accountPreset.value] || accountPresets.admin;
refs.username.value = preset.username;
refs.password.value = preset.password;
}
function setStatus(message, tone) {
refs.statusBox.textContent = message;
refs.statusBox.className = 'status' + (tone ? ' ' + tone : '');
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
</script>
</body>
</html>

37
docker-compose.yml Normal file
View File

@@ -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: