Compare commits

...

15 Commits

Author SHA1 Message Date
0fc9b4545c fix: 修复用户导入功能
1. 修改角色/岗位处理逻辑
2. 添加必填字段校验
2025-01-09 17:44:06 +08:00
296477bdb0 aaaa
Some checks failed
构建和部署Spring Boot应用程序 / build (push) Failing after 6m15s
构建和部署Spring Boot应用程序 / deploy (push) Has been skipped
2025-01-08 20:54:45 +08:00
a8f6a3864a csadasdas
Some checks failed
构建和部署Spring Boot应用程序 / build (push) Failing after 37s
构建和部署Spring Boot应用程序 / deploy (push) Has been skipped
2025-01-08 20:52:55 +08:00
2e21140921 ciii
Some checks failed
构建和部署Spring Boot应用程序 / build (push) Failing after 4m40s
构建和部署Spring Boot应用程序 / deploy (push) Has been skipped
2025-01-08 20:28:29 +08:00
ab6ed0b409 cii
Some checks failed
构建和部署Spring Boot应用程序 / build (push) Failing after 9s
构建和部署Spring Boot应用程序 / deploy (push) Has been skipped
2025-01-08 20:26:21 +08:00
6eb7e12030 ci
Some checks failed
Build and Deploy Spring Boot Application / deploy (push) Has been cancelled
Build and Deploy Spring Boot Application / build (push) Has been cancelled
2025-01-08 20:19:54 +08:00
44a25e65a5 测试
Some checks failed
自动部署 Spring Boot 项目到 CentOS 🚀 / deploy (push) Failing after 34s
2025-01-08 20:15:08 +08:00
0e395fef32 测试
Some checks failed
自动部署 Spring Boot 项目到 CentOS 🚀 / deploy (push) Failing after 24s
2025-01-08 20:06:20 +08:00
c207a202b8 测试Actions
Some checks failed
自动部署 Spring Boot 项目到 CentOS 🚀 / deploy (push) Failing after 1m2s
2025-01-08 18:30:42 +08:00
4f645c6e78 docs:删除文档 2025-01-06 16:15:16 +08:00
a19102f4d4 refactor: 整理代码 2025-01-05 19:51:32 +08:00
5c632a724f feat: 添加批量导入功能
1. 添加用户、课程任务和部门的批量导入功能
2. 优化错误处理逻辑,提供更详细的错误信息
3. 更新API文档,添加批量导入接口说明
2025-01-04 15:07:14 +08:00
7436928328 修改API文档Spring Security 相关字段 2025-01-03 16:30:25 +08:00
7f48021452 docs: 更新API文档
1. 添加搜索教师接口文档
2. 更新错误码说明,添加非教师用户(10002)错误码
2025-01-03 16:26:52 +08:00
3c40fca9b3 feat: 添加根据邮箱查询教师信息接口 2025-01-03 14:35:59 +08:00
17 changed files with 653 additions and 705 deletions

3
.gitignore vendored
View File

@ -31,3 +31,6 @@ build/
### VS Code ###
.vscode/
# 忽略 .DS_Store 文件
.DS_Store

View File

@ -1,499 +0,0 @@
# 进度管理系统 API 文档
## 基础信息
- 基础 URL: `http://localhost:1218`
- 所有请求和响应均使用 JSON 格式
- 所有需要认证的接口都需要在请求头中携带 `Authorization: Bearer {token}`
## 通用响应格式
```json
{
"code": 10000, // 响应码
"message": "成功", // 响应消息
"data": {} // 响应数据
}
```
### 响应码说明
| 响应码 | 说明 | 消息 |
| ------ | ---------- | ---------- |
| 10000 | 成功 | 成功 |
| 10001 | 参数错误 | 参数无效 |
| 10002 | 邮箱重复 | 邮箱已存在 |
| 10003 | 用户不存在 | 用户不存在 |
| 10004 | 密码错误 | 密码错误 |
| 10005 | 邮箱已存在 | 邮箱已存在 |
| 10006 | 未授权 | 未授权 |
| 10007 | 令牌过期 | 令牌已过期 |
| 10008 | 令牌无效 | 无效的令牌 |
| 10009 | 系统错误 | 系统错误 |
## 字段说明
### 用户相关字段
1. **角色 (roles)**
- 1: 教师
- 2: 普通管理员
- 3: 沟通联络人
- 4: 系统管理员
2. **岗位 (jobs)**
- 1: 课程制作教师
- 2: 课程购买方项目负责人
- 3: 课程制作方沟通联络人
- 4: 系统制作方项目负责人
3. **用户状态 (status)**
- 1: 正常
- 0: 禁用
### 课程任务相关字段
1. **进度状态 (progressStatus)**
- 0: 未开始
- 1: 脚本制作
- 2: 脚本审核
- 3: 脚本确认
- 4: 视频拍摄与制作
- 5: 视频确认
## 用户接口
### 1. 用户注册
- **接口**`POST /api/users`
- **描述**:创建新用户
- **认证**:不需要
- **请求体**
```json
{
"username": "testuser", // 用户名,不可为空
"email": "test@example.com", // 邮箱,不可为空且唯一
"password": "password123", // 密码,不可为空
"departmentId": 1, // 部门ID不可为空关联departments表
"roles": 1, // 角色1-教师2-普通管理员3-沟通联络人4-系统管理员
"jobs": 1, // 岗位1-课程制作教师2-课程购买方项目负责人3-课程制作方沟通联络人4-系统制作方项目负责人
"avatar": "http://example.com/avatar.jpg", // 头像URL可选
"creatorId": 1 // 创建者ID不可为空
}
```
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": null
}
```
- **错误响应**
```json
{
"code": 10005,
"message": "邮箱已存在",
"data": null
}
```
### 2. 用户登录
- **接口**`POST /api/users/login`
- **描述**:用户登录获取 token
- **认证**:不需要
- **请求体**
```json
{
"email": "test@example.com", // 邮箱
"password": "password123", // 密码
"remember": true // 是否记住登录(可选)
}
```
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"token": "eyJhbGciOiJIUzM4NCJ9..." // JWT令牌
}
}
```
### 3. 用户登出
- **接口**`POST /api/users/logout`
- **描述**:用户登出,使当前 token 失效
- **认证**:需要
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": "登出成功"
}
```
### 4. 获取当前用户信息
- **接口**`GET /api/users/current`
- **描述**:获取当前登录用户信息
- **认证**:需要
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"id": 12,
"username": "testuser",
"email": "test@example.com",
"departmentId": 1,
"roles": 1,
"jobs": 1,
"avatar": null,
"creatorId": 1,
"status": 1,
"createdAt": 1734578081,
"updatedAt": 1734578081,
"enabled": true,
"authorities": [
{
"authority": "ROLE_USER"
}
]
}
}
```
### 5. 获取用户列表
- **接口**`GET /api/users/list`
- **描述**:分页获取用户列表
- **认证**:需要
- **查询参数**
- `page`: 页码(从 1 开始)
- `limit`: 每页数量(默认 10
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"list": [
{
"id": 1,
"username": "admin",
"email": "admin@example.com",
"departmentId": 1,
"roles": 4,
"jobs": 4,
"avatar": null,
"creatorId": 1,
"status": 1,
"createdAt": 1734578081,
"updatedAt": 1734578081
}
],
"total": 12,
"currentPage": 1,
"pageSize": 10
}
}
```
### 6. 禁用用户
- **接口**`POST /api/users/disable/{userId}`
- **描述**:禁用指定用户
- **认证**:需要
- **路径参数**
- `userId`: 用户 ID
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": "用户已禁用"
}
```
### 7. 查询部门用户列表
- **接口**`GET /api/users/department/{departmentId}`
- **描述**:获取指定部门下的所有正常状态用户列表
- **认证**:需要
- **路径参数**
- `departmentId`: 部门 ID
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": [
{
"id": 2,
"username": "普通管理员账号2",
"email": "user2@qq.com",
"password": null,
"departmentId": 1,
"roles": 2,
"jobs": 2,
"avatar": null,
"creatorId": 1,
"status": 1,
"createdAt": 1734504506,
"updatedAt": 1734504506,
"enabled": true,
"authorities": [
{
"authority": "ROLE_USER"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true
}
]
}
```
- **错误响应**
```json
{
"code": 50000,
"message": "获取部门用户列表失败",
"data": null
}
```
## 课程任务接口
### 1. 获取课程任务列表
- **接口**`GET /api/lesson-tasks`
- **描述**:分页获取课程任务列表,可按用户筛选
- **认证**:需要
- **查询参数**
- `page`: 页码(从 1 开始)
- `size`: 每页数量(默认 10
- `userId`: 用户ID可选
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"content": [
{
"id": 1,
"courseName": "数学",
"microLessonName": "数学1-1",
"userId": 1,
"progressStatus": 4,
"scriptCreateTime": 1734940587,
"scriptReviewTime": null,
"scriptConfirmTime": 1734940825,
"videoCreateTime": 1734940832,
"videoConfirmTime": 1734940837,
"finishTime": 1734940837,
"advise": "",
"createdAt": 1734674726,
"updatedAt": 1734940837
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
}
},
"totalElements": 7,
"totalPages": 1,
"last": true,
"first": true,
"empty": false
}
}
```
### 2. 获取单个课程任务
- **接口**`GET /api/lesson-tasks/{id}`
- **描述**根据ID获取课程任务详情
- **认证**:需要
- **路径参数**
- `id`: 课程任务ID
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"id": 1,
"courseName": "数学",
"microLessonName": "数学1-1",
"userId": 1,
"progressStatus": 4,
"scriptCreateTime": 1734940587,
"scriptReviewTime": null,
"scriptConfirmTime": 1734940825,
"videoCreateTime": 1734940832,
"videoConfirmTime": 1734940837,
"finishTime": 1734940837,
"advise": "",
"createdAt": 1734674726,
"updatedAt": 1734940837
}
}
```
### 3. 创建课程任务
- **接口**`POST /api/lesson-tasks`
- **描述**:创建新的课程任务
- **认证**:需要
- **请求体**
```json
{
"courseName": "测试课程",
"microLessonName": "测试微课",
"userId": 1,
"advise": "任务说明"
}
```
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"id": 11,
"courseName": "测试课程",
"microLessonName": "测试微课",
"userId": 1,
"progressStatus": 0,
"scriptCreateTime": null,
"scriptReviewTime": null,
"scriptConfirmTime": null,
"videoCreateTime": null,
"videoConfirmTime": null,
"finishTime": null,
"advise": "任务说明",
"createdAt": 1735003870,
"updatedAt": 1735003870
}
}
```
### 4. 更新课程任务
- **接口**`PUT /api/lesson-tasks/{id}`
- **描述**:更新课程任务信息
- **认证**:需要
- **路径参数**
- `id`: 课程任务ID
- **请求体**
```json
{
"progressStatus": 1,
"advise": "更新的任务说明"
}
```
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"id": 11,
"courseName": "测试课程",
"microLessonName": "测试微课",
"userId": 1,
"progressStatus": 1,
"scriptCreateTime": 1735003922,
"scriptReviewTime": null,
"scriptConfirmTime": null,
"videoCreateTime": null,
"videoConfirmTime": null,
"finishTime": null,
"advise": "更新的任务说明",
"createdAt": 1735003870,
"updatedAt": 1735003922
}
}
```
### 5. 获取部门课程任务
- **接口**`GET /api/lesson-tasks/department/{departmentId}`
- **描述**:获取指定部门的课程任务列表
- **认证**:需要
- **路径参数**
- `departmentId`: 部门ID
- **查询参数**
- `page`: 页码(从 1 开始)
- `size`: 每页数量(默认 10
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": {
"content": [
{
"id": 1,
"courseName": "数学",
"microLessonName": "数学1-1",
"userId": 1,
"username": "教师账号1",
"progressStatus": 4,
"scriptCreateTime": 1734940587,
"scriptReviewTime": null,
"scriptConfirmTime": 1734940825,
"videoCreateTime": 1734940832,
"videoConfirmTime": 1734940837,
"finishTime": 1734940837,
"advise": "",
"createdAt": 1734674726,
"updatedAt": 1734940837
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10
},
"totalElements": 11,
"totalPages": 2
}
}
```
### 6. 删除课程任务
- **接口**`DELETE /api/lesson-tasks/{id}`
- **描述**:删除指定的课程任务
- **认证**:需要
- **路径参数**
- `id`: 课程任务ID
- **成功响应**
```json
{
"code": 10000,
"message": "成功",
"data": null
}
```
## 注意事项
1. 所有时间戳字段均使用秒级时间戳10位
2. 课程任务状态变更时会自动记录相应的时间戳
3. 部门课程任务列表会额外返回用户名信息
4. 分页参数中的页码从1开始

13
pom.xml
View File

@ -92,6 +92,19 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- Apache Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
<build>

View File

@ -1,14 +1,68 @@
package com.huertian.jinduguanli.common;
public class ErrorCode {
/**
* 操作成功
*/
public static final int SUCCESS = 10000;
/**
* 系统内部错误
*/
public static final int SYSTEM_ERROR = 10001;
/**
* 参数校验错误
*/
public static final int INVALID_PARAM = 10002;
/**
* 用户不存在
*/
public static final int USER_NOT_FOUND = 10003;
/**
* 用户不存在或已被禁用
*/
public static final int USER_NOT_FOUND_OR_DISABLED = 10004;
/**
* 邮箱已存在
*/
public static final int EMAIL_EXISTS = 10005;
public static final int PASSWORD_INCORRECT = 10006;
/**
* 业务逻辑错误
*/
public static final int BUSINESS_ERROR = 10006;
/**
* 无效的令牌
*/
public static final int INVALID_TOKEN = 10007;
/**
* 未授权访问
*/
public static final int UNAUTHORIZED = 10008;
/**
* 用户名已存在
*/
public static final int USERNAME_EXISTS = 10009;
/**
* 密码错误
*/
public static final int PASSWORD_INCORRECT = 10010;
/**
* 参数为空
*/
public static final int PARAM_IS_BLANK = 10011;
/**
* 参数错误
*/
public static final int PARAM_ERROR = 10012;
}

View File

@ -52,6 +52,7 @@ public class RedisConfig {
.build();
}
@SuppressWarnings("null")
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
@ -59,15 +60,15 @@ public class RedisConfig {
// 使用 GenericJackson2JsonRedisSerializer 进行序列化
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置 key 的序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 设置 value 的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
// 测试连接
@ -77,7 +78,7 @@ public class RedisConfig {
} catch (Exception e) {
logger.error("Redis连接测试失败: {}", e.getMessage(), e);
}
return template;
}
}

View File

@ -0,0 +1,134 @@
package com.huertian.jinduguanli.controller;
import com.alibaba.excel.EasyExcel;
import com.huertian.jinduguanli.common.ApiResponse;
import com.huertian.jinduguanli.common.ErrorCode;
import com.huertian.jinduguanli.dto.DepartmentImportDTO;
import com.huertian.jinduguanli.dto.LessonTaskImportDTO;
import com.huertian.jinduguanli.dto.UserImportDTO;
import com.huertian.jinduguanli.service.DepartmentService;
import com.huertian.jinduguanli.service.LessonTaskService;
import com.huertian.jinduguanli.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/import")
public class ImportController {
@Autowired
private UserService userService;
@Autowired
private LessonTaskService lessonTaskService;
@Autowired
private DepartmentService departmentService;
@PostMapping("/users")
public ApiResponse<String> importUsers(
@RequestParam("file") MultipartFile file,
@RequestHeader("Authorization") String authHeader) {
if (file.isEmpty()) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "文件不能为空", null);
}
String originalFilename = file.getOriginalFilename();
if (!isValidExcelFile(originalFilename)) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "只支持.xlsx格式的文件", null);
}
try {
log.info("开始导入用户数据,文件名:{}", originalFilename);
List<UserImportDTO> userList = EasyExcel.read(file.getInputStream())
.head(UserImportDTO.class)
.sheet()
.doReadSync();
log.info("读取到{}条用户数据", userList.size());
for (int i = 0; i < userList.size(); i++) {
log.info("第{}行数据:{}", i + 1, userList.get(i));
}
String token = authHeader.substring(7);
return userService.batchImportUsers(userList, token);
} catch (Exception e) {
log.error("导入用户数据失败", e);
return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "导入用户数据失败:" + e.getMessage(), null);
}
}
@PostMapping("/lesson-tasks")
public ApiResponse<String> importLessonTasks(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "文件不能为空", null);
}
String originalFilename = file.getOriginalFilename();
if (!isValidExcelFile(originalFilename)) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "只支持.xlsx格式的文件", null);
}
try {
log.info("开始导入课程任务数据,文件名:{}", originalFilename);
List<LessonTaskImportDTO> taskList = EasyExcel.read(file.getInputStream())
.head(LessonTaskImportDTO.class)
.sheet()
.doReadSync();
log.info("读取到{}条课程任务数据", taskList.size());
for (int i = 0; i < taskList.size(); i++) {
log.info("第{}行数据:{}", i + 1, taskList.get(i));
}
return lessonTaskService.batchImportLessonTasks(taskList);
} catch (Exception e) {
log.error("导入课程任务数据失败", e);
return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "导入课程任务数据失败:" + e.getMessage(), null);
}
}
@PostMapping("/departments")
public ApiResponse<String> importDepartments(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "文件不能为空", null);
}
String originalFilename = file.getOriginalFilename();
if (!isValidExcelFile(originalFilename)) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "只支持.xlsx格式的文件", null);
}
try {
log.info("开始导入部门数据,文件名:{}", originalFilename);
List<DepartmentImportDTO> departmentList = EasyExcel.read(file.getInputStream())
.head(DepartmentImportDTO.class)
.sheet()
.doReadSync();
log.info("读取到{}条部门数据", departmentList.size());
for (int i = 0; i < departmentList.size(); i++) {
log.info("第{}行数据:{}", i + 1, departmentList.get(i));
}
return departmentService.batchImportDepartments(departmentList);
} catch (Exception e) {
log.error("导入部门数据失败", e);
return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "导入部门数据失败:" + e.getMessage(), null);
}
}
private boolean isValidExcelFile(String filename) {
return StringUtils.isNotBlank(filename) && filename.toLowerCase().endsWith(".xlsx");
}
}

View File

@ -102,4 +102,15 @@ public class UserController {
return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "获取部门用户列表失败", null);
}
}
@GetMapping("/teacher/search")
public ApiResponse<User> searchTeacher(@RequestParam String email) {
logger.info("收到查询教师信息请求,邮箱: {}", email);
try {
return userService.findTeacherByEmail(email);
} catch (Exception e) {
logger.error("查询教师信息失败", e);
return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "查询教师信息失败", null);
}
}
}

View File

@ -0,0 +1,28 @@
package com.huertian.jinduguanli.dto;
import com.alibaba.excel.annotation.ExcelProperty;
public class DepartmentImportDTO {
@ExcelProperty("部门名称")
private String departmentName;
@ExcelProperty("部门描述")
private String description;
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,38 @@
package com.huertian.jinduguanli.dto;
import com.alibaba.excel.annotation.ExcelProperty;
public class LessonTaskImportDTO {
@ExcelProperty("课程名称")
private String lessonName;
@ExcelProperty("微课名称")
private String microLessonName;
@ExcelProperty("教师账号")
private String teacherEmail;
public String getLessonName() {
return lessonName;
}
public void setLessonName(String lessonName) {
this.lessonName = lessonName;
}
public String getMicroLessonName() {
return microLessonName;
}
public void setMicroLessonName(String microLessonName) {
this.microLessonName = microLessonName;
}
public String getTeacherEmail() {
return teacherEmail;
}
public void setTeacherEmail(String teacherEmail) {
this.teacherEmail = teacherEmail;
}
}

View File

@ -0,0 +1,60 @@
package com.huertian.jinduguanli.dto;
import com.alibaba.excel.annotation.ExcelProperty;
public class UserImportDTO {
@ExcelProperty(index = 0)
private String username;
@ExcelProperty(index = 1)
private String email;
@ExcelProperty(index = 2)
private String password;
@ExcelProperty(index = 3)
private String departmentName;
@ExcelProperty(index = 4)
private String roleAndJob;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
public String getRoleAndJob() {
return roleAndJob;
}
public void setRoleAndJob(String roleAndJob) {
this.roleAndJob = roleAndJob;
}
}

View File

@ -0,0 +1,22 @@
package com.huertian.jinduguanli.entity;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String description;
private Long createdAt;
private Long updatedAt;
}

View File

@ -0,0 +1,8 @@
package com.huertian.jinduguanli.repository;
import com.huertian.jinduguanli.entity.Department;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DepartmentRepository extends JpaRepository<Department, Long> {
Department findByName(String name);
}

View File

@ -66,6 +66,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
final String jwt = authHeader.substring(7);
logger.debug("Received JWT token: {}", jwt);
final String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

View File

@ -0,0 +1,76 @@
package com.huertian.jinduguanli.service;
import com.huertian.jinduguanli.common.ApiResponse;
import com.huertian.jinduguanli.common.ErrorCode;
import com.huertian.jinduguanli.dto.DepartmentImportDTO;
import com.huertian.jinduguanli.entity.Department;
import com.huertian.jinduguanli.repository.DepartmentRepository;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DepartmentService {
private static final Logger logger = LoggerFactory.getLogger(DepartmentService.class);
private final DepartmentRepository departmentRepository;
@Autowired
public DepartmentService(DepartmentRepository departmentRepository) {
this.departmentRepository = departmentRepository;
}
public ApiResponse<String> batchImportDepartments(List<DepartmentImportDTO> departmentList) {
if (departmentList == null || departmentList.isEmpty()) {
return new ApiResponse<>(ErrorCode.PARAM_ERROR, "导入数据不能为空", null);
}
StringBuilder errorMsg = new StringBuilder();
int successCount = 0;
for (int i = 0; i < departmentList.size(); i++) {
DepartmentImportDTO dto = departmentList.get(i);
try {
// 基本数据验证
if (StringUtils.isBlank(dto.getDepartmentName())) {
errorMsg.append(String.format("第%d行部门名称不能为空;", i + 2));
continue;
}
// 检查部门名称是否已存在
if (departmentRepository.findByName(dto.getDepartmentName()) != null) {
errorMsg.append(String.format("第%d行部门名称已存在;", i + 2));
continue;
}
// 创建部门
Department department = new Department();
department.setName(dto.getDepartmentName());
department.setDescription(dto.getDescription());
department.setCreatedAt(System.currentTimeMillis() / 1000);
department.setUpdatedAt(System.currentTimeMillis() / 1000);
departmentRepository.save(department);
successCount++;
} catch (Exception e) {
logger.error("导入第{}行数据失败", i + 2, e);
errorMsg.append(String.format("第%d行导入失败;", i + 2));
}
}
String resultMsg = String.format("成功导入%d条数据", successCount);
if (errorMsg.length() > 0) {
resultMsg += "。错误信息:" + errorMsg;
}
// 如果没有成功导入任何数据,返回错误状态
if (successCount == 0) {
return new ApiResponse<>(ErrorCode.PARAM_ERROR, resultMsg, null);
}
return ApiResponse.success(resultMsg);
}
}

View File

@ -1,8 +1,12 @@
package com.huertian.jinduguanli.service;
import com.huertian.jinduguanli.common.ApiResponse;
import com.huertian.jinduguanli.common.ErrorCode;
import com.huertian.jinduguanli.dto.LessonTaskDTO;
import com.huertian.jinduguanli.dto.LessonTaskImportDTO;
import com.huertian.jinduguanli.dto.LessonTaskRequest;
import com.huertian.jinduguanli.entity.LessonTask;
import com.huertian.jinduguanli.entity.User;
import com.huertian.jinduguanli.repository.LessonTaskRepository;
import jakarta.persistence.EntityNotFoundException;
import org.slf4j.Logger;
@ -15,13 +19,19 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.BeanUtils;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
@Service
public class LessonTaskService {
private static final Logger logger = LoggerFactory.getLogger(LessonTaskService.class);
private final LessonTaskRepository lessonTaskRepository;
private final UserService userService; // 添加 UserService 的依赖
public LessonTaskService(LessonTaskRepository lessonTaskRepository) {
public LessonTaskService(LessonTaskRepository lessonTaskRepository, UserService userService) {
this.lessonTaskRepository = lessonTaskRepository;
this.userService = userService;
}
@Cacheable(value = "lessonTasks", key = "#userId != null ? 'user:' + #userId + ':page:' + #pageable.pageNumber : 'all:page:' + #pageable.pageNumber")
@ -192,6 +202,74 @@ public class LessonTaskService {
return lessonTaskRepository.findByDepartmentIdAndUserStatus(departmentId, userStatus, pageable);
}
@Transactional
public ApiResponse<String> batchImportLessonTasks(List<LessonTaskImportDTO> taskList) {
if (taskList == null || taskList.isEmpty()) {
logger.error("导入任务列表为空");
return new ApiResponse<>(ErrorCode.PARAM_IS_BLANK, "导入任务列表为空", null);
}
StringBuilder errorMsg = new StringBuilder();
int successCount = 0;
for (int i = 0; i < taskList.size(); i++) {
LessonTaskImportDTO dto = taskList.get(i);
try {
// 基本数据验证
if (StringUtils.isBlank(dto.getLessonName())) {
errorMsg.append(String.format("第%d行课程名称不能为空;", i + 2));
continue;
}
if (StringUtils.isBlank(dto.getMicroLessonName())) {
errorMsg.append(String.format("第%d行微课名称不能为空;", i + 2));
continue;
}
if (StringUtils.isBlank(dto.getTeacherEmail())) {
errorMsg.append(String.format("第%d行教师账号不能为空;", i + 2));
continue;
}
// 查找教师用户ID
ApiResponse<User> teacherResponse = userService.findTeacherByEmail(dto.getTeacherEmail());
if (teacherResponse.getCode() != ErrorCode.SUCCESS) {
errorMsg.append(String.format("第%d行未找到教师用户%s;", i + 2, dto.getTeacherEmail()));
continue;
}
LessonTask task = new LessonTask();
task.setCourseName(dto.getLessonName());
task.setMicroLessonName(dto.getMicroLessonName());
task.setUserId(teacherResponse.getData().getId());
task.setProgressStatus(0); // 初始状态:未开始
task.setCreatedAt(System.currentTimeMillis() / 1000);
task.setUpdatedAt(System.currentTimeMillis() / 1000);
try {
lessonTaskRepository.save(task);
successCount++;
} catch (Exception e) {
logger.error("保存课程任务失败", e);
errorMsg.append(String.format("第%d行保存失败: %s;", i + 2, e.getMessage()));
}
} catch (Exception e) {
logger.error("导入第{}行数据失败", i + 2, e);
errorMsg.append(String.format("第%d行导入失败: %s;", i + 2, e.getMessage()));
}
}
String resultMsg = String.format("成功导入%d条数据", successCount);
if (errorMsg.length() > 0) {
resultMsg += "。错误信息:" + errorMsg;
}
// 如果没有成功导入任何数据,返回错误状态
if (successCount == 0) {
return new ApiResponse<>(ErrorCode.PARAM_ERROR, resultMsg, null);
}
return ApiResponse.success(resultMsg);
}
private void validateRequest(LessonTaskRequest request) {
if (request.getCourseName() == null || request.getCourseName().trim().isEmpty()) {
throw new IllegalArgumentException("课程名称不能为空");

View File

@ -2,13 +2,13 @@ package com.huertian.jinduguanli.service;
import com.huertian.jinduguanli.common.ApiResponse;
import com.huertian.jinduguanli.common.ErrorCode;
import com.huertian.jinduguanli.dto.CreateUserRequest;
import com.huertian.jinduguanli.dto.LoginResponse;
import com.huertian.jinduguanli.dto.UserLoginRequest;
import com.huertian.jinduguanli.dto.UserPageResponse;
import com.huertian.jinduguanli.dto.*;
import com.huertian.jinduguanli.entity.Department;
import com.huertian.jinduguanli.entity.User;
import com.huertian.jinduguanli.repository.DepartmentRepository;
import com.huertian.jinduguanli.repository.UserRepository;
import com.huertian.jinduguanli.security.service.JwtService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -28,13 +28,16 @@ public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final DepartmentRepository departmentRepository;
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
public UserService(JwtService jwtService, PasswordEncoder passwordEncoder, UserRepository userRepository) {
public UserService(JwtService jwtService, PasswordEncoder passwordEncoder, UserRepository userRepository,
DepartmentRepository departmentRepository) {
this.jwtService = jwtService;
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
this.departmentRepository = departmentRepository;
}
@Override
@ -77,7 +80,7 @@ public class UserService implements UserDetailsService {
user.setCreatorId(request.getCreatorId());
user.setStatus(1);
// 时间戳由 @PrePersist 处理,这里不需要手动设置
userRepository.save(user);
return ApiResponse.success();
@ -172,4 +175,115 @@ public class UserService implements UserDetailsService {
logger.info("查询部门用户列表部门ID: {}", departmentId);
return userRepository.findByDepartmentIdAndNormalStatus(departmentId);
}
public ApiResponse<User> findTeacherByEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "邮箱不能为空", null);
}
User user = userRepository.findByEmail(email).orElse(null);
if (user == null) {
return new ApiResponse<>(ErrorCode.USER_NOT_FOUND, "用户不存在", null);
}
if (user.getStatus() != 1) {
return new ApiResponse<>(ErrorCode.INVALID_PARAM, "用户状态异常", null);
}
return ApiResponse.success(user);
}
public ApiResponse<String> batchImportUsers(List<UserImportDTO> userList, String token) {
if (userList == null || userList.isEmpty()) {
return new ApiResponse<>(ErrorCode.PARAM_ERROR, "导入数据不能为空", null);
}
StringBuilder errorMsg = new StringBuilder();
int successCount = 0;
for (int i = 0; i < userList.size(); i++) {
UserImportDTO dto = userList.get(i);
try {
// 基本数据验证
if (StringUtils.isBlank(dto.getUsername()) ||
StringUtils.isBlank(dto.getEmail()) ||
StringUtils.isBlank(dto.getPassword()) ||
StringUtils.isBlank(dto.getDepartmentName()) ||
StringUtils.isBlank(dto.getRoleAndJob())) {
errorMsg.append(String.format("第%d行数据不完整;", i + 2));
continue;
}
// 检查邮箱是否已存在
if (userRepository.findByEmail(dto.getEmail()).isPresent()) {
errorMsg.append(String.format("第%d行邮箱已存在;", i + 2));
continue;
}
// 查找部门ID
Department department = departmentRepository.findByName(dto.getDepartmentName());
logger.info("查找部门:{},结果:{}", dto.getDepartmentName(), department);
if (department == null) {
errorMsg.append(String.format("第%d行部门不存在;", i + 2));
continue;
}
// 创建新用户
User user = new User();
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setDepartmentId(department.getId());
// 处理角色和岗位
String roleAndJob = dto.getRoleAndJob();
if (!StringUtils.isBlank(roleAndJob)) {
try {
user.setRoles(Integer.parseInt(roleAndJob));
user.setJobs(Integer.parseInt(roleAndJob));
} catch (NumberFormatException e) {
logger.warn("角色/岗位不是有效的数字: {}", roleAndJob);
errorMsg.append(String.format("第%d行角色/岗位不是有效的数字;", i + 2));
continue;
}
} else {
logger.warn("角色/岗位不能为空");
errorMsg.append(String.format("第%d行角色/岗位不能为空;", i + 2));
continue;
}
// 设置创建者ID
String email = jwtService.extractUsername(token);
Optional<User> creator = userRepository.findByEmail(email);
user.setCreatorId(creator.map(User::getId).orElse(0L));
user.setStatus(1); // 设置状态为正常
user.setCreatedAt(System.currentTimeMillis() / 1000);
user.setUpdatedAt(System.currentTimeMillis() / 1000);
try {
userRepository.save(user);
successCount++;
} catch (Exception e) {
logger.error("保存用户失败", e);
errorMsg.append(String.format("第%d行保存失败: %s;", i + 2, e.getMessage()));
}
} catch (Exception e) {
logger.error("导入第{}行数据失败", i + 2, e);
errorMsg.append(String.format("第%d行导入失败;", i + 2));
}
}
String resultMsg = String.format("成功导入%d条数据", successCount);
if (errorMsg.length() > 0) {
resultMsg += "。错误信息:" + errorMsg;
}
// 如果没有成功导入任何数据,返回错误状态
if (successCount == 0) {
return new ApiResponse<>(ErrorCode.PARAM_ERROR, resultMsg, null);
}
return ApiResponse.success(resultMsg);
}
}

View File

@ -1,194 +0,0 @@
# 数据库设计文档
## 1. 部门表 (departments)
用于存储组织的部门信息。
```sql
CREATE TABLE departments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL COMMENT '部门名称',
description TEXT DEFAULT NULL COMMENT '部门描述',
created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)',
updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='部门表';
```
### 字段说明
- `id`: 部门 ID自增主键
- `name`: 部门名称,不可为空
- `description`: 部门描述,可为空
- `created_at`: 创建时间,毫秒级时间戳
- `updated_at`: 更新时间,毫秒级时间戳
## 2. 用户表 (users)
存储系统用户信息,包括教师、管理员等角色。
```sql
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
password VARCHAR(100) NOT NULL COMMENT '密码',
department_id BIGINT NOT NULL COMMENT '所属部门',
roles INT NOT NULL COMMENT '角色: 1-教师, 2-普通管理员, 3-沟通联络人, 4-系统管理员',
jobs INT NOT NULL COMMENT '岗位: 1-课程制作教师, 2-课程购买方项目负责人, 3-课程制作方沟通联络人, 4-系统制作方项目负责人',
avatar VARCHAR(255) DEFAULT NULL COMMENT '头像',
creator_id BIGINT NOT NULL DEFAULT 1 COMMENT '创建用户的管理员ID',
status INT NOT NULL DEFAULT 1 COMMENT '用户状态: 1-正常, 0-禁用',
created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)',
updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)',
FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE RESTRICT,
INDEX idx_users_department_id (department_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='用户表';
```
### 字段说明
- `id`: 用户 ID自增主键
- `username`: 用户名,不可为空
- `email`: 邮箱地址,不可为空,唯一索引
- `password`: 密码(加密存储),不可为空
- `department_id`: 所属部门 ID外键关联 departments 表
- `roles`: 用户角色,整数枚举:
- 1: 教师
- 2: 普通管理员
- 3: 沟通联络人
- 4: 系统管理员
- `jobs`: 用户岗位,整数枚举:
- 1: 课程制作教师
- 2: 课程购买方项目负责人
- 3: 课程制作方沟通联络人
- 4: 系统制作方项目负责人
- `avatar`: 用户头像 URL可为空
- `creator_id`: 创建该用户的管理员 ID
- `status`: 用户状态:
- 1: 正常
- 0: 禁用
- `created_at`: 创建时间,毫秒级时间戳
- `updated_at`: 更新时间,毫秒级时间戳
### 索引
- 主键索引:`id`
- 外键索引:`idx_users_department_id (department_id)`
- 唯一索引:`email`
## 3. 课程任务表 (lesson_tasks)
存储课程制作任务的信息和进度。
```sql
CREATE TABLE lesson_tasks
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID系统自动生成的唯一标识',
course_name VARCHAR(100) NOT NULL COMMENT '所属课程名称,标识任务所属的主课程',
micro_lesson_name VARCHAR(100) NOT NULL COMMENT '具体微课名称,标识任务所属的具体微课单元',
user_id BIGINT NOT NULL COMMENT '任务负责人ID关联users表的主键id',
progress_status INT NOT NULL DEFAULT 0 COMMENT '任务进度状态:
0-未开始:任务创建后的初始状态
1-脚本制作:正在编写课程脚本
2-脚本审核:脚本提交审核阶段
3-脚本确认:脚本审核通过确认
4-视频拍摄与制作:进行视频录制和后期制作
5-视频确认:最终视频审核确认',
script_create_time BIGINT DEFAULT NULL COMMENT '脚本开始制作的时间戳状态1时记录',
script_review_time BIGINT DEFAULT NULL COMMENT '脚本提交审核的时间戳状态2时记录',
script_confirm_time BIGINT DEFAULT NULL COMMENT '脚本审核通过的时间戳状态3时记录',
video_create_time BIGINT DEFAULT NULL COMMENT '开始视频制作的时间戳状态4时记录',
video_confirm_time BIGINT DEFAULT NULL COMMENT '视频审核通过的时间戳状态5时记录',
finish_time BIGINT DEFAULT NULL COMMENT '整个任务完成的时间戳,最终确认后记录',
advise TEXT DEFAULT NULL COMMENT '任务相关的建议、修改意见或其他重要备注信息',
created_at BIGINT NOT NULL COMMENT '记录创建的时间戳,系统自动生成',
updated_at BIGINT NOT NULL COMMENT '记录最后更新的时间戳,系统自动更新',
-- 外键约束确保user_id关联到users表的有效用户
CONSTRAINT fk_lesson_tasks_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE,
-- 索引设计:
-- 1. 用户ID索引用于快速查找特定用户的所有任务
INDEX idx_user_id (user_id),
-- 2. 进度状态索引:用于按状态筛选和统计任务
INDEX idx_progress_status (progress_status),
-- 3. 课程名称索引:用于按课程分组和查询
INDEX idx_course_name (course_name),
-- 4. 复合索引:用于同时按用户和状态查询
INDEX idx_user_progress (user_id, progress_status)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COMMENT='课程任务管理表:记录微课制作的完整流程,包括脚本编写、审核、视频制作等各个环节的进度和时间节点';
```
### 字段说明
- `id`: 任务 ID自增主键
- `course_name`: 课程名称,不可为空
- `micro_lesson_name`: 微课名称,不可为空
- `user_id`: 负责人 ID外键关联 users 表
- `progress_status`: 任务进度状态:
- 0: 未开始
- 1: 脚本制作
- 2: 脚本审核
- 3: 脚本确认
- 4: 视频拍摄与制作
- 5: 视频确认
- `script_create_time`: 脚本开始制作的时间戳,状态 1 时记录
- `script_review_time`: 脚本提交审核的时间戳,状态 2 时记录
- `script_confirm_time`: 脚本审核通过的时间戳,状态 3 时记录
- `video_create_time`: 开始视频制作的时间戳,状态 4 时记录
- `video_confirm_time`: 视频审核通过的时间戳,状态 5 时记录
- `finish_time`: 整个任务完成的时间戳,最终确认后记录
- `advise`: 任务相关的建议、修改意见或其他重要备注信息
- `created_at`: 记录创建的时间戳,系统自动生成
- `updated_at`: 记录最后更新的时间戳,系统自动更新
### 索引
- 主键索引:`id`
- 外键索引:`idx_user_id (user_id)`
- 普通索引:`idx_progress_status (progress_status)`
- 普通索引:`idx_course_name (course_name)`
- 复合索引:`idx_user_progress (user_id, progress_status)`
## 数据库关系
1. `users.department_id` -> `departments.id`
- 一个部门可以有多个用户
- 一个用户只能属于一个部门
- 使用 RESTRICT 约束,防止删除仍有用户的部门
2. `lesson_tasks.user_id` -> `users.id`
- 一个用户可以负责多个课程任务
- 一个课程任务只能有一个负责人
- 使用 CASCADE 约束,删除用户时自动删除其负责的课程任务
3. `lesson_tasks``users` 表关系:
- 一对多关系一个用户users可以负责多个课程任务lesson_tasks
- 通过 `user_id` 外键关联,确保任务负责人的有效性
- 使用 CASCADE 级联删除:当用户被删除时,相关任务也会被自动删除
- 使用 CASCADE 级联更新:当用户 ID 更新时,相关任务的 user_id 也会自动更新
4. 索引说明:
- `idx_user_id`: 优化按负责人查询任务的性能
- `idx_progress_status`: 优化按任务状态筛选和统计的性能
- `idx_course_name`: 优化按课程名称查询和分组的性能
- `idx_user_progress`: 优化同时按用户和任务状态查询的性能,适用于查看特定用户的特定状态任务
5. 时间节点追踪:
- 系统通过各个时间戳字段script_create_time, script_review_time 等)完整记录任务的每个阶段
- 可以通过这些时间戳计算各阶段的耗时,用于任务进度分析和效率优化
## 注意事项
1. 所有时间戳字段使用 BIGINT 类型,存储毫秒级时间戳
2. 字符编码统一使用 utf8mb4支持完整的 Unicode 字符集
3. 所有表都使用 InnoDB 引擎,支持事务和外键
4. 关键字段都建立了适当的索引以提高查询性能
5. 用户密码在存储前需要进行加密处理
6. 删除用户时会自动删除其关联的课程任务,但不会影响部门数据