Compare commits
2 Commits
master
...
cursor/cre
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d8faa09372 | ||
![]() |
bbf6135e39 |
469
.cursor/rules/api-design-standards.mdc
Normal file
469
.cursor/rules/api-design-standards.mdc
Normal file
@@ -0,0 +1,469 @@
|
||||
# API 设计规范
|
||||
|
||||
## RESTful API 设计原则
|
||||
|
||||
### URL 设计规范
|
||||
```
|
||||
/{应用类型}-api/{模块}/{业务对象}/{操作}
|
||||
|
||||
示例:
|
||||
/admin-api/system/user/create # 管理后台创建用户
|
||||
/admin-api/system/user/page # 管理后台用户分页
|
||||
/app-api/member/user/profile # 用户端获取个人信息
|
||||
```
|
||||
|
||||
### HTTP 方法使用
|
||||
```http
|
||||
GET /admin-api/system/user/get # 获取单个资源
|
||||
GET /admin-api/system/user/page # 获取资源列表(分页)
|
||||
GET /admin-api/system/user/list # 获取资源列表(不分页)
|
||||
POST /admin-api/system/user/create # 创建资源
|
||||
PUT /admin-api/system/user/update # 更新资源
|
||||
DELETE /admin-api/system/user/delete # 删除资源
|
||||
```
|
||||
|
||||
### 应用类型前缀
|
||||
- **admin-api**: 管理后台接口
|
||||
- **app-api**: 用户端 APP 接口
|
||||
- **mp-api**: 微信小程序接口
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"nickname": "管理员"
|
||||
},
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 1002001001,
|
||||
"data": null,
|
||||
"msg": "用户不存在"
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "admin"
|
||||
}
|
||||
],
|
||||
"total": 100
|
||||
},
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Controller 层实现规范
|
||||
|
||||
### 标准 Controller 结构
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/system/user")
|
||||
@Api(tags = "管理后台 - 用户管理")
|
||||
@Validated
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建用户")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:create')")
|
||||
@OperateLog(type = CREATE)
|
||||
public CommonResult<Long> createUser(@Valid @RequestBody UserCreateReqVO reqVO) {
|
||||
return success(userService.createUser(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@ApiOperation("修改用户")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:update')")
|
||||
@OperateLog(type = UPDATE)
|
||||
public CommonResult<Boolean> updateUser(@Valid @RequestBody UserUpdateReqVO reqVO) {
|
||||
userService.updateUser(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@ApiOperation("删除用户")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:delete')")
|
||||
@OperateLog(type = DELETE)
|
||||
public CommonResult<Boolean> deleteUser(@RequestParam("id") Long id) {
|
||||
userService.deleteUser(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@ApiOperation("获得用户")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
public CommonResult<UserRespVO> getUser(@RequestParam("id") Long id) {
|
||||
UserDO user = userService.getUser(id);
|
||||
return success(UserConvert.INSTANCE.convert(user));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@ApiOperation("获得用户分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO reqVO) {
|
||||
PageResult<UserDO> pageResult = userService.getUserPage(reqVO);
|
||||
return success(UserConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperation("获得用户列表")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
public CommonResult<List<UserRespVO>> getUserList(@Valid UserListReqVO reqVO) {
|
||||
List<UserDO> list = userService.getUserList(reqVO);
|
||||
return success(UserConvert.INSTANCE.convertList(list));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 必备注解使用
|
||||
```java
|
||||
// 类级别注解
|
||||
@RestController // 声明为REST控制器
|
||||
@RequestMapping("/admin-api/system/user") // 请求路径映射
|
||||
@Api(tags = "管理后台 - 用户管理") // API文档分组
|
||||
@Validated // 启用参数校验
|
||||
|
||||
// 方法级别注解
|
||||
@PostMapping("/create") // HTTP方法映射
|
||||
@ApiOperation("创建用户") // API文档说明
|
||||
@PreAuthorize("@ss.hasPermission('system:user:create')") // 权限控制
|
||||
@OperateLog(type = CREATE) // 操作日志
|
||||
```
|
||||
|
||||
## 请求参数规范
|
||||
|
||||
### 创建请求 VO
|
||||
```java
|
||||
@Data
|
||||
@ApiModel("用户创建 Request VO")
|
||||
public class UserCreateReqVO {
|
||||
|
||||
@ApiModelProperty(value = "用户账号", required = true, example = "admin")
|
||||
@NotBlank(message = "用户账号不能为空")
|
||||
@Size(max = 30, message = "用户账号长度不能超过30个字符")
|
||||
private String username;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称", required = true, example = "管理员")
|
||||
@NotBlank(message = "用户昵称不能为空")
|
||||
@Size(max = 30, message = "用户昵称长度不能超过30个字符")
|
||||
private String nickname;
|
||||
|
||||
@ApiModelProperty(value = "用户邮箱", example = "admin@example.com")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 50, message = "邮箱长度不能超过50个字符")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty(value = "手机号码", example = "15601691300")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
|
||||
private String mobile;
|
||||
|
||||
@ApiModelProperty(value = "用户性别", example = "1")
|
||||
@InEnum(CommonStatusEnum.class, message = "修改状态必须是 {value}")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "部门ID", example = "1")
|
||||
@NotNull(message = "部门不能为空")
|
||||
private Long deptId;
|
||||
|
||||
@ApiModelProperty(value = "岗位编号数组", example = "1")
|
||||
private Set<Long> postIds;
|
||||
|
||||
@ApiModelProperty(value = "用户角色编号数组", example = "1")
|
||||
private Set<Long> roleIds;
|
||||
}
|
||||
```
|
||||
|
||||
### 更新请求 VO
|
||||
```java
|
||||
@Data
|
||||
@ApiModel("用户更新 Request VO")
|
||||
public class UserUpdateReqVO {
|
||||
|
||||
@ApiModelProperty(value = "用户编号", required = true, example = "1")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long id;
|
||||
|
||||
// 其他字段与创建请求相同...
|
||||
}
|
||||
```
|
||||
|
||||
### 分页查询 VO
|
||||
```java
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ApiModel("用户分页 Request VO")
|
||||
public class UserPageReqVO extends PageParam {
|
||||
|
||||
@ApiModelProperty(value = "用户账号", example = "admin")
|
||||
private String username;
|
||||
|
||||
@ApiModelProperty(value = "手机号码", example = "15601691300")
|
||||
private String mobile;
|
||||
|
||||
@ApiModelProperty(value = "展示状态", example = "1")
|
||||
@InEnum(CommonStatusEnum.class, message = "状态必须是 {value}")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
@ApiModelProperty(value = "部门编号", example = "1")
|
||||
private Long deptId;
|
||||
}
|
||||
```
|
||||
|
||||
## 响应参数规范
|
||||
|
||||
### 响应 VO
|
||||
```java
|
||||
@Data
|
||||
@ApiModel("用户信息 Response VO")
|
||||
public class UserRespVO {
|
||||
|
||||
@ApiModelProperty(value = "用户编号", example = "1")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "用户账号", example = "admin")
|
||||
private String username;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称", example = "管理员")
|
||||
private String nickname;
|
||||
|
||||
@ApiModelProperty(value = "用户邮箱", example = "admin@example.com")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty(value = "手机号码", example = "15601691300")
|
||||
private String mobile;
|
||||
|
||||
@ApiModelProperty(value = "用户性别", example = "1")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "用户头像", example = "https://www.example.com/avatar.jpg")
|
||||
private String avatar;
|
||||
|
||||
@ApiModelProperty(value = "帐号状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "最后登录IP", example = "192.168.1.1")
|
||||
private String loginIp;
|
||||
|
||||
@ApiModelProperty(value = "最后登录时间", example = "2022-01-01 00:00:00")
|
||||
private LocalDateTime loginDate;
|
||||
|
||||
@ApiModelProperty(value = "创建时间", example = "2022-01-01 00:00:00")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 所属角色
|
||||
*/
|
||||
private Set<Long> roleIds;
|
||||
|
||||
/**
|
||||
* 所属岗位
|
||||
*/
|
||||
private Set<Long> postIds;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@ApiModelProperty(value = "部门编号", example = "1")
|
||||
private Long deptId;
|
||||
}
|
||||
```
|
||||
|
||||
## 参数校验规范
|
||||
|
||||
### 常用校验注解
|
||||
```java
|
||||
// 非空校验
|
||||
@NotNull(message = "ID不能为空")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@NotEmpty(message = "角色列表不能为空")
|
||||
|
||||
// 长度校验
|
||||
@Size(min = 2, max = 30, message = "用户名长度必须在2-30字符之间")
|
||||
@Length(max = 50, message = "邮箱长度不能超过50个字符")
|
||||
|
||||
// 格式校验
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
|
||||
|
||||
// 数值校验
|
||||
@Min(value = 0, message = "年龄不能小于0")
|
||||
@Max(value = 150, message = "年龄不能大于150")
|
||||
@Range(min = 1, max = 100, message = "百分比必须在1-100之间")
|
||||
|
||||
// 自定义校验
|
||||
@InEnum(CommonStatusEnum.class, message = "状态必须是 {value}")
|
||||
```
|
||||
|
||||
### 分组校验
|
||||
```java
|
||||
// 定义校验分组
|
||||
public interface AddGroup {}
|
||||
public interface UpdateGroup {}
|
||||
|
||||
// 在字段上使用分组
|
||||
@NotNull(groups = UpdateGroup.class, message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = {AddGroup.class, UpdateGroup.class}, message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
// 在Controller中指定分组
|
||||
@PostMapping("/create")
|
||||
public CommonResult<Long> createUser(@Validated(AddGroup.class) @RequestBody UserSaveReqVO reqVO) {
|
||||
return success(userService.createUser(reqVO));
|
||||
}
|
||||
```
|
||||
|
||||
## 异常处理规范
|
||||
|
||||
### 全局异常处理
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(ServiceException.class)
|
||||
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
|
||||
log.info("[serviceExceptionHandler]", ex);
|
||||
return CommonResult.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public CommonResult<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
|
||||
FieldError fieldError = ex.getBindingResult().getFieldError();
|
||||
assert fieldError != null;
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务异常抛出
|
||||
```java
|
||||
// 在Service中抛出业务异常
|
||||
public void validateUserExists(Long id) {
|
||||
if (userMapper.selectById(id) == null) {
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误码定义
|
||||
public interface ErrorCodeConstants {
|
||||
ErrorCode USER_NOT_EXISTS = new ErrorCode(1002001001, "用户不存在");
|
||||
ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1002001002, "用户账号已经存在");
|
||||
}
|
||||
```
|
||||
|
||||
## 接口文档规范
|
||||
|
||||
### Swagger 注解使用
|
||||
```java
|
||||
@Api(tags = "管理后台 - 用户管理") // 控制器分组
|
||||
@ApiOperation("创建用户") // 接口说明
|
||||
@ApiParam(value = "用户ID", required = true) // 参数说明
|
||||
|
||||
@ApiModel("用户信息") // 模型说明
|
||||
@ApiModelProperty(value = "用户编号", example = "1") // 字段说明
|
||||
```
|
||||
|
||||
### 接口文档示例
|
||||
```yaml
|
||||
# 生成的 OpenAPI 文档格式
|
||||
paths:
|
||||
/admin-api/system/user/create:
|
||||
post:
|
||||
tags:
|
||||
- 管理后台 - 用户管理
|
||||
summary: 创建用户
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCreateReqVO'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CommonResultLong'
|
||||
```
|
||||
|
||||
## 版本管理
|
||||
|
||||
### API 版本控制
|
||||
```java
|
||||
// 使用路径版本控制
|
||||
@RequestMapping("/admin-api/v1/system/user")
|
||||
|
||||
// 使用请求头版本控制
|
||||
@RequestMapping(value = "/admin-api/system/user", headers = "version=1.0")
|
||||
|
||||
// 使用参数版本控制
|
||||
@RequestMapping(value = "/admin-api/system/user", params = "version=1.0")
|
||||
```
|
||||
|
||||
### 向后兼容
|
||||
- 新增字段设为可选
|
||||
- 保持现有字段不变
|
||||
- 废弃字段标记 @Deprecated
|
||||
- 提供迁移指南
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 批量操作
|
||||
```java
|
||||
@PostMapping("/batch-delete")
|
||||
@ApiOperation("批量删除用户")
|
||||
public CommonResult<Boolean> deleteUsers(@RequestBody Set<Long> ids) {
|
||||
userService.deleteUsers(ids);
|
||||
return success(true);
|
||||
}
|
||||
```
|
||||
|
||||
### 异步处理
|
||||
```java
|
||||
@PostMapping("/export")
|
||||
@ApiOperation("导出用户数据")
|
||||
public CommonResult<String> exportUsers(@Valid UserExportReqVO reqVO) {
|
||||
// 异步处理导出任务
|
||||
String taskId = userService.exportUsersAsync(reqVO);
|
||||
return success(taskId);
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
```java
|
||||
@GetMapping("/get")
|
||||
@Cacheable(value = "user", key = "#id")
|
||||
public CommonResult<UserRespVO> getUser(@RequestParam("id") Long id) {
|
||||
UserDO user = userService.getUser(id);
|
||||
return success(UserConvert.INSTANCE.convert(user));
|
||||
}
|
||||
```
|
73
.cursor/rules/business-modules.mdc
Normal file
73
.cursor/rules/business-modules.mdc
Normal file
@@ -0,0 +1,73 @@
|
||||
# 业务模块指南
|
||||
|
||||
## 系统核心模块
|
||||
|
||||
### system 模块 - 系统管理
|
||||
**路径**: [yudao-module-system](mdc:yudao-module-system)
|
||||
**功能**: 用户管理、角色权限、菜单管理、部门岗位、字典配置、操作日志等
|
||||
**核心包**: `cn.iocoder.yudao.module.system`
|
||||
|
||||
### infra 模块 - 基础设施
|
||||
**路径**: [yudao-module-infra](mdc:yudao-module-infra)
|
||||
**功能**: 文件存储、配置管理、代码生成、定时任务、API 文档等
|
||||
**核心包**: `cn.iocoder.yudao.module.infra`
|
||||
|
||||
## 可选业务模块
|
||||
|
||||
### bpm 模块 - 工作流程
|
||||
**路径**: [yudao-module-bpm](mdc:yudao-module-bpm)
|
||||
**技术栈**: Flowable 6.8.0
|
||||
**功能**: 流程设计器、流程实例、任务管理、表单设计
|
||||
**特色**: 支持 BPMN 和仿钉钉双设计器
|
||||
|
||||
### pay 模块 - 支付系统
|
||||
**路径**: [yudao-module-pay](mdc:yudao-module-pay)
|
||||
**功能**: 支付应用、支付订单、退款管理、支付渠道配置
|
||||
**支持**: 支付宝、微信支付、银联等主流支付方式
|
||||
|
||||
### mall 模块 - 商城系统
|
||||
**路径**: [yudao-module-mall](mdc:yudao-module-mall)
|
||||
**子模块**:
|
||||
- `yudao-module-product`: 商品管理
|
||||
- `yudao-module-promotion`: 营销活动
|
||||
- `yudao-module-trade`: 交易订单
|
||||
- `yudao-module-statistics`: 数据统计
|
||||
|
||||
### member 模块 - 会员中心
|
||||
**路径**: [yudao-module-member](mdc:yudao-module-member)
|
||||
**功能**: 会员管理、等级体系、积分签到、标签分组
|
||||
|
||||
### crm 模块 - 客户关系管理
|
||||
**路径**: [yudao-module-crm](mdc:yudao-module-crm)
|
||||
**功能**: 线索管理、客户管理、联系人、商机、合同、回款
|
||||
|
||||
### erp 模块 - 企业资源计划
|
||||
**路径**: [yudao-module-erp](mdc:yudao-module-erp)
|
||||
**功能**: 产品管理、库存管理、采购管理、销售管理、财务管理
|
||||
|
||||
### mp 模块 - 微信公众号
|
||||
**路径**: [yudao-module-mp](mdc:yudao-module-mp)
|
||||
**功能**: 账号管理、粉丝管理、消息管理、自动回复、菜单管理
|
||||
|
||||
### ai 模块 - AI 大模型
|
||||
**路径**: [yudao-module-ai](mdc:yudao-module-ai)
|
||||
**功能**: 对话聊天、图片生成、音乐生成、思维导图
|
||||
**支持**: OpenAI、文心一言、通义千问等主流模型
|
||||
|
||||
### iot 模块 - 物联网
|
||||
**路径**: [yudao-module-iot](mdc:yudao-module-iot)
|
||||
**功能**: 设备管理、数据采集、远程控制
|
||||
**插件化**: 支持 MQTT、HTTP 等多种协议
|
||||
|
||||
### report 模块 - 数据报表
|
||||
**路径**: [yudao-module-report](mdc:yudao-module-report)
|
||||
**功能**: 报表设计器、大屏设计器、数据可视化
|
||||
|
||||
## 模块间依赖关系
|
||||
- **基础依赖**: system <- 所有业务模块
|
||||
- **支付依赖**: pay <- mall (商城依赖支付)
|
||||
- **会员依赖**: member <- mall (商城依赖会员)
|
||||
- **工作流**: bpm 可被任意模块集成
|
||||
|
||||
## 模块配置激活
|
||||
在 [pom.xml](mdc:pom.xml) 中控制模块的启用/禁用,根据业务需求按需集成。
|
665
.cursor/rules/cache-strategy.mdc
Normal file
665
.cursor/rules/cache-strategy.mdc
Normal file
@@ -0,0 +1,665 @@
|
||||
# 缓存策略详解
|
||||
|
||||
## Redis 缓存架构
|
||||
|
||||
### 缓存层次结构
|
||||
```
|
||||
应用层 -> 本地缓存 (Caffeine) -> Redis 缓存 -> 数据库
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
- **Spring Cache**: 统一缓存抽象
|
||||
- **Redis**: 分布式缓存存储
|
||||
- **Caffeine**: 本地缓存提升性能
|
||||
- **Redisson**: Redis 客户端和分布式锁
|
||||
|
||||
## 缓存配置
|
||||
|
||||
### Redis 连接配置
|
||||
```yaml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 200
|
||||
max-idle: 20
|
||||
min-idle: 5
|
||||
max-wait: 1000ms
|
||||
```
|
||||
|
||||
### 缓存管理器配置
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
|
||||
// Redis 缓存管理器
|
||||
RedisCacheManager.Builder builder = RedisCacheManager
|
||||
.RedisCacheManagerBuilder
|
||||
.fromConnectionFactory(redisTemplate.getConnectionFactory())
|
||||
.cacheDefaults(buildCacheConfiguration());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean("localCacheManager")
|
||||
public CacheManager localCacheManager() {
|
||||
// 本地缓存管理器
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||
.maximumSize(1000)
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES));
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存使用模式
|
||||
|
||||
### 基础缓存注解
|
||||
```java
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
/**
|
||||
* 查询缓存
|
||||
*/
|
||||
@Cacheable(value = "user", key = "#id")
|
||||
public UserDO getUser(Long id) {
|
||||
return userMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
*/
|
||||
@CachePut(value = "user", key = "#user.id")
|
||||
public UserDO updateUser(UserDO user) {
|
||||
userMapper.updateById(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
@CacheEvict(value = "user", key = "#id")
|
||||
public void deleteUser(Long id) {
|
||||
userMapper.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
*/
|
||||
@CacheEvict(value = "user", allEntries = true)
|
||||
public void clearUserCache() {
|
||||
// 清空所有用户缓存
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 条件缓存
|
||||
```java
|
||||
// 条件缓存 - 只有状态为启用的用户才缓存
|
||||
@Cacheable(value = "user", key = "#id", condition = "#user.status == 1")
|
||||
public UserDO getUser(Long id) {
|
||||
return userMapper.selectById(id);
|
||||
}
|
||||
|
||||
// 排除缓存 - 管理员用户不缓存
|
||||
@Cacheable(value = "user", key = "#id", unless = "#result.username == 'admin'")
|
||||
public UserDO getUser(Long id) {
|
||||
return userMapper.selectById(id);
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义缓存键
|
||||
```java
|
||||
// 复合键
|
||||
@Cacheable(value = "user", key = "#tenantId + ':' + #username")
|
||||
public UserDO getUserByUsername(Long tenantId, String username) {
|
||||
return userMapper.selectOne(new LambdaQueryWrapper<UserDO>()
|
||||
.eq(UserDO::getTenantId, tenantId)
|
||||
.eq(UserDO::getUsername, username));
|
||||
}
|
||||
|
||||
// 使用 SpEL 表达式
|
||||
@Cacheable(value = "user", key = "T(String).valueOf(#user.tenantId).concat(':').concat(#user.username)")
|
||||
public UserDO createUser(UserCreateReqVO reqVO) {
|
||||
// 创建用户逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## Redis 数据结构使用
|
||||
|
||||
### 字符串 (String)
|
||||
```java
|
||||
@Component
|
||||
public class StringCacheService {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
public void set(String key, String value, Duration timeout) {
|
||||
stringRedisTemplate.opsForValue().set(key, value, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
public String get(String key) {
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
public Long increment(String key) {
|
||||
return stringRedisTemplate.opsForValue().increment(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 哈希 (Hash)
|
||||
```java
|
||||
@Component
|
||||
public class HashCacheService {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 设置 Hash 字段
|
||||
*/
|
||||
public void hset(String key, String field, String value) {
|
||||
stringRedisTemplate.opsForHash().put(key, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Hash 字段
|
||||
*/
|
||||
public String hget(String key, String field) {
|
||||
return (String) stringRedisTemplate.opsForHash().get(key, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 Hash 字段
|
||||
*/
|
||||
public Map<Object, Object> hgetAll(String key) {
|
||||
return stringRedisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 列表 (List)
|
||||
```java
|
||||
@Component
|
||||
public class ListCacheService {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 左推入
|
||||
*/
|
||||
public void lpush(String key, String value) {
|
||||
stringRedisTemplate.opsForList().leftPush(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右弹出
|
||||
*/
|
||||
public String rpop(String key) {
|
||||
return stringRedisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表
|
||||
*/
|
||||
public List<String> range(String key, long start, long end) {
|
||||
return stringRedisTemplate.opsForList().range(key, start, end);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 集合 (Set)
|
||||
```java
|
||||
@Component
|
||||
public class SetCacheService {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 添加成员
|
||||
*/
|
||||
public void sadd(String key, String... values) {
|
||||
stringRedisTemplate.opsForSet().add(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有成员
|
||||
*/
|
||||
public Set<String> smembers(String key) {
|
||||
return stringRedisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在
|
||||
*/
|
||||
public Boolean sismember(String key, String value) {
|
||||
return stringRedisTemplate.opsForSet().isMember(key, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 有序集合 (ZSet)
|
||||
```java
|
||||
@Component
|
||||
public class ZSetCacheService {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 添加成员
|
||||
*/
|
||||
public void zadd(String key, String value, double score) {
|
||||
stringRedisTemplate.opsForZSet().add(key, value, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排名范围
|
||||
*/
|
||||
public Set<String> zrange(String key, long start, long end) {
|
||||
return stringRedisTemplate.opsForZSet().range(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分数范围
|
||||
*/
|
||||
public Set<String> zrangeByScore(String key, double min, double max) {
|
||||
return stringRedisTemplate.opsForZSet().rangeByScore(key, min, max);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存键设计规范
|
||||
|
||||
### 键命名规范
|
||||
```java
|
||||
public class RedisKeyConstants {
|
||||
|
||||
/**
|
||||
* 用户缓存
|
||||
* KEY 格式:user:{id}
|
||||
* 过期时间:30 分钟
|
||||
*/
|
||||
public static final String USER = "user";
|
||||
|
||||
/**
|
||||
* 验证码缓存
|
||||
* KEY 格式:captcha:{uuid}
|
||||
* 过期时间:5 分钟
|
||||
*/
|
||||
public static final String CAPTCHA = "captcha";
|
||||
|
||||
/**
|
||||
* 权限缓存
|
||||
* KEY 格式:permission:{userId}
|
||||
* 过期时间:1 小时
|
||||
*/
|
||||
public static final String PERMISSION = "permission";
|
||||
|
||||
/**
|
||||
* 字典缓存
|
||||
* KEY 格式:dict:{type}
|
||||
* 过期时间:永不过期
|
||||
*/
|
||||
public static final String DICT = "dict";
|
||||
}
|
||||
```
|
||||
|
||||
### 键生成工具
|
||||
```java
|
||||
@Component
|
||||
public class RedisKeyBuilder {
|
||||
|
||||
/**
|
||||
* 构建用户缓存键
|
||||
*/
|
||||
public static String buildUserKey(Long userId) {
|
||||
return RedisKeyConstants.USER + ":" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建验证码缓存键
|
||||
*/
|
||||
public static String buildCaptchaKey(String uuid) {
|
||||
return RedisKeyConstants.CAPTCHA + ":" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限缓存键
|
||||
*/
|
||||
public static String buildPermissionKey(Long userId) {
|
||||
return RedisKeyConstants.PERMISSION + ":" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建租户级别缓存键
|
||||
*/
|
||||
public static String buildTenantKey(String prefix, Long tenantId, Object suffix) {
|
||||
return prefix + ":" + tenantId + ":" + suffix;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分布式锁
|
||||
|
||||
### Redisson 分布式锁
|
||||
```java
|
||||
@Component
|
||||
public class DistributedLockService {
|
||||
|
||||
@Resource
|
||||
private RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*/
|
||||
public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
return lock.tryLock(waitTime, leaseTime, unit);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*/
|
||||
public void unlock(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁模板方法
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime,
|
||||
TimeUnit unit, Supplier<T> supplier) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
if (lock.tryLock(waitTime, leaseTime, unit)) {
|
||||
return supplier.get();
|
||||
} else {
|
||||
throw new ServiceException("获取锁失败");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("获取锁被中断");
|
||||
} finally {
|
||||
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分布式锁应用
|
||||
```java
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
@Resource
|
||||
private DistributedLockService lockService;
|
||||
|
||||
/**
|
||||
* 创建订单(防重复提交)
|
||||
*/
|
||||
public OrderDO createOrder(OrderCreateReqVO reqVO) {
|
||||
String lockKey = "order:create:" + reqVO.getUserId();
|
||||
|
||||
return lockService.executeWithLock(lockKey, 5, 10, TimeUnit.SECONDS, () -> {
|
||||
// 检查重复订单
|
||||
validateDuplicateOrder(reqVO);
|
||||
|
||||
// 创建订单
|
||||
return doCreateOrder(reqVO);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存预热
|
||||
|
||||
### 应用启动预热
|
||||
```java
|
||||
@Component
|
||||
public class CacheWarmUpService {
|
||||
|
||||
@Resource
|
||||
private DictDataService dictDataService;
|
||||
|
||||
@Resource
|
||||
private ConfigService configService;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void warmUpCache() {
|
||||
log.info("[warmUpCache][开始预热缓存]");
|
||||
|
||||
// 预热字典缓存
|
||||
dictDataService.warmUpCache();
|
||||
|
||||
// 预热配置缓存
|
||||
configService.warmUpCache();
|
||||
|
||||
log.info("[warmUpCache][预热缓存完成]");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定时预热
|
||||
```java
|
||||
@Component
|
||||
public class CacheScheduleService {
|
||||
|
||||
/**
|
||||
* 定时刷新热点数据
|
||||
*/
|
||||
@Scheduled(fixedRate = 300000) // 5分钟执行一次
|
||||
public void refreshHotData() {
|
||||
// 刷新热点商品缓存
|
||||
refreshProductCache();
|
||||
|
||||
// 刷新活动缓存
|
||||
refreshActivityCache();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存监控
|
||||
|
||||
### 缓存统计
|
||||
```java
|
||||
@Component
|
||||
public class CacheMetricsService {
|
||||
|
||||
@Resource
|
||||
private CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
public Map<String, Object> getCacheStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
Collection<String> cacheNames = cacheManager.getCacheNames();
|
||||
for (String cacheName : cacheNames) {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache instanceof RedisCache) {
|
||||
// 获取 Redis 缓存统计
|
||||
stats.put(cacheName, getRedisCacheStats(cache));
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存健康检查
|
||||
```java
|
||||
@Component
|
||||
public class CacheHealthIndicator extends AbstractHealthIndicator {
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||||
try {
|
||||
// 测试 Redis 连接
|
||||
String pong = stringRedisTemplate.getConnectionFactory()
|
||||
.getConnection().ping();
|
||||
|
||||
if ("PONG".equals(pong)) {
|
||||
builder.up().withDetail("redis", "连接正常");
|
||||
} else {
|
||||
builder.down().withDetail("redis", "连接异常");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
builder.down().withException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存优化策略
|
||||
|
||||
### 多级缓存
|
||||
```java
|
||||
@Service
|
||||
public class MultiLevelCacheService {
|
||||
|
||||
@Resource
|
||||
private CacheManager localCacheManager;
|
||||
|
||||
@Resource
|
||||
private CacheManager redisCacheManager;
|
||||
|
||||
public <T> T get(String key, Class<T> type, Supplier<T> loader) {
|
||||
// 1. 先查本地缓存
|
||||
Cache localCache = localCacheManager.getCache("local");
|
||||
T value = localCache.get(key, type);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 2. 再查 Redis 缓存
|
||||
Cache redisCache = redisCacheManager.getCache("redis");
|
||||
value = redisCache.get(key, type);
|
||||
if (value != null) {
|
||||
// 回写本地缓存
|
||||
localCache.put(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3. 最后查数据库
|
||||
value = loader.get();
|
||||
if (value != null) {
|
||||
// 写入两级缓存
|
||||
localCache.put(key, value);
|
||||
redisCache.put(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存穿透防护
|
||||
```java
|
||||
@Service
|
||||
public class BloomFilterService {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 检查是否可能存在
|
||||
*/
|
||||
public boolean mightContain(String key) {
|
||||
// 使用布隆过滤器检查
|
||||
String bloomKey = "bloom:user";
|
||||
return redisTemplate.opsForValue().getBit(bloomKey, hash(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到布隆过滤器
|
||||
*/
|
||||
public void put(String key) {
|
||||
String bloomKey = "bloom:user";
|
||||
redisTemplate.opsForValue().setBit(bloomKey, hash(key), true);
|
||||
}
|
||||
|
||||
private long hash(String key) {
|
||||
// 简单的哈希算法
|
||||
return Math.abs(key.hashCode()) % 1000000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存雪崩防护
|
||||
```java
|
||||
@Service
|
||||
public class AntiAvalancheService {
|
||||
|
||||
/**
|
||||
* 随机过期时间防止雪崩
|
||||
*/
|
||||
@Cacheable(value = "user", key = "#id")
|
||||
public UserDO getUser(Long id) {
|
||||
// 缓存时间:30分钟 + 随机0-10分钟
|
||||
Duration timeout = Duration.ofMinutes(30 + new Random().nextInt(10));
|
||||
|
||||
UserDO user = userMapper.selectById(id);
|
||||
// 手动设置过期时间
|
||||
redisTemplate.expire("user:" + id, timeout);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 缓存设计原则
|
||||
1. **热点数据优先**: 经常访问的数据才缓存
|
||||
2. **合理的过期时间**: 根据业务场景设置过期时间
|
||||
3. **缓存键规范**: 使用有意义的键名和统一的命名规范
|
||||
4. **避免大对象**: 单个缓存对象不要太大
|
||||
5. **监控告警**: 设置缓存命中率等监控指标
|
||||
|
||||
### 常见问题解决
|
||||
1. **缓存击穿**: 使用分布式锁保护热点数据
|
||||
2. **缓存穿透**: 使用布隆过滤器或缓存空值
|
||||
3. **缓存雪崩**: 设置随机过期时间
|
||||
4. **数据一致性**: 使用缓存更新策略和事务
|
||||
5. **内存泄漏**: 设置合理的过期时间和大小限制
|
916
.cursor/rules/code-generator-guide.mdc
Normal file
916
.cursor/rules/code-generator-guide.mdc
Normal file
@@ -0,0 +1,916 @@
|
||||
# 代码生成器使用指南
|
||||
|
||||
## 代码生成器概述
|
||||
|
||||
### 功能特性
|
||||
- **前后端代码生成**: 一键生成 Java + Vue 完整代码
|
||||
- **支持单表、树表、主子表**: 多种表结构类型
|
||||
- **CRUD 操作**: 增删改查的完整实现
|
||||
- **权限控制**: 自动生成权限注解
|
||||
- **接口文档**: 自动生成 Swagger 文档
|
||||
- **单元测试**: 生成完整的单元测试代码
|
||||
|
||||
### 生成内容
|
||||
**后端 Java 代码**:
|
||||
- Controller 控制器
|
||||
- Service 业务逻辑层
|
||||
- Mapper 数据访问层
|
||||
- DO 数据对象
|
||||
- VO 视图对象
|
||||
- Convert 对象转换器
|
||||
- 单元测试类
|
||||
|
||||
**前端 Vue 代码**:
|
||||
- 列表页面
|
||||
- 新增/编辑弹窗
|
||||
- 搜索表单
|
||||
- API 接口调用
|
||||
|
||||
**SQL 脚本**:
|
||||
- 菜单权限 SQL
|
||||
- 按钮权限 SQL
|
||||
|
||||
## 代码生成器表结构
|
||||
|
||||
### 生成表配置
|
||||
```sql
|
||||
-- 代码生成表定义
|
||||
CREATE TABLE infra_codegen_table (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
data_source_config_id BIGINT NOT NULL COMMENT '数据源配置的编号',
|
||||
scene TINYINT NOT NULL DEFAULT 1 COMMENT '生成场景',
|
||||
table_name VARCHAR(200) NOT NULL DEFAULT '' COMMENT '表名称',
|
||||
table_comment VARCHAR(500) NOT NULL DEFAULT '' COMMENT '表描述',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
module_name VARCHAR(30) NOT NULL COMMENT '模块名',
|
||||
business_name VARCHAR(30) NOT NULL COMMENT '业务名',
|
||||
class_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '类名称',
|
||||
class_comment VARCHAR(50) NOT NULL COMMENT '类描述',
|
||||
author VARCHAR(50) NOT NULL COMMENT '作者',
|
||||
template_type TINYINT NOT NULL DEFAULT 1 COMMENT '模板类型',
|
||||
front_type TINYINT NOT NULL COMMENT '前端类型',
|
||||
parent_menu_id BIGINT DEFAULT NULL COMMENT '父菜单编号',
|
||||
master_table_id BIGINT DEFAULT NULL COMMENT '主表的编号',
|
||||
sub_join_column_name VARCHAR(30) DEFAULT NULL COMMENT '子表关联主表的字段名',
|
||||
sub_join_many BIT DEFAULT NULL COMMENT '主表与子表是否一对多',
|
||||
tree_parent_column_name VARCHAR(30) DEFAULT NULL COMMENT '树表的父字段名',
|
||||
tree_name_column_name VARCHAR(30) DEFAULT NULL COMMENT '树表的名字字段名',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
|
||||
-- 代码生成表字段定义
|
||||
CREATE TABLE infra_codegen_column (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
table_id BIGINT NOT NULL COMMENT '表编号',
|
||||
column_name VARCHAR(200) NOT NULL COMMENT '字段名',
|
||||
data_type VARCHAR(100) NOT NULL COMMENT '字段类型',
|
||||
column_comment VARCHAR(500) NOT NULL COMMENT '字段描述',
|
||||
nullable BIT NOT NULL COMMENT '是否允许为空',
|
||||
primary_key BIT NOT NULL COMMENT '是否主键',
|
||||
ordinal_position INT NOT NULL COMMENT '排序',
|
||||
java_type VARCHAR(32) NOT NULL COMMENT 'Java 属性类型',
|
||||
java_field VARCHAR(64) NOT NULL COMMENT 'Java 属性名',
|
||||
dict_type VARCHAR(200) DEFAULT '' COMMENT '字典类型',
|
||||
example VARCHAR(64) DEFAULT NULL COMMENT '数据示例',
|
||||
create_operation BIT NOT NULL COMMENT '是否为 Create 创建操作的字段',
|
||||
update_operation BIT NOT NULL COMMENT '是否为 Update 更新操作的字段',
|
||||
list_operation BIT NOT NULL COMMENT '是否为 List 查询操作的字段',
|
||||
list_operation_condition VARCHAR(32) NOT NULL DEFAULT '=' COMMENT 'List 查询操作的条件类型',
|
||||
list_operation_result BIT NOT NULL COMMENT '是否为 List 查询操作的返回字段',
|
||||
html_type VARCHAR(32) NOT NULL COMMENT '显示类型',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
```
|
||||
|
||||
## 代码生成服务
|
||||
|
||||
### 表导入服务
|
||||
```java
|
||||
@Service
|
||||
public class CodegenTableServiceImpl implements CodegenTableService {
|
||||
|
||||
@Resource
|
||||
private DatabaseTableService databaseTableService;
|
||||
|
||||
@Resource
|
||||
private CodegenTableMapper codegenTableMapper;
|
||||
|
||||
@Resource
|
||||
private CodegenColumnMapper codegenColumnMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<Long> createCodegenList(Long dataSourceConfigId, List<String> tableNames) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
// 遍历添加每个表
|
||||
tableNames.forEach(tableName -> {
|
||||
Long id = createCodegen(dataSourceConfigId, tableName);
|
||||
ids.add(id);
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Long createCodegen(Long dataSourceConfigId, String tableName) {
|
||||
// 从数据库中,获得数据库表结构
|
||||
DatabaseTableRespDTO table = databaseTableService.getDatabaseTable(dataSourceConfigId, tableName);
|
||||
List<DatabaseColumnRespDTO> columns = databaseTableService.getDatabaseColumnList(dataSourceConfigId, tableName);
|
||||
|
||||
// 导入数据库表结构
|
||||
CodegenTableDO codegenTable = convertTable(table);
|
||||
codegenTable.setDataSourceConfigId(dataSourceConfigId);
|
||||
codegenTable.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认管理后台
|
||||
codegenTable.setAuthor(CodegenConstants.DEFAULT_AUTHOR);
|
||||
initTable(codegenTable);
|
||||
codegenTableMapper.insert(codegenTable);
|
||||
|
||||
// 导入数据库字段结构
|
||||
List<CodegenColumnDO> codegenColumns = convertColumns(codegenTable.getId(), columns);
|
||||
// 初始化字段的默认值
|
||||
codegenColumns.forEach(column -> initColumn(column, codegenTable));
|
||||
codegenColumnMapper.insertBatch(codegenColumns);
|
||||
|
||||
return codegenTable.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表的配置
|
||||
*/
|
||||
private void initTable(CodegenTableDO table) {
|
||||
// 设置模块名
|
||||
table.setModuleName(CodegenConstants.DEFAULT_MODULE_NAME);
|
||||
|
||||
// 设置业务名
|
||||
table.setBusinessName(StrUtil.toCamelCase(table.getTableName().replace(table.getModuleName() + "_", "")));
|
||||
|
||||
// 设置类名
|
||||
table.setClassName(StrUtil.upperFirst(StrUtil.toCamelCase(table.getBusinessName())));
|
||||
|
||||
// 设置类描述
|
||||
if (StrUtil.isBlank(table.getClassComment())) {
|
||||
table.setClassComment(table.getTableComment());
|
||||
}
|
||||
|
||||
// 设置模板类型
|
||||
table.setTemplateType(CodegenTemplateTypeEnum.CRUD.getType());
|
||||
|
||||
// 设置前端类型
|
||||
table.setFrontType(CodegenFrontTypeEnum.VUE3.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化字段的配置
|
||||
*/
|
||||
private void initColumn(CodegenColumnDO column, CodegenTableDO table) {
|
||||
// 设置 Java 属性类型
|
||||
column.setJavaType(getJavaType(column.getDataType()));
|
||||
|
||||
// 设置 Java 属性名
|
||||
column.setJavaField(StrUtil.toCamelCase(column.getColumnName()));
|
||||
|
||||
// 设置字典类型
|
||||
column.setDictType(getDictType(column.getColumnName(), column.getColumnComment()));
|
||||
|
||||
// 设置操作字段
|
||||
if (isBaseColumn(column.getColumnName())) {
|
||||
column.setCreateOperation(false);
|
||||
column.setUpdateOperation(false);
|
||||
column.setListOperation(false);
|
||||
column.setListOperationResult(false);
|
||||
} else {
|
||||
column.setCreateOperation(true);
|
||||
column.setUpdateOperation(true);
|
||||
column.setListOperation(true);
|
||||
column.setListOperationResult(true);
|
||||
}
|
||||
|
||||
// 设置 HTML 类型
|
||||
column.setHtmlType(getHtmlType(column));
|
||||
|
||||
// 设置查询条件
|
||||
column.setListOperationCondition(getListOperationCondition(column));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码生成服务
|
||||
```java
|
||||
@Service
|
||||
public class CodegenEngineServiceImpl implements CodegenEngineService {
|
||||
|
||||
@Resource
|
||||
private CodegenTableService codegenTableService;
|
||||
|
||||
@Resource
|
||||
private CodegenColumnService codegenColumnService;
|
||||
|
||||
@Override
|
||||
public Map<String, String> execute(Long tableId) {
|
||||
// 校验表和字段的有效性
|
||||
CodegenTableDO table = codegenTableService.getCodegenTable(tableId);
|
||||
List<CodegenColumnDO> columns = codegenColumnService.getCodegenColumnListByTableId(tableId);
|
||||
|
||||
// 执行生成
|
||||
return execute0(table, columns);
|
||||
}
|
||||
|
||||
public Map<String, String> execute0(CodegenTableDO table, List<CodegenColumnDO> columns) {
|
||||
// 创建生成上下文
|
||||
Map<String, Object> bindingMap = buildBindingMap(table, columns);
|
||||
|
||||
// 获得生成模板
|
||||
List<CodegenTemplateDO> templates = getCodegenTemplateList(table.getTemplateType());
|
||||
|
||||
// 执行生成
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (CodegenTemplateDO template : templates) {
|
||||
String content = templateEngine.getTemplate(template.getContent()).execute(bindingMap);
|
||||
result.put(template.getFilePath(), content);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模板上下文
|
||||
*/
|
||||
private Map<String, Object> buildBindingMap(CodegenTableDO table, List<CodegenColumnDO> columns) {
|
||||
Map<String, Object> bindingMap = new HashMap<>();
|
||||
|
||||
// 全局变量
|
||||
bindingMap.put("basePackage", CodegenConstants.BASE_PACKAGE);
|
||||
bindingMap.put("baseFrameworkPackage", CodegenConstants.BASE_FRAMEWORK_PACKAGE);
|
||||
bindingMap.put("dateTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
bindingMap.put("author", table.getAuthor());
|
||||
|
||||
// 表信息
|
||||
bindingMap.put("table", table);
|
||||
bindingMap.put("columns", columns);
|
||||
|
||||
// 主键信息
|
||||
CodegenColumnDO primaryColumn = CollUtil.findOne(columns, CodegenColumnDO::getPrimaryKey);
|
||||
bindingMap.put("primaryColumn", primaryColumn);
|
||||
|
||||
// 分类字段
|
||||
List<CodegenColumnDO> createColumns = filterList(columns, CodegenColumnDO::getCreateOperation);
|
||||
List<CodegenColumnDO> updateColumns = filterList(columns, CodegenColumnDO::getUpdateOperation);
|
||||
List<CodegenColumnDO> listColumns = filterList(columns, CodegenColumnDO::getListOperation);
|
||||
List<CodegenColumnDO> listOperationResultColumns = filterList(columns, CodegenColumnDO::getListOperationResult);
|
||||
|
||||
bindingMap.put("createColumns", createColumns);
|
||||
bindingMap.put("updateColumns", updateColumns);
|
||||
bindingMap.put("listColumns", listColumns);
|
||||
bindingMap.put("listOperationResultColumns", listOperationResultColumns);
|
||||
|
||||
// 导入包
|
||||
bindingMap.put("importPackages", buildImportPackages(table, columns));
|
||||
|
||||
return bindingMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建导入包
|
||||
*/
|
||||
private Set<String> buildImportPackages(CodegenTableDO table, List<CodegenColumnDO> columns) {
|
||||
Set<String> packages = new HashSet<>();
|
||||
|
||||
// 根据字段类型,添加对应的包
|
||||
for (CodegenColumnDO column : columns) {
|
||||
if (JavaTypeEnum.DATE.getType().equals(column.getJavaType())
|
||||
|| JavaTypeEnum.LOCAL_DATE_TIME.getType().equals(column.getJavaType())) {
|
||||
packages.add("java.time.LocalDateTime");
|
||||
}
|
||||
if (JavaTypeEnum.BIG_DECIMAL.getType().equals(column.getJavaType())) {
|
||||
packages.add("java.math.BigDecimal");
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 java.lang 包下的类
|
||||
packages.removeIf(packageName -> packageName.startsWith("java.lang."));
|
||||
|
||||
return packages;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 代码模板
|
||||
|
||||
### Java Controller 模板
|
||||
```java
|
||||
package ${basePackage}.module.${table.moduleName}.controller.admin;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.annotations.*;
|
||||
|
||||
import javax.validation.*;
|
||||
import javax.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.*;
|
||||
|
||||
import ${baseFrameworkPackage}.common.pojo.PageResult;
|
||||
import ${baseFrameworkPackage}.common.pojo.CommonResult;
|
||||
import ${baseFrameworkPackage}.common.util.object.BeanUtils;
|
||||
import static ${baseFrameworkPackage}.common.pojo.CommonResult.success;
|
||||
|
||||
import ${baseFrameworkPackage}.excel.core.util.ExcelUtils;
|
||||
|
||||
import ${baseFrameworkPackage}.operatelog.core.annotations.OperateLog;
|
||||
import static ${baseFrameworkPackage}.operatelog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import ${basePackage}.module.${table.moduleName}.controller.admin.${table.className}Controller;
|
||||
import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.className}DO;
|
||||
import ${basePackage}.module.${table.moduleName}.service.${table.className}Service;
|
||||
import ${basePackage}.module.${table.moduleName}.controller.admin.vo.${table.businessName}.*;
|
||||
|
||||
@Api(tags = "管理后台 - ${table.classComment}")
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/${table.moduleName}/${table.businessName}")
|
||||
@Validated
|
||||
public class ${table.className}Controller {
|
||||
|
||||
@Resource
|
||||
private ${table.className}Service ${table.businessName}Service;
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建${table.classComment}")
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:create')")
|
||||
public CommonResult<${primaryColumn.javaType}> create${table.className}(@Valid @RequestBody ${table.className}CreateReqVO createReqVO) {
|
||||
return success(${table.businessName}Service.create${table.className}(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@ApiOperation("更新${table.classComment}")
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:update')")
|
||||
public CommonResult<Boolean> update${table.className}(@Valid @RequestBody ${table.className}UpdateReqVO updateReqVO) {
|
||||
${table.businessName}Service.update${table.className}(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@ApiOperation("删除${table.classComment}")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:delete')")
|
||||
public CommonResult<Boolean> delete${table.className}(@RequestParam("id") ${primaryColumn.javaType} id) {
|
||||
${table.businessName}Service.delete${table.className}(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@ApiOperation("获得${table.classComment}")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:query')")
|
||||
public CommonResult<${table.className}RespVO> get${table.className}(@RequestParam("id") ${primaryColumn.javaType} id) {
|
||||
${table.className}DO ${table.businessName} = ${table.businessName}Service.get${table.className}(id);
|
||||
return success(BeanUtils.toBean(${table.businessName}, ${table.className}RespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@ApiOperation("获得${table.classComment}分页")
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:query')")
|
||||
public CommonResult<PageResult<${table.className}RespVO>> get${table.className}Page(@Valid ${table.className}PageReqVO pageReqVO) {
|
||||
PageResult<${table.className}DO> pageResult = ${table.businessName}Service.get${table.className}Page(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, ${table.className}RespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@ApiOperation("导出${table.classComment} Excel")
|
||||
@PreAuthorize("@ss.hasPermission('${table.moduleName}:${table.businessName}:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
public void export${table.className}Excel(@Valid ${table.className}PageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<${table.className}DO> list = ${table.businessName}Service.get${table.className}Page(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class,
|
||||
BeanUtils.toBean(list, ${table.className}RespVO.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vue 列表页面模板
|
||||
```vue
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
:model="queryParams"
|
||||
ref="queryForm"
|
||||
size="small"
|
||||
:inline="true"
|
||||
v-show="showSearch"
|
||||
label-width="68px"
|
||||
>
|
||||
#foreach ($column in $listColumns)
|
||||
#if ($column.listOperation)
|
||||
<el-form-item label="${column.columnComment}" prop="${column.javaField}">
|
||||
#if ($column.htmlType == "input")
|
||||
<el-input
|
||||
v-model="queryParams.${column.javaField}"
|
||||
placeholder="请输入${column.columnComment}"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
#elseif ($column.htmlType == "select" || $column.htmlType == "radio")
|
||||
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnComment}" clearable>
|
||||
<el-option
|
||||
v-for="dict in ${column.javaField}DictDatas"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
#elseif ($column.htmlType == "datetime")
|
||||
<el-date-picker
|
||||
v-model="queryParams.${column.javaField}"
|
||||
style="width: 240px"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
/>
|
||||
#end
|
||||
</el-form-item>
|
||||
#end
|
||||
#end
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['${table.moduleName}:${table.businessName}:create']"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['${table.moduleName}:${table.businessName}:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<el-table v-loading="loading" :data="${table.businessName}List">
|
||||
#foreach ($column in $listOperationResultColumns)
|
||||
<el-table-column label="${column.columnComment}" align="center" prop="${column.javaField}" />
|
||||
#end
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['${table.moduleName}:${table.businessName}:update']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['${table.moduleName}:${table.businessName}:delete']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNo"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 对话框(添加 / 修改) -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
#foreach ($column in $createColumns)
|
||||
<el-form-item label="${column.columnComment}" prop="${column.javaField}">
|
||||
#if ($column.htmlType == "input")
|
||||
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnComment}" />
|
||||
#elseif ($column.htmlType == "select")
|
||||
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnComment}">
|
||||
<el-option
|
||||
v-for="dict in ${column.javaField}DictDatas"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
#elseif ($column.htmlType == "radio")
|
||||
<el-radio-group v-model="form.${column.javaField}">
|
||||
<el-radio
|
||||
v-for="dict in ${column.javaField}DictDatas"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
#elseif ($column.htmlType == "datetime")
|
||||
<el-date-picker clearable
|
||||
v-model="form.${column.javaField}"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择${column.columnComment}">
|
||||
</el-date-picker>
|
||||
#elseif ($column.htmlType == "textarea")
|
||||
<el-input v-model="form.${column.javaField}" type="textarea" placeholder="请输入内容" />
|
||||
#end
|
||||
</el-form-item>
|
||||
#end
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { list${table.className}, get${table.className}, del${table.className}, add${table.className}, update${table.className} } from "@/api/${table.moduleName}/${table.businessName}";
|
||||
|
||||
export default {
|
||||
name: "${table.className}",
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 导出遮罩层
|
||||
exportLoading: false,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// ${table.classComment}表格数据
|
||||
${table.businessName}List: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
#foreach ($column in $listColumns)
|
||||
#if ($column.listOperation)
|
||||
${column.javaField}: null,
|
||||
#end
|
||||
#end
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
#foreach ($column in $createColumns)
|
||||
#if (!$column.nullable && !${column.primaryKey})
|
||||
${column.javaField}: [{ required: true, message: "${column.columnComment}不能为空", trigger: #if($column.htmlType == "select")"change"#else"blur"#end }],
|
||||
#end
|
||||
#end
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
/** 查询列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
list${table.className}(this.queryParams).then(response => {
|
||||
this.${table.businessName}List = response.data.list;
|
||||
this.total = response.data.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
#foreach ($column in $createColumns)
|
||||
${column.javaField}: null,
|
||||
#end
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNo = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加${table.classComment}";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset();
|
||||
const ${primaryColumn.javaField} = row.${primaryColumn.javaField}
|
||||
get${table.className}(${primaryColumn.javaField}).then(response => {
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改${table.classComment}";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.${primaryColumn.javaField} != null) {
|
||||
update${table.className}(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
} else {
|
||||
add${table.className}(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const ${primaryColumn.javaField}s = row.${primaryColumn.javaField};
|
||||
this.$modal.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField}s + '"的数据项?').then(function() {
|
||||
return del${table.className}(${primaryColumn.javaField}s);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
this.download('${table.moduleName}/${table.businessName}/export-excel', {
|
||||
...this.queryParams
|
||||
}, `${table.businessName}_#{new Date().getTime()}.xls`)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## 高级配置
|
||||
|
||||
### 主子表生成
|
||||
```java
|
||||
// 主表配置
|
||||
CodegenTableDO masterTable = new CodegenTableDO();
|
||||
masterTable.setTemplateType(CodegenTemplateTypeEnum.MASTER_INNER.getType());
|
||||
|
||||
// 子表配置
|
||||
CodegenTableDO subTable = new CodegenTableDO();
|
||||
subTable.setMasterTableId(masterTable.getId());
|
||||
subTable.setSubJoinColumnName("master_id");
|
||||
subTable.setSubJoinMany(true);
|
||||
subTable.setTemplateType(CodegenTemplateTypeEnum.SUB.getType());
|
||||
```
|
||||
|
||||
### 树表生成
|
||||
```java
|
||||
CodegenTableDO treeTable = new CodegenTableDO();
|
||||
treeTable.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
treeTable.setTreeParentColumnName("parent_id");
|
||||
treeTable.setTreeNameColumnName("name");
|
||||
```
|
||||
|
||||
### 字段高级配置
|
||||
```java
|
||||
// 字典类型设置
|
||||
CodegenColumnDO column = new CodegenColumnDO();
|
||||
column.setDictType("common_status");
|
||||
|
||||
// HTML 类型设置
|
||||
column.setHtmlType("select"); // 下拉框
|
||||
column.setHtmlType("radio"); // 单选框
|
||||
column.setHtmlType("checkbox"); // 复选框
|
||||
column.setHtmlType("textarea"); // 文本域
|
||||
column.setHtmlType("datetime"); // 日期时间
|
||||
|
||||
// 查询条件设置
|
||||
column.setListOperationCondition("LIKE"); // 模糊查询
|
||||
column.setListOperationCondition("BETWEEN"); // 范围查询
|
||||
column.setListOperationCondition("IN"); // 包含查询
|
||||
```
|
||||
|
||||
## 自定义模板
|
||||
|
||||
### 模板引擎
|
||||
代码生成器使用 **Velocity** 模板引擎,支持自定义模板。
|
||||
|
||||
### 模板变量
|
||||
```velocity
|
||||
## 表信息
|
||||
$table.tableName ## 表名
|
||||
$table.className ## 类名
|
||||
$table.classComment ## 类注释
|
||||
$table.moduleName ## 模块名
|
||||
$table.businessName ## 业务名
|
||||
|
||||
## 字段信息
|
||||
#foreach($column in $columns)
|
||||
$column.columnName ## 字段名
|
||||
$column.javaField ## Java 属性名
|
||||
$column.javaType ## Java 类型
|
||||
$column.columnComment ## 字段注释
|
||||
$column.htmlType ## HTML 类型
|
||||
#end
|
||||
|
||||
## 工具方法
|
||||
$tool.firstLower($table.className) ## 首字母小写
|
||||
$tool.firstUpper($table.businessName) ## 首字母大写
|
||||
```
|
||||
|
||||
### 自定义模板示例
|
||||
```velocity
|
||||
package ${basePackage}.module.${table.moduleName}.enums;
|
||||
|
||||
import ${baseFrameworkPackage}.common.core.IntArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* ${table.classComment} 枚举类
|
||||
*
|
||||
* @author ${author}
|
||||
* @date ${dateTime}
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum ${table.className}Enum implements IntArrayValuable {
|
||||
|
||||
#foreach($column in $columns)
|
||||
#if($column.dictType && $column.dictType != "")
|
||||
// TODO: 根据字典类型 ${column.dictType} 补充枚举值
|
||||
#end
|
||||
#end
|
||||
|
||||
;
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(${table.className}Enum::getValue).toArray();
|
||||
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
private final Integer value;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
@Override
|
||||
public int[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 代码生成最佳实践
|
||||
|
||||
### 数据库表设计规范
|
||||
1. **表名规范**: 使用模块前缀,如 `system_user`
|
||||
2. **字段规范**: 包含基础字段(id, create_time, update_time 等)
|
||||
3. **注释完整**: 表和字段都要有详细注释
|
||||
4. **类型选择**: 合理选择字段类型和长度
|
||||
|
||||
### 生成前配置检查
|
||||
1. **模块名**: 确认模块名正确
|
||||
2. **业务名**: 设置有意义的业务名
|
||||
3. **类注释**: 完善类注释信息
|
||||
4. **字段配置**: 检查字段的操作权限和显示类型
|
||||
5. **字典类型**: 为枚举字段设置字典类型
|
||||
|
||||
### 生成后代码优化
|
||||
1. **业务逻辑**: 添加具体的业务校验逻辑
|
||||
2. **权限菜单**: 导入生成的菜单权限 SQL
|
||||
3. **前端优化**: 根据业务需求调整前端页面
|
||||
4. **测试验证**: 运行单元测试验证代码正确性
|
||||
|
||||
### 版本控制
|
||||
1. **代码备份**: 生成前备份现有代码
|
||||
2. **分支管理**: 在独立分支中生成代码
|
||||
3. **合并策略**: 谨慎合并生成的代码到主分支
|
||||
4. **冲突解决**: 手工解决代码冲突
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 自定义模板类型
|
||||
```java
|
||||
@Component
|
||||
public class CustomCodegenTemplateLoader implements CodegenTemplateLoader {
|
||||
|
||||
@Override
|
||||
public List<CodegenTemplateDO> getTemplateList(Integer templateType) {
|
||||
if (Objects.equals(templateType, CustomTemplateTypeEnum.CUSTOM.getType())) {
|
||||
return loadCustomTemplates();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<CodegenTemplateDO> loadCustomTemplates() {
|
||||
// 加载自定义模板
|
||||
return Arrays.asList(
|
||||
buildTemplate("custom/controller.vm", "${table.moduleName}/${table.businessName}/controller/${table.className}Controller.java"),
|
||||
buildTemplate("custom/service.vm", "${table.moduleName}/${table.businessName}/service/${table.className}Service.java")
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义字段类型映射
|
||||
```java
|
||||
@Component
|
||||
public class CustomColumnTypeMapping implements ColumnTypeMapping {
|
||||
|
||||
@Override
|
||||
public String getJavaType(String columnType) {
|
||||
if ("json".equalsIgnoreCase(columnType)) {
|
||||
return "String"; // JSON 字段映射为 String
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHtmlType(CodegenColumnDO column) {
|
||||
if ("json".equalsIgnoreCase(column.getDataType())) {
|
||||
return "textarea"; // JSON 字段使用文本域
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码生成插件
|
||||
```java
|
||||
@Component
|
||||
public class CodegenPlugin {
|
||||
|
||||
/**
|
||||
* 生成前处理
|
||||
*/
|
||||
public void beforeGenerate(CodegenTableDO table, List<CodegenColumnDO> columns) {
|
||||
// 自定义预处理逻辑
|
||||
log.info("开始生成代码: {}", table.getClassName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成后处理
|
||||
*/
|
||||
public void afterGenerate(CodegenTableDO table, Map<String, String> result) {
|
||||
// 自定义后处理逻辑
|
||||
log.info("代码生成完成: {}, 文件数: {}", table.getClassName(), result.size());
|
||||
|
||||
// 可以在这里执行:
|
||||
// 1. 代码格式化
|
||||
// 2. 文件写入磁盘
|
||||
// 3. Git 提交
|
||||
// 4. 发送通知
|
||||
}
|
||||
}
|
||||
```
|
189
.cursor/rules/common-patterns.mdc
Normal file
189
.cursor/rules/common-patterns.mdc
Normal file
@@ -0,0 +1,189 @@
|
||||
# 常用开发模式指南
|
||||
|
||||
## Controller 层开发模式
|
||||
|
||||
### 标准 REST API 模式
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/system/user")
|
||||
@Validated
|
||||
@Api(tags = "管理后台 - 用户")
|
||||
public class UserController {
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建用户")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:create')")
|
||||
public CommonResult<Long> createUser(@Valid @RequestBody UserCreateReqVO reqVO) {
|
||||
return success(userService.createUser(reqVO));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页查询模式
|
||||
```java
|
||||
@GetMapping("/page")
|
||||
@ApiOperation("获得用户分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO reqVO) {
|
||||
PageResult<UserDO> pageResult = userService.getUserPage(reqVO);
|
||||
return success(UserConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
```
|
||||
|
||||
## Service 层开发模式
|
||||
|
||||
### 事务处理模式
|
||||
```java
|
||||
@Service
|
||||
@Validated
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createUser(UserCreateReqVO reqVO) {
|
||||
// 校验用户存在
|
||||
validateUserExists(reqVO.getUsername());
|
||||
// 插入用户
|
||||
UserDO user = UserConvert.INSTANCE.convert(reqVO);
|
||||
userMapper.insert(user);
|
||||
return user.getId();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据访问层模式
|
||||
|
||||
### MyBatis Plus 使用
|
||||
```java
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapperX<UserDO> {
|
||||
|
||||
default PageResult<UserDO> selectPage(UserPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<UserDO>()
|
||||
.likeIfPresent(UserDO::getUsername, reqVO.getUsername())
|
||||
.eqIfPresent(UserDO::getStatus, reqVO.getStatus())
|
||||
.betweenIfPresent(UserDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(UserDO::getId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 权限控制模式
|
||||
|
||||
### 菜单权限
|
||||
```java
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
```
|
||||
|
||||
### 数据权限
|
||||
```java
|
||||
@DataPermission(deptAlias = "d", userAlias = "u")
|
||||
```
|
||||
|
||||
### 多租户
|
||||
```java
|
||||
@TenantIgnore // 忽略多租户
|
||||
@TenantInfo // 获取租户信息
|
||||
```
|
||||
|
||||
## 异常处理模式
|
||||
|
||||
### 业务异常
|
||||
```java
|
||||
// 抛出业务异常
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
|
||||
// 在 ErrorCodeConstants 中定义
|
||||
ErrorCode USER_NOT_EXISTS = new ErrorCode(1002001001, "用户不存在");
|
||||
```
|
||||
|
||||
### 全局异常处理
|
||||
框架自动处理,返回统一格式的错误响应。
|
||||
|
||||
## 对象转换模式
|
||||
|
||||
### MapStruct 转换器
|
||||
```java
|
||||
@Mapper
|
||||
public interface UserConvert {
|
||||
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
|
||||
|
||||
UserDO convert(UserCreateReqVO bean);
|
||||
UserRespVO convert(UserDO bean);
|
||||
PageResult<UserRespVO> convertPage(PageResult<UserDO> page);
|
||||
}
|
||||
```
|
||||
|
||||
## 枚举使用模式
|
||||
|
||||
### 通用枚举
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum CommonStatusEnum implements IntArrayValuable {
|
||||
ENABLE(0, "开启"),
|
||||
DISABLE(1, "关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
}
|
||||
```
|
||||
|
||||
## 配置管理模式
|
||||
|
||||
### 配置类定义
|
||||
```java
|
||||
@ConfigurationProperties(prefix = "yudao.security")
|
||||
@Data
|
||||
public class SecurityProperties {
|
||||
private Set<String> permitAllUrls = new HashSet<>();
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存使用模式
|
||||
|
||||
### Redis 缓存
|
||||
```java
|
||||
@Cacheable(value = RedisKeyConstants.USER, key = "#id")
|
||||
public UserDO getUser(Long id) {
|
||||
return userMapper.selectById(id);
|
||||
}
|
||||
```
|
||||
|
||||
## 消息队列模式
|
||||
|
||||
### 发送消息
|
||||
```java
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// 发送消息
|
||||
redisTemplate.convertAndSend("user.create", userId);
|
||||
```
|
||||
|
||||
## 单元测试模式
|
||||
|
||||
### 服务层测试
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private UserServiceImpl userService;
|
||||
|
||||
@Mock
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Test
|
||||
void testCreateUser() {
|
||||
// given
|
||||
UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class);
|
||||
|
||||
// when
|
||||
Long userId = userService.createUser(reqVO);
|
||||
|
||||
// then
|
||||
assertNotNull(userId);
|
||||
verify(userMapper).insert(any());
|
||||
}
|
||||
}
|
||||
```
|
295
.cursor/rules/database-design.mdc
Normal file
295
.cursor/rules/database-design.mdc
Normal file
@@ -0,0 +1,295 @@
|
||||
# 数据库设计规范
|
||||
|
||||
## 表命名规范
|
||||
|
||||
### 模块表前缀
|
||||
- **系统模块**: `system_` (如: system_users, system_role)
|
||||
- **基础设施**: `infra_` (如: infra_file, infra_config)
|
||||
- **工作流**: `bpm_` (如: bpm_process_instance)
|
||||
- **支付模块**: `pay_` (如: pay_order, pay_channel)
|
||||
- **商城模块**: `mall_` (如: mall_product, mall_order)
|
||||
- **会员模块**: `member_` (如: member_user, member_level)
|
||||
- **CRM模块**: `crm_` (如: crm_customer, crm_contract)
|
||||
- **ERP模块**: `erp_` (如: erp_product, erp_stock)
|
||||
|
||||
### 表名规范
|
||||
- 使用小写字母和下划线
|
||||
- 表名要有意义,能表达业务含义
|
||||
- 避免使用系统保留字
|
||||
- 中间表命名:`主表_从表` 或 `模块_relation`
|
||||
|
||||
## 字段设计规范
|
||||
|
||||
### 必备字段 (BaseEntity)
|
||||
每个业务表都应包含以下基础字段:
|
||||
```sql
|
||||
-- 主键 (雪花算法生成)
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
|
||||
-- 创建信息
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
-- 更新信息
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
-- 逻辑删除
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
|
||||
-- 多租户 (SaaS版本必须)
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
||||
```
|
||||
|
||||
### 字段命名规范
|
||||
- 使用小写字母和下划线
|
||||
- 布尔类型字段使用 `is_` 前缀
|
||||
- 时间字段使用 `_time` 后缀
|
||||
- 状态字段使用 `status` 或 `state`
|
||||
- 外键字段使用 `表名_id` 格式
|
||||
|
||||
### 字段类型选择
|
||||
```sql
|
||||
-- 主键
|
||||
BIGINT AUTO_INCREMENT
|
||||
|
||||
-- 字符串
|
||||
VARCHAR(255) -- 普通字符串
|
||||
VARCHAR(1024) -- 长字符串
|
||||
TEXT -- 超长文本 (如备注)
|
||||
|
||||
-- 数值
|
||||
TINYINT -- 状态、类型 (0-255)
|
||||
SMALLINT -- 小整数 (-32768 到 32767)
|
||||
INT -- 普通整数
|
||||
BIGINT -- 大整数、金额(分)
|
||||
DECIMAL(10,2) -- 精确小数 (金额元)
|
||||
|
||||
-- 时间
|
||||
DATETIME -- 日期时间
|
||||
DATE -- 日期
|
||||
TIME -- 时间
|
||||
|
||||
-- 布尔
|
||||
BIT -- 是否字段 (0/1)
|
||||
```
|
||||
|
||||
## 索引设计规范
|
||||
|
||||
### 主键索引
|
||||
```sql
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
```
|
||||
|
||||
### 普通索引
|
||||
```sql
|
||||
-- 单列索引
|
||||
KEY `idx_username` (`username`) USING BTREE,
|
||||
KEY `idx_create_time` (`create_time`) USING BTREE,
|
||||
KEY `idx_status` (`status`) USING BTREE,
|
||||
|
||||
-- 复合索引 (注意字段顺序)
|
||||
KEY `idx_tenant_status` (`tenant_id`, `status`) USING BTREE,
|
||||
KEY `idx_user_dept` (`user_id`, `dept_id`) USING BTREE
|
||||
```
|
||||
|
||||
### 唯一索引
|
||||
```sql
|
||||
-- 业务唯一约束
|
||||
UNIQUE KEY `uk_username` (`username`, `tenant_id`) USING BTREE,
|
||||
UNIQUE KEY `uk_mobile` (`mobile`) USING BTREE
|
||||
```
|
||||
|
||||
### 索引设计原则
|
||||
1. **选择性高的字段**: 优先为区分度高的字段建索引
|
||||
2. **查询频繁的字段**: 经常用于 WHERE 条件的字段
|
||||
3. **复合索引顺序**: 高选择性字段在前,查询条件字段在前
|
||||
4. **避免过多索引**: 影响写入性能,一般不超过5个
|
||||
|
||||
## 多租户设计
|
||||
|
||||
### 租户隔离方案
|
||||
```sql
|
||||
-- 所有业务表都添加租户字段
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
|
||||
-- 复合唯一索引包含租户ID
|
||||
UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`) USING BTREE,
|
||||
|
||||
-- 查询索引包含租户ID
|
||||
KEY `idx_tenant_status` (`tenant_id`, `status`) USING BTREE
|
||||
```
|
||||
|
||||
### 系统表设计
|
||||
系统级表不需要租户隔离:
|
||||
```sql
|
||||
-- 租户表本身
|
||||
CREATE TABLE system_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '租户编号',
|
||||
name VARCHAR(30) NOT NULL COMMENT '租户名',
|
||||
-- ... 其他字段,无需 tenant_id
|
||||
);
|
||||
|
||||
-- 租户套餐表
|
||||
CREATE TABLE system_tenant_package (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '套餐编号',
|
||||
name VARCHAR(30) NOT NULL COMMENT '套餐名',
|
||||
-- ... 其他字段,无需 tenant_id
|
||||
);
|
||||
```
|
||||
|
||||
## 逻辑删除设计
|
||||
|
||||
### 删除标记
|
||||
```sql
|
||||
-- 使用 BIT 类型
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
|
||||
-- 0: 未删除
|
||||
-- 1: 已删除
|
||||
```
|
||||
|
||||
### 查询处理
|
||||
```sql
|
||||
-- 查询时过滤已删除数据
|
||||
SELECT * FROM system_users
|
||||
WHERE deleted = 0
|
||||
AND tenant_id = #{tenantId};
|
||||
```
|
||||
|
||||
### 唯一约束处理
|
||||
```sql
|
||||
-- 唯一索引需要包含 deleted 字段
|
||||
UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`, `deleted`) USING BTREE
|
||||
```
|
||||
|
||||
## 常用表结构示例
|
||||
|
||||
### 用户表
|
||||
```sql
|
||||
CREATE TABLE system_users (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
username VARCHAR(30) NOT NULL COMMENT '用户账号',
|
||||
password VARCHAR(100) DEFAULT '' COMMENT '密码',
|
||||
nickname VARCHAR(30) NOT NULL COMMENT '用户昵称',
|
||||
email VARCHAR(50) DEFAULT '' COMMENT '用户邮箱',
|
||||
mobile VARCHAR(11) DEFAULT '' COMMENT '手机号码',
|
||||
sex TINYINT DEFAULT 0 COMMENT '用户性别',
|
||||
avatar VARCHAR(512) DEFAULT '' COMMENT '头像地址',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '帐号状态',
|
||||
login_ip VARCHAR(50) DEFAULT '' COMMENT '最后登录IP',
|
||||
login_date DATETIME COMMENT '最后登录时间',
|
||||
|
||||
-- 基础字段
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
|
||||
-- 索引
|
||||
PRIMARY KEY (id) USING BTREE,
|
||||
UNIQUE KEY uk_username (username, tenant_id, deleted) USING BTREE,
|
||||
UNIQUE KEY uk_mobile (mobile, tenant_id, deleted) USING BTREE,
|
||||
KEY idx_tenant_status (tenant_id, status) USING BTREE,
|
||||
KEY idx_create_time (create_time) USING BTREE
|
||||
) ENGINE=InnoDB COMMENT='用户信息表';
|
||||
```
|
||||
|
||||
### 配置表
|
||||
```sql
|
||||
CREATE TABLE infra_config (
|
||||
id INT NOT NULL AUTO_INCREMENT COMMENT '参数主键',
|
||||
category VARCHAR(50) DEFAULT NULL COMMENT '参数分组',
|
||||
type TINYINT NOT NULL COMMENT '参数类型',
|
||||
name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '参数名称',
|
||||
config_key VARCHAR(100) NOT NULL DEFAULT '' COMMENT '参数键名',
|
||||
value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '参数键值',
|
||||
visible BIT NOT NULL COMMENT '是否可见',
|
||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||
|
||||
-- 基础字段
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
|
||||
-- 索引
|
||||
PRIMARY KEY (id) USING BTREE,
|
||||
UNIQUE KEY uk_config_key (config_key, deleted) USING BTREE,
|
||||
KEY idx_category (category) USING BTREE,
|
||||
KEY idx_type (type) USING BTREE
|
||||
) ENGINE=InnoDB COMMENT='参数配置表';
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 分表分库策略
|
||||
```sql
|
||||
-- 按租户分表 (大租户场景)
|
||||
CREATE TABLE system_users_1 LIKE system_users;
|
||||
CREATE TABLE system_users_2 LIKE system_users;
|
||||
|
||||
-- 按时间分表 (日志表)
|
||||
CREATE TABLE system_operate_log_202401 LIKE system_operate_log;
|
||||
CREATE TABLE system_operate_log_202402 LIKE system_operate_log;
|
||||
```
|
||||
|
||||
### 读写分离
|
||||
- 主库: 写操作
|
||||
- 从库: 读操作
|
||||
- 使用 `@DS("slave")` 注解指定数据源
|
||||
|
||||
### 连接池配置
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
validation-query: SELECT 1
|
||||
```
|
||||
|
||||
## 数据库变更管理
|
||||
|
||||
### 版本控制
|
||||
- 使用 Flyway 管理数据库版本
|
||||
- SQL 文件命名: `V{版本号}__{描述}.sql`
|
||||
- 存放路径: `src/main/resources/db/migration/`
|
||||
|
||||
### 变更脚本示例
|
||||
```sql
|
||||
-- V2.6.0__add_user_avatar.sql
|
||||
ALTER TABLE system_users
|
||||
ADD COLUMN avatar VARCHAR(512) DEFAULT '' COMMENT '头像地址'
|
||||
AFTER mobile;
|
||||
|
||||
-- 更新索引
|
||||
ALTER TABLE system_users
|
||||
ADD INDEX idx_avatar (avatar) USING BTREE;
|
||||
```
|
||||
|
||||
### 数据迁移
|
||||
```sql
|
||||
-- 数据迁移脚本
|
||||
INSERT INTO new_table (field1, field2)
|
||||
SELECT old_field1, old_field2
|
||||
FROM old_table
|
||||
WHERE condition;
|
||||
```
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 性能监控
|
||||
- 慢查询日志: `slow_query_log = ON`
|
||||
- 执行计划分析: `EXPLAIN` 语句
|
||||
- 索引使用率: `SHOW INDEX` 统计
|
||||
|
||||
### 定期维护
|
||||
- 表空间清理: `OPTIMIZE TABLE`
|
||||
- 统计信息更新: `ANALYZE TABLE`
|
||||
- 索引重建: `ALTER TABLE ... ENGINE=InnoDB`
|
171
.cursor/rules/deployment-guide.mdc
Normal file
171
.cursor/rules/deployment-guide.mdc
Normal file
@@ -0,0 +1,171 @@
|
||||
# 项目启动和部署指南
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 环境要求
|
||||
- **JDK**: 1.8+ (推荐 JDK 8 或 JDK 17)
|
||||
- **Maven**: 3.6+
|
||||
- **MySQL**: 5.7+ 或 8.0+
|
||||
- **Redis**: 5.0+
|
||||
- **Node.js**: 16+ (前端项目)
|
||||
|
||||
### 启动步骤
|
||||
|
||||
#### 1. 数据库初始化
|
||||
```sql
|
||||
-- 创建数据库
|
||||
CREATE DATABASE ruoyi_vue_pro;
|
||||
|
||||
-- 导入数据表结构和基础数据
|
||||
-- 使用 sql/mysql/ruoyi-vue-pro.sql
|
||||
```
|
||||
SQL 脚本路径:[sql/mysql/ruoyi-vue-pro.sql](mdc:sql/mysql/ruoyi-vue-pro.sql)
|
||||
|
||||
#### 2. 配置修改
|
||||
编辑配置文件:[application.yaml](mdc:yudao-server/src/main/resources/application.yaml)
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi_vue_pro
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
```
|
||||
|
||||
#### 3. 启动后端服务
|
||||
运行主类:[YudaoServerApplication.java](mdc:yudao-server/src/main/java/cn/iocoder/yudao/server/YudaoServerApplication.java)
|
||||
```bash
|
||||
# 或使用 Maven 命令
|
||||
mvn spring-boot:run -f yudao-server/pom.xml
|
||||
```
|
||||
|
||||
#### 4. 访问管理后台
|
||||
- **后端 API**: http://localhost:48080
|
||||
- **API 文档**: http://localhost:48080/doc.html
|
||||
- **默认账号**: admin / admin123
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### Docker Compose 部署
|
||||
使用提供的 Docker 配置:[script/docker/docker-compose.yml](mdc:script/docker/docker-compose.yml)
|
||||
|
||||
```bash
|
||||
cd script/docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 自定义 Docker 镜像
|
||||
使用项目的 Dockerfile:[yudao-server/Dockerfile](mdc:yudao-server/Dockerfile)
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t yudao-server .
|
||||
|
||||
# 运行容器
|
||||
docker run -d -p 48080:48080 yudao-server
|
||||
```
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 配置优化
|
||||
1. **数据库连接池调优**
|
||||
2. **Redis 连接配置**
|
||||
3. **JVM 参数优化**
|
||||
4. **日志级别调整**
|
||||
|
||||
### 部署脚本
|
||||
使用提供的部署脚本:[script/shell/deploy.sh](mdc:script/shell/deploy.sh)
|
||||
|
||||
### 监控配置
|
||||
- **应用监控**: Spring Boot Admin
|
||||
- **链路追踪**: SkyWalking
|
||||
- **日志收集**: 集成日志中心
|
||||
|
||||
## 前端项目启动
|
||||
|
||||
### Vue3 版本 (推荐)
|
||||
```bash
|
||||
git clone https://gitee.com/yudaocode/yudao-ui-admin-vue3.git
|
||||
cd yudao-ui-admin-vue3
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Vue2 版本
|
||||
```bash
|
||||
git clone https://gitee.com/yudaocode/yudao-ui-admin-vue2.git
|
||||
cd yudao-ui-admin-vue2
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 启动失败
|
||||
1. 检查数据库连接配置
|
||||
2. 确认 Redis 服务正常
|
||||
3. 查看启动日志错误信息
|
||||
4. 参考官方文档: https://doc.iocoder.cn/quick-start/
|
||||
|
||||
### 端口冲突
|
||||
默认端口 48080,可在配置文件中修改:
|
||||
```yaml
|
||||
server:
|
||||
port: 48080
|
||||
```
|
||||
|
||||
### 内存不足
|
||||
调整 JVM 参数:
|
||||
```bash
|
||||
java -Xms512m -Xmx1024m -jar yudao-server.jar
|
||||
```
|
||||
|
||||
## 多环境配置
|
||||
|
||||
### 开发环境
|
||||
- 配置文件:`application-dev.yaml`
|
||||
- 数据库:本地 MySQL
|
||||
- Redis:本地 Redis
|
||||
|
||||
### 测试环境
|
||||
- 配置文件:`application-test.yaml`
|
||||
- 外部数据库和 Redis
|
||||
|
||||
### 生产环境
|
||||
- 配置文件:`application-prod.yaml`
|
||||
- 高可用数据库集群
|
||||
- Redis 集群
|
||||
|
||||
## CI/CD 集成
|
||||
|
||||
### Jenkins 部署
|
||||
使用提供的 Jenkins 配置:[script/jenkins/Jenkinsfile](mdc:script/jenkins/Jenkinsfile)
|
||||
|
||||
### 自动化部署流程
|
||||
1. 代码提交触发构建
|
||||
2. 执行单元测试
|
||||
3. 构建 Docker 镜像
|
||||
4. 部署到目标环境
|
||||
5. 健康检查
|
||||
|
||||
## 性能调优
|
||||
|
||||
### 数据库优化
|
||||
- 添加适当索引
|
||||
- 分库分表(如需要)
|
||||
- 读写分离配置
|
||||
|
||||
### 缓存策略
|
||||
- Redis 缓存热点数据
|
||||
- 本地缓存配置
|
||||
- 缓存过期策略
|
||||
|
||||
### 应用优化
|
||||
- 连接池参数调优
|
||||
- 线程池配置
|
||||
- GC 参数优化
|
82
.cursor/rules/development-guide.mdc
Normal file
82
.cursor/rules/development-guide.mdc
Normal file
@@ -0,0 +1,82 @@
|
||||
# 开发规范指南
|
||||
|
||||
## 代码规范
|
||||
遵循《阿里巴巴 Java 开发手册》规范,代码注释详细。
|
||||
|
||||
## 新增业务模块步骤
|
||||
|
||||
### 1. 创建模块目录
|
||||
```
|
||||
yudao-module-{模块名}/
|
||||
├── pom.xml
|
||||
└── src/main/java/cn/iocoder/yudao/module/{模块名}/
|
||||
```
|
||||
|
||||
### 2. 标准包结构
|
||||
```java
|
||||
cn.iocoder.yudao.module.{模块名}
|
||||
├── api/
|
||||
│ ├── {功能}Api.java // 对外接口定义
|
||||
│ └── {功能}ApiImpl.java // 接口实现
|
||||
├── controller/
|
||||
│ └── admin/ // 管理后台控制器
|
||||
│ └── {功能}Controller.java
|
||||
├── service/
|
||||
│ ├── {功能}Service.java // 业务接口
|
||||
│ └── {功能}ServiceImpl.java // 业务实现
|
||||
├── dal/
|
||||
│ ├── dataobject/ // 数据对象
|
||||
│ │ └── {功能}DO.java
|
||||
│ └── mysql/ // MySQL 实现
|
||||
│ └── {功能}Mapper.java
|
||||
├── convert/
|
||||
│ └── {功能}Convert.java // 对象转换
|
||||
├── enums/
|
||||
│ └── {功能}Enum.java // 枚举定义
|
||||
└── framework/
|
||||
└── {模块}Configuration.java // 配置类
|
||||
```
|
||||
|
||||
### 3. 核心注解和工具类使用
|
||||
|
||||
#### 权限控制
|
||||
```java
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
```
|
||||
|
||||
#### 数据权限
|
||||
```java
|
||||
@DataPermission(deptAlias = "d")
|
||||
```
|
||||
|
||||
#### 多租户
|
||||
```java
|
||||
@TenantIgnore // 忽略多租户
|
||||
```
|
||||
|
||||
#### 操作日志
|
||||
```java
|
||||
@OperateLog(type = CREATE)
|
||||
```
|
||||
|
||||
### 4. API 设计规范
|
||||
- 使用 RESTful 风格
|
||||
- 统一返回 `CommonResult<T>` 格式
|
||||
- 使用 `@Valid` 进行参数校验
|
||||
- 使用 `@ApiOperation` 添加接口文档
|
||||
|
||||
### 5. 数据库设计规范
|
||||
- 表名使用模块前缀:`{模块名}_{表名}`
|
||||
- 必备字段:`id`, `create_time`, `update_time`, `creator`, `updater`, `deleted`, `tenant_id`
|
||||
- 使用 MyBatis Plus 的 `BaseEntity` 基类
|
||||
|
||||
## 代码生成器使用
|
||||
1. 访问管理后台的代码生成功能
|
||||
2. 导入数据库表
|
||||
3. 配置生成选项
|
||||
4. 一键生成前后端代码
|
||||
|
||||
## 测试规范
|
||||
- 单元测试使用 JUnit 5 + Mockito
|
||||
- 集成测试继承 `BaseDbUnitTest`
|
||||
- 测试配置文件:[application-unit-test.yaml](mdc:yudao-module-system/src/test/resources/application-unit-test.yaml)
|
906
.cursor/rules/file-upload-download.mdc
Normal file
906
.cursor/rules/file-upload-download.mdc
Normal file
@@ -0,0 +1,906 @@
|
||||
# 文件上传下载指南
|
||||
|
||||
## 文件服务架构
|
||||
|
||||
### 存储方案
|
||||
- **本地存储**: 直接存储到服务器磁盘
|
||||
- **数据库存储**: 文件内容存储到数据库
|
||||
- **云存储**: 对接阿里云 OSS、腾讯云 COS、七牛云等
|
||||
- **FTP 存储**: 通过 FTP 协议存储文件
|
||||
- **S3 存储**: 兼容 AWS S3 协议(如 MinIO)
|
||||
|
||||
### 文件管理表结构
|
||||
```sql
|
||||
-- 文件配置表
|
||||
CREATE TABLE infra_file_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
name VARCHAR(63) NOT NULL COMMENT '配置名',
|
||||
storage TINYINT NOT NULL COMMENT '存储器',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
master BIT NOT NULL COMMENT '是否为主配置',
|
||||
config VARCHAR(4096) NOT NULL COMMENT '存储配置',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
|
||||
-- 文件表
|
||||
CREATE TABLE infra_file (
|
||||
id VARCHAR(32) NOT NULL COMMENT '文件编号',
|
||||
config_id BIGINT NOT NULL COMMENT '配置编号',
|
||||
name VARCHAR(256) DEFAULT NULL COMMENT '文件名',
|
||||
path VARCHAR(512) NOT NULL COMMENT '文件路径',
|
||||
url VARCHAR(1024) NOT NULL COMMENT '文件 URL',
|
||||
type VARCHAR(128) DEFAULT NULL COMMENT 'MIME 类型',
|
||||
size INT NOT NULL COMMENT '文件大小',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
```
|
||||
|
||||
## 文件客户端抽象
|
||||
|
||||
### 文件客户端接口
|
||||
```java
|
||||
public interface FileClient {
|
||||
|
||||
/**
|
||||
* 获得客户端编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
String upload(byte[] content, String path, String type) throws Exception;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
void delete(String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件的内容
|
||||
*/
|
||||
byte[] getContent(String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件的预签名 URL
|
||||
*/
|
||||
FilePresignedUrlRespDTO getPresignedObjectUrl(String path, Duration expiration) throws Exception;
|
||||
}
|
||||
```
|
||||
|
||||
### 抽象文件客户端
|
||||
```java
|
||||
public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {
|
||||
|
||||
protected Long id;
|
||||
protected Config config;
|
||||
|
||||
public AbstractFileClient(Long id, Config config) {
|
||||
this.id = id;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][配置({}) 初始化完成]", config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
/**
|
||||
* 刷新配置
|
||||
*/
|
||||
public final void refresh(Config config) {
|
||||
// 判断是否更新
|
||||
if (this.config.equals(config)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", config);
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 具体存储实现
|
||||
|
||||
### 本地存储客户端
|
||||
```java
|
||||
public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
|
||||
|
||||
public LocalFileClient(Long id, LocalFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全风格
|
||||
if (!config.getBasePath().endsWith(File.separator)) {
|
||||
config.setBasePath(config.getBasePath() + File.separator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path, String type) throws Exception {
|
||||
// 执行写入
|
||||
String filePath = getFilePath(path);
|
||||
FileUtil.writeBytes(content, filePath);
|
||||
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws Exception {
|
||||
String filePath = getFilePath(path);
|
||||
FileUtil.del(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) throws Exception {
|
||||
String filePath = getFilePath(path);
|
||||
return FileUtil.readBytes(filePath);
|
||||
}
|
||||
|
||||
private String getFilePath(String path) {
|
||||
return config.getBasePath() + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path, Duration expiration) {
|
||||
throw new UnsupportedOperationException("本地存储不支持预签名 URL");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### S3 存储客户端
|
||||
```java
|
||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
private AmazonS3 client;
|
||||
|
||||
public S3FileClient(Long id, S3FileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 初始化客户端
|
||||
AWSCredentials credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
ClientConfiguration clientConfig = new ClientConfiguration();
|
||||
if (StrUtil.isNotEmpty(config.getRegion())) {
|
||||
clientConfig.setSignerOverride("AWSS3V4SignerType");
|
||||
}
|
||||
|
||||
// 创建 client
|
||||
this.client = AmazonS3ClientBuilder.standard()
|
||||
.withCredentials(credentialsProvider)
|
||||
.withClientConfiguration(clientConfig)
|
||||
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(
|
||||
config.getEndpoint(), config.getRegion()))
|
||||
.withPathStyleAccessEnabled(config.getPathStyleAccess())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path, String type) throws Exception {
|
||||
// 执行上传
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(content.length);
|
||||
metadata.setContentType(type);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
|
||||
client.putObject(config.getBucket(), path, inputStream, metadata);
|
||||
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws Exception {
|
||||
client.deleteObject(config.getBucket(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) throws Exception {
|
||||
S3Object object = client.getObject(config.getBucket(), path);
|
||||
return IoUtil.readBytes(object.getObjectContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path, Duration expiration) throws Exception {
|
||||
Date expireDate = new Date(System.currentTimeMillis() + expiration.toMillis());
|
||||
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(config.getBucket(), path)
|
||||
.withMethod(HttpMethod.GET)
|
||||
.withExpiration(expireDate);
|
||||
URL url = client.generatePresignedUrl(request);
|
||||
return new FilePresignedUrlRespDTO(url.toString(),
|
||||
LocalDateTime.now().plus(expiration));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 阿里云 OSS 客户端
|
||||
```java
|
||||
public class AliyunFileClient extends AbstractFileClient<AliyunFileClientConfig> {
|
||||
|
||||
private OSS client;
|
||||
|
||||
public AliyunFileClient(Long id, AliyunFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全 endpoint
|
||||
if (!config.getEndpoint().contains("://")) {
|
||||
config.setEndpoint("https://" + config.getEndpoint());
|
||||
}
|
||||
// 初始化客户端
|
||||
client = new OSSClientBuilder().build(config.getEndpoint(),
|
||||
config.getAccessKey(),
|
||||
config.getAccessSecret());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path, String type) throws Exception {
|
||||
// 执行上传
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentLength(content.length);
|
||||
metadata.setContentType(type);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
|
||||
client.putObject(config.getBucket(), path, inputStream, metadata);
|
||||
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws Exception {
|
||||
client.deleteObject(config.getBucket(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) throws Exception {
|
||||
OSSObject ossObject = client.getObject(config.getBucket(), path);
|
||||
return IoUtil.readBytes(ossObject.getObjectContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path, Duration expiration) throws Exception {
|
||||
Date expireDate = new Date(System.currentTimeMillis() + expiration.toMillis());
|
||||
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(config.getBucket(), path, HttpMethod.GET);
|
||||
request.setExpiration(expireDate);
|
||||
URL url = client.generatePresignedUrl(request);
|
||||
return new FilePresignedUrlRespDTO(url.toString(),
|
||||
LocalDateTime.now().plus(expiration));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文件服务实现
|
||||
|
||||
### 文件服务接口
|
||||
```java
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@Resource
|
||||
private FileMapper fileMapper;
|
||||
|
||||
@Resource
|
||||
private FileClientFactory fileClientFactory;
|
||||
|
||||
@Override
|
||||
public String createFile(String name, String path, byte[] content) throws Exception {
|
||||
// 获取文件配置
|
||||
FileConfigDO config = fileConfigService.getMasterFileConfig();
|
||||
Assert.notNull(config, "客户端({}) 不能为空", config);
|
||||
|
||||
// 获取文件客户端
|
||||
FileClient client = fileClientFactory.getFileClient(config.getId());
|
||||
|
||||
// 上传到文件存储器
|
||||
String url = client.upload(content, path, name);
|
||||
|
||||
// 保存到数据库
|
||||
FileDO file = new FileDO();
|
||||
file.setConfigId(config.getId());
|
||||
file.setName(name);
|
||||
file.setPath(path);
|
||||
file.setUrl(url);
|
||||
file.setType(FileTypeUtils.getMimeType(name));
|
||||
file.setSize(content.length);
|
||||
fileMapper.insert(file);
|
||||
return file.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFile(String id) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
|
||||
// 从文件存储器中删除
|
||||
FileClient client = fileClientFactory.getFileClient(file.getConfigId());
|
||||
client.delete(file.getPath());
|
||||
|
||||
// 删除记录
|
||||
fileMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFileContent(String id) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
|
||||
// 从文件存储器中获取
|
||||
FileClient client = fileClientFactory.getFileClient(file.getConfigId());
|
||||
return client.getContent(file.getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilePresignedUrlRespDTO getFilePresignedUrl(String id, Duration expiration) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
|
||||
// 生成预签名 URL
|
||||
FileClient client = fileClientFactory.getFileClient(file.getConfigId());
|
||||
return client.getPresignedObjectUrl(file.getPath(), expiration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文件客户端工厂
|
||||
```java
|
||||
@Component
|
||||
public class FileClientFactory {
|
||||
|
||||
/**
|
||||
* 文件客户端 Map
|
||||
* key:配置编号
|
||||
*/
|
||||
private final Map<Long, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@PostConstruct
|
||||
public void initLocalCache() {
|
||||
// 第一步:查询文件配置
|
||||
List<FileConfigDO> configs = fileConfigService.getFileConfigList();
|
||||
log.info("[initLocalCache][缓存文件配置,数量为:{}]", configs.size());
|
||||
|
||||
// 第二步:构建缓存
|
||||
configs.forEach(this::refreshFileClient);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onFileConfigRefresh(FileConfigRefreshMessage message) {
|
||||
refreshFileClient(message.getFileConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得文件客户端
|
||||
*/
|
||||
public FileClient getFileClient(Long configId) {
|
||||
AbstractFileClient<?> client = clients.get(configId);
|
||||
if (client == null) {
|
||||
log.error("[getFileClient][配置编号({}) 找不到客户端]", configId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void refreshFileClient(FileConfigDO config) {
|
||||
// 情况一:如果 config 被禁用,则移除
|
||||
AbstractFileClient<?> client = clients.get(config.getId());
|
||||
if (!config.getStatus()) {
|
||||
if (client != null) {
|
||||
clients.remove(config.getId());
|
||||
log.info("[refreshFileClient][移除文件配置:{}]", config);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:如果 config 存在,则进行更新
|
||||
if (client != null) {
|
||||
client.refresh(config.getConfig());
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况三:如果 client 不存在,则进行创建
|
||||
client = this.createFileClient(config.getId(), config.getStorage(), config.getConfig());
|
||||
client.init();
|
||||
clients.put(client.getId(), client);
|
||||
log.info("[refreshFileClient][添加文件配置:{}]", config);
|
||||
}
|
||||
|
||||
private AbstractFileClient<?> createFileClient(Long configId, Integer storage, Object config) {
|
||||
FileStorageEnum storageEnum = FileStorageEnum.valueOf(storage);
|
||||
switch (storageEnum) {
|
||||
case DB:
|
||||
return new DBFileClient(configId, (DBFileClientConfig) config);
|
||||
case LOCAL:
|
||||
return new LocalFileClient(configId, (LocalFileClientConfig) config);
|
||||
case FTP:
|
||||
return new FtpFileClient(configId, (FtpFileClientConfig) config);
|
||||
case S3:
|
||||
return new S3FileClient(configId, (S3FileClientConfig) config);
|
||||
case ALIYUN:
|
||||
return new AliyunFileClient(configId, (AliyunFileClientConfig) config);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的文件存储器类型(%d)", storage));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文件上传控制器
|
||||
|
||||
### 通用文件上传
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/infra/file")
|
||||
@Api(tags = "管理后台 - 文件存储")
|
||||
@Validated
|
||||
public class FileController {
|
||||
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@PostMapping("/upload")
|
||||
@ApiOperation("上传文件")
|
||||
@OperateLog(logArgs = false) // 上传文件的参数太大,不打印
|
||||
public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
|
||||
String path = generatePath(file);
|
||||
String url = fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()));
|
||||
return success(url);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@ApiOperation("删除文件")
|
||||
public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) throws Exception {
|
||||
fileService.deleteFile(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{configId}/get/**")
|
||||
@ApiOperation("下载文件")
|
||||
@ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = Long.class)
|
||||
public void getFile(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
@PathVariable("configId") Long configId) throws Exception {
|
||||
// 获取请求的路径
|
||||
String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
throw new IllegalArgumentException("结尾的 path 路径必须传递");
|
||||
}
|
||||
|
||||
// 读取内容
|
||||
byte[] content = fileService.getFileContent(configId, path);
|
||||
if (content == null) {
|
||||
log.warn("[getFile][configId({}) path({}) 文件不存在]", configId, path);
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
return;
|
||||
}
|
||||
ServletUtils.writeAttachment(response, path, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件路径
|
||||
*/
|
||||
private String generatePath(MultipartFile file) {
|
||||
String fileName = file.getOriginalFilename();
|
||||
String extName = FileUtil.extName(fileName);
|
||||
return DateUtil.format(new Date(), "yyyy/MM/dd") + "/" + IdUtil.fastUUID() + "." + extName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 头像上传
|
||||
```java
|
||||
@PostMapping("/upload-avatar")
|
||||
@ApiOperation("上传用户头像")
|
||||
public CommonResult<String> uploadAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception {
|
||||
// 校验文件格式
|
||||
validateImageFile(file);
|
||||
|
||||
// 校验文件大小
|
||||
if (file.getSize() > 2 * 1024 * 1024) { // 2MB
|
||||
throw exception(FILE_SIZE_TOO_LARGE);
|
||||
}
|
||||
|
||||
// 生成头像路径
|
||||
String path = generateAvatarPath(file);
|
||||
|
||||
// 上传文件
|
||||
String url = fileService.createFile(file.getOriginalFilename(), path,
|
||||
IoUtil.readBytes(file.getInputStream()));
|
||||
|
||||
// 更新用户头像
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
userService.updateUserAvatar(userId, url);
|
||||
|
||||
return success(url);
|
||||
}
|
||||
|
||||
private void validateImageFile(MultipartFile file) {
|
||||
String fileName = file.getOriginalFilename();
|
||||
String extName = FileUtil.extName(fileName).toLowerCase();
|
||||
|
||||
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "bmp", "webp");
|
||||
if (!allowedExtensions.contains(extName)) {
|
||||
throw exception(FILE_TYPE_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateAvatarPath(MultipartFile file) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
String extName = FileUtil.extName(file.getOriginalFilename());
|
||||
return "avatar/" + userId + "/" + System.currentTimeMillis() + "." + extName;
|
||||
}
|
||||
```
|
||||
|
||||
## 图片处理
|
||||
|
||||
### 图片缩放和裁剪
|
||||
```java
|
||||
@Service
|
||||
public class ImageProcessService {
|
||||
|
||||
/**
|
||||
* 图片缩放
|
||||
*/
|
||||
public byte[] resizeImage(byte[] imageData, int width, int height) {
|
||||
try {
|
||||
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
|
||||
// 计算缩放比例
|
||||
double scaleX = (double) width / originalImage.getWidth();
|
||||
double scaleY = (double) height / originalImage.getHeight();
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
int targetWidth = (int) (originalImage.getWidth() * scale);
|
||||
int targetHeight = (int) (originalImage.getHeight() * scale);
|
||||
|
||||
// 创建缩放后的图片
|
||||
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = resizedImage.createGraphics();
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
|
||||
g2d.dispose();
|
||||
|
||||
// 输出为字节数组
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(resizedImage, "jpg", baos);
|
||||
return baos.toByteArray();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("图片处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片裁剪
|
||||
*/
|
||||
public byte[] cropImage(byte[] imageData, int x, int y, int width, int height) {
|
||||
try {
|
||||
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
|
||||
// 裁剪图片
|
||||
BufferedImage croppedImage = originalImage.getSubimage(x, y, width, height);
|
||||
|
||||
// 输出为字节数组
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(croppedImage, "jpg", baos);
|
||||
return baos.toByteArray();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("图片裁剪失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水印
|
||||
*/
|
||||
public byte[] addWatermark(byte[] imageData, String watermarkText) {
|
||||
try {
|
||||
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
|
||||
Graphics2D g2d = originalImage.createGraphics();
|
||||
|
||||
// 设置水印样式
|
||||
g2d.setFont(new Font("Arial", Font.BOLD, 30));
|
||||
g2d.setColor(new Color(255, 255, 255, 128)); // 半透明白色
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 计算水印位置
|
||||
FontMetrics fontMetrics = g2d.getFontMetrics();
|
||||
int textWidth = fontMetrics.stringWidth(watermarkText);
|
||||
int textHeight = fontMetrics.getHeight();
|
||||
int x = originalImage.getWidth() - textWidth - 10;
|
||||
int y = originalImage.getHeight() - 10;
|
||||
|
||||
// 绘制水印
|
||||
g2d.drawString(watermarkText, x, y);
|
||||
g2d.dispose();
|
||||
|
||||
// 输出为字节数组
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(originalImage, "jpg", baos);
|
||||
return baos.toByteArray();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("添加水印失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 大文件上传
|
||||
|
||||
### 分片上传
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/admin-api/infra/file/chunk")
|
||||
@Api(tags = "管理后台 - 分片上传")
|
||||
public class ChunkUploadController {
|
||||
|
||||
@Resource
|
||||
private ChunkUploadService chunkUploadService;
|
||||
|
||||
@PostMapping("/init")
|
||||
@ApiOperation("初始化分片上传")
|
||||
public CommonResult<ChunkUploadInitRespVO> initChunkUpload(@Valid @RequestBody ChunkUploadInitReqVO reqVO) {
|
||||
return success(chunkUploadService.initChunkUpload(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@ApiOperation("上传文件分片")
|
||||
public CommonResult<Boolean> uploadChunk(@RequestParam("uploadId") String uploadId,
|
||||
@RequestParam("chunkNumber") Integer chunkNumber,
|
||||
@RequestParam("file") MultipartFile file) throws Exception {
|
||||
chunkUploadService.uploadChunk(uploadId, chunkNumber, IoUtil.readBytes(file.getInputStream()));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/merge")
|
||||
@ApiOperation("合并文件分片")
|
||||
public CommonResult<String> mergeChunks(@Valid @RequestBody ChunkUploadMergeReqVO reqVO) throws Exception {
|
||||
String fileId = chunkUploadService.mergeChunks(reqVO);
|
||||
return success(fileId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/abort")
|
||||
@ApiOperation("取消分片上传")
|
||||
public CommonResult<Boolean> abortChunkUpload(@RequestParam("uploadId") String uploadId) {
|
||||
chunkUploadService.abortChunkUpload(uploadId);
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
public class ChunkUploadService {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
public ChunkUploadInitRespVO initChunkUpload(ChunkUploadInitReqVO reqVO) {
|
||||
// 生成上传ID
|
||||
String uploadId = IdUtil.fastUUID();
|
||||
|
||||
// 缓存上传信息
|
||||
ChunkUploadInfo uploadInfo = new ChunkUploadInfo();
|
||||
uploadInfo.setFileName(reqVO.getFileName());
|
||||
uploadInfo.setFileSize(reqVO.getFileSize());
|
||||
uploadInfo.setChunkSize(reqVO.getChunkSize());
|
||||
uploadInfo.setTotalChunks(calculateTotalChunks(reqVO.getFileSize(), reqVO.getChunkSize()));
|
||||
uploadInfo.setUploadedChunks(new HashSet<>());
|
||||
|
||||
String key = "chunk_upload:" + uploadId;
|
||||
redisTemplate.opsForValue().set(key, uploadInfo, Duration.ofHours(24));
|
||||
|
||||
return new ChunkUploadInitRespVO(uploadId, uploadInfo.getTotalChunks());
|
||||
}
|
||||
|
||||
public void uploadChunk(String uploadId, Integer chunkNumber, byte[] chunkData) {
|
||||
String key = "chunk_upload:" + uploadId;
|
||||
ChunkUploadInfo uploadInfo = (ChunkUploadInfo) redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (uploadInfo == null) {
|
||||
throw new ServiceException("上传信息不存在或已过期");
|
||||
}
|
||||
|
||||
// 存储分片数据
|
||||
String chunkKey = "chunk_data:" + uploadId + ":" + chunkNumber;
|
||||
redisTemplate.opsForValue().set(chunkKey, chunkData, Duration.ofHours(24));
|
||||
|
||||
// 记录已上传分片
|
||||
uploadInfo.getUploadedChunks().add(chunkNumber);
|
||||
redisTemplate.opsForValue().set(key, uploadInfo, Duration.ofHours(24));
|
||||
}
|
||||
|
||||
public String mergeChunks(ChunkUploadMergeReqVO reqVO) throws Exception {
|
||||
String uploadId = reqVO.getUploadId();
|
||||
String key = "chunk_upload:" + uploadId;
|
||||
ChunkUploadInfo uploadInfo = (ChunkUploadInfo) redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (uploadInfo == null) {
|
||||
throw new ServiceException("上传信息不存在或已过期");
|
||||
}
|
||||
|
||||
// 检查所有分片是否已上传
|
||||
if (uploadInfo.getUploadedChunks().size() != uploadInfo.getTotalChunks()) {
|
||||
throw new ServiceException("还有分片未上传完成");
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
ByteArrayOutputStream mergedContent = new ByteArrayOutputStream();
|
||||
for (int i = 1; i <= uploadInfo.getTotalChunks(); i++) {
|
||||
String chunkKey = "chunk_data:" + uploadId + ":" + i;
|
||||
byte[] chunkData = (byte[]) redisTemplate.opsForValue().get(chunkKey);
|
||||
if (chunkData != null) {
|
||||
mergedContent.write(chunkData);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传合并后的文件
|
||||
String path = generatePath(uploadInfo.getFileName());
|
||||
String fileId = fileService.createFile(uploadInfo.getFileName(), path, mergedContent.toByteArray());
|
||||
|
||||
// 清理临时数据
|
||||
cleanupChunkData(uploadId, uploadInfo.getTotalChunks());
|
||||
|
||||
return fileId;
|
||||
}
|
||||
|
||||
private void cleanupChunkData(String uploadId, int totalChunks) {
|
||||
// 删除上传信息
|
||||
redisTemplate.delete("chunk_upload:" + uploadId);
|
||||
|
||||
// 删除分片数据
|
||||
for (int i = 1; i <= totalChunks; i++) {
|
||||
redisTemplate.delete("chunk_data:" + uploadId + ":" + i);
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateTotalChunks(long fileSize, int chunkSize) {
|
||||
return (int) Math.ceil((double) fileSize / chunkSize);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文件安全
|
||||
|
||||
### 文件类型校验
|
||||
```java
|
||||
@Component
|
||||
public class FileSecurityValidator {
|
||||
|
||||
private static final Set<String> ALLOWED_IMAGE_TYPES = Set.of(
|
||||
"image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp"
|
||||
);
|
||||
|
||||
private static final Set<String> ALLOWED_DOCUMENT_TYPES = Set.of(
|
||||
"application/pdf", "application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
);
|
||||
|
||||
private static final Set<String> DANGEROUS_EXTENSIONS = Set.of(
|
||||
"exe", "bat", "cmd", "com", "pif", "scr", "vbs", "js", "jar", "php", "asp", "jsp"
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验文件类型
|
||||
*/
|
||||
public void validateFileType(MultipartFile file, FileTypeEnum allowedType) {
|
||||
String contentType = file.getContentType();
|
||||
String fileName = file.getOriginalFilename();
|
||||
String extension = FileUtil.extName(fileName).toLowerCase();
|
||||
|
||||
// 检查危险扩展名
|
||||
if (DANGEROUS_EXTENSIONS.contains(extension)) {
|
||||
throw new ServiceException("不允许上传此类型文件");
|
||||
}
|
||||
|
||||
// 根据允许的类型进行校验
|
||||
switch (allowedType) {
|
||||
case IMAGE:
|
||||
if (!ALLOWED_IMAGE_TYPES.contains(contentType)) {
|
||||
throw new ServiceException("只允许上传图片文件");
|
||||
}
|
||||
break;
|
||||
case DOCUMENT:
|
||||
if (!ALLOWED_DOCUMENT_TYPES.contains(contentType)) {
|
||||
throw new ServiceException("只允许上传文档文件");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 通用校验,禁止可执行文件
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验文件大小
|
||||
*/
|
||||
public void validateFileSize(MultipartFile file, long maxSize) {
|
||||
if (file.getSize() > maxSize) {
|
||||
String maxSizeStr = FileUtil.readableFileSize(maxSize);
|
||||
throw new ServiceException("文件大小不能超过 " + maxSizeStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描文件内容
|
||||
*/
|
||||
public void scanFileContent(byte[] content) {
|
||||
// TODO: 集成病毒扫描引擎
|
||||
// 这里可以集成 ClamAV 等开源病毒扫描引擎
|
||||
|
||||
// 简单的恶意代码检测
|
||||
String contentStr = new String(content, StandardCharsets.UTF_8);
|
||||
List<String> maliciousPatterns = Arrays.asList(
|
||||
"<script", "javascript:", "eval(", "exec(", "system("
|
||||
);
|
||||
|
||||
for (String pattern : maliciousPatterns) {
|
||||
if (contentStr.toLowerCase().contains(pattern)) {
|
||||
throw new ServiceException("文件包含潜在恶意内容");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 性能优化
|
||||
1. **异步上传**: 大文件使用异步上传提升用户体验
|
||||
2. **CDN 加速**: 配置 CDN 加速文件访问
|
||||
3. **预签名 URL**: 使用预签名 URL 减少服务器压力
|
||||
4. **缓存策略**: 合理设置文件缓存策略
|
||||
5. **分片上传**: 大文件使用分片上传避免超时
|
||||
|
||||
### 安全考虑
|
||||
1. **文件类型校验**: 严格校验上传文件类型
|
||||
2. **文件大小限制**: 设置合理的文件大小限制
|
||||
3. **病毒扫描**: 集成病毒扫描引擎
|
||||
4. **访问控制**: 实现文件访问权限控制
|
||||
5. **存储隔离**: 不同租户文件存储隔离
|
||||
|
||||
### 运维监控
|
||||
1. **存储使用量**: 监控存储空间使用情况
|
||||
2. **上传失败率**: 监控文件上传成功率
|
||||
3. **访问日志**: 记录文件访问日志
|
||||
4. **清理策略**: 定期清理临时和过期文件
|
50
.cursor/rules/module-structure.mdc
Normal file
50
.cursor/rules/module-structure.mdc
Normal file
@@ -0,0 +1,50 @@
|
||||
# 模块结构指南
|
||||
|
||||
## 核心模块详解
|
||||
|
||||
### 基础框架模块
|
||||
- **yudao-common**:通用工具类和基础组件
|
||||
- **yudao-framework**:Spring Boot 自动配置和扩展
|
||||
- **yudao-dependencies**:统一依赖版本管理
|
||||
|
||||
### 系统核心模块
|
||||
- **yudao-module-system**:系统管理功能(用户、角色、菜单、部门等)
|
||||
- **yudao-module-infra**:基础设施功能(文件、配置、代码生成等)
|
||||
|
||||
### 业务扩展模块(可选)
|
||||
- **yudao-module-bpm**:工作流程管理,基于 Flowable
|
||||
- **yudao-module-pay**:支付系统,支持多渠道支付
|
||||
- **yudao-module-member**:会员中心管理
|
||||
- **yudao-module-mall**:商城系统功能
|
||||
- **yudao-module-crm**:客户关系管理
|
||||
- **yudao-module-erp**:企业资源计划
|
||||
- **yudao-module-mp**:微信公众号管理
|
||||
- **yudao-module-report**:报表和大屏功能
|
||||
- **yudao-module-ai**:AI 大模型集成
|
||||
- **yudao-module-iot**:物联网设备管理
|
||||
|
||||
## 模块激活配置
|
||||
在 [pom.xml](mdc:pom.xml) 中通过注释/取消注释来控制模块的激活:
|
||||
```xml
|
||||
<!-- 默认激活的核心模块 -->
|
||||
<module>yudao-module-system</module>
|
||||
<module>yudao-module-infra</module>
|
||||
|
||||
<!-- 可选业务模块(注释表示未激活)-->
|
||||
<!--<module>yudao-module-member</module>-->
|
||||
<!--<module>yudao-module-bpm</module>-->
|
||||
```
|
||||
|
||||
## 前端对应项目
|
||||
- **Vue3 + element-plus**:现代化管理后台
|
||||
- **Vue3 + vben(ant-design-vue)**:企业级管理后台
|
||||
- **Vue2 + element-ui**:经典版管理后台
|
||||
- **uni-app**:移动端和小程序支持
|
||||
|
||||
## 数据库支持
|
||||
项目支持多种数据库,脚本位于 [sql/](mdc:sql/) 目录:
|
||||
- MySQL(推荐)
|
||||
- PostgreSQL
|
||||
- Oracle
|
||||
- SQL Server
|
||||
- 国产数据库(达梦、人大金仓等)
|
709
.cursor/rules/multi-tenant-architecture.mdc
Normal file
709
.cursor/rules/multi-tenant-architecture.mdc
Normal file
@@ -0,0 +1,709 @@
|
||||
# 多租户架构详解
|
||||
|
||||
## SaaS 多租户模式
|
||||
|
||||
### 租户隔离方案
|
||||
芋道框架采用**共享数据库、共享 Schema**的租户隔离模式:
|
||||
- 所有租户共享同一个数据库实例
|
||||
- 通过 `tenant_id` 字段进行数据隔离
|
||||
- 在应用层面实现租户数据的自动过滤
|
||||
|
||||
### 核心表结构
|
||||
```sql
|
||||
-- 租户表
|
||||
CREATE TABLE system_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '租户编号',
|
||||
name VARCHAR(30) NOT NULL COMMENT '租户名',
|
||||
contact_user_id BIGINT COMMENT '联系人用户编号',
|
||||
contact_name VARCHAR(30) NOT NULL COMMENT '联系人',
|
||||
contact_mobile VARCHAR(500) DEFAULT NULL COMMENT '联系手机',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '租户状态',
|
||||
website VARCHAR(256) DEFAULT '' COMMENT '绑定域名',
|
||||
package_id BIGINT NOT NULL COMMENT '租户套餐编号',
|
||||
expire_time DATETIME NOT NULL COMMENT '过期时间',
|
||||
account_count INT NOT NULL COMMENT '账号数量',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
|
||||
-- 租户套餐表
|
||||
CREATE TABLE system_tenant_package (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '套餐编号',
|
||||
name VARCHAR(30) NOT NULL COMMENT '套餐名',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态',
|
||||
remark VARCHAR(256) DEFAULT '' COMMENT '备注',
|
||||
menu_ids VARCHAR(4096) NOT NULL COMMENT '关联的菜单编号',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
```
|
||||
|
||||
## 租户上下文管理
|
||||
|
||||
### 租户上下文类
|
||||
```java
|
||||
/**
|
||||
* 租户上下文 Holder
|
||||
*/
|
||||
public class TenantContextHolder {
|
||||
|
||||
/**
|
||||
* 当前租户编号
|
||||
*/
|
||||
private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 是否忽略租户
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> IGNORE = new ThreadLocal<>();
|
||||
|
||||
public static void setTenantId(Long tenantId) {
|
||||
TENANT_ID.set(tenantId);
|
||||
}
|
||||
|
||||
public static Long getTenantId() {
|
||||
return TENANT_ID.get();
|
||||
}
|
||||
|
||||
public static void setIgnore(Boolean ignore) {
|
||||
IGNORE.set(ignore);
|
||||
}
|
||||
|
||||
public static Boolean getIgnore() {
|
||||
return IGNORE.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
TENANT_ID.remove();
|
||||
IGNORE.remove();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 租户拦截器
|
||||
```java
|
||||
@Component
|
||||
public class TenantSecurityWebFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
|
||||
// 获取租户 ID(从请求头、域名、用户信息等)
|
||||
Long tenantId = getTenantId(httpRequest);
|
||||
|
||||
try {
|
||||
// 设置租户上下文
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
|
||||
// 继续处理请求
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
// 清理租户上下文
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private Long getTenantId(HttpServletRequest request) {
|
||||
// 1. 优先从请求头获取
|
||||
String tenantIdHeader = request.getHeader("Tenant-Id");
|
||||
if (StrUtil.isNotBlank(tenantIdHeader)) {
|
||||
return Long.valueOf(tenantIdHeader);
|
||||
}
|
||||
|
||||
// 2. 从域名获取
|
||||
String domain = request.getServerName();
|
||||
Long tenantId = getTenantIdByDomain(domain);
|
||||
if (tenantId != null) {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
// 3. 从用户 Token 获取
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(request);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
return getTenantIdFromToken(token);
|
||||
}
|
||||
|
||||
// 4. 默认租户
|
||||
return TenantConstants.DEFAULT_TENANT_ID;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库租户隔离
|
||||
|
||||
### MyBatis Plus 租户插件
|
||||
```java
|
||||
@Configuration
|
||||
public class TenantConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
|
||||
TenantLineInnerInterceptor interceptor = new TenantLineInnerInterceptor();
|
||||
|
||||
// 设置租户处理器
|
||||
interceptor.setTenantLineHandler(new TenantLineHandler() {
|
||||
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
return new LongValue(TenantConstants.DEFAULT_TENANT_ID);
|
||||
}
|
||||
return new LongValue(tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantIdColumn() {
|
||||
return "tenant_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
// 系统表不需要租户隔离
|
||||
return TenantConstants.IGNORE_TABLES.contains(tableName);
|
||||
}
|
||||
});
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 租户注解
|
||||
```java
|
||||
/**
|
||||
* 忽略租户
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TenantIgnore {
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户信息
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TenantInfo {
|
||||
}
|
||||
```
|
||||
|
||||
### 租户 AOP 切面
|
||||
```java
|
||||
@Aspect
|
||||
@Component
|
||||
public class TenantAspect {
|
||||
|
||||
@Around("@annotation(tenantIgnore)")
|
||||
public Object aroundTenantIgnore(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
|
||||
Boolean oldIgnore = TenantContextHolder.getIgnore();
|
||||
try {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户管理服务
|
||||
|
||||
### 租户服务接口
|
||||
```java
|
||||
@Service
|
||||
public class TenantServiceImpl implements TenantService {
|
||||
|
||||
@Resource
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Override
|
||||
@TenantIgnore // 系统级操作,忽略租户
|
||||
public Long createTenant(TenantCreateReqVO reqVO) {
|
||||
// 校验租户套餐
|
||||
TenantPackageDO tenantPackage = validateTenantPackage(reqVO.getPackageId());
|
||||
|
||||
// 创建租户
|
||||
TenantDO tenant = TenantConvert.INSTANCE.convert(reqVO);
|
||||
tenant.setAccountCount(tenantPackage.getAccountCount());
|
||||
tenant.setExpireTime(calculateExpireTime(reqVO.getExpireDays()));
|
||||
tenantMapper.insert(tenant);
|
||||
|
||||
// 初始化租户数据
|
||||
initTenantData(tenant.getId(), tenantPackage);
|
||||
|
||||
return tenant.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public void updateTenant(TenantUpdateReqVO reqVO) {
|
||||
// 校验租户存在
|
||||
validateTenantExists(reqVO.getId());
|
||||
|
||||
// 更新租户
|
||||
TenantDO updateObj = TenantConvert.INSTANCE.convert(reqVO);
|
||||
tenantMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public void deleteTenant(Long tenantId) {
|
||||
// 校验租户存在
|
||||
validateTenantExists(tenantId);
|
||||
|
||||
// 删除租户(软删除)
|
||||
tenantMapper.deleteById(tenantId);
|
||||
|
||||
// 清理租户相关数据
|
||||
cleanTenantData(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化租户数据
|
||||
*/
|
||||
private void initTenantData(Long tenantId, TenantPackageDO tenantPackage) {
|
||||
// 创建租户管理员账号
|
||||
createTenantAdmin(tenantId);
|
||||
|
||||
// 初始化租户菜单权限
|
||||
initTenantMenus(tenantId, tenantPackage.getMenuIds());
|
||||
|
||||
// 初始化租户角色
|
||||
initTenantRoles(tenantId);
|
||||
|
||||
// 初始化其他基础数据
|
||||
initTenantBasicData(tenantId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 租户套餐管理
|
||||
```java
|
||||
@Service
|
||||
public class TenantPackageServiceImpl implements TenantPackageService {
|
||||
|
||||
@Override
|
||||
@TenantIgnore // 套餐管理是系统级功能
|
||||
public Long createTenantPackage(TenantPackageCreateReqVO reqVO) {
|
||||
// 校验菜单编号合法性
|
||||
validateMenuIds(reqVO.getMenuIds());
|
||||
|
||||
// 创建租户套餐
|
||||
TenantPackageDO tenantPackage = TenantPackageConvert.INSTANCE.convert(reqVO);
|
||||
tenantPackageMapper.insert(tenantPackage);
|
||||
|
||||
return tenantPackage.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public void updateTenantPackage(TenantPackageUpdateReqVO reqVO) {
|
||||
// 校验套餐存在
|
||||
validateTenantPackageExists(reqVO.getId());
|
||||
|
||||
// 校验菜单编号
|
||||
validateMenuIds(reqVO.getMenuIds());
|
||||
|
||||
// 更新套餐
|
||||
TenantPackageDO updateObj = TenantPackageConvert.INSTANCE.convert(reqVO);
|
||||
tenantPackageMapper.updateById(updateObj);
|
||||
|
||||
// 同步更新使用该套餐的租户权限
|
||||
syncTenantMenuPermissions(reqVO.getId(), reqVO.getMenuIds());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租户菜单权限
|
||||
*/
|
||||
private void syncTenantMenuPermissions(Long packageId, List<Long> menuIds) {
|
||||
// 查找使用该套餐的租户
|
||||
List<TenantDO> tenants = tenantMapper.selectListByPackageId(packageId);
|
||||
|
||||
for (TenantDO tenant : tenants) {
|
||||
// 更新租户菜单权限
|
||||
updateTenantMenuPermissions(tenant.getId(), menuIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户数据初始化
|
||||
|
||||
### 租户初始化服务
|
||||
```java
|
||||
@Service
|
||||
public class TenantInitService {
|
||||
|
||||
/**
|
||||
* 初始化租户基础数据
|
||||
*/
|
||||
@TenantIgnore
|
||||
public void initTenantData(Long tenantId, Long packageId) {
|
||||
// 切换到目标租户上下文
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
|
||||
try {
|
||||
// 1. 创建默认角色
|
||||
Long adminRoleId = createDefaultAdminRole(tenantId);
|
||||
Long userRoleId = createDefaultUserRole(tenantId);
|
||||
|
||||
// 2. 创建默认部门
|
||||
Long rootDeptId = createDefaultDepartment(tenantId);
|
||||
|
||||
// 3. 创建租户管理员
|
||||
Long adminUserId = createTenantAdmin(tenantId, adminRoleId, rootDeptId);
|
||||
|
||||
// 4. 初始化系统配置
|
||||
initSystemConfigs(tenantId);
|
||||
|
||||
// 5. 初始化字典数据
|
||||
initDictData(tenantId);
|
||||
|
||||
} finally {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private Long createDefaultAdminRole(Long tenantId) {
|
||||
RoleCreateReqVO roleReq = new RoleCreateReqVO();
|
||||
roleReq.setName("租户管理员");
|
||||
roleReq.setCode("tenant_admin");
|
||||
roleReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
roleReq.setType(RoleTypeEnum.CUSTOM.getType());
|
||||
roleReq.setRemark("租户默认管理员角色");
|
||||
|
||||
return roleService.createRole(roleReq, RoleTypeEnum.CUSTOM);
|
||||
}
|
||||
|
||||
private Long createTenantAdmin(Long tenantId, Long roleId, Long deptId) {
|
||||
UserCreateReqVO userReq = new UserCreateReqVO();
|
||||
userReq.setUsername("admin");
|
||||
userReq.setNickname("管理员");
|
||||
userReq.setPassword("admin123");
|
||||
userReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
userReq.setDeptId(deptId);
|
||||
userReq.setRoleIds(Collections.singleton(roleId));
|
||||
|
||||
return userService.createUser(userReq);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户权限控制
|
||||
|
||||
### 菜单权限隔离
|
||||
```java
|
||||
@Service
|
||||
public class TenantMenuService {
|
||||
|
||||
/**
|
||||
* 获取租户可用菜单
|
||||
*/
|
||||
public List<MenuDO> getTenantMenus(Long tenantId) {
|
||||
// 获取租户套餐
|
||||
TenantDO tenant = tenantService.getTenant(tenantId);
|
||||
TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId());
|
||||
|
||||
// 解析套餐菜单编号
|
||||
List<Long> menuIds = StrUtils.splitToLong(tenantPackage.getMenuIds(), ',');
|
||||
|
||||
// 查询菜单列表
|
||||
return menuMapper.selectListByIds(menuIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户菜单权限
|
||||
*/
|
||||
public boolean hasMenuPermission(Long userId, Long menuId) {
|
||||
// 获取用户租户
|
||||
UserDO user = userService.getUser(userId);
|
||||
Long tenantId = user.getTenantId();
|
||||
|
||||
// 获取租户可用菜单
|
||||
List<MenuDO> tenantMenus = getTenantMenus(tenantId);
|
||||
|
||||
// 检查菜单是否在租户套餐中
|
||||
boolean inTenantPackage = tenantMenus.stream()
|
||||
.anyMatch(menu -> menu.getId().equals(menuId));
|
||||
|
||||
if (!inTenantPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查用户角色权限
|
||||
return permissionService.hasMenuPermission(userId, menuId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 数据权限隔离
|
||||
```java
|
||||
@Service
|
||||
public class TenantDataPermissionService {
|
||||
|
||||
/**
|
||||
* 校验数据归属权限
|
||||
*/
|
||||
public void validateDataPermission(Long dataId, String dataType) {
|
||||
Long currentTenantId = TenantContextHolder.getTenantId();
|
||||
if (currentTenantId == null) {
|
||||
throw new ServiceException("未设置租户上下文");
|
||||
}
|
||||
|
||||
// 查询数据所属租户
|
||||
Long dataTenantId = getDataTenantId(dataId, dataType);
|
||||
|
||||
if (!Objects.equals(currentTenantId, dataTenantId)) {
|
||||
throw new ServiceException("无权访问其他租户数据");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤租户数据
|
||||
*/
|
||||
public <T> List<T> filterTenantData(List<T> dataList, Function<T, Long> tenantIdExtractor) {
|
||||
Long currentTenantId = TenantContextHolder.getTenantId();
|
||||
if (currentTenantId == null) {
|
||||
return dataList;
|
||||
}
|
||||
|
||||
return dataList.stream()
|
||||
.filter(data -> Objects.equals(currentTenantId, tenantIdExtractor.apply(data)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户计费和限制
|
||||
|
||||
### 租户使用量统计
|
||||
```java
|
||||
@Service
|
||||
public class TenantUsageService {
|
||||
|
||||
/**
|
||||
* 统计租户用户数量
|
||||
*/
|
||||
public long countTenantUsers(Long tenantId) {
|
||||
return userMapper.selectCount(new LambdaQueryWrapper<UserDO>()
|
||||
.eq(UserDO::getTenantId, tenantId)
|
||||
.eq(UserDO::getDeleted, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计租户存储使用量
|
||||
*/
|
||||
public long calculateTenantStorage(Long tenantId) {
|
||||
return fileMapper.selectStorageByTenantId(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查租户账号数量限制
|
||||
*/
|
||||
public void checkAccountLimit(Long tenantId) {
|
||||
TenantDO tenant = tenantService.getTenant(tenantId);
|
||||
long currentUserCount = countTenantUsers(tenantId);
|
||||
|
||||
if (currentUserCount >= tenant.getAccountCount()) {
|
||||
throw new ServiceException("账号数量已达上限");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查租户有效期
|
||||
*/
|
||||
public void checkTenantExpire(Long tenantId) {
|
||||
TenantDO tenant = tenantService.getTenant(tenantId);
|
||||
|
||||
if (tenant.getExpireTime().before(new Date())) {
|
||||
throw new ServiceException("租户已过期");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 租户限流控制
|
||||
```java
|
||||
@Component
|
||||
public class TenantRateLimitService {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 检查 API 调用频率限制
|
||||
*/
|
||||
public boolean checkApiRateLimit(Long tenantId, String apiPath) {
|
||||
String key = "tenant:rate_limit:" + tenantId + ":" + apiPath;
|
||||
|
||||
// 获取租户套餐限制
|
||||
TenantPackageDO tenantPackage = getTenantPackage(tenantId);
|
||||
int rateLimit = tenantPackage.getApiRateLimit();
|
||||
|
||||
// 使用滑动窗口限流
|
||||
return checkRateLimit(key, rateLimit, Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查存储空间限制
|
||||
*/
|
||||
public void checkStorageLimit(Long tenantId, long fileSize) {
|
||||
long currentStorage = tenantUsageService.calculateTenantStorage(tenantId);
|
||||
TenantPackageDO tenantPackage = getTenantPackage(tenantId);
|
||||
|
||||
if (currentStorage + fileSize > tenantPackage.getStorageLimit()) {
|
||||
throw new ServiceException("存储空间不足");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户域名绑定
|
||||
|
||||
### 域名解析服务
|
||||
```java
|
||||
@Service
|
||||
public class TenantDomainService {
|
||||
|
||||
/**
|
||||
* 绑定租户域名
|
||||
*/
|
||||
@TenantIgnore
|
||||
public void bindDomain(Long tenantId, String domain) {
|
||||
// 校验域名格式
|
||||
validateDomainFormat(domain);
|
||||
|
||||
// 校验域名未被使用
|
||||
validateDomainNotUsed(domain);
|
||||
|
||||
// 更新租户域名
|
||||
TenantDO updateObj = new TenantDO();
|
||||
updateObj.setId(tenantId);
|
||||
updateObj.setWebsite(domain);
|
||||
tenantMapper.updateById(updateObj);
|
||||
|
||||
// 清理域名缓存
|
||||
clearDomainCache(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据域名获取租户ID
|
||||
*/
|
||||
public Long getTenantIdByDomain(String domain) {
|
||||
String cacheKey = "tenant:domain:" + domain;
|
||||
|
||||
// 先从缓存获取
|
||||
Long tenantId = (Long) redisTemplate.opsForValue().get(cacheKey);
|
||||
if (tenantId != null) {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
TenantDO tenant = tenantMapper.selectOneByWebsite(domain);
|
||||
if (tenant != null) {
|
||||
tenantId = tenant.getId();
|
||||
// 缓存结果
|
||||
redisTemplate.opsForValue().set(cacheKey, tenantId, Duration.ofHours(1));
|
||||
}
|
||||
|
||||
return tenantId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 租户数据迁移
|
||||
|
||||
### 租户数据导出
|
||||
```java
|
||||
@Service
|
||||
public class TenantDataExportService {
|
||||
|
||||
/**
|
||||
* 导出租户数据
|
||||
*/
|
||||
public TenantDataExportVO exportTenantData(Long tenantId) {
|
||||
TenantDataExportVO exportData = new TenantDataExportVO();
|
||||
|
||||
// 设置租户上下文
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
|
||||
try {
|
||||
// 导出用户数据
|
||||
exportData.setUsers(userService.getUserList(null));
|
||||
|
||||
// 导出角色数据
|
||||
exportData.setRoles(roleService.getRoleList());
|
||||
|
||||
// 导出部门数据
|
||||
exportData.setDepts(deptService.getDeptList(null));
|
||||
|
||||
// 导出菜单数据
|
||||
exportData.setMenus(menuService.getMenuList());
|
||||
|
||||
// 导出其他业务数据
|
||||
exportBusinessData(exportData, tenantId);
|
||||
|
||||
} finally {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
return exportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入租户数据
|
||||
*/
|
||||
public void importTenantData(Long tenantId, TenantDataExportVO importData) {
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
|
||||
try {
|
||||
// 导入用户数据
|
||||
importUsers(importData.getUsers());
|
||||
|
||||
// 导入角色数据
|
||||
importRoles(importData.getRoles());
|
||||
|
||||
// 导入部门数据
|
||||
importDepts(importData.getDepts());
|
||||
|
||||
// 导入菜单数据
|
||||
importMenus(importData.getMenus());
|
||||
|
||||
} finally {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 性能优化
|
||||
1. **租户缓存**: 缓存租户基础信息和套餐信息
|
||||
2. **索引优化**: 为 tenant_id 字段添加复合索引
|
||||
3. **连接池**: 考虑为大租户使用独立连接池
|
||||
4. **数据分片**: 超大租户可考虑数据分片
|
||||
|
||||
### 安全考虑
|
||||
1. **数据隔离**: 确保租户数据完全隔离
|
||||
2. **权限校验**: 多层权限校验防止越权访问
|
||||
3. **审计日志**: 记录跨租户操作的审计日志
|
||||
4. **数据备份**: 定期备份租户重要数据
|
||||
|
||||
### 监控告警
|
||||
1. **租户状态**: 监控租户是否正常运行
|
||||
2. **使用量**: 监控租户资源使用情况
|
||||
3. **性能指标**: 监控租户相关 API 性能
|
||||
4. **异常访问**: 检测异常的跨租户访问
|
866
.cursor/rules/notification-system.mdc
Normal file
866
.cursor/rules/notification-system.mdc
Normal file
@@ -0,0 +1,866 @@
|
||||
# 消息通知系统
|
||||
|
||||
## 通知系统架构
|
||||
|
||||
### 通知类型
|
||||
- **站内信**: 系统内部消息通知
|
||||
- **邮件通知**: 电子邮件发送
|
||||
- **短信通知**: 手机短信发送
|
||||
- **微信通知**: 微信公众号/小程序推送
|
||||
- **系统公告**: 全站公告通知
|
||||
|
||||
### 核心模块
|
||||
- **通知模板**: 消息模板管理
|
||||
- **通知渠道**: 发送渠道配置
|
||||
- **通知日志**: 发送记录和状态
|
||||
- **通知队列**: 异步消息处理
|
||||
|
||||
## 站内信系统
|
||||
|
||||
### 站内信表结构
|
||||
```sql
|
||||
-- 站内信模板表
|
||||
CREATE TABLE system_notify_template (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
||||
code VARCHAR(64) NOT NULL COMMENT '模板编码',
|
||||
nickname VARCHAR(255) NOT NULL COMMENT '发送人名称',
|
||||
content TEXT NOT NULL COMMENT '模板内容',
|
||||
type TINYINT NOT NULL COMMENT '类型',
|
||||
params VARCHAR(255) DEFAULT NULL COMMENT '参数数组',
|
||||
status TINYINT NOT NULL COMMENT '开启状态',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
||||
);
|
||||
|
||||
-- 站内信消息表
|
||||
CREATE TABLE system_notify_message (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户id',
|
||||
user_type TINYINT NOT NULL DEFAULT 0 COMMENT '用户类型',
|
||||
template_id BIGINT NOT NULL COMMENT '模版编号',
|
||||
template_code VARCHAR(64) NOT NULL COMMENT '模板编码',
|
||||
template_nickname VARCHAR(63) NOT NULL COMMENT '模版发送人名称',
|
||||
template_content TEXT NOT NULL COMMENT '模版内容',
|
||||
template_type TINYINT NOT NULL COMMENT '模版类型',
|
||||
template_params VARCHAR(255) NOT NULL COMMENT '模版参数',
|
||||
read_status BIT NOT NULL COMMENT '是否已读',
|
||||
read_time DATETIME DEFAULT NULL COMMENT '阅读时间',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
||||
);
|
||||
```
|
||||
|
||||
### 站内信服务
|
||||
```java
|
||||
@Service
|
||||
public class NotifyMessageServiceImpl implements NotifyMessageService {
|
||||
|
||||
@Resource
|
||||
private NotifyMessageMapper notifyMessageMapper;
|
||||
|
||||
@Resource
|
||||
private NotifyTemplateService notifyTemplateService;
|
||||
|
||||
@Override
|
||||
public Long createNotifyMessage(Long userId, Integer userType,
|
||||
NotifyTemplateReqDTO templateReq) {
|
||||
// 获取消息模板
|
||||
NotifyTemplateDO template = notifyTemplateService.getNotifyTemplate(templateReq.getTemplateCode());
|
||||
|
||||
// 渲染模板内容
|
||||
String content = renderTemplateContent(template, templateReq.getTemplateParams());
|
||||
|
||||
// 创建消息
|
||||
NotifyMessageDO message = new NotifyMessageDO();
|
||||
message.setUserId(userId);
|
||||
message.setUserType(userType);
|
||||
message.setTemplateId(template.getId());
|
||||
message.setTemplateCode(template.getCode());
|
||||
message.setTemplateNickname(template.getNickname());
|
||||
message.setTemplateContent(content);
|
||||
message.setTemplateType(template.getType());
|
||||
message.setTemplateParams(JsonUtils.toJsonString(templateReq.getTemplateParams()));
|
||||
message.setReadStatus(false);
|
||||
|
||||
notifyMessageMapper.insert(message);
|
||||
return message.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<NotifyMessageDO> getNotifyMessagePage(Long userId,
|
||||
NotifyMessagePageReqVO pageReqVO) {
|
||||
return notifyMessageMapper.selectPage(userId, pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsRead(Long userId, Long messageId) {
|
||||
NotifyMessageDO message = validateNotifyMessageExists(messageId);
|
||||
|
||||
// 校验用户权限
|
||||
if (!Objects.equals(message.getUserId(), userId)) {
|
||||
throw exception(NOTIFY_MESSAGE_NOT_BELONGS_TO_USER);
|
||||
}
|
||||
|
||||
// 标记已读
|
||||
if (!message.getReadStatus()) {
|
||||
NotifyMessageDO updateObj = new NotifyMessageDO();
|
||||
updateObj.setId(messageId);
|
||||
updateObj.setReadStatus(true);
|
||||
updateObj.setReadTime(new Date());
|
||||
notifyMessageMapper.updateById(updateObj);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAllAsRead(Long userId) {
|
||||
notifyMessageMapper.updateReadStatusByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getUnreadCount(Long userId) {
|
||||
return notifyMessageMapper.selectUnreadCountByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板内容
|
||||
*/
|
||||
private String renderTemplateContent(NotifyTemplateDO template, Map<String, Object> params) {
|
||||
String content = template.getContent();
|
||||
|
||||
// 使用占位符替换参数
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
String placeholder = "{" + entry.getKey() + "}";
|
||||
String value = String.valueOf(entry.getValue());
|
||||
content = content.replace(placeholder, value);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 站内信模板管理
|
||||
```java
|
||||
@Service
|
||||
public class NotifyTemplateServiceImpl implements NotifyTemplateService {
|
||||
|
||||
@Override
|
||||
public Long createNotifyTemplate(NotifyTemplateCreateReqVO reqVO) {
|
||||
// 校验模板编码唯一性
|
||||
validateTemplateCodeDuplicate(null, reqVO.getCode());
|
||||
|
||||
// 创建模板
|
||||
NotifyTemplateDO template = NotifyTemplateConvert.INSTANCE.convert(reqVO);
|
||||
template.setParams(buildTemplateParams(reqVO.getContent()));
|
||||
notifyTemplateMapper.insert(template);
|
||||
|
||||
return template.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNotifyTemplate(NotifyTemplateUpdateReqVO reqVO) {
|
||||
// 校验模板存在
|
||||
validateNotifyTemplateExists(reqVO.getId());
|
||||
|
||||
// 校验编码唯一性
|
||||
validateTemplateCodeDuplicate(reqVO.getId(), reqVO.getCode());
|
||||
|
||||
// 更新模板
|
||||
NotifyTemplateDO updateObj = NotifyTemplateConvert.INSTANCE.convert(reqVO);
|
||||
updateObj.setParams(buildTemplateParams(reqVO.getContent()));
|
||||
notifyTemplateMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析模板参数
|
||||
*/
|
||||
private String buildTemplateParams(String content) {
|
||||
Set<String> params = new HashSet<>();
|
||||
|
||||
// 提取 {参数名} 格式的参数
|
||||
Pattern pattern = Pattern.compile("\\{([^}]+)\\}");
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
|
||||
while (matcher.find()) {
|
||||
params.add(matcher.group(1));
|
||||
}
|
||||
|
||||
return JsonUtils.toJsonString(new ArrayList<>(params));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 邮件通知系统
|
||||
|
||||
### 邮件配置
|
||||
```yaml
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.qq.com
|
||||
port: 587
|
||||
username: your-email@qq.com
|
||||
password: your-password
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
required: true
|
||||
```
|
||||
|
||||
### 邮件模板管理
|
||||
```sql
|
||||
-- 邮件模板表
|
||||
CREATE TABLE system_mail_template (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
||||
code VARCHAR(63) NOT NULL COMMENT '模板编码',
|
||||
account_id BIGINT NOT NULL COMMENT '发送的邮箱账号编号',
|
||||
nickname VARCHAR(255) DEFAULT NULL COMMENT '发送人名称',
|
||||
title VARCHAR(255) NOT NULL COMMENT '邮件标题',
|
||||
content TEXT NOT NULL COMMENT '邮件内容',
|
||||
params VARCHAR(255) NOT NULL COMMENT '参数数组',
|
||||
status TINYINT NOT NULL COMMENT '开启状态',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
```
|
||||
|
||||
### 邮件发送服务
|
||||
```java
|
||||
@Service
|
||||
public class MailSendServiceImpl implements MailSendService {
|
||||
|
||||
@Resource
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Resource
|
||||
private MailTemplateService mailTemplateService;
|
||||
|
||||
@Resource
|
||||
private MailLogService mailLogService;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public Long sendSingleMail(String mail, Long userId, Integer userType,
|
||||
String templateCode, Map<String, Object> templateParams) {
|
||||
// 检查邮箱格式
|
||||
if (!Validator.isEmail(mail)) {
|
||||
throw exception(MAIL_SEND_MAIL_NOT_VALID, mail);
|
||||
}
|
||||
|
||||
// 获取邮件模板
|
||||
MailTemplateDO template = mailTemplateService.getMailTemplateByCodeFromCache(templateCode);
|
||||
|
||||
// 渲染邮件内容
|
||||
String title = renderTemplateContent(template.getTitle(), templateParams);
|
||||
String content = renderTemplateContent(template.getContent(), templateParams);
|
||||
|
||||
// 创建发送日志
|
||||
Long sendLogId = mailLogService.createMailLog(userId, userType, mail,
|
||||
template, templateParams);
|
||||
|
||||
try {
|
||||
// 发送邮件
|
||||
doSendMail(template, mail, title, content);
|
||||
|
||||
// 更新发送状态
|
||||
mailLogService.updateMailSendResult(sendLogId, true, null);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 更新发送失败状态
|
||||
mailLogService.updateMailSendResult(sendLogId, false, e.getMessage());
|
||||
log.error("[sendSingleMail][发送邮件失败, mail:{}, template:{}]", mail, templateCode, e);
|
||||
throw exception(MAIL_SEND_ERROR);
|
||||
}
|
||||
|
||||
return sendLogId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void sendBatchMail(List<String> mails, String templateCode,
|
||||
Map<String, Object> templateParams) {
|
||||
for (String mail : mails) {
|
||||
try {
|
||||
sendSingleMail(mail, null, null, templateCode, templateParams);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendBatchMail][发送邮件失败, mail:{}, template:{}]", mail, templateCode, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行邮件发送
|
||||
*/
|
||||
private void doSendMail(MailTemplateDO template, String toMail,
|
||||
String title, String content) throws Exception {
|
||||
// 获取邮箱账号配置
|
||||
MailAccountDO account = mailAccountService.getMailAccount(template.getAccountId());
|
||||
|
||||
// 创建邮件消息
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
// 设置发件人
|
||||
helper.setFrom(account.getMail(), template.getNickname());
|
||||
|
||||
// 设置收件人
|
||||
helper.setTo(toMail);
|
||||
|
||||
// 设置邮件主题和内容
|
||||
helper.setSubject(title);
|
||||
helper.setText(content, true); // true 表示支持 HTML
|
||||
|
||||
// 发送邮件
|
||||
mailSender.send(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 短信通知系统
|
||||
|
||||
### 短信渠道配置
|
||||
```sql
|
||||
-- 短信渠道表
|
||||
CREATE TABLE system_sms_channel (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
signature VARCHAR(12) NOT NULL COMMENT '短信签名',
|
||||
code VARCHAR(63) NOT NULL COMMENT '渠道编码',
|
||||
status TINYINT NOT NULL COMMENT '开启状态',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
api_key VARCHAR(128) NOT NULL COMMENT '短信 API 的账号',
|
||||
api_secret VARCHAR(128) DEFAULT NULL COMMENT '短信 API 的密钥',
|
||||
callback_url VARCHAR(255) DEFAULT NULL COMMENT '短信发送回调 URL',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
|
||||
-- 短信模板表
|
||||
CREATE TABLE system_sms_template (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
|
||||
type TINYINT NOT NULL COMMENT '短信签名',
|
||||
status TINYINT NOT NULL COMMENT '开启状态',
|
||||
code VARCHAR(63) NOT NULL COMMENT '模板编码',
|
||||
name VARCHAR(63) NOT NULL COMMENT '模板名称',
|
||||
content VARCHAR(255) NOT NULL COMMENT '模板内容',
|
||||
params VARCHAR(255) NOT NULL COMMENT '参数数组',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
api_template_id VARCHAR(63) NOT NULL COMMENT '短信 API 的模板编号',
|
||||
channel_id BIGINT NOT NULL COMMENT '短信渠道编号',
|
||||
channel_code VARCHAR(63) NOT NULL COMMENT '短信渠道编码',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除'
|
||||
);
|
||||
```
|
||||
|
||||
### 短信发送服务
|
||||
```java
|
||||
@Service
|
||||
public class SmsSendServiceImpl implements SmsSendService {
|
||||
|
||||
@Resource
|
||||
private SmsTemplateService smsTemplateService;
|
||||
|
||||
@Resource
|
||||
private SmsLogService smsLogService;
|
||||
|
||||
@Resource
|
||||
private SmsClientFactory smsClientFactory;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public Long sendSingleSms(String mobile, Long userId, Integer userType,
|
||||
String templateCode, Map<String, Object> templateParams) {
|
||||
// 校验手机号码格式
|
||||
if (!Validator.isMobile(mobile)) {
|
||||
throw exception(SMS_SEND_MOBILE_NOT_VALID, mobile);
|
||||
}
|
||||
|
||||
// 获取短信模板
|
||||
SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
|
||||
|
||||
// 创建发送日志
|
||||
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType,
|
||||
template, templateParams);
|
||||
|
||||
try {
|
||||
// 执行发送
|
||||
SmsClient smsClient = smsClientFactory.getSmsClient(template.getChannelId());
|
||||
SmsSendResult sendResult = smsClient.sendSms(mobile, template.getApiTemplateId(),
|
||||
templateParams);
|
||||
|
||||
// 更新发送结果
|
||||
smsLogService.updateSmsSendResult(sendLogId, sendResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendSingleSms][发送短信失败, mobile:{}, template:{}]", mobile, templateCode, e);
|
||||
smsLogService.updateSmsSendResult(sendLogId, false, e.getMessage());
|
||||
throw exception(SMS_SEND_ERROR);
|
||||
}
|
||||
|
||||
return sendLogId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void sendBatchSms(List<String> mobiles, String templateCode,
|
||||
Map<String, Object> templateParams) {
|
||||
for (String mobile : mobiles) {
|
||||
try {
|
||||
sendSingleSms(mobile, null, null, templateCode, templateParams);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendBatchSms][发送短信失败, mobile:{}, template:{}]", mobile, templateCode, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveSmsStatus(String channelCode, String text, HttpServletRequest request)
|
||||
throws Throwable {
|
||||
// 获取短信客户端
|
||||
SmsClient smsClient = smsClientFactory.getSmsClient(channelCode);
|
||||
|
||||
// 解析回调数据
|
||||
List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text, request);
|
||||
|
||||
// 更新发送状态
|
||||
for (SmsReceiveRespDTO result : receiveResults) {
|
||||
smsLogService.updateSmsReceiveResult(result.getLogId(), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 短信客户端抽象
|
||||
```java
|
||||
public interface SmsClient {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
*/
|
||||
String getChannelCode();
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*/
|
||||
SmsSendRespDTO sendSms(String mobile, String templateId,
|
||||
Map<String, Object> templateParams) throws Throwable;
|
||||
|
||||
/**
|
||||
* 解析接收短信的结果
|
||||
*/
|
||||
List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text,
|
||||
HttpServletRequest request) throws Throwable;
|
||||
}
|
||||
|
||||
@Component
|
||||
public abstract class AbstractSmsClient implements SmsClient {
|
||||
|
||||
protected final SmsChannelProperties properties;
|
||||
|
||||
public AbstractSmsClient(SmsChannelProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
@PostConstruct
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][配置({}) 初始化完成]", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
}
|
||||
```
|
||||
|
||||
## 微信通知系统
|
||||
|
||||
### 微信模板消息
|
||||
```java
|
||||
@Service
|
||||
public class WechatNotifyService {
|
||||
|
||||
@Resource
|
||||
private WxMpService wxMpService;
|
||||
|
||||
/**
|
||||
* 发送模板消息
|
||||
*/
|
||||
public void sendTemplateMessage(String openId, String templateId,
|
||||
Map<String, Object> data, String url) {
|
||||
try {
|
||||
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
|
||||
.toUser(openId)
|
||||
.templateId(templateId)
|
||||
.url(url)
|
||||
.build();
|
||||
|
||||
// 设置模板数据
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
templateMessage.addData(new WxMpTemplateData(
|
||||
entry.getKey(),
|
||||
String.valueOf(entry.getValue()),
|
||||
"#173177"
|
||||
));
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendTemplateMessage][发送微信模板消息失败, openId:{}, templateId:{}]",
|
||||
openId, templateId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送小程序订阅消息
|
||||
*/
|
||||
public void sendSubscribeMessage(String openId, String templateId,
|
||||
Map<String, Object> data, String page) {
|
||||
try {
|
||||
WxMaSubscribeMessage subscribeMessage = WxMaSubscribeMessage.builder()
|
||||
.toUser(openId)
|
||||
.templateId(templateId)
|
||||
.page(page)
|
||||
.build();
|
||||
|
||||
// 设置数据
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
subscribeMessage.addData(new WxMaSubscribeData(
|
||||
entry.getKey(),
|
||||
String.valueOf(entry.getValue())
|
||||
));
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
wxMaService.getMsgService().sendSubscribeMsg(subscribeMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[sendSubscribeMessage][发送小程序订阅消息失败, openId:{}, templateId:{}]",
|
||||
openId, templateId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统公告
|
||||
|
||||
### 公告管理
|
||||
```sql
|
||||
-- 通知公告表
|
||||
CREATE TABLE system_notice (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '公告ID',
|
||||
title VARCHAR(50) NOT NULL COMMENT '公告标题',
|
||||
content TEXT NOT NULL COMMENT '公告内容',
|
||||
type TINYINT NOT NULL COMMENT '公告类型',
|
||||
status TINYINT NOT NULL DEFAULT 0 COMMENT '公告状态',
|
||||
|
||||
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号'
|
||||
);
|
||||
```
|
||||
|
||||
### 公告服务
|
||||
```java
|
||||
@Service
|
||||
public class NoticeServiceImpl implements NoticeService {
|
||||
|
||||
@Override
|
||||
public Long createNotice(NoticeCreateReqVO reqVO) {
|
||||
// 创建通知公告
|
||||
NoticeDO notice = NoticeConvert.INSTANCE.convert(reqVO);
|
||||
noticeMapper.insert(notice);
|
||||
|
||||
// 推送给所有在线用户(可选)
|
||||
if (Objects.equals(reqVO.getType(), NoticeTypeEnum.NOTICE.getType())) {
|
||||
pushNoticeToAllUsers(notice);
|
||||
}
|
||||
|
||||
return notice.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNotice(NoticeUpdateReqVO reqVO) {
|
||||
// 校验公告存在
|
||||
validateNoticeExists(reqVO.getId());
|
||||
|
||||
// 更新公告
|
||||
NoticeDO updateObj = NoticeConvert.INSTANCE.convert(reqVO);
|
||||
noticeMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送公告给所有用户
|
||||
*/
|
||||
private void pushNoticeToAllUsers(NoticeDO notice) {
|
||||
// 获取所有在线用户
|
||||
Set<Long> onlineUserIds = getOnlineUserIds();
|
||||
|
||||
// 创建站内信
|
||||
Map<String, Object> templateParams = new HashMap<>();
|
||||
templateParams.put("title", notice.getTitle());
|
||||
templateParams.put("content", notice.getContent());
|
||||
|
||||
for (Long userId : onlineUserIds) {
|
||||
try {
|
||||
notifyMessageService.createNotifyMessage(userId, UserTypeEnum.ADMIN.getValue(),
|
||||
new NotifyTemplateReqDTO("SYSTEM_NOTICE", templateParams));
|
||||
} catch (Exception e) {
|
||||
log.error("[pushNoticeToAllUsers][推送公告失败, userId:{}, noticeId:{}]",
|
||||
userId, notice.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 消息队列集成
|
||||
|
||||
### 异步消息处理
|
||||
```java
|
||||
@Component
|
||||
public class NotificationProducer {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 发送站内信消息
|
||||
*/
|
||||
public void sendNotifyMessage(NotifyMessageSendDTO sendDTO) {
|
||||
redisTemplate.convertAndSend("notify.message.send", sendDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件消息
|
||||
*/
|
||||
public void sendMailMessage(MailSendDTO sendDTO) {
|
||||
redisTemplate.convertAndSend("mail.send", sendDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信消息
|
||||
*/
|
||||
public void sendSmsMessage(SmsSendDTO sendDTO) {
|
||||
redisTemplate.convertAndSend("sms.send", sendDTO);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class NotificationConsumer {
|
||||
|
||||
@Resource
|
||||
private NotifyMessageService notifyMessageService;
|
||||
|
||||
@Resource
|
||||
private MailSendService mailSendService;
|
||||
|
||||
@Resource
|
||||
private SmsSendService smsSendService;
|
||||
|
||||
@EventListener
|
||||
public void onNotifyMessage(NotifyMessageSendDTO sendDTO) {
|
||||
try {
|
||||
notifyMessageService.createNotifyMessage(
|
||||
sendDTO.getUserId(),
|
||||
sendDTO.getUserType(),
|
||||
sendDTO.getTemplateReq()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("[onNotifyMessage][处理站内信消息失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onMailMessage(MailSendDTO sendDTO) {
|
||||
try {
|
||||
mailSendService.sendSingleMail(
|
||||
sendDTO.getMail(),
|
||||
sendDTO.getUserId(),
|
||||
sendDTO.getUserType(),
|
||||
sendDTO.getTemplateCode(),
|
||||
sendDTO.getTemplateParams()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("[onMailMessage][处理邮件消息失败]", e);
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onSmsMessage(SmsSendDTO sendDTO) {
|
||||
try {
|
||||
smsSendService.sendSingleSms(
|
||||
sendDTO.getMobile(),
|
||||
sendDTO.getUserId(),
|
||||
sendDTO.getUserType(),
|
||||
sendDTO.getTemplateCode(),
|
||||
sendDTO.getTemplateParams()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("[onSmsMessage][处理短信消息失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 通知统一发送
|
||||
|
||||
### 通知发送门面
|
||||
```java
|
||||
@Service
|
||||
public class NotificationFacadeService {
|
||||
|
||||
@Resource
|
||||
private NotifyMessageService notifyMessageService;
|
||||
|
||||
@Resource
|
||||
private MailSendService mailSendService;
|
||||
|
||||
@Resource
|
||||
private SmsSendService smsSendService;
|
||||
|
||||
@Resource
|
||||
private WechatNotifyService wechatNotifyService;
|
||||
|
||||
@Resource
|
||||
private NotificationProducer notificationProducer;
|
||||
|
||||
/**
|
||||
* 发送通知(多渠道)
|
||||
*/
|
||||
public void sendNotification(NotificationSendDTO sendDTO) {
|
||||
// 站内信
|
||||
if (sendDTO.getChannels().contains(NotifyChannelEnum.SITE_MESSAGE)) {
|
||||
sendSiteMessage(sendDTO);
|
||||
}
|
||||
|
||||
// 邮件
|
||||
if (sendDTO.getChannels().contains(NotifyChannelEnum.EMAIL)) {
|
||||
sendEmail(sendDTO);
|
||||
}
|
||||
|
||||
// 短信
|
||||
if (sendDTO.getChannels().contains(NotifyChannelEnum.SMS)) {
|
||||
sendSms(sendDTO);
|
||||
}
|
||||
|
||||
// 微信
|
||||
if (sendDTO.getChannels().contains(NotifyChannelEnum.WECHAT)) {
|
||||
sendWechat(sendDTO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送通知
|
||||
*/
|
||||
@Async
|
||||
public void sendBatchNotification(List<NotificationSendDTO> sendDTOs) {
|
||||
for (NotificationSendDTO sendDTO : sendDTOs) {
|
||||
try {
|
||||
sendNotification(sendDTO);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendBatchNotification][批量发送通知失败]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSiteMessage(NotificationSendDTO sendDTO) {
|
||||
NotifyMessageSendDTO messageDTO = new NotifyMessageSendDTO();
|
||||
messageDTO.setUserId(sendDTO.getUserId());
|
||||
messageDTO.setUserType(sendDTO.getUserType());
|
||||
messageDTO.setTemplateReq(new NotifyTemplateReqDTO(
|
||||
sendDTO.getTemplateCode(),
|
||||
sendDTO.getTemplateParams()
|
||||
));
|
||||
|
||||
notificationProducer.sendNotifyMessage(messageDTO);
|
||||
}
|
||||
|
||||
private void sendEmail(NotificationSendDTO sendDTO) {
|
||||
if (StrUtil.isBlank(sendDTO.getEmail())) {
|
||||
return;
|
||||
}
|
||||
|
||||
MailSendDTO mailDTO = new MailSendDTO();
|
||||
mailDTO.setMail(sendDTO.getEmail());
|
||||
mailDTO.setUserId(sendDTO.getUserId());
|
||||
mailDTO.setUserType(sendDTO.getUserType());
|
||||
mailDTO.setTemplateCode(sendDTO.getTemplateCode());
|
||||
mailDTO.setTemplateParams(sendDTO.getTemplateParams());
|
||||
|
||||
notificationProducer.sendMailMessage(mailDTO);
|
||||
}
|
||||
|
||||
private void sendSms(NotificationSendDTO sendDTO) {
|
||||
if (StrUtil.isBlank(sendDTO.getMobile())) {
|
||||
return;
|
||||
}
|
||||
|
||||
SmsSendDTO smsDTO = new SmsSendDTO();
|
||||
smsDTO.setMobile(sendDTO.getMobile());
|
||||
smsDTO.setUserId(sendDTO.getUserId());
|
||||
smsDTO.setUserType(sendDTO.getUserType());
|
||||
smsDTO.setTemplateCode(sendDTO.getTemplateCode());
|
||||
smsDTO.setTemplateParams(sendDTO.getTemplateParams());
|
||||
|
||||
notificationProducer.sendSmsMessage(smsDTO);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 性能优化
|
||||
1. **异步发送**: 使用消息队列异步处理通知
|
||||
2. **批量发送**: 支持批量发送减少系统开销
|
||||
3. **失败重试**: 实现发送失败的重试机制
|
||||
4. **流量控制**: 设置发送频率限制防止滥用
|
||||
|
||||
### 用户体验
|
||||
1. **消息分类**: 不同类型消息使用不同模板
|
||||
2. **个性化**: 支持用户自定义通知偏好设置
|
||||
3. **及时性**: 重要消息实时推送
|
||||
4. **历史记录**: 保留通知历史方便用户查看
|
||||
|
||||
### 监控告警
|
||||
1. **发送成功率**: 监控各渠道发送成功率
|
||||
2. **延迟监控**: 监控消息发送延迟
|
||||
3. **失败分析**: 分析发送失败原因
|
||||
4. **用量统计**: 统计各类型消息发送量
|
221
.cursor/rules/permission-system.mdc
Normal file
221
.cursor/rules/permission-system.mdc
Normal file
@@ -0,0 +1,221 @@
|
||||
# 权限系统详解
|
||||
|
||||
## 权限架构概述
|
||||
|
||||
### RBAC 权限模型
|
||||
基于角色的访问控制(Role-Based Access Control),包含:
|
||||
- **用户 (User)**: 系统操作者
|
||||
- **角色 (Role)**: 权限的集合
|
||||
- **菜单 (Menu)**: 系统功能模块
|
||||
- **权限 (Permission)**: 具体操作权限
|
||||
|
||||
### 核心权限表
|
||||
- `system_users`: 用户表
|
||||
- `system_role`: 角色表
|
||||
- `system_menu`: 菜单权限表
|
||||
- `system_user_role`: 用户角色关联表
|
||||
- `system_role_menu`: 角色菜单关联表
|
||||
|
||||
## 权限控制实现
|
||||
|
||||
### 菜单权限控制
|
||||
```java
|
||||
// 控制器方法级别权限
|
||||
@PreAuthorize("@ss.hasPermission('system:user:query')")
|
||||
public CommonResult<PageResult<UserRespVO>> getUserPage() {
|
||||
// 业务逻辑
|
||||
}
|
||||
|
||||
// 权限标识规范:模块:功能:操作
|
||||
// system:user:query - 系统管理:用户管理:查询
|
||||
// system:user:create - 系统管理:用户管理:新增
|
||||
// system:user:update - 系统管理:用户管理:修改
|
||||
// system:user:delete - 系统管理:用户管理:删除
|
||||
```
|
||||
|
||||
### 数据权限控制
|
||||
```java
|
||||
// 部门数据权限
|
||||
@DataPermission(deptAlias = "d", userAlias = "u")
|
||||
public PageResult<UserDO> getUserPage(UserPageReqVO reqVO) {
|
||||
// 自动添加数据权限 WHERE 条件
|
||||
}
|
||||
|
||||
// 数据权限范围:
|
||||
// 1. 全部数据权限
|
||||
// 2. 指定部门数据权限
|
||||
// 3. 本部门数据权限
|
||||
// 4. 本部门及以下数据权限
|
||||
// 5. 仅本人数据权限
|
||||
```
|
||||
|
||||
### 字段权限控制
|
||||
```java
|
||||
// 在 VO 类中使用注解控制字段显示
|
||||
@JsonIgnore
|
||||
@ApiModelProperty(hidden = true)
|
||||
private String sensitiveField;
|
||||
```
|
||||
|
||||
## 权限配置管理
|
||||
|
||||
### 菜单配置
|
||||
在系统管理 -> 菜单管理中配置:
|
||||
- **菜单类型**: 目录、菜单、按钮
|
||||
- **权限标识**: 用于 @PreAuthorize 检查
|
||||
- **路由地址**: 前端路由路径
|
||||
- **组件路径**: Vue 组件路径
|
||||
- **是否缓存**: 页面缓存控制
|
||||
|
||||
### 角色配置
|
||||
在系统管理 -> 角色管理中配置:
|
||||
- **角色权限**: 分配菜单权限
|
||||
- **数据权限**: 设置数据访问范围
|
||||
- **状态控制**: 启用/禁用角色
|
||||
|
||||
### 用户配置
|
||||
在系统管理 -> 用户管理中配置:
|
||||
- **角色分配**: 为用户分配角色
|
||||
- **部门归属**: 确定数据权限范围
|
||||
- **状态控制**: 启用/禁用用户
|
||||
|
||||
## 前端权限集成
|
||||
|
||||
### 路由权限
|
||||
```javascript
|
||||
// 在路由配置中检查权限
|
||||
{
|
||||
path: '/user',
|
||||
component: Layout,
|
||||
meta: {
|
||||
permissions: ['system:user:query']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按钮权限
|
||||
```vue
|
||||
<!-- 使用 v-hasPermi 指令控制按钮显示 -->
|
||||
<el-button
|
||||
v-hasPermi="['system:user:create']"
|
||||
@click="handleAdd">
|
||||
新增
|
||||
</el-button>
|
||||
```
|
||||
|
||||
### 动态菜单
|
||||
前端根据用户权限动态生成菜单结构。
|
||||
|
||||
## 权限缓存策略
|
||||
|
||||
### Redis 缓存
|
||||
- **用户权限**: `user:permission:{userId}`
|
||||
- **角色权限**: `role:permission:{roleId}`
|
||||
- **菜单树**: `menu:tree`
|
||||
|
||||
### 缓存更新
|
||||
权限变更时自动清理相关缓存:
|
||||
```java
|
||||
@CacheEvict(value = "user:permission", key = "#userId")
|
||||
public void updateUserRole(Long userId, Set<Long> roleIds) {
|
||||
// 更新用户角色
|
||||
}
|
||||
```
|
||||
|
||||
## 权限验证流程
|
||||
|
||||
### 登录验证
|
||||
1. 用户登录验证
|
||||
2. 获取用户角色和权限
|
||||
3. 生成 JWT Token
|
||||
4. 缓存用户权限信息
|
||||
|
||||
### 请求验证
|
||||
1. 解析 JWT Token
|
||||
2. 获取当前用户权限
|
||||
3. 检查接口权限要求
|
||||
4. 验证数据权限范围
|
||||
|
||||
## 特殊权限场景
|
||||
|
||||
### 超级管理员
|
||||
```java
|
||||
// 超级管理员拥有所有权限
|
||||
public static final Long SUPER_ADMIN_ID = 1L;
|
||||
|
||||
if (Objects.equals(userId, SUPER_ADMIN_ID)) {
|
||||
return true; // 跳过权限检查
|
||||
}
|
||||
```
|
||||
|
||||
### 租户隔离
|
||||
```java
|
||||
// 多租户权限隔离
|
||||
@TenantIgnore // 忽略租户隔离的接口
|
||||
public class SystemController {
|
||||
// 系统级别操作
|
||||
}
|
||||
```
|
||||
|
||||
### 临时权限
|
||||
```java
|
||||
// 临时提升权限执行操作
|
||||
@WithMockUser(authorities = "system:admin")
|
||||
public void executeAdminOperation() {
|
||||
// 执行需要管理员权限的操作
|
||||
}
|
||||
```
|
||||
|
||||
## 权限扩展
|
||||
|
||||
### 自定义权限验证器
|
||||
```java
|
||||
@Component("ss")
|
||||
public class SecurityFrameworkService {
|
||||
|
||||
public boolean hasPermission(String permission) {
|
||||
return hasAnyPermissions(permission);
|
||||
}
|
||||
|
||||
public boolean hasRole(String role) {
|
||||
return hasAnyRoles(role);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 权限注解扩展
|
||||
```java
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("@ss.hasPermission(#permission)")
|
||||
public @interface RequiresPermissions {
|
||||
String value();
|
||||
}
|
||||
```
|
||||
|
||||
## 权限审计
|
||||
|
||||
### 操作日志
|
||||
自动记录权限相关的操作:
|
||||
- 用户登录/登出
|
||||
- 权限变更操作
|
||||
- 敏感数据访问
|
||||
|
||||
### 权限报表
|
||||
- 用户权限分析
|
||||
- 角色使用统计
|
||||
- 权限覆盖度分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 权限设计原则
|
||||
1. **最小权限原则**: 用户只获得必需的最小权限
|
||||
2. **职责分离**: 不同角色负责不同职责
|
||||
3. **权限继承**: 合理的权限层级结构
|
||||
4. **审计跟踪**: 完整的权限操作日志
|
||||
|
||||
### 性能优化
|
||||
1. **权限缓存**: 合理使用 Redis 缓存
|
||||
2. **批量查询**: 避免 N+1 查询问题
|
||||
3. **索引优化**: 权限查询相关表添加索引
|
||||
4. **异步处理**: 权限变更异步通知
|
45
.cursor/rules/project-architecture.mdc
Normal file
45
.cursor/rules/project-architecture.mdc
Normal file
@@ -0,0 +1,45 @@
|
||||
# 芋道管理系统项目架构指南
|
||||
|
||||
## 项目概述
|
||||
这是一个基于 Spring Boot 2.7.18 + Vue3 的企业级快速开发平台,采用多模块架构设计。
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 主要入口点
|
||||
- 应用启动类:[YudaoServerApplication.java](mdc:yudao-server/src/main/java/cn/iocoder/yudao/server/YudaoServerApplication.java)
|
||||
- 根 Maven 配置:[pom.xml](mdc:pom.xml)
|
||||
|
||||
### 核心模块结构
|
||||
- **yudao-dependencies**:Maven 依赖版本管理
|
||||
- **yudao-framework**:Java 框架拓展和通用组件
|
||||
- **yudao-server**:主服务器,整合所有模块
|
||||
- **各业务模块**:按业务领域拆分的独立模块
|
||||
|
||||
### 技术栈
|
||||
- **后端框架**:Spring Boot 2.7.18 + Spring Security + MyBatis Plus
|
||||
- **数据库**:MySQL + Redis + 支持多种国产数据库
|
||||
- **工作流**:Flowable 6.8.0
|
||||
- **API 文档**:Springdoc + Swagger
|
||||
- **监控**:Spring Boot Admin + SkyWalking
|
||||
|
||||
### 包结构规范
|
||||
```
|
||||
cn.iocoder.yudao.module.{模块名}
|
||||
├── api/ # 对外接口定义
|
||||
├── controller/ # REST 控制器
|
||||
├── service/ # 业务逻辑层
|
||||
├── dal/ # 数据访问层
|
||||
├── convert/ # 对象转换器
|
||||
├── enums/ # 枚举定义
|
||||
└── framework/ # 模块框架配置
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
- 主配置:[application.yaml](mdc:yudao-server/src/main/resources/application.yaml)
|
||||
- 数据库脚本:[sql/mysql/](mdc:sql/mysql/)
|
||||
|
||||
## 关键设计模式
|
||||
- **多模块架构**:业务功能按模块独立开发和部署
|
||||
- **统一响应格式**:所有 API 使用统一的响应结构
|
||||
- **分层架构**:Controller -> Service -> DAO 的经典三层架构
|
||||
- **权限控制**:基于 RBAC 的细粒度权限管理
|
701
.cursor/rules/workflow-guide.mdc
Normal file
701
.cursor/rules/workflow-guide.mdc
Normal file
@@ -0,0 +1,701 @@
|
||||
# 工作流使用指南
|
||||
|
||||
## Flowable 工作流引擎
|
||||
|
||||
### 核心概念
|
||||
- **流程定义 (Process Definition)**: 流程的静态描述
|
||||
- **流程实例 (Process Instance)**: 流程的运行实例
|
||||
- **任务 (Task)**: 需要人工处理的节点
|
||||
- **执行实例 (Execution)**: 流程执行的路径
|
||||
|
||||
### 数据库表结构
|
||||
```sql
|
||||
-- 流程定义相关
|
||||
ACT_RE_DEPLOYMENT -- 部署信息
|
||||
ACT_RE_PROCDEF -- 流程定义
|
||||
|
||||
-- 运行时数据
|
||||
ACT_RU_EXECUTION -- 执行实例
|
||||
ACT_RU_TASK -- 运行时任务
|
||||
ACT_RU_VARIABLE -- 运行时变量
|
||||
|
||||
-- 历史数据
|
||||
ACT_HI_PROCINST -- 历史流程实例
|
||||
ACT_HI_TASKINST -- 历史任务实例
|
||||
ACT_HI_ACTINST -- 历史活动实例
|
||||
```
|
||||
|
||||
## 流程设计器
|
||||
|
||||
### BPMN 设计器
|
||||
支持标准 BPMN 2.0 规范,适合复杂流程设计:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
|
||||
<process id="leaveProcess" name="请假流程">
|
||||
<startEvent id="startEvent" name="开始"/>
|
||||
<userTask id="deptApproval" name="部门审批">
|
||||
<assignee>${deptLeader}</assignee>
|
||||
</userTask>
|
||||
<userTask id="hrApproval" name="HR审批">
|
||||
<assignee>${hrManager}</assignee>
|
||||
</userTask>
|
||||
<endEvent id="endEvent" name="结束"/>
|
||||
|
||||
<sequenceFlow sourceRef="startEvent" targetRef="deptApproval"/>
|
||||
<sequenceFlow sourceRef="deptApproval" targetRef="hrApproval"/>
|
||||
<sequenceFlow sourceRef="hrApproval" targetRef="endEvent"/>
|
||||
</process>
|
||||
</definitions>
|
||||
```
|
||||
|
||||
### Simple 设计器 (仿钉钉)
|
||||
支持拖拽式流程设计,适合简单审批流程:
|
||||
```json
|
||||
{
|
||||
"name": "请假流程",
|
||||
"version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "START",
|
||||
"name": "发起人"
|
||||
},
|
||||
{
|
||||
"id": "approval1",
|
||||
"type": "APPROVAL",
|
||||
"name": "部门主管审批",
|
||||
"assigneeType": "ROLE",
|
||||
"assignees": ["dept_manager"]
|
||||
},
|
||||
{
|
||||
"id": "approval2",
|
||||
"type": "APPROVAL",
|
||||
"name": "HR审批",
|
||||
"assigneeType": "USER",
|
||||
"assignees": ["hr001"]
|
||||
},
|
||||
{
|
||||
"id": "end",
|
||||
"type": "END",
|
||||
"name": "结束"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 流程部署
|
||||
|
||||
### 程序化部署
|
||||
```java
|
||||
@Service
|
||||
public class BpmDeploymentService {
|
||||
|
||||
@Resource
|
||||
private RepositoryService repositoryService;
|
||||
|
||||
/**
|
||||
* 部署 BPMN 流程
|
||||
*/
|
||||
public String deployBpmnProcess(String processName, InputStream bpmnStream) {
|
||||
Deployment deployment = repositoryService.createDeployment()
|
||||
.name(processName)
|
||||
.addInputStream(processName + ".bpmn20.xml", bpmnStream)
|
||||
.deploy();
|
||||
|
||||
return deployment.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除流程部署
|
||||
*/
|
||||
public void deleteDeployment(String deploymentId) {
|
||||
repositoryService.deleteDeployment(deploymentId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程定义列表
|
||||
*/
|
||||
public List<ProcessDefinition> getProcessDefinitions() {
|
||||
return repositoryService.createProcessDefinitionQuery()
|
||||
.latestVersion()
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 流程版本管理
|
||||
```java
|
||||
@Service
|
||||
public class ProcessVersionService {
|
||||
|
||||
/**
|
||||
* 升级流程版本
|
||||
*/
|
||||
public void upgradeProcessVersion(String processKey, InputStream newBpmn) {
|
||||
// 部署新版本
|
||||
String deploymentId = deployBpmnProcess(processKey + "_v2", newBpmn);
|
||||
|
||||
// 迁移运行中的实例(可选)
|
||||
migrateRunningInstances(processKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移运行中的流程实例
|
||||
*/
|
||||
private void migrateRunningInstances(String processKey) {
|
||||
// 获取旧版本的流程实例
|
||||
List<ProcessInstance> instances = runtimeService
|
||||
.createProcessInstanceQuery()
|
||||
.processDefinitionKey(processKey)
|
||||
.list();
|
||||
|
||||
// 执行实例迁移逻辑
|
||||
for (ProcessInstance instance : instances) {
|
||||
// 具体的迁移策略
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流程启动
|
||||
|
||||
### 启动流程实例
|
||||
```java
|
||||
@Service
|
||||
public class BpmProcessService {
|
||||
|
||||
@Resource
|
||||
private RuntimeService runtimeService;
|
||||
|
||||
/**
|
||||
* 启动流程
|
||||
*/
|
||||
public String startProcess(String processKey, String businessKey,
|
||||
Map<String, Object> variables, Long startUserId) {
|
||||
// 设置启动用户
|
||||
identityService.setAuthenticatedUserId(String.valueOf(startUserId));
|
||||
|
||||
// 启动流程实例
|
||||
ProcessInstance processInstance = runtimeService
|
||||
.startProcessInstanceByKey(processKey, businessKey, variables);
|
||||
|
||||
return processInstance.getProcessInstanceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动流程(带表单数据)
|
||||
*/
|
||||
public String startProcessWithForm(String processKey, String businessKey,
|
||||
Map<String, Object> formData, Long startUserId) {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("formData", formData);
|
||||
variables.put("startUserId", startUserId);
|
||||
variables.put("startTime", new Date());
|
||||
|
||||
return startProcess(processKey, businessKey, variables, startUserId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 流程变量管理
|
||||
```java
|
||||
@Service
|
||||
public class ProcessVariableService {
|
||||
|
||||
/**
|
||||
* 设置流程变量
|
||||
*/
|
||||
public void setVariable(String processInstanceId, String variableName, Object value) {
|
||||
runtimeService.setVariable(processInstanceId, variableName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程变量
|
||||
*/
|
||||
public Object getVariable(String processInstanceId, String variableName) {
|
||||
return runtimeService.getVariable(processInstanceId, variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置局部变量(任务级别)
|
||||
*/
|
||||
public void setTaskLocalVariable(String taskId, String variableName, Object value) {
|
||||
taskService.setVariableLocal(taskId, variableName, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 任务处理
|
||||
|
||||
### 任务查询
|
||||
```java
|
||||
@Service
|
||||
public class BpmTaskService {
|
||||
|
||||
@Resource
|
||||
private TaskService taskService;
|
||||
|
||||
/**
|
||||
* 查询用户待办任务
|
||||
*/
|
||||
public List<Task> getTodoTasks(Long userId) {
|
||||
return taskService.createTaskQuery()
|
||||
.taskAssignee(String.valueOf(userId))
|
||||
.active()
|
||||
.orderByTaskCreateTime()
|
||||
.desc()
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户参与的任务(候选用户)
|
||||
*/
|
||||
public List<Task> getCandidateTasks(Long userId) {
|
||||
return taskService.createTaskQuery()
|
||||
.taskCandidateUser(String.valueOf(userId))
|
||||
.active()
|
||||
.orderByTaskCreateTime()
|
||||
.desc()
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色任务
|
||||
*/
|
||||
public List<Task> getRoleTasks(String roleKey) {
|
||||
return taskService.createTaskQuery()
|
||||
.taskCandidateGroup(roleKey)
|
||||
.active()
|
||||
.orderByTaskCreateTime()
|
||||
.desc()
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 任务处理操作
|
||||
```java
|
||||
@Service
|
||||
public class TaskOperationService {
|
||||
|
||||
/**
|
||||
* 完成任务
|
||||
*/
|
||||
public void completeTask(String taskId, Map<String, Object> variables, Long userId) {
|
||||
// 设置处理人
|
||||
identityService.setAuthenticatedUserId(String.valueOf(userId));
|
||||
|
||||
// 完成任务
|
||||
taskService.complete(taskId, variables);
|
||||
|
||||
// 记录操作日志
|
||||
recordTaskOperation(taskId, "complete", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝任务
|
||||
*/
|
||||
public void rejectTask(String taskId, String reason, Long userId) {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("approved", false);
|
||||
variables.put("rejectReason", reason);
|
||||
|
||||
completeTask(taskId, variables, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办任务
|
||||
*/
|
||||
public void delegateTask(String taskId, Long targetUserId, Long currentUserId) {
|
||||
// 设置任务委派
|
||||
taskService.delegateTask(taskId, String.valueOf(targetUserId));
|
||||
|
||||
// 记录操作
|
||||
recordTaskOperation(taskId, "delegate", currentUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认领任务
|
||||
*/
|
||||
public void claimTask(String taskId, Long userId) {
|
||||
taskService.claim(taskId, String.valueOf(userId));
|
||||
recordTaskOperation(taskId, "claim", userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流程控制
|
||||
|
||||
### 会签处理
|
||||
```java
|
||||
@Service
|
||||
public class MultiInstanceService {
|
||||
|
||||
/**
|
||||
* 并行会签配置
|
||||
*/
|
||||
public void configParallelMultiInstance(BpmnModelInstance modelInstance,
|
||||
String taskId, String collection) {
|
||||
UserTask userTask = modelInstance.getModelElementById(taskId);
|
||||
|
||||
// 设置多实例配置
|
||||
MultiInstanceLoopCharacteristics multiInstance =
|
||||
modelInstance.newInstance(MultiInstanceLoopCharacteristics.class);
|
||||
multiInstance.setSequential(false); // 并行执行
|
||||
multiInstance.setCollection(collection);
|
||||
multiInstance.setElementVariable("assignee");
|
||||
|
||||
userTask.setLoopCharacteristics(multiInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行会签配置
|
||||
*/
|
||||
public void configSequentialMultiInstance(BpmnModelInstance modelInstance,
|
||||
String taskId, String collection) {
|
||||
UserTask userTask = modelInstance.getModelElementById(taskId);
|
||||
|
||||
MultiInstanceLoopCharacteristics multiInstance =
|
||||
modelInstance.newInstance(MultiInstanceLoopCharacteristics.class);
|
||||
multiInstance.setSequential(true); // 串行执行
|
||||
multiInstance.setCollection(collection);
|
||||
multiInstance.setElementVariable("assignee");
|
||||
|
||||
userTask.setLoopCharacteristics(multiInstance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态任务分配
|
||||
```java
|
||||
@Component
|
||||
public class TaskAssignmentService {
|
||||
|
||||
/**
|
||||
* 根据部门分配任务
|
||||
*/
|
||||
public String assignByDept(DelegateTask delegateTask) {
|
||||
// 获取发起人部门
|
||||
String startUserId = delegateTask.getVariable("startUserId").toString();
|
||||
UserDO startUser = userService.getUser(Long.valueOf(startUserId));
|
||||
|
||||
// 查找部门主管
|
||||
UserDO deptManager = userService.getDeptManager(startUser.getDeptId());
|
||||
|
||||
return String.valueOf(deptManager.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据金额分配任务
|
||||
*/
|
||||
public String assignByAmount(DelegateTask delegateTask) {
|
||||
BigDecimal amount = (BigDecimal) delegateTask.getVariable("amount");
|
||||
|
||||
if (amount.compareTo(new BigDecimal("10000")) > 0) {
|
||||
// 大于1万,总经理审批
|
||||
return "general_manager";
|
||||
} else if (amount.compareTo(new BigDecimal("5000")) > 0) {
|
||||
// 大于5千,部门经理审批
|
||||
return "dept_manager";
|
||||
} else {
|
||||
// 直接主管审批
|
||||
return "direct_supervisor";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流程监控
|
||||
|
||||
### 流程实例监控
|
||||
```java
|
||||
@Service
|
||||
public class ProcessMonitorService {
|
||||
|
||||
/**
|
||||
* 获取流程实例状态
|
||||
*/
|
||||
public ProcessInstanceInfo getProcessInstanceInfo(String processInstanceId) {
|
||||
// 获取流程实例
|
||||
ProcessInstance processInstance = runtimeService
|
||||
.createProcessInstanceQuery()
|
||||
.processInstanceId(processInstanceId)
|
||||
.singleResult();
|
||||
|
||||
if (processInstance != null) {
|
||||
// 运行中
|
||||
return buildRunningInstanceInfo(processInstance);
|
||||
} else {
|
||||
// 已结束,查询历史
|
||||
HistoricProcessInstance historicInstance = historyService
|
||||
.createHistoricProcessInstanceQuery()
|
||||
.processInstanceId(processInstanceId)
|
||||
.singleResult();
|
||||
|
||||
return buildFinishedInstanceInfo(historicInstance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前活动任务
|
||||
*/
|
||||
public List<Task> getCurrentTasks(String processInstanceId) {
|
||||
return taskService.createTaskQuery()
|
||||
.processInstanceId(processInstanceId)
|
||||
.active()
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程轨迹
|
||||
*/
|
||||
public List<ActivityInstance> getProcessTrace(String processInstanceId) {
|
||||
return historyService.createHistoricActivityInstanceQuery()
|
||||
.processInstanceId(processInstanceId)
|
||||
.orderByHistoricActivityInstanceStartTime()
|
||||
.asc()
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 流程统计分析
|
||||
```java
|
||||
@Service
|
||||
public class ProcessAnalyticsService {
|
||||
|
||||
/**
|
||||
* 统计流程执行时长
|
||||
*/
|
||||
public ProcessDurationStats getProcessDurationStats(String processKey) {
|
||||
// 查询已完成的流程实例
|
||||
List<HistoricProcessInstance> instances = historyService
|
||||
.createHistoricProcessInstanceQuery()
|
||||
.processDefinitionKey(processKey)
|
||||
.finished()
|
||||
.list();
|
||||
|
||||
// 计算统计数据
|
||||
return calculateDurationStats(instances);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计任务处理效率
|
||||
*/
|
||||
public TaskEfficiencyStats getTaskEfficiencyStats(String taskDefinitionKey) {
|
||||
List<HistoricTaskInstance> tasks = historyService
|
||||
.createHistoricTaskInstanceQuery()
|
||||
.taskDefinitionKey(taskDefinitionKey)
|
||||
.finished()
|
||||
.list();
|
||||
|
||||
return calculateTaskEfficiency(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程热力图数据
|
||||
*/
|
||||
public Map<String, Integer> getProcessHeatmapData(String processKey) {
|
||||
// 统计各个节点的通过次数
|
||||
Map<String, Integer> heatmap = new HashMap<>();
|
||||
|
||||
List<HistoricActivityInstance> activities = historyService
|
||||
.createHistoricActivityInstanceQuery()
|
||||
.processDefinitionKey(processKey)
|
||||
.list();
|
||||
|
||||
for (HistoricActivityInstance activity : activities) {
|
||||
String activityId = activity.getActivityId();
|
||||
heatmap.put(activityId, heatmap.getOrDefault(activityId, 0) + 1);
|
||||
}
|
||||
|
||||
return heatmap;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 表单集成
|
||||
|
||||
### 动态表单配置
|
||||
```java
|
||||
@Service
|
||||
public class FormConfigService {
|
||||
|
||||
/**
|
||||
* 获取任务表单配置
|
||||
*/
|
||||
public FormConfigVO getTaskFormConfig(String taskId) {
|
||||
Task task = taskService.createTaskQuery()
|
||||
.taskId(taskId)
|
||||
.singleResult();
|
||||
|
||||
// 获取表单配置
|
||||
String formKey = task.getFormKey();
|
||||
return formConfigMapper.selectByFormKey(formKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表单字段
|
||||
*/
|
||||
public List<FormFieldVO> renderFormFields(String taskId) {
|
||||
FormConfigVO formConfig = getTaskFormConfig(taskId);
|
||||
List<FormFieldVO> fields = JSON.parseArray(formConfig.getFieldsConfig(), FormFieldVO.class);
|
||||
|
||||
// 设置字段权限(可读、可写、隐藏)
|
||||
setFieldPermissions(fields, taskId);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字段权限
|
||||
*/
|
||||
private void setFieldPermissions(List<FormFieldVO> fields, String taskId) {
|
||||
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
|
||||
String taskDefinitionKey = task.getTaskDefinitionKey();
|
||||
|
||||
// 根据任务节点设置字段权限
|
||||
for (FormFieldVO field : fields) {
|
||||
TaskFieldPermissionDO permission = taskFieldPermissionMapper
|
||||
.selectByTaskAndField(taskDefinitionKey, field.getFieldKey());
|
||||
|
||||
if (permission != null) {
|
||||
field.setPermission(permission.getPermission());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 表单数据处理
|
||||
```java
|
||||
@Service
|
||||
public class FormDataService {
|
||||
|
||||
/**
|
||||
* 保存表单数据
|
||||
*/
|
||||
public void saveFormData(String taskId, Map<String, Object> formData) {
|
||||
// 验证表单数据
|
||||
validateFormData(taskId, formData);
|
||||
|
||||
// 保存到流程变量
|
||||
for (Map.Entry<String, Object> entry : formData.entrySet()) {
|
||||
taskService.setVariable(taskId, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// 保存到业务表
|
||||
saveToBusinessTable(taskId, formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表单数据
|
||||
*/
|
||||
public Map<String, Object> getFormData(String taskId) {
|
||||
Map<String, Object> formData = new HashMap<>();
|
||||
|
||||
// 从流程变量获取
|
||||
Map<String, Object> variables = taskService.getVariables(taskId);
|
||||
formData.putAll(variables);
|
||||
|
||||
// 从业务表获取
|
||||
Map<String, Object> businessData = getFromBusinessTable(taskId);
|
||||
formData.putAll(businessData);
|
||||
|
||||
return formData;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流程扩展
|
||||
|
||||
### 自定义监听器
|
||||
```java
|
||||
@Component
|
||||
public class CustomTaskListener implements TaskListener {
|
||||
|
||||
@Override
|
||||
public void notify(DelegateTask delegateTask) {
|
||||
String eventName = delegateTask.getEventName();
|
||||
|
||||
switch (eventName) {
|
||||
case EVENTNAME_CREATE:
|
||||
handleTaskCreate(delegateTask);
|
||||
break;
|
||||
case EVENTNAME_ASSIGNMENT:
|
||||
handleTaskAssignment(delegateTask);
|
||||
break;
|
||||
case EVENTNAME_COMPLETE:
|
||||
handleTaskComplete(delegateTask);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTaskCreate(DelegateTask delegateTask) {
|
||||
// 任务创建时的处理逻辑
|
||||
// 例如:发送通知、记录日志等
|
||||
sendTaskNotification(delegateTask);
|
||||
}
|
||||
|
||||
private void handleTaskAssignment(DelegateTask delegateTask) {
|
||||
// 任务分配时的处理逻辑
|
||||
recordTaskAssignment(delegateTask);
|
||||
}
|
||||
|
||||
private void handleTaskComplete(DelegateTask delegateTask) {
|
||||
// 任务完成时的处理逻辑
|
||||
updateBusinessStatus(delegateTask);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义服务任务
|
||||
```java
|
||||
@Component("customServiceTask")
|
||||
public class CustomServiceTask implements JavaDelegate {
|
||||
|
||||
@Override
|
||||
public void execute(DelegateExecution execution) {
|
||||
// 获取流程变量
|
||||
String businessKey = execution.getProcessBusinessKey();
|
||||
Map<String, Object> variables = execution.getVariables();
|
||||
|
||||
// 执行业务逻辑
|
||||
Object result = executeBusinessLogic(businessKey, variables);
|
||||
|
||||
// 设置返回变量
|
||||
execution.setVariable("serviceResult", result);
|
||||
}
|
||||
|
||||
private Object executeBusinessLogic(String businessKey, Map<String, Object> variables) {
|
||||
// 具体的业务处理逻辑
|
||||
// 例如:调用外部服务、更新数据库等
|
||||
return "处理完成";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 流程设计原则
|
||||
1. **简化流程**: 避免过于复杂的流程设计
|
||||
2. **明确责任**: 每个节点都要有明确的责任人
|
||||
3. **异常处理**: 考虑各种异常情况的处理
|
||||
4. **性能优化**: 避免长时间运行的流程
|
||||
5. **版本管理**: 合理管理流程版本升级
|
||||
|
||||
### 性能优化建议
|
||||
1. **分页查询**: 大量任务数据使用分页
|
||||
2. **索引优化**: 为常用查询字段添加索引
|
||||
3. **历史数据清理**: 定期清理历史流程数据
|
||||
4. **缓存策略**: 缓存流程定义和用户信息
|
||||
5. **异步处理**: 耗时操作使用异步任务
|
||||
|
||||
### 监控告警
|
||||
1. **流程超时**: 监控长时间未处理的任务
|
||||
2. **错误率**: 监控流程执行错误率
|
||||
3. **性能指标**: 监控流程执行时长
|
||||
4. **资源使用**: 监控数据库和内存使用情况
|
Reference in New Issue
Block a user