forked from admin/innovation-platform
feat: refresh innovation platform demo
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
backend/target/
|
||||
.idea/
|
||||
*.iml
|
||||
.DS_Store
|
||||
288
01-需求分析文档.md
Normal file
288
01-需求分析文档.md
Normal 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
477
02-数据库设计文档.md
Normal 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
45
README.md
Normal 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
5
backend/Dockerfile
Normal 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
92
backend/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
28
backend/src/main/resources/application.yml
Normal file
28
backend/src/main/resources/application.yml
Normal 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
|
||||
201
backend/src/main/resources/data.sql
Normal file
201
backend/src/main/resources/data.sql
Normal 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);
|
||||
133
backend/src/main/resources/schema.sql
Normal file
133
backend/src/main/resources/schema.sql
Normal 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;
|
||||
562
backend/src/main/resources/static/index.html
Normal file
562
backend/src/main/resources/static/index.html
Normal 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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal 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:
|
||||
Reference in New Issue
Block a user