Compare commits

...

2 Commits

15 changed files with 6358 additions and 0 deletions

View 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));
}
```

View 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) 中控制模块的启用/禁用,根据业务需求按需集成。

View 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. **内存泄漏**: 设置合理的过期时间和大小限制

View 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. 发送通知
}
}
```

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

View 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`

View 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 参数优化

View 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)

View 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. **清理策略**: 定期清理临时和过期文件

View 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
- 国产数据库(达梦、人大金仓等)

View 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. **异常访问**: 检测异常的跨租户访问

View 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. **用量统计**: 统计各类型消息发送量

View 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. **异步处理**: 权限变更异步通知

View 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 的细粒度权限管理

View 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. **资源使用**: 监控数据库和内存使用情况