diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 410b3a1b61e3295adaef6c0ff86808aaa338bf5e..0000000000000000000000000000000000000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# exam-question - -#### Description -{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index 0d6bdd4be7245b51823ccb082cfb757b309d95ba..fbffa6f2d5ddd978fd2b6059e1fdcad3783c5eef 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,175 @@ -# exam-question +# 乐考刷 -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +## 介绍 +乐考刷在线考试刷题系统具备强大的功能集,包括但不限于租户管理、用户管理、部门管理、题库管理、题目管理、在线考试、随机出题、按知识点出题以及在线刷题等。通过灵活的配置和直观的界面,用户可以轻松创建和参与各种类型的测试活动。系统基于Spring Boot 3构建,采用微服务架构设计。该架构允许系统各部分独立开发、部署和扩展,提高了系统的灵活性和可维护性。通过使用Spring Cloud的相关组件,实现了服务发现、配置管理、负载均衡、熔断器等功能,确保了系统的高可用性和稳定性。 -#### 软件架构 -软件架构说明 +## 功能说明 +### 1. 租户管理 -#### 安装教程 +- 支持多租户架构,允许不同组织独立使用同一平台。 +- 租户可以自定义品牌化设置(如Logo、颜色主题)。 +- 每个租户拥有独立的数据存储空间,确保数据隔离。 +- 可复用于其他项目,便于快速部署新的业务场景。 -1. xxxx -2. xxxx -3. xxxx +### 2. 用户管理 -#### 使用说明 +- 用户角色划分(管理员、教师、学生等),权限控制精细。 +- 支持批量导入/导出用户信息,简化用户管理流程。 +- 提供用户活动日志记录,便于审计追踪。 +- 集成第三方认证服务,支持多种登录方式(如OAuth2, SSO)。 -1. xxxx -2. xxxx -3. xxxx +### 3. 部门管理 -#### 参与贡献 +- 组织结构树形展示,清晰呈现各部门层级关系。 +- 支持部门间转移人员,调整组织架构。 +- 自动同步部门变动至相关联的应用模块。 +- 同样适用于非考试类项目的组织管理需求。 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +### 4. 题库管理 +- 创建和分类管理各类题库,涵盖不同学科领域。 +- 题库可共享或私有化,根据需要设定访问权限。 +- 提供题库版本控制,方便更新维护。 -#### 特技 +### 5. 题目管理 -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +- 多种题型支持(选择题、填空题、简答题等)。 +- 图片、音频、视频等多媒体元素嵌入。 +- 题目难度分级,适应不同层次的学习者。 +- 题目解析与答案提示,帮助用户理解正确答案。 + +### 6. 在线考试 + +- 定时开考、限时作答,模拟真实考场环境。 +- 考试过程中防作弊机制(如摄像头监控、屏幕锁定)。 +- 实时统计考试进度,成绩即时反馈。 + +### 7. 随机出题 + +- 根据设定规则自动从题库中抽取题目组成试卷。 +- 支持指定题目数量、题型比例等参数。 + +### 8. 按知识点出题 + +- 精准定位知识点,构建有针对性的试题集合。 +- 有助于评估特定领域的知识掌握情况。 + +### 9. 在线刷题 + +- 个性化推荐练习题目,依据用户的答题历史和薄弱点。 +- 刷题记录保存,便于回顾复习。 +- 成绩跟踪与进步分析,激励持续学习。 + +## 模块设计 + +### 模块划分 + +乐考刷在线考试刷题系统包含系统服务、API网关、消息服务、题目服务、考试服务、刷题服务、日志服务等7个模块。 + +### 模块描述 + +#### API网关 + +API网关作为单一入口点来管理API调用之间的交互,能够将多个微服务整合在一起,为外部提供统一且简化的接口,其职责为: + +1. **统一入口点**:API网关作为所有外部请求进入系统的唯一通道,它可以有效地隐藏后端微服务的具体部署信息,保护内部架构不被外界直接访问。 + +2. **请求路由与负载均衡**:API网关可以根据预定义的规则将请求分发给正确的后端服务实例。这不仅包括基本的路径匹配,还可以基于内容类型、地理位置、用户身份等因素进行更复杂的路由决策。此外,API网关通常内置了负载均衡机制,能够在多个服务实例之间分配流量,确保即使某个实例出现故障,也不会影响整体的服务可用性。 + +3. **安全性增强**:为了保障API的安全调用,API网关实施了严格的访问控制措施,如IP黑白名单、认证鉴权、防重放攻击、防止恶意请求等。这些安全策略可以集中配置在API网关层面,避免了每个微服务都需要单独处理这些问题,大大降低了开发成本和潜在的风险。 + +4. **流量管理和限流**:API网关支持多种算法实现精细化的流量控制,例如固定窗口、滑动窗口、令牌桶等。通过灵活自定义的流量控制策略,API网关可以帮助应对突发的大流量冲击,保证API服务的稳定性和连续性。当检测到异常高的请求量时,API网关还可以自动触发限流措施,限制某些用户的请求频率,防止过载。 + +5. **监控与日志记录**:API网关是监控微服务运行状况的理想位置,因为它可以捕获所有的入站和出站流量。通过集成监控工具,API网关能够实时收集有关API性能、延迟、错误率等关键指标的数据,并生成详细的日志文件供后续分析使用。这对于快速定位问题根源、优化系统性能至关重要。 + +6. **版本管理和服务演进**:随着业务的发展和技术的进步,API可能会经历多次迭代更新。API网关允许开发者在同一URL下同时维护多个版本的API,确保旧版本的兼容性不受新版本的影响。同时,API网关也支持灰度发布功能,使得新的特性或改进可以在不影响现有用户的情况下逐步推广。 + +7. **静态响应处理**:对于一些不需要动态计算的结果,API网关可以直接返回预先准备好的静态响应,减少对后端服务的压力。这类场景特别适用于那些访问频率高但数据变化较少的资源,如网站的常见问题解答页面、API文档等。 + +#### 系统服务 + +系统服务主要包含管理维护租户、用户、部门、角色、权限、菜单等系统基础数据,其职责为: + +1. **用户管理**:管理用户信息,包括注册、登录、权限管理等,支持OAuth2和JWT进行身份验证。 +2. **部门管理**:维护组织结构,处理部门间的层级关系,支持部门人员的添加、删除和转移操作。 +3. **租户管理**:管理租户信息,支持多租户架构,实现租户级别的数据隔离,提供品牌化设置接口。 +4. **角色管理**:管理维护系统角色信息,包括角色数据权限管理、菜单权限管理等。 +5. **菜单管理**:维护菜单结构,处理菜单之间的层级管理,可修改、删除菜单图标、名称、对应路径等数据。 + +#### 消息服务 + +todo + +#### 题目服务 + +todo + +#### 考试服务 + +todo + +#### 刷题服务 + +todo + +#### 日志服务 + +todo + +## 数据库设计 + +### 数据库选型 + +乐考刷在线考试刷题系统使用MySQL数据库,原因如下: + +- MySQL是一种高性能的关系型数据库管理系统,具有优秀的读写速度。通过优化数据库结构,如范式化设计、选择合适的数据类型、添加索引等,可以进一步提高MySQL的读写性能。 +- MySQL的数据管理能力非常关键。通过数据库分区、分表等技术,MySQL可以更好地管理数据,提高查询效率。 +- MySQL具有良好的可扩展性,可以随着系统的增长而不断扩展。无论是通过增加硬件资源还是通过分布式架构,MySQL都能够应对不断增长的数据量和用户量。 +- 报警管理服务数据请求并发量较小,使用MySQL即可很好地存储和管理数据。 + +### 数据库结构 + +#### 表设计 + +系统用户信息表(sys_user)如下: + +| 字段名称 | 字段类型 | 长度 | 是否可为null | 描述 | 备注 | +| ----------- | --------- | ---- | ------------ | ------------ | ----------------------- | +| user_id | int | | 否 | 用户id,主键 | | +| username | varchar | 64 | 否 | 用户名称 | | +| nickname | varchar | 64 | 否 | 用户昵称 | | +| password | varchar | 512 | 否 | 密码 | | +| phone | varchar | 11 | 否 | 手机号 | | +| email | varchar | 64 | 是 | 邮箱 | | +| sex | tinyint | | 是 | 性别 | 0男,1女,3未知 | +| avatar | varchar | 512 | 是 | 头像 | | +| user_status | tinyint | | 否 | 用户状态 | 0可用,1禁用,默认为0 | +| remark | varchar | 512 | 是 | 备注 | | +| dept_id | int | | 否 | 部门id | | +| tenant_id | int | | 否 | 租户id | | +| create_time | timestamp | | 否 | 创建时间 | | +| creator_id | int | | 否 | 创建者id | | +| update_time | timestamp | | 是 | 更新时间 | | +| updater_id | int | | 是 | 更新者id | | +| deleted | tinyint | | 否 | 删除标志 | 0未删除,1删除,默认为0 | + + + +## 技术栈 + +- **前端**:Vue.js +- **后端**:SpringBoot 3.4.0 +- **数据库**:MySQL +- **缓存**:Redis +- **搜索**:Elasticsearch +- **部署**:Docker +- **API文档生成**:Swagger + +## 贡献代码 + +我们欢迎任何形式的贡献,无论是修复bug、优化性能还是新增特性。请参照[贡献指南](https://tongyi.aliyun.com/qianwen/CONTRIBUTING.md)了解如何参与项目开发。 + +## 版权声明 + +本项目遵循MIT许可证发布。更多信息,请参阅[LICENSE](https://tongyi.aliyun.com/qianwen/LICENSE)文件。 diff --git a/common-api/pom.xml b/common-api/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..bd78451932eac23056b012ae717b3c2722d8c4cf --- /dev/null +++ b/common-api/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + + + + + + common-api + 0.0.1-SNAPSHOT + common-api + common-api + + 17 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + org.projectlombok + lombok + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.3.7 + + + + + diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/api/ApiResult.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/api/ApiResult.java new file mode 100644 index 0000000000000000000000000000000000000000..c8c8f9d749a9870021022dc49b1e0f851a3da346 --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/api/ApiResult.java @@ -0,0 +1,85 @@ +package cn.edu.whut.commonapi.core.api; + + +import cn.edu.whut.commonapi.core.error.CommonErrorCodes; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + + +/** + * api接口返回信息封装 + */ +@Schema(description = "API接口返回信息封装") +@JsonInclude(JsonInclude.Include.NON_NULL) +@Data +public class ApiResult implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 状态码 + */ + @Schema(description = "状态码") + private Integer code; + + /** + * 提示信息 + */ + @Schema(description = "响应信息") + private String msg; + + /** + * 数据 + */ + @Schema(description = "数据") + private T data; + + public ApiResult(){ + + } + + public ApiResult(int code, String msg){ + this.code = code; + this.msg = msg; + } + + public ApiResult(int code, String msg, T data){ + this.code = code; + this.msg = msg; + this.data = data; + } + + public ApiResult(T data){ + this.code = CommonErrorCodes.SUCCESS.code(); + this.msg = CommonErrorCodes.SUCCESS.msg(); + this.data = data; + } + + public ApiResult(CommonErrorCodes errorCode, T data) { + this.code = errorCode.code(); + this.msg = errorCode.msg(); + this.data = data; + } + + public static ApiResult success(T data){ + return (ApiResult) new ApiResult(data); + } + + public static ApiResult success(){ + return (ApiResult) new ApiResult(CommonErrorCodes.SUCCESS.code(), CommonErrorCodes.SUCCESS.msg()); + } + + public static ApiResult error(int code, String msg){ + return new ApiResult<>(code, msg); + } + + public static ApiResult error(CommonErrorCodes errorCode){ + return new ApiResult<>(errorCode, null); + } + +} diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/BaseEntity.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/BaseEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..0e087d905ff4a19eb91d08a3c5bee445443a6fed --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/BaseEntity.java @@ -0,0 +1,47 @@ +package cn.edu.whut.commonapi.core.domain; + + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +@Data +@JsonIgnoreProperties(value = "transMap") +public class BaseEntity implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Timestamp createTime; + + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Timestamp updateTime; + + /** + * 创建者id + * + */ + @TableField(fill = FieldFill.INSERT) + private Integer creatorId; + + /** + * 更新者id + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Integer updaterId; + + /** + * 是否删除 + */ + @TableLogic + private Integer deleted; +} diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/PageEntity.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/PageEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..38821c6e2df6181ea654ec3152664fe57357db26 --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/domain/PageEntity.java @@ -0,0 +1,23 @@ +package cn.edu.whut.commonapi.core.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Data; + +@Data +@Schema(description = "分页参数 Request DTO") +public class PageEntity { + + /** 当前记录起始索引 */ + @Schema(description = "当前记录起始索引", example = "0", requiredMode = Schema.RequiredMode.REQUIRED) + @Min(value = 0, message = "当前记录起始索引不能小于0") + private Integer pageNum; + + /** 每页显示记录数 */ + @Schema(description = "每页显示记录数", example = "10", requiredMode = Schema.RequiredMode.REQUIRED) + @Min(value = 1, message = "每页显示记录数不能小于1") + @Max(value = 100, message = "每页显示记录数不能大于100") + private Integer pageSize; + +} diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonErrorCodes.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonErrorCodes.java new file mode 100644 index 0000000000000000000000000000000000000000..96f84b9b0abab141470eeea50ef390afe86aa8a6 --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonErrorCodes.java @@ -0,0 +1,39 @@ +package cn.edu.whut.commonapi.core.error; + +public enum CommonErrorCodes implements ErrorCode{ + + SUCCESS(0, "成功"), + + PARAM_ERROR(400, "参数错误"), + + NOT_FOUND(404, "未找到资源"), + + UNAUTHORIZED(401, "未授权"), + + FORBIDDEN(403, "禁止访问"), + + NOT_IMPLEMENTED(501, "未实现"), + + SERVICE_UNAVAILABLE(503, "服务不可用"), + + GATEWAY_TIMEOUT(504, "网关超时"); + + private int code; + + private String msg; + + CommonErrorCodes(int code, String msg){ + this.code = code; + this.msg = msg; + } + + @Override + public int code() { + return this.code; + } + + @Override + public String msg() { + return this.msg; + } +} diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonException.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonException.java new file mode 100644 index 0000000000000000000000000000000000000000..a502d8e8d72cf6e0378ccc6719560b75d7730a3d --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/CommonException.java @@ -0,0 +1,24 @@ +package cn.edu.whut.commonapi.core.error; + +import lombok.Getter; + +@Getter +public class CommonException extends RuntimeException{ + + private int code; + + public CommonException(String message, int code) { + super(message); + this.code = code; + } + + public CommonException(String message, Throwable cause, int code) { + super(message, cause); + this.code = code; + } + + public CommonException(Throwable cause, int code) { + super(cause); + this.code = code; + } +} diff --git a/common-api/src/main/java/cn/edu/whut/commonapi/core/error/ErrorCode.java b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/ErrorCode.java new file mode 100644 index 0000000000000000000000000000000000000000..0bad29bcab95919191d2a505e4506627281e4cdc --- /dev/null +++ b/common-api/src/main/java/cn/edu/whut/commonapi/core/error/ErrorCode.java @@ -0,0 +1,8 @@ +package cn.edu.whut.commonapi.core.error; + +public interface ErrorCode { + + int code(); + + String msg(); +} diff --git a/common-api/src/test/java/cn/edu/whut/commonapi/CommonApiApplicationTests.java b/common-api/src/test/java/cn/edu/whut/commonapi/CommonApiApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..83447b9b1c03d7a63beecce8631b3d3fa8e3b24a --- /dev/null +++ b/common-api/src/test/java/cn/edu/whut/commonapi/CommonApiApplicationTests.java @@ -0,0 +1,13 @@ +package cn.edu.whut.commonapi; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CommonApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..4be4e7e8ec13428091b8ff4c15f4ac22b2f9eb57 --- /dev/null +++ b/gateway-server/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + + + + gateway-server + 0.0.1-SNAPSHOT + gateway-server + gateway-server + + + 17 + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + cn.hutool + hutool-all + + + cn.edu.whut + common-api + 0.0.1-SNAPSHOT + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/GatewayServerApplication.java b/gateway-server/src/main/java/cn/edu/whut/gateway/GatewayServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..a61336de770928e810dc762dc18e5fae6261df77 --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/GatewayServerApplication.java @@ -0,0 +1,16 @@ +package cn.edu.whut.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + + +@SpringBootApplication +@EnableConfigurationProperties +public class GatewayServerApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayServerApplication.class, args); + } + +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/config/SecurityConfig.java b/gateway-server/src/main/java/cn/edu/whut/gateway/config/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2f957649fdc7433dab032305c0cec922499a56ae --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/config/SecurityConfig.java @@ -0,0 +1,26 @@ +package cn.edu.whut.gateway.config; + +import cn.edu.whut.gateway.config.properties.JwtConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; + +import java.security.KeyPair; + +@Configuration +@EnableConfigurationProperties(JwtConfigurationProperties.class) +public class SecurityConfig { + + @Bean + public KeyPair keyPair(JwtConfigurationProperties jwtConfigurationProperties){ + // 获取秘钥工厂 + KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( + jwtConfigurationProperties.getLocation(), + jwtConfigurationProperties.getPassword().toCharArray()); + return keyStoreKeyFactory.getKeyPair( + jwtConfigurationProperties.getAlias(), + jwtConfigurationProperties.getPassword().toCharArray() + ); + } +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/AuthConfigurationProperties.java b/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/AuthConfigurationProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..3eb4119003188b059c2f103de746973fc6f0f7ba --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/AuthConfigurationProperties.java @@ -0,0 +1,19 @@ +package cn.edu.whut.gateway.config.properties; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Data +@ConfigurationProperties(prefix = "gateway.auth") +@Configuration +public class AuthConfigurationProperties { + + private List includePaths; + + + private List excludePaths; +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/JwtConfigurationProperties.java b/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/JwtConfigurationProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..44cece71a22090e2a28b1bd558a7ed3c08559903 --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/config/properties/JwtConfigurationProperties.java @@ -0,0 +1,20 @@ +package cn.edu.whut.gateway.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +import java.time.Duration; + +@Data +@ConfigurationProperties(prefix = "gateway.jwt") +public class JwtConfigurationProperties { + + private Resource location; + + private String password; + + private String alias; + + private Duration tokenExpire = Duration.ofMinutes(10); +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/error/UnauthorizedException.java b/gateway-server/src/main/java/cn/edu/whut/gateway/error/UnauthorizedException.java new file mode 100644 index 0000000000000000000000000000000000000000..50a9237f5227b1309032454f0e4047c8ff5604c3 --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/error/UnauthorizedException.java @@ -0,0 +1,18 @@ +package cn.edu.whut.gateway.error; + +import cn.edu.whut.commonapi.core.error.CommonException; + +public class UnauthorizedException extends CommonException { + + public UnauthorizedException(String message) { + super(message, 401); + } + + public UnauthorizedException(String message, Throwable cause) { + super(message, cause, 401); + } + + public UnauthorizedException(Throwable cause) { + super(cause, 401); + } +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/filter/AuthGlobalFilter.java b/gateway-server/src/main/java/cn/edu/whut/gateway/filter/AuthGlobalFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..1e89069e3ebdeba6796bcb58bcaa6667f63b61da --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/filter/AuthGlobalFilter.java @@ -0,0 +1,30 @@ +package cn.edu.whut.gateway.filter; + +import cn.edu.whut.gateway.config.properties.AuthConfigurationProperties; +import cn.edu.whut.gateway.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +@EnableConfigurationProperties(AuthConfigurationProperties.class) +@RequiredArgsConstructor +public class AuthGlobalFilter implements GlobalFilter, Ordered { + + private final JwtUtil jwtUtil; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return null; + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/gateway-server/src/main/java/cn/edu/whut/gateway/util/JwtUtil.java b/gateway-server/src/main/java/cn/edu/whut/gateway/util/JwtUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e13cd38a16299fb412638c3f5e9f1ce0529186de --- /dev/null +++ b/gateway-server/src/main/java/cn/edu/whut/gateway/util/JwtUtil.java @@ -0,0 +1,75 @@ +package cn.edu.whut.gateway.util; + +import cn.edu.whut.gateway.error.UnauthorizedException; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTValidator; +import cn.hutool.jwt.signers.JWTSigner; +import cn.hutool.jwt.signers.JWTSignerUtil; +import org.springframework.stereotype.Component; + +import java.security.KeyPair; +import java.time.Duration; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class JwtUtil { + + private final JWTSigner jwtSigner; + + public JwtUtil(KeyPair keyPair){ + this.jwtSigner = JWTSignerUtil.createSigner("rs256", keyPair); + } + + /** + * 创建token + * + * @param userId 用户id + * @param tenantId 租户id + * @param ttl token过期时间 + * @return token + */ + public String createToken(Integer userId, Integer tenantId, Duration ttl){ + Map payloadData = new HashMap<>(); + payloadData.put("userId", userId); + payloadData.put("tenantId", tenantId); + return JWT.create() + .setPayload("user", payloadData) + .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis())) + .setSigner(jwtSigner) + .sign(); + } + + /** + * 解析token + * @param token token + * @return 用户id和租户id + */ + public Map parseToken(String token) { + if (token == null) { + throw new UnauthorizedException("用户未登录"); + } + JWT jwt; + try { + jwt = JWT.of(token).setSigner(jwtSigner); + } catch (Exception e) { + throw new UnauthorizedException("无效的token", e); + } + // 验证token是否有效 + if (!jwt.verify()) { + throw new UnauthorizedException("无效的token"); + } + try { + JWTValidator.of(jwt).validateDate(); + } catch (Exception e) { + throw new UnauthorizedException("token已过期"); + } + Object userPayloadData = jwt.getPayload("user"); + if (userPayloadData == null) { + throw new UnauthorizedException("无效的token"); + }else{ + return (Map) jwt.getPayload("user"); + } + } +} diff --git a/gateway-server/src/main/resources/application.yml b/gateway-server/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..97ed114ff01b0c408a5bc0b974b47d56b4bca82c --- /dev/null +++ b/gateway-server/src/main/resources/application.yml @@ -0,0 +1,24 @@ +spring: + application: + name: gateway-server + cloud: + nacos: + server-addr: 127.0.0.1:8848 + gateway: + routes: + - id: system-server + uri: lb://system-server + predicates: + - Path=/api/common/system/** + +server: + port: 8070 +gateway: + jwt: + alias: examquestion # 密钥别名 + location: classpath:examquestion.jks # 密钥库路径 + password: GYdx@2024.! # 密钥库密码 + tokenExpire: 30m # token过期时间 + auth: + exclude-paths: # 不需要认证的路径 + - /api/common/system/login diff --git a/gateway-server/src/main/resources/examquestion.jks b/gateway-server/src/main/resources/examquestion.jks new file mode 100644 index 0000000000000000000000000000000000000000..3492c78d6af4653f9140363ca9f5819eee3d6da0 Binary files /dev/null and b/gateway-server/src/main/resources/examquestion.jks differ diff --git a/gateway-server/src/test/java/cn/edu/whut/gateway/GatewayServerApplicationTests.java b/gateway-server/src/test/java/cn/edu/whut/gateway/GatewayServerApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..903c32d22a1c41b1c811dde5e697fb2b7002b5f8 --- /dev/null +++ b/gateway-server/src/test/java/cn/edu/whut/gateway/GatewayServerApplicationTests.java @@ -0,0 +1,13 @@ +package cn.edu.whut.gateway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GatewayServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0b15fe9e517021c9ee5bd737764a26a3c575534 --- /dev/null +++ b/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.7 + + + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + exam-question + 在线刷题和考试 + pom + + + 17 + 2023.0.3 + 2023.0.3.2 + 5.8.16 + 4.4.0 + 1.18.36 + 3.5.9 + 1.5.2 + 1.2.24 + + + + common-api + system-server + gateway-server + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + cn.hutool + hutool-all + ${hutool.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + com.baomidou + mybatis-plus-bom + ${mybatisplus.version} + pom + import + + + + com.github.yulichang + mybatis-plus-join-boot-starter + ${mybatisplusjoin.version} + + + + com.alibaba + druid-spring-boot-3-starter + ${druid.version} + + + + + + diff --git a/sql/sys_user.sql b/sql/sys_user.sql new file mode 100644 index 0000000000000000000000000000000000000000..fa0520e9ede2d57274d651192e3837901eac724e --- /dev/null +++ b/sql/sys_user.sql @@ -0,0 +1,20 @@ +CREATE TABLE sys_user ( + user_id INT NOT NULL AUTO_INCREMENT, -- 用户id,主键 + username VARCHAR(64) NOT NULL, -- 用户名称 + nickname VARCHAR(64) NOT NULL, -- 用户昵称 + password VARCHAR(512) NOT NULL, -- 密码 + phone VARCHAR(11) NOT NULL, -- 手机号 + email VARCHAR(64) DEFAULT NULL, -- 邮箱 + sex TINYINT DEFAULT NULL COMMENT '性别 0男,1女,3未知', -- 性别 + avatar VARCHAR(512) DEFAULT NULL, -- 头像 + user_status TINYINT NOT NULL DEFAULT 0 COMMENT '用户状态 0可用,1禁用,默认为0', -- 用户状态 + remark VARCHAR(512) DEFAULT NULL, -- 备注 + dept_id INT NOT NULL, -- 部门id + tenant_id INT NOT NULL, -- 租户id + create_time TIMESTAMP NOT NULL, -- 创建时间 + creator_id INT NOT NULL, -- 创建者id + update_time TIMESTAMP NULL, -- 更新时间 + updater_id INT DEFAULT NULL, -- 更新者id + deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标志 0未删除,1删除,默认为0', -- 删除标志 + PRIMARY KEY (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; \ No newline at end of file diff --git a/system-server/.gitignore b/system-server/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/system-server/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/system-server/pom.xml b/system-server/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9bb76afd6d4dd17e3443db7dc6f51bc286012ff9 --- /dev/null +++ b/system-server/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + + + + system-server + 0.0.1-SNAPSHOT + system-server + system-server + pom + + 17 + + + system-server-api + system-server-biz + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.3.7 + + + + + diff --git a/system-server/system-server-api/pom.xml b/system-server/system-server-api/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..68c9970fd002370f4b5c99f220e9a82948c4bdd1 --- /dev/null +++ b/system-server/system-server-api/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + + + + + + system-server-api + 0.0.1-SNAPSHOT + system-server-api + system-server-api + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.3.7 + + + + + diff --git a/system-server/system-server-api/src/test/java/cn/edu/whut/system/SystemServerApiApplicationTests.java b/system-server/system-server-api/src/test/java/cn/edu/whut/system/SystemServerApiApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..2e26f6442f0db7475a8d041f3b3a2e89363ed4d5 --- /dev/null +++ b/system-server/system-server-api/src/test/java/cn/edu/whut/system/SystemServerApiApplicationTests.java @@ -0,0 +1,13 @@ +package cn.edu.whut.system; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SystemServerApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/system-server/system-server-biz/pom.xml b/system-server/system-server-biz/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb3427292bacb01e8a928829f9d38c3772f6d113 --- /dev/null +++ b/system-server/system-server-biz/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + cn.edu.whut + exam-question + 0.0.1-SNAPSHOT + + + + + + system-server-biz + 0.0.1-SNAPSHOT + system-server-biz + system-server-biz + + 17 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.baomidou + mybatis-plus-jsqlparser + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + com.github.yulichang + mybatis-plus-join-boot-starter + + + com.alibaba + druid-spring-boot-3-starter + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + cn.hutool + hutool-all + + + + cn.edu.whut + common-api + 0.0.1-SNAPSHOT + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.3.7 + + + + + diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/SystemServerApplication.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/SystemServerApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..91e7cd993cdcee8f899e3b67fd354a00c30735ca --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/SystemServerApplication.java @@ -0,0 +1,16 @@ +package cn.edu.whut.system; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + +@SpringBootApplication(scanBasePackages = "cn.edu.whut.system") +public class SystemServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SystemServerApplication.class, args); + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/config/SecurityConfig.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/config/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..db121cf11618aeaafd992c900bcb745f9d8b0d9f --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/config/SecurityConfig.java @@ -0,0 +1,15 @@ +package cn.edu.whut.system.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysAuthController.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysAuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..20913d70db0ee509a6be992d866bed9e1172c233 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysAuthController.java @@ -0,0 +1,28 @@ +package cn.edu.whut.system.controller; + +import cn.edu.whut.commonapi.core.api.ApiResult; +import cn.edu.whut.system.vo.auth.SysAuthUserLoginReq; +import cn.edu.whut.system.vo.auth.SysAuthUserLoginRes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.edu.whut.commonapi.core.api.ApiResult.success; + + +@Tag(name = "系统服务 - 用户验证") +@RequestMapping("auth") +@RestController +@RequiredArgsConstructor +public class SysAuthController { + + @PostMapping("v1/login") + @Operation(summary = "用户登录") + public ApiResult login(@RequestBody SysAuthUserLoginReq sysAuthUserLoginReq){ + return success(); + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysUserController.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysUserController.java new file mode 100644 index 0000000000000000000000000000000000000000..8aa3796d49a0deac2db93db6e2f84112dcecdaae --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/controller/SysUserController.java @@ -0,0 +1,29 @@ +package cn.edu.whut.system.controller; + +import cn.edu.whut.commonapi.core.api.ApiResult; +import cn.edu.whut.system.service.ISysUserService; +import cn.edu.whut.system.vo.user.SysUserReq; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.edu.whut.commonapi.core.api.ApiResult.success; + +@Tag(name = "系统服务 - 用户管理") +@RequestMapping("user") +@RestController +@RequiredArgsConstructor +public class SysUserController { + + private final ISysUserService sysUserService; + + @PostMapping("v1/create") + @Operation(summary = "新增用户") + public ApiResult createUser(@RequestBody SysUserReq sysUserReq) { + return success(sysUserService.createUser(sysUserReq)); + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/mapper/SysUserMapper.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/mapper/SysUserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..11e5b5e401ede30376e591b6f8f81e31ec39e5d0 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/mapper/SysUserMapper.java @@ -0,0 +1,14 @@ +package cn.edu.whut.system.mapper; + +import cn.edu.whut.system.repository.SysUserRepository; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysUserMapper extends BaseMapper { + + default SysUserRepository findByUsername(String username){ + return selectOne(new LambdaQueryWrapper().eq(SysUserRepository::getUsername, username)); + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/repository/SysUserRepository.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/repository/SysUserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..42fc29ec66402cb0647ce4ee906f24daef7ea55c --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/repository/SysUserRepository.java @@ -0,0 +1,75 @@ +package cn.edu.whut.system.repository; + +import cn.edu.whut.commonapi.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@TableName("sys_user") +@EqualsAndHashCode(callSuper = true) +@Data +public class SysUserRepository extends BaseEntity { + + /** + * 用户id + */ + @TableId(value = "user_id", type = IdType.AUTO) + private Integer userId; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 密码 + */ + private String password; + + /** + * 手机号 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 性别 + */ + private Integer sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 状态 + */ + private Integer userStatus; + + /** + * 备注 + */ + private String remark; + + /** + * 部门 ID + */ + private Integer deptId; + + /** + * 租户 ID + */ + private Integer tenantId; +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/ISysUserService.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/ISysUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..a73a9cb0adde95974c6a2dcd6caf5c7db484315b --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/ISysUserService.java @@ -0,0 +1,16 @@ +package cn.edu.whut.system.service; + +import cn.edu.whut.system.repository.SysUserRepository; +import cn.edu.whut.system.vo.user.SysUserReq; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface ISysUserService extends IService { + + /** + * 创建用户 + * + * @param sysUserReq 用户信息 + * @return 创建的用户id + */ + Integer createUser(SysUserReq sysUserReq); +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/impl/SysUserServiceImpl.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..21b86a01667060d028a56670233dbc3e60529ac5 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,94 @@ +package cn.edu.whut.system.service.impl; + +import cn.edu.whut.system.mapper.SysUserMapper; +import cn.edu.whut.system.repository.SysUserRepository; +import cn.edu.whut.system.service.ISysUserService; +import cn.edu.whut.system.vo.user.SysUserReq; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; + +@Service +@RequiredArgsConstructor +public class SysUserServiceImpl extends ServiceImpl implements ISysUserService { + + private final PasswordEncoder passwordEncoder; + + /** + * 创建用户 + * + * @param sysUserReq 用户信息 + * @return 创建的用户id + */ + @Override + public Integer createUser(SysUserReq sysUserReq) { + SysUserRepository sysUserRepository = BeanUtil.toBean(sysUserReq, SysUserRepository.class); + validateUserForCreateOrUpdate(null, sysUserReq.getUsername()); + sysUserRepository.setCreateTime(new Timestamp(System.currentTimeMillis())); + sysUserRepository.setCreatorId(1); + sysUserRepository.setPassword(encodePassword(sysUserRepository.getPassword())); + baseMapper.insert(sysUserRepository); + return sysUserRepository.getUserId(); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + + /** + * 校验用户信息是否符合要求 + * + * @param userId 用户id + * @param username 用户名 + */ + private void validateUserForCreateOrUpdate(Integer userId, String username){ + validateUserExists(userId); + validateUsernameUnique(userId, username); + } + + /** + * 验证用户是否存在 + * + * @param userId 用户id + */ + private void validateUserExists(Integer userId) { + if(userId == null){ + return ; + } + SysUserRepository sysUserRepository = baseMapper.selectById(userId); + if (sysUserRepository == null) { + throw new RuntimeException("用户不存在"); + } + } + + /** + * 验证用户名是否唯一 + * + * @param userId 用户id + * @param username 用户名 + */ + private void validateUsernameUnique(Integer userId, String username){ + if(StrUtil.isBlank(username)){ + return; + } + SysUserRepository sysUserRepository = baseMapper.findByUsername(username); + if(sysUserRepository == null){ + return; + } + if(userId == null || !userId.equals(sysUserRepository.getUserId())){ + throw new RuntimeException("用户名已存在"); + } + } +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginReq.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginReq.java new file mode 100644 index 0000000000000000000000000000000000000000..5b0402021d5463542775ca53303f47aa8d5ed0f9 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginReq.java @@ -0,0 +1,23 @@ +package cn.edu.whut.system.vo.auth; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +@Schema(description = "系统服务 - 账号密码登录 Request VO") +public class SysAuthUserLoginReq { + + @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma") + @NotEmpty(message = "登录账号不能为空") + @Size(min = 4, max = 16, message = "账号长度为 4-16 位") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Size(min = 8, max = 16, message = "密码长度为 8-16 位") + private String password; +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginRes.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginRes.java new file mode 100644 index 0000000000000000000000000000000000000000..c4215485d989491bb27fbef9ecfee2ef85790206 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/auth/SysAuthUserLoginRes.java @@ -0,0 +1,37 @@ +package cn.edu.whut.system.vo.auth; + +import cn.edu.whut.system.vo.user.SysUserRes; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Schema(description = "系统服务 - 账号密码登录 Response VO") +@Data +public class SysAuthUserLoginRes implements Serializable { + + /** + * 访问令牌 + */ + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") + private String accessToken; + + /** + * 刷新令牌 + */ + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + /** + * 过期时间 + */ + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + + /** + * 用户信息 + */ + @Schema(description = "用户信息", requiredMode = Schema.RequiredMode.REQUIRED) + private SysUserRes user; +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserReq.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserReq.java new file mode 100644 index 0000000000000000000000000000000000000000..079a9bcf899a58b3417f50a6f422b783c6786b8f --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserReq.java @@ -0,0 +1,78 @@ +package cn.edu.whut.system.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "系统服务 - 用户信息Request VO") +public class SysUserReq { + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "admin", requiredMode = Schema.RequiredMode.REQUIRED) + private String username; + + /** + * 昵称 + */ + @Schema(description = "昵称", example = "管理员", requiredMode = Schema.RequiredMode.REQUIRED) + private String nickname; + + /** + * 密码 + */ + @Schema(description = "密码", example = "123456", requiredMode = Schema.RequiredMode.REQUIRED) + private String password; + + /** + * 手机号 + */ + @Schema(description = "手机号", example = "13888888888", requiredMode = Schema.RequiredMode.REQUIRED) + private String phone; + + /** + * 邮箱 + */ + @Schema(description = "邮箱", example = "admin@whut.edu.cn") + private String email; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1", allowableValues = {"0", "1", "2"}) + private Integer sex; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1", allowableValues = {"0", "1"}, requiredMode = Schema.RequiredMode.REQUIRED) + private Integer userStatus; + + /** + * 备注 + */ + @Schema(description = "备注", example = "管理员") + private String remark; + + /** + * 部门id + */ + @Schema(description = "部门id", example = "1") + private Integer deptId; + + /** + * 租户id + */ + @Schema(description = "租户id", example = "1", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer tenantId; + + /** + * 角色id列表 + */ + @Schema(description = "角色id列表", example = "[1,2]") + private List roleIdList; + +} diff --git a/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserRes.java b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserRes.java new file mode 100644 index 0000000000000000000000000000000000000000..f85ac5af8c5d3a832fe60f5022f1c90fa8ee67f2 --- /dev/null +++ b/system-server/system-server-biz/src/main/java/cn/edu/whut/system/vo/user/SysUserRes.java @@ -0,0 +1,64 @@ +package cn.edu.whut.system.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "系统服务 - 用户信息Response VO") +public class SysUserRes { + + /** + * 用户id + */ + @Schema(description = "用户id", example = "1") + private Integer userId; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "admin") + private String username; + + /** + * 昵称 + */ + @Schema(description = "昵称", example = "管理员") + private String nickname; + + /** + * 手机号 + */ + @Schema(description = "手机号", example = "13888888888") + private String phone; + + /** + * 邮箱 + */ + @Schema(description = "邮箱", example = "admin@whut.edu.cn") + private String email; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1", allowableValues = {"0", "1", "2"}) + private Integer sex; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1", allowableValues = {"0", "1"}) + private Integer userStatus; + + /** + * 备注 + */ + @Schema(description = "备注", example = "管理员") + private String remark; + + /** + * 租户id + */ + @Schema(description = "租户id", example = "1") + private Integer tenantId; + +} diff --git a/system-server/system-server-biz/src/main/resources/application-loc.yml b/system-server/system-server-biz/src/main/resources/application-loc.yml new file mode 100644 index 0000000000000000000000000000000000000000..4ba6d733b9d3c7fde472c4b9ed8ab72752796749 --- /dev/null +++ b/system-server/system-server-biz/src/main/resources/application-loc.yml @@ -0,0 +1,16 @@ +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3310/system_database?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8 + username: root + password: 123456 + data: + redis: + host: localhost + port: 6379 + password: + cloud: + nacos: + discovery: + server-addr: localhost:8848 diff --git a/system-server/system-server-biz/src/main/resources/application.yml b/system-server/system-server-biz/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..59b6cbaed0fcdbf3e4fc527b1659ea446e2c534f --- /dev/null +++ b/system-server/system-server-biz/src/main/resources/application.yml @@ -0,0 +1,73 @@ +spring: + application: + name: system-server + + profiles: + active: loc + + jackson: + serialization: + write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + +server: + port: 8081 + servlet: + context-path: /api/common/system + +mybatis-plus: + global-config: + db-config: + id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: cn.edu.whut.system.*.mapper + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath:mapper/*Mapper.xml + +mybatis-plus-join: + banner: true # 是否打印 mybatis plus join banner,默认true + sub-table-logic: true # 全局启用副表逻辑删除,默认true。关闭后关联查询不会加副表逻辑删除 + ms-cache: true # 拦截器MappedStatement缓存,默认 true + table-alias: t # 表别名(默认 t) + logic-del-type: on # 副表逻辑删除条件的位置,支持 WHERE、ON,默认 ON + +springdoc: + swagger-ui: + path: /swagger-ui.html + # path: 配置swagger-ui.html/UI界面的访问路径,默认为/swagger-ui.html + tags-sorter: alpha + # tags-sorter: 接口文档中的tags排序规则,默认为alpha,可选值为alpha(按字母顺序排序)或as-is(按照在代码中定义的顺序排序) + operations-sorter: alpha + + api-docs: + path: /v3/api-docs + # path: 配置api-docs的访问路径,默认为/v3/api-docs + + group-configs: + # group-configs: 配置分组信息 + - group: 'default' + # group: 分组名称 + paths-to-match: '/**' + # paths-to-match: 配置要匹配的路径,默认为/** + packages-to-scan: cn.edu.whut.system + # packages-to-scan: 配置要扫描的包的路径,直接配置为Controller类所在的包名即可 + +# knife4j项目访问访问地址:http://127.0.0.1:8080/doc.html#/home +knife4j: + enable: true + # 设置为true以启用Knife4j增强功能,这将再应用程序中启用Knife4j UI + setting: + # language: 设置Knife4j UI的语言,默认为zh_cn,可选值为zh_cn或en + language: zh-CN + #开启生产环境屏蔽 + production: false + #是否启用登录认证 + basic: + enable: false + username: # 自己设置一个 + password: # 自己设置一个 \ No newline at end of file diff --git a/system-server/system-server-biz/src/test/java/cn/edu/whut/system/SystemServerBizApplicationTests.java b/system-server/system-server-biz/src/test/java/cn/edu/whut/system/SystemServerBizApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..6811084431700517792e892f20243cdaeb16bbd1 --- /dev/null +++ b/system-server/system-server-biz/src/test/java/cn/edu/whut/system/SystemServerBizApplicationTests.java @@ -0,0 +1,13 @@ +package cn.edu.whut.system; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SystemServerBizApplicationTests { + + @Test + void contextLoads() { + } + +}