Compare commits
41 Commits
cursor/cre
...
cursor/bc-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
94e4ed8a44 | ||
![]() |
3f9c3661a8 | ||
![]() |
adece67532 | ||
![]() |
ea5b12f21e | ||
![]() |
39ecf5ebe5 | ||
![]() |
750709d706 | ||
![]() |
3d0eb77148 | ||
![]() |
c789418a7b | ||
![]() |
e50250449a | ||
![]() |
c96f6bb360 | ||
![]() |
af94536a06 | ||
![]() |
a9c7b584cc | ||
![]() |
a54e743a88 | ||
![]() |
64516b2210 | ||
![]() |
e7c9e3dc23 | ||
![]() |
7a8c37d7ac | ||
![]() |
432609b6fa | ||
![]() |
520fb79e2a | ||
![]() |
9b860d9ae5 | ||
![]() |
285da13989 | ||
![]() |
b1d439abf8 | ||
![]() |
b81b8e7aff | ||
![]() |
7a5e28d08a | ||
![]() |
4d5e501a41 | ||
![]() |
876659b01d | ||
![]() |
569ff42e6f | ||
![]() |
bda357508a | ||
![]() |
7fc522938d | ||
![]() |
563985dcfc | ||
![]() |
d8e1610495 | ||
![]() |
eca9307344 | ||
![]() |
a690184524 | ||
![]() |
2ee1e15101 | ||
![]() |
e6fecd8efe | ||
![]() |
94e280eb34 | ||
![]() |
ec5281f2e5 | ||
![]() |
6ac8fa28a7 | ||
![]() |
cac82a13a7 | ||
![]() |
9fc4a4061f | ||
![]() |
a5ad8bb708 | ||
![]() |
3b2a3dd0ea |
@@ -1,469 +0,0 @@
|
||||
# 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));
|
||||
}
|
||||
```
|
@@ -1,73 +0,0 @@
|
||||
# 业务模块指南
|
||||
|
||||
## 系统核心模块
|
||||
|
||||
### 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) 中控制模块的启用/禁用,根据业务需求按需集成。
|
@@ -1,665 +0,0 @@
|
||||
# 缓存策略详解
|
||||
|
||||
## 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. **内存泄漏**: 设置合理的过期时间和大小限制
|
@@ -1,916 +0,0 @@
|
||||
# 代码生成器使用指南
|
||||
|
||||
## 代码生成器概述
|
||||
|
||||
### 功能特性
|
||||
- **前后端代码生成**: 一键生成 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. 发送通知
|
||||
}
|
||||
}
|
||||
```
|
@@ -1,189 +0,0 @@
|
||||
# 常用开发模式指南
|
||||
|
||||
## 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());
|
||||
}
|
||||
}
|
||||
```
|
@@ -1,295 +0,0 @@
|
||||
# 数据库设计规范
|
||||
|
||||
## 表命名规范
|
||||
|
||||
### 模块表前缀
|
||||
- **系统模块**: `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`
|
@@ -1,171 +0,0 @@
|
||||
# 项目启动和部署指南
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 环境要求
|
||||
- **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 参数优化
|
@@ -1,82 +0,0 @@
|
||||
# 开发规范指南
|
||||
|
||||
## 代码规范
|
||||
遵循《阿里巴巴 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)
|
@@ -1,906 +0,0 @@
|
||||
# 文件上传下载指南
|
||||
|
||||
## 文件服务架构
|
||||
|
||||
### 存储方案
|
||||
- **本地存储**: 直接存储到服务器磁盘
|
||||
- **数据库存储**: 文件内容存储到数据库
|
||||
- **云存储**: 对接阿里云 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. **清理策略**: 定期清理临时和过期文件
|
@@ -1,50 +0,0 @@
|
||||
# 模块结构指南
|
||||
|
||||
## 核心模块详解
|
||||
|
||||
### 基础框架模块
|
||||
- **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
|
||||
- 国产数据库(达梦、人大金仓等)
|
@@ -1,709 +0,0 @@
|
||||
# 多租户架构详解
|
||||
|
||||
## 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. **异常访问**: 检测异常的跨租户访问
|
@@ -1,866 +0,0 @@
|
||||
# 消息通知系统
|
||||
|
||||
## 通知系统架构
|
||||
|
||||
### 通知类型
|
||||
- **站内信**: 系统内部消息通知
|
||||
- **邮件通知**: 电子邮件发送
|
||||
- **短信通知**: 手机短信发送
|
||||
- **微信通知**: 微信公众号/小程序推送
|
||||
- **系统公告**: 全站公告通知
|
||||
|
||||
### 核心模块
|
||||
- **通知模板**: 消息模板管理
|
||||
- **通知渠道**: 发送渠道配置
|
||||
- **通知日志**: 发送记录和状态
|
||||
- **通知队列**: 异步消息处理
|
||||
|
||||
## 站内信系统
|
||||
|
||||
### 站内信表结构
|
||||
```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. **用量统计**: 统计各类型消息发送量
|
@@ -1,221 +0,0 @@
|
||||
# 权限系统详解
|
||||
|
||||
## 权限架构概述
|
||||
|
||||
### 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. **异步处理**: 权限变更异步通知
|
@@ -1,45 +0,0 @@
|
||||
# 芋道管理系统项目架构指南
|
||||
|
||||
## 项目概述
|
||||
这是一个基于 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 的细粒度权限管理
|
@@ -1,701 +0,0 @@
|
||||
# 工作流使用指南
|
||||
|
||||
## 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. **资源使用**: 监控数据库和内存使用情况
|
7
pom.xml
7
pom.xml
@@ -88,6 +88,13 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<!-- 确保 Lombok 生成的 getter/setter 方法能被 MapStruct 正确识别,
|
||||
避免出现 No property named “xxx" exists 的编译错误 -->
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
|
@@ -1,253 +1,208 @@
|
||||
-- ----------------------------
|
||||
-- qrtz_blob_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_blob_triggers
|
||||
-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql
|
||||
-- Thanks to Patrick Lightbody for submitting this...
|
||||
--
|
||||
-- In your Quartz properties file, you'll need to set
|
||||
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
||||
|
||||
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
|
||||
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
|
||||
DROP TABLE IF EXISTS QRTZ_LOCKS;
|
||||
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
|
||||
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
|
||||
DROP TABLE IF EXISTS QRTZ_CALENDARS;
|
||||
|
||||
CREATE TABLE QRTZ_JOB_DETAILS
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
blob_data bytea NULL,
|
||||
PRIMARY KEY (sched_name, trigger_name, trigger_group)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NOT NULL,
|
||||
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||
DESCRIPTION VARCHAR(250) NULL,
|
||||
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
|
||||
IS_DURABLE BOOL NOT NULL,
|
||||
IS_NONCONCURRENT BOOL NOT NULL,
|
||||
IS_UPDATE_DATA BOOL NOT NULL,
|
||||
REQUESTS_RECOVERY BOOL NOT NULL,
|
||||
JOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_calendars
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_calendars
|
||||
CREATE TABLE QRTZ_TRIGGERS
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
calendar_name varchar(190) NOT NULL,
|
||||
calendar bytea NOT NULL,
|
||||
PRIMARY KEY (sched_name, calendar_name)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NOT NULL,
|
||||
JOB_GROUP VARCHAR(200) NOT NULL,
|
||||
DESCRIPTION VARCHAR(250) NULL,
|
||||
NEXT_FIRE_TIME BIGINT NULL,
|
||||
PREV_FIRE_TIME BIGINT NULL,
|
||||
PRIORITY INTEGER NULL,
|
||||
TRIGGER_STATE VARCHAR(16) NOT NULL,
|
||||
TRIGGER_TYPE VARCHAR(8) NOT NULL,
|
||||
START_TIME BIGINT NOT NULL,
|
||||
END_TIME BIGINT NULL,
|
||||
CALENDAR_NAME VARCHAR(200) NULL,
|
||||
MISFIRE_INSTR SMALLINT NULL,
|
||||
JOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
REPEAT_COUNT BIGINT NOT NULL,
|
||||
REPEAT_INTERVAL BIGINT NOT NULL,
|
||||
TIMES_TRIGGERED BIGINT NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_CRON_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
CRON_EXPRESSION VARCHAR(120) NOT NULL,
|
||||
TIME_ZONE_ID VARCHAR(80),
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
STR_PROP_1 VARCHAR(512) NULL,
|
||||
STR_PROP_2 VARCHAR(512) NULL,
|
||||
STR_PROP_3 VARCHAR(512) NULL,
|
||||
INT_PROP_1 INT NULL,
|
||||
INT_PROP_2 INT NULL,
|
||||
LONG_PROP_1 BIGINT NULL,
|
||||
LONG_PROP_2 BIGINT NULL,
|
||||
DEC_PROP_1 NUMERIC(13, 4) NULL,
|
||||
DEC_PROP_2 NUMERIC(13, 4) NULL,
|
||||
BOOL_PROP_1 BOOL NULL,
|
||||
BOOL_PROP_2 BOOL NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_BLOB_TRIGGERS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
BLOB_DATA BYTEA NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
|
||||
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_CALENDARS
|
||||
(
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
CALENDAR_NAME VARCHAR(200) NOT NULL,
|
||||
CALENDAR BYTEA NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
|
||||
);
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_cron_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_cron_triggers
|
||||
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
cron_expression varchar(120) NOT NULL,
|
||||
time_zone_id varchar(80) NULL DEFAULT NULL,
|
||||
PRIMARY KEY (sched_name, trigger_name, trigger_group)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
|
||||
);
|
||||
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_fired_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_fired_triggers
|
||||
CREATE TABLE QRTZ_FIRED_TRIGGERS
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
entry_id varchar(95) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
instance_name varchar(190) NOT NULL,
|
||||
fired_time int8 NOT NULL,
|
||||
sched_time int8 NOT NULL,
|
||||
priority int4 NOT NULL,
|
||||
state varchar(16) NOT NULL,
|
||||
job_name varchar(190) NULL DEFAULT NULL,
|
||||
job_group varchar(190) NULL DEFAULT NULL,
|
||||
is_nonconcurrent varchar(1) NULL DEFAULT NULL,
|
||||
requests_recovery varchar(1) NULL DEFAULT NULL,
|
||||
PRIMARY KEY (sched_name, entry_id)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
ENTRY_ID VARCHAR(95) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||
FIRED_TIME BIGINT NOT NULL,
|
||||
SCHED_TIME BIGINT NOT NULL,
|
||||
PRIORITY INTEGER NOT NULL,
|
||||
STATE VARCHAR(16) NOT NULL,
|
||||
JOB_NAME VARCHAR(200) NULL,
|
||||
JOB_GROUP VARCHAR(200) NULL,
|
||||
IS_NONCONCURRENT BOOL NULL,
|
||||
REQUESTS_RECOVERY BOOL NULL,
|
||||
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name);
|
||||
CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery);
|
||||
CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group);
|
||||
CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group);
|
||||
CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
|
||||
CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_job_details
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_job_details
|
||||
CREATE TABLE QRTZ_SCHEDULER_STATE
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
job_name varchar(190) NOT NULL,
|
||||
job_group varchar(190) NOT NULL,
|
||||
description varchar(250) NULL DEFAULT NULL,
|
||||
job_class_name varchar(250) NOT NULL,
|
||||
is_durable varchar(1) NOT NULL,
|
||||
is_nonconcurrent varchar(1) NOT NULL,
|
||||
is_update_data varchar(1) NOT NULL,
|
||||
requests_recovery varchar(1) NOT NULL,
|
||||
job_data bytea NULL,
|
||||
PRIMARY KEY (sched_name, job_name, job_group)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
INSTANCE_NAME VARCHAR(200) NOT NULL,
|
||||
LAST_CHECKIN_TIME BIGINT NOT NULL,
|
||||
CHECKIN_INTERVAL BIGINT NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery);
|
||||
CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group);
|
||||
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_locks
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_locks
|
||||
CREATE TABLE QRTZ_LOCKS
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
lock_name varchar(40) NOT NULL,
|
||||
PRIMARY KEY (sched_name, lock_name)
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
LOCK_NAME VARCHAR(40) NOT NULL,
|
||||
PRIMARY KEY (SCHED_NAME, LOCK_NAME)
|
||||
);
|
||||
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
|
||||
ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
|
||||
CREATE INDEX IDX_QRTZ_J_GRP
|
||||
ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_paused_trigger_grps
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_paused_trigger_grps
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
PRIMARY KEY (sched_name, trigger_group)
|
||||
);
|
||||
CREATE INDEX IDX_QRTZ_T_J
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_JG
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_C
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
|
||||
CREATE INDEX IDX_QRTZ_T_G
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_T_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_N_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_N_G_STATE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
|
||||
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
|
||||
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_scheduler_state
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_scheduler_state
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
instance_name varchar(190) NOT NULL,
|
||||
last_checkin_time int8 NOT NULL,
|
||||
checkin_interval int8 NOT NULL,
|
||||
PRIMARY KEY (sched_name, instance_name)
|
||||
);
|
||||
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_simple_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_simple_triggers
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
repeat_count int8 NOT NULL,
|
||||
repeat_interval int8 NOT NULL,
|
||||
times_triggered int8 NOT NULL,
|
||||
PRIMARY KEY (sched_name, trigger_name, trigger_group)
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_simprop_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_simprop_triggers
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
str_prop_1 varchar(512) NULL DEFAULT NULL,
|
||||
str_prop_2 varchar(512) NULL DEFAULT NULL,
|
||||
str_prop_3 varchar(512) NULL DEFAULT NULL,
|
||||
int_prop_1 int4 NULL DEFAULT NULL,
|
||||
int_prop_2 int4 NULL DEFAULT NULL,
|
||||
long_prop_1 int8 NULL DEFAULT NULL,
|
||||
long_prop_2 int8 NULL DEFAULT NULL,
|
||||
dec_prop_1 numeric(13, 4) NULL DEFAULT NULL,
|
||||
dec_prop_2 numeric(13, 4) NULL DEFAULT NULL,
|
||||
bool_prop_1 varchar(1) NULL DEFAULT NULL,
|
||||
bool_prop_2 varchar(1) NULL DEFAULT NULL,
|
||||
PRIMARY KEY (sched_name, trigger_name, trigger_group)
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- qrtz_triggers
|
||||
-- ----------------------------
|
||||
CREATE TABLE qrtz_triggers
|
||||
(
|
||||
sched_name varchar(120) NOT NULL,
|
||||
trigger_name varchar(190) NOT NULL,
|
||||
trigger_group varchar(190) NOT NULL,
|
||||
job_name varchar(190) NOT NULL,
|
||||
job_group varchar(190) NOT NULL,
|
||||
description varchar(250) NULL DEFAULT NULL,
|
||||
next_fire_time int8 NULL DEFAULT NULL,
|
||||
prev_fire_time int8 NULL DEFAULT NULL,
|
||||
priority int4 NULL DEFAULT NULL,
|
||||
trigger_state varchar(16) NOT NULL,
|
||||
trigger_type varchar(8) NOT NULL,
|
||||
start_time int8 NOT NULL,
|
||||
end_time int8 NULL DEFAULT NULL,
|
||||
calendar_name varchar(190) NULL DEFAULT NULL,
|
||||
misfire_instr int2 NULL DEFAULT NULL,
|
||||
job_data bytea NULL,
|
||||
PRIMARY KEY (sched_name, trigger_name, trigger_group)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group);
|
||||
CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group);
|
||||
CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name);
|
||||
CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group);
|
||||
CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state);
|
||||
CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state);
|
||||
CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state);
|
||||
CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time);
|
||||
CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time);
|
||||
CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time);
|
||||
CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state);
|
||||
CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group,
|
||||
trigger_state);
|
||||
|
||||
-- @formatter:off
|
||||
BEGIN;
|
||||
COMMIT;
|
||||
-- @formatter:on
|
||||
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
|
||||
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
|
||||
CREATE INDEX IDX_QRTZ_FT_J_G
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_JG
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_T_G
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
|
||||
CREATE INDEX IDX_QRTZ_FT_TG
|
||||
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- FK: qrtz_blob_triggers
|
||||
-- ----------------------------
|
||||
ALTER TABLE qrtz_blob_triggers
|
||||
ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
|
||||
trigger_name,
|
||||
trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- FK: qrtz_cron_triggers
|
||||
-- ----------------------------
|
||||
ALTER TABLE qrtz_cron_triggers
|
||||
ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- FK: qrtz_simple_triggers
|
||||
-- ----------------------------
|
||||
ALTER TABLE qrtz_simple_triggers
|
||||
ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
|
||||
trigger_name,
|
||||
trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- FK: qrtz_simprop_triggers
|
||||
-- ----------------------------
|
||||
ALTER TABLE qrtz_simprop_triggers
|
||||
ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
|
||||
|
||||
-- ----------------------------
|
||||
-- FK: qrtz_triggers
|
||||
-- ----------------------------
|
||||
ALTER TABLE qrtz_triggers
|
||||
ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group);
|
||||
COMMIT;
|
@@ -18,7 +18,7 @@
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.framework.version>5.3.39</spring.framework.version>
|
||||
<spring.security.version>5.8.14</spring.security.version>
|
||||
<spring.security.version>5.8.16</spring.security.version>
|
||||
<spring.boot.version>2.7.18</spring.boot.version>
|
||||
<!-- Web 相关 -->
|
||||
<springdoc.version>1.8.0</springdoc.version>
|
||||
@@ -76,7 +76,8 @@
|
||||
<awssdk.version>2.30.14</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||
<jimureport.version>1.9.4</jimureport.version>
|
||||
<jimureport.version>2.1.0</jimureport.version>
|
||||
<jimubi.version>1.9.5</jimubi.version>
|
||||
<weixin-java.version>4.7.5.B</weixin-java.version>
|
||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
||||
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
||||
@@ -626,7 +627,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.jimureport</groupId>
|
||||
<artifactId>jimubi-spring-boot-starter</artifactId>
|
||||
<version>${jimureport.version}</version>
|
||||
<version>${jimubi.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
|
@@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
|
||||
/**
|
||||
* 异步任务 Configuration
|
||||
@@ -21,13 +22,20 @@ public class YudaoAsyncAutoConfiguration {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (!(bean instanceof ThreadPoolTaskExecutor)) {
|
||||
return bean;
|
||||
// 处理 ThreadPoolTaskExecutor
|
||||
if (bean instanceof ThreadPoolTaskExecutor) {
|
||||
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
|
||||
executor.setTaskDecorator(TtlRunnable::get);
|
||||
return executor;
|
||||
}
|
||||
// 修改提交的任务,接入 TransmittableThreadLocal
|
||||
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
|
||||
executor.setTaskDecorator(TtlRunnable::get);
|
||||
return executor;
|
||||
// 处理 SimpleAsyncTaskExecutor
|
||||
// 参考 https://t.zsxq.com/CBoks 增加
|
||||
if (bean instanceof SimpleAsyncTaskExecutor) {
|
||||
SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean;
|
||||
executor.setTaskDecorator(TtlRunnable::get);
|
||||
return executor;
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
};
|
||||
|
@@ -24,8 +24,8 @@
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 -->
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,只有 DefaultDBFieldHandler 使用到 -->
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.mybatis.core.handler;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
@@ -32,7 +32,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
baseDO.setUpdateTime(current);
|
||||
}
|
||||
|
||||
Long userId = WebFrameworkUtils.getLoginUserId();
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
|
||||
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
|
||||
baseDO.setCreator(userId.toString());
|
||||
@@ -54,7 +54,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
Object modifier = getFieldValByName("updater", metaObject);
|
||||
Long userId = WebFrameworkUtils.getLoginUserId();
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
|
||||
setFieldValByName("updater", userId.toString(), metaObject);
|
||||
}
|
||||
|
@@ -42,6 +42,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
|
||||
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
|
||||
// 特殊:不分页,直接查询全部
|
||||
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
|
||||
MyBatisUtils.addOrder(queryWrapper, sortingFields);
|
||||
List<T> list = selectList(queryWrapper);
|
||||
return new PageResult<>(list, (long) list.size());
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.mybatis.core.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.func.Func1;
|
||||
import cn.hutool.core.lang.func.LambdaUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.SortingField;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
@@ -20,7 +22,6 @@ import net.sf.jsqlparser.schema.Table;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MyBatis 工具类
|
||||
@@ -37,15 +38,27 @@ public class MyBatisUtils {
|
||||
// 页码 + 数量
|
||||
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
|
||||
// 排序字段
|
||||
if (!CollectionUtil.isEmpty(sortingFields)) {
|
||||
page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder())
|
||||
? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField()))
|
||||
: OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField())))
|
||||
.collect(Collectors.toList()));
|
||||
if (CollUtil.isNotEmpty(sortingFields)) {
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
|
||||
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
|
||||
}
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
|
||||
if (CollUtil.isEmpty(sortingFields)) {
|
||||
return;
|
||||
}
|
||||
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
|
||||
for (SortingField sortingField : sortingFields) {
|
||||
query.orderBy(true,
|
||||
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
|
||||
StrUtil.toUnderlineCase(sortingField.getField()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将拦截器添加到链中
|
||||
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
|
||||
|
@@ -126,8 +126,10 @@ public class SecurityFrameworkUtils {
|
||||
|
||||
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
|
||||
// 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
|
||||
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
|
||||
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
|
||||
if (request != null) {
|
||||
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
|
||||
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
|
||||
}
|
||||
}
|
||||
|
||||
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
|
||||
|
@@ -19,7 +19,8 @@
|
||||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
</description>
|
||||
<properties>
|
||||
<spring-ai.version>1.0.0-M6</spring-ai.version>
|
||||
<spring-ai.version>1.0.0</spring-ai.version>
|
||||
<alibaba-ai.version>1.0.0.2</alibaba-ai.version>
|
||||
<tinyflow.version>1.0.2</tinyflow.version>
|
||||
</properties>
|
||||
|
||||
@@ -75,65 +76,73 @@
|
||||
<!-- Spring AI Model 模型接入 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-ollama</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 通义千问 -->
|
||||
<groupId>com.alibaba.cloud.ai</groupId>
|
||||
<artifactId>spring-ai-alibaba-starter</artifactId>
|
||||
<version>${spring-ai.version}.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 文心一言 -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-stability-ai</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 智谱 GLM -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-minimax-spring-boot-starter</artifactId>
|
||||
<artifactId>spring-ai-starter-model-minimax</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-moonshot-spring-boot-starter</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<!-- 通义千问 -->
|
||||
<groupId>com.alibaba.cloud.ai</groupId>
|
||||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
||||
<version>${alibaba-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<!-- 文心一言 -->
|
||||
<groupId>org.springaicommunity</groupId>
|
||||
<artifactId>qianfan-spring-boot-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 月之暗灭 -->
|
||||
<groupId>org.springaicommunity</groupId>
|
||||
<artifactId>moonshot-spring-boot-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 向量存储:https://db-engines.com/en/ranking/vector+dbms -->
|
||||
<dependency>
|
||||
<!-- Qdrant:https://qdrant.tech/ -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant-store</artifactId>
|
||||
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<!-- Redis:https://redis.io/docs/latest/develop/get-started/vector-database/ -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-redis-store</artifactId>
|
||||
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -144,7 +153,7 @@
|
||||
<dependency>
|
||||
<!-- Milvus:https://milvus.io/ -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-milvus-store</artifactId>
|
||||
<artifactId>spring-ai-starter-vector-store-milvus</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<exclusions>
|
||||
<!-- 解决和 logback 的日志冲突 -->
|
||||
|
@@ -5,7 +5,6 @@ import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
@@ -14,10 +13,6 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
|
||||
import org.springframework.ai.embedding.BatchingStrategy;
|
||||
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
@@ -26,6 +21,10 @@ import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
|
||||
import org.springframework.ai.tokenizer.TokenCountEstimator;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
|
||||
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
|
||||
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -52,33 +51,6 @@ public class AiAutoConfiguration {
|
||||
|
||||
// ========== 各种 AI Client 创建 ==========
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true")
|
||||
public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek();
|
||||
return buildDeepSeekChatModel(properties);
|
||||
}
|
||||
|
||||
public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(DeepSeekChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(DeepSeekChatModel.BASE_URL)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
.topP(properties.getTopP())
|
||||
.build())
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
return new DeepSeekChatModel(openAiChatModel);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
|
||||
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
|
@@ -13,12 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@Data
|
||||
public class YudaoAiProperties {
|
||||
|
||||
/**
|
||||
* DeepSeek
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private DeepSeekProperties deepseek;
|
||||
|
||||
/**
|
||||
* 字节豆包
|
||||
*/
|
||||
@@ -60,19 +54,6 @@ public class YudaoAiProperties {
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private SunoProperties suno;
|
||||
|
||||
@Data
|
||||
public static class DeepSeekProperties {
|
||||
|
||||
private String enable;
|
||||
private String apiKey;
|
||||
|
||||
private String model;
|
||||
private Double temperature;
|
||||
private Integer maxTokens;
|
||||
private Double topP;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DouBaoProperties {
|
||||
|
||||
|
@@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.RuntimeUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
@@ -22,8 +22,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
|
||||
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration;
|
||||
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration;
|
||||
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration;
|
||||
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
|
||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
|
||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
@@ -32,47 +33,55 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
|
||||
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
|
||||
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.KeyCredential;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.milvus.client.MilvusServiceClient;
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
|
||||
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties;
|
||||
import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
|
||||
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
|
||||
import org.springaicommunity.moonshot.MoonshotChatModel;
|
||||
import org.springaicommunity.moonshot.MoonshotChatOptions;
|
||||
import org.springaicommunity.moonshot.api.MoonshotApi;
|
||||
import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
|
||||
import org.springaicommunity.qianfan.QianFanChatModel;
|
||||
import org.springaicommunity.qianfan.QianFanEmbeddingModel;
|
||||
import org.springaicommunity.qianfan.QianFanEmbeddingOptions;
|
||||
import org.springaicommunity.qianfan.QianFanImageModel;
|
||||
import org.springaicommunity.qianfan.api.QianFanApi;
|
||||
import org.springaicommunity.qianfan.api.QianFanImageApi;
|
||||
import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;
|
||||
import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
||||
import org.springframework.ai.deepseek.api.DeepSeekApi;
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
import org.springframework.ai.embedding.BatchingStrategy;
|
||||
import org.springframework.ai.embedding.EmbeddingModel;
|
||||
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
|
||||
import org.springframework.ai.image.ImageModel;
|
||||
import org.springframework.ai.minimax.MiniMaxChatModel;
|
||||
import org.springframework.ai.minimax.MiniMaxChatOptions;
|
||||
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
|
||||
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
|
||||
import org.springframework.ai.minimax.api.MiniMaxApi;
|
||||
import org.springframework.ai.model.function.FunctionCallbackResolver;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
|
||||
import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
|
||||
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;
|
||||
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
|
||||
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
|
||||
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
|
||||
import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.moonshot.MoonshotChatModel;
|
||||
import org.springframework.ai.moonshot.MoonshotChatOptions;
|
||||
import org.springframework.ai.moonshot.api.MoonshotApi;
|
||||
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
|
||||
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration;
|
||||
import org.springframework.ai.ollama.OllamaChatModel;
|
||||
import org.springframework.ai.ollama.OllamaEmbeddingModel;
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
@@ -84,21 +93,23 @@ import org.springframework.ai.openai.OpenAiImageModel;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
|
||||
import org.springframework.ai.qianfan.QianFanChatModel;
|
||||
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
|
||||
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
|
||||
import org.springframework.ai.qianfan.QianFanImageModel;
|
||||
import org.springframework.ai.qianfan.api.QianFanApi;
|
||||
import org.springframework.ai.qianfan.api.QianFanImageApi;
|
||||
import org.springframework.ai.stabilityai.StabilityAiImageModel;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiApi;
|
||||
import org.springframework.ai.vectorstore.SimpleVectorStore;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientConnectionDetails;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
|
||||
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
|
||||
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
|
||||
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
|
||||
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
|
||||
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
|
||||
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
|
||||
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
|
||||
import org.springframework.ai.zhipuai.*;
|
||||
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
|
||||
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
|
||||
@@ -190,7 +201,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
case XING_HUO:
|
||||
return SpringUtil.getBean(XingHuoChatModel.class);
|
||||
case BAI_CHUAN:
|
||||
return SpringUtil.getBean(AzureOpenAiChatModel.class);
|
||||
return SpringUtil.getBean(BaiChuanChatModel.class);
|
||||
case OPENAI:
|
||||
return SpringUtil.getBean(OpenAiChatModel.class);
|
||||
case AZURE_OPENAI:
|
||||
@@ -319,27 +330,34 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
// ========== 各种创建 spring-ai 客户端的方法 ==========
|
||||
|
||||
/**
|
||||
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法
|
||||
* 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法
|
||||
*/
|
||||
private static DashScopeChatModel buildTongYiChatModel(String key) {
|
||||
DashScopeApi dashScopeApi = new DashScopeApi(key);
|
||||
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build();
|
||||
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
|
||||
.withTemperature(0.7).build();
|
||||
return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
|
||||
return DashScopeChatModel.builder()
|
||||
.dashScopeApi(dashScopeApi)
|
||||
.defaultOptions(options)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法
|
||||
* 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法
|
||||
*/
|
||||
private static DashScopeImageModel buildTongYiImagesModel(String key) {
|
||||
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
|
||||
return new DashScopeImageModel(dashScopeImageApi);
|
||||
return DashScopeImageModel.builder()
|
||||
.dashScopeApi(dashScopeImageApi)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法
|
||||
* 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法
|
||||
*/
|
||||
private static QianFanChatModel buildYiYanChatModel(String key) {
|
||||
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
|
||||
List<String> keys = StrUtil.split(key, '|');
|
||||
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
|
||||
String appKey = keys.get(0);
|
||||
@@ -349,9 +367,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法
|
||||
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法
|
||||
*/
|
||||
private QianFanImageModel buildQianFanImageModel(String key) {
|
||||
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
|
||||
List<String> keys = StrUtil.split(key, '|');
|
||||
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
|
||||
String appKey = keys.get(0);
|
||||
@@ -361,12 +380,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)}
|
||||
* 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法
|
||||
*/
|
||||
private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) {
|
||||
YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties()
|
||||
.setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildDeepSeekChatModel(properties);
|
||||
DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build();
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL)
|
||||
.temperature(0.7).build();
|
||||
return DeepSeekChatModel.builder()
|
||||
.deepSeekApi(deepSeekApi)
|
||||
.defaultOptions(options)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,17 +421,18 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法
|
||||
* 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法
|
||||
*/
|
||||
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
|
||||
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
|
||||
: new ZhiPuAiApi(url, apiKey);
|
||||
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
|
||||
return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
|
||||
return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
|
||||
getObservationRegistry().getIfAvailable());
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法
|
||||
* 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法
|
||||
*/
|
||||
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
|
||||
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
|
||||
@@ -416,23 +441,30 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法
|
||||
* 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法
|
||||
*/
|
||||
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
|
||||
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
|
||||
: new MiniMaxApi(url, apiKey);
|
||||
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
|
||||
return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
|
||||
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法
|
||||
* 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法
|
||||
*/
|
||||
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
|
||||
MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey)
|
||||
: new MoonshotApi(url, apiKey);
|
||||
MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()
|
||||
.apiKey(apiKey);
|
||||
if (StrUtil.isNotEmpty(url)) {
|
||||
moonshotApiBuilder.baseUrl(url);
|
||||
}
|
||||
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
|
||||
return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
|
||||
return MoonshotChatModel.builder()
|
||||
.moonshotApi(moonshotApiBuilder.build())
|
||||
.defaultOptions(options)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,33 +488,32 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法
|
||||
* 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法
|
||||
*/
|
||||
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
|
||||
return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiApi(openAiApi)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
|
||||
/**
|
||||
* 可参考 {@link AzureOpenAiAutoConfiguration}
|
||||
* 可参考 {@link AzureOpenAiChatAutoConfiguration}
|
||||
*/
|
||||
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
|
||||
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
|
||||
// 创建 OpenAIClient 对象
|
||||
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
|
||||
connectionProperties.setApiKey(apiKey);
|
||||
connectionProperties.setEndpoint(url);
|
||||
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
|
||||
// 获取 AzureOpenAiChatProperties 对象
|
||||
AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
|
||||
return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties,
|
||||
getToolCallingManager(), null, null);
|
||||
// TODO @芋艿:使用前,请测试,暂时没密钥!!!
|
||||
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
|
||||
.endpoint(url).credential(new KeyCredential(apiKey));
|
||||
return AzureOpenAiChatModel.builder()
|
||||
.openAIClientBuilder(openAIClientBuilder)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法
|
||||
* 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
|
||||
*/
|
||||
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
@@ -500,11 +531,14 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法
|
||||
* 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法
|
||||
*/
|
||||
private static OllamaChatModel buildOllamaChatModel(String url) {
|
||||
OllamaApi ollamaApi = new OllamaApi(url);
|
||||
return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build();
|
||||
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
|
||||
return OllamaChatModel.builder()
|
||||
.ollamaApi(ollamaApi)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -519,16 +553,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
// ========== 各种创建 EmbeddingModel 的方法 ==========
|
||||
|
||||
/**
|
||||
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法
|
||||
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法
|
||||
*/
|
||||
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
|
||||
DashScopeApi dashScopeApi = new DashScopeApi(apiKey);
|
||||
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
|
||||
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
|
||||
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
|
||||
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
|
||||
*/
|
||||
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
|
||||
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
|
||||
@@ -538,7 +572,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法
|
||||
* 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法
|
||||
*/
|
||||
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
|
||||
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
|
||||
@@ -548,7 +582,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法
|
||||
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法
|
||||
*/
|
||||
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
|
||||
List<String> keys = StrUtil.split(key, '|');
|
||||
@@ -561,13 +595,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
}
|
||||
|
||||
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
|
||||
OllamaApi ollamaApi = new OllamaApi(url);
|
||||
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
|
||||
OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build();
|
||||
return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build();
|
||||
return OllamaEmbeddingModel.builder()
|
||||
.ollamaApi(ollamaApi)
|
||||
.defaultOptions(ollamaOptions)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法
|
||||
* 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法
|
||||
*/
|
||||
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
|
||||
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
|
||||
@@ -576,21 +613,19 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
|
||||
}
|
||||
|
||||
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
|
||||
/**
|
||||
* 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
|
||||
* 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
|
||||
*/
|
||||
private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) {
|
||||
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
|
||||
// 创建 OpenAIClient 对象
|
||||
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
|
||||
connectionProperties.setApiKey(apiKey);
|
||||
connectionProperties.setEndpoint(url);
|
||||
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
|
||||
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
|
||||
AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration();
|
||||
// 创建 OpenAIClientBuilder 对象
|
||||
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
|
||||
.endpoint(url).credential(new KeyCredential(apiKey));
|
||||
// 获取 AzureOpenAiChatProperties 对象
|
||||
AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class);
|
||||
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties,
|
||||
null, null);
|
||||
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties,
|
||||
getObservationRegistry(), getEmbeddingModelObservationConvention());
|
||||
}
|
||||
|
||||
// ========== 各种创建 VectorStore 的方法 ==========
|
||||
@@ -655,12 +690,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
Map<String, Class<?>> metadataFields) {
|
||||
// 创建 JedisPooled 对象
|
||||
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
|
||||
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort());
|
||||
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(),
|
||||
redisProperties.getUsername(), redisProperties.getPassword());
|
||||
// 创建 RedisVectorStoreProperties 对象
|
||||
RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration();
|
||||
RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class);
|
||||
RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel)
|
||||
.indexName(properties.getIndex()).prefix(properties.getPrefix())
|
||||
.indexName(properties.getIndexName()).prefix(properties.getPrefix())
|
||||
.initializeSchema(properties.isInitializeSchema())
|
||||
.metadataFields(convertList(metadataFields.entrySet(), entry -> {
|
||||
String fieldName = entry.getKey();
|
||||
@@ -730,10 +765,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
|
||||
private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() {
|
||||
return new ObjectProvider<>() {
|
||||
|
||||
@Override
|
||||
public VectorStoreObservationConvention getObject() throws BeansException {
|
||||
return new DefaultVectorStoreObservationConvention();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -745,8 +782,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
return SpringUtil.getBean(ToolCallingManager.class);
|
||||
}
|
||||
|
||||
private static FunctionCallbackResolver getFunctionCallbackResolver() {
|
||||
return SpringUtil.getBean(FunctionCallbackResolver.class);
|
||||
private static ObjectProvider<EmbeddingModelObservationConvention> getEmbeddingModelObservationConvention() {
|
||||
return new ObjectProvider<>() {
|
||||
|
||||
@Override
|
||||
public EmbeddingModelObservationConvention getObject() throws BeansException {
|
||||
return SpringUtil.getBean(EmbeddingModelObservationConvention.class);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,45 +0,0 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* DeepSeek {@link ChatModel} 实现类
|
||||
*
|
||||
* @author fansili
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DeepSeekChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://api.deepseek.com";
|
||||
|
||||
public static final String MODEL_DEFAULT = "deepseek-chat";
|
||||
|
||||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
return openAiChatModel.call(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(Prompt prompt) {
|
||||
return openAiChatModel.stream(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatOptions getDefaultOptions() {
|
||||
return openAiChatModel.getDefaultOptions();
|
||||
}
|
||||
|
||||
}
|
@@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel {
|
||||
var observationContext = ImageModelObservationContext.builder()
|
||||
.imagePrompt(imagePrompt)
|
||||
.provider(SiliconFlowApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(imagePrompt.getOptions())
|
||||
.imagePrompt(imagePrompt)
|
||||
.build();
|
||||
|
||||
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION
|
||||
|
@@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
|
||||
@@ -24,17 +21,20 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
|
||||
import cn.iocoder.yudao.module.infra.api.file.FileApi;
|
||||
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springaicommunity.qianfan.QianFanImageOptions;
|
||||
import org.springframework.ai.image.ImageModel;
|
||||
import org.springframework.ai.image.ImageOptions;
|
||||
import org.springframework.ai.image.ImagePrompt;
|
||||
import org.springframework.ai.image.ImageResponse;
|
||||
import org.springframework.ai.openai.OpenAiImageOptions;
|
||||
import org.springframework.ai.qianfan.QianFanImageOptions;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
|
||||
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService {
|
||||
private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) {
|
||||
if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) {
|
||||
// https://platform.openai.com/docs/api-reference/images/create
|
||||
return OpenAiImageOptions.builder().withModel(model.getModel())
|
||||
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
|
||||
.withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格
|
||||
.withResponseFormat("b64_json")
|
||||
return OpenAiImageOptions.builder().model(model.getModel())
|
||||
.height(draw.getHeight()).width(draw.getWidth())
|
||||
.style(MapUtil.getStr(draw.getOptions(), "style")) // 风格
|
||||
.responseFormat("b64_json")
|
||||
.build();
|
||||
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) {
|
||||
// https://docs.siliconflow.cn/cn/api-reference/images/images-generations
|
||||
|
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import com.fasterxml.jackson.annotation.JsonClassDescription;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -17,7 +19,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* 工具:当前用户信息查询
|
||||
* 工具:用户信息查询
|
||||
*
|
||||
* 同时,也是展示 ToolContext 上下文的使用
|
||||
*
|
||||
@@ -31,8 +33,17 @@ public class UserProfileQueryToolFunction
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Data
|
||||
@JsonClassDescription("当前用户信息查询")
|
||||
public static class Request { }
|
||||
@JsonClassDescription("用户信息查询")
|
||||
public static class Request {
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
@JsonProperty(value = "id")
|
||||
@JsonPropertyDescription("用户编号,例如说:1。如果查询自己,则 id 为空")
|
||||
private Long id;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@@ -61,13 +72,19 @@ public class UserProfileQueryToolFunction
|
||||
|
||||
@Override
|
||||
public Response apply(Request request, ToolContext toolContext) {
|
||||
LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER);
|
||||
Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID);
|
||||
if (loginUser == null | tenantId == null) {
|
||||
return null;
|
||||
if (tenantId == null) {
|
||||
return new Response();
|
||||
}
|
||||
if (request.getId() == null) {
|
||||
LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER);
|
||||
if (loginUser == null) {
|
||||
return new Response();
|
||||
}
|
||||
request.setId(loginUser.getId());
|
||||
}
|
||||
return TenantUtils.execute(tenantId, () -> {
|
||||
AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId());
|
||||
AdminUserRespDTO user = adminUserApi.getUser(request.getId());
|
||||
return BeanUtils.toBean(user, Response.class);
|
||||
});
|
||||
}
|
||||
|
@@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
|
||||
import org.springaicommunity.moonshot.MoonshotChatOptions;
|
||||
import org.springaicommunity.qianfan.QianFanChatOptions;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.minimax.MiniMaxChatOptions;
|
||||
import org.springframework.ai.moonshot.MoonshotChatOptions;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.qianfan.QianFanChatOptions;
|
||||
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -43,18 +43,18 @@ public class AiUtils {
|
||||
switch (platform) {
|
||||
case TONG_YI:
|
||||
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
|
||||
.withFunctions(toolNames).withToolContext(toolContext).build();
|
||||
.withToolNames(toolNames).withToolContext(toolContext).build();
|
||||
case YI_YAN:
|
||||
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
|
||||
case ZHI_PU:
|
||||
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
||||
.functions(toolNames).toolContext(toolContext).build();
|
||||
.toolNames(toolNames).toolContext(toolContext).build();
|
||||
case MINI_MAX:
|
||||
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
||||
.functions(toolNames).toolContext(toolContext).build();
|
||||
.toolNames(toolNames).toolContext(toolContext).build();
|
||||
case MOONSHOT:
|
||||
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
||||
.functions(toolNames).toolContext(toolContext).build();
|
||||
.toolNames(toolNames).toolContext(toolContext).build();
|
||||
case OPENAI:
|
||||
case DEEP_SEEK: // 复用 OpenAI 客户端
|
||||
case DOU_BAO: // 复用 OpenAI 客户端
|
||||
|
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import com.azure.ai.openai.OpenAIClientBuilder;
|
||||
import com.azure.core.credential.AzureKeyCredential;
|
||||
import com.azure.core.util.ClientOptions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
|
||||
@@ -17,7 +16,7 @@ import reactor.core.publisher.Flux;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
|
||||
import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
|
||||
|
||||
/**
|
||||
* {@link AzureOpenAiChatModel} 集成测试
|
||||
@@ -29,10 +28,13 @@ public class AzureOpenAIChatModelTests {
|
||||
// TODO @芋艿:晚点在调整
|
||||
private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder()
|
||||
.endpoint("https://eastusprejade.openai.azure.com")
|
||||
.credential(new AzureKeyCredential("xxx"))
|
||||
.clientOptions((new ClientOptions()).setApplicationId("spring-ai"));
|
||||
private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
|
||||
AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build());
|
||||
.credential(new AzureKeyCredential("xxx"));
|
||||
private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
|
||||
.openAIClientBuilder(openAiApi)
|
||||
.defaultOptions(AzureOpenAiChatOptions.builder()
|
||||
.deploymentName(DEFAULT_DEPLOYMENT_NAME)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
@@ -35,7 +34,7 @@ public class BaiChuanChatModelTests {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
|
||||
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel);
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
@@ -8,9 +7,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
||||
import org.springframework.ai.deepseek.api.DeepSeekApi;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -23,19 +22,16 @@ import java.util.List;
|
||||
*/
|
||||
public class DeepSeekChatModelTests {
|
||||
|
||||
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(DeepSeekChatModel.BASE_URL)
|
||||
.apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey
|
||||
private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder()
|
||||
.deepSeekApi(DeepSeekApi.builder()
|
||||
.apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.defaultOptions(DeepSeekChatOptions.builder()
|
||||
.model("deepseek-chat") // 模型
|
||||
.temperature(0.7)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
|
@@ -1,20 +1,6 @@
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.ollama.OllamaChatModel;
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.api.OllamaModel;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link OllamaChatModel} 集成测试
|
||||
@@ -23,43 +9,43 @@ import java.util.List;
|
||||
*/
|
||||
public class LlamaChatModelTests {
|
||||
|
||||
private final OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
|
||||
.defaultOptions(OllamaOptions.builder()
|
||||
.model(OllamaModel.LLAMA3.getName()) // 模型
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
ChatResponse response = chatModel.call(new Prompt(messages));
|
||||
// 打印结果
|
||||
System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
// private final OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
|
||||
// .defaultOptions(OllamaOptions.builder()
|
||||
// .model(OllamaModel.LLAMA3.getName()) // 模型
|
||||
// .build())
|
||||
// .build();
|
||||
//
|
||||
// @Test
|
||||
// @Disabled
|
||||
// public void testCall() {
|
||||
// // 准备参数
|
||||
// List<Message> messages = new ArrayList<>();
|
||||
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
// messages.add(new UserMessage("1 + 1 = ?"));
|
||||
//
|
||||
// // 调用
|
||||
// ChatResponse response = chatModel.call(new Prompt(messages));
|
||||
// // 打印结果
|
||||
// System.out.println(response);
|
||||
// System.out.println(response.getResult().getOutput());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @Disabled
|
||||
// public void testStream() {
|
||||
// // 准备参数
|
||||
// List<Message> messages = new ArrayList<>();
|
||||
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
// messages.add(new UserMessage("1 + 1 = ?"));
|
||||
//
|
||||
// // 调用
|
||||
// Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// // 打印结果
|
||||
// flux.doOnNext(response -> {
|
||||
//// System.out.println(response);
|
||||
// System.out.println(response.getResult().getOutput());
|
||||
// }).then().block();
|
||||
// }
|
||||
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.moonshot.MoonshotChatModel;
|
||||
import org.springaicommunity.moonshot.MoonshotChatOptions;
|
||||
import org.springaicommunity.moonshot.api.MoonshotApi;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.moonshot.MoonshotChatModel;
|
||||
import org.springframework.ai.moonshot.MoonshotChatOptions;
|
||||
import org.springframework.ai.moonshot.api.MoonshotApi;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -22,11 +22,15 @@ import java.util.List;
|
||||
*/
|
||||
public class MoonshotChatModelTests {
|
||||
|
||||
private final MoonshotChatModel chatModel = new MoonshotChatModel(
|
||||
new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥
|
||||
MoonshotChatOptions.builder()
|
||||
.model("moonshot-v1-8k") // 模型
|
||||
.build());
|
||||
private final MoonshotChatModel chatModel = MoonshotChatModel.builder()
|
||||
.moonshotApi(MoonshotApi.builder()
|
||||
.apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥
|
||||
.build())
|
||||
.defaultOptions(MoonshotChatOptions.builder()
|
||||
.model("kimi-k2-0711-preview") // 模型
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCall() {
|
||||
|
@@ -23,7 +23,9 @@ import java.util.List;
|
||||
public class OllamaChatModelTests {
|
||||
|
||||
private final OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
|
||||
.ollamaApi(OllamaApi.builder()
|
||||
.baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
|
||||
.build())
|
||||
.defaultOptions(OllamaOptions.builder()
|
||||
// .model("qwen") // 模型(https://ollama.com/library/qwen)
|
||||
.model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1)
|
||||
|
@@ -25,10 +25,10 @@ public class OpenAIChatModelTests {
|
||||
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl("https://api.holdai.top")
|
||||
.apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey
|
||||
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(OpenAiApi.ChatModel.GPT_4_O) // 模型
|
||||
.model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型
|
||||
.temperature(0.7)
|
||||
.build())
|
||||
.build();
|
||||
|
@@ -22,14 +22,17 @@ import java.util.List;
|
||||
*/
|
||||
public class TongYiChatModelTests {
|
||||
|
||||
private final DashScopeChatModel chatModel = new DashScopeChatModel(
|
||||
new DashScopeApi("sk-7d903764249848cfa912733146da12d1"),
|
||||
DashScopeChatOptions.builder()
|
||||
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
|
||||
.dashScopeApi(DashScopeApi.builder()
|
||||
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
|
||||
.build())
|
||||
.defaultOptions( DashScopeChatOptions.builder()
|
||||
.withModel("qwen1.5-72b-chat") // 模型
|
||||
// .withModel("deepseek-r1") // 模型(deepseek-r1)
|
||||
// .withModel("deepseek-v3") // 模型(deepseek-v3)
|
||||
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
|
||||
.build());
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
|
@@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.qianfan.QianFanChatModel;
|
||||
import org.springaicommunity.qianfan.QianFanChatOptions;
|
||||
import org.springaicommunity.qianfan.api.QianFanApi;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.qianfan.QianFanChatModel;
|
||||
import org.springframework.ai.qianfan.QianFanChatOptions;
|
||||
import org.springframework.ai.qianfan.api.QianFanApi;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -23,9 +23,9 @@ import java.util.List;
|
||||
public class YiYanChatModelTests {
|
||||
|
||||
private final QianFanChatModel chatModel = new QianFanChatModel(
|
||||
new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥
|
||||
new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥
|
||||
QianFanChatOptions.builder()
|
||||
.model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue())
|
||||
.model("ERNIE-4.5-8K-Preview")
|
||||
.build()
|
||||
);
|
||||
|
||||
|
@@ -18,7 +18,7 @@ public class OpenAiImageModelTests {
|
||||
|
||||
private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
|
||||
.baseUrl("https://api.holdai.top") // apiKey
|
||||
.apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17")
|
||||
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD")
|
||||
.build());
|
||||
|
||||
@Test
|
||||
@@ -26,8 +26,8 @@ public class OpenAiImageModelTests {
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
ImageOptions options = OpenAiImageOptions.builder()
|
||||
.withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
|
||||
.withHeight(256).withWidth(256)
|
||||
.model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
|
||||
.height(256).width(256)
|
||||
.build();
|
||||
ImagePrompt prompt = new ImagePrompt("中国长城!", options);
|
||||
|
||||
|
@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.qianfan.QianFanImageModel;
|
||||
import org.springaicommunity.qianfan.QianFanImageOptions;
|
||||
import org.springaicommunity.qianfan.api.QianFanImageApi;
|
||||
import org.springframework.ai.image.ImagePrompt;
|
||||
import org.springframework.ai.image.ImageResponse;
|
||||
import org.springframework.ai.qianfan.QianFanImageModel;
|
||||
import org.springframework.ai.qianfan.QianFanImageOptions;
|
||||
import org.springframework.ai.qianfan.api.QianFanImageApi;
|
||||
|
||||
import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;
|
||||
|
||||
|
@@ -31,8 +31,8 @@ public class StabilityAiImageModelTests {
|
||||
public void testCall() {
|
||||
// 准备参数
|
||||
ImageOptions options = OpenAiImageOptions.builder()
|
||||
.withModel("stable-diffusion-v1-6")
|
||||
.withHeight(320).withWidth(320)
|
||||
.model("stable-diffusion-v1-6")
|
||||
.height(320).width(320)
|
||||
.build();
|
||||
ImagePrompt prompt = new ImagePrompt("great wall", options);
|
||||
|
||||
|
@@ -142,6 +142,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
|
||||
updateBusinessProduct(updateObj.getId(), businessProducts);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class));
|
||||
LogRecordContext.putVariable("businessName", oldBusiness.getName());
|
||||
}
|
||||
|
@@ -92,19 +92,20 @@ public class CrmClueServiceImpl implements CrmClueService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
|
||||
success = CRM_CLUE_UPDATE_SUCCESS)
|
||||
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER)
|
||||
public void updateClue(CrmClueSaveReqVO updateReq) {
|
||||
Assert.notNull(updateReq.getId(), "线索编号不能为空");
|
||||
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER)
|
||||
public void updateClue(CrmClueSaveReqVO updateReqVO) {
|
||||
Assert.notNull(updateReqVO.getId(), "线索编号不能为空");
|
||||
// 1.1 校验线索是否存在
|
||||
CrmClueDO oldClue = validateClueExists(updateReq.getId());
|
||||
CrmClueDO oldClue = validateClueExists(updateReqVO.getId());
|
||||
// 1.2 校验关联数据
|
||||
validateRelationDataExists(updateReq);
|
||||
validateRelationDataExists(updateReqVO);
|
||||
|
||||
// 2. 更新线索
|
||||
CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class);
|
||||
CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class);
|
||||
clueMapper.updateById(updateObj);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class));
|
||||
LogRecordContext.putVariable("clueName", oldClue.getName());
|
||||
}
|
||||
|
@@ -114,6 +114,7 @@ public class CrmContactServiceImpl implements CrmContactService {
|
||||
contactMapper.updateById(updateObj);
|
||||
|
||||
// 3. 记录操作日志
|
||||
updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class));
|
||||
LogRecordContext.putVariable("contactName", oldContact.getName());
|
||||
}
|
||||
|
@@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService {
|
||||
Assert.notNull(updateReqVO.getId(), "合同编号不能为空");
|
||||
updateReqVO.setOwnerUserId(null); // 不允许更新的字段
|
||||
// 1.1 校验存在
|
||||
CrmContractDO contract = validateContractExists(updateReqVO.getId());
|
||||
CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
|
||||
// 1.2 只有草稿、审批中,可以编辑;
|
||||
if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
|
||||
if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
|
||||
CrmAuditStatusEnum.PROCESS.getStatus())) {
|
||||
throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT);
|
||||
}
|
||||
@@ -159,8 +159,9 @@ public class CrmContractServiceImpl implements CrmContractService {
|
||||
updateContractProduct(updateReqVO.getId(), contractProducts);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class));
|
||||
LogRecordContext.putVariable("contractName", contract.getName());
|
||||
updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class));
|
||||
LogRecordContext.putVariable("contractName", oldContract.getName());
|
||||
}
|
||||
|
||||
private void updateContractProduct(Long id, List<CrmContractProductDO> newList) {
|
||||
|
@@ -137,6 +137,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
|
||||
customerMapper.updateById(updateObj);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class));
|
||||
LogRecordContext.putVariable("customerName", oldCustomer.getName());
|
||||
}
|
||||
|
@@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
|
||||
CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId(
|
||||
transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId());
|
||||
String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
|
||||
if (oldPermission == null // 不是拥有者,并且不是超管
|
||||
|| (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) {
|
||||
if ((oldPermission == null || !isOwner(oldPermission.getLevel()))
|
||||
&& !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管
|
||||
throw exception(CRM_PERMISSION_DENIED, bizTypeName);
|
||||
}
|
||||
// 1.1 校验转移对象是否已经是该负责人
|
||||
if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
|
||||
if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
|
||||
throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName);
|
||||
}
|
||||
// 1.2 校验新负责人是否存在
|
||||
|
@@ -104,6 +104,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
|
||||
receivablePlanMapper.updateById(updateObj);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class));
|
||||
LogRecordContext.putVariable("receivablePlan", oldReceivablePlan);
|
||||
}
|
||||
|
@@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
|
||||
Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
|
||||
updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
|
||||
// 1.1 校验存在
|
||||
CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId());
|
||||
updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId())
|
||||
.setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值
|
||||
CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
|
||||
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId())
|
||||
.setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值
|
||||
// 1.2 校验可回款金额超过上限
|
||||
validateReceivablePriceExceedsLimit(updateReqVO);
|
||||
|
||||
// 1.3 只有草稿、审批中,可以编辑;
|
||||
if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
|
||||
if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
|
||||
CrmAuditStatusEnum.PROCESS.getStatus())) {
|
||||
throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
|
||||
}
|
||||
@@ -179,9 +179,10 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
|
||||
receivableMapper.updateById(updateObj);
|
||||
|
||||
// 3. 记录操作日志上下文
|
||||
LogRecordContext.putVariable("receivable", receivable);
|
||||
LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId()));
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class));
|
||||
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
||||
LogRecordContext.putVariable("oldReceivable", oldReceivable);
|
||||
LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId()));
|
||||
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class));
|
||||
}
|
||||
|
||||
private Integer getReceivablePeriod(Long planId) {
|
||||
|
@@ -10,7 +10,9 @@
|
||||
<if test="endTime != null">
|
||||
AND in_time < #{endTime}
|
||||
</if>
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
|
||||
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
|
||||
</if>
|
||||
AND deleted = 0) -
|
||||
(SELECT IFNULL(SUM(total_price), 0)
|
||||
FROM erp_purchase_return
|
||||
@@ -18,7 +20,9 @@
|
||||
<if test="endTime != null">
|
||||
AND return_time < #{endTime}
|
||||
</if>
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
|
||||
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
|
||||
</if>
|
||||
AND deleted = 0)
|
||||
</select>
|
||||
|
||||
|
@@ -10,7 +10,9 @@
|
||||
<if test="endTime != null">
|
||||
AND out_time < #{endTime}
|
||||
</if>
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
|
||||
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
|
||||
</if>
|
||||
AND deleted = 0) -
|
||||
(SELECT IFNULL(SUM(total_price), 0)
|
||||
FROM erp_sale_return
|
||||
@@ -18,7 +20,9 @@
|
||||
<if test="endTime != null">
|
||||
AND return_time < #{endTime}
|
||||
</if>
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
|
||||
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
|
||||
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
|
||||
</if>
|
||||
AND deleted = 0)
|
||||
</select>
|
||||
|
||||
|
@@ -1,9 +1,12 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "管理后台 - 上传文件 Request VO")
|
||||
@@ -17,4 +20,10 @@ public class FileUploadReqVO {
|
||||
@Schema(description = "文件目录", example = "XXX/YYY")
|
||||
private String directory;
|
||||
|
||||
@AssertTrue(message = "文件目录不正确")
|
||||
@JsonIgnore
|
||||
public boolean isDirectoryValid() {
|
||||
return !StrUtil.containsAny(directory, "..", "/", "\\");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.app.file.vo;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "用户 App - 上传文件 Request VO")
|
||||
@@ -17,4 +20,10 @@ public class AppFileUploadReqVO {
|
||||
@Schema(description = "文件目录", example = "XXX/YYY")
|
||||
private String directory;
|
||||
|
||||
@AssertTrue(message = "文件目录不正确")
|
||||
@JsonIgnore
|
||||
public boolean isDirectoryValid() {
|
||||
return !StrUtil.containsAny(directory, "..", "/", "\\");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.tika.Tika;
|
||||
@@ -86,8 +85,8 @@ public class FileTypeUtils {
|
||||
response.setContentType(contentType);
|
||||
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
|
||||
if (StrUtil.containsIgnoreCase(contentType, "video")) {
|
||||
response.setHeader("Content-Length", String.valueOf(content.length - 1));
|
||||
response.setHeader("Content-Range", String.valueOf(content.length - 1));
|
||||
response.setHeader("Content-Length", String.valueOf(content.length));
|
||||
response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
|
||||
response.setHeader("Accept-Ranges", "bytes");
|
||||
}
|
||||
// 输出附件
|
||||
|
@@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.promotion.api.seckill;
|
||||
|
||||
import cn.iocoder.yudao.module.promotion.api.seckill.dto.SeckillValidateJoinRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.service.seckill.SeckillActivityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 秒杀活动接口 Api 接口实现类
|
||||
*
|
||||
@@ -14,10 +13,10 @@ import javax.annotation.Resource;
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
public class SeckillActivityApiImpl implements SeckillActivityApi {
|
||||
|
||||
@Resource
|
||||
private SeckillActivityService activityService;
|
||||
private final SeckillActivityService activityService;
|
||||
|
||||
@Override
|
||||
public void updateSeckillStockDecr(Long id, Long skuId, Integer count) {
|
||||
|
@@ -31,9 +31,13 @@ import java.util.List;
|
||||
public class CouponTemplateDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 不限制领取数量
|
||||
* 领取数量 - 不限制
|
||||
*/
|
||||
public static final Integer TIME_LIMIT_COUNT_MAX = -1;
|
||||
public static final Integer TAKE_LIMIT_COUNT_MAX = -1;
|
||||
/**
|
||||
* 发放数量 - 不限制
|
||||
*/
|
||||
public static final Integer TOTAL_COUNT_MAX = -1;
|
||||
|
||||
// ========== 基本信息 BEGIN ==========
|
||||
/**
|
||||
|
@@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
|
||||
.orderByDesc(CouponTemplateDO::getId));
|
||||
}
|
||||
|
||||
default void updateTakeCount(Long id, Integer incrCount) {
|
||||
update(null, new LambdaUpdateWrapper<CouponTemplateDO>()
|
||||
default int updateTakeCount(Long id, Integer incrCount) {
|
||||
LambdaUpdateWrapper<CouponTemplateDO> updateWrapper = new LambdaUpdateWrapper<CouponTemplateDO>()
|
||||
.eq(CouponTemplateDO::getId, id)
|
||||
.setSql("take_count = take_count + " + incrCount));
|
||||
.setSql("take_count = take_count + " + incrCount);
|
||||
// 增加已领取的数量(incrCount 为正数),需要考虑发放数量 totalCount 的限制
|
||||
if (incrCount > 0) {
|
||||
updateWrapper.and(i -> i.apply("take_count < total_count")
|
||||
.or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX));
|
||||
}
|
||||
return update(updateWrapper);
|
||||
}
|
||||
|
||||
default List<CouponTemplateDO> selectListByTakeType(Integer takeType) {
|
||||
|
@@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
|
||||
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId));
|
||||
}
|
||||
// 查找是否有其它活动,选择了该产品
|
||||
List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId));
|
||||
List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId));
|
||||
if (CollUtil.isNotEmpty(matchActivityList)) {
|
||||
throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS);
|
||||
}
|
||||
|
@@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService {
|
||||
|
||||
// 4. 增加优惠劵模板的领取数量
|
||||
couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size());
|
||||
|
||||
return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
|
||||
}
|
||||
|
||||
@@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService {
|
||||
}
|
||||
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
|
||||
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
|
||||
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
|
||||
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
|
||||
&& couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
|
||||
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
|
||||
}
|
||||
|
@@ -22,8 +22,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL;
|
||||
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 优惠劵模板 Service 实现类
|
||||
@@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
|
||||
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
|
||||
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
|
||||
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
|
||||
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
|
||||
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
|
||||
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
|
||||
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
|
||||
}
|
||||
@@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
|
||||
|
||||
@Override
|
||||
public void updateCouponTemplateTakeCount(Long id, int incrCount) {
|
||||
couponTemplateMapper.updateTakeCount(id, incrCount);
|
||||
int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount);
|
||||
if (updateCount == 0) {
|
||||
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -76,8 +76,8 @@
|
||||
<select id="selectListByPayTimeBetweenAndGroupByMonth"
|
||||
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO">
|
||||
SELECT DATE_FORMAT(pay_time, '%Y-%m') AS date,
|
||||
COUNT(1) AS orderPayCount,
|
||||
SUM(pay_price) AS orderPayPrice
|
||||
COUNT(1) AS order_pay_count,
|
||||
SUM(pay_price) AS order_pay_price
|
||||
FROM trade_order
|
||||
WHERE pay_status = TRUE
|
||||
AND create_time BETWEEN #{beginTime} AND #{endTime}
|
||||
@@ -95,8 +95,8 @@
|
||||
|
||||
<select id="selectPaySummaryByPayStatusAndPayTimeBetween"
|
||||
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO">
|
||||
SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice,
|
||||
COUNT(1) AS orderPayCount
|
||||
SELECT IFNULL(SUM(pay_price), 0) AS order_pay_price,
|
||||
COUNT(1) AS order_pay_count
|
||||
FROM trade_order
|
||||
WHERE pay_status = #{payStatus}
|
||||
AND pay_time BETWEEN #{beginTime} AND #{endTime}
|
||||
|
@@ -39,7 +39,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在");
|
||||
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】");
|
||||
ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限");
|
||||
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足");
|
||||
ErrorCode ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS = new ErrorCode(1_011_000_037, "交易订单自提失败,原因:商品拼团记录不是【成功】状态");
|
||||
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_038, "交易订单创建失败,原因:用户积分不足");
|
||||
|
||||
// ========== After Sale 模块 1-011-000-100 ==========
|
||||
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
|
||||
|
@@ -141,9 +141,8 @@ public class AfterSaleController {
|
||||
public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
|
||||
log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
|
||||
if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) {
|
||||
tradeOrderUpdateService.updatePaidOrderRefunded(
|
||||
Long.parseLong(notifyReqDTO.getMerchantRefundId()),
|
||||
notifyReqDTO.getPayRefundId());
|
||||
Long orderId = Long.parseLong(StrUtil.subAfter(notifyReqDTO.getMerchantRefundId(), "order-", true));
|
||||
tradeOrderUpdateService.updatePaidOrderRefunded(orderId, notifyReqDTO.getPayRefundId());
|
||||
} else {
|
||||
afterSaleService.updateAfterSaleRefunded(
|
||||
Long.parseLong(notifyReqDTO.getMerchantRefundId()),
|
||||
|
@@ -25,6 +25,9 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
|
||||
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
|
||||
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
|
||||
import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
|
||||
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
|
||||
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
|
||||
@@ -121,6 +124,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
|
||||
public SocialClientApi socialClientApi;
|
||||
@Resource
|
||||
public PayRefundApi payRefundApi;
|
||||
@Resource
|
||||
private CombinationRecordApi combinationRecordApi;
|
||||
|
||||
@Resource
|
||||
private TradeOrderProperties tradeOrderProperties;
|
||||
@@ -775,6 +780,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
|
||||
if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) {
|
||||
throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP);
|
||||
}
|
||||
// 情况一:如果是拼团订单,则校验拼团是否成功
|
||||
if (TradeOrderTypeEnum.isCombination(order.getType())) {
|
||||
CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(
|
||||
order.getUserId(), order.getId());
|
||||
if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) {
|
||||
throw exception(ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS);
|
||||
}
|
||||
}
|
||||
DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId());
|
||||
if (deliveryPickUpStore == null
|
||||
|| !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) {
|
||||
|
@@ -1,10 +1,12 @@
|
||||
package cn.iocoder.yudao.module.mp.service.handler.user;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
|
||||
import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService;
|
||||
import cn.iocoder.yudao.module.mp.service.user.MpUserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.error.WxMpErrorMsgEnum;
|
||||
import me.chanjar.weixin.common.session.WxSessionManager;
|
||||
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
@@ -40,6 +42,13 @@ public class SubscribeHandler implements WxMpMessageHandler {
|
||||
wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser());
|
||||
} catch (WxErrorException e) {
|
||||
log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e);
|
||||
// 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5
|
||||
if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) {
|
||||
wxMpUser = new WxMpUser();
|
||||
wxMpUser.setOpenId(wxMessage.getFromUser());
|
||||
wxMpUser.setSubscribe(true);
|
||||
wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步,保存粉丝信息
|
||||
|
@@ -127,6 +127,8 @@ public class AuthController {
|
||||
@PostMapping("/sms-login")
|
||||
@PermitAll
|
||||
@Operation(summary = "使用短信验证码登录")
|
||||
// 可按需开启限流:https://github.com/YunaiV/ruoyi-vue-pro/issues/851
|
||||
// @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile")
|
||||
public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
|
||||
return success(authService.smsLogin(reqVO));
|
||||
}
|
||||
|
@@ -38,7 +38,8 @@ public class SocialUserController {
|
||||
@PostMapping("/bind")
|
||||
@Operation(summary = "社交绑定,使用 code 授权码")
|
||||
public CommonResult<Boolean> socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) {
|
||||
socialUserService.bindSocialUser(BeanUtils.toBean(reqVO, SocialUserBindReqDTO.class)
|
||||
socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType())
|
||||
.setCode(reqVO.getCode()).setState(reqVO.getState())
|
||||
.setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ server:
|
||||
spring:
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建
|
||||
- org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建
|
||||
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建
|
||||
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建
|
||||
# 数据源配置项
|
||||
autoconfigure:
|
||||
exclude:
|
||||
|
@@ -11,8 +11,8 @@ spring:
|
||||
- de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置
|
||||
- de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置
|
||||
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
|
||||
- org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建
|
||||
- org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建
|
||||
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建
|
||||
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建
|
||||
# 数据源配置项
|
||||
datasource:
|
||||
druid: # Druid 【监控】相关的全局配置
|
||||
|
@@ -150,7 +150,7 @@ spring:
|
||||
vectorstore: # 向量存储
|
||||
redis:
|
||||
initialize-schema: true
|
||||
index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行
|
||||
index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行
|
||||
prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构
|
||||
qdrant:
|
||||
initialize-schema: true
|
||||
@@ -188,13 +188,14 @@ spring:
|
||||
api-key: xxxx
|
||||
moonshot: # 月之暗灭(KIMI)
|
||||
api-key: sk-abc
|
||||
deepseek: # DeepSeek
|
||||
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
|
||||
chat:
|
||||
options:
|
||||
model: deepseek-chat
|
||||
|
||||
yudao:
|
||||
ai:
|
||||
deep-seek: # DeepSeek
|
||||
enable: true
|
||||
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
|
||||
model: deepseek-chat
|
||||
doubao: # 字节豆包
|
||||
enable: true
|
||||
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
|
||||
|
Reference in New Issue
Block a user