feat: 增加流程审批样例代码

This commit is contained in:
qiufeng
2026-04-20 22:39:42 +08:00
parent 3da638ab79
commit 571df1c434
41 changed files with 2328 additions and 7 deletions
+59
View File
@@ -0,0 +1,59 @@
---
name: comment-checker
description: 检查并补充Java代码中的缺失注释。扫描项目中的Java文件,识别缺失JavaDoc注释的类、方法和字段,并根据代码逻辑自动生成规范的中文注释。
tools: Read, Grep, Glob, Edit
---
# 角色定义
你是一个专业的Java代码注释补充专家,专注于为缺失注释的代码添加规范的JavaDoc注释。
## 工作流程
1. 扫描指定目录下的所有Java文件
2. 识别缺失注释的位置(类、方法、字段)
3. 分析代码逻辑,生成合适的注释内容
4. 使用search_replace工具补充注释
5. 验证修改后的代码
## 注释规范
### 类注释
```java
/**
* @description: 类的功能描述
* @author: mfish
* @date: YYYY/MM/dd
*/
```
### 方法注释
```java
/**
* 方法功能描述
*
* @param param1 参数1说明
* @param param2 参数2说明
* @return 返回值说明
* @throws ExceptionType 异常说明
*/
```
### 字段注释
```java
/** 字段用途说明 */
```
## 处理优先级
1. 优先处理public类和方法
2. 优先处理Service接口和Controller类
3. 优先处理API模块中的Feign接口
4. 复杂业务逻辑方法必须添加注释
## 注意事项
- 使用中文编写注释
- 保持与现有代码风格一致
- 不要修改已有注释的内容
- 确保注释准确描述代码功能
+622
View File
@@ -0,0 +1,622 @@
---
name: crud-generator
description: 为 mfish-nocode-pro 项目生成标准增删改查(CRUD)代码,包括 Entity、Req、Mapper、Service、ServiceImpl、Controller 六层结构。当用户说"帮我生成增删改查"、"新增一个模块"、"生成CRUD代码"时使用此 skill。
---
# CRUD 代码生成器
## 项目架构概览
```
mf-api/mf-xxx-api/
└── src/main/java/cn/com/mfish/xxx/api/entity/ # API 层实体(跨服务共享)
mf-business/mf-xxx/
└── src/main/java/cn/com/mfish/xxx/
├── controller/ # Controller 层
├── entity/ # 业务实体
├── mapper/ # Mapper 接口
├── req/ # 请求参数类
└── service/
├── XxxService.java
└── impl/XxxServiceImpl.java
```
## 技术栈约定
- ORM: **MyBatis-Plus**`BaseMapper<T>``ServiceImpl<M,T>``IService<T>`
- 分页: **PageHelper**`PageHelper.startPage(pageNum, pageSize)`
- 权限: `@RequiresPermissions("模块:功能:操作")`insert/update/delete/query/export
- 日志: `@Log(title = "xxx-操作", operateType = OperateType.INSERT/UPDATE/DELETE)`
- 返回值: `Result<T>``Result<PageResult<T>>``Result<Boolean>`
- 文档: SpringDoc `@Tag``@Operation``@Parameter`
- ID类型: String UUID → `@TableId(type = IdType.ASSIGN_UUID)`;数值自增 → `@TableId(type = IdType.AUTO)`
- 基类: `BaseEntity<T>`(含 id、createBy、createTime、updateBy、updateTime),T 与 ID 类型一致
- 导出: `ExcelUtils.write(fileName, list)`(来自 `cn.com.mfish.common.core.utils.excel.ExcelUtils`
- 字符串判空:`StringUtils.isEmpty()`(来自 `cn.com.mfish.common.core.utils.StringUtils`
- **Swagger 注解**: 所有 Entity、Req 类必须添加 `@Schema` 注解(类级和字段级)
---
## 生成步骤
### 第一步:收集信息
询问用户(如未提供):
1. **模块名**(如:order、product
2. **中文名称**(如:订单、商品)
3. **数据表名**(如:sys_order
4. **字段列表**:字段名、类型、中文描述、是否必填
5. **权限前缀**(如:`sys:order`
6. **包路径**(如:`cn.com.mfish.sys`
7. **数据权限**(可选):是否需要按租户/用户/角色/组织过滤数据,表中需有对应字段
---
### 第二步:生成各层代码
按以下顺序生成,文件路径基于 `mf-business/mf-xxx/src/main/java/` 目录。
#### 1. Entity 实体类
```java
package {包路径}.entity;
import cn.com.mfish.common.core.entity.BaseEntity;
import cn.idev.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
// 有 Date 类型字段时引入:
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
// 有 BigDecimal 类型字段时引入:
import java.math.BigDecimal;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
@Data
@TableName("{表名}")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "{表名}对象 {中文名称}")
public class {类名} extends BaseEntity<String> {
// String UUID 主键:
@ExcelProperty("唯一ID")
@Schema(description = "唯一ID")
@TableId(type = IdType.ASSIGN_UUID)
@Accessors(chain = true)
private String id;
// 数值自增主键(替换上面的 id 声明):
// @TableId(type = IdType.AUTO)
// @Accessors(chain = true)
// private Integer id;
// 普通字段:
@ExcelProperty("{字段注释}")
@Schema(description = "{字段注释}")
private {类型} {字段名};
// 日期字段(DATE 类型):
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@ExcelProperty("{字段注释}")
@Schema(description = "{字段注释}")
private Date {字段名};
// 日期时间字段(DATETIME 类型):
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty("{字段注释}")
@Schema(description = "{字段注释}")
private Date {字段名};
}
```
#### 2. Req 请求参数类
```java
package {包路径}.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
// 有 Date 类型搜索字段时引入:
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
@Data
@Accessors(chain = true)
@Schema(description = "{中文名称}请求参数")
public class Req{类名} {
// 普通搜索字段:
@Schema(description = "{字段注释}")
private {类型} {字段名};
// DATE 类型搜索字段:
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Schema(description = "{字段注释}")
private Date {字段名};
// DATETIME 类型搜索字段:
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "{字段注释}")
private Date {字段名};
}
```
#### 3. Mapper 接口 + XML
```java
package {包路径}.mapper;
import {包路径}.entity.{类名};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
public interface {类名}Mapper extends BaseMapper<{类名}> {
// 如有复杂查询,声明自定义方法,并对应 XML
}
```
对应 XML 文件(`resources/mapper/{类名}Mapper.xml`):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="{包路径}.mapper.{类名}Mapper">
</mapper>
```
#### 4. Service 接口
```java
package {包路径}.service;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import {包路径}.entity.{类名};
import {包路径}.req.Req{类名};
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.IOException;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
public interface {类名}Service extends IService<{类名}> {
/** 分页列表查询 */
Result<PageResult<{类名}>> queryPageList(Req{类名} req{类名}, ReqPage reqPage);
/** 添加 */
Result<{类名}> add({类名} {变量名});
/** 编辑 */
Result<{类名}> edit({类名} {变量名});
/** 通过id删除 */
Result<Boolean> delete(String id);
/** 批量删除(ids 逗号分隔) */
Result<Boolean> deleteBatch(String ids);
/** 通过id查询 */
Result<{类名}> queryById(String id);
/** 导出 */
void export(Req{类名} req{类名}, ReqPage reqPage) throws IOException;
}
```
> 若 ID 为数值型(如 `Integer`),将 `String id` 改为对应类型。
#### 5. ServiceImpl 实现类
```java
package {包路径}.service.impl;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.utils.excel.ExcelUtils;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import {包路径}.entity.{类名};
import {包路径}.mapper.{类名}Mapper;
import {包路径}.req.Req{类名};
import {包路径}.service.{类名}Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
@Service
public class {类名}ServiceImpl extends ServiceImpl<{类名}Mapper, {类名}> implements {类名}Service {
@Override
public Result<PageResult<{类名}>> queryPageList(Req{类名} req{类名}, ReqPage reqPage) {
return Result.ok(new PageResult<>(queryList(req{类名}, reqPage)), "{中文名称}-查询成功!");
}
private List<{类名}> queryList(Req{类名} req{类名}, ReqPage reqPage) {
PageHelper.startPage(reqPage.getPageNum(), reqPage.getPageSize());
LambdaQueryWrapper<{类名}> lambdaQueryWrapper = new LambdaQueryWrapper<{类名}>()
// String 类型字段用 StringUtils.isEmpty 判断:
.like(!StringUtils.isEmpty(req{类名}.get{字段}()), {类名}::get{字段}, req{类名}.get{字段}())
// 非 String 类型字段用 null != xxx 判断:
.eq(null != req{类名}.get{字段}(), {类名}::get{字段}, req{类名}.get{字段}());
return list(lambdaQueryWrapper);
}
@Override
public Result<{类名}> add({类名} {变量名}) {
if (save({变量名})) {
return Result.ok({变量名}, "{中文名称}-添加成功!");
}
return Result.fail({变量名}, "错误:{中文名称}-添加失败!");
}
@Override
public Result<{类名}> edit({类名} {变量名}) {
if (updateById({变量名})) {
return Result.ok({变量名}, "{中文名称}-编辑成功!");
}
return Result.fail({变量名}, "错误:{中文名称}-编辑失败!");
}
@Override
public Result<Boolean> delete(String id) {
if (removeById(id)) {
return Result.ok(true, "{中文名称}-删除成功!");
}
return Result.fail(false, "错误:{中文名称}-删除失败!");
}
@Override
public Result<Boolean> deleteBatch(String ids) {
if (removeByIds(Arrays.asList(ids.split(",")))) {
return Result.ok(true, "{中文名称}-批量删除成功!");
}
return Result.fail(false, "错误:{中文名称}-批量删除失败!");
}
@Override
public Result<{类名}> queryById(String id) {
{类名} {变量名} = getById(id);
return Result.ok({变量名}, "{中文名称}-查询成功!");
}
@Override
public void export(Req{类名} req{类名}, ReqPage reqPage) throws IOException {
// swagger 调用有问题,使用 postman 测试
ExcelUtils.write("{中文名称}_" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()), queryList(req{类名}, reqPage));
}
}
```
**搜索条件 LambdaQueryWrapper 规则:**
- `String` 类型字段 → `.like(!StringUtils.isEmpty(req.getXxx()), Entity::getXxx, req.getXxx())`(模糊) 或 `.eq(!StringUtils.isEmpty(req.getXxx()), Entity::getXxx, req.getXxx())`(精确)
-`String` 类型字段 → `.eq(null != req.getXxx(), Entity::getXxx, req.getXxx())`
#### 6. Controller 控制器
```java
package {包路径}.controller;
import cn.com.mfish.common.core.enums.OperateType;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.log.annotation.Log;
import cn.com.mfish.common.oauth.annotation.RequiresPermissions;
import {包路径}.entity.{类名};
import {包路径}.req.Req{类名};
import {包路径}.service.{类名}Service;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
@Slf4j
@Tag(name = "{中文名称}")
@RestController
@RequestMapping("/{变量名}")
public class {类名}Controller {
@Resource
private {类名}Service {变量名}Service;
/**
* 分页列表查询
*/
@Operation(summary = "{中文名称}-分页列表查询", description = "{中文名称}-分页列表查询")
@GetMapping
@RequiresPermissions("{权限前缀}:query")
// 需要数据权限时加:@DataScope(table = "{表名}", type = DataScopeType.Tenant)
public Result<PageResult<{类名}>> queryPageList(Req{类名} req{类名}, ReqPage reqPage) {
return {变量名}Service.queryPageList(req{类名}, reqPage);
}
/**
* 添加
*/
@Log(title = "{中文名称}-添加", operateType = OperateType.INSERT)
@Operation(summary = "{中文名称}-添加")
@PostMapping
@RequiresPermissions("{权限前缀}:insert")
public Result<{类名}> add(@RequestBody {类名} {变量名}) {
return {变量名}Service.add({变量名});
}
/**
* 编辑
*/
@Log(title = "{中文名称}-编辑", operateType = OperateType.UPDATE)
@Operation(summary = "{中文名称}-编辑")
@PutMapping
@RequiresPermissions("{权限前缀}:update")
public Result<{类名}> edit(@RequestBody {类名} {变量名}) {
return {变量名}Service.edit({变量名});
}
/**
* 通过id删除
*/
@Log(title = "{中文名称}-通过id删除", operateType = OperateType.DELETE)
@Operation(summary = "{中文名称}-通过id删除")
@DeleteMapping("/{id}")
@RequiresPermissions("{权限前缀}:delete")
public Result<Boolean> delete(@Parameter(name = "id", description = "唯一性ID") @PathVariable String id) {
return {变量名}Service.delete(id);
}
/**
* 批量删除
*/
@Log(title = "{中文名称}-批量删除", operateType = OperateType.DELETE)
@Operation(summary = "{中文名称}-批量删除")
@DeleteMapping("/batch/{ids}")
@RequiresPermissions("{权限前缀}:delete")
public Result<Boolean> deleteBatch(@Parameter(name = "ids", description = "唯一性ID") @PathVariable String ids) {
return {变量名}Service.deleteBatch(ids);
}
/**
* 通过id查询
*/
@Operation(summary = "{中文名称}-通过id查询")
@GetMapping("/{id}")
@RequiresPermissions("{权限前缀}:query")
// 需要数据权限时加:@DataScope(table = "{表名}", type = DataScopeType.Tenant)
public Result<{类名}> queryById(@Parameter(name = "id", description = "唯一性ID") @PathVariable String id) {
return {变量名}Service.queryById(id);
}
/**
* 导出
*/
@Operation(summary = "导出{中文名称}", description = "导出{中文名称}")
@GetMapping("/export")
@RequiresPermissions("{权限前缀}:export")
public void export(Req{类名} req{类名}, ReqPage reqPage) throws IOException {
{变量名}Service.export(req{类名}, reqPage);
}
}
```
> **Controller 核心原则**Controller 只做路由转发,**所有业务逻辑在 Service 中实现**Controller 方法体直接 `return xxxService.方法()`。
---
## 命名约定
| 占位符 | 说明 | 示例 |
|--------|------|------|
| `{类名}` | PascalCase 类名 | `SysOrder` |
| `{变量名}` | camelCase 变量名(也用作 @RequestMapping 路径) | `sysOrder` |
| `{表名}` | 数据库表名(下划线) | `sys_order` |
| `{权限前缀}` | 权限标识符(`{apiPrefix}:{变量名}` | `sys:sysOrder` |
| `{包路径}` | Java 包名 | `cn.com.mfish.sys` |
---
## 注意事项
1. **业务逻辑分层**Controller 只做路由转发,`return xxxService.方法()` 即可,业务实现全在 ServiceImpl
2. **有业务校验逻辑时**:在 ServiceImpl 中提取 `verifyXxx()` 私有方法,并在 add/edit 中调用
3. **有关联删除时**:在 ServiceImpl 中用 `@Transactional` 处理级联操作
4. **复杂查询**:在 Mapper 中声明方法,在 `resources/mapper/` 下创建对应 XML 文件
5. **模块位置**:根据功能归属放到对应的 `mf-business/mf-xxx` 子模块
6. **API 层实体**:如需跨服务调用,将实体放到 `mf-api/mf-xxx-api` 模块
7. **软删除场景**:实体增加 `delFlag` 字段,删除时 `updateById(new Xxx().setId(id).setDelFlag(1))`,查询时 `.eq(Xxx::getDelFlag, 0)`
8. **租户隔离场景**:实体增加 `tenantId` 字段,Controller 的写操作加 `@DataScope(table="表名", type=DataScopeType.Tenant)` 注解,Service 中用 `AuthInfoUtils.getCurrentTenantId()` 写入,修改时 `setTenantId(null)` 避免覆盖
9. **数值型主键**`BaseEntity<Integer>``@TableId(type = IdType.AUTO)`Service/Controller 中 `id` 参数类型相应改为 `Integer`
10. **Swagger 注解强制要求**:所有 Entity、Req 类必须添加 `@Schema` 注解,包括:
- **类级别**`@Schema(description = "描述信息", name = "类名")`
- **字段级别**:每个字段都需添加 `@Schema(description = "字段描述")`
- 便于生成完整的 API 文档和 Swagger UI 展示
11. **异常处理规范**:人为抛出的业务异常统一采用 `MyRuntimeException` 处理
- **引入包**`import cn.com.mfish.common.core.exception.MyRuntimeException;`
- **使用场景**:业务校验失败、数据不存在、权限不足等业务异常情况
- **示例代码**
```java
// 数据不存在校验
if (entity == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
// 重复性校验
if (baseMapper.exists(new LambdaQueryWrapper<Entity>()
.eq(Entity::getField, value))) {
throw new MyRuntimeException("错误:已存在,不能重复提交!");
}
// 状态校验
if (!"active".equals(entity.getStatus())) {
throw new MyRuntimeException("错误:记录状态不正确!");
}
```
- **消息格式**:建议以 `"错误:"` 开头,便于前端统一处理和识别
- **不要使用**:避免直接使用 `RuntimeException` 或其他自定义异常
12. **安全规范 - 异常信息不暴露给前端**:
- **核心原则**:返回给前端的错误消息必须是友好的、通用的提示,不能包含具体的异常堆栈或技术细节
- **错误示例**`return Result.fail(false, "错误:配置不正确," + e.getMessage());` ❌
- **正确示例**`return Result.fail(false, "错误:配置不正确,请检查配置是否完整且符合规范");` ✅
- **日志记录**:详细的异常信息应通过 `log.error()` 记录到日志文件中,便于开发人员排查
- **实现模式**
```java
try {
// 业务逻辑
someOperation();
} catch (Exception e) {
// 详细异常信息记录到日志
log.error("操作失败:{}", e.getMessage(), e);
// 返回给前端的是友好的提示信息
return Result.fail(false, "错误:操作失败,请检查配置是否正确");
}
```
- **适用范围**:所有 catch 块中返回给前端的错误消息都必须遵循此规范
---
## 数据权限控制
数据权限通过 `@DataScope` / `@DataScopes` 注解在 **Controller 查询方法** 上声明,框架自动在 SQL 中追加过滤条件。
> **重要约束**
> - 注解只能用于**查询方法**,不能用于新增/修改/删除
> - 注解加在 **Controller 层**,不在 Service 层
> - 表中须有对应的权限字段:租户 `tenant_id`、用户 `user_id`、角色 `role_id`、组织 `org_id`
### DataScopeType 权限类型
| 类型 | 说明 | 表中需要字段 |
|------|------|-----------|
| `DataScopeType.Tenant` | 按当前租户过滤 | `tenant_id` |
| `DataScopeType.User` | 按当前用户过滤 | `user_id` |
| `DataScopeType.Role` | 按当前角色过滤 | `role_id` |
| `DataScopeType.Org` | 按当前组织及下级过滤 | `org_id` |
| `DataScopeType.None` | 不过滤(默认) | — |
### 常用场景示例
**1. 单表租户过滤(最常用)**
```java
@GetMapping
@RequiresPermissions("{权限前缀}:query")
@DataScope(table = "{表名}", type = DataScopeType.Tenant)
public Result<PageResult<{类名}>> queryPageList(Req{类名} req, ReqPage reqPage) { ... }
```
**2. 单表组织过滤**
```java
@DataScope(table = "{表名}", type = DataScopeType.Org)
```
**3. 固定角色值过滤(指定具体角色编码)**
```java
@DataScope(table = "{表名}", type = DataScopeType.Role, values = {"manage", "superAdmin"})
```
**4. 排除公开数据(满足排除条件的记录不被过滤,始终可查)**
```java
@DataScope(table = "{表名}", type = DataScopeType.Tenant, excludes = "is_public=1")
```
**5. 忽略条件(优先级最高,满足时其他权限条件全部失效)**
```java
// 变量值从 ServletRequest.getParameter 中取,为空时不使用忽略条件
@DataScope(table = "{表名}", ignores = "share_token=#{_shareToken} and share_end_time>=now()")
```
**6. 多表组合权限(使用 @DataScopes**
```java
import cn.com.mfish.common.oauth.annotation.DataScopes;
@DataScopes({
@DataScope(table = "{主表名}", type = DataScopeType.Tenant),
@DataScope(table = "{关联表名}", type = DataScopeType.Tenant, excludes = "is_public=1")
})
```
**7. 租户 + 角色组合过滤**
```java
@DataScopes({
@DataScope(table = "{表名}", type = DataScopeType.Tenant),
@DataScope(table = "{表名}", type = DataScopeType.Role, values = {"manage", "superAdmin"})
})
```
### 所需 Import
```java
import cn.com.mfish.common.oauth.annotation.DataScope;
import cn.com.mfish.common.oauth.annotation.DataScopes; // 多条件时引入
import cn.com.mfish.common.oauth.common.DataScopeType;
```
### 参考实现
- 完整样例:[DemoDataScopeController.java](mf-business/mf-demo/src/main/java/cn/com/mfish/demo/controller/DemoDataScopeController.java)
- 排除条件示例:[DbConnectController.java](mf-business/mf-sys/src/main/java/cn/com/mfish/sys/controller/DbConnectController.java)
- 忽略条件示例:[MfApiController.java](mf-business/mf-nocode/src/main/java/cn/com/mfish/nocode/controller/MfApiController.java)
---
## 相关参考
- 参考现有实现:[DictController.java](mf-business/mf-sys/src/main/java/cn/com/mfish/sys/controller/DictController.java)
- 基础实体类:[BaseEntity.java](mf-common/mf-common-core/src/main/java/cn/com/mfish/common/core/entity/BaseEntity.java)
- 返回值规范:[Result.java](mf-common/mf-common-core/src/main/java/cn/com/mfish/common/core/web/Result.java)
- 代码生成模板:[mf-common-code/template/src/main/java/](mf-common/mf-common-code/src/main/resources/template/src/main/java/)
- 工作流审批集成请使用 **workflow-audit** skill
- 前端页面生成请使用 **frontend-crud** skill
+596
View File
@@ -0,0 +1,596 @@
---
name: frontend-crud
description: 为 mfish-nocode-pro 项目前端(Vue3 + TypeScript)生成标准增删改查页面代码,包括 Model、API、data、index.vue、Modal、ViewModal 六个文件。当用户说"帮我生成前端增删改查"、"新增前端页面"、"生成前端CRUD"时使用此 skill。
---
# 前端 CRUD 代码生成器
## 项目前端架构
```
mfish-nocode-view/src/
├── api/
│ └── {apiPrefix}/ # 如:sys、demo、nocode
│ ├── model/
│ │ └── {类名}Model.ts # 接口类型定义
│ └── {类名}.ts # API 请求函数
└── views/
└── {apiPrefix}/
└── {entity-kebab-case}/ # 如:demo-order、sys-dict
├── {变量名}.data.ts # 表格列、搜索表单、表单Schema、详情Schema
├── index.vue # 列表页主页面
├── {类名}Modal.vue # 新增/编辑弹窗
└── {类名}ViewModal.vue # 详情查看弹窗
```
## 技术栈约定
- 框架:**Vue 3** + **TypeScript** + `<script lang="ts" setup>`
- HTTP`defHttp`(来自 `@mfish/core/utils/http/axios`
- 表格:`BasicTable` + `useTable`(来自 `@mfish/core/components/Table`
- 弹窗:`BasicModal` + `useModal` / `useModalInner`(来自 `@mfish/core/components/Modal`
- 表单:`BasicForm` + `useForm`(来自 `@mfish/core/components/Form`
- 详情:`Description` + `useDescription`(来自 `@mfish/core/components/Description`
- 字典:`buildDictTag` + `getDictProps`(来自 `@mfish/core/components/DictTag`
- 权限:`v-auth="'{apiPrefix}:{entityName}:操作'"`insert/update/delete/query/export
- 基础类型:`BaseEntity<string>``PageResult<T>``ReqPage`(来自 `@mfish/core/api`
- ID 类型:默认 `string`,数值型主键时为 `number`
---
## 生成步骤
### 第一步:收集信息
询问用户(如未提供):
1. **模块名(apiPrefix**(如:`demo``sys``nocode`
2. **类名(PascalCase**(如:`DemoOrder`
3. **中文名称**(如:销售订单)
4. **字段列表**:字段名(camelCase)、TS 类型(`string`/`number`/`boolean`)、中文描述、是否可选
5. **搜索字段**:哪些字段出现在搜索表单中(及组件类型:`Input` / `ApiSelect`+字典编码 / `DatePicker`
6. **表单字段**:哪些字段出现在新增/编辑表单中(及组件类型,是否必填)
7. **字典字段**:哪些字段使用字典渲染(需提供字典编码)
---
### 第二步:生成六个文件
按以下顺序生成,所有文件路径基于 `mfish-nocode-view/src/` 目录。
#### 1. Model 类型定义(`api/{apiPrefix}/model/{类名}Model.ts`
```typescript
import { BaseEntity, PageResult, ReqPage } from "@mfish/core/api";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
export interface {} extends BaseEntity<string> {
//{字段注释}
{}?: {TS类型};
// ... 更多字段
}
export interface Req{} extends ReqPage {
//{搜索字段注释}
{}?: {TS类型};
// ... 更多搜索字段
}
//分页结果集
export type {}PageModel = PageResult<{}>;
```
**字段类型映射规则:**
| Java/DB 类型 | TS 类型 |
|---|---|
| `String``Date` | `string` |
| `Integer``Long``Short``Double``BigDecimal` | `number` |
| `Boolean` | `boolean` |
---
#### 2. API 请求文件(`api/{apiPrefix}/{类名}.ts`
```typescript
import { defHttp } from "@mfish/core/utils/http/axios";
import { {}, Req{}, {}PageModel } from "@/api/{apiPrefix}/model/{类名}Model";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
enum Api {
{} = "/{apiPrefix}/{变量名}"
}
/**
* 分页列表查询
*/
export const get{}List = (req{}?: Req{}) => {
return defHttp.get<{}PageModel>({ url: Api.{}, params: req{} });
};
/**
* 通过id查询
*/
export function get{}ById(id: string) {
return defHttp.get<{}>({ url: `${Api.{}}/${id}` });
}
/**
* 导出{中文名称}
*/
export function export{}(req{}?: Req{}) {
return defHttp.download({ url: `${Api.{}}/export`, params: req{} });
}
/**
* 新增{中文名称}
*/
export function insert{}({}: {}) {
return defHttp.post<{}>({ url: Api.{}, params: {} }, { successMessageMode: "message" });
}
/**
* 修改{中文名称}
*/
export function update{}({}: {}) {
return defHttp.put<{}>({ url: Api.{}, params: {} }, { successMessageMode: "message" });
}
/**
* 删除{中文名称}
*/
export function delete{}(id: string) {
return defHttp.delete<boolean>({ url: `${Api.{}}/${id}` }, { successMessageMode: "message" });
}
/**
* 批量删除{中文名称}
*/
export function deleteBatch{}(ids: string) {
return defHttp.delete<boolean>({ url: `${Api.{}}/batch/${ids}` }, { successMessageMode: "message" });
}
```
> 若 ID 类型为数值型(`number`),`delete{类名}` 参数类型改为 `number`。
---
#### 3. data 配置文件(`views/{apiPrefix}/{entity-kebab-case}/{变量名}.data.ts`
```typescript
import { BasicColumn, FormSchema } from "@mfish/core/components/Table";
import { DescItem } from "@mfish/core/components/Description";
// 有字典字段时引入(无字典字段则删除)
import { buildDictTag, getDictProps } from "@mfish/core/components/DictTag";
/**
* @description: {中文名称}
* @author: mfish
* @date: {当前日期}
* @version: V2.3.1
*/
// ========== 表格列定义 ==========
export const columns: BasicColumn[] = [
// 普通字段
{
title: "{字段中文名}",
dataIndex: "{字段名}",
width: 120
},
// 字典字段(有字典时使用 customRender
{
customRender: ({ record }) => {
return buildDictTag("{字典编码}", record.{});
},
title: "{字段中文名}",
dataIndex: "{字段名}",
width: 120
}
];
// ========== 搜索表单 Schema ==========
export const searchFormSchema: FormSchema[] = [
// 普通输入框
{
field: "{字段名}",
label: "{字段中文名}",
component: "Input",
colProps: { xl: 5, md: 6 }
},
// 字典下拉(单选)
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: getDictProps("{字典编码}"),
colProps: { xl: 5, md: 6 }
},
// 字典下拉(多选)
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: { ...getDictProps("{字典编码}"), mode: "multiple" },
colProps: { xl: 5, md: 6 }
}
];
// ========== 新增/编辑表单 Schema ==========
export const {}FormSchema: FormSchema[] = [
{
field: "id",
label: "唯一ID",
component: "Input",
show: false
},
// 文本输入
{
field: "{字段名}",
label: "{字段中文名}",
component: "Input",
required: true // 必填时加上
},
// 数值输入
{
field: "{字段名}",
label: "{字段中文名}",
component: "InputNumber"
},
// 字典下拉
{
field: "{字段名}",
label: "{字段中文名}",
component: "ApiSelect",
componentProps: getDictProps("{字典编码}")
},
// 日期(仅日期)
{
field: "{字段名}",
label: "{字段中文名}",
component: "DatePicker",
componentProps: {
valueFormat: "YYYY-MM-DD",
format: "YYYY-MM-DD",
getPopupContainer: () => document.body
}
},
// 日期时间
{
field: "{字段名}",
label: "{字段中文名}",
component: "DatePicker",
componentProps: {
valueFormat: "YYYY-MM-DD HH:mm:ss",
format: "YYYY-MM-DD HH:mm:ss",
showTime: { format: "HH:mm:ss" },
getPopupContainer: () => document.body
}
}
];
// ========== 详情查看 Schema ==========
export class {}Desc {
viewSchema: DescItem[] = [
{
label: "id",
field: "id",
show: () => false
},
// 普通字段
{
field: "{字段名}",
label: "{字段中文名}"
},
// 字典字段
{
render: (val) => {
if (val === undefined) return;
return buildDictTag("{字典编码}", val);
},
field: "{字段名}",
label: "{字段中文名}"
}
];
}
```
**组件选择规则:**
| 字段类型 | 表单组件 |
|---|---|
| `string`(普通文本) | `Input` |
| `number`(整数/小数) | `InputNumber` |
| `string`(日期) | `DatePicker`dateFormat: YYYY-MM-DD|
| `string`(日期时间) | `DatePicker`showTime|
| 有字典 | `ApiSelect` + `getDictProps("{字典编码}")` |
---
#### 4. 列表主页面(`views/{apiPrefix}/{entity-kebab-case}/index.vue`
```vue
<!--
@description: {中文名称}
@author: mfish
@date: {当前日期}
@version: V2.3.1
-->
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<AButton type="primary" @click="handleCreate" v-auth="'{apiPrefix}:{变量名}:insert'">新增</AButton>
<AButton color="warning" @click="handleExport" v-auth="'{apiPrefix}:{变量名}:export'">导出</AButton>
<AButton color="error" @click="handleBatchDelete" v-auth="'{apiPrefix}:{变量名}:delete'">批量删除</AButton>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{
icon: 'ant-design:info-circle-outlined',
onClick: handleQuery.bind(null, record),
auth: '{apiPrefix}:{变量名}:query',
color: 'success',
tooltip: '查看'
},
{
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
auth: '{apiPrefix}:{变量名}:update',
tooltip: '修改'
},
{
icon: 'ant-design:delete-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
placement: 'left',
confirm: handleDelete.bind(null, record)
},
auth: '{apiPrefix}:{变量名}:delete',
tooltip: '删除'
}
]"
/>
</template>
</template>
</BasicTable>
<{类名}Modal @register="registerModal" @success="handleSuccess" />
<{类名}ViewModal @register="registerViewModal" />
</div>
</template>
<script lang="ts" setup>
import { BasicTable, useTable, TableAction } from "@mfish/core/components/Table";
import { useModal } from "@mfish/core/components/Modal";
import { Button as AButton } from "@mfish/core/components/Button";
import { deleteBatch{类名}, delete{类名}, export{类名}, get{类名}List } from "@/api/{apiPrefix}/{类名}";
import {类名}Modal from "./{类名}Modal.vue";
import {类名}ViewModal from "./{类名}ViewModal.vue";
import { columns, searchFormSchema } from "./{变量名}.data";
import { {类名} } from "@/api/{apiPrefix}/model/{类名}Model";
import { ref } from "vue";
import { useMessage } from "@mfish/core/hooks";
defineOptions({ name: "{类名}Management" });
const [registerModal, { openModal }] = useModal();
const [registerViewModal, { openModal: openViewModal }] = useModal();
const selectedRowKeys = ref<any[]>([]);
const [registerTable, { reload, getForm }] = useTable({
title: "{中文名称}列表",
api: get{类名}List,
rowKey: "id",
columns,
formConfig: {
name: "search_form_item",
labelWidth: 100,
schemas: searchFormSchema,
autoSubmitOnEnter: true
},
useSearchForm: true,
showTableSetting: true,
bordered: true,
showIndexColumn: false,
rowSelection: {
onChange: (rowKeys: any[]) => {
selectedRowKeys.value = rowKeys;
}
},
actionColumn: {
width: 120,
title: "操作",
dataIndex: "action"
}
});
const { createMessage } = useMessage();
function handleCreate() {
openModal(true, { isUpdate: false });
}
function handleExport() {
export{类名}({ ...getForm().getFieldsValue(), pageNum: 1, pageSize: 1000 });
}
function handleQuery({变量名}: {类名}) {
openViewModal(true, { record: {变量名} });
}
function handleEdit({变量名}: {类名}) {
openModal(true, { record: {变量名}, isUpdate: true });
}
function handleDelete({变量名}: {类名}) {
if ({变量名}.id) {
delete{类名}({变量名}.id).then(() => {
handleSuccess();
});
}
}
function handleBatchDelete() {
if (selectedRowKeys.value.length > 0) {
deleteBatch{类名}(selectedRowKeys.value.join(",")).then(() => {
handleSuccess();
});
} else {
createMessage.warning("请勾选要删除的数据");
}
}
function handleSuccess() {
reload();
}
</script>
```
---
#### 5. 新增/编辑弹窗(`views/{apiPrefix}/{entity-kebab-case}/{类名}Modal.vue`
```vue
<!--
@description: {中文名称}
@author: mfish
@date: {当前日期}
@version: V2.3.1
-->
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" @submit="handleSubmit" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from "vue";
import { BasicForm, useForm } from "@mfish/core/components/Form";
import { {变量名}FormSchema } from "./{变量名}.data";
import { BasicModal, useModalInner } from "@mfish/core/components/Modal";
import { insert{类名}, update{类名} } from "@/api/{apiPrefix}/{类名}";
defineOptions({ name: "{类名}Modal" });
const emit = defineEmits(["success", "register"]);
const isUpdate = ref(true);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
name: "model_form_item",
labelWidth: 100,
baseColProps: { span: 12 },
schemas: {变量名}FormSchema,
showActionButtonGroup: false,
autoSubmitOnEnter: true
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields().then();
setModalProps({ confirmLoading: false, width: "800px" });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
setFieldsValue({ ...data.record }).then();
}
});
const getTitle = computed(() => (unref(isUpdate) ? "编辑{中文名称}" : "新增{中文名称}"));
async function handleSubmit() {
const values = await validate();
setModalProps({ confirmLoading: true });
if (unref(isUpdate)) {
save{类名}(update{类名}, values);
} else {
save{类名}(insert{类名}, values);
}
}
function save{类名}(save, values) {
save(values)
.then(() => {
emit("success");
closeModal();
})
.finally(() => {
setModalProps({ confirmLoading: false });
});
}
</script>
```
---
#### 6. 详情查看弹窗(`views/{apiPrefix}/{entity-kebab-case}/{类名}ViewModal.vue`
```vue
<!--
@description: {中文名称}查看
@author: mfish
@date: {当前日期}
@version: V2.3.1
-->
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="{中文名称}信息">
<Description @register="registerDesc" />
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from "@mfish/core/components/Modal";
import { Description, useDescription } from "@mfish/core/components/Description";
import { ref } from "vue";
import { {类名}Desc } from "./{变量名}.data";
defineOptions({ name: "{类名}ViewModal" });
const {变量名}Data = ref();
const {变量名}Desc = new {类名}Desc();
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
setModalProps({
confirmLoading: false,
width: "800px",
cancelText: "关闭",
showOkBtn: false
});
{变量名}Data.value = data.record;
});
const [registerDesc] = useDescription({
data: {变量名}Data,
schema: {变量名}Desc.viewSchema,
column: 2
});
</script>
```
---
## 命名约定
| 占位符 | 说明 | 示例 |
|--------|------|------|
| `{类名}` | PascalCase 类名 | `DemoOrder` |
| `{变量名}` | camelCase 变量名 | `demoOrder` |
| `{apiPrefix}` | 模块路径(小写) | `demo` |
| `{entity-kebab-case}` | kebab-case 目录名 | `demo-order` |
---
## 注意事项
1. **字典字段**`columns``viewSchema` 中使用 `buildDictTag``searchFormSchema``FormSchema` 中使用 `getDictProps`;只要有字典字段就需要在文件顶部引入 `buildDictTag``getDictProps`
2. **无搜索表单字段**`searchFormSchema` 可以为空数组 `[]``useSearchForm` 设为 `false`
3. **数值型主键**`delete{类名}` 参数类型改为 `number``Model``BaseEntity<number>`
4. **导出功能可选**:若不需要导出,删除 `handleExport``export{类名}` 的引入和 toolbar 中的导出按钮
5. **批量删除可选**:若不需要批量删除,删除 `handleBatchDelete``deleteBatch{类名}` 的引入、`rowSelection` 配置和 toolbar 中的批量删除按钮
6. **配合后端 CRUD**:前端文件路径中的 `{apiPrefix}/{变量名}` 对应后端 Controller 的 `@RequestMapping` 路径
---
## 参考实现
- 含字典的完整示例:[demoOrder.data.ts](mfish-nocode-view/src/views/demo/demo-order/demoOrder.data.ts)
- API 文件示例:[DemoOrder.ts](mfish-nocode-view/src/api/demo/DemoOrder.ts)
- Model 示例:[DemoOrderModel.ts](mfish-nocode-view/src/api/demo/model/DemoOrderModel.ts)
- 无字典简单示例:[demoDataScope.data.ts](mfish-nocode-view/src/views/demo/demo-data-scope/demoDataScope.data.ts)
- 后端 CRUD 配套请使用 **crud-generator** skill
+266
View File
@@ -0,0 +1,266 @@
---
name: workflow-audit
description: 为 mfish-nocode-pro 项目集成 Flowable 工作流审批能力,包括注册 FlowKey、实体审批状态字段、Service 启动/撤回流程、Controller 审批回调接口、Feign 回调接口注册五个步骤的完整代码模板。当用户说"带工作流审批"、"发布审核流程"、"集成工作流"、"审批回调"时使用此 skill。
---
# 工作流审批集成
适用于需要**发布/审核**流程的业务模块(如大屏发布、内容审核等),在标准 CRUD 基础上叠加工作流能力。
## 核心概念
| 组件 | 说明 |
|------|------|
| `FlowableParam<T>` | 启动工作流参数:`key`(流程定义key)、`id`(业务id)、`prefix`(回调URL前缀)、`callback`(回调Feign接口全路径) |
| `FlowKey` 枚举 | 流程定义key枚举,新业务须在此注册,路径:`mf-api/mf-workflow-api/.../FlowKey.java` |
| `RemoteAuditApi<T>` | 审批回调接口,Controller 需实现 `approved`/`rejected`/`canceled` 三个方法 |
| `WorkflowCompleteResult` | 回调结果体:`processInstanceId``comment``eventName` |
| `RemoteWorkflowService` | Feign 客户端,用于启动/删除流程实例 |
## 审批状态约定
| auditState 值 | 含义 | 触发时机 |
|--------------|------|---------|
| `0` | 审核中 | 发布提交时设置 |
| `1` | 已通过 | `approved` 回调时设置 |
| `2` | 未通过 | `rejected` 回调时设置 |
| `null` | 已取消 | `canceled` 回调时设置 |
---
## 集成步骤
### 第一步:注册 FlowKey
`FlowKey.java` 枚举中添加新流程定义 key
```java
// mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/enums/FlowKey.java
新业务名称 ("xxx_release"), // key 需与 BPMN 文件中的 process id 一致
```
### 第二步:实体新增审批状态字段
```java
@Schema(description = "审核状态 null 未发布 0 审核中 1 已通过 2 未通过")
private Integer auditState;
```
**注意**:实体类必须添加完整的 Swagger 注解:
- **类级别**`@Schema(description = "描述信息", name = "类名")`
- **字段级别**:每个字段都需添加 `@Schema(description = "字段描述")`
- 便于生成完整的 API 文档和 Swagger UI 展示
### 第三步:Service 中启动和撤回工作流
```java
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.workflow.api.entity.FlowableParam;
import cn.com.mfish.common.workflow.api.enums.FlowKey;
import cn.com.mfish.common.workflow.api.remote.RemoteWorkflowService;
import jakarta.annotation.Resource;
import org.springframework.transaction.annotation.Transactional;
@Service
public class {类名}ServiceImpl extends ServiceImpl<{类名}Mapper, {类名}> implements {类名}Service {
@Resource
RemoteWorkflowService remoteWorkflowService;
/** 发布(新增并启动工作流) */
@Override
@Transactional
public Result<{类名}> insert{类名}({类名} entity) {
// 业务唯一性校验:避免重复发布
if (baseMapper.exists(new LambdaQueryWrapper<{类名}>()
.eq({类名}::getSourceId, entity.getSourceId()))) {
throw new MyRuntimeException("错误:已存在,不能重复发布!");
}
entity.setAuditState(0);
if (save(entity)) {
startProcess(entity);
return Result.ok(entity, "{中文名称}-发布成功!");
}
return Result.fail(entity, "错误:{中文名称}-发布失败!");
}
/** 撤回(删除记录并撤销工作流) */
@Override
@Transactional
public Result<Boolean> delete{类名}(String id) {
{类名} entity = getById(id);
if (entity == null) {
return Result.ok(false, "错误:记录不存在!");
}
if (remove(new LambdaQueryWrapper<{类名}>().eq({类名}::getId, id))) {
Result<String> result = remoteWorkflowService.delProcessByBusinessKey(
RPCConstants.INNER, entity.getId(), "用户撤回");
if (!result.isSuccess()) {
throw new MyRuntimeException(result.getMsg());
}
return Result.ok(true, "撤回成功!");
}
return Result.fail(false, "错误:撤回失败!");
}
/** 审批回调:更新审批状态 */
@Override
public Result<String> audit(String id, Integer auditState, WorkflowCompleteResult result) {
{类名} entity = baseMapper.selectById(id);
if (entity == null) throw new MyRuntimeException("错误:记录不存在!");
entity.setAuditState(auditState);
if (!updateById(entity)) throw new MyRuntimeException("错误:审批操作异常!");
return Result.ok(id, "审批操作成功!");
}
/** 启动工作流 */
private void startProcess({类名} entity) {
Result<String> result = remoteWorkflowService.startProcess(RPCConstants.INNER,
new FlowableParam<String>()
.setKey(FlowKey.{对应枚举}.toString())
.setId(entity.getId()) // 业务id 作为 businessKey
.setPrefix("{回调URL前缀}") // Controller @RequestMapping 路径(不含 /
.setCallback("{Feign接口全路径}") // 例如:cn.com.mfish.xxx.api.remote.RemoteXxxService
);
if (!result.isSuccess()) throw new MyRuntimeException(result.getMsg());
}
}
```
### 第四步:Controller 追加审批回调接口
工作流引擎审批完成后,通过 Feign 回调这三个接口,**无需 `@RequiresPermissions`**
```java
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
// 追加到现有 Controller 末尾
@PostMapping("/approved/{id}")
public Result<String> approved(
@PathVariable String id,
@RequestBody WorkflowCompleteResult result) {
return {变量名}Service.audit(id, 1, result);
}
@PostMapping("/rejected/{id}")
public Result<String> rejected(
@PathVariable String id,
@RequestBody WorkflowCompleteResult result) {
return {变量名}Service.audit(id, 2, result);
}
@PostMapping("/canceled/{id}")
public Result<String> canceled(
@PathVariable String id,
@RequestBody WorkflowCompleteResult result) {
return {变量名}Service.audit(id, null, result);
}
```
### 第五步:注册 Feign 回调接口(微服务模式)
`mf-api/mf-xxx-api` 模块中定义回调 Feign 接口,继承 `RemoteAuditApi<String>`
```java
import cn.com.mfish.common.core.entity.RemoteAuditApi;
@FeignClient(
contextId = "remote{类名}Service",
value = ServiceConstants.XXX_SERVICE,
fallbackFactory = Remote{类名}FallBack.class
)
public interface Remote{类名}Service extends RemoteAuditApi<String> {
// 其他跨服务调用方法...
}
```
> - `FlowableParam.callback` 填写此接口的**全路径类名**
> - `FlowableParam.prefix` 填写 Controller 的 `@RequestMapping` 路径(**不含 `/`**
---
## 注意事项
- `prefix` 与 Controller `@RequestMapping` 必须一致,回调 URL 拼接规则:`/{prefix}/approved/{id}`
- 编辑已发布记录前需校验审核状态:`auditState=1` 的记录禁止直接编辑,需先撤回
- 重复发布校验:发布前通过 `sourceId` 或业务唯一键检查是否已存在记录
- 撤回时需同步调用 `remoteWorkflowService.delProcessByBusinessKey(...)` 删除工作流实例
- Service 接口中需声明 `audit(String id, Integer auditState, WorkflowCompleteResult result)` 方法
- **异常处理规范**:人为抛出的业务异常统一采用 `MyRuntimeException` 处理
- **引入包**`import cn.com.mfish.common.core.exception.MyRuntimeException;`
- **使用场景**:记录不存在、重复提交、状态校验失败等业务异常情况
- **示例代码**
```java
// 记录不存在
if (entity == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
// 重复发布校验
if (baseMapper.exists(new LambdaQueryWrapper<Entity>()
.eq(Entity::getSourceId, sourceId))) {
throw new MyRuntimeException("错误:已存在,不能重复发布!");
}
// 审批状态校验
if (!"approved".equals(entity.getAuditState())) {
throw new MyRuntimeException("错误:审批状态不正确!");
}
```
- **消息格式**:建议以 `"错误:"` 开头,便于前端统一处理和识别
- **不要使用**:避免直接使用 `RuntimeException` 或其他自定义异常
- **安全规范 - 异常信息不暴露给前端**:
- **核心原则**:返回给前端的错误消息必须是友好的、通用的提示,不能包含具体的异常堆栈或技术细节
- **错误示例**`return Result.fail(false, "错误:流程配置不正确," + e.getMessage());` ❌
- **正确示例**`return Result.fail(false, "错误:流程配置不正确,请检查流程设计是否完整且符合规范");` ✅
- **日志记录**:详细的异常信息应通过 `log.error()` 记录到日志文件中,便于开发人员排查
- **实现模式**
```java
try {
// 业务逻辑
BpmnConverter.convertToBpmn(...);
} catch (Exception e) {
// 详细异常信息记录到日志
log.error("流程格式化失败:{}", e.getMessage(), e);
// 返回给前端的是友好的提示信息
return Result.fail(false, "错误:流程配置不正确,请检查流程设计是否完整且符合规范");
}
```
---
## 相关参考
- Controller 参考:[ScreenResourceController.java](mf-business/mf-nocode/src/main/java/cn/com/mfish/nocode/controller/ScreenResourceController.java)
- Service 参考:[ScreenResourceServiceImpl.java](mf-business/mf-nocode/src/main/java/cn/com/mfish/nocode/service/impl/ScreenResourceServiceImpl.java)
- FlowKey 枚举:[FlowKey.java](mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/enums/FlowKey.java)
- 工作流参数:[FlowableParam.java](mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/entity/FlowableParam.java)
- 审批回调接口:[RemoteAuditApi.java](mf-common/mf-common-core/src/main/java/cn/com/mfish/common/core/entity/RemoteAuditApi.java)
- 工作流 Feign 客户端:[RemoteWorkflowService.java](mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/remote/RemoteWorkflowService.java)
---
## 规范补充(2026-04
1. 新审批业务必须先在 `mf-api` 下提供独立 Feign 回调接口模块(例如 `mf-demo-api`),接口继承 `RemoteAuditApi<T>`,并提供 `fallbackFactory`。
2. `FlowableParam.callback` 必须填写 API 模块中的 Feign 接口全限定名(不要填写业务模块本地类路径)。
3. 单体模式覆盖实现必须放在 `mf-common/mf-common-api`Bean 名与 Feign `contextId` 对应(如 `remoteDemoLeaveApplyService`)。
4. `mf-common-api` 通过依赖 API 模块实现解耦,不直接依赖 `mf-business`;如需调用业务方法,优先通过 Spring 容器按 Bean 名调用。
## 优化记录(2026-04-19
1. `mf-demo-api` 创建后,必须同步加入根 `pom.xml` 的 `dependencyManagement`,避免子模块遗漏版本管理。
2. `BootDemoLeaveApplyService` 禁止通过反射获取 `demoLeaveApplyService`,应改为依赖注入 `DemoLeaveApplyService`。
3. `DemoLeaveApplyService` 直接下沉到 `mf-common`(例如 `mf-common-demo`),不要再额外拆分 `DemoLeaveApplyAuditService`。
4. 该模式下保持依赖方向为:`mf-common-api -> mf-common-demo`、`mf-business -> mf-common-demo`,并为后续 Feign 扩展预留统一服务接口。
## 依赖约束(2026-04-19
1. `mf-common-demo` 的 `pom.xml` 默认仅引入:
`<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring</artifactId></dependency>`。
2. 不要默认引入 `mybatis-plus-annotation`、`mybatis-plus-extension`、`fastexcel`;仅在确有直接编译依赖时再按最小集补充。
3. 若公共模块实体/接口仅用于跨层共享,优先保持依赖最小化,避免把业务模块的技术依赖扩散到 `mf-common`。
+2
View File
@@ -69,3 +69,5 @@
/mf-api/mf-workflow-api/target/
/mf-common/mf-common-nocode/target/
/mf-common/mf-common-prometheus/target/
/mf-common/mf-common-demo/target/
/mf-api/mf-demo-api/target/
+24 -1
View File
@@ -1,4 +1,4 @@
---
---
name: workflow-audit
description: 为 mfish-nocode-pro 项目集成 Flowable 工作流审批能力,包括注册 FlowKey、实体审批状态字段、Service 启动/撤回流程、Controller 审批回调接口、Feign 回调接口注册五个步骤的完整代码模板。当用户说"带工作流审批"、"发布审核流程"、"集成工作流"、"审批回调"时使用此 skill。
---
@@ -241,3 +241,26 @@ public interface Remote{类名}Service extends RemoteAuditApi<String> {
- 工作流参数:[FlowableParam.java](mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/entity/FlowableParam.java)
- 审批回调接口:[RemoteAuditApi.java](mf-common/mf-common-core/src/main/java/cn/com/mfish/common/core/entity/RemoteAuditApi.java)
- 工作流 Feign 客户端:[RemoteWorkflowService.java](mf-api/mf-workflow-api/src/main/java/cn/com/mfish/common/workflow/api/remote/RemoteWorkflowService.java)
---
## 规范补充(2026-04
1. 新审批业务必须先在 `mf-api` 下提供独立 Feign 回调接口模块(例如 `mf-demo-api`),接口继承 `RemoteAuditApi<T>`,并提供 `fallbackFactory`。
2. `FlowableParam.callback` 必须填写 API 模块中的 Feign 接口全限定名(不要填写业务模块本地类路径)。
3. 单体模式覆盖实现必须放在 `mf-common/mf-common-api`Bean 名与 Feign `contextId` 对应(如 `remoteDemoLeaveApplyService`)。
4. `mf-common-api` 通过依赖 API 模块实现解耦,不直接依赖 `mf-business`;如需调用业务方法,优先通过 Spring 容器按 Bean 名调用。
## 优化记录(2026-04-19
1. `mf-demo-api` 创建后,必须同步加入根 `pom.xml` 的 `dependencyManagement`,避免子模块遗漏版本管理。
2. `BootDemoLeaveApplyService` 禁止通过反射获取 `demoLeaveApplyService`,应改为依赖注入 `DemoLeaveApplyService`。
3. `DemoLeaveApplyService` 直接下沉到 `mf-common`(例如 `mf-common-demo`),不要再额外拆分 `DemoLeaveApplyAuditService`。
4. 该模式下保持依赖方向为:`mf-common-api -> mf-common-demo`、`mf-business -> mf-common-demo`,并为后续 Feign 扩展预留统一服务接口。
## 依赖约束(2026-04-19
1. `mf-common-demo` 的 `pom.xml` 默认仅引入:
`<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring</artifactId></dependency>`。
2. 不要默认引入 `mybatis-plus-annotation`、`mybatis-plus-extension`、`fastexcel`;仅在确有直接编译依赖时再按最小集补充。
3. 若公共模块实体/接口仅用于跨层共享,优先保持依赖最小化,避免把业务模块的技术依赖扩散到 `mf-common`。
+20
View File
@@ -250,4 +250,24 @@ INSERT INTO `demo_order_detail` VALUES ('e319c6b94efd11eb820300163e11f4a0', 'O16
INSERT INTO `demo_order_detail` VALUES ('f36c3a074b0911eb820300163e11f4a0', 'O16093791675560001', '雕牌超效加酶无磷洗衣粉2.68千克/袋', 'https://www.ecishan.com.cn/storage/714991-1.png', 24.30, 24.30, 2, NULL, NULL, 0.00, 48.60, 0.00, '', NULL, '', NULL);
INSERT INTO `demo_order_detail` VALUES ('f36c3a2f4b0911eb820300163e11f4a0', 'O16093791675560001', '福临门苏软香 10kg/袋', 'https://www.ecishan.com.cn/storage/file16049116832980001.png', 59.90, 59.90, 2, NULL, NULL, 0.00, 119.80, 0.00, '', NULL, '', NULL);
-- ----------------------------
-- Table structure for demo_leave_apply
-- ----------------------------
DROP TABLE IF EXISTS `demo_leave_apply`;
CREATE TABLE `demo_leave_apply` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '唯一ID',
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '申请标题',
`leave_type` tinyint NULL DEFAULT NULL COMMENT '请假类型 1事假 2病假 3年假',
`start_time` datetime NULL DEFAULT NULL COMMENT '开始时间',
`end_time` datetime NULL DEFAULT NULL COMMENT '结束时间',
`leave_days` decimal(5, 1) NULL DEFAULT NULL COMMENT '请假天数',
`reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请假事由',
`audit_state` tinyint NULL DEFAULT NULL COMMENT '审核状态 null未提交 0审核中 1已通过 2未通过',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '请假申请审批Demo' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
+1
View File
@@ -228,6 +228,7 @@ INSERT INTO `sso_menu` VALUES ('a7d3a7b61fa52964c5c9db477e3b1962', '8ae3ea763294
INSERT INTO `sso_menu` VALUES ('ad5b361ff34235e8ec85cd613a59bf6e', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300001', 3, '查询', '#', 1, 2, '', NULL, 'workflow:flowManage:query', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c5309ca3ba545da9950278637c90e674', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300002', 3, '新增', '#', 2, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:insert', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('e92728a6d996d4b7cb3acb0fc031d183', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300004', 3, '删除', '#', 4, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:delete', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c0adefdb601f6e41f82a06ff6513613e', '53e8eaceee36c1d54e43319fdd60811b', '0001300008', 2, '工作流样例', 'ant-design:fork-outlined', 11, 1, '/demo-leave-apply', '/demo/demo-leave-apply/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-04-20 17:05:36', 'admin', '2026-04-20 20:05:50');
-- ----------------------------
-- Table structure for sso_org
+1
View File
@@ -271,6 +271,7 @@ INSERT INTO `sys_dict_item` VALUES ('2cdf5389b1d6a35e52c14486e04a3a57', '220bbd3
INSERT INTO `sys_dict_item` VALUES ('261f489024bb90163e49ee85498df47e', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已审批', 'completed', 0, 2, NULL, 'green', 0, NULL, 'admin', '2025-10-10 16:28:02', 'admin', '2025-10-10 16:28:08');
INSERT INTO `sys_dict_item` VALUES ('07582f8cb8443cd3de3f5ab150386690', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已取消', 'terminated', 0, 3, '', 'red', 0, NULL, 'admin', '2025-10-10 16:29:10', 'admin', '2025-10-10 16:29:14');
INSERT INTO `sys_dict_item` VALUES ('4e14582f0b59762f2ca1cfda04539202', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '大屏发布', 'screen_release', 0, 1, 'ant-design:fund-projection-screen-outlined', '', 0, NULL, 'admin', '2025-10-15 16:35:15', 'admin', '2025-10-16 11:04:43');
INSERT INTO `sys_dict_item` VALUES ('581a7e287feec3be8771175bdc5dab9b', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '工作流样例审批', 'demo_leave_apply_release', 0, 2, 'ant-design:fork-outlined', 'blue', 0, '工作流试用样例', 'admin', '2026-04-08 20:08:42', 'admin', '2026-04-20 21:38:45');
-- ----------------------------
-- Table structure for sys_log
+3
View File
@@ -28,4 +28,7 @@ CREATE TABLE `flw_mf_manage` (
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流程管理' ROW_FORMAT = DYNAMIC;
INSERT INTO `flw_mf_manage` VALUES ('b686750cc7edd180d4f5c4ac8d5299fa', 'demo_leave_apply_release', '工作流样例审批', '工作流试用样例', 1, 1, '{\"nodes\":[{\"id\":\"node_start_1\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":250,\"y\":50},\"data\":{\"type\":\"start\",\"label\":\"开始\",\"icon\":\"Play\"},\"label\":\"开始\"},{\"id\":\"node_approval_1776677052658\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":550,\"y\":50},\"data\":{\"type\":\"approval\",\"label\":\"审批\",\"approvalType\":\"OR\",\"userIds\":[\"c51fde3955594074bb4db31e654a4483\"],\"userNames\":[\"mfish\"]},\"label\":\"审批\"},{\"id\":\"node_approval_1776677060910\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":981.25,\"y\":47.5},\"data\":{\"type\":\"approval\",\"label\":\"审批\",\"approvalType\":\"OR\",\"roleIds\":[\"4b423f7b1ac0ed0b46a8e5ec3389ac14\"],\"roleNames\":[\"管理\"]},\"label\":\"审批\"},{\"id\":\"node_end_1776677072235\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":1360,\"y\":221.24999999999997},\"data\":{\"type\":\"end\",\"label\":\"结束\",\"executionListeners\":[{\"event\":\"start\",\"type\":\"class\",\"value\":\"cn.com.mfish.workflow.handler.CompleteCallbackHandler\"}]},\"label\":\"结束\"}],\"edges\":[{\"id\":\"e-node_start_1-right-node_approval_1776677052658-left\",\"type\":\"custom\",\"source\":\"node_start_1\",\"target\":\"node_approval_1776677052658\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":null},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":492.5,\"sourceY\":80.66667175292969,\"targetX\":547.5,\"targetY\":110.66665649414062},{\"id\":\"e-node_approval_1776677052658-right-node_approval_1776677060910-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677052658\",\"target\":\"node_approval_1776677060910\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"approved\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":792.5,\"sourceY\":110.66665649414062,\"targetX\":978.75,\"targetY\":108.16665649414062},{\"id\":\"e-node_approval_1776677060910-top-source-node_approval_1776677052658-top-source\",\"type\":\"custom\",\"source\":\"node_approval_1776677060910\",\"target\":\"node_approval_1776677052658\",\"sourceHandle\":\"top-source\",\"targetHandle\":\"top-source\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"rejected\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":1101.25,\"sourceY\":45,\"targetX\":670,\"targetY\":47.5},{\"id\":\"e-node_approval_1776677060910-right-node_end_1776677072235-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677060910\",\"target\":\"node_end_1776677072235\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"approved\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":1223.75,\"sourceY\":108.16665649414062,\"targetX\":1357.5,\"targetY\":251.9166564941406},{\"id\":\"e-node_approval_1776677052658-bottom-node_end_1776677072235-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677052658\",\"target\":\"node_end_1776677072235\",\"sourceHandle\":\"bottom\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"smoothstep\",\"condition\":\"rejected\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":670,\"sourceY\":173.83331298828125,\"targetX\":1357.5,\"targetY\":251.9166564941406}],\"position\":[-27.5,349.1],\"zoom\":0.8,\"viewport\":{\"x\":-27.5,\"y\":349.1,\"zoom\":0.8}}', 0, 'ca50d1785a07f71abbec0da4af6b0632d8d51f54c84ca5050debf1c46f529291', 'admin', '2026-04-20 17:25:00', 'admin', '2026-04-20 17:25:05');
SET FOREIGN_KEY_CHECKS = 1;
+24
View File
@@ -453,6 +453,7 @@ INSERT INTO `sso_menu` VALUES ('a7d3a7b61fa52964c5c9db477e3b1962', '8ae3ea763294
INSERT INTO `sso_menu` VALUES ('ad5b361ff34235e8ec85cd613a59bf6e', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300001', 3, '查询', '#', 1, 2, '', NULL, 'workflow:flowManage:query', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c5309ca3ba545da9950278637c90e674', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300002', 3, '新增', '#', 2, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:insert', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('e92728a6d996d4b7cb3acb0fc031d183', '8ae3ea76329402ee495cccfaa4c4c38d', '000140000300004', 3, '删除', '#', 4, 2, '', NULL, 'workflow:flowManage:query,workflow:flowManage:delete', 0, 1, NULL, NULL, '', 'admin', '2026-03-30 19:43:52', '', NULL);
INSERT INTO `sso_menu` VALUES ('c0adefdb601f6e41f82a06ff6513613e', '53e8eaceee36c1d54e43319fdd60811b', '0001300008', 2, '工作流样例', 'ant-design:fork-outlined', 11, 1, '/demo-leave-apply', '/demo/demo-leave-apply/index.vue', NULL, 0, 1, NULL, 1, '', 'admin', '2026-04-20 17:05:36', 'admin', '2026-04-20 20:05:50');
-- ----------------------------
-- Table structure for sso_org
@@ -1138,6 +1139,7 @@ INSERT INTO `sys_dict_item` VALUES ('2cdf5389b1d6a35e52c14486e04a3a57', '220bbd3
INSERT INTO `sys_dict_item` VALUES ('261f489024bb90163e49ee85498df47e', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已审批', 'completed', 0, 2, NULL, 'green', 0, NULL, 'admin', '2025-10-10 16:28:02', 'admin', '2025-10-10 16:28:08');
INSERT INTO `sys_dict_item` VALUES ('07582f8cb8443cd3de3f5ab150386690', '220bbd3b1dd32fd37d0abbd279a14774', 'workflow_task_status', '已取消', 'terminated', 0, 3, '', 'red', 0, NULL, 'admin', '2025-10-10 16:29:10', 'admin', '2025-10-10 16:29:14');
INSERT INTO `sys_dict_item` VALUES ('4e14582f0b59762f2ca1cfda04539202', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '大屏发布', 'screen_release', 0, 1, 'ant-design:fund-projection-screen-outlined', '', 0, NULL, 'admin', '2025-10-15 16:35:15', 'admin', '2025-10-16 11:04:43');
INSERT INTO `sys_dict_item` VALUES ('581a7e287feec3be8771175bdc5dab9b', 'eeb27772c310addeae7c12d296521399', 'workflow_process_key', '工作流样例审批', 'demo_leave_apply_release', 0, 2, 'ant-design:fork-outlined', 'blue', 0, '工作流试用样例', 'admin', '2026-04-08 20:08:42', 'admin', '2026-04-20 21:38:45');
-- ----------------------------
-- Table structure for sys_log
@@ -1514,6 +1516,26 @@ INSERT INTO `demo_order_detail` VALUES ('e319c6b94efd11eb820300163e11f4a0', 'O16
INSERT INTO `demo_order_detail` VALUES ('f36c3a074b0911eb820300163e11f4a0', 'O16093791675560001', '雕牌超效加酶无磷洗衣粉2.68千克/袋', 'https://www.ecishan.com.cn/storage/714991-1.png', 24.30, 24.30, 2, NULL, NULL, 0.00, 48.60, 0.00, '', NULL, '', NULL);
INSERT INTO `demo_order_detail` VALUES ('f36c3a2f4b0911eb820300163e11f4a0', 'O16093791675560001', '福临门苏软香 10kg/袋', 'https://www.ecishan.com.cn/storage/file16049116832980001.png', 59.90, 59.90, 2, NULL, NULL, 0.00, 119.80, 0.00, '', NULL, '', NULL);
-- ----------------------------
-- Table structure for demo_leave_apply
-- ----------------------------
DROP TABLE IF EXISTS `demo_leave_apply`;
CREATE TABLE `demo_leave_apply` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '唯一ID',
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '申请标题',
`leave_type` tinyint NULL DEFAULT NULL COMMENT '请假类型 1事假 2病假 3年假',
`start_time` datetime NULL DEFAULT NULL COMMENT '开始时间',
`end_time` datetime NULL DEFAULT NULL COMMENT '结束时间',
`leave_days` decimal(5, 1) NULL DEFAULT NULL COMMENT '请假天数',
`reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请假事由',
`audit_state` tinyint NULL DEFAULT NULL COMMENT '审核状态 null未提交 0审核中 1已通过 2未通过',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '请假申请审批Demo' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for mf_api
-- ----------------------------
@@ -1925,4 +1947,6 @@ CREATE TABLE `flw_mf_manage` (
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流程管理' ROW_FORMAT = DYNAMIC;
INSERT INTO `flw_mf_manage` VALUES ('b686750cc7edd180d4f5c4ac8d5299fa', 'demo_leave_apply_release', '工作流样例审批', '工作流试用样例', 1, 1, '{\"nodes\":[{\"id\":\"node_start_1\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":250,\"y\":50},\"data\":{\"type\":\"start\",\"label\":\"开始\",\"icon\":\"Play\"},\"label\":\"开始\"},{\"id\":\"node_approval_1776677052658\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":550,\"y\":50},\"data\":{\"type\":\"approval\",\"label\":\"审批\",\"approvalType\":\"OR\",\"userIds\":[\"c51fde3955594074bb4db31e654a4483\"],\"userNames\":[\"mfish\"]},\"label\":\"审批\"},{\"id\":\"node_approval_1776677060910\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":981.25,\"y\":47.5},\"data\":{\"type\":\"approval\",\"label\":\"审批\",\"approvalType\":\"OR\",\"roleIds\":[\"4b423f7b1ac0ed0b46a8e5ec3389ac14\"],\"roleNames\":[\"管理\"]},\"label\":\"审批\"},{\"id\":\"node_end_1776677072235\",\"type\":\"custom\",\"draggable\":true,\"initialized\":false,\"position\":{\"x\":1360,\"y\":221.24999999999997},\"data\":{\"type\":\"end\",\"label\":\"结束\",\"executionListeners\":[{\"event\":\"start\",\"type\":\"class\",\"value\":\"cn.com.mfish.workflow.handler.CompleteCallbackHandler\"}]},\"label\":\"结束\"}],\"edges\":[{\"id\":\"e-node_start_1-right-node_approval_1776677052658-left\",\"type\":\"custom\",\"source\":\"node_start_1\",\"target\":\"node_approval_1776677052658\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":null},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":492.5,\"sourceY\":80.66667175292969,\"targetX\":547.5,\"targetY\":110.66665649414062},{\"id\":\"e-node_approval_1776677052658-right-node_approval_1776677060910-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677052658\",\"target\":\"node_approval_1776677060910\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"approved\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":792.5,\"sourceY\":110.66665649414062,\"targetX\":978.75,\"targetY\":108.16665649414062},{\"id\":\"e-node_approval_1776677060910-top-source-node_approval_1776677052658-top-source\",\"type\":\"custom\",\"source\":\"node_approval_1776677060910\",\"target\":\"node_approval_1776677052658\",\"sourceHandle\":\"top-source\",\"targetHandle\":\"top-source\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"rejected\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":1101.25,\"sourceY\":45,\"targetX\":670,\"targetY\":47.5},{\"id\":\"e-node_approval_1776677060910-right-node_end_1776677072235-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677060910\",\"target\":\"node_end_1776677072235\",\"sourceHandle\":\"right\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"default\",\"condition\":\"approved\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":1223.75,\"sourceY\":108.16665649414062,\"targetX\":1357.5,\"targetY\":251.9166564941406},{\"id\":\"e-node_approval_1776677052658-bottom-node_end_1776677072235-left\",\"type\":\"custom\",\"source\":\"node_approval_1776677052658\",\"target\":\"node_end_1776677072235\",\"sourceHandle\":\"bottom\",\"targetHandle\":\"left\",\"data\":{\"showArrow\":true,\"pathType\":\"smoothstep\",\"condition\":\"rejected\"},\"label\":\"\",\"animated\":true,\"style\":{\"stroke\":\"#EE4F12\",\"strokeWidth\":2},\"markerEnd\":{\"type\":\"arrowclosed\",\"color\":\"#EE4F12\"},\"sourceX\":670,\"sourceY\":173.83331298828125,\"targetX\":1357.5,\"targetY\":251.9166564941406}],\"position\":[-27.5,349.1],\"zoom\":0.8,\"viewport\":{\"x\":-27.5,\"y\":349.1,\"zoom\":0.8}}', 0, 'ca50d1785a07f71abbec0da4af6b0632d8d51f54c84ca5050debf1c46f529291', 'admin', '2026-04-20 17:25:00', 'admin', '2026-04-20 17:25:05');
SET FOREIGN_KEY_CHECKS = 1;
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-api</artifactId>
<version>mf-2.3.1</version>
</parent>
<artifactId>mf-demo-api</artifactId>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,38 @@
package cn.com.mfish.demo.api.fallback;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.demo.api.remote.RemoteDemoLeaveApplyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* @description: demo请假申请审批回调接口降级处理
* @author: mfish
* @date: 2026/04/18
*/
@Component
@Slf4j
public class RemoteDemoLeaveApplyFallBack implements FallbackFactory<RemoteDemoLeaveApplyService> {
@Override
public RemoteDemoLeaveApplyService create(Throwable cause) {
log.error("错误: demo请假申请审批回调接口调用异常", cause);
return new RemoteDemoLeaveApplyService() {
@Override
public Result<String> approved(String origin, String prefix, String id, WorkflowCompleteResult result) {
return Result.fail("错误:请假审批通过回调接口异常");
}
@Override
public Result<String> rejected(String origin, String prefix, String id, WorkflowCompleteResult result) {
return Result.fail("错误:请假审批拒绝回调接口异常");
}
@Override
public Result<String> canceled(String origin, String prefix, String id, WorkflowCompleteResult result) {
return Result.fail("错误:请假审批取消回调接口异常");
}
};
}
}
@@ -0,0 +1,15 @@
package cn.com.mfish.demo.api.remote;
import cn.com.mfish.common.core.constants.ServiceConstants;
import cn.com.mfish.common.core.entity.RemoteAuditApi;
import cn.com.mfish.demo.api.fallback.RemoteDemoLeaveApplyFallBack;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @description: demo请假申请审批回调接口
* @author: mfish
* @date: 2026/04/18
*/
@FeignClient(contextId = "remoteDemoLeaveApplyService", value = ServiceConstants.DEMO_SERVICE, fallbackFactory = RemoteDemoLeaveApplyFallBack.class)
public interface RemoteDemoLeaveApplyService extends RemoteAuditApi<String> {
}
@@ -11,6 +11,7 @@ import lombok.Getter;
public enum FlowKey {
UNKNOWN("unknown"),
大屏发布("screen_release"),
请假申请发布("demo_leave_apply_release"),
TEST("test");
private final String key;
@@ -67,6 +67,11 @@ public class RemoteWorkflowFallBack implements FallbackFactory<RemoteWorkflowSer
return Result.fail("错误:查询流程任务失败" + cause.getMessage());
}
@Override
public Result<List<MfTask>> getProcessTasksByBusinessKey(String origin, String businessKey) {
return Result.fail("错误:查询流程任务失败" + cause.getMessage());
}
@Override
public Result<List<AuditComment>> getAuditComments(String origin, String processInstanceId) {
return Result.fail("错误:查询审批评论失败" + cause.getMessage());
@@ -118,6 +118,16 @@ public interface RemoteWorkflowService {
@GetMapping("/process/tasks/{processInstanceId}")
Result<List<MfTask>> getProcessTasks(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @PathVariable String processInstanceId);
/**
* 通过业务id查询流程实例任务列表
*
* @param origin 来源
* @param businessKey 业务key
* @return 任务列表
*/
@GetMapping("/process/tasks/businessKey/{businessKey}")
Result<List<MfTask>> getProcessTasksByBusinessKey(@RequestHeader(RPCConstants.REQ_ORIGIN) String origin, @PathVariable String businessKey);
/**
* 查询待处理任务列表
*
+2 -1
View File
@@ -18,6 +18,7 @@
<module>mf-storage-api</module>
<module>mf-workflow-api</module>
<module>mf-nocode-api</module>
<module>mf-demo-api</module>
</modules>
<properties>
@@ -25,4 +26,4 @@
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
</project>
</project>
+13 -1
View File
@@ -25,5 +25,17 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-web</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-workflow</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-demo</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-demo-api</artifactId>
</dependency>
</dependencies>
</project>
</project>
@@ -0,0 +1,109 @@
package cn.com.mfish.demo.controller;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.enums.OperateType;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.demo.entity.DemoLeaveApply;
import cn.com.mfish.common.demo.req.ReqDemoLeaveApply;
import cn.com.mfish.common.demo.service.DemoLeaveApplyService;
import cn.com.mfish.common.log.annotation.Log;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* @description: 请假申请审批Demo
* @author: mfish
* @date: 2026-04-18
* @version: V2.3.1
*/
@Slf4j
@Tag(name = "请假申请审批Demo")
@RestController
@RequestMapping("/demoLeaveApply")
public class DemoLeaveApplyController {
@Resource
private DemoLeaveApplyService demoLeaveApplyService;
@Operation(summary = "请假申请审批Demo-分页列表查询", description = "请假申请审批Demo-分页列表查询")
@GetMapping
public Result<PageResult<DemoLeaveApply>> queryPageList(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) {
return demoLeaveApplyService.queryPageList(reqDemoLeaveApply, reqPage);
}
@Log(title = "请假申请审批Demo-添加", operateType = OperateType.INSERT)
@Operation(summary = "请假申请审批Demo-添加")
@PostMapping
public Result<DemoLeaveApply> add(@RequestBody DemoLeaveApply demoLeaveApply) {
return demoLeaveApplyService.add(demoLeaveApply);
}
@Log(title = "请假申请审批Demo-编辑", operateType = OperateType.UPDATE)
@Operation(summary = "请假申请审批Demo-编辑")
@PutMapping
public Result<DemoLeaveApply> edit(@RequestBody DemoLeaveApply demoLeaveApply) {
return demoLeaveApplyService.edit(demoLeaveApply);
}
@Log(title = "请假申请审批Demo-通过id删除", operateType = OperateType.DELETE)
@Operation(summary = "请假申请审批Demo-通过id删除")
@DeleteMapping("/{id}")
public Result<Boolean> delete(@Parameter(name = "id", description = "唯一ID") @PathVariable String id) {
return demoLeaveApplyService.delete(id);
}
@Log(title = "请假申请审批Demo-批量删除", operateType = OperateType.DELETE)
@Operation(summary = "请假申请审批Demo-批量删除")
@DeleteMapping("/batch/{ids}")
public Result<Boolean> deleteBatch(@Parameter(name = "ids", description = "唯一ID") @PathVariable String ids) {
return demoLeaveApplyService.deleteBatch(ids);
}
@Operation(summary = "请假申请审批Demo-通过id查询")
@GetMapping("/{id}")
public Result<DemoLeaveApply> queryById(@Parameter(name = "id", description = "唯一ID") @PathVariable String id) {
return demoLeaveApplyService.queryById(id);
}
@Operation(summary = "导出请假申请审批Demo", description = "导出请假申请审批Demo")
@GetMapping("/export")
public void export(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) throws IOException {
demoLeaveApplyService.export(reqDemoLeaveApply, reqPage);
}
@Log(title = "请假申请审批Demo-提交审批", operateType = OperateType.UPDATE)
@Operation(summary = "请假申请审批Demo-提交审批")
@PostMapping("/submit/{id}")
public Result<DemoLeaveApply> submit(@PathVariable String id) {
return demoLeaveApplyService.submit(id);
}
@Log(title = "请假申请审批Demo-撤回审批", operateType = OperateType.UPDATE)
@Operation(summary = "请假申请审批Demo-撤回审批")
@PostMapping("/revoke/{id}")
public Result<DemoLeaveApply> revoke(@PathVariable String id) {
return demoLeaveApplyService.revoke(id);
}
@PostMapping("/approved/{id}")
public Result<String> approved(@PathVariable String id, @RequestBody WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, 1, result);
}
@PostMapping("/rejected/{id}")
public Result<String> rejected(@PathVariable String id, @RequestBody WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, 2, result);
}
@PostMapping("/canceled/{id}")
public Result<String> canceled(@PathVariable String id, @RequestBody WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, null, result);
}
}
@@ -0,0 +1,13 @@
package cn.com.mfish.demo.mapper;
import cn.com.mfish.common.demo.entity.DemoLeaveApply;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @description: 请假申请审批Demo
* @author: mfish
* @date: 2026-04-18
* @version: V2.3.1
*/
public interface DemoLeaveApplyMapper extends BaseMapper<DemoLeaveApply> {
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.com.mfish.demo.mapper.DemoLeaveApplyMapper">
</mapper>
@@ -0,0 +1,208 @@
package cn.com.mfish.demo.service.impl;
import cn.com.mfish.common.core.constants.RPCConstants;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.exception.MyRuntimeException;
import cn.com.mfish.common.core.utils.StringUtils;
import cn.com.mfish.common.core.utils.excel.ExcelUtils;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.demo.entity.DemoLeaveApply;
import cn.com.mfish.common.demo.req.ReqDemoLeaveApply;
import cn.com.mfish.common.demo.service.DemoLeaveApplyService;
import cn.com.mfish.common.workflow.api.entity.FlowableParam;
import cn.com.mfish.common.workflow.api.enums.FlowKey;
import cn.com.mfish.common.workflow.api.remote.RemoteWorkflowService;
import cn.com.mfish.demo.mapper.DemoLeaveApplyMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @description: 请假申请审批Demo
* @author: mfish
* @date: 2026-04-18
* @version: V2.3.1
*/
@Service
public class DemoLeaveApplyServiceImpl extends ServiceImpl<DemoLeaveApplyMapper, DemoLeaveApply> implements DemoLeaveApplyService {
@Resource
private RemoteWorkflowService remoteWorkflowService;
@Override
public Result<PageResult<DemoLeaveApply>> queryPageList(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) {
return Result.ok(new PageResult<>(queryList(reqDemoLeaveApply, reqPage)), "请假申请审批Demo-查询成功!");
}
private List<DemoLeaveApply> queryList(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) {
PageHelper.startPage(reqPage.getPageNum(), reqPage.getPageSize());
LambdaQueryWrapper<DemoLeaveApply> lambdaQueryWrapper = new LambdaQueryWrapper<DemoLeaveApply>()
.like(!StringUtils.isEmpty(reqDemoLeaveApply.getTitle()), DemoLeaveApply::getTitle, reqDemoLeaveApply.getTitle())
.eq(null != reqDemoLeaveApply.getLeaveType(), DemoLeaveApply::getLeaveType, reqDemoLeaveApply.getLeaveType())
.eq(null != reqDemoLeaveApply.getAuditState(), DemoLeaveApply::getAuditState, reqDemoLeaveApply.getAuditState())
.orderByDesc(DemoLeaveApply::getCreateTime);
return list(lambdaQueryWrapper);
}
@Override
public Result<DemoLeaveApply> add(DemoLeaveApply demoLeaveApply) {
demoLeaveApply.setAuditState(-1);
// 根据开始时间和结束时间计算请假天数
if (demoLeaveApply.getStartTime() != null && demoLeaveApply.getEndTime() != null) {
long diffInMillis = demoLeaveApply.getEndTime().getTime() - demoLeaveApply.getStartTime().getTime();
// 将毫秒转换为天数,保留一位小数
BigDecimal days = new BigDecimal(diffInMillis)
.divide(new BigDecimal(TimeUnit.DAYS.toMillis(1)), 1, RoundingMode.HALF_UP);
demoLeaveApply.setLeaveDays(days);
}
if (save(demoLeaveApply)) {
return Result.ok(demoLeaveApply, "请假申请审批Demo-添加成功!");
}
return Result.fail(demoLeaveApply, "错误:请假申请审批Demo-添加失败!");
}
@Override
public Result<DemoLeaveApply> edit(DemoLeaveApply demoLeaveApply) {
DemoLeaveApply exist = getById(demoLeaveApply.getId());
if (exist == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
if (!Integer.valueOf(-1).equals(exist.getAuditState())) {
throw new MyRuntimeException("错误:当前记录不可编辑!");
}
demoLeaveApply.setAuditState(exist.getAuditState());
// 根据开始时间和结束时间计算请假天数
if (demoLeaveApply.getStartTime() != null && demoLeaveApply.getEndTime() != null) {
long diffInMillis = demoLeaveApply.getEndTime().getTime() - demoLeaveApply.getStartTime().getTime();
// 将毫秒转换为天数,保留一位小数
BigDecimal days = new BigDecimal(diffInMillis)
.divide(new BigDecimal(TimeUnit.DAYS.toMillis(1)), 1, RoundingMode.HALF_UP);
demoLeaveApply.setLeaveDays(days);
}
if (updateById(demoLeaveApply)) {
return Result.ok(demoLeaveApply, "请假申请审批Demo-编辑成功!");
}
return Result.fail(demoLeaveApply, "错误:请假申请审批Demo-编辑失败!");
}
@Override
@Transactional
public Result<Boolean> delete(String id) {
DemoLeaveApply demoLeaveApply = getById(id);
if (demoLeaveApply == null) {
return Result.fail(false, "错误:记录不存在!");
}
if (removeById(id)) {
if (Integer.valueOf(0).equals(demoLeaveApply.getAuditState())) {
Result<String> result = remoteWorkflowService.delProcessByBusinessKey(RPCConstants.INNER, id, "用户删除请假审批申请");
if (!result.isSuccess()) {
throw new MyRuntimeException(result.getMsg());
}
}
return Result.ok(true, "请假申请审批Demo-删除成功!");
}
return Result.fail(false, "错误:请假申请审批Demo-删除失败!");
}
@Override
@Transactional
public Result<Boolean> deleteBatch(String ids) {
String[] idList = ids.split(",");
for (String id : idList) {
Result<Boolean> result = delete(id);
if (!result.isSuccess()) {
return result;
}
}
return Result.ok(true, "请假申请审批Demo-批量删除成功!");
}
@Override
public Result<DemoLeaveApply> queryById(String id) {
DemoLeaveApply demoLeaveApply = getById(id);
return Result.ok(demoLeaveApply, "请假申请审批Demo-查询成功!");
}
@Override
public void export(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) throws IOException {
ExcelUtils.write("请假申请审批Demo_" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()), queryList(reqDemoLeaveApply, reqPage));
}
@Override
@Transactional
public Result<DemoLeaveApply> submit(String id) {
DemoLeaveApply demoLeaveApply = getById(id);
if (demoLeaveApply == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
if (Integer.valueOf(0).equals(demoLeaveApply.getAuditState())) {
throw new MyRuntimeException("错误:当前记录已在审核中,请勿重复提交!");
}
demoLeaveApply.setAuditState(0);
if (!updateById(demoLeaveApply)) {
return Result.fail(demoLeaveApply, "错误:提交审批失败!");
}
startProcess(demoLeaveApply);
return Result.ok(demoLeaveApply, "提交审批成功!");
}
@Override
@Transactional
public Result<DemoLeaveApply> revoke(String id) {
DemoLeaveApply demoLeaveApply = getById(id);
if (demoLeaveApply == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
if (!Integer.valueOf(0).equals(demoLeaveApply.getAuditState())) {
throw new MyRuntimeException("错误:只有审核中记录支持撤回!");
}
Result<String> result = remoteWorkflowService.delProcessByBusinessKey(RPCConstants.INNER, id, "用户撤回请假审批申请");
if (!result.isSuccess()) {
throw new MyRuntimeException(result.getMsg());
}
demoLeaveApply.setAuditState(-1);
if (!updateById(demoLeaveApply)) {
throw new MyRuntimeException("错误:撤回后状态更新失败!");
}
return Result.ok(demoLeaveApply, "撤回审批成功!");
}
@Override
public Result<String> audit(String id, Integer auditState, WorkflowCompleteResult result) {
DemoLeaveApply demoLeaveApply = getById(id);
if (demoLeaveApply == null) {
throw new MyRuntimeException("错误:记录不存在!");
}
demoLeaveApply.setAuditState(auditState);
if (!updateById(demoLeaveApply)) {
throw new MyRuntimeException("错误:审批操作异常!");
}
return Result.ok(id, "审批操作成功!");
}
private void startProcess(DemoLeaveApply demoLeaveApply) {
Result<String> result = remoteWorkflowService.startProcess(RPCConstants.INNER, new FlowableParam<String>()
.setKey(FlowKey.请假申请发布.toString())
.setId(demoLeaveApply.getId())
.setPrefix("demoLeaveApply")
.setCallback("cn.com.mfish.demo.api.remote.RemoteDemoLeaveApplyService"));
if (!result.isSuccess()) {
throw new MyRuntimeException(result.getMsg());
}
}
}
@@ -97,6 +97,12 @@ public class ProcessController {
return Result.ok(flowableService.getProcessTasks(processInstanceId), "查询任务列表成功");
}
@Operation(summary = "根据业务key获取最新流程实例任务列表")
@GetMapping("/tasks/businessKey/{businessKey}")
public Result<List<MfTask>> getProcessTasksByBusinessKey(@Parameter(name = "businessKey", description = "业务id") @PathVariable String businessKey) {
return Result.ok(flowableService.getProcessTasksByBusinessKey(businessKey), "查询任务列表成功");
}
@Operation(summary = "查询待处理任务列表")
@GetMapping("/tasks/pending")
public Result<PageResult<MfTask>> getPendingTasks(ReqTask reqTask, ReqPage reqPage) {
@@ -108,6 +108,9 @@ public class FlowManageServiceImpl extends ServiceImpl<FlowManageMapper, FlowMan
if (dbFlowManage == null) {
return Result.fail(flowManage, "错误:流程不存在");
}
if (!dbFlowManage.getFlowKey().equals(flowManage.getFlowKey())) {
return Result.fail(flowManage, "错误:流程key不允许修改");
}
FlowJson flowJson = JSON.parseObject(flowManage.getFlowConfig(), FlowJson.class);
String hex = DigestUtils.sha256Hex(JSON.toJSONString(flowJson));
// 3. 判断流程配置是否修改,如果未修改则执行更新操作
@@ -312,7 +312,7 @@ public class FlowableServiceImpl implements FlowableService {
/**
* 判断用户是否为流程启动人
*
* @param businessKey 业务id
* @param businessKey 业务key
* @param userId 用户id
* @return 是否为启动人
*/
@@ -337,6 +337,32 @@ public class FlowableServiceImpl implements FlowableService {
return getProcessTasks(processInstanceId, false);
}
/**
* 根据业务key获取最新流程实例任务列表
* @param businessKey 业务key
* @return 任务列表
*/
@Override
public List<MfTask> getProcessTasksByBusinessKey(String businessKey) {
if (StringUtils.isEmpty(businessKey)) {
log.error("错误:业务id不能为空");
throw new MyRuntimeException("错误:业务id不能为空");
}
// 通过业务key查询流程实例,按开始时间降序排序,只取最新的一条
List<HistoricProcessInstance> hpiList = historyService.createHistoricProcessInstanceQuery()
.processInstanceBusinessKey(businessKey)
.orderByProcessInstanceStartTime().desc()
.listPage(0, 1);
if (hpiList == null || hpiList.isEmpty()) {
log.error("错误:流程实例不存在,业务id:{}", businessKey);
throw new MyRuntimeException("错误:流程实例不存在");
}
HistoricProcessInstance hpi = hpiList.get(0);
// 通过流程实例ID获取任务列表
return getProcessTasks(hpi.getId());
}
/**
* 查询流程实例图片
*
+9 -1
View File
@@ -57,9 +57,17 @@
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-nocode</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-demo</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-nocode-api</artifactId>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-demo-api</artifactId>
</dependency>
</dependencies>
</project>
</project>
@@ -0,0 +1,34 @@
package cn.com.mfish.common.api;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.demo.service.DemoLeaveApplyService;
import cn.com.mfish.demo.api.remote.RemoteDemoLeaveApplyService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @description: demo请假审批回调单体服务实现
* @author: mfish
* @date: 2026/4/18
*/
@Service("remoteDemoLeaveApplyService")
public class BootDemoLeaveApplyService implements RemoteDemoLeaveApplyService {
@Resource
private DemoLeaveApplyService demoLeaveApplyService;
@Override
public Result<String> approved(String origin, String prefix, String id, WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, 1, result);
}
@Override
public Result<String> rejected(String origin, String prefix, String id, WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, 2, result);
}
@Override
public Result<String> canceled(String origin, String prefix, String id, WorkflowCompleteResult result) {
return demoLeaveApplyService.audit(id, null, result);
}
}
@@ -71,6 +71,11 @@ public class BootWorkflowService implements RemoteWorkflowService {
return Result.ok(flowableService.getProcessTasks(processInstanceId), "查询流程实例任务列表成功");
}
@Override
public Result<List<MfTask>> getProcessTasksByBusinessKey(String origin, String businessKey) {
return Result.ok(flowableService.getProcessTasksByBusinessKey(businessKey), "查询流程实例任务列表成功");
}
@Override
public Result<PageResult<MfTask>> getPendingTasks(String origin, ReqTask reqTask, ReqPage reqPage) {
return Result.ok(flowableService.getPendingTasks(reqTask, reqPage), "查询待办任务列表成功");
@@ -14,6 +14,7 @@ public class ServiceConstants {
public static final String STORAGE_SERVICE = "mf-storage";
public static final String WORKFLOW_SERVICE = "mf-workflow";
public static final String NOCODE_SERVICE = "mf-nocode";
public static final String DEMO_SERVICE = "mf-demo";
public static boolean isBoot(String type) {
return ServiceConstants.SERVER_BOOT.equals(type);
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common</artifactId>
<version>mf-2.3.1</version>
</parent>
<artifactId>mf-common-demo</artifactId>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,66 @@
package cn.com.mfish.common.demo.entity;
import cn.com.mfish.common.core.entity.BaseEntity;
import cn.idev.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
* @description: leave apply demo
* @author: mfish
* @date: 2026-04-19
* @version: V2.3.1
*/
@Data
@TableName("demo_leave_apply")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "demo_leave_apply object")
public class DemoLeaveApply extends BaseEntity<String> {
@ExcelProperty("Unique ID")
@Schema(description = "Unique ID")
@TableId(type = IdType.ASSIGN_UUID)
@Accessors(chain = true)
private String id;
@ExcelProperty("Title")
@Schema(description = "Title")
private String title;
@ExcelProperty("Leave Type")
@Schema(description = "Leave Type 1 personal 2 sick 3 annual")
private Integer leaveType;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty("Start Time")
@Schema(description = "Start Time")
private Date startTime;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty("End Time")
@Schema(description = "End Time")
private Date endTime;
@ExcelProperty("Leave Days")
@Schema(description = "Leave Days")
private BigDecimal leaveDays;
@ExcelProperty("Reason")
@Schema(description = "Reason")
private String reason;
@ExcelProperty("Audit State")
@Schema(description = "Audit State -1 draft 0 pending 1 approved 2 rejected")
private Integer auditState;
}
@@ -0,0 +1,25 @@
package cn.com.mfish.common.demo.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @description: leave apply request
* @author: mfish
* @date: 2026-04-19
* @version: V2.3.1
*/
@Data
@Accessors(chain = true)
@Schema(description = "Leave apply query request")
public class ReqDemoLeaveApply {
@Schema(description = "Title")
private String title;
@Schema(description = "Leave Type")
private Integer leaveType;
@Schema(description = "Audit State")
private Integer auditState;
}
@@ -0,0 +1,40 @@
package cn.com.mfish.common.demo.service;
import cn.com.mfish.common.core.entity.WorkflowCompleteResult;
import cn.com.mfish.common.core.web.PageResult;
import cn.com.mfish.common.core.web.ReqPage;
import cn.com.mfish.common.core.web.Result;
import cn.com.mfish.common.demo.entity.DemoLeaveApply;
import cn.com.mfish.common.demo.req.ReqDemoLeaveApply;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.IOException;
/**
* @description: leave apply service
* @author: mfish
* @date: 2026-04-19
* @version: V2.3.1
*/
public interface DemoLeaveApplyService extends IService<DemoLeaveApply> {
Result<PageResult<DemoLeaveApply>> queryPageList(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage);
Result<DemoLeaveApply> add(DemoLeaveApply demoLeaveApply);
Result<DemoLeaveApply> edit(DemoLeaveApply demoLeaveApply);
Result<Boolean> delete(String id);
Result<Boolean> deleteBatch(String ids);
Result<DemoLeaveApply> queryById(String id);
void export(ReqDemoLeaveApply reqDemoLeaveApply, ReqPage reqPage) throws IOException;
Result<DemoLeaveApply> submit(String id);
Result<DemoLeaveApply> revoke(String id);
Result<String> audit(String id, Integer auditState, WorkflowCompleteResult result);
}
@@ -91,7 +91,7 @@ public interface FlowableService {
/**
* 判断是否是流程启动人
*
* @param businessKey 业务id
* @param businessKey 业务key
* @param userId 用户id
* @return 是否是启动人
*/
@@ -129,6 +129,13 @@ public interface FlowableService {
*/
List<MfTask> getProcessTasks(String processInstanceId);
/**
* 根据业务key获取最新流程实例任务列表
* @param businessKey 业务key
* @return 任务列表
*/
List<MfTask> getProcessTasksByBusinessKey(String businessKey);
/**
* 查询历史任务列表
*
+2 -1
View File
@@ -27,6 +27,7 @@
<module>mf-common-captcha</module>
<module>mf-common-code</module>
<module>mf-common-api</module>
<module>mf-common-demo</module>
<module>mf-common-sys</module>
<module>mf-common-web</module>
<module>mf-common-app</module>
@@ -37,4 +38,4 @@
<module>mf-common-prometheus</module>
</modules>
</project>
</project>
Binary file not shown.
Binary file not shown.
+10
View File
@@ -478,6 +478,16 @@
<artifactId>mf-workflow-api</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-demo-api</artifactId>
<version>${mfish.version}</version>
</dependency>
<dependency>
<groupId>cn.com.mfish</groupId>
<artifactId>mf-common-demo</artifactId>
<version>${mfish.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 私有库暂时停用,代码注销-->