This commit is contained in:
yuhaiming
2026-09-16 10:11:41 +08:00
commit 926426fe4f
901 changed files with 103701 additions and 0 deletions

27
ruoyi-modules/pom.xml Normal file
View File

@@ -0,0 +1,27 @@
<?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">
<parent>
<artifactId>bird_monitoring</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-modules</artifactId>
<packaging>pom</packaging>
<description>
ruoyi-modules 业务模块
</description>
<modules>
<module>ruoyi-demo</module>
<module>ruoyi-gen</module>
<module>ruoyi-job</module>
<module>ruoyi-system</module>
<module>ruoyi-workflow</module>
<module>ruoyi-ai</module>
</modules>
</project>

View File

@@ -0,0 +1,48 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-ai</artifactId>
<description>
ai模块
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
<!-- ai模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,73 @@
package org.dromara.ai.controller;
import com.aizuda.snail.ai.common.execption.SnailAiException;
import com.aizuda.snail.ai.common.model.Result;
import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserRegisterRequest;
import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserVO;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiUserClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.model.LoginUser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Snail AI OpenAPI 控制器
*
* @author opensnail
* @date 2026-04-25
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/snail-ai")
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "snail-ai.open-api", name = "enabled", havingValue = "true")
public class SnailAiController extends BaseController {
/**
* Snail AI 成功状态码。
*/
private static final int SNAIL_AI_SUCCESS = 1;
private final OpenApiUserClient userClient;
/**
* 注册当前登录用户并返回 OpenAPI 用户信息。
*/
@PostMapping("/user/register")
public R<OpenApiUserVO> registerCurrentUser() {
return R.ok(ensureOpenApiUser());
}
/**
* 确保当前登录用户已注册为 OpenAPI 用户。
*/
private OpenApiUserVO ensureOpenApiUser() {
Long userId = LoginHelper.getUserId();
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null || userId == null) {
throw new SnailAiException("当前登录用户为空");
}
OpenApiUserRegisterRequest registerRequest = new OpenApiUserRegisterRequest();
registerRequest.setExternalId(String.valueOf(userId));
registerRequest.setNickname(loginUser.getNickname());
Result<OpenApiUserVO> registerResult = userClient.register(registerRequest);
if (registerResult == null) {
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
}
if (registerResult.getStatus() != SNAIL_AI_SUCCESS) {
throw new SnailAiException(registerResult.getMessage());
}
if (registerResult.getData() == null) {
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
}
return registerResult.getData();
}
}

View File

@@ -0,0 +1,130 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-demo</artifactId>
<description>
demo模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<!-- excel -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<!-- 翻译模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-translation</artifactId>
</dependency>
<!-- 脱敏模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sensitive</artifactId>
</dependency>
<!-- 数据库加解密模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-encrypt</artifactId>
</dependency>
<!-- ES搜索引擎服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-elasticsearch</artifactId>
</dependency>
<!-- 消息推送模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-push</artifactId>
</dependency>
<!-- mqtt模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mqtt</artifactId>
</dependency>
<!-- mcp模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mcp</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,89 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.demo.domain.Document;
import org.dromara.demo.esmapper.DocumentMapper;
import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 搜索引擎 crud 演示案例
*
* @author Lion Li
*/
@ConditionalOnProperty(value = "easy-es.enable", havingValue = "true")
@RequiredArgsConstructor
@RestController
@RequestMapping("/es")
public class EsCrudController {
private final DocumentMapper documentMapper;
/**
* 查询(指定)
*
* @param title 标题
*/
@GetMapping("/select")
public Document select(String title) {
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.eq(Document::getTitle, title);
return documentMapper.selectOne(wrapper);
}
/**
* 搜索(模糊)
*
* @param key 搜索关键字
*/
@GetMapping("/search")
public List<Document> search(String key) {
LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>();
wrapper.like(Document::getTitle, key);
return documentMapper.selectList(wrapper);
}
/**
* 插入
*/
@PostMapping("/insert")
public Integer insert(@RequestBody Document document) {
return documentMapper.insert(document);
}
/**
* 更新
*/
@PutMapping("/update")
public R<Void> update(@RequestBody Document document) {
// 测试更新 更新有两种情况 分别演示如下:
// case1: 已知id, 根据id更新 (为了演示方便,此id是从上一步查询中复制过来的,实际业务可以自行查询)
documentMapper.updateById(document);
// case2: id未知, 根据条件更新
// LambdaEsUpdateWrapper<Document> wrapper = new LambdaEsUpdateWrapper<>();
// wrapper.like(Document::getTitle, document.getTitle());
// Document document2 = new Document();
// document2.setTitle(document.getTitle());
// document2.setContent(document.getContent());
// documentMapper.update(document2, wrapper);
return R.ok();
}
/**
* 删除
*
* @param id 主键
*/
@DeleteMapping("/delete/{id}")
public R<Integer> delete(@PathVariable String id) {
// 测试删除数据 删除有两种情况:根据id删或根据条件删
return R.ok(documentMapper.deleteById(id));
}
}

View File

@@ -0,0 +1,69 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mail.core.MailBuilder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.Arrays;
/**
* 邮件发送案例
*
* @author Michelle.Chung
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mail")
public class MailSendController {
/**
* 发送邮件
*
* @param to 接收人
* @param subject 标题
* @param text 内容
*/
@GetMapping("/sendSimpleMessage")
public R<Void> sendSimpleMessage(String to, String subject, String text) {
MailBuilder.of().to(to).subject(subject).text(text).send();
return R.ok();
}
/**
* 发送邮件(带附件)
*
* @param to 接收人
* @param subject 标题
* @param text 内容
*/
@GetMapping("/sendMessageWithAttachment")
public R<Void> sendMessageWithAttachment(String to, String subject, String text) {
// 附件路径 禁止前端传递 有任意读取系统文件风险
MailBuilder.of().to(to).subject(subject).text(text).files(new File("/xxx/xxx")).send();
return R.ok();
}
/**
* 发送邮件(多附件)
*
* @param to 接收人
* @param subject 标题
* @param text 内容
*/
@GetMapping("/sendMessageWithAttachments")
public R<Void> sendMessageWithAttachments(String to, String subject, String text) {
// 附件路径 禁止前端传递 有任意读取系统文件风险
String[] paths = new String[]{"/xxx/xxx", "/xxx/xxx"};
File[] array = Arrays.stream(paths).map(File::new).toArray(File[]::new);
MailBuilder.of().to(to).subject(subject).text(text).files(array).send();
return R.ok();
}
}

View File

@@ -0,0 +1,75 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mcp.core.McpResourceReadResult;
import org.dromara.demo.mcp.McpDemoClientService;
import org.dromara.demo.mcp.McpDemoClientService.McpDemoHandleResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* MCP Client 演示案例。
* <p>
* 这些接口不是 MCP 协议接口,只是为了在后台系统中手动触发 MCP Client 调用。
* 真实 MCP Server 入口由 Spring AI 挂载到 `/mcp`。
* <p>
* 调用示例:
* <ul>
* <li>`GET /demo/mcp/tools`:查看已连接 MCP Server 的工具列表</li>
* <li>`GET /demo/mcp/receive?toolName=demo_get_data&amp;id=1`:调用外部工具并模拟业务处理</li>
* <li>`GET /demo/mcp/resource?uri=demo://summary`:读取外部 MCP 资源</li>
* </ul>
* 当 `spring.ai.mcp.client.enabled=false` 时,接口会返回提示,不影响应用启动。
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mcp")
public class McpDemoController {
private final McpDemoClientService mcpDemoClientService;
/**
* 查询外部 MCP Server 工具列表。
*/
@GetMapping("/tools")
public R<Map<String, List<String>>> tools() {
try {
return R.ok(mcpDemoClientService.listRemoteTools());
} catch (IllegalStateException e) {
return R.fail(e.getMessage());
}
}
/**
* 调用外部 MCP 工具并模拟业务处理。
*/
@GetMapping("/receive")
public R<McpDemoHandleResult> receive(@RequestParam(defaultValue = "demo_get_data") String toolName,
@RequestParam(defaultValue = "1") String id) {
try {
return R.ok(mcpDemoClientService.receiveAndHandle(toolName, Map.of("id", id)));
} catch (IllegalStateException e) {
return R.fail(e.getMessage());
}
}
/**
* 读取外部 MCP 资源。
*/
@GetMapping("/resource")
public R<Map<String, McpResourceReadResult>> resource(@RequestParam(defaultValue = "demo://summary") String uri) {
try {
return R.ok(mcpDemoClientService.readRemoteResource(uri));
} catch (IllegalStateException e) {
return R.fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,106 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.demo.domain.TestDemo;
import org.dromara.mica.mqtt.codec.MqttQoS;
import org.dromara.mica.mqtt.codec.message.MqttPublishMessage;
import org.dromara.mica.mqtt.core.annotation.MqttClientSubscribe;
import org.dromara.mica.mqtt.core.deserialize.MqttJsonDeserializer;
import org.dromara.mica.mqtt.spring.client.MqttClientTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
/**
* mqtt 演示案例
* <p>
* 用法文档 <a href="https://mica-mqtt.dreamlu.net/guide/spring/client.html">...</a>
* 测试server搭建:
* 可执行下载其他mqtt服务端搭建
* 也可使用 mica自带的server搭建 <a href="https://mica-mqtt.dreamlu.net/guide/spring/server.html">...</a>
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mqtt")
@Slf4j
public class MqttController {
@Lazy
@Autowired
private MqttClientTemplate client;
/**
* 发布一条 MQTT 测试消息。
*
* @return 是否发送成功
*/
@GetMapping("/send")
public boolean send() {
client.publish("/test/client", "测试测试".getBytes(StandardCharsets.UTF_8));
return true;
}
/**
* 订阅 QoS0 测试主题并打印原始消息。
*
* @param topic 消息主题
* @param payload 消息内容
*/
@MqttClientSubscribe("/test/#")
public void subQos0(String topic, byte[] payload) {
log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8));
}
/**
* 订阅 QoS1 测试主题并打印原始消息。
*
* @param topic 消息主题
* @param payload 消息内容
*/
@MqttClientSubscribe(value = "/qos1/#", qos = MqttQoS.QOS1)
public void subQos1(String topic, byte[] payload) {
log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8));
}
/**
* 订阅带产品和设备占位符的注册主题。
*
* @param topic 消息主题
* @param payload 消息内容
*/
@MqttClientSubscribe("/sys/${productKey}/${deviceName}/thing/sub/register")
public void thingSubRegister(String topic, byte[] payload) {
// 1.3.8 开始支持,@MqttClientSubscribe 注解支持 ${} 变量替换,会默认替换成 +
// 注意:mica-mqtt 会先从 Spring boot 配置中替换参数 ${},如果存在配置会优先被替换。
log.info("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8));
}
/**
* 订阅 JSON 测试主题并演示自定义反序列化。
*
* @param topic 消息主题
* @param message MQTT 原始消息
* @param data 反序列化后的演示数据
*/
@MqttClientSubscribe(
value = "/test/json",
deserialize = MqttJsonDeserializer.class // 2.4.5 开始支持 自定义序列化,默认 json 序列化
)
public void testJson(String topic, MqttPublishMessage message, TestDemo data) {
// 2.4.5 开始支持,支持 2 到 3 个参数,字段类型映射规则如下
// String 字符串会默认映射到 topic,
// MqttPublishMessage 会默认映射到 原始的消息,可以拿到 mqtt5 的 props 参数
// byte[] 会映射到 mqtt 消息内容 payload
// ByteBuffer 会映射到 mqtt 消息内容 payload
// 其他类型会走序列化,确保消息能够序列化,默认为 json 序列化
log.info("topic:{} json data:{}", topic, data);
}
}

View File

@@ -0,0 +1,92 @@
package org.dromara.demo.controller;
import cn.hutool.core.thread.ThreadUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.domain.R;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
/**
* spring-cache 演示案例
*
* @author Lion Li
*/
// 类级别 缓存统一配置
//@CacheConfig(cacheNames = CacheNames.DEMO_CACHE)
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/cache")
public class RedisCacheController {
/**
* 测试 @Cacheable
* <p>
* 表示这个方法有了缓存的功能,方法的返回值会被缓存下来
* 下一次调用该方法前,会去检查是否缓存中已经有值
* 如果有就直接返回,不调用方法
* 如果没有,就调用方法,然后把结果缓存起来
* 这个注解「一般用在查询方法上」
* <p>
* 重点说明: 缓存注解严谨与其他筛选数据功能一起使用
* 例如: 数据权限注解 会造成 缓存击穿 与 数据不一致问题
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@Cacheable(cacheNames = "demo:cache#60s#10m#20#1", key = "#key", condition = "#key != null")
@GetMapping("/test1")
public R<String> test1(String key, String value) {
return R.data(value);
}
/**
* 测试 @CachePut
* <p>
* 加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用
* 它「通常用在新增或者实时更新方法上」
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@CachePut(cacheNames = CacheNames.DEMO_CACHE, key = "#key", condition = "#key != null")
@GetMapping("/test2")
public R<String> test2(String key, String value) {
return R.data(value);
}
/**
* 测试 @CacheEvict
* <p>
* 使用了CacheEvict注解的方法,会清空指定缓存
* 「一般用在删除的方法上」
* <p>
* cacheNames 命名规则 查看 {@link CacheNames} 注释 支持多参数
*/
@CacheEvict(cacheNames = CacheNames.DEMO_CACHE, key = "#key", condition = "#key != null")
@GetMapping("/test3")
public R<String> test3(String key, String value) {
return R.data(value);
}
/**
* 测试设置过期时间
* 手动设置过期时间10秒
* 11秒后获取 判断是否相等
*/
@GetMapping("/test6")
public R<Boolean> test6(String key, String value) {
RedisUtils.setCacheObject(key, value);
boolean flag = RedisUtils.expire(key, Duration.ofSeconds(10));
System.out.println("***********" + flag);
ThreadUtil.sleep(11 * 1000);
Object obj = RedisUtils.getCacheObject(key);
return R.ok(value.equals(obj));
}
}

View File

@@ -0,0 +1,64 @@
package org.dromara.demo.controller;
import cn.hutool.core.thread.ThreadUtil;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.lock.executor.RedissonLockExecutor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalTime;
/**
* 测试分布式锁的样例
*
* @author shenxinquan
*/
@Slf4j
@RestController
@RequestMapping("/demo/redisLock")
public class RedisLockController {
@Autowired
private LockTemplate lockTemplate;
/**
* 测试lock4j 注解
*/
@Lock4j(keys = {"#key"})
@GetMapping("/testLock4j")
public R<String> testLock4j(String key, String value) {
System.out.println("start:" + key + ",time:" + LocalTime.now());
ThreadUtil.sleep(10000);
System.out.println("end :" + key + ",time:" + LocalTime.now());
return R.data(value);
}
/**
* 测试lock4j 工具
*/
@GetMapping("/testLock4jLockTemplate")
public R<String> testLock4jLockTemplate(String key, String value) {
final LockInfo lockInfo = lockTemplate.lock(key, 30000L, 5000L, RedissonLockExecutor.class);
if (null == lockInfo) {
throw new RuntimeException("业务处理中,请稍后再试");
}
// 获取锁成功,处理业务
try {
ThreadUtil.sleep(8000);
System.out.println("执行简单方法1 , 当前线程:" + Thread.currentThread().getName());
} finally {
//释放锁
lockTemplate.releaseLock(lockInfo);
}
//结束
return R.data(value);
}
}

View File

@@ -0,0 +1,47 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Redis 发布订阅 演示案例
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/redis/pubsub")
public class RedisPubSubController {
/**
* 发布消息
*
* @param key 通道Key
* @param value 发送内容
*/
@GetMapping("/pub")
public R<Void> pub(String key, String value) {
RedisUtils.publish(key, value, consumer -> {
System.out.println("发布通道 => " + key + ", 发送值 => " + value);
});
return R.ok("操作成功");
}
/**
* 订阅消息
*
* @param key 通道Key
*/
@GetMapping("/sub")
public R<Void> sub(String key) {
RedisUtils.subscribe(key, String.class, msg -> {
System.out.println("订阅通道 => " + key + ", 接收值 => " + msg);
});
return R.ok("操作成功");
}
}

View File

@@ -0,0 +1,64 @@
package org.dromara.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.redis.enums.LimitType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试分布式限流样例
*
* @author Lion Li
*/
@Slf4j
@RestController
@RequestMapping("/demo/rateLimiter")
public class RedisRateLimiterController {
/**
* 测试全局限流
* 全局影响
*/
@RateLimiter(count = 2, time = 10)
@GetMapping("/test")
public R<String> test(String value) {
return R.data(value);
}
/**
* 测试请求IP限流
* 同一IP请求受影响
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.IP)
@GetMapping("/testip")
public R<String> testip(String value) {
return R.data(value);
}
/**
* 测试集群实例限流
* 启动两个后端服务互不影响
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.CLUSTER)
@GetMapping("/testcluster")
public R<String> testcluster(String value) {
return R.data(value);
}
/**
* 测试请求IP限流(key基于参数获取)
* 同一IP请求受影响
* <p>
* 简单变量获取 #变量 复杂表达式 #{#变量 != 1 ? 1 : 0}
*/
@RateLimiter(count = 2, time = 10, limitType = LimitType.IP, key = "#value")
@GetMapping("/testObj")
public R<String> testObj(String value) {
return R.data(value);
}
}

View File

@@ -0,0 +1,198 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.*;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* SaToken 权限测试 接口文档输出测试
*
* @author AprilWind
*/
@Slf4j
@RestController
@RequestMapping("/demo/saTokenDoc")
public class SaTokenTestController {
// ====================== 基础场景:单一校验规则 ======================
/**
* 场景1:仅登录校验(无角色/权限限制,只需登录态)
*/
@SaCheckLogin
@GetMapping("/basic/loginOnly")
public R<Void> loginOnly() {
log.info("【场景1】仅登录校验通过");
return R.ok("仅登录校验通过,无需角色/权限");
}
/**
* 场景2:单一角色校验(AND模式,默认)
*/
@SaCheckRole("admin")
@GetMapping("/basic/singleRole")
public R<Void> singleRole() {
log.info("【场景2】单一角色(admin)校验通过");
return R.ok("拥有admin角色,校验通过");
}
/**
* 场景3:单一权限校验(AND模式,默认)
*/
@SaCheckPermission("system:user:view")
@GetMapping("/basic/singlePermission")
public R<Void> singlePermission() {
log.info("【场景3】单一权限(system:user:view)校验通过");
return R.ok("拥有system:user:view权限,校验通过");
}
/**
* 场景4:忽略所有权限校验(SaIgnore优先级最高)
*/
@SaIgnore
@SaCheckRole("none_exist") // 该注解会被忽略
@GetMapping("/basic/ignoreAll")
public R<Void> ignoreAll() {
log.info("【场景4】SaIgnore忽略所有权限校验");
return R.ok("SaIgnore生效,所有权限校验被忽略");
}
// ====================== 进阶场景:多条件组合(AND/OR) ======================
/**
* 场景5:多角色AND模式(必须同时拥有所有角色)
*/
@SaCheckRole(value = {"admin", "operator"}, mode = SaMode.AND)
@GetMapping("/advance/multiRoleAnd")
public R<Void> multiRoleAnd() {
log.info("【场景5】多角色AND模式(admin+operator)校验通过");
return R.ok("同时拥有admin和operator角色,校验通过");
}
/**
* 场景6:多角色OR模式(拥有任一角色即可)
*/
@SaCheckRole(value = {"admin", "test"}, mode = SaMode.OR)
@GetMapping("/advance/multiRoleOr")
public R<Void> multiRoleOr() {
log.info("【场景6】多角色OR模式(admin|test)校验通过");
return R.ok("拥有admin或test角色,校验通过");
}
/**
* 场景7:多权限AND模式(必须同时拥有所有权限)
*/
@SaCheckPermission(value = {"system:user:edit", "system:log:view"}, mode = SaMode.AND)
@GetMapping("/advance/multiPermAnd")
public R<Void> multiPermAnd() {
log.info("【场景7】多权限AND模式(system:user:edit+system:log:view)校验通过");
return R.ok("同时拥有system:user:edit和system:log:view权限,校验通过");
}
/**
* 场景8:多权限OR模式(拥有任一权限即可)
*/
@SaCheckPermission(value = {"system:user:add", "system:user:delete"}, mode = SaMode.OR)
@GetMapping("/advance/multiPermOr")
public R<Void> multiPermOr() {
log.info("【场景8】多权限OR模式(system:user:add|system:user:delete)校验通过");
return R.ok("拥有system:user:add或system:user:delete权限,校验通过");
}
// ====================== 高级场景:通配符/混合组合 ======================
/**
* 场景9:权限通配符匹配(前缀匹配)
* 拥有system:user:* 即可匹配所有用户模块权限
*/
@SaCheckPermission("system:user:*")
@GetMapping("/advanced/permWildcardPrefix")
public R<Void> permWildcardPrefix() {
log.info("【场景9】权限通配符(system:user:*)校验通过");
return R.ok("拥有system:user:*前缀权限,校验通过");
}
/**
* 场景10:角色通配符匹配(前缀匹配)
* 拥有admin_* 即可匹配所有admin开头的角色
*/
@SaCheckRole("admin_*")
@GetMapping("/advanced/roleWildcardPrefix")
public R<Void> roleWildcardPrefix() {
log.info("【场景10】角色通配符(admin_*)校验通过");
return R.ok("拥有admin_*前缀角色,校验通过");
}
/**
* 场景11:权限+角色混合AND模式(所有条件必须满足)
* 需同时满足:拥有admin角色 + 拥有system:user:all权限
*/
@SaCheckRole("admin")
@SaCheckPermission("system:user:all")
@GetMapping("/advanced/mixRolePermAnd")
public R<Void> mixRolePermAnd() {
log.info("【场景11】角色+权限混合AND(admin+system:user:all)校验通过");
return R.ok("拥有admin角色且拥有system:user:all权限,校验通过");
}
/**
* 场景12:权限+角色混合OR模式(任一条件满足即可)
* 满足任一:拥有super_admin角色 | 拥有system:manage权限
*/
@SaCheckRole(value = {"super_admin"}, mode = SaMode.OR)
@SaCheckPermission(value = {"system:manage"}, mode = SaMode.OR)
@GetMapping("/advanced/mixRolePermOr")
public R<Void> mixRolePermOr() {
log.info("【场景12】角色+权限混合OR(super_admin|system:manage)校验通过");
return R.ok("拥有super_admin角色或system:manage权限,校验通过");
}
/**
* 场景13:orRole参数(权限校验失败时,兜底角色校验)
* 核心逻辑:无system:user:export权限时,检查是否有admin/operator角色
*/
@SaCheckPermission(value = "system:user:export", orRole = {"admin", "operator"})
@GetMapping("/advanced/permWithOrRole")
public R<Void> permWithOrRole() {
log.info("【场景13】权限+orRole兜底校验通过");
return R.ok("拥有system:user:export权限,或拥有admin/operator角色,校验通过");
}
// ====================== 特殊场景:临时权限/注解覆盖 ======================
/**
* 场景14:SaIgnore局部覆盖(方法注解覆盖类注解,若有)
* 假设类上有@SaCheckLogin,方法上@SaIgnore会覆盖
*/
@SaIgnore
@GetMapping("/special/ignoreOverride")
public R<Void> ignoreOverride() {
log.info("【场景14】SaIgnore覆盖类级别权限注解");
return R.ok("方法级SaIgnore覆盖类级别权限校验");
}
/**
* 场景15:临时权限校验(SaCheckPermission逻辑:临时权限>永久权限)
* 注:临时权限需通过SaToken API手动设置,如 SaHolder.getStpLogic().setTempPermission("system:temp:test")
*/
@SaCheckPermission("system:temp:test")
@GetMapping("/special/tempPermission")
public R<Void> tempPermission() {
log.info("【场景15】临时权限(system:temp:test)校验通过");
return R.ok("临时权限校验通过(需先通过API设置临时权限)");
}
/**
* 场景16:登录类型指定(多端登录场景,如PC/APP/小程序)
* 注:需配合SaToken多账号体系配置
*/
@SaCheckLogin(type = "PC") // 仅校验PC端的登录态
@GetMapping("/special/loginTypeSpecify")
public R<Void> loginTypeSpecify() {
log.info("【场景16】指定登录类型(PC)校验通过");
return R.ok("仅PC端登录态校验通过");
}
}

View File

@@ -0,0 +1,82 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
/**
* 短信演示案例
* 请先阅读文档 否则无法使用
*
* @author Lion Li
* @version 4.2.0
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/sms")
public class SmsController {
/**
* 发送短信Aliyun
*
* @param phones 电话号
* @param templateId 模板ID
*/
@GetMapping("/sendAliyun")
public R<Object> sendAliyun(String phones, String templateId) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", "1234");
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
return R.ok(smsResponse);
}
/**
* 发送短信Tencent
*
* @param phones 电话号
* @param templateId 模板ID
*/
@GetMapping("/sendTencent")
public R<Object> sendTencent(String phones, String templateId) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
// map.put("2", "测试测试");
map.put("1", "1234");
SmsBlend smsBlend = SmsFactory.getSmsBlend("config2");
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
return R.ok(smsResponse);
}
/**
* 添加黑名单
*
* @param phone 手机号
*/
@GetMapping("/addBlacklist")
public R<Object> addBlacklist(String phone) {
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
smsBlend.joinInBlacklist(phone);
return R.ok();
}
/**
* 移除黑名单
*
* @param phone 手机号
*/
@GetMapping("/removeBlacklist")
public R<Object> removeBlacklist(String phone) {
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
smsBlend.removeFromBlacklist(phone);
return R.ok();
}
}

View File

@@ -0,0 +1,31 @@
package org.dromara.demo.controller;
import org.dromara.common.core.domain.R;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* swagger3 用法示例
*
* @author Lion Li
*/
@RestController
@RequestMapping("/swagger/demo")
public class Swagger3DemoController {
/**
* 上传请求
* 必须使用 @RequestPart 注解标注为文件
*
* @param file 文件
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<String> upload(@RequestPart("file") MultipartFile file) {
return R.data(file.getOriginalFilename());
}
}

View File

@@ -0,0 +1,90 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.mapper.TestDemoMapper;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* 测试批量方法
*
* @author Lion Li
* @date 2021-05-30
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/batch")
public class TestBatchController extends BaseController {
/**
* 为了便于测试 直接引入mapper
*/
private final TestDemoMapper testDemoMapper;
/**
* 新增批量方法 可完美替代 saveBatch 秒级插入上万数据 (对mysql负荷较大)
* <p>
* 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
*/
@PostMapping("/add")
// @DS("slave")
public R<Void> add() {
List<TestDemo> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
TestDemo testDemo = new TestDemo();
testDemo.setOrderNum(-1);
testDemo.setTestKey("批量新增");
testDemo.setValue("测试新增");
list.add(testDemo);
}
return toAjax(testDemoMapper.insertBatch(list));
}
/**
* 新增或更新 可完美替代 saveOrUpdateBatch 高性能
* <p>
* 3.5.0 版本 增加 rewriteBatchedStatements=true 批处理参数 使 MP 原生批处理可以达到同样的速度
*/
@PostMapping("/addOrUpdate")
// @DS("slave")
public R<Void> addOrUpdate() {
List<TestDemo> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
TestDemo testDemo = new TestDemo();
testDemo.setOrderNum(-1);
testDemo.setTestKey("批量新增");
testDemo.setValue("测试新增");
list.add(testDemo);
}
testDemoMapper.insertBatch(list);
for (int i = 0; i < list.size(); i++) {
TestDemo testDemo = list.get(i);
testDemo.setTestKey("批量新增或修改");
testDemo.setValue("批量新增或修改");
if (i % 2 == 0) {
testDemo.setId(null);
}
}
return toAjax(testDemoMapper.insertOrUpdateBatch(list));
}
/**
* 删除批量方法
*/
@DeleteMapping()
// @DS("slave")
public R<Void> remove() {
return toAjax(testDemoMapper.lambda()
.eq(TestDemo::getOrderNum, -1L)
.deleteCount());
}
}

View File

@@ -0,0 +1,149 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.core.validate.QueryGroup;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.vo.TestDemoImportVo;
import org.dromara.demo.domain.vo.TestDemoVo;
import org.dromara.demo.service.ITestDemoService;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 测试单表Controller
*
* @author Lion Li
* @date 2021-07-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/demo")
public class TestDemoController extends BaseController {
private final ITestDemoService testDemoService;
/**
* 查询测试单表列表
*/
@SaCheckPermission("demo:demo:list")
@GetMapping("/list")
public R<PageResult<TestDemoVo>> list(@Validated(QueryGroup.class) TestDemoBo bo, PageQuery pageQuery) {
return R.ok(testDemoService.queryPageList(bo, pageQuery));
}
/**
* 自定义分页查询
*/
@SaCheckPermission("demo:demo:list")
@GetMapping("/page")
public R<PageResult<TestDemoVo>> page(@Validated(QueryGroup.class) TestDemoBo bo, PageQuery pageQuery) {
return R.ok(testDemoService.customPageList(bo, pageQuery));
}
/**
* 导入数据
*
* @param file 导入文件
*/
@Log(title = "测试单表", businessType = BusinessType.IMPORT)
@SaCheckPermission("demo:demo:import")
@PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Void> importData(@RequestPart("file") MultipartFile file) throws Exception {
ExcelResult<TestDemoImportVo> excelResult = ExcelBuilder.read(file.getInputStream(), TestDemoImportVo.class)
.validate(true)
.doRead();
List<TestDemo> list = MapstructUtils.convert(excelResult.getList(), TestDemo.class);
testDemoService.saveBatch(list);
return R.ok(excelResult.getAnalysis());
}
/**
* 导出测试单表列表
*/
@SaCheckPermission("demo:demo:export")
@Log(title = "测试单表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Validated TestDemoBo bo, HttpServletResponse response) {
List<TestDemoVo> list = testDemoService.queryList(bo);
// 测试雪花id导出
// for (TestDemoVo vo : list) {
// vo.setId(1234567891234567893L);
// }
ExcelBuilder.of(list, TestDemoVo.class).sheetName("测试单表").toResponse(response);
}
/**
* 获取测试单表详细信息
*
* @param id 测试ID
*/
@SaCheckPermission("demo:demo:query")
@GetMapping("/{id}")
public R<TestDemoVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("id") Long id) {
return R.ok(testDemoService.queryById(id));
}
/**
* 新增测试单表
*/
@SaCheckPermission("demo:demo:add")
@Log(title = "测试单表", businessType = BusinessType.INSERT)
@RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS, message = "{repeat.submit.message}")
@PostMapping()
public R<Void> add(@RequestBody TestDemoBo bo) {
// 使用校验工具对标 @Validated(AddGroup.class) 注解
// 用于在非 Controller 的地方校验对象
ValidatorUtils.validate(bo, AddGroup.class);
return toAjax(testDemoService.insertByBo(bo));
}
/**
* 修改测试单表
*/
@SaCheckPermission("demo:demo:edit")
@Log(title = "测试单表", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody TestDemoBo bo) {
return toAjax(testDemoService.updateByBo(bo));
}
/**
* 删除测试单表
*
* @param ids 测试ID串
*/
@SaCheckPermission("demo:demo:remove")
@Log(title = "测试单表", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(testDemoService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}

View File

@@ -0,0 +1,55 @@
package org.dromara.demo.controller;
import org.dromara.common.core.domain.R;
import org.dromara.demo.domain.TestDemoEncrypt;
import org.dromara.demo.mapper.TestDemoEncryptMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 测试数据库加解密功能
*
* @author Lion Li
*/
@Validated
@RestController
@RequestMapping("/demo/encrypt")
public class TestEncryptController {
@Autowired
private TestDemoEncryptMapper mapper;
@Value("${mybatis-encryptor.enable}")
private Boolean encryptEnable;
/**
* 测试数据库加解密
*
* @param key 测试key
* @param value 测试value
*/
@GetMapping()
public R<Map<String, TestDemoEncrypt>> test(String key, String value) {
if (!encryptEnable) {
throw new RuntimeException("加密功能未开启!");
}
Map<String, TestDemoEncrypt> map = new HashMap<>(2);
TestDemoEncrypt demo = new TestDemoEncrypt();
demo.setTestKey(key);
demo.setValue(value);
mapper.insert(demo);
map.put("加密", demo);
TestDemoEncrypt testDemo = mapper.selectById(demo.getId());
map.put("解密", testDemo);
return R.ok(map);
}
}

View File

@@ -0,0 +1,211 @@
package org.dromara.demo.controller;
import cn.hutool.core.collection.CollUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.demo.domain.vo.ExportDemoVo;
import org.dromara.demo.listener.ExportDemoListener;
import org.dromara.demo.service.IExportExcelService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 测试Excel功能
*
* @author Lion Li
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/excel")
public class TestExcelController {
private final IExportExcelService exportExcelService;
/**
* 单列表多数据
*/
@GetMapping("/exportTemplateOne")
public void exportTemplateOne(HttpServletResponse response) {
Map<String, String> map = new HashMap<>();
map.put("title", "单列表多数据");
map.put("test1", "数据测试1");
map.put("test2", "数据测试2");
map.put("test3", "数据测试3");
map.put("test4", "数据测试4");
map.put("testTest", "666");
List<TestObj> list = new ArrayList<>();
list.add(new TestObj("单列表测试1", "列表测试1", "列表测试2", "列表测试3", "列表测试4"));
list.add(new TestObj("单列表测试2", "列表测试5", "列表测试6", "列表测试7", "列表测试8"));
list.add(new TestObj("单列表测试3", "列表测试9", "列表测试10", "列表测试11", "列表测试12"));
ExcelBuilder.template("excel/单列表.xlsx")
.filename("单列表.xlsx")
.data(CollUtil.newArrayList(map, list))
.toResponse(response);
}
/**
* 多列表多数据
*/
@GetMapping("/exportTemplateMuliti")
public void exportTemplateMuliti(HttpServletResponse response) {
Map<String, String> map = new HashMap<>();
map.put("title1", "标题1");
map.put("title2", "标题2");
map.put("title3", "标题3");
map.put("title4", "标题4");
map.put("author", "Lion Li");
List<TestObj1> list1 = new ArrayList<>();
list1.add(new TestObj1("list1测试1", "list1测试2", "list1测试3"));
list1.add(new TestObj1("list1测试4", "list1测试5", "list1测试6"));
list1.add(new TestObj1("list1测试7", "list1测试8", "list1测试9"));
List<TestObj1> list2 = new ArrayList<>();
list2.add(new TestObj1("list2测试1", "list2测试2", "list2测试3"));
list2.add(new TestObj1("list2测试4", "list2测试5", "list2测试6"));
List<TestObj1> list3 = new ArrayList<>();
list3.add(new TestObj1("list3测试1", "list3测试2", "list3测试3"));
List<TestObj1> list4 = new ArrayList<>();
list4.add(new TestObj1("list4测试1", "list4测试2", "list4测试3"));
list4.add(new TestObj1("list4测试4", "list4测试5", "list4测试6"));
list4.add(new TestObj1("list4测试7", "list4测试8", "list4测试9"));
list4.add(new TestObj1("list4测试10", "list4测试11", "list4测试12"));
Map<String, Object> multiListMap = new HashMap<>();
multiListMap.put("map", map);
multiListMap.put("data1", list1);
multiListMap.put("data2", list2);
multiListMap.put("data3", list3);
multiListMap.put("data4", list4);
ExcelBuilder.template("excel/多列表.xlsx")
.filename("多列表.xlsx")
.multiList(multiListMap)
.toResponse(response);
}
/**
* 导出下拉框
*
* @param response /
*/
@GetMapping("/exportWithOptions")
public void exportWithOptions(HttpServletResponse response) {
exportExcelService.exportWithOptions(response);
}
/**
* 自定义导出
*
* @param response /
*/
@GetMapping("/customExport")
public void customExport(HttpServletResponse response) {
exportExcelService.customExport(response);
}
/**
* 多个sheet导出
*/
@GetMapping("/exportTemplateMultiSheet")
public void exportTemplateMultiSheet(HttpServletResponse response) {
List<TestObj1> list1 = new ArrayList<>();
list1.add(new TestObj1("list1测试1", "list1测试2", "list1测试3"));
list1.add(new TestObj1("list1测试4", "list1测试5", "list1测试6"));
List<TestObj1> list2 = new ArrayList<>();
list2.add(new TestObj1("list2测试1", "list2测试2", "list2测试3"));
list2.add(new TestObj1("list2测试4", "list2测试5", "list2测试6"));
List<TestObj1> list3 = new ArrayList<>();
list3.add(new TestObj1("list3测试1", "list3测试2", "list3测试3"));
list3.add(new TestObj1("list3测试4", "list3测试5", "list3测试6"));
List<TestObj1> list4 = new ArrayList<>();
list4.add(new TestObj1("list4测试1", "list4测试2", "list4测试3"));
list4.add(new TestObj1("list4测试4", "list4测试5", "list4测试6"));
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> sheetMap1 = new HashMap<>();
sheetMap1.put("data1", list1);
Map<String, Object> sheetMap2 = new HashMap<>();
sheetMap2.put("data2", list2);
Map<String, Object> sheetMap3 = new HashMap<>();
sheetMap3.put("data3", list3);
Map<String, Object> sheetMap4 = new HashMap<>();
sheetMap4.put("data4", list4);
list.add(sheetMap1);
list.add(sheetMap2);
list.add(sheetMap3);
list.add(sheetMap4);
ExcelBuilder.template("excel/多sheet列表.xlsx")
.filename("多sheet列表")
.multiSheet(list)
.toResponse(response);
}
/**
* 导入表格
*/
@PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<ExportDemoVo> importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
// 处理解析结果
ExcelResult<ExportDemoVo> excelResult = ExcelBuilder.read(file.getInputStream(), ExportDemoVo.class)
.listener(new ExportDemoListener())
.doRead();
return excelResult.getList();
}
/**
* 多列表模板填充测试对象。
*/
@Data
@AllArgsConstructor
static class TestObj1 {
/**
* 测试字段1。
*/
private String test1;
/**
* 测试字段2。
*/
private String test2;
/**
* 测试字段3。
*/
private String test3;
}
/**
* 单列表模板填充测试对象。
*/
@Data
@AllArgsConstructor
static class TestObj {
/**
* 名称。
*/
private String name;
/**
* 列表字段1。
*/
private String list1;
/**
* 列表字段2。
*/
private String list2;
/**
* 列表字段3。
*/
private String list3;
/**
* 列表字段4。
*/
private String list4;
}
}

View File

@@ -0,0 +1,79 @@
package org.dromara.demo.controller;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.MessageUtils;
import org.hibernate.validator.constraints.Range;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试国际化
*
* @author Lion Li
*/
@Validated
@RestController
@RequestMapping("/demo/i18n")
public class TestI18nController {
/**
* 通过code获取国际化内容
* code为 messages.properties 中的 key
* <p>
* 测试使用 user.register.success
*
* @param code 国际化code
*/
@GetMapping()
public R<Void> get(String code) {
return R.ok(MessageUtils.message(code));
}
/**
* Validator 校验国际化
* 不传值 分别查看异常返回
* <p>
* 测试使用 not.null
*/
@GetMapping("/test1")
public R<Void> test1(@NotBlank(message = "{not.null}") String str) {
return R.ok(str);
}
/**
* Bean 校验国际化
* 不传值 分别查看异常返回
* <p>
* 测试使用 not.null
*/
@GetMapping("/test2")
public R<TestI18nBo> test2(@Validated TestI18nBo bo) {
return R.ok(bo);
}
/**
* 国际化 Bean 校验测试对象。
*/
@Data
public static class TestI18nBo {
/**
* 名称。
*/
@NotBlank(message = "{not.null}")
private String name;
/**
* 年龄。
*/
@NotNull(message = "{not.null}")
@Range(min = 0, max = 100, message = "{length.not.valid}")
private Integer age;
}
}

View File

@@ -0,0 +1,79 @@
package org.dromara.demo.controller;
import lombok.Data;
import org.dromara.common.core.domain.R;
import org.dromara.common.sensitive.annotation.Sensitive;
import org.dromara.common.sensitive.core.SensitiveService;
import org.dromara.common.sensitive.core.SensitiveStrategy;
import org.dromara.common.web.core.BaseController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试数据脱敏控制器
* <p>
* 默认管理员不过滤
* 需自行根据业务重写实现
*
* @author Lion Li
* @version 3.6.0
* @see SensitiveService
*/
@RestController
@RequestMapping("/demo/sensitive")
public class TestSensitiveController extends BaseController {
/**
* 测试数据脱敏
*/
@GetMapping("/test")
public R<TestSensitive> test() {
TestSensitive testSensitive = new TestSensitive();
testSensitive.setIdCard("210397198608215431");
testSensitive.setPhone("17640125371");
testSensitive.setAddress("北京市朝阳区某某四合院1203室");
testSensitive.setEmail("17640125371@163.com");
testSensitive.setBankCard("6226456952351452853");
return R.ok(testSensitive);
}
/**
* 脱敏测试对象。
*/
@Data
static class TestSensitive {
/**
* 身份证
*/
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
/**
* 电话
*/
@Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = "common")
private String phone;
/**
* 地址
*/
@Sensitive(strategy = SensitiveStrategy.ADDRESS, perms = "system:user:query")
private String address;
/**
* 邮箱
*/
@Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = "common", perms = "system:user:query1")
private String email;
/**
* 银行卡
*/
@Sensitive(strategy = SensitiveStrategy.BANK_CARD, roleKey = "common1", perms = "system:user:query")
private String bankCard;
}
}

View File

@@ -0,0 +1,107 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.core.validate.QueryGroup;
import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.demo.domain.bo.TestTreeBo;
import org.dromara.demo.domain.vo.TestTreeVo;
import org.dromara.demo.service.ITestTreeService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 测试树表Controller
*
* @author Lion Li
* @date 2021-07-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/tree")
public class TestTreeController extends BaseController {
private final ITestTreeService testTreeService;
/**
* 查询测试树表列表
*/
@SaCheckPermission("demo:tree:list")
@GetMapping("/list")
public R<List<TestTreeVo>> list(@Validated(QueryGroup.class) TestTreeBo bo) {
List<TestTreeVo> list = testTreeService.queryList(bo);
return R.ok(list);
}
/**
* 导出测试树表列表
*/
@SaCheckPermission("demo:tree:export")
@Log(title = "测试树表", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public void export(@Validated TestTreeBo bo, HttpServletResponse response) {
List<TestTreeVo> list = testTreeService.queryList(bo);
ExcelBuilder.of(list, TestTreeVo.class).sheetName("测试树表").toResponse(response);
}
/**
* 获取测试树表详细信息
*
* @param id 测试树ID
*/
@SaCheckPermission("demo:tree:query")
@GetMapping("/{id}")
public R<TestTreeVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable("id") Long id) {
return R.ok(testTreeService.queryById(id));
}
/**
* 新增测试树表
*/
@SaCheckPermission("demo:tree:add")
@Log(title = "测试树表", businessType = BusinessType.INSERT)
@RepeatSubmit
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody TestTreeBo bo) {
return toAjax(testTreeService.insertByBo(bo));
}
/**
* 修改测试树表
*/
@SaCheckPermission("demo:tree:edit")
@Log(title = "测试树表", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody TestTreeBo bo) {
return toAjax(testTreeService.updateByBo(bo));
}
/**
* 删除测试树表
*
* @param ids 测试树ID串
*/
@SaCheckPermission("demo:tree:remove")
@Log(title = "测试树表", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(testTreeService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}

View File

@@ -0,0 +1,50 @@
package org.dromara.demo.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.enums.PushSourceEnum;
import org.dromara.common.core.enums.PushTypeEnum;
import org.dromara.system.api.MessageService;
import org.dromara.system.api.domain.PushPayloadDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* WebSocket 演示案例
*
* @author zendwang
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/websocket")
@Slf4j
public class WebSocketController {
private final MessageService messageService;
/**
* 发布消息
*
* @param userId 目标用户
* @param message 发送内容
*/
@GetMapping("/send")
public R<Void> send(Long userId, String message) {
PushPayloadDTO payload = PushPayloadDTO.of(
PushTypeEnum.MESSAGE,
PushSourceEnum.BACKEND,
message,
null
);
if (userId == null) {
messageService.publishAll(payload);
} else {
messageService.publishMessage(List.of(userId), payload);
}
return R.ok("操作成功");
}
}

View File

@@ -0,0 +1 @@
package org.dromara.demo.controller;

View File

@@ -0,0 +1,35 @@
package org.dromara.demo.controller.queue;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 实体类 注意不允许使用内部类 否则会找不到类
*
* @author Lion Li
* @version 3.6.0
*/
@Data
@NoArgsConstructor
public class PriorityDemo implements Comparable<PriorityDemo> {
/**
* 队列元素名称。
*/
private String name;
/**
* 优先级排序值。
*/
private Integer orderNum;
/**
* 按排序值比较优先级。
*
* @param other 另一个队列元素
* @return 比较结果
*/
@Override
public int compareTo(PriorityDemo other) {
return Integer.compare(getOrderNum(), other.getOrderNum());
}
}

View File

@@ -0,0 +1,89 @@
package org.dromara.demo.controller.queue;
import cn.hutool.core.util.RandomUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.redis.utils.QueueUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 优先队列 演示案例
* <p>
* 轻量级队列 重量级数据量 请使用 MQ
* <p>
* 集群测试通过 同一个消息只会被消费一次 做好事务补偿
* 集群测试流程 在其中一台发送数据 两端分别调用获取接口 一次获取一条
*
* @author Lion Li
* @version 3.6.0
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/queue/priority")
public class PriorityQueueController {
/**
* 添加队列数据
*
* @param queueName 队列名
*/
@GetMapping("/add")
public R<Void> add(String queueName) {
// 用完了一定要销毁 否则会一直存在
boolean b = QueueUtils.destroyPriorityQueue(queueName);
log.info("通道: {} , 删除: {}", queueName, b);
for (int i = 0; i < 10; i++) {
int randomNum = RandomUtil.randomInt(10);
PriorityDemo data = new PriorityDemo();
data.setName("data-" + i);
data.setOrderNum(randomNum);
if (QueueUtils.addPriorityQueueObject(queueName, data)) {
log.info("通道: {} , 发送数据: {}", queueName, data);
} else {
log.info("通道: {} , 发送数据: {}, 发送失败", queueName, data);
}
}
return R.ok("操作成功");
}
/**
* 删除队列数据
*
* @param queueName 队列名
* @param name 对象名
* @param orderNum 排序号
*/
@GetMapping("/remove")
public R<Void> remove(String queueName, String name, Integer orderNum) {
PriorityDemo data = new PriorityDemo();
data.setName(name);
data.setOrderNum(orderNum);
if (QueueUtils.removePriorityQueueObject(queueName, data)) {
log.info("通道: {} , 删除数据: {}", queueName, data);
} else {
return R.fail("操作失败");
}
return R.ok("操作成功");
}
/**
* 获取队列数据
*
* @param queueName 队列名
*/
@GetMapping("/get")
public R<Void> get(String queueName) {
PriorityDemo data;
do {
data = QueueUtils.getPriorityQueueObject(queueName);
log.info("通道: {} , 获取数据: {}", queueName, data);
} while (data != null);
return R.ok("操作成功");
}
}

View File

@@ -0,0 +1,26 @@
package org.dromara.demo.domain;
import lombok.Data;
/**
* 文档实体
*/
@Data
public class Document {
/**
* es中的唯一id
*/
private String id;
/**
* 文档标题
*/
private String title;
/**
* 文档内容
*/
private String content;
}

View File

@@ -0,0 +1,68 @@
package org.dromara.demo.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 测试单表对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_demo")
public class TestDemo extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 部门id
*/
private Long deptId;
/**
* 用户id
*/
private Long userId;
/**
* 排序号
*/
@OrderBy(asc = false, sort = 1)
private Integer orderNum;
/**
* key键
*/
private String testKey;
/**
* 值
*/
private String value;
/**
* 版本
*/
@Version
private Long version;
/**
* 删除标志
*/
@TableLogic
private Long delFlag;
}

View File

@@ -0,0 +1,34 @@
package org.dromara.demo.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.dromara.common.encrypt.enums.AlgorithmType;
/**
* 测试加密字段实体。
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_demo")
public class TestDemoEncrypt extends TestDemo {
/**
* key键
*/
// @EncryptField(algorithm=AlgorithmType.SM2, privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgZSlOvw8FBiH+aFJWLYZP/VRjg9wjfRarTkGBZd/T3N+gCgYIKoEcz1UBgi2hRANCAAR5DGuQwJqkxnbCsP+iPSDoHWIF4RwcR5EsSvT8QPxO1wRkR2IhCkzvRb32x2CUgJFdvoqVqfApFDPZzShqzBwX", publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEeQxrkMCapMZ2wrD/oj0g6B1iBeEcHEeRLEr0/ED8TtcEZEdiIQpM70W99sdglICRXb6KlanwKRQz2c0oaswcFw==")
@EncryptField(algorithm = AlgorithmType.RSA, privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANBBEeueWlXlkkj2+WY5l+IWe42d8b5K28g+G/CFKC/yYAEHtqGlCsBOrb+YBkG9mPzmuYA/n9k0NFIc8E8yY5vZQaroyFBrTTWEzG9RY2f7Y3svVyybs6jpXSUs4xff8abo7wL1Y/wUaeatTViamxYnyTvdTmLm3d+JjRij68rxAgMBAAECgYAB0TnhXraSopwIVRfmboea1b0upl+BUdTJcmci412UjrKr5aE695ZLPkXbFXijVu7HJlyyv94NVUdaMACV7Ku/S2RuNB70M7YJm8rAjHFC3/i2ZeIM60h1Ziy4QKv0XM3pRATlDCDNhC1WUrtQCQSgU8kcp6eUUppruOqDzcY04QJBAPm9+sBP9CwDRgy3e5+V8aZtJkwDstb0lVVV/KY890cydVxiCwvX3fqVnxKMlb+x0YtH0sb9v+71xvK2lGobaRECQQDVePU6r/cCEfpc+nkWF6osAH1f8Mux3rYv2DoBGvaPzV2BGfsLed4neRfCwWNCKvGPCdW+L0xMJg8+RwaoBUPhAkAT5kViqXxFPYWJYd1h2+rDXhMdH3ZSlm6HvDBDdrwlWinr0Iwcx3iSjPV93uHXwm118aUj4fg3LDJMCKxOwBxhAkByrQXfvwOMYygBprRBf/j0plazoWFrbd6lGR0f1uI5IfNnFRPdeFw1DEINZ2Hw+6zEUF44SqRMC+4IYJNc02dBAkBCgy7RvfyV/A7N6kKXxTHauY0v6XwSSvpeKtRJkbIcRWOdIYvaHO9L7cklj3vIEdwjSUp9K4VTBYYlmAz1xh03", publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQQRHrnlpV5ZJI9vlmOZfiFnuNnfG+StvIPhvwhSgv8mABB7ahpQrATq2/mAZBvZj85rmAP5/ZNDRSHPBPMmOb2UGq6MhQa001hMxvUWNn+2N7L1csm7Oo6V0lLOMX3/Gm6O8C9WP8FGnmrU1YmpsWJ8k73U5i5t3fiY0Yo+vK8QIDAQAB")
private String testKey;
/**
* 值
*/
// @EncryptField // 什么也不写走默认yml配置
// @EncryptField(algorithm = AlgorithmType.SM4, password = "10rfylhtccpuyke5")
@EncryptField(algorithm = AlgorithmType.AES, password = "10rfylhtccpuyke5")
private String value;
}

View File

@@ -0,0 +1,65 @@
package org.dromara.demo.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 测试树表对象 test_tree
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_tree")
public class TestTree extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 父ID
*/
private Long parentId;
/**
* 部门id
*/
private Long deptId;
/**
* 用户id
*/
private Long userId;
/**
* 树节点名
*/
private String treeName;
/**
* 版本
*/
@Version
private Long version;
/**
* 删除标志
*/
@TableLogic
private Long delFlag;
}

View File

@@ -0,0 +1,69 @@
package org.dromara.demo.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.demo.domain.TestDemo;
import java.io.Serial;
import java.io.Serializable;
/**
* 测试单表业务对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@AutoMapper(target = TestDemo.class, reverseConvertGenerate = false)
public class TestDemoBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long id;
/**
* 部门id
*/
@NotNull(message = "部门id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long deptId;
/**
* 用户id
*/
@NotNull(message = "用户id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long userId;
/**
* 排序号
*/
@NotNull(message = "排序号不能为空", groups = {AddGroup.class, EditGroup.class})
private Integer orderNum;
/**
* key键
*/
@NotBlank(message = "key键不能为空", groups = {AddGroup.class, EditGroup.class})
private String testKey;
/**
* 值
*/
@NotBlank(message = "值不能为空", groups = {AddGroup.class, EditGroup.class})
private String value;
/**
* 版本
*/
private Long version;
}

View File

@@ -0,0 +1,57 @@
package org.dromara.demo.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.demo.domain.TestTree;
import java.io.Serial;
import java.io.Serializable;
/**
* 测试树表业务对象 test_tree
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@AutoMapper(target = TestTree.class, reverseConvertGenerate = false)
public class TestTreeBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long id;
/**
* 父ID
*/
private Long parentId;
/**
* 部门id
*/
@NotNull(message = "部门id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long deptId;
/**
* 用户id
*/
@NotNull(message = "用户id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long userId;
/**
* 树节点名
*/
@NotBlank(message = "树节点名不能为空", groups = {AddGroup.class, EditGroup.class})
private String treeName;
}

View File

@@ -0,0 +1 @@
package org.dromara.demo.domain;

View File

@@ -0,0 +1,122 @@
package org.dromara.demo.domain.vo;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelEnumFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.common.excel.convert.ExcelEnumConvert;
import java.io.Serial;
import java.io.Serializable;
/**
* 带有下拉选的Excel导出
*
* @author Emil.Zhang
*/
@Data
@ExcelIgnoreUnannotated
@AllArgsConstructor
@NoArgsConstructor
public class ExportDemoVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户昵称
*/
@ExcelProperty(value = "用户昵称", index = 0)
@NotEmpty(message = "用户昵称不能为空", groups = AddGroup.class)
private String nickName;
/**
* 用户类型
* </p>
* 使用ExcelEnumFormat注解需要进行下拉选的部分
*/
@ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
@ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
@NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
private String userStatus;
/**
* 性别
* <p>
* 使用ExcelDictFormat注解需要进行下拉选的部分
*/
@ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_user_gender")
@NotEmpty(message = "性别不能为空", groups = AddGroup.class)
private String gender;
/**
* 手机号
*/
@ExcelProperty(value = "手机号", index = 3)
@NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
private String phoneNumber;
/**
* Email
*/
@ExcelProperty(value = "Email", index = 4)
@NotEmpty(message = "Email不能为空", groups = AddGroup.class)
private String email;
/**
* 省
* <p>
* 级联下拉,仅判断是否选了
*/
@ExcelProperty(value = "省", index = 5)
@NotNull(message = "省不能为空", groups = AddGroup.class)
private String province;
/**
* 数据库中的省ID
* </p>
* 处理完毕后再判断是否市正确的值
*/
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
private Integer provinceId;
/**
* 市
* <p>
* 级联下拉
*/
@ExcelProperty(value = "市", index = 6)
@NotNull(message = "市不能为空", groups = AddGroup.class)
private String city;
/**
* 数据库中的市ID
*/
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
private Integer cityId;
/**
* 县
* <p>
* 级联下拉
*/
@ExcelProperty(value = "县", index = 7)
@NotNull(message = "县不能为空", groups = AddGroup.class)
private String area;
/**
* 数据库中的县ID
*/
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
private Integer areaId;
}

View File

@@ -0,0 +1,55 @@
package org.dromara.demo.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.demo.domain.TestDemo;
/**
* 测试单表业务对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@AutoMapper(target = TestDemo.class)
public class TestDemoImportVo {
/**
* 部门id
*/
@NotNull(message = "部门id不能为空")
@ExcelProperty(value = "部门id")
private Long deptId;
/**
* 用户id
*/
@NotNull(message = "用户id不能为空")
@ExcelProperty(value = "用户id")
private Long userId;
/**
* 排序号
*/
@NotNull(message = "排序号不能为空")
@ExcelProperty(value = "排序号")
private Long orderNum;
/**
* key键
*/
@NotBlank(message = "key键不能为空")
@ExcelProperty(value = "key键")
private String testKey;
/**
* 值
*/
@NotBlank(message = "值不能为空")
@ExcelProperty(value = "值")
private String value;
}

View File

@@ -0,0 +1,119 @@
package org.dromara.demo.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.annotation.ExcelRequired;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.demo.domain.TestDemo;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 测试单表视图对象 test_demo
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = TestDemo.class)
public class TestDemoVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@ExcelProperty(value = "主键")
private Long id;
/**
* 部门id
*/
@ExcelRequired
@ExcelProperty(value = "部门id")
private Long deptId;
/**
* 用户id
*/
@ExcelRequired
@ExcelProperty(value = "用户id", index = 5)
private Long userId;
/**
* 排序号
*/
@ExcelRequired
@ExcelProperty(value = "排序号")
private Integer orderNum;
/**
* key键
*/
@ExcelNotation(value = "测试key")
@ExcelProperty(value = "key键")
private String testKey;
/**
* 值
*/
@ExcelNotation(value = "测试value")
@ExcelProperty(value = "值")
private String value;
/**
* 创建时间
*/
@ExcelRequired
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
/**
* 创建人
*/
@ExcelProperty(value = "创建人")
private Long createBy;
/**
* 创建人账号
*/
@Translation(type = TransConstant.USER_ID_TO_NAME, mapper = "createBy")
@ExcelProperty(value = "创建人账号")
private String createByName;
/**
* 更新时间
*/
@ExcelProperty(value = "更新时间")
private LocalDateTime updateTime;
/**
* 更新人
*/
@ExcelProperty(value = "更新人")
private Long updateBy;
/**
* 更新人账号
*/
@Translation(type = TransConstant.USER_ID_TO_NAME, mapper = "updateBy")
@ExcelProperty(value = "更新人账号")
private String updateByName;
/**
* 版本
*/
private Long version;
}

View File

@@ -0,0 +1,64 @@
package org.dromara.demo.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.demo.domain.TestTree;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 测试树表视图对象 test_tree
*
* @author Lion Li
* @date 2021-07-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = TestTree.class)
public class TestTreeVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 父id
*/
@ExcelProperty(value = "父id")
private Long parentId;
/**
* 部门id
*/
@ExcelProperty(value = "部门id")
private Long deptId;
/**
* 用户id
*/
@ExcelProperty(value = "用户id")
private Long userId;
/**
* 树节点名
*/
@ExcelProperty(value = "树节点名")
private String treeName;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,12 @@
package org.dromara.demo.esmapper;
import org.dromara.demo.domain.Document;
import org.dromara.easyes.core.kernel.BaseEsMapper;
/**
* Easy-Es 文档 Mapper。
*
* @author Lion Li
*/
public interface DocumentMapper extends BaseEsMapper<Document> {
}

View File

@@ -0,0 +1,77 @@
package org.dromara.demo.listener;
import cn.hutool.core.util.NumberUtil;
import org.apache.fesod.sheet.context.AnalysisContext;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.core.DefaultExcelListener;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.demo.domain.vo.ExportDemoVo;
import java.util.List;
/**
* Excel带下拉框的解析处理器
*
* @author Emil.Zhang
*/
public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
/**
* 创建下拉框导入解析监听器。
*/
public ExportDemoListener() {
// 显示使用构造函数,否则将导致空指针
super(true);
}
/**
* 解析并校验一行下拉框演示数据。
*
* @param data 行数据
* @param context 解析上下文
*/
@Override
public void invoke(ExportDemoVo data, AnalysisContext context) {
// 先校验必填
ValidatorUtils.validate(data, AddGroup.class);
// 处理级联下拉的部分
String province = data.getProvince();
String city = data.getCity();
String area = data.getArea();
// 本行用户选择的省
List<String> thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
if (thisRowSelectedProvinceOption.size() == 2) {
String provinceIdStr = thisRowSelectedProvinceOption.get(1);
if (NumberUtil.isNumber(provinceIdStr)) {
// 严格要求数据的话可以在这里做与数据库相关的判断
// 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
data.setProvinceId(Integer.parseInt(provinceIdStr));
}
}
// 本行用户选择的市
List<String> thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
if (thisRowSelectedCityOption.size() == 2) {
String cityIdStr = thisRowSelectedCityOption.get(1);
if (NumberUtil.isNumber(cityIdStr)) {
data.setCityId(Integer.parseInt(cityIdStr));
}
}
// 本行用户选择的县
List<String> thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
if (thisRowSelectedAreaOption.size() == 2) {
String areaIdStr = thisRowSelectedAreaOption.get(1);
if (NumberUtil.isNumber(areaIdStr)) {
data.setAreaId(Integer.parseInt(areaIdStr));
}
}
// 处理完毕以后判断是否符合规则
ValidatorUtils.validate(data, EditGroup.class);
// 添加到处理结果中
getExcelResult().getList().add(data);
}
}

View File

@@ -0,0 +1,13 @@
package org.dromara.demo.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.demo.domain.TestDemoEncrypt;
/**
* 测试加密功能
*
* @author Lion Li
*/
public interface TestDemoEncryptMapper extends BaseMapperPlus<TestDemoEncrypt, TestDemoEncrypt> {
}

View File

@@ -0,0 +1,96 @@
package org.dromara.demo.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.vo.TestDemoVo;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 测试单表Mapper接口
*
* @author Lion Li
* @date 2021-07-26
*/
public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
/**
* 自定义分页查询演示数据。
*
* @param page 分页对象
* @param wrapper 查询条件
* @return 分页结果
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
Page<TestDemoVo> customPageList(@Param("page") Page<TestDemo> page, @Param("ew") Wrapper<TestDemo> wrapper);
/**
* 分页查询演示 VO 列表并应用数据权限。
*
* @param page 分页对象
* @param wrapper 查询条件
* @return 分页结果
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
default <P extends IPage<TestDemoVo>> P selectVoPage(IPage<TestDemo> page, Wrapper<TestDemo> wrapper) {
return selectVoPage(page, wrapper, this.currentVoClass());
}
/**
* 查询演示 VO 列表并应用数据权限。
*
* @param wrapper 查询条件
* @return 演示 VO 列表
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
default List<TestDemoVo> selectVoList(Wrapper<TestDemo> wrapper) {
return selectVoList(wrapper, this.currentVoClass());
}
/**
* 按主键集合查询演示数据并应用数据权限。
*
* @param idList 主键集合
* @return 演示数据列表
*/
@Override
@DataPermission(value = {
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
}, joinStr = "AND")
List<TestDemo> selectByIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
/**
* 按主键更新演示数据并应用数据权限。
*
* @param entity 演示数据实体
* @return 更新行数
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int updateById(@Param(Constants.ENTITY) TestDemo entity);
}

View File

@@ -0,0 +1,21 @@
package org.dromara.demo.mapper;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.demo.domain.TestTree;
import org.dromara.demo.domain.vo.TestTreeVo;
/**
* 测试树表Mapper接口
*
* @author Lion Li
* @date 2021-07-26
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
public interface TestTreeMapper extends BaseMapperPlus<TestTree, TestTreeVo> {
}

View File

@@ -0,0 +1 @@
package org.dromara.demo.mapper;

View File

@@ -0,0 +1,58 @@
package org.dromara.demo.mcp;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.mcp.annotation.McpElicitation;
import org.springframework.ai.mcp.annotation.McpSampling;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* MCP Client 能力回调演示。
* <p>
* `@McpSampling` 与 `@McpElicitation` 不是对外提供给 MCP Client 调用的工具,
* 而是 MCP Server 在处理请求时,反向请求 MCP Client 进行模型采样或补充信息时的回调处理器。
* <p>
* 这里提供最小示例,主要用于展示注解写法,并让 Spring AI MCP 注解扫描器能扫描到
* sampling/elicitation 方法,避免启动时输出 "No sampling methods found" 与
* "No elicitation methods found" 提示。
* <p>
* Spring AI 2.0.0-M6 要求 `clients` 不能为空。这里使用 `ruoyi-demo` 作为示例客户端名称,
* 实际使用时应与 `spring.ai.mcp.client.*.connections` 中的连接名保持一致。
*
* @author Lion Li
*/
@Component
public class McpDemoClientHandlers {
/**
* MCP sampling 回调示例。
*
* @param request 采样请求
* @return 采样结果
*/
@McpSampling(clients = "ruoyi-demo")
public McpSchema.CreateMessageResult sampling(McpSchema.CreateMessageRequest request) {
String text = request.systemPrompt() == null ? "demo sampling response" : request.systemPrompt();
return new McpSchema.CreateMessageResult(
McpSchema.Role.ASSISTANT,
new McpSchema.TextContent(text),
"ruoyi-demo-sampling",
McpSchema.CreateMessageResult.StopReason.END_TURN
);
}
/**
* MCP elicitation 回调示例。
*
* @param request 补充信息请求
* @return 补充信息结果
*/
@McpElicitation(clients = "ruoyi-demo")
public McpSchema.ElicitResult elicitation(McpSchema.ElicitRequest request) {
return new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.ACCEPT,
Map.of("message", request.message())
);
}
}

View File

@@ -0,0 +1,104 @@
package org.dromara.demo.mcp;
import lombok.RequiredArgsConstructor;
import org.dromara.common.mcp.core.McpClientTemplate;
import org.dromara.common.mcp.core.McpResourceReadResult;
import org.dromara.common.mcp.core.McpToolCallResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* MCP Client 演示服务,从外部 MCP Server 接收数据后交给业务层处理。
* <p>
* 本服务会注入 Spring AI 自动创建的多个 `McpSyncClient`。当
* `spring.ai.mcp.client.enabled=true` 且配置了多个 connection 时,
* 这里可以统一遍历这些 MCP Server。
* 未启用 MCP Client 时,本服务仍会注册,避免演示 Controller 影响应用启动。
* <p>
* 如果要用当前项目自己连自己测试,建议启动两个 `ruoyi-admin` 实例:
* 一个作为 server,一个作为 client,client 指向 server 的地址。
*
* @author Lion Li
*/
@Service
@RequiredArgsConstructor
public class McpDemoClientService {
private final ObjectProvider<McpClientTemplate> mcpClientTemplateProvider;
/**
* 查询已接入的 MCP Server 与工具列表。
*
* @return Server 名称与工具名称列表
*/
public Map<String, List<String>> listRemoteTools() {
return execute(McpClientTemplate::listTools);
}
/**
* 调用外部 MCP 工具接收数据。
*
* @param toolName 工具名称
* @param arguments 工具参数
* @return 工具返回内容
*/
public Map<String, McpToolCallResult> callRemoteTool(String toolName, Map<String, Object> arguments) {
return execute(template -> template.callTool(toolName, arguments));
}
/**
* 读取外部 MCP 资源。
*
* @param uri 资源地址
* @return 资源内容
*/
public Map<String, McpResourceReadResult> readRemoteResource(String uri) {
return execute(template -> template.readResource(uri));
}
/**
* 模拟业务处理入口。真实业务可在这里做校验、转换、去重、入库和审计。
* <p>
* MCP 返回数据不建议直接入库,应先转换成业务 BO/DTO,再进入业务 Service。
*
* @param toolName 工具名称
* @param arguments 工具参数
* @return 处理结果
*/
public McpDemoHandleResult receiveAndHandle(String toolName, Map<String, Object> arguments) {
return new McpDemoHandleResult("MCP", true, callRemoteTool(toolName, arguments));
}
/**
* 执行 MCP Client 操作。
*
* @param action MCP Client 操作
* @param <T> 返回值类型
* @return 操作结果
*/
private <T> T execute(Function<McpClientTemplate, T> action) {
McpClientTemplate template = mcpClientTemplateProvider.getIfAvailable();
if (template == null) {
throw new IllegalStateException("MCP Client 未启用,请配置 spring.ai.mcp.client.enabled=true 并设置 connections");
}
return action.apply(template);
}
/**
* MCP 数据处理结果。
*
* @param sourceType 数据来源类型
* @param handled 是否已处理
* @param data MCP 原始返回数据
*/
public record McpDemoHandleResult(
String sourceType,
boolean handled,
Map<String, McpToolCallResult> data
) {
}
}

View File

@@ -0,0 +1,85 @@
package org.dromara.demo.mcp;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* MCP Server 演示工具,对外提供业务数据。
* <p>
* 启动 `ruoyi-admin` 后,本类中的 `@McpTool` 与 `@McpResource` 会被 Spring AI
* MCP 注解扫描器注册到当前应用的 MCP Server,默认通过同端口 `/mcp` 暴露。
* <p>
* 可被外部 MCP Client 调用的示例能力:
* <ul>
* <li>`demo_get_data`:根据 id 查询一条演示数据</li>
* <li>`demo_list_data`:按 limit 查询演示数据列表</li>
* <li>`demo://summary`:读取演示模块摘要资源</li>
* </ul>
* 真实业务中不要在 MCP 工具里直接访问 Mapper,应继续调用业务 Service,
* 并保留权限、租户、脱敏、审计和幂等规则。
*
* @author Lion Li
*/
@Component
public class McpDemoServerTool {
/**
* 查询一条演示数据。
*
* @param id 业务主键
* @return 演示数据
*/
@McpTool(name = "demo_get_data", description = "根据业务主键查询演示数据")
public McpDemoData getData(@McpToolParam(description = "业务主键") String id) {
return new McpDemoData(id, "演示数据-" + id, "ruoyi-demo", 100, LocalDateTime.now());
}
/**
* 查询演示数据列表。
*
* @param limit 返回条数
* @return 演示数据列表
*/
@McpTool(name = "demo_list_data", description = "查询演示数据列表")
public List<McpDemoData> listData(@McpToolParam(description = "返回条数") Integer limit) {
int size = limit == null || limit < 1 ? 3 : Math.min(limit, 20);
return java.util.stream.IntStream.rangeClosed(1, size)
.mapToObj(index -> new McpDemoData(String.valueOf(index), "演示数据-" + index, "ruoyi-demo", index * 10, LocalDateTime.now()))
.toList();
}
/**
* 读取演示资源。
*
* @return 演示资源内容
*/
@McpResource(
name = "demo_summary",
uri = "demo://summary",
description = "演示模块摘要资源",
mimeType = "text/plain"
)
public String summary() {
return "ruoyi-demo MCP resource summary";
}
/**
* MCP 演示数据。
*
* @author Lion Li
*/
public record McpDemoData(
String id,
String name,
String source,
Integer amount,
LocalDateTime syncTime
) {
}
}

View File

@@ -0,0 +1,25 @@
package org.dromara.demo.service;
import jakarta.servlet.http.HttpServletResponse;
/**
* 导出下拉框Excel示例
*
* @author Emil.Zhang
*/
public interface IExportExcelService {
/**
* 导出下拉框
*
* @param response /
*/
void exportWithOptions(HttpServletResponse response);
/**
* 自定义导出
*
* @param response /
*/
void customExport(HttpServletResponse response);
}

View File

@@ -0,0 +1,86 @@
package org.dromara.demo.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.vo.TestDemoVo;
import java.util.Collection;
import java.util.List;
/**
* 测试单表Service接口
*
* @author Lion Li
* @date 2021-07-26
*/
public interface ITestDemoService {
/**
* 查询单个
*
* @param id 主键
* @return 测试单表视图对象
*/
TestDemoVo queryById(Long id);
/**
* 分页查询测试单表列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 分页结果
*/
PageResult<TestDemoVo> queryPageList(TestDemoBo bo, PageQuery pageQuery);
/**
* 按自定义 SQL 分页查询测试单表列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 分页结果
*/
PageResult<TestDemoVo> customPageList(TestDemoBo bo, PageQuery pageQuery);
/**
* 查询符合条件的测试单表列表。
*
* @param bo 查询条件
* @return 结果列表
*/
List<TestDemoVo> queryList(TestDemoBo bo);
/**
* 根据新增业务对象插入测试单表
*
* @param bo 测试单表新增业务对象
* @return 是否新增成功
*/
Boolean insertByBo(TestDemoBo bo);
/**
* 根据编辑业务对象修改测试单表
*
* @param bo 测试单表编辑业务对象
* @return 是否修改成功
*/
Boolean updateByBo(TestDemoBo bo);
/**
* 校验并删除数据
*
* @param ids 主键集合
* @param isValid 是否校验,true-删除前校验,false-不校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 批量保存测试单表数据。
*
* @param list 待保存数据
* @return 是否保存成功
*/
Boolean saveBatch(List<TestDemo> list);
}

View File

@@ -0,0 +1,56 @@
package org.dromara.demo.service;
import org.dromara.demo.domain.bo.TestTreeBo;
import org.dromara.demo.domain.vo.TestTreeVo;
import java.util.Collection;
import java.util.List;
/**
* 测试树表Service接口
*
* @author Lion Li
* @date 2021-07-26
*/
public interface ITestTreeService {
/**
* 查询单个
*
* @param id 主键
* @return 测试树表视图对象
*/
TestTreeVo queryById(Long id);
/**
* 查询符合条件的测试树表列表。
*
* @param bo 查询条件
* @return 结果列表
*/
List<TestTreeVo> queryList(TestTreeBo bo);
/**
* 根据新增业务对象插入测试树表
*
* @param bo 测试树表新增业务对象
* @return 是否新增成功
*/
Boolean insertByBo(TestTreeBo bo);
/**
* 根据编辑业务对象修改测试树表
*
* @param bo 测试树表编辑业务对象
* @return 是否修改成功
*/
Boolean updateByBo(TestTreeBo bo);
/**
* 校验并删除数据
*
* @param ids 主键集合
* @param isValid 是否校验,true-删除前校验,false-不校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -0,0 +1,317 @@
package org.dromara.demo.service.impl;
import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.apache.fesod.sheet.write.metadata.WriteSheet;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.common.excel.utils.ExcelWriterWrapper;
import org.dromara.demo.domain.vo.ExportDemoVo;
import org.dromara.demo.service.IExportExcelService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 导出下拉框Excel示例
*
* @author Emil.Zhang
*/
@Service
@RequiredArgsConstructor
public class ExportExcelServiceImpl implements IExportExcelService {
/**
* 导出带下拉框选项的 Excel 示例文件。
*
* @param response HTTP 响应
*/
@Override
public void exportWithOptions(HttpServletResponse response) {
// 创建表格数据,业务中一般通过数据库查询
List<ExportDemoVo> excelDataList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
// 模拟数据库中的一条数据
ExportDemoVo everyRowData = new ExportDemoVo();
everyRowData.setNickName("用户-" + i);
everyRowData.setUserStatus(SystemConstants.NORMAL);
everyRowData.setGender("1");
everyRowData.setPhoneNumber(String.format("175%08d", i));
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
everyRowData.setProvinceId(i);
everyRowData.setCityId(i);
everyRowData.setAreaId(i);
excelDataList.add(everyRowData);
}
// 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
// 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
// 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
// 首先从数据库中查询下拉框内的可选项
// 这里模拟查询结果
List<DemoCityData> provinceList = getProvinceList(),
cityList = getCityList(provinceList),
areaList = getAreaList(cityList);
int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
provinceList,
provinceIndex,
cityList,
cityIndex,
DemoCityData::getId,
DemoCityData::getPid,
everyOptions -> DropDownOptions.createOptionValue(
everyOptions.getName(),
everyOptions.getId()
)
);
DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
cityList,
cityIndex,
areaList,
areaIndex,
DemoCityData::getId,
DemoCityData::getPid,
everyOptions -> DropDownOptions.createOptionValue(
everyOptions.getName(),
everyOptions.getId()
)
);
// 把所有的下拉框存储
List<DropDownOptions> options = new ArrayList<>();
options.add(provinceToCity);
options.add(cityToArea);
// 到此为止所有的下拉框可选项已全部配置完毕
// 接下来需要将Excel中的展示数据转换为对应的下拉选
List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
// 只需要处理没有使用@ExcelDictFormat注解的下拉框
// 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
return everyRowData;
});
ExcelBuilder.of(outList, ExportDemoVo.class)
.sheetName("下拉框示例")
.options(options)
.toResponse(response);
}
/**
* 构建下拉框显示值。
*
* @param cityDataList 城市演示数据列表
* @param id 选项 id
* @return 下拉框显示值
*/
private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
Map<Integer, List<DemoCityData>> groupByIdMap =
StreamUtils.groupByKey(cityDataList, DemoCityData::getId);
if (groupByIdMap.containsKey(id)) {
DemoCityData demoCityData = groupByIdMap.get(id).getFirst();
return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
} else {
return StringUtils.EMPTY;
}
}
/**
* 模拟查询数据库操作
*
* @return /
*/
private List<DemoCityData> getProvinceList() {
List<DemoCityData> provinceList = new ArrayList<>();
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
provinceList.add(new DemoCityData(0, null, "P100000"));
provinceList.add(new DemoCityData(1, null, "P200000"));
provinceList.add(new DemoCityData(2, null, "P300000"));
return provinceList;
}
/**
* 模拟查找数据库操作,需要连带查询出省的数据
*
* @param provinceList 模拟的父省数据
* @return /
*/
private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
List<DemoCityData> cityList = new ArrayList<>();
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
cityList.add(new DemoCityData(0, 0, "C110000"));
cityList.add(new DemoCityData(1, 0, "C120000"));
cityList.add(new DemoCityData(2, 1, "C210000"));
cityList.add(new DemoCityData(3, 1, "C220000"));
cityList.add(new DemoCityData(4, 1, "C230000"));
selectParentData(provinceList, cityList);
return cityList;
}
/**
* 模拟查找数据库操作,需要连带查询出市的数据
*
* @param cityList 模拟的父市数据
* @return /
*/
private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
List<DemoCityData> areaList = new ArrayList<>();
int minCount = 500;
int maxCount = 10000;
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
for (int i = 0; i < RandomUtil.randomInt(minCount, maxCount); i++) {
areaList.add(new DemoCityData(areaList.size(), 0, String.format("A11%04d", i)));
}
for (int i = 0; i < RandomUtil.randomInt(minCount, maxCount); i++) {
areaList.add(new DemoCityData(areaList.size(), 1, String.format("A12%04d", i)));
}
for (int i = 0; i < RandomUtil.randomInt(minCount, maxCount); i++) {
areaList.add(new DemoCityData(areaList.size(), 2, String.format("A21%04d", i)));
}
for (int i = 0; i < RandomUtil.randomInt(minCount, maxCount); i++) {
areaList.add(new DemoCityData(areaList.size(), 3, String.format("A22%04d", i)));
}
for (int i = 0; i < RandomUtil.randomInt(minCount, maxCount); i++) {
areaList.add(new DemoCityData(areaList.size(), 4, String.format("A23%04d", i)));
}
selectParentData(cityList, areaList);
return areaList;
}
/**
* 模拟数据库的查询父数据操作
*
* @param parentList /
* @param sonList /
*/
private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
Map<Integer, List<DemoCityData>> parentGroupByIdMap =
StreamUtils.groupByKey(parentList, DemoCityData::getId);
sonList.forEach(everySon -> {
if (parentGroupByIdMap.containsKey(everySon.getPid())) {
everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).getFirst());
}
});
}
/**
* 模拟的数据库省市县
*/
@Data
private static class DemoCityData {
/**
* 数据库id字段
*/
private Integer id;
/**
* 数据库pid字段
*/
private Integer pid;
/**
* 数据库name字段
*/
private String name;
/**
* MyBatisPlus连带查询父数据
*/
private DemoCityData pData;
/**
* 构造城市演示数据。
*
* @param id 数据库 id
* @param pid 父级 id
* @param name 名称
*/
public DemoCityData(Integer id, Integer pid, String name) {
this.id = id;
this.pid = pid;
this.name = name;
}
}
/**
* 自定义写入多个工作表。
*
* @param response HTTP 响应
*/
@Override
public void customExport(HttpServletResponse response) {
ExcelBuilder.writer(ExportDemoVo.class).sheetName("自定义导出").toResponse(response, wrapper -> {
// 创建表格数据,业务中一般通过数据库查询
List<ExportDemoVo> excelDataList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
// 模拟数据库中的一条数据
ExportDemoVo everyRowData = new ExportDemoVo();
everyRowData.setNickName("用户-" + i);
everyRowData.setUserStatus(SystemConstants.NORMAL);
everyRowData.setGender("1");
everyRowData.setPhoneNumber(String.format("175%08d", i));
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
everyRowData.setProvinceId(i);
everyRowData.setCityId(i);
everyRowData.setAreaId(i);
excelDataList.add(everyRowData);
}
// 创建表格
WriteSheet sheet = ExcelWriterWrapper.sheetBuilder("自定义导出demo")
// 合并单元格
// .registerWriteHandler(new CellMergeStrategy(excelDataList, true))
.build();
wrapper.write(excelDataList, sheet);
List<ExportDemoVo> excelDataList2 = new ArrayList<>();
for (int i = 0; i < 20; i++) {
int index = 1000 + i;
// 模拟数据库中的一条数据
ExportDemoVo everyRowData = new ExportDemoVo();
everyRowData.setNickName("用户-" + index);
everyRowData.setUserStatus(SystemConstants.NORMAL);
everyRowData.setGender("1");
everyRowData.setPhoneNumber(String.format("175%08d", index));
everyRowData.setEmail(String.format("175%08d", index) + "@163.com");
everyRowData.setProvinceId(index);
everyRowData.setCityId(index);
everyRowData.setAreaId(index);
excelDataList2.add(everyRowData);
}
wrapper.write(excelDataList2, sheet);
// 或者在同一个excel中创建多个表格
// WriteSheet sheet2 = ExcelWriterWrapper.sheetBuilder("自定义导出demo2").build();
// wrapper.write(excelDataList2, sheet2);
});
}
}

View File

@@ -0,0 +1,168 @@
package org.dromara.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.vo.TestDemoVo;
import org.dromara.demo.mapper.TestDemoMapper;
import org.dromara.demo.service.ITestDemoService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 测试单表Service业务层处理
*
* @author Lion Li
* @date 2021-07-26
*/
@RequiredArgsConstructor
@Service
public class TestDemoServiceImpl implements ITestDemoService {
private final TestDemoMapper demoMapper;
/**
* 根据主键查询测试单表详情。
*
* @param id 主键
* @return 测试单表视图对象
*/
@Override
public TestDemoVo queryById(Long id) {
return demoMapper.selectVoById(id);
}
/**
* 分页查询测试单表列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 分页结果
*/
@Override
public PageResult<TestDemoVo> queryPageList(TestDemoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestDemo> lqw = buildQueryWrapper(bo);
Page<TestDemoVo> result = demoMapper.selectVoPage(pageQuery.build(), lqw);
return PageResult.build(result.getRecords(), result.getTotal());
}
/**
* 通过自定义 SQL 分页查询测试单表列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 分页结果
*/
@Override
public PageResult<TestDemoVo> customPageList(TestDemoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestDemo> lqw = buildQueryWrapper(bo);
Page<TestDemoVo> result = demoMapper.customPageList(pageQuery.build(), lqw);
return PageResult.build(result.getRecords(), result.getTotal());
}
/**
* 查询符合条件的测试单表列表。
*
* @param bo 查询条件
* @return 结果列表
*/
@Override
public List<TestDemoVo> queryList(TestDemoBo bo) {
return demoMapper.selectVoList(buildQueryWrapper(bo));
}
/**
* 构建测试单表动态查询条件。
*
* @param bo 查询条件
* @return 查询条件包装器
*/
private LambdaQueryWrapper<TestDemo> buildQueryWrapper(TestDemoBo bo) {
LambdaQueryWrapper<TestDemo> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getDeptId() != null, TestDemo::getDeptId, bo.getDeptId());
lqw.eq(bo.getUserId() != null, TestDemo::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getTestKey()), TestDemo::getTestKey, bo.getTestKey());
lqw.eq(StringUtils.isNotBlank(bo.getValue()), TestDemo::getValue, bo.getValue());
lqw.orderByAsc(TestDemo::getId);
return lqw;
}
/**
* 新增测试单表数据。
*
* @param bo 新增业务对象
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(TestDemoBo bo) {
TestDemo add = MapstructUtils.convert(bo, TestDemo.class);
validEntityBeforeSave(add);
boolean flag = demoMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 更新测试单表数据。
*
* @param bo 编辑业务对象
* @return 是否更新成功
*/
@Override
public Boolean updateByBo(TestDemoBo bo) {
TestDemo update = MapstructUtils.convert(bo, TestDemo.class);
validEntityBeforeSave(update);
return demoMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*
* @param entity 实体类数据
*/
private void validEntityBeforeSave(TestDemo entity) {
//TODO 做一些数据校验,如唯一约束
}
/**
* 按主键集合删除测试单表数据,并按需执行删除前校验。
*
* @param ids 主键集合
* @param isValid 是否执行删除校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 做一些业务上的校验,判断是否需要校验
List<TestDemo> list = demoMapper.selectByIds(ids);
if (list.size() != ids.size()) {
throw new ServiceException("您没有删除权限!");
}
}
return demoMapper.deleteByIds(ids) > 0;
}
/**
* 批量保存测试单表数据。
*
* @param list 待保存实体列表
* @return 是否保存成功
*/
@Override
public Boolean saveBatch(List<TestDemo> list) {
return demoMapper.insertBatch(list);
}
}

View File

@@ -0,0 +1,123 @@
package org.dromara.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.demo.domain.TestTree;
import org.dromara.demo.domain.bo.TestTreeBo;
import org.dromara.demo.domain.vo.TestTreeVo;
import org.dromara.demo.mapper.TestTreeMapper;
import org.dromara.demo.service.ITestTreeService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 测试树表Service业务层处理
*
* @author Lion Li
* @date 2021-07-26
*/
// @DS("slave") // 切换从库查询
@RequiredArgsConstructor
@Service
public class TestTreeServiceImpl implements ITestTreeService {
private final TestTreeMapper treeMapper;
/**
* 根据主键查询测试树表详情。
*
* @param id 主键
* @return 测试树表视图对象
*/
@Override
public TestTreeVo queryById(Long id) {
return treeMapper.selectVoById(id);
}
/**
* 查询符合条件的测试树表列表。
*
* @param bo 查询条件
* @return 结果列表
*/
// @DS("slave") // 切换从库查询
@Override
public List<TestTreeVo> queryList(TestTreeBo bo) {
LambdaQueryWrapper<TestTree> lqw = buildQueryWrapper(bo);
return treeMapper.selectVoList(lqw);
}
/**
* 构建测试树表动态查询条件。
*
* @param bo 查询条件
* @return 查询条件包装器
*/
private LambdaQueryWrapper<TestTree> buildQueryWrapper(TestTreeBo bo) {
LambdaQueryWrapper<TestTree> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getDeptId() != null, TestTree::getDeptId, bo.getDeptId());
lqw.eq(bo.getUserId() != null, TestTree::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getTreeName()), TestTree::getTreeName, bo.getTreeName());
lqw.orderByAsc(TestTree::getId);
return lqw;
}
/**
* 新增测试树表数据。
*
* @param bo 新增业务对象
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(TestTreeBo bo) {
TestTree add = MapstructUtils.convert(bo, TestTree.class);
validEntityBeforeSave(add);
boolean flag = treeMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 更新测试树表数据。
*
* @param bo 编辑业务对象
* @return 是否更新成功
*/
@Override
public Boolean updateByBo(TestTreeBo bo) {
TestTree update = MapstructUtils.convert(bo, TestTree.class);
validEntityBeforeSave(update);
return treeMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*
* @param entity 实体类数据
*/
private void validEntityBeforeSave(TestTree entity) {
//TODO 做一些数据校验,如唯一约束
}
/**
* 按主键集合删除测试树表数据,并按需执行删除前校验。
*
* @param ids 主键集合
* @param isValid 是否执行删除校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
}
return treeMapper.deleteByIds(ids) > 0;
}
}

View File

@@ -0,0 +1 @@
package org.dromara.demo.service.impl;

View File

@@ -0,0 +1 @@
package org.dromara.demo.service;

View File

@@ -0,0 +1,11 @@
<?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="org.dromara.demo.mapper.TestDemoMapper">
<select id="customPageList" resultType="org.dromara.demo.domain.vo.TestDemoVo">
SELECT * FROM test_demo ${ew.customSqlSegment}
</select>
</mapper>

View File

@@ -0,0 +1,7 @@
<?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="org.dromara.demo.mapper.TestTreeMapper">
</mapper>

View File

@@ -0,0 +1,3 @@
java包使用 `.` 分割 resource 目录使用 `/` 分割
<br>
此文件目的 防止文件夹粘连找不到 `xml` 文件

View File

@@ -0,0 +1,90 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-gen</artifactId>
<description>
generator 代码生成
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<!-- FreeMarker代码生成模板 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- AnyLine Spring JDBC 环境 -->
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-environment-spring-data-jdbc</artifactId>
<version>${anyline.version}</version>
</dependency>
<!-- AnyLine MySQL 驱动 -->
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-mysql</artifactId>
<version>${anyline.version}</version>
</dependency>
<!-- anyline支持100+种类型数据库 添加对应的jdbc依赖与anyline对应数据库依赖包即可 -->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-oracle</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-postgresql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-mssql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@@ -0,0 +1,111 @@
package org.dromara.gen.config;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.anyline.data.datasource.DataSourceMonitor;
import org.anyline.data.runtime.DataRuntime;
import org.anyline.util.ConfigTable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.HashMap;
import java.util.Map;
/**
* anyline 适配 动态数据源改造
*
* @author Lion Li
*/
@Slf4j
@Component
public class MyBatisDataSourceMonitor implements DataSourceMonitor {
/**
* 初始化 anyline 与动态数据源联动所需的元数据解析策略。
*/
public MyBatisDataSourceMonitor() {
// 调整执行模式为自定义
ConfigTable.KEEP_ADAPTER = 2;
// 禁用缓存
ConfigTable.METADATA_CACHE_SCOPE = 0;
}
private final Map<String, String> features = new HashMap<>();
/**
* 数据源特征 用来定准 adapter 包含数据库或JDBC协议关键字<br/>
* 一般会通过 产品名_url 合成 如果返回null 上层方法会通过driver_产品名_url合成
*
* @param datasource 数据源
* @return String 返回null由上层自动提取
*/
@Override
public String feature(DataRuntime runtime, Object datasource) {
String feature = null;
if (datasource instanceof JdbcTemplate jdbc) {
DataSource ds = jdbc.getDataSource();
if (ds instanceof DynamicRoutingDataSource) {
String key = DynamicDataSourceContextHolder.peek();
feature = features.get(key);
if (null == feature) {
Connection con = null;
try {
con = DataSourceUtils.getConnection(ds);
DatabaseMetaData meta = con.getMetaData();
String url = meta.getURL();
feature = meta.getDatabaseProductName().toLowerCase().replace(" ", "") + "_" + url;
features.put(key, feature);
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (null != con && !DataSourceUtils.isConnectionTransactional(con, ds)) {
DataSourceUtils.releaseConnection(con, ds);
}
}
}
}
}
return feature;
}
/**
* 数据源唯一标识 如果不实现则默认feature
*
* @param runtime 数据运行时上下文
* @param datasource 数据源
* @return String 返回null由上层自动提取
*/
@Override
public String key(DataRuntime runtime, Object datasource) {
if (datasource instanceof JdbcTemplate jdbc) {
DataSource ds = jdbc.getDataSource();
if (ds instanceof DynamicRoutingDataSource) {
return DynamicDataSourceContextHolder.peek();
}
}
return runtime.getKey();
}
/**
* ConfigTable.KEEP_ADAPTER=2 : 根据当前接口判断是否保持同一个数据源绑定同一个adapter<br/>
* DynamicRoutingDataSource类型的返回false,因为同一个DynamicRoutingDataSource可能对应多类数据库, 如果项目中只有一种数据库 应该直接返回true
*
* @param runtime 数据运行时上下文
* @param datasource 数据源
* @return boolean
*/
@Override
public boolean keepAdapter(DataRuntime runtime, Object datasource) {
if (datasource instanceof JdbcTemplate jdbc) {
DataSource ds = jdbc.getDataSource();
return !(ds instanceof DynamicRoutingDataSource);
}
return true;
}
}

View File

@@ -0,0 +1,40 @@
package org.dromara.gen.config.properties;
import lombok.Data;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* 代码生成配置属性
*
* @author 秋辞未寒
*/
@Data
@Component
@ConfigurationProperties(prefix = "gen")
@PropertySource(value = "classpath:generator.yml", factory = YmlPropertySourceFactory.class)
public class GenProperties {
/**
* 作者
*/
private String author;
/**
* 生成包路径
*/
private String packageName;
/**
* 自动去除表前缀,默认是false
*/
private boolean autoRemovePre = false;
/**
* 表前缀(类名不会包含表前缀)
*/
private String tablePrefix;
}

View File

@@ -0,0 +1,321 @@
package org.dromara.gen.constant;
import cn.hutool.core.collection.CollUtil;
import java.util.Set;
/**
* 代码生成通用常量
*
* @author ruoyi
*/
public interface GenConstants {
/**
* 单表(增删改查)
*/
String TPL_CRUD = "crud";
/**
* 树表(增删改查)
*/
String TPL_TREE = "tree";
/**
* 树编码字段
*/
String TREE_CODE = "treeCode";
/**
* 树父编码字段
*/
String TREE_PARENT_CODE = "treeParentCode";
/**
* 树名称字段
*/
String TREE_NAME = "treeName";
/**
* 上级菜单ID字段
*/
String PARENT_MENU_ID = "parentMenuId";
/**
* 上级菜单名称字段
*/
String PARENT_MENU_NAME = "parentMenuName";
/**
* 是否启用导出能力
*/
String ENABLE_EXPORT = "enableExport";
/**
* 是否启用状态切换能力
*/
String ENABLE_STATUS = "enableStatus";
/**
* 状态字段
*/
String STATUS_FIELD = "statusField";
/**
* 是否启用组合唯一校验
*/
String ENABLE_UNIQUE = "enableUnique";
/**
* 组合唯一字段
*/
String UNIQUE_FIELDS = "uniqueFields";
/**
* 是否启用排序调整能力
*/
String ENABLE_SORT = "enableSort";
/**
* 排序字段
*/
String SORT_FIELD = "sortField";
/**
* 默认前端模板类型,对应模板根目录下的 vue 目录。
*/
String FRONTEND_TYPE_VUE = "vue";
/**
* React 前端模板类型,对应模板根目录下的 react 目录。
*/
String FRONTEND_TYPE_REACT = "react";
/**
* 树根节点值
*/
String TREE_ROOT_VALUE = "treeRootValue";
/**
* 树祖级字段
*/
String TREE_ANCESTORS = "treeAncestors";
/**
* 树排序字段
*/
String TREE_ORDER_FIELD = "treeOrderField";
/**
* 数据库字符串类型
*/
String[] COLUMNTYPE_STR = {"char", "varchar", "enum", "set", "nchar", "nvarchar", "varchar2", "nvarchar2",
"long varchar", "long nvarchar", "uniqueidentifier", "uuid", "json", "jsonb", "xml"};
/**
* 数据库文本类型
*/
String[] COLUMNTYPE_TEXT = {"tinytext", "text", "mediumtext", "longtext", "binary", "varbinary", "blob",
"ntext", "image", "bytea", "clob", "nclob", "long", "long raw", "raw"};
/**
* 数据库时间类型
*/
String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp", "year", "interval",
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz", "timestamp with time zone",
"timestamp without time zone", "time with time zone", "time without time zone"};
/**
* 数据库数字类型
*/
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "int2", "int4", "int8", "number", "integer",
"bit", "boolean", "bool", "bigint", "float", "float4", "float8", "double", "decimal", "dec", "fixed",
"numeric", "real", "double precision", "binary_float", "binary_double", "smallserial", "serial",
"serial2", "serial4", "serial8", "bigserial", "money", "smallmoney"};
/**
* BO对象 不需要添加字段
*/
String[] COLUMNNAME_NOT_ADD = {"del_flag", "version"};
/**
* BO对象 不需要编辑字段
*/
String[] COLUMNNAME_NOT_EDIT = {"del_flag", "version"};
/**
* VO对象 不需要返回字段
*/
String[] COLUMNNAME_NOT_LIST = {"create_dept", "create_by", "create_time", "del_flag", "update_by",
"update_time", "version"};
/**
* BO对象 不需要查询字段
*/
String[] COLUMNNAME_NOT_QUERY = {"id", "del_flag", "remark", "version"};
/**
* Entity基类字段
*/
String[] BASE_ENTITY = {"createDept", "createBy", "createTime", "updateBy", "updateTime"};
/**
* 文本框
*/
String HTML_INPUT = "input";
/**
* 文本域
*/
String HTML_TEXTAREA = "textarea";
/**
* 下拉框
*/
String HTML_SELECT = "select";
/**
* 单选框
*/
String HTML_RADIO = "radio";
/**
* 复选框
*/
String HTML_CHECKBOX = "checkbox";
/**
* 日期控件
*/
String HTML_DATETIME = "datetime";
/**
* 开关控件
*/
String HTML_SWITCH = "switch";
/**
* 数字输入控件
*/
String HTML_INPUT_NUMBER = "inputNumber";
/**
* 图片上传控件
*/
String HTML_IMAGE_UPLOAD = "imageUpload";
/**
* 文件上传控件
*/
String HTML_FILE_UPLOAD = "fileUpload";
/**
* 富文本控件
*/
String HTML_EDITOR = "editor";
/**
* 字符串类型
*/
String TYPE_STRING = "String";
/**
* 整型
*/
String TYPE_INTEGER = "Integer";
/**
* 长整型
*/
String TYPE_LONG = "Long";
/**
* 浮点型
*/
String TYPE_DOUBLE = "Double";
/**
* 单精度浮点型
*/
String TYPE_FLOAT = "Float";
/**
* 高精度计算类型
*/
String TYPE_BIGDECIMAL = "BigDecimal";
/**
* 布尔类型
*/
String TYPE_BOOLEAN = "Boolean";
/**
* 时间类型
*/
String TYPE_DATE = "LocalDateTime";
/**
* 模糊查询
*/
String QUERY_LIKE = "LIKE";
/**
* 相等查询
*/
String QUERY_EQ = "EQ";
/**
* 范围查询
*/
String QUERY_BETWEEN = "BETWEEN";
/**
* 必填标识,对应前端表单规则中的必填字段配置。
*/
String REQUIRE = "1";
// 模板根路径与后缀
String TEMPLATE_ROOT_PATH = "fm";
String TEMPLATE_FILE_SUFFIX = ".ftl";
String TEMPLATE_RESOURCE_PREFIX = "classpath*:";
String TEMPLATE_PAGE_PATTERN_SUFFIX = ".*" + TEMPLATE_FILE_SUFFIX;
// 后端源码模板
String JAVA_TEMPLATE_ROOT_PATH = TEMPLATE_ROOT_PATH + "/java";
String JAVA_DOMAIN_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/domain.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_VO_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/vo.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_BO_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/bo.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_MAPPER_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/mapper.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_SERVICE_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/service.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_SERVICE_IMPL_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/serviceImpl.java" + TEMPLATE_FILE_SUFFIX;
String JAVA_CONTROLLER_TEMPLATE_PATH = JAVA_TEMPLATE_ROOT_PATH + "/controller.java" + TEMPLATE_FILE_SUFFIX;
// MyBatis MapperXML 模板
String XML_TEMPLATE_ROOT_PATH = TEMPLATE_ROOT_PATH + "/xml";
String XML_MAPPER_TEMPLATE_PATH = XML_TEMPLATE_ROOT_PATH + "/mapper.xml" + TEMPLATE_FILE_SUFFIX;
// 前端源码模板
String FRONTEND_API_TEMPLATE_NAME = "api.ts" + TEMPLATE_FILE_SUFFIX;
String FRONTEND_TYPES_TEMPLATE_NAME = "types.ts" + TEMPLATE_FILE_SUFFIX;
String FRONTEND_INDEX_TEMPLATE_PREFIX = "index";
String FRONTEND_INDEX_TREE_TEMPLATE_PREFIX = "index-tree";
// 数据库SQL模板
String SQL_TEMPLATE_ROOT_PATH = TEMPLATE_ROOT_PATH + "/sql";
String SQL_ORACLE_TEMPLATE_PATH = SQL_TEMPLATE_ROOT_PATH + "/oracle.sql" + TEMPLATE_FILE_SUFFIX;
String SQL_POSTGRES_TEMPLATE_PATH = SQL_TEMPLATE_ROOT_PATH + "/postgres.sql" + TEMPLATE_FILE_SUFFIX;
String SQL_SQLSERVER_TEMPLATE_PATH = SQL_TEMPLATE_ROOT_PATH + "/sqlserver.sql" + TEMPLATE_FILE_SUFFIX;
String SQL_MYSQL_TEMPLATE_PATH = SQL_TEMPLATE_ROOT_PATH + "/mysql.sql" + TEMPLATE_FILE_SUFFIX;
/**
* 固定模板路径集合,前端模板按 frontendType 动态解析。
*/
Set<String> TEMPLATE_PATHS = CollUtil.newHashSet(
JAVA_DOMAIN_TEMPLATE_PATH
, JAVA_VO_TEMPLATE_PATH
, JAVA_BO_TEMPLATE_PATH
, JAVA_MAPPER_TEMPLATE_PATH
, JAVA_SERVICE_TEMPLATE_PATH
, JAVA_SERVICE_IMPL_TEMPLATE_PATH
, JAVA_CONTROLLER_TEMPLATE_PATH
, XML_MAPPER_TEMPLATE_PATH
, SQL_ORACLE_TEMPLATE_PATH
, SQL_POSTGRES_TEMPLATE_PATH
, SQL_SQLSERVER_TEMPLATE_PATH
, SQL_MYSQL_TEMPLATE_PATH
);
}

View File

@@ -0,0 +1,231 @@
package org.dromara.gen.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
import org.dromara.gen.service.IGenTableService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 代码生成 操作处理
*
* @author Lion Li
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/tool/gen")
public class GenController extends BaseController {
private final IGenTableService genTableService;
/**
* 分页查询代码生成业务列表。
*
* @param genTable 查询条件
* @param pageQuery 分页参数
* @return 代码生成列表
*/
@SaCheckPermission("tool:gen:list")
@GetMapping("/list")
public R<PageResult<GenTable>> genList(GenTable genTable, PageQuery pageQuery) {
return R.ok(genTableService.selectPageGenTableList(genTable, pageQuery));
}
/**
* 修改代码生成业务
*
* @param tableId 表ID
* @return 表与字段信息
*/
@RepeatSubmit()
@SaCheckPermission("tool:gen:query")
@GetMapping(value = "/{tableId}")
public R<Map<String, Object>> getInfo(@PathVariable Long tableId) {
GenTable table = genTableService.selectGenTableById(tableId);
List<GenTableColumn> list = genTableService.selectGenTableColumnListByTableId(tableId);
Map<String, Object> map = new HashMap<>(2);
map.put("info", table);
map.put("rows", list);
return R.ok(map);
}
/**
* 分页查询数据库表列表。
*
* @param genTable 查询条件
* @param pageQuery 分页参数
* @return 数据库表列表
*/
@SaCheckPermission("tool:gen:list")
@GetMapping("/db/list")
public R<PageResult<GenTable>> dataList(GenTable genTable, PageQuery pageQuery) {
return R.ok(genTableService.selectPageDbTableList(genTable, pageQuery));
}
/**
* 查询数据表字段列表
*
* @param tableId 表ID
* @return 字段列表
*/
@SaCheckPermission("tool:gen:list")
@GetMapping(value = "/column/{tableId}")
public R<PageResult<GenTableColumn>> columnList(@PathVariable("tableId") Long tableId) {
List<GenTableColumn> list = genTableService.selectGenTableColumnListByTableId(tableId);
return R.ok(PageResult.build(list));
}
/**
* 导入表结构(保存)
*
* @param tables 表名串
* @param dataName 数据源名称
* @return 操作结果
*/
@SaCheckPermission("tool:gen:import")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@Lock4j(keys = {"#dataName"}, acquireTimeout = 10000)
@RepeatSubmit()
@PostMapping("/importTable")
public R<Void> importTableSave(String tables, String dataName) {
String[] tableNames = Convert.toStrArray(tables);
// 查询表信息
List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames, dataName);
genTableService.importGenTable(tableList, dataName);
return R.ok();
}
/**
* 保存代码生成业务配置。
*
* @param genTable 业务配置
* @return 操作结果
*/
@SaCheckPermission("tool:gen:edit")
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
public R<Void> editSave(@Validated @RequestBody GenTable genTable) {
genTableService.validateEdit(genTable);
genTableService.updateGenTable(genTable);
return R.ok();
}
/**
* 删除代码生成
*
* @param tableIds 表ID串
* @return 操作结果
*/
@SaCheckPermission("tool:gen:remove")
@Log(title = "代码生成", businessType = BusinessType.DELETE)
@DeleteMapping("/{tableIds}")
public R<Void> remove(@PathVariable Long[] tableIds) {
genTableService.deleteGenTableByIds(tableIds);
return R.ok();
}
/**
* 预览代码
*
* @param tableId 表ID
* @return 模板路径与生成代码内容映射
*/
@SaCheckPermission("tool:gen:preview")
@GetMapping("/preview/{tableId}")
public R<Map<String, String>> preview(@PathVariable("tableId") Long tableId) throws IOException {
Map<String, String> dataMap = genTableService.previewCode(tableId);
return R.ok(dataMap);
}
/**
* 生成代码(下载方式)
*
* @param response HTTP 响应
* @param tableId 表ID
*/
@SaCheckPermission("tool:gen:code")
@Log(title = "代码生成", businessType = BusinessType.GENCODE)
@GetMapping("/download/{tableId}")
public void download(HttpServletResponse response, @PathVariable("tableId") Long tableId) throws IOException {
byte[] data = genTableService.downloadCode(tableId);
genCode(response, data);
}
/**
* 同步数据库
*
* @param tableId 表ID
* @return 操作结果
*/
@SaCheckPermission("tool:gen:edit")
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
@Lock4j(keys = {"#tableId"}, acquireTimeout = 5000)
@GetMapping("/synchDb/{tableId}")
public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
genTableService.synchDb(tableId);
return R.ok();
}
/**
* 批量生成代码
*
* @param response HTTP 响应
* @param tableIdStr 表ID串
*/
@SaCheckPermission("tool:gen:code")
@Log(title = "代码生成", businessType = BusinessType.GENCODE)
@GetMapping("/batchGenCode")
public void batchGenCode(HttpServletResponse response, String tableIdStr) throws IOException {
String[] tableIds = Convert.toStrArray(tableIdStr);
byte[] data = genTableService.downloadCode(tableIds);
genCode(response, data);
}
/**
* 将生成结果写出为 zip 文件流。
*
* @param response HTTP 响应
* @param data zip 二进制数据
*/
private void genCode(HttpServletResponse response, byte[] data) throws IOException {
response.reset();
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\"");
response.addHeader("Content-Length", "" + data.length);
response.setContentType("application/octet-stream; charset=UTF-8");
IoUtil.write(response.getOutputStream(), false, data);
}
/**
* 查询当前可用数据源名称列表。
*
* @return 数据源名称集合
*/
@SaCheckPermission("tool:gen:list")
@GetMapping(value = "/getDataNames")
public R<Object> getCurrentDataSourceNameList() {
return R.ok(DataBaseHelper.getDataSourceNameList());
}
}

View File

@@ -0,0 +1,282 @@
package org.dromara.gen.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.gen.constant.GenConstants;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 业务表 gen_table
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gen_table")
public class GenTable extends BaseEntity {
/**
* 编号
*/
@TableId(value = "table_id")
private Long tableId;
/**
* 数据源名称
*/
@NotBlank(message = "数据源名称不能为空")
private String dataName;
/**
* 表名称
*/
@NotBlank(message = "表名称不能为空")
private String tableName;
/**
* 表描述
*/
@NotBlank(message = "表描述不能为空")
private String tableComment;
/**
* 实体类名称(首字母大写)
*/
@NotBlank(message = "实体类名称不能为空")
private String className;
/**
* 使用的模板(crud单表操作 tree树表操作)
*/
private String tplCategory;
/**
* 前端模板类型,对应 fm 下的模板目录
*/
private String frontendType;
/**
* 生成包路径
*/
@NotBlank(message = "生成包路径不能为空")
private String packageName;
/**
* 生成模块名
*/
@NotBlank(message = "生成模块名不能为空")
private String moduleName;
/**
* 生成业务名
*/
@NotBlank(message = "生成业务名不能为空")
private String businessName;
/**
* 生成功能名
*/
@NotBlank(message = "生成功能名不能为空")
private String functionName;
/**
* 生成作者
*/
@NotBlank(message = "作者不能为空")
private String functionAuthor;
/**
* 主键信息
*/
@TableField(exist = false)
private GenTableColumn pkColumn;
/**
* 表列信息
*/
@Valid
@TableField(exist = false)
private List<GenTableColumn> columns;
/**
* 其它生成选项
*/
private String options;
/**
* 备注
*/
private String remark;
/**
* 树编码字段
*/
@TableField(exist = false)
private String treeCode;
/**
* 树父编码字段
*/
@TableField(exist = false)
private String treeParentCode;
/**
* 树名称字段
*/
@TableField(exist = false)
private String treeName;
/**
* 菜单id列表
*/
@TableField(exist = false)
private List<Long> menuIds;
/**
* 上级菜单ID字段
*/
@TableField(exist = false)
private Long parentMenuId;
/**
* 上级菜单名称字段
*/
@TableField(exist = false)
private String parentMenuName;
/**
* 是否启用导出
*/
@TableField(exist = false)
private Boolean enableExport;
/**
* 是否启用状态切换
*/
@TableField(exist = false)
private Boolean enableStatus;
/**
* 状态字段
*/
@TableField(exist = false)
private String statusField;
/**
* 是否启用组合唯一校验
*/
@TableField(exist = false)
private Boolean enableUnique;
/**
* 组合唯一字段
*/
@TableField(exist = false)
private List<String> uniqueFields;
/**
* 是否启用排序调整
*/
@TableField(exist = false)
private Boolean enableSort;
/**
* 排序字段
*/
@TableField(exist = false)
private String sortField;
/**
* 树根节点值
*/
@TableField(exist = false)
private String treeRootValue;
/**
* 树祖级字段
*/
@TableField(exist = false)
private String treeAncestorsField;
/**
* 树排序字段
*/
@TableField(exist = false)
private String treeOrderField;
/**
* 请求参数
*/
@TableField(exist = false)
private Map<String, Object> params = new HashMap<>();
/**
* 判断当前业务表是否采用树表模板。
*
* @return 树表模板返回 {@code true}
*/
public boolean isTree() {
return isTree(this.tplCategory);
}
/**
* 根据模板分类判断是否为树表模板。
*
* @param tplCategory 模板分类
* @return 树表模板返回 {@code true}
*/
public static boolean isTree(String tplCategory) {
return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory);
}
/**
* 判断当前业务表是否采用普通 CRUD 模板。
*
* @return 普通 CRUD 模板返回 {@code true}
*/
public boolean isCrud() {
return isCrud(this.tplCategory);
}
/**
* 根据模板分类判断是否为普通 CRUD 模板。
*
* @param tplCategory 模板分类
* @return 普通 CRUD 模板返回 {@code true}
*/
public static boolean isCrud(String tplCategory) {
return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory);
}
/**
* 判断指定 Java 字段是否属于基类公共字段。
*
* @param javaField Java 字段名
* @return 基类公共字段返回 {@code true}
*/
public boolean isSuperColumn(String javaField) {
return isSuperColumn(this.tplCategory, javaField);
}
/**
* 根据模板分类与字段名判断是否属于基类公共字段。
*
* @param tplCategory 模板分类
* @param javaField Java 字段名
* @return 基类公共字段返回 {@code true}
*/
public static boolean isSuperColumn(String tplCategory, String javaField) {
return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY);
}
}

View File

@@ -0,0 +1,442 @@
package org.dromara.gen.domain;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.JdbcType;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.gen.constant.GenConstants;
/**
* 代码生成业务字段表 gen_table_column
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gen_table_column")
public class GenTableColumn extends BaseEntity {
/**
* 编号
*/
@TableId(value = "column_id")
private Long columnId;
/**
* 归属表编号
*/
private Long tableId;
/**
* 列名称
*/
private String columnName;
/**
* 列描述
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String columnComment;
/**
* 列类型
*/
private String columnType;
/**
* JAVA类型
*/
private String javaType;
/**
* JAVA字段名
*/
@NotBlank(message = "Java属性不能为空")
private String javaField;
/**
* 是否主键(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isPk;
/**
* 是否自增(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isIncrement;
/**
* 是否必填(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isRequired;
/**
* 是否为插入字段(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isInsert;
/**
* 是否编辑字段(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isEdit;
/**
* 是否列表字段(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isList;
/**
* 是否查询字段(1是)
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String isQuery;
/**
* 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围)
*/
private String queryType;
/**
* 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件)
*/
private String htmlType;
/**
* 字典类型
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String dictType;
/**
* 排序
*/
private Integer sort;
/**
* 获取首字母大写后的 Java 字段名。
*
* @return 首字母大写的 Java 字段名
*/
public String getCapJavaField() {
return StringUtils.capitalize(javaField);
}
/**
* 获取适合界面展示的字段注释,去除括号内的枚举说明。
*
* @return 字段展示名称
*/
public String getColumnLabel() {
int index = StringUtils.indexOf(this.columnComment, "(");
if (index != StringUtils.INDEX_NOT_FOUND) {
return StringUtils.substring(this.columnComment, 0, index);
}
return this.columnComment;
}
/**
* 获取 TypeScript 字段类型。
*
* @return TypeScript 类型
*/
public String getTsType() {
if (StringUtils.containsAny(this.javaField, "id", "Id")) {
return "string | number";
}
if (StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER, GenConstants.TYPE_DOUBLE,
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return "number";
}
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "boolean";
}
return "string";
}
/**
* 获取开关启用值的前端字面量。
*
* @return 前端字面量
*/
public String getSwitchActiveValue() {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "true";
}
if (isNumberType()) {
return "0";
}
return "'0'";
}
/**
* 获取开关停用值的前端字面量。
*
* @return 前端字面量
*/
public String getSwitchInactiveValue() {
if (StringUtils.equals(this.javaType, GenConstants.TYPE_BOOLEAN)) {
return "false";
}
if (isNumberType()) {
return "1";
}
return "'1'";
}
/**
* 判断当前字段是否为数值类型。
*
* @return 数值类型返回 {@code true}
*/
public boolean isNumberType() {
return StringUtils.equalsAny(this.javaType,
GenConstants.TYPE_INTEGER, GenConstants.TYPE_LONG, GenConstants.TYPE_DOUBLE,
GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL);
}
/**
* 是否为日期范围查询字段。
*
* @return 日期范围查询字段返回 {@code true}
*/
public boolean isDateRangeQuery() {
return isQuery()
&& StringUtils.equals(this.htmlType, GenConstants.HTML_DATETIME)
&& StringUtils.equals(this.queryType, GenConstants.QUERY_BETWEEN);
}
/**
* 判断当前列是否为主键列。
*
* @return 主键列返回 {@code true}
*/
public boolean isPk() {
return isPk(this.isPk);
}
/**
* 根据标识判断是否为主键列。
*
* @param isPk 主键标识
* @return 主键列返回 {@code true}
*/
public boolean isPk(String isPk) {
return isPk != null && StringUtils.equals("1", isPk);
}
/**
* 判断当前列是否为自增列。
*
* @return 自增列返回 {@code true}
*/
public boolean isIncrement() {
return isIncrement(this.isIncrement);
}
/**
* 根据标识判断是否为自增列。
*
* @param isIncrement 自增标识
* @return 自增列返回 {@code true}
*/
public boolean isIncrement(String isIncrement) {
return isIncrement != null && StringUtils.equals("1", isIncrement);
}
/**
* 判断当前列是否必填。
*
* @return 必填返回 {@code true}
*/
public boolean isRequired() {
return isRequired(this.isRequired);
}
/**
* 根据标识判断当前列是否必填。
*
* @param isRequired 必填标识
* @return 必填返回 {@code true}
*/
public boolean isRequired(String isRequired) {
return isRequired != null && StringUtils.equals("1", isRequired);
}
/**
* 判断当前列是否参与新增。
*
* @return 参与新增返回 {@code true}
*/
public boolean isInsert() {
return isInsert(this.isInsert);
}
/**
* 根据标识判断当前列是否参与新增。
*
* @param isInsert 新增标识
* @return 参与新增返回 {@code true}
*/
public boolean isInsert(String isInsert) {
return isInsert != null && StringUtils.equals("1", isInsert);
}
/**
* 判断当前列是否参与编辑。
*
* @return 参与编辑返回 {@code true}
*/
public boolean isEdit() {
return isEdit(this.isEdit);
}
/**
* 根据标识判断当前列是否参与编辑。
*
* @param isEdit 编辑标识
* @return 参与编辑返回 {@code true}
*/
public boolean isEdit(String isEdit) {
return isEdit != null && StringUtils.equals("1", isEdit);
}
/**
* 判断当前列是否参与列表展示。
*
* @return 参与列表展示返回 {@code true}
*/
public boolean isList() {
return isList(this.isList);
}
/**
* 根据标识判断当前列是否参与列表展示。
*
* @param isList 列表展示标识
* @return 参与列表展示返回 {@code true}
*/
public boolean isList(String isList) {
return isList != null && StringUtils.equals("1", isList);
}
/**
* 判断当前列是否参与查询条件。
*
* @return 参与查询返回 {@code true}
*/
public boolean isQuery() {
return isQuery(this.isQuery);
}
/**
* 根据标识判断当前列是否参与查询条件。
*
* @param isQuery 查询标识
* @return 参与查询返回 {@code true}
*/
public boolean isQuery(String isQuery) {
return isQuery != null && StringUtils.equals("1", isQuery);
}
/**
* 判断当前列是否为基类公共字段。
*
* @return 基类公共字段返回 {@code true}
*/
public boolean isSuperColumn() {
return isSuperColumn(this.javaField);
}
/**
* 根据字段名判断是否为基类公共字段。
*
* @param javaField Java 字段名
* @return 基类公共字段返回 {@code true}
*/
public static boolean isSuperColumn(String javaField) {
return StringUtils.equalsAnyIgnoreCase(javaField,
// BaseEntity
"createBy", "createTime", "updateBy", "updateTime",
// TreeEntity
"parentName", "parentId");
}
/**
* 判断当前列是否属于生成页面需要保留的白名单字段。
*
* @return 白名单字段返回 {@code true}
*/
public boolean isUsableColumn() {
return isUsableColumn(javaField);
}
/**
* 根据字段名判断是否属于生成页面需要保留的白名单字段。
*
* @param javaField Java 字段名
* @return 白名单字段返回 {@code true}
*/
public static boolean isUsableColumn(String javaField) {
// isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单
return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark");
}
/**
* 判断当前列是否需要显式声明 MP 字段映射。
*
* @return 字段名与下划线列名不一致时返回 {@code true}
*/
public boolean isNeedTableField() {
if (StringUtils.isAnyBlank(this.columnName, this.javaField)) {
return false;
}
return !StringUtils.equalsIgnoreCase(this.columnName, StringUtils.toUnderScoreCase(this.javaField));
}
/**
* 判断当前列是否属于字典控件列。
*
* @return 仅当已配置字典类型且显示类型支持字典时返回 {@code true}
*/
public boolean isDictColumn() {
return StringUtils.isNotBlank(this.dictType) && StringUtils.equalsAny(this.htmlType,
GenConstants.HTML_SELECT,
GenConstants.HTML_RADIO,
GenConstants.HTML_CHECKBOX,
GenConstants.HTML_SWITCH);
}
/**
* 从字段注释中解析字典读转换表达式。
*
* @return 形如 `0=男,1=女` 的转换表达式,无法解析时返回原始注释
*/
public String readConverterExp() {
String remarks = StringUtils.substringBetween(this.columnComment, "(", ")");
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotEmpty(remarks)) {
for (String value : remarks.split(" ")) {
if (StringUtils.isNotEmpty(value)) {
Object startStr = value.subSequence(0, 1);
String endStr = value.substring(1);
sb.append(StringUtils.EMPTY).append(startStr).append("=").append(endStr).append(StringUtils.SEPARATOR);
}
}
return sb.deleteCharAt(sb.length() - 1).toString();
} else {
return this.columnComment;
}
}
}

View File

@@ -0,0 +1,15 @@
package org.dromara.gen.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.gen.domain.GenTableColumn;
/**
* 业务字段 数据层
*
* @author Lion Li
*/
@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
public interface GenTableColumnMapper extends BaseMapperPlus<GenTableColumn, GenTableColumn> {
}

View File

@@ -0,0 +1,27 @@
package org.dromara.gen.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.gen.domain.GenTable;
import java.util.List;
/**
* 业务 数据层
*
* @author Lion Li
*/
@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
public interface GenTableMapper extends BaseMapperPlus<GenTable, GenTable> {
/**
* 查询指定数据源下的所有表名列表
*
* @param dataName 数据源名称,用于选择不同的数据源
* @return 当前数据库中的表名列表
* @DS("") 使用默认数据源执行查询操作
*/
@DS("")
List<String> selectTableNameList(String dataName);
}

View File

@@ -0,0 +1,654 @@
package org.dromara.gen.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.anyline.metadata.Column;
import org.anyline.metadata.Table;
import org.anyline.proxy.ServiceProxy;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.utils.IdGeneratorUtil;
import org.dromara.gen.constant.GenConstants;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
import org.dromara.gen.mapper.GenTableColumnMapper;
import org.dromara.gen.mapper.GenTableMapper;
import org.dromara.gen.util.GenUtils;
import org.dromara.gen.util.TemplateEngineUtils;
import org.dromara.gen.util.template.PathNamedTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 业务 服务层实现
*
* @author Lion Li
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class GenTableServiceImpl implements IGenTableService {
private final GenTableMapper tableMapper;
private final GenTableColumnMapper genTableColumnMapper;
private static final String[] TABLE_IGNORE = new String[]{"sai_", "sj_", "flow_", "gen_"};
/**
* 查询业务字段列表
*
* @param tableId 业务字段编号
* @return 业务字段集合
*/
@Override
public List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId) {
return genTableColumnMapper.lambda()
.eq(GenTableColumn::getTableId, tableId)
.orderByAsc(GenTableColumn::getSort)
.list();
}
/**
* 查询业务信息
*
* @param id 业务ID
* @return 业务信息
*/
@Override
public GenTable selectGenTableById(Long id) {
GenTable genTable = getGenTable(id);
setTableFromOptions(genTable);
return genTable;
}
/**
* 分页查询已导入的代码生成业务表。
*
* @param genTable 业务表筛选条件
* @param pageQuery 分页参数
* @return 业务表分页结果
*/
@Override
public PageResult<GenTable> selectPageGenTableList(GenTable genTable, PageQuery pageQuery) {
Page<GenTable> page = tableMapper.selectPage(pageQuery.build(), this.buildGenTableQueryWrapper(genTable));
return PageResult.build(page.getRecords(), page.getTotal());
}
/**
* 构造代码生成业务表查询条件。
*
* @param genTable 业务表筛选条件
* @return 包含数据源、表名、表注释和时间区间的查询包装器
*/
private QueryWrapper<GenTable> buildGenTableQueryWrapper(GenTable genTable) {
Map<String, Object> params = genTable.getParams();
QueryWrapper<GenTable> wrapper = Wrappers.query();
wrapper
.eq(StringUtils.isNotEmpty(genTable.getDataName()), "data_name", genTable.getDataName())
.like(StringUtils.isNotBlank(genTable.getTableName()), "lower(table_name)", StringUtils.lowerCase(genTable.getTableName()))
.like(StringUtils.isNotBlank(genTable.getTableComment()), "lower(table_comment)", StringUtils.lowerCase(genTable.getTableComment()))
.between(params.get("beginTime") != null && params.get("endTime") != null,
"create_time", params.get("beginTime"), params.get("endTime"))
.orderByDesc("update_time");
return wrapper;
}
/**
* 查询数据库列表
*
* @param genTable 包含查询条件的GenTable对象
* @param pageQuery 包含分页信息的PageQuery对象
* @return 包含分页结果的TableDataInfo对象
*/
@DS("#genTable.dataName")
@Override
public PageResult<GenTable> selectPageDbTableList(GenTable genTable, PageQuery pageQuery) {
// 获取查询条件
String tableName = genTable.getTableName();
String tableComment = genTable.getTableComment();
LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
if (CollUtil.isEmpty(tablesMap)) {
return PageResult.build();
}
List<String> tableNames = tableMapper.selectTableNameList(genTable.getDataName());
String[] tableArrays;
if (CollUtil.isNotEmpty(tableNames)) {
tableArrays = tableNames.toArray(new String[0]);
} else {
tableArrays = new String[0];
}
// 过滤并转换表格数据
List<GenTable> tables = tablesMap.values().stream()
.filter(x -> !StringUtils.startWithAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> {
if (CollUtil.isEmpty(tableNames)) {
return true;
}
return !StringUtils.equalsAnyIgnoreCase(x.getName(), tableArrays);
})
.filter(x -> {
boolean nameMatches = true;
boolean commentMatches = true;
// 进行表名称的模糊查询
if (StringUtils.isNotBlank(tableName)) {
nameMatches = StringUtils.containsIgnoreCase(x.getName(), tableName);
}
// 进行表描述的模糊查询
if (StringUtils.isNotBlank(tableComment)) {
commentMatches = StringUtils.containsIgnoreCase(x.getComment(), tableComment);
}
// 同时匹配名称和描述
return nameMatches && commentMatches;
})
.map(x -> {
GenTable gen = new GenTable();
gen.setTableName(x.getName());
gen.setTableComment(x.getComment());
// postgresql的表元数据没有创建时间这个东西(好奇葩) 只能new Date代替
Date createDate = ObjectUtil.defaultIfNull(x.getCreateTime(), new Date());
gen.setCreateTime(LocalDateTimeUtil.of(createDate));
gen.setUpdateTime(x.getUpdateTime() != null ? LocalDateTimeUtil.of(x.getUpdateTime()) : null);
return gen;
}).sorted(Comparator.comparing(GenTable::getCreateTime).reversed())
.toList();
// 根据原始数据列表和分页参数,构建表格分页数据对象(用于假分页)
if (CollUtil.isEmpty(tables)) {
return PageResult.build();
}
Page<Object> page = pageQuery.build();
List<GenTable> pageList = CollUtil.page((int) page.getCurrent() - 1, (int) page.getSize(), tables);
return PageResult.build(pageList, tables.size());
}
/**
* 查询据库列表
*
* @param tableNames 表名称组
* @param dataName 数据源名称
* @return 数据库表集合
*/
@DS("#dataName")
@Override
public List<GenTable> selectDbTableListByNames(String[] tableNames, String dataName) {
Set<String> tableNameSet = new HashSet<>(List.of(tableNames));
LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
if (CollUtil.isEmpty(tablesMap)) {
return new ArrayList<>();
}
List<Table<?>> tableList = tablesMap.values().stream()
.filter(x -> !StringUtils.startWithAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> tableNameSet.contains(x.getName())).toList();
if (CollUtil.isEmpty(tableList)) {
return new ArrayList<>();
}
return tableList.stream().map(x -> {
GenTable gen = new GenTable();
gen.setDataName(dataName);
gen.setTableName(x.getName());
gen.setTableComment(x.getComment());
gen.setCreateTime(LocalDateTimeUtil.of(x.getCreateTime()));
gen.setUpdateTime(LocalDateTimeUtil.of(x.getUpdateTime()));
return gen;
}).toList();
}
/**
* 修改业务
*
* @param genTable 业务信息
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void updateGenTable(GenTable genTable) {
normalizeColumnOptions(genTable.getColumns());
String options = JsonUtils.toJsonString(genTable.getParams());
genTable.setOptions(options);
int row = tableMapper.updateById(genTable);
if (row > 0) {
genTableColumnMapper.updateBatchById(genTable.getColumns());
}
}
/**
* 删除业务对象
*
* @param tableIds 需要删除的数据ID
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteGenTableByIds(Long[] tableIds) {
List<Long> ids = Arrays.asList(tableIds);
tableMapper.deleteByIds(ids);
genTableColumnMapper.lambda()
.in(GenTableColumn::getTableId, ids)
.deleteCount();
}
/**
* 导入表结构
*
* @param tableList 导入表列表
* @param dataName 数据源名称
*/
@DSTransactional
@Override
public void importGenTable(List<GenTable> tableList, String dataName) {
try {
for (GenTable table : tableList) {
String tableName = table.getTableName();
GenUtils.initTable(table);
table.setDataName(dataName);
int row = tableMapper.insert(table);
if (row > 0) {
// 保存列信息
List<GenTableColumn> genTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(tableName, dataName);
List<GenTableColumn> saveColumns = new ArrayList<>();
for (GenTableColumn column : genTableColumns) {
GenUtils.initColumnField(column, table);
saveColumns.add(column);
}
if (CollUtil.isNotEmpty(saveColumns)) {
genTableColumnMapper.insertBatch(saveColumns);
}
}
}
} catch (Exception e) {
log.error("导入失败", e);
throw new ServiceException("导入失败:" + e.getMessage());
}
}
/**
* 根据表名称查询列信息
*
* @param tableName 表名称
* @param dataName 数据源名称
* @return 列信息
*/
@DS("#dataName")
@Override
public List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName) {
Table<?> table = ServiceProxy.metadata().table(tableName);
if (ObjectUtil.isNull(table)) {
return new ArrayList<>();
}
LinkedHashMap<String, Column> columns = table.getColumns();
List<GenTableColumn> tableColumns = new ArrayList<>();
columns.forEach((columnName, column) -> {
GenTableColumn tableColumn = new GenTableColumn();
tableColumn.setIsPk(column.isPrimaryKey() ? "1" : "0");
tableColumn.setColumnName(column.getName());
tableColumn.setColumnComment(column.getComment());
tableColumn.setColumnType(column.getOriginType().toLowerCase());
tableColumn.setSort(column.getPosition());
tableColumn.setIsRequired(column.isNullable() ? "0" : "1");
tableColumn.setIsIncrement(column.isAutoIncrement() ? "1" : "0");
tableColumns.add(tableColumn);
});
return tableColumns;
}
/**
* 预览代码
*
* @param tableId 表编号
* @return 预览数据列表
*/
@Override
public Map<String, String> previewCode(Long tableId) {
Map<String, String> dataMap = new LinkedHashMap<>();
RenderContext rc = buildRenderContext(tableId);
for (PathNamedTemplate template : rc.templates()) {
dataMap.put(template.getPathName(), template.render(rc.context()));
}
return dataMap;
}
/**
* 生成代码(下载方式)
*
* @param tableId 表名称
* @return 数据
*/
@Override
public byte[] downloadCode(Long tableId) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
writeCodeToZip(tableId, zip);
IoUtil.close(zip);
return outputStream.toByteArray();
}
/**
* 同步数据库
*
* @param tableId 表名称
*/
@DSTransactional
@Override
public void synchDb(Long tableId) {
GenTable table = getGenTable(tableId);
List<GenTableColumn> tableColumns = table.getColumns();
Map<String, GenTableColumn> tableColumnMap = StreamUtils.toIdentityMap(tableColumns, GenTableColumn::getColumnName);
List<GenTableColumn> dbTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(table.getTableName(), table.getDataName());
if (CollUtil.isEmpty(dbTableColumns)) {
throw new ServiceException("同步数据失败,原表结构不存在");
}
List<String> dbTableColumnNames = StreamUtils.toList(dbTableColumns, GenTableColumn::getColumnName);
List<GenTableColumn> saveColumns = new ArrayList<>();
dbTableColumns.forEach(column -> {
GenUtils.initColumnField(column, table);
if (tableColumnMap.containsKey(column.getColumnName())) {
GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName());
column.setColumnId(prevColumn.getColumnId());
if (column.isList()) {
// 如果是列表,继续保留查询方式/字典类型选项
column.setDictType(prevColumn.getDictType());
column.setQueryType(prevColumn.getQueryType());
}
if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk()
&& (column.isInsert() || column.isEdit())
&& ((column.isUsableColumn()) || (!column.isSuperColumn()))) {
// 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项
column.setIsRequired(prevColumn.getIsRequired());
column.setHtmlType(prevColumn.getHtmlType());
}
}
saveColumns.add(column);
});
if (CollUtil.isNotEmpty(saveColumns)) {
normalizeColumnOptions(saveColumns);
genTableColumnMapper.insertOrUpdateBatch(saveColumns);
}
List<GenTableColumn> delColumns = StreamUtils.filter(tableColumns, column -> !dbTableColumnNames.contains(column.getColumnName()));
if (CollUtil.isNotEmpty(delColumns)) {
List<Long> ids = StreamUtils.toList(delColumns, GenTableColumn::getColumnId);
if (CollUtil.isNotEmpty(ids)) {
genTableColumnMapper.deleteByIds(ids);
}
}
}
/**
* 批量生成代码(下载方式)
*
* @param tableIds 表ID数组
* @return 数据
*/
@Override
public byte[] downloadCode(String[] tableIds) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
for (String tableId : tableIds) {
writeCodeToZip(Long.parseLong(tableId), zip);
}
IoUtil.close(zip);
return outputStream.toByteArray();
}
/**
* 查询表信息并生成代码
*
* @param tableId 业务表主键
* @param zip 代码压缩输出流
*/
private void writeCodeToZip(Long tableId, ZipOutputStream zip) {
RenderContext rc = buildRenderContext(tableId);
GenTable table = rc.table();
for (PathNamedTemplate template : rc.templates()) {
String pathName = template.getPathName();
try {
String render = template.render(rc.context());
zip.putNextEntry(new ZipEntry(TemplateEngineUtils.getFileName(pathName, table)));
IoUtil.write(zip, StandardCharsets.UTF_8, false, render);
zip.flush();
zip.closeEntry();
} catch (IOException e) {
log.error("渲染模板失败,表名:{}", table.getTableName(), e);
}
}
}
/**
* 构建代码渲染上下文(含表信息、菜单ID、主键列、模板列表)
*
* @param tableId 业务表主键
* @return 渲染上下文
*/
private RenderContext buildRenderContext(Long tableId) {
GenTable table = getGenTable(tableId);
List<Long> menuIds = new ArrayList<>();
for (int i = 0; i < 6; i++) {
menuIds.add(IdGeneratorUtil.nextLongId());
}
table.setMenuIds(menuIds);
setPkColumn(table);
Dict context = TemplateEngineUtils.buildContext(table);
List<PathNamedTemplate> templates = TemplateEngineUtils.getTemplateList(table.getTplCategory(), table.getDataName(), table.getFrontendType());
return new RenderContext(table, context, templates);
}
/**
* 模板渲染上下文。
*
* @param table 生成表信息
* @param context 模板上下文
* @param templates 待渲染模板
*/
private record RenderContext(GenTable table, Dict context, List<PathNamedTemplate> templates) {
}
/**
* 修改保存参数校验
*
* @param genTable 业务信息
*/
@Override
public void validateEdit(GenTable genTable) {
validateOptionColumns(genTable);
if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) {
String options = JsonUtils.toJsonString(genTable.getParams());
Dict paramsObj = JsonUtils.parseMap(options);
if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_CODE))) {
throw new ServiceException("树编码字段不能为空");
} else if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_PARENT_CODE))) {
throw new ServiceException("树父编码字段不能为空");
} else if (StringUtils.isEmpty(paramsObj.getStr(GenConstants.TREE_NAME))) {
throw new ServiceException("树名称字段不能为空");
}
}
}
/**
* 校验生成选项中配置的字段是否存在。
*
* @param genTable 业务表信息
*/
private void validateOptionColumns(GenTable genTable) {
Map<String, Object> params = genTable.getParams();
if (CollUtil.isEmpty(params) || CollUtil.isEmpty(genTable.getColumns())) {
return;
}
Set<String> validFields = new HashSet<>();
genTable.getColumns().forEach(column -> {
validFields.add(column.getColumnName());
validFields.add(column.getJavaField());
});
validateOptionField(validFields, params.get(GenConstants.STATUS_FIELD), "状态字段");
validateOptionField(validFields, params.get(GenConstants.SORT_FIELD), "排序字段");
validateOptionField(validFields, params.get(GenConstants.TREE_ANCESTORS), "树祖级字段");
validateOptionField(validFields, params.get(GenConstants.TREE_ORDER_FIELD), "树排序字段");
Object uniqueFields = params.get(GenConstants.UNIQUE_FIELDS);
if (uniqueFields instanceof Collection<?> collection) {
for (Object field : collection) {
validateOptionField(validFields, field, "组合唯一字段");
}
}
}
/**
* 校验单个选项字段。
*
* @param validFields 有效字段集合
* @param field 待校验字段
* @param label 字段显示名称
*/
private void validateOptionField(Set<String> validFields, Object field, String label) {
if (ObjectUtil.isNull(field)) {
return;
}
String fieldValue = Convert.toStr(field);
if (StringUtils.isBlank(fieldValue)) {
return;
}
if (!validFields.contains(fieldValue)) {
throw new ServiceException(label + "不存在,请刷新字段后重试");
}
}
/**
* 规范化字段扩展配置。
*
* @param columns 表字段列表
*/
private void normalizeColumnOptions(List<GenTableColumn> columns) {
if (CollUtil.isEmpty(columns)) {
return;
}
for (GenTableColumn column : columns) {
if (!column.isDictColumn()) {
column.setDictType(StringUtils.EMPTY);
}
}
}
/**
* 查询业务表并补齐其列信息。
*
* @param tableId 业务表主键
* @return 包含字段集合的业务表实体
*/
private GenTable getGenTable(Long tableId) {
GenTable table = tableMapper.selectById(tableId);
if (ObjectUtil.isNull(table)) {
throw new ServiceException("业务表不存在");
}
fillTableColumns(Collections.singletonList(table));
return table;
}
/**
* 批量填充业务表对应的字段列表。
*
* @param tables 业务表集合
* @return 已填充字段信息的业务表集合
*/
private List<GenTable> fillTableColumns(List<GenTable> tables) {
if (CollUtil.isEmpty(tables)) {
return tables;
}
List<Long> tableIds = StreamUtils.toList(tables, GenTable::getTableId);
List<GenTableColumn> columns = genTableColumnMapper.lambda()
.in(GenTableColumn::getTableId, tableIds)
.orderByAsc(GenTableColumn::getTableId)
.orderByAsc(GenTableColumn::getSort)
.list();
Map<Long, List<GenTableColumn>> columnMap = StreamUtils.groupByKey(columns, GenTableColumn::getTableId);
tables.forEach(table -> table.setColumns(columnMap.getOrDefault(table.getTableId(), new ArrayList<>())));
return tables;
}
/**
* 设置主键列信息
*
* @param table 业务表信息
*/
public void setPkColumn(GenTable table) {
if (CollUtil.isEmpty(table.getColumns())) {
throw new ServiceException("表【" + table.getTableName() + "】字段为空,请检查表结构");
}
for (GenTableColumn column : table.getColumns()) {
if (column.isPk()) {
table.setPkColumn(column);
break;
}
}
if (ObjectUtil.isNull(table.getPkColumn())) {
table.setPkColumn(table.getColumns().getFirst());
}
}
/**
* 设置代码生成其他选项值
*
* @param genTable 设置后的生成对象
*/
public void setTableFromOptions(GenTable genTable) {
Dict paramsObj = JsonUtils.parseMap(genTable.getOptions());
if (ObjectUtil.isNull(paramsObj)) {
paramsObj = new Dict();
}
String treeCode = paramsObj.getStr(GenConstants.TREE_CODE);
String treeParentCode = paramsObj.getStr(GenConstants.TREE_PARENT_CODE);
String treeName = paramsObj.getStr(GenConstants.TREE_NAME);
Long parentMenuId = Convert.toLong(TemplateEngineUtils.getParentMenuId(paramsObj));
String parentMenuName = paramsObj.getStr(GenConstants.PARENT_MENU_NAME);
Boolean enableExport = Convert.toBool(paramsObj.get(GenConstants.ENABLE_EXPORT), true);
Boolean enableStatus = Convert.toBool(paramsObj.get(GenConstants.ENABLE_STATUS), false);
String statusField = paramsObj.getStr(GenConstants.STATUS_FIELD);
Boolean enableUnique = Convert.toBool(paramsObj.get(GenConstants.ENABLE_UNIQUE), false);
List<String> uniqueFields = Convert.toList(String.class, paramsObj.get(GenConstants.UNIQUE_FIELDS));
Boolean enableSort = Convert.toBool(paramsObj.get(GenConstants.ENABLE_SORT), false);
String sortField = paramsObj.getStr(GenConstants.SORT_FIELD);
String treeRootValue = paramsObj.getStr(GenConstants.TREE_ROOT_VALUE);
String treeAncestorsField = paramsObj.getStr(GenConstants.TREE_ANCESTORS);
String treeOrderField = paramsObj.getStr(GenConstants.TREE_ORDER_FIELD);
genTable.setTreeCode(treeCode);
genTable.setTreeParentCode(treeParentCode);
genTable.setTreeName(treeName);
genTable.setParentMenuId(parentMenuId);
genTable.setParentMenuName(parentMenuName);
genTable.setEnableExport(enableExport);
genTable.setEnableStatus(enableStatus);
genTable.setStatusField(statusField);
genTable.setEnableUnique(enableUnique);
genTable.setUniqueFields(uniqueFields);
genTable.setEnableSort(enableSort);
genTable.setSortField(sortField);
genTable.setTreeRootValue(treeRootValue);
genTable.setTreeAncestorsField(treeAncestorsField);
genTable.setTreeOrderField(treeOrderField);
}
}

View File

@@ -0,0 +1,129 @@
package org.dromara.gen.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
import java.util.List;
import java.util.Map;
/**
* 业务 服务层
*
* @author Lion Li
*/
public interface IGenTableService {
/**
* 查询业务字段列表
*
* @param tableId 业务字段编号
* @return 业务字段集合
*/
List<GenTableColumn> selectGenTableColumnListByTableId(Long tableId);
/**
* 查询业务列表
*
* @param genTable 业务信息
* @param pageQuery 分页参数
* @return 业务分页集合
*/
PageResult<GenTable> selectPageGenTableList(GenTable genTable, PageQuery pageQuery);
/**
* 查询据库列表
*
* @param genTable 业务信息
* @param pageQuery 分页参数
* @return 数据库表分页集合
*/
PageResult<GenTable> selectPageDbTableList(GenTable genTable, PageQuery pageQuery);
/**
* 查询据库列表
*
* @param tableNames 表名称组
* @param dataName 数据源名称
* @return 数据库表集合
*/
List<GenTable> selectDbTableListByNames(String[] tableNames, String dataName);
/**
* 查询业务信息
*
* @param id 业务ID
* @return 业务信息
*/
GenTable selectGenTableById(Long id);
/**
* 修改业务
*
* @param genTable 业务信息
*/
void updateGenTable(GenTable genTable);
/**
* 删除业务信息
*
* @param tableIds 需要删除的表数据ID
*/
void deleteGenTableByIds(Long[] tableIds);
/**
* 导入表结构
*
* @param tableList 导入表列表
* @param dataName 数据源名称
*/
void importGenTable(List<GenTable> tableList, String dataName);
/**
* 根据表名称查询列信息
*
* @param tableName 表名称
* @param dataName 数据源名称
* @return 列信息
*/
List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName);
/**
* 预览代码
*
* @param tableId 表编号
* @return 预览数据列表
*/
Map<String, String> previewCode(Long tableId);
/**
* 生成代码(下载方式)
*
* @param tableId 表名称
* @return 数据
*/
byte[] downloadCode(Long tableId);
/**
* 同步数据库
*
* @param tableId 表名称
*/
void synchDb(Long tableId);
/**
* 批量生成代码(下载方式)
*
* @param tableIds 表ID数组
* @return 数据
*/
byte[] downloadCode(String[] tableIds);
/**
* 修改保存参数校验
*
* @param genTable 业务信息
*/
void validateEdit(GenTable genTable);
}

View File

@@ -0,0 +1,380 @@
package org.dromara.gen.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.RegExUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.gen.config.properties.GenProperties;
import org.dromara.gen.constant.GenConstants;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
import java.util.Arrays;
/**
* 代码生成器 工具类
*
* @author ruoyi
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class GenUtils {
private final static GenProperties PROPERTIES = SpringUtils.getBean(GenProperties.class);
/**
* 初始化表信息
*
* @param genTable 待初始化的业务表对象
*/
public static void initTable(GenTable genTable) {
genTable.setClassName(convertClassName(genTable.getTableName()));
genTable.setPackageName(PROPERTIES.getPackageName());
genTable.setModuleName(getModuleName(PROPERTIES.getPackageName()));
genTable.setBusinessName(getBusinessName(genTable.getTableName()));
genTable.setFunctionName(replaceText(genTable.getTableComment()));
genTable.setFunctionAuthor(PROPERTIES.getAuthor());
genTable.setFrontendType(GenConstants.FRONTEND_TYPE_VUE);
genTable.setCreateTime(null);
genTable.setUpdateTime(null);
}
/**
* 初始化列属性字段
*
* @param column 待初始化的列对象
* @param table 所属业务表对象
*/
public static void initColumnField(GenTableColumn column, GenTable table) {
String dataType = normalizeDbType(getDbType(column.getColumnType()));
// 统一转小写 避免有些数据库默认大写问题 如果需要特别书写方式 请在实体类增加注解标注别名
String columnName = column.getColumnName().toLowerCase();
Integer columnLength = getColumnLength(column.getColumnType());
Integer columnScale = getColumnScale(column.getColumnType());
column.setTableId(table.getTableId());
column.setCreateTime(null);
column.setUpdateTime(null);
// 设置java字段名
column.setJavaField(StringUtils.toCamelCase(columnName));
// 设置默认类型
column.setJavaType(GenConstants.TYPE_STRING);
// 设置默认HTML类型
column.setHtmlType(GenConstants.HTML_INPUT);
column.setQueryType(GenConstants.QUERY_EQ);
if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) {
// 字符串长度超过500设置为文本域
String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT;
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
column.setJavaType(GenConstants.TYPE_BOOLEAN);
htmlType = GenConstants.HTML_SWITCH;
}
column.setHtmlType(htmlType);
} else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) {
column.setJavaType(GenConstants.TYPE_DATE);
column.setHtmlType(GenConstants.HTML_DATETIME);
} else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) {
column.setJavaType(resolveNumberJavaType(dataType, columnLength, columnScale, columnName));
column.setHtmlType(GenConstants.TYPE_BOOLEAN.equals(column.getJavaType()) ? GenConstants.HTML_SWITCH : GenConstants.HTML_INPUT_NUMBER);
}
// BO对象 默认插入勾选
if (!arraysContains(GenConstants.COLUMNNAME_NOT_ADD, columnName) && !column.isPk()) {
column.setIsInsert(GenConstants.REQUIRE);
}
// BO对象 默认编辑勾选
if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName)) {
column.setIsEdit(GenConstants.REQUIRE);
}
// VO对象 默认返回勾选
if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName)) {
column.setIsList(GenConstants.REQUIRE);
}
// BO对象 默认查询勾选
if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) {
column.setIsQuery(GenConstants.REQUIRE);
}
// 查询字段类型
if (StringUtils.endsWithIgnoreCase(columnName, "name")) {
column.setQueryType(GenConstants.QUERY_LIKE);
}
if (GenConstants.HTML_DATETIME.equals(column.getHtmlType()) && column.isQuery()) {
column.setQueryType(GenConstants.QUERY_BETWEEN);
}
// 状态字段设置单选框
if (isSwitchColumn(columnName) || GenConstants.TYPE_BOOLEAN.equals(column.getJavaType())) {
column.setHtmlType(GenConstants.HTML_SWITCH);
}
// 状态字段设置单选框/开关
if (StringUtils.endsWithIgnoreCase(columnName, "status")) {
column.setHtmlType(GenConstants.TYPE_BOOLEAN.equals(column.getJavaType()) ? GenConstants.HTML_SWITCH : GenConstants.HTML_RADIO);
}
// 类型&性别字段设置下拉框
else if (StringUtils.endsWithIgnoreCase(columnName, "type")
|| StringUtils.endsWithIgnoreCase(columnName, "sex")) {
column.setHtmlType(GenConstants.HTML_SELECT);
}
// 排序字段设置数字输入控件
else if (isSortColumn(columnName)) {
column.setHtmlType(GenConstants.HTML_INPUT_NUMBER);
}
// 图片字段设置图片上传控件
else if (StringUtils.endsWithAny(columnName, "image", "avatar", "logo", "picture")) {
column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD);
}
// 文件字段设置文件上传控件
else if (StringUtils.endsWithAny(columnName, "file", "attachment")) {
column.setHtmlType(GenConstants.HTML_FILE_UPLOAD);
}
// 备注描述类字段设置文本域
else if (StringUtils.endsWithAny(columnName, "remark", "description", "desc", "note")) {
column.setHtmlType(GenConstants.HTML_TEXTAREA);
}
// 内容字段设置富文本控件
else if (StringUtils.endsWithAny(columnName, "content", "html", "body")) {
column.setHtmlType(GenConstants.HTML_EDITOR);
}
}
/**
* 解析数值类型对应的 Java 类型。
*
* @param dataType 数据库字段类型
* @param columnLength 字段长度
* @param columnScale 小数位数
* @param columnName 字段名称
* @return Java 类型
*/
private static String resolveNumberJavaType(String dataType, Integer columnLength, Integer columnScale, String columnName) {
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
return GenConstants.TYPE_BOOLEAN;
}
if (arraysContains(new String[]{"decimal", "dec", "fixed", "numeric", "money", "smallmoney"}, dataType)) {
return GenConstants.TYPE_BIGDECIMAL;
}
if (arraysContains(new String[]{"float4", "real", "binary_float"}, dataType)) {
return GenConstants.TYPE_FLOAT;
}
if (arraysContains(new String[]{"float", "float8", "double", "double precision", "binary_double"}, dataType)) {
return GenConstants.TYPE_DOUBLE;
}
if (arraysContains(new String[]{"bigint", "int8", "bigserial", "serial8"}, dataType)) {
return GenConstants.TYPE_LONG;
}
if (arraysContains(new String[]{"tinyint", "smallint", "mediumint", "int", "int2", "int4", "integer",
"smallserial", "serial", "serial2", "serial4"}, dataType)) {
return GenConstants.TYPE_INTEGER;
}
if (StringUtils.equals(dataType, "number")) {
if (columnScale > 0) {
return GenConstants.TYPE_BIGDECIMAL;
}
return resolveIntegerJavaType(columnLength);
}
return GenConstants.TYPE_LONG;
}
/**
* 规范化数据库字段类型,去除 MySQL 等数据库的类型修饰。
*
* @param dataType 数据库字段类型
* @return 规范化后的数据库字段类型
*/
private static String normalizeDbType(String dataType) {
return StringUtils.trim(dataType)
.toLowerCase()
.replace(" unsigned", "")
.replace(" zerofill", "");
}
/**
* 根据整数字段长度解析 Java 类型。
*
* @param columnLength 字段长度
* @return Java 类型
*/
private static String resolveIntegerJavaType(Integer columnLength) {
if (columnLength > 0 && columnLength <= 9) {
return GenConstants.TYPE_INTEGER;
}
return GenConstants.TYPE_LONG;
}
/**
* 判断字段是否适合按布尔类型生成。
*
* @param dataType 数据库字段类型
* @param columnLength 字段长度
* @param columnScale 小数位数
* @param columnName 字段名称
* @return 是否布尔字段
*/
private static boolean isBooleanColumn(String dataType, Integer columnLength, Integer columnScale, String columnName) {
if (columnScale > 0) {
return false;
}
if (StringUtils.equalsAny(dataType, "bit", "boolean", "bool")) {
return true;
}
if (StringUtils.equalsAny(dataType, "tinyint", "number", "numeric", "decimal", "char", "nchar")
&& columnLength == 1 && isSwitchColumn(columnName)) {
return true;
}
return false;
}
/**
* 判断字段名称是否为开关类字段。
*
* @param columnName 字段名称
* @return 是否开关类字段
*/
private static boolean isSwitchColumn(String columnName) {
return StringUtils.endsWithAny(columnName, "status", "flag", "enabled", "disabled", "available", "visible")
|| columnName.startsWith("is_")
|| columnName.startsWith("has_")
|| columnName.startsWith("enable_")
|| columnName.startsWith("disable_");
}
/**
* 判断字段名称是否为排序字段。
*
* @param columnName 字段名称
* @return 是否排序字段
*/
private static boolean isSortColumn(String columnName) {
return StringUtils.endsWithAny(columnName, "sort", "order_num", "order", "rank", "seq", "sequence");
}
/**
* 校验数组是否包含指定值
*
* @param arr 数组
* @param targetValue 值
* @return 是否包含
*/
public static boolean arraysContains(String[] arr, String targetValue) {
return Arrays.asList(arr).contains(targetValue);
}
/**
* 获取模块名
*
* @param packageName 包名
* @return 模块名
*/
public static String getModuleName(String packageName) {
int lastIndex = packageName.lastIndexOf(".");
int nameLength = packageName.length();
return StringUtils.substring(packageName, lastIndex + 1, nameLength);
}
/**
* 获取业务名
*
* @param tableName 表名
* @return 业务名
*/
public static String getBusinessName(String tableName) {
int firstIndex = tableName.indexOf("_");
int nameLength = tableName.length();
String businessName = StringUtils.substring(tableName, firstIndex + 1, nameLength);
businessName = StringUtils.toCamelCase(businessName);
return businessName;
}
/**
* 表名转换成Java类名
*
* @param tableName 表名称
* @return 类名
*/
public static String convertClassName(String tableName) {
boolean autoRemovePre = PROPERTIES.isAutoRemovePre();
String tablePrefix = PROPERTIES.getTablePrefix();
if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) {
String[] searchList = StringUtils.split(tablePrefix, StringUtils.SEPARATOR);
tableName = replaceFirst(tableName, searchList);
}
return StringUtils.convertToCamelCase(tableName);
}
/**
* 批量替换前缀
*
* @param replacementm 替换值
* @param searchList 替换列表
* @return 去除命中前缀后的字符串
*/
public static String replaceFirst(String replacementm, String[] searchList) {
String text = replacementm;
for (String searchString : searchList) {
if (replacementm.startsWith(searchString)) {
text = StringUtils.removeStart(replacementm, searchString);
break;
}
}
return text;
}
/**
* 关键字替换
*
* @param text 需要被替换的名字
* @return 替换后的名字
*/
public static String replaceText(String text) {
return RegExUtils.replaceAll(text, "(?:表|若依)", "");
}
/**
* 获取数据库类型字段
*
* @param columnType 列类型
* @return 截取后的列类型
*/
public static String getDbType(String columnType) {
if (StringUtils.indexOf(columnType, "(") > 0) {
return StringUtils.substringBefore(columnType, "(");
} else {
return columnType;
}
}
/**
* 获取字段长度
*
* @param columnType 列类型
* @return 字段长度,未声明长度时返回 0
*/
public static Integer getColumnLength(String columnType) {
if (StringUtils.indexOf(columnType, "(") > 0) {
String length = StringUtils.substringBetween(columnType, "(", ")").trim();
// 处理 decimal(10,2) 这类带精度的类型,只取长度部分
if (length.contains(",")) {
length = StringUtils.substringBefore(length, ",").trim();
}
return Integer.valueOf(length);
} else {
return 0;
}
}
/**
* 获取字段精度
*
* @param columnType 列类型
* @return 字段精度,未声明精度时返回 0
*/
public static Integer getColumnScale(String columnType) {
if (StringUtils.indexOf(columnType, "(") > 0) {
String length = StringUtils.substringBetween(columnType, "(", ")").trim();
if (length.contains(",")) {
return Integer.valueOf(StringUtils.substringAfter(length, ",").trim());
}
}
return 0;
}
}

View File

@@ -0,0 +1,763 @@
package org.dromara.gen.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.gen.constant.GenConstants;
import org.dromara.gen.domain.GenTable;
import org.dromara.gen.domain.GenTableColumn;
import org.dromara.gen.util.template.PathNamedTemplate;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
/**
* 模板引擎工具
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TemplateEngineUtils {
/**
* 项目空间路径
*/
private static final String PROJECT_PATH = "main/java";
/**
* mybatis空间路径
*/
private static final String MYBATIS_PATH = "main/resources/mapper";
/**
* 默认上级菜单,系统工具
*/
private static final String DEFAULT_PARENT_MENU_ID = "1761400000000000003";
// 模板引擎
private static final TemplateEngine TEMPLATE_ENGINE;
private static final Map<String, PathNamedTemplate> TEMPLATE_MAPPER;
private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver();
static {
// 模板引擎初始化
TemplateConfig templateConfig = new TemplateConfig(StandardCharsets.UTF_8, StringUtils.EMPTY, TemplateConfig.ResourceMode.CLASSPATH);
templateConfig.setCustomEngine(FreemarkerEngine.class);
TEMPLATE_ENGINE = TemplateUtil.createEngine(templateConfig);
// 禁用数字千分位格式化 避免 Long 类型的 ID 渲染是被加上逗号
if (TEMPLATE_ENGINE instanceof FreemarkerEngine freemarkerEngine) {
freemarkerEngine.getConfiguration().setNumberFormat("computer");
}
TEMPLATE_MAPPER = PathNamedTemplate.form(TEMPLATE_ENGINE, GenConstants.TEMPLATE_PATHS);
}
/**
* 构建模板上下文
*
* @param contextInit 模板上下文初始化函数
* @return 模板上下文
*/
public static Dict buildContext(Consumer<Dict> contextInit) {
Dict context = new Dict();
contextInit.accept(context);
return context;
}
/**
* 构建模板上下文
*
* @param genTable 代码生成业务表对象
* @return 模板上下文
*/
public static Dict buildContext(GenTable genTable) {
// 构建上下文
Dict context = new Dict();
String moduleName = genTable.getModuleName();
String businessName = genTable.getBusinessName();
String packageName = genTable.getPackageName();
String tplCategory = genTable.getTplCategory();
String functionName = genTable.getFunctionName();
context.put("tplCategory", genTable.getTplCategory());
context.put("frontendType", resolveFrontendType(genTable.getFrontendType()));
context.put("tableName", genTable.getTableName());
context.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
context.put("ClassName", genTable.getClassName());
context.put("className", StringUtils.uncapitalize(genTable.getClassName()));
context.put("moduleName", moduleName);
context.put("BusinessName", StringUtils.capitalize(businessName));
context.put("businessName", businessName);
context.put("basePackage", getPackagePrefix(packageName));
context.put("packageName", packageName);
context.put("author", genTable.getFunctionAuthor());
context.put("datetime", DateUtils.now());
context.put("pkColumn", genTable.getPkColumn());
String dicts = getDicts(genTable);
context.put("importList", getImportList(genTable));
context.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
context.put("columns", genTable.getColumns());
context.put("table", genTable);
context.put("dicts", dicts);
context.put("dictsNoSymbol", StringUtils.replace(dicts, "'", StringUtils.EMPTY));
setColumnFeatureContext(context, genTable, dicts);
// 向模板上下文写入菜单相关变量
String options = genTable.getOptions();
Dict paramsObj = JsonUtils.parseMap(options);
if (ObjectUtil.isNull(paramsObj)) {
paramsObj = new Dict();
}
String parentMenuId = getParentMenuId(paramsObj);
context.put("parentMenuId", parentMenuId);
boolean enableExport = getBooleanOption(paramsObj, GenConstants.ENABLE_EXPORT, true);
boolean enableStatus = getBooleanOption(paramsObj, GenConstants.ENABLE_STATUS, false);
boolean enableUnique = getBooleanOption(paramsObj, GenConstants.ENABLE_UNIQUE, false);
boolean enableSort = getBooleanOption(paramsObj, GenConstants.ENABLE_SORT, false);
GenTableColumn statusColumn = getColumn(genTable, paramsObj.getStr(GenConstants.STATUS_FIELD));
GenTableColumn sortColumn = getColumn(genTable, paramsObj.getStr(GenConstants.SORT_FIELD));
List<GenTableColumn> uniqueColumns = getColumns(genTable, paramsObj.get(GenConstants.UNIQUE_FIELDS));
context.put("enableExport", enableExport);
context.put("enableStatus", enableStatus && ObjectUtil.isNotNull(statusColumn));
context.put("statusColumn", statusColumn);
context.put("statusField", ObjectUtil.isNotNull(statusColumn) ? statusColumn.getJavaField() : StringUtils.EMPTY);
context.put("enableUnique", enableUnique && CollUtil.isNotEmpty(uniqueColumns));
context.put("uniqueColumns", uniqueColumns);
context.put("enableSort", enableSort && ObjectUtil.isNotNull(sortColumn));
context.put("sortColumn", sortColumn);
context.put("sortField", ObjectUtil.isNotNull(sortColumn) ? sortColumn.getJavaField() : StringUtils.EMPTY);
context.put("statusActiveValue", ObjectUtil.isNotNull(statusColumn) ? statusColumn.getSwitchActiveValue() : StringUtils.EMPTY);
context.put("statusInactiveValue", ObjectUtil.isNotNull(statusColumn) ? statusColumn.getSwitchInactiveValue() : StringUtils.EMPTY);
// 向树形模板上下文写入树字段相关变量
if (GenConstants.TPL_TREE.equals(tplCategory)) {
setTreeContext(context, genTable, paramsObj);
}
return context;
}
/**
* 写入模板中常用的列派生开关,避免在模板内反复扫描和处理。
*
* @param context 模板上下文
* @param genTable 代码生成业务表对象
* @param dicts 字典类型表达式
*/
private static void setColumnFeatureContext(Dict context, GenTable genTable, String dicts) {
boolean hasBetween = false;
boolean needImagePreview = false;
boolean needImageUpload = false;
boolean needFileUpload = false;
boolean needEditor = false;
boolean needCheckbox = false;
boolean needSelect = false;
boolean needTextArea = false;
boolean needDigit = false;
boolean needDateField = false;
boolean needSwitchField = false;
boolean needParseTime = false;
String firstTreeListField = StringUtils.EMPTY;
for (GenTableColumn column : genTable.getColumns()) {
boolean writable = column.isInsert() || column.isEdit();
hasBetween = hasBetween || column.isQuery() && StringUtils.equals(column.getQueryType(), GenConstants.QUERY_BETWEEN);
needImagePreview = needImagePreview || column.isList() && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_IMAGE_UPLOAD);
needImageUpload = needImageUpload || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_IMAGE_UPLOAD);
needFileUpload = needFileUpload || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_FILE_UPLOAD);
needEditor = needEditor || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_EDITOR);
needCheckbox = needCheckbox || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_CHECKBOX);
needSelect = needSelect || (writable || column.isQuery()) && StringUtils.equalsAny(column.getHtmlType(),
GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_SWITCH);
needTextArea = needTextArea || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_TEXTAREA);
needDigit = needDigit || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_INPUT_NUMBER);
needDateField = needDateField || writable && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
needSwitchField = needSwitchField || (writable || column.isList()) && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_SWITCH);
needParseTime = needParseTime || column.isList() && StringUtils.equals(column.getHtmlType(), GenConstants.HTML_DATETIME);
if (StringUtils.isBlank(firstTreeListField) && column.isList()) {
firstTreeListField = column.getJavaField();
}
}
boolean needDateRange = hasDateRangeQuery(genTable);
context.put("hasBetween", hasBetween);
context.put("needAddDateRange", needDateRange);
context.put("needDateRange", needDateRange);
context.put("needDict", StringUtils.isNotBlank(dicts));
context.put("needImagePreview", needImagePreview);
context.put("needImageUpload", needImageUpload);
context.put("needFileUpload", needFileUpload);
context.put("needEditor", needEditor);
context.put("needCheckbox", needCheckbox);
context.put("needSelect", needSelect);
context.put("needTextArea", needTextArea);
context.put("needDigit", needDigit);
context.put("needDateField", needDateField);
context.put("needSwitchField", needSwitchField);
context.put("needParseTime", needParseTime);
context.put("firstTreeListField", firstTreeListField);
}
private static boolean hasDateRangeQuery(GenTable genTable) {
for (GenTableColumn column : genTable.getColumns()) {
if (column.isDateRangeQuery()) {
return true;
}
}
return false;
}
/**
* 向树形模板上下文写入树字段相关变量。
*
* @param context 模板上下文
* @param genTable 代码生成业务表对象
* @param paramsObj 已解析的 options 参数(避免重复解析)
*/
public static void setTreeContext(Dict context, GenTable genTable, Dict paramsObj) {
String treeCode = getTreeCode(paramsObj);
String treeParentCode = getTreeParentCode(paramsObj);
String treeName = getTreeName(paramsObj);
GenTableColumn treeParentColumn = getColumn(genTable, paramsObj.getStr(GenConstants.TREE_PARENT_CODE));
GenTableColumn treeAncestorsColumn = getColumn(genTable, paramsObj.getStr(GenConstants.TREE_ANCESTORS));
GenTableColumn treeOrderColumn = getColumn(genTable, paramsObj.getStr(GenConstants.TREE_ORDER_FIELD));
String treeRootValue = getTreeRootValue(paramsObj, treeParentColumn);
context.put("treeCode", treeCode);
context.put("treeParentCode", treeParentCode);
context.put("treeName", treeName);
context.put("treeParentColumn", treeParentColumn);
context.put("treeAncestorsField", ObjectUtil.isNotNull(treeAncestorsColumn) ? treeAncestorsColumn.getJavaField() : StringUtils.EMPTY);
context.put("treeOrderField", ObjectUtil.isNotNull(treeOrderColumn) ? treeOrderColumn.getJavaField() : StringUtils.EMPTY);
context.put("treeOrderColumn", treeOrderColumn);
context.put("treeRootValue", treeRootValue);
context.put("treeRootValueJavaLiteral", getJavaLiteral(treeParentColumn, treeRootValue));
context.put("treeRootValueTsLiteral", getTsLiteral(treeParentColumn, treeRootValue));
context.put("treeParentCap", StringUtils.capitalize(treeParentCode));
context.put("treeAncestorsCap", StringUtils.capitalize(ObjectUtil.isNotNull(treeAncestorsColumn) ? treeAncestorsColumn.getJavaField() : StringUtils.EMPTY));
context.put("treeOrderCap", StringUtils.capitalize(ObjectUtil.isNotNull(treeOrderColumn) ? treeOrderColumn.getJavaField() : StringUtils.EMPTY));
String expandTreeName = paramsObj.getStr(GenConstants.TREE_NAME);
int expandColumn = 0;
for (GenTableColumn column : genTable.getColumns()) {
if (column.isList()) {
expandColumn++;
if (column.getColumnName().equals(expandTreeName)) {
break;
}
}
}
context.put("expandColumn", expandColumn);
if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) {
context.put("tree_parent_code", paramsObj.get(GenConstants.TREE_PARENT_CODE));
}
if (paramsObj.containsKey(GenConstants.TREE_NAME)) {
context.put("tree_name", paramsObj.get(GenConstants.TREE_NAME));
}
}
/**
* 获取模板信息
*
* @return 模板列表
*/
public static List<PathNamedTemplate> getTemplateList(String tplCategory, String dsName) {
return getTemplateList(tplCategory, dsName, GenConstants.FRONTEND_TYPE_VUE);
}
/**
* 获取模板信息
*
* @param tplCategory 前端页面模板分类
* @param dsName 数据源名称
* @param frontendType 前端模板类型
* @return 模板列表
*/
public static List<PathNamedTemplate> getTemplateList(String tplCategory, String dsName, String frontendType) {
List<PathNamedTemplate> templates = new ArrayList<>();
// 后端源码模板
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_DOMAIN_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_VO_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_BO_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_MAPPER_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_SERVICE_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_SERVICE_IMPL_TEMPLATE_PATH));
templates.add(TEMPLATE_MAPPER.get(GenConstants.JAVA_CONTROLLER_TEMPLATE_PATH));
// MyBatis MapperXML 模板
templates.add(TEMPLATE_MAPPER.get(GenConstants.XML_MAPPER_TEMPLATE_PATH));
// 前端 API 与类型模板
templates.add(getTemplate(getFrontendApiTemplatePath(frontendType)));
templates.add(getTemplate(getFrontendTypesTemplatePath(frontendType)));
// 数据库模板
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType(dsName);
if (dataBaseType.isOracle()) {
templates.add(TEMPLATE_MAPPER.get(GenConstants.SQL_ORACLE_TEMPLATE_PATH));
} else if (dataBaseType.isPostgreSql()) {
templates.add(TEMPLATE_MAPPER.get(GenConstants.SQL_POSTGRES_TEMPLATE_PATH));
} else if (dataBaseType.isSqlServer()) {
templates.add(TEMPLATE_MAPPER.get(GenConstants.SQL_SQLSERVER_TEMPLATE_PATH));
} else {
// 默认使用MySQL模板
templates.add(TEMPLATE_MAPPER.get(GenConstants.SQL_MYSQL_TEMPLATE_PATH));
}
// 前端页面模板
if (GenConstants.TPL_CRUD.equals(tplCategory)) {
templates.add(getTemplate(getFrontendIndexTemplatePath(frontendType)));
} else if (GenConstants.TPL_TREE.equals(tplCategory)) {
templates.add(getTemplate(getFrontendIndexTreeTemplatePath(frontendType)));
}
return templates;
}
/**
* 获取模板,支持按前端模板类型动态加载模板根目录下的模板。
*
* @param templatePath 模板路径
* @return 带路径名的模板
*/
private static PathNamedTemplate getTemplate(String templatePath) {
PathNamedTemplate template = TEMPLATE_MAPPER.get(templatePath);
if (ObjectUtil.isNotNull(template)) {
return template;
}
return PathNamedTemplate.form(TEMPLATE_ENGINE, templatePath);
}
/**
* 获取文件名
*
* @param template 模板路径
* @param genTable 代码生成业务表对象
* @return 模板对应的目标文件相对路径
*/
public static String getFileName(String template, GenTable genTable) {
// 文件名称
String fileName = "";
// 包路径
String packageName = genTable.getPackageName();
// 模块名
String moduleName = genTable.getModuleName();
// 大写类名
String className = genTable.getClassName();
// 业务名称
String businessName = genTable.getBusinessName();
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String frontendPath = getFrontendPath(genTable.getFrontendType());
String frontendPagePath = getFrontendPagePath(genTable.getFrontendType());
// templatePath
// genFilePathFormat
if (template.contains("domain.java.")) {
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
} else if (template.contains("vo.java.")) {
fileName = StringUtils.format("{}/domain/vo/{}Vo.java", javaPath, className);
} else if (template.contains("bo.java.")) {
fileName = StringUtils.format("{}/domain/bo/{}Bo.java", javaPath, className);
} else if (template.contains("mapper.java.")) {
fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className);
} else if (template.contains("service.java.")) {
fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className);
} else if (template.contains("serviceImpl.java.")) {
fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className);
} else if (template.contains("controller.java.")) {
fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className);
} else if (template.contains("mapper.xml.")) {
fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className);
} else if (template.contains("sql.")) {
fileName = businessName + "Menu.sql";
} else if (template.contains("api.ts.")) {
fileName = StringUtils.format("{}/api/{}/{}/index.ts", frontendPath, moduleName, businessName);
} else if (template.contains("types.ts.")) {
fileName = StringUtils.format("{}/api/{}/{}/types.ts", frontendPath, moduleName, businessName);
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX));
} else if (isFrontendPageTemplate(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX)) {
fileName = StringUtils.format("{}/{}/{}/{}/index.{}", frontendPath, frontendPagePath, moduleName, businessName,
getFrontendPageExtension(template, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX));
}
return fileName;
}
/**
* 获取前端输出目录。
*
* @param frontendType 前端模板类型
* @return 前端输出目录
*/
private static String getFrontendPath(String frontendType) {
return resolveFrontendType(frontendType);
}
/**
* 获取前端页面输出目录。
*
* @param frontendType 前端模板类型
* @return 页面输出目录
*/
private static String getFrontendPagePath(String frontendType) {
return GenConstants.FRONTEND_TYPE_REACT.equals(resolveFrontendType(frontendType)) ? "pages" : "views";
}
/**
* 获取前端页面文件扩展名。
*
* @param template 模板路径
* @param templatePrefix 模板文件名前缀
* @return 文件扩展名
*/
private static String getFrontendPageExtension(String template, String templatePrefix) {
String fileName = getTemplateFileName(template);
int suffixIndex = fileName.lastIndexOf('.');
String withoutTemplateSuffix = suffixIndex >= 0 ? fileName.substring(0, suffixIndex) : fileName;
return withoutTemplateSuffix.substring((templatePrefix + ".").length());
}
private static String getFrontendApiTemplatePath(String frontendType) {
return getFrontendTemplatePath(frontendType, GenConstants.FRONTEND_API_TEMPLATE_NAME);
}
private static String getFrontendTypesTemplatePath(String frontendType) {
return getFrontendTemplatePath(frontendType, GenConstants.FRONTEND_TYPES_TEMPLATE_NAME);
}
private static String getFrontendIndexTemplatePath(String frontendType) {
return getFrontendPageTemplatePath(frontendType, GenConstants.FRONTEND_INDEX_TEMPLATE_PREFIX);
}
private static String getFrontendIndexTreeTemplatePath(String frontendType) {
return getFrontendPageTemplatePath(frontendType, GenConstants.FRONTEND_INDEX_TREE_TEMPLATE_PREFIX);
}
/**
* 解析前端模板类型。
*
* @param frontendType 前端模板类型
* @return 已规范化的模板目录名
*/
private static String resolveFrontendType(String frontendType) {
String type = StringUtils.blankToDefault(frontendType, GenConstants.FRONTEND_TYPE_VUE);
if (!type.matches("[A-Za-z0-9_-]+")) {
throw new ServiceException("前端模板类型仅支持字母、数字、下划线和中划线");
}
return type;
}
private static String getFrontendTemplatePath(String frontendType, String templateName) {
return StringUtils.format("{}/{}/{}", GenConstants.TEMPLATE_ROOT_PATH, resolveFrontendType(frontendType), templateName);
}
/**
* 获取前端页面模板路径,页面输出扩展名由模板文件名决定。
*
* @param frontendType 前端模板类型
* @param templatePrefix 页面模板文件名前缀
* @return 页面模板路径
*/
private static String getFrontendPageTemplatePath(String frontendType, String templatePrefix) {
String type = resolveFrontendType(frontendType);
String templatePathPattern = getFrontendPageTemplatePathPattern(type, templatePrefix);
String resourcePattern = GenConstants.TEMPLATE_RESOURCE_PREFIX + templatePathPattern;
try {
Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources(resourcePattern);
if (resources.length == 0) {
throw new ServiceException(StringUtils.format("未找到前端模板: {}", templatePathPattern));
}
return Arrays.stream(resources)
.map(Resource::getFilename)
.filter(StringUtils::isNotBlank)
.sorted()
.findFirst()
.map(fileName -> StringUtils.format("{}/{}/{}", GenConstants.TEMPLATE_ROOT_PATH, type, fileName))
.orElseThrow(() -> new ServiceException(StringUtils.format("未找到前端模板: {}", templatePathPattern)));
} catch (IOException e) {
throw new ServiceException(StringUtils.format("读取前端模板失败: {}", templatePathPattern), e);
}
}
private static String getFrontendPageTemplatePathPattern(String frontendType, String templatePrefix) {
return StringUtils.format("{}/{}/{}{}", GenConstants.TEMPLATE_ROOT_PATH, frontendType,
templatePrefix, GenConstants.TEMPLATE_PAGE_PATTERN_SUFFIX);
}
private static boolean isFrontendPageTemplate(String template, String templatePrefix) {
String fileName = getTemplateFileName(template);
return fileName.startsWith(templatePrefix + ".") && fileName.endsWith(GenConstants.TEMPLATE_FILE_SUFFIX);
}
private static String getTemplateFileName(String template) {
int index = Math.max(template.lastIndexOf('/'), template.lastIndexOf('\\'));
return index >= 0 ? template.substring(index + 1) : template;
}
/**
* 获取包前缀
*
* @param packageName 包名称
* @return 包前缀名称
*/
public static String getPackagePrefix(String packageName) {
int lastIndex = packageName.lastIndexOf(".");
return StringUtils.substring(packageName, 0, lastIndex);
}
/**
* 根据列类型获取导入包
*
* @param genTable 业务表对象
* @return 返回需要导入的包列表
*/
public static HashSet<String> getImportList(GenTable genTable) {
List<GenTableColumn> columns = genTable.getColumns();
HashSet<String> importList = new HashSet<>();
for (GenTableColumn column : columns) {
if (needImportColumn(column) && GenConstants.TYPE_DATE.equals(column.getJavaType())) {
importList.add("java.time.LocalDateTime");
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
} else if (needImportColumn(column) && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
importList.add("java.math.BigDecimal");
}
}
return importList;
}
/**
* 判断列是否需要生成类型导入。
*
* @param column 列对象
* @return 需要生成类型导入返回 {@code true}
*/
private static boolean needImportColumn(GenTableColumn column) {
return !column.isSuperColumn() || column.isQuery();
}
/**
* 根据列类型获取字典组
*
* @param genTable 业务表对象
* @return 返回字典组
*/
public static String getDicts(GenTable genTable) {
List<GenTableColumn> columns = genTable.getColumns();
Set<String> dicts = new HashSet<>();
addDicts(dicts, columns);
return StringUtils.join(dicts, ", ");
}
/**
* 添加字典列表
*
* @param dicts 字典列表
* @param columns 列集合
*/
public static void addDicts(Set<String> dicts, List<GenTableColumn> columns) {
for (GenTableColumn column : columns) {
if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny(
column.getHtmlType(),
GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX, GenConstants.HTML_SWITCH)) {
dicts.add("'" + column.getDictType() + "'");
}
}
}
/**
* 获取权限前缀
*
* @param moduleName 模块名称
* @param businessName 业务名称
* @return 返回权限前缀
*/
public static String getPermissionPrefix(String moduleName, String businessName) {
return StringUtils.format("{}:{}", moduleName, businessName);
}
/**
* 获取上级菜单ID字段
*
* @param paramsObj 生成其他选项
* @return 上级菜单ID字段
*/
public static String getParentMenuId(Dict paramsObj) {
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)
&& StringUtils.isNotEmpty(paramsObj.getStr(GenConstants.PARENT_MENU_ID))) {
return paramsObj.getStr(GenConstants.PARENT_MENU_ID);
}
return DEFAULT_PARENT_MENU_ID;
}
/**
* 获取树编码
*
* @param paramsObj 生成其他选项
* @return 树编码
*/
public static String getTreeCode(Map<String, Object> paramsObj) {
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_CODE)) {
return StringUtils.toCamelCase(Convert.toStr(paramsObj.get(GenConstants.TREE_CODE)));
}
return StringUtils.EMPTY;
}
/**
* 获取树父编码
*
* @param paramsObj 生成其他选项
* @return 树父编码
*/
public static String getTreeParentCode(Dict paramsObj) {
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) {
return StringUtils.toCamelCase(paramsObj.getStr(GenConstants.TREE_PARENT_CODE));
}
return StringUtils.EMPTY;
}
/**
* 获取树名称
*
* @param paramsObj 生成其他选项
* @return 树名称
*/
public static String getTreeName(Dict paramsObj) {
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_NAME)) {
return StringUtils.toCamelCase(paramsObj.getStr(GenConstants.TREE_NAME));
}
return StringUtils.EMPTY;
}
/**
* 获取树根节点值。
*
* @param paramsObj 其他选项
* @param treeParentColumn 父节点字段
* @return 树根节点值
*/
public static String getTreeRootValue(Dict paramsObj, GenTableColumn treeParentColumn) {
String defaultValue = "0";
if (ObjectUtil.isNotNull(treeParentColumn) && StringUtils.equals(treeParentColumn.getJavaType(), GenConstants.TYPE_STRING)) {
defaultValue = "0";
}
if (CollUtil.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_ROOT_VALUE)) {
return StringUtils.blankToDefault(paramsObj.getStr(GenConstants.TREE_ROOT_VALUE), defaultValue);
}
return defaultValue;
}
/**
* 读取布尔类型生成选项。
*
* @param paramsObj 生成选项
* @param key 选项键
* @param defaultValue 默认值
* @return 选项值
*/
private static boolean getBooleanOption(Dict paramsObj, String key, boolean defaultValue) {
if (CollUtil.isEmpty(paramsObj) || !paramsObj.containsKey(key)) {
return defaultValue;
}
return Convert.toBool(paramsObj.get(key), defaultValue);
}
/**
* 根据字段名查找业务表字段。
*
* @param genTable 业务表
* @param field 字段名称或 Java 属性名
* @return 业务表字段
*/
private static GenTableColumn getColumn(GenTable genTable, String field) {
if (StringUtils.isBlank(field) || CollUtil.isEmpty(genTable.getColumns())) {
return null;
}
for (GenTableColumn column : genTable.getColumns()) {
if (StringUtils.equalsAny(field, column.getColumnName(), column.getJavaField())) {
return column;
}
}
return null;
}
/**
* 根据字段配置查找业务表字段列表。
*
* @param genTable 业务表
* @param fieldValues 字段配置值
* @return 业务表字段列表
*/
private static List<GenTableColumn> getColumns(GenTable genTable, Object fieldValues) {
List<String> fields = new ArrayList<>();
if (fieldValues instanceof Collection<?> collection) {
collection.stream().map(Convert::toStr).forEach(fields::add);
} else if (ObjectUtil.isNotNull(fieldValues)) {
fields.addAll(StringUtils.str2List(Convert.toStr(fieldValues), StringUtils.SEPARATOR, true, true));
}
List<GenTableColumn> columns = new ArrayList<>();
for (String field : fields) {
GenTableColumn column = getColumn(genTable, field);
if (ObjectUtil.isNotNull(column)) {
columns.add(column);
}
}
return columns;
}
/**
* 转换为 Java 字面量。
*
* @param column 业务表字段
* @param value 字段值
* @return Java 字面量
*/
private static String getJavaLiteral(GenTableColumn column, String value) {
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
return "null";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_LONG)) {
return value + "L";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_FLOAT)) {
return value + "F";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_DOUBLE)) {
return value + "D";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_BIGDECIMAL)) {
return "new java.math.BigDecimal(\"" + value + "\")";
}
if (StringUtils.equals(column.getJavaType(), GenConstants.TYPE_INTEGER)) {
return value;
}
return "\"" + value + "\"";
}
/**
* 转换为 TypeScript 字面量。
*
* @param column 业务表字段
* @param value 字段值
* @return TypeScript 字面量
*/
private static String getTsLiteral(GenTableColumn column, String value) {
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
return "undefined";
}
if (StringUtils.equalsAny(column.getJavaType(), GenConstants.TYPE_LONG, GenConstants.TYPE_INTEGER,
GenConstants.TYPE_DOUBLE, GenConstants.TYPE_FLOAT, GenConstants.TYPE_BIGDECIMAL)) {
return value;
}
return "'" + value + "'";
}
}

View File

@@ -0,0 +1,119 @@
package org.dromara.gen.util.template;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateEngine;
import lombok.Getter;
import java.io.File;
import java.io.OutputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 基于路径命名的模板委托实现
*
* @author 秋辞未寒
*/
public class PathNamedTemplate implements Template {
@Getter
private final String pathName;
private final Template delegate;
/**
* 创建基于路径命名的模板。
*
* @param pathName 路径名称
* @param delegate 委托模板
*/
private PathNamedTemplate(String pathName, Template delegate) {
this.pathName = pathName;
this.delegate = delegate;
}
/**
* 渲染模板到字符输出器。
*
* @param bindingMap 模板绑定数据
* @param writer 字符输出器
*/
@Override
public void render(Map<?, ?> bindingMap, Writer writer) {
delegate.render(bindingMap, writer);
}
/**
* 渲染模板到输出流。
*
* @param bindingMap 模板绑定数据
* @param out 输出流
*/
@Override
public void render(Map<?, ?> bindingMap, OutputStream out) {
delegate.render(bindingMap, out);
}
/**
* 渲染模板到文件。
*
* @param bindingMap 模板绑定数据
* @param file 输出文件
*/
@Override
public void render(Map<?, ?> bindingMap, File file) {
delegate.render(bindingMap, file);
}
/**
* 渲染模板为字符串。
*
* @param bindingMap 模板绑定数据
* @return 渲染结果
*/
@Override
public String render(Map<?, ?> bindingMap) {
return delegate.render(bindingMap);
}
/**
* 构建带路径名称的模板。
*
* @param pathName 路径名称
* @param delegate 模板对象
* @return 带路径名称的模板
*/
public static PathNamedTemplate form(String pathName, Template delegate) {
return new PathNamedTemplate(pathName, delegate);
}
/**
* 根据模板引擎和路径名称构建模板。
*
* @param templateEngine 模板引擎
* @param pathName 路径名称
* @return 带路径名称的模板
*/
public static PathNamedTemplate form(TemplateEngine templateEngine, String pathName) {
return new PathNamedTemplate(pathName, templateEngine.getTemplate(pathName));
}
/**
* 根据模板引擎和路径名称集合构建模板映射。
*
* @param templateEngine 模板引擎
* @param pathNames 路径名称集合
* @return 模板映射
*/
public static Map<String, PathNamedTemplate> form(TemplateEngine templateEngine, Set<String> pathNames) {
Map<String, PathNamedTemplate> result = new HashMap<>();
for (String pathName : pathNames) {
PathNamedTemplate pathNamedTemplate = form(templateEngine, pathName);
result.put(pathName, pathNamedTemplate);
}
return result;
}
}

View File

@@ -0,0 +1,62 @@
package ${packageName}.domain.bo;
import ${packageName}.domain.${ClassName};
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import java.io.Serial;
import java.io.Serializable;
<#if hasBetween>
import java.util.HashMap;
import java.util.Map;
</#if>
import lombok.Data;
import jakarta.validation.constraints.*;
<#list importList as import>
import ${import};
</#list>
/**
* ${functionName}业务对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
@Data
@AutoMapper(target = ${ClassName}.class, reverseConvertGenerate = false)
public class ${ClassName}Bo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
<#list columns as column>
<#if column.query || (!table.isSuperColumn(column.javaField) && (column.insert || column.edit))>
/**
* ${column.columnComment}
*/
<#if column.insert && column.edit>
<#assign Group = "AddGroup.class, EditGroup.class">
<#elseif column.insert>
<#assign Group = "AddGroup.class">
<#elseif column.edit>
<#assign Group = "EditGroup.class">
</#if>
<#if column.required && !table.isSuperColumn(column.javaField) && (column.insert || column.edit)>
<#if column.javaType == 'String'>
@NotBlank(message = "${column.columnComment}不能为空", groups = { ${Group} })
<#else>
@NotNull(message = "${column.columnComment}不能为空", groups = { ${Group} })
</#if>
</#if>
private ${column.javaType} ${column.javaField};
</#if>
</#list>
<#if hasBetween>
/**
* 查询参数
*/
private Map<String, Object> params = new HashMap<>();
</#if>
}

View File

@@ -0,0 +1,155 @@
package ${packageName}.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
<#if enableExport>
import jakarta.servlet.http.HttpServletResponse;
</#if>
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
<#if enableExport>
import org.dromara.common.excel.utils.ExcelBuilder;
</#if>
import ${packageName}.domain.vo.${ClassName}Vo;
import ${packageName}.domain.bo.${ClassName}Bo;
import ${packageName}.service.I${ClassName}Service;
<#if table.crud>
import org.dromara.common.core.domain.PageResult;
<#elseif table.tree>
</#if>
/**
* ${functionName}
*
* @author ${author}
* @date ${datetime}
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/${moduleName}/${businessName}")
public class ${ClassName}Controller extends BaseController {
private final I${ClassName}Service ${className}Service;
/**
* 查询${functionName}列表
*/
@SaCheckPermission("${permissionPrefix}:list")
@GetMapping("/list")
<#if table.crud>
public R<PageResult<${ClassName}Vo>> list(${ClassName}Bo bo, PageQuery pageQuery) {
return R.ok(${className}Service.queryPageList(bo, pageQuery));
}
<#elseif table.tree>
public R<List<${ClassName}Vo>> list(${ClassName}Bo bo) {
List<${ClassName}Vo> list = ${className}Service.queryList(bo);
return R.ok(list);
}
</#if>
<#if enableExport>
/**
* 导出${functionName}列表
*/
@SaCheckPermission("${permissionPrefix}:export")
@Log(title = "${functionName}", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(${ClassName}Bo bo, HttpServletResponse response) {
List<${ClassName}Vo> list = ${className}Service.queryList(bo);
ExcelBuilder.of(list, ${ClassName}Vo.class).sheetName("${functionName}").toResponse(response);
}
</#if>
/**
* 获取${functionName}详细信息
*
* @param ${pkColumn.javaField} 主键
*/
@SaCheckPermission("${permissionPrefix}:query")
@GetMapping("/{${pkColumn.javaField}}")
public R<${ClassName}Vo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable ${pkColumn.javaType} ${pkColumn.javaField}) {
return R.ok(${className}Service.queryById(${pkColumn.javaField}));
}
/**
* 新增${functionName}
*/
@SaCheckPermission("${permissionPrefix}:add")
@Log(title = "${functionName}", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody ${ClassName}Bo bo) {
<#if enableUnique>
if (!${className}Service.checkUnique(bo)) {
return R.fail("新增${functionName}失败,组合唯一字段已存在");
}
</#if>
return toAjax(${className}Service.insertByBo(bo));
}
/**
* 修改${functionName}
*/
@SaCheckPermission("${permissionPrefix}:edit")
@Log(title = "${functionName}", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody ${ClassName}Bo bo) {
<#if enableUnique>
if (!${className}Service.checkUnique(bo)) {
return R.fail("修改${functionName}失败,组合唯一字段已存在");
}
</#if>
return toAjax(${className}Service.updateByBo(bo));
}
<#if enableStatus>
/**
* 修改${functionName}状态
*/
@SaCheckPermission("${permissionPrefix}:edit")
@Log(title = "${functionName}", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public R<Void> changeStatus(@RequestBody ${ClassName}Bo bo) {
return toAjax(${className}Service.updateStatus(bo.get${pkColumn.capJavaField}(), bo.get${statusColumn.capJavaField}()));
}
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
*/
@SaCheckPermission("${permissionPrefix}:edit")
@Log(title = "${functionName}", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public R<Void> updateSort(@RequestBody ${ClassName}Bo bo) {
return toAjax(${className}Service.updateSort(bo.get${pkColumn.capJavaField}(), bo.get${sortColumn.capJavaField}()));
}
</#if>
/**
* 删除${functionName}
*
* @param ${pkColumn.javaField}s 主键串
*/
@SaCheckPermission("${permissionPrefix}:remove")
@Log(title = "${functionName}", businessType = BusinessType.DELETE)
@DeleteMapping("/{${pkColumn.javaField}s}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) {
return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkColumn.javaField}s), true));
}
}

View File

@@ -0,0 +1,48 @@
package ${packageName}.domain;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
<#list importList as import>
import ${import};
</#list>
import java.io.Serial;
/**
* ${functionName}对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("${tableName}")
public class ${ClassName} extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
<#list columns as column>
<#if !table.isSuperColumn(column.javaField)>
/**
* ${column.columnComment}
*/
<#if column.javaField=='delFlag'>
@TableLogic
</#if>
<#if column.javaField=='version'>
@Version
</#if>
<#if column.pk>
@TableId(value = "${column.columnName}")
<#elseif column.needTableField>
@TableField(value = "${column.columnName}")
</#if>
private ${column.javaType} ${column.javaField};
</#if>
</#list>
}

View File

@@ -0,0 +1,15 @@
package ${packageName}.mapper;
import ${packageName}.domain.${ClassName};
import ${packageName}.domain.vo.${ClassName}Vo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* ${functionName}Mapper接口
*
* @author ${author}
* @date ${datetime}
*/
public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
}

View File

@@ -0,0 +1,105 @@
package ${packageName}.service;
import ${packageName}.domain.vo.${ClassName}Vo;
import ${packageName}.domain.bo.${ClassName}Bo;
<#if table.crud>
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
</#if>
import java.util.Collection;
import java.util.List;
/**
* ${functionName}Service接口
*
* @author ${author}
* @date ${datetime}
*/
public interface I${ClassName}Service {
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} 主键
* @return ${functionName}
*/
${ClassName}Vo queryById(${pkColumn.javaType} ${pkColumn.javaField});
<#if table.crud>
/**
* 分页查询${functionName}列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return ${functionName}分页列表
*/
PageResult<${ClassName}Vo> queryPageList(${ClassName}Bo bo, PageQuery pageQuery);
</#if>
/**
* 查询符合条件的${functionName}列表
*
* @param bo 查询条件
* @return ${functionName}列表
*/
List<${ClassName}Vo> queryList(${ClassName}Bo bo);
<#if enableUnique>
/**
* 校验${functionName}是否满足组合唯一约束
*
* @param bo ${functionName}
* @return 是否唯一
*/
boolean checkUnique(${ClassName}Bo bo);
</#if>
/**
* 新增${functionName}
*
* @param bo ${functionName}
* @return 是否新增成功
*/
Boolean insertByBo(${ClassName}Bo bo);
/**
* 修改${functionName}
*
* @param bo ${functionName}
* @return 是否修改成功
*/
Boolean updateByBo(${ClassName}Bo bo);
<#if enableStatus>
/**
* 修改${functionName}状态
*
* @param ${pkColumn.javaField} 主键
* @param status 状态值
* @return 是否修改成功
*/
Boolean updateStatus(${pkColumn.javaType} ${pkColumn.javaField}, ${statusColumn.javaType} status);
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
*
* @param ${pkColumn.javaField} 主键
* @param sortValue 排序值
* @return 是否修改成功
*/
Boolean updateSort(${pkColumn.javaType} ${pkColumn.javaField}, ${sortColumn.javaType} sortValue);
</#if>
/**
* 校验并批量删除${functionName}信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
}

View File

@@ -0,0 +1,346 @@
package ${packageName}.service.impl;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
<#if table.crud>
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
</#if>
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
<#if enableUnique>
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
</#if>
import org.dromara.common.mybatis.core.query.QueryBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ${packageName}.domain.bo.${ClassName}Bo;
import ${packageName}.domain.vo.${ClassName}Vo;
import ${packageName}.domain.${ClassName};
import ${packageName}.mapper.${ClassName}Mapper;
import ${packageName}.service.I${ClassName}Service;
<#if table.tree>
import org.dromara.common.core.exception.ServiceException;
</#if>
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* ${functionName}Service业务层处理
*
* @author ${author}
* @date ${datetime}
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ${ClassName}ServiceImpl implements I${ClassName}Service {
private final ${ClassName}Mapper ${className}Mapper;
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} 主键
* @return ${functionName}
*/
@Override
public ${ClassName}Vo queryById(${pkColumn.javaType} ${pkColumn.javaField}) {
return ${className}Mapper.selectVoById(${pkColumn.javaField});
}
<#if table.crud>
/**
* 分页查询${functionName}列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return ${functionName}分页列表
*/
@Override
public PageResult<${ClassName}Vo> queryPageList(${ClassName}Bo bo, PageQuery pageQuery) {
LambdaQueryWrapper<${ClassName}> lqw = buildQueryWrapper(bo);
Page<${ClassName}Vo> result = ${className}Mapper.selectVoPage(pageQuery.build(), lqw);
return PageResult.build(result.getRecords(), result.getTotal());
}
</#if>
/**
* 查询符合条件的${functionName}列表
*
* @param bo 查询条件
* @return ${functionName}列表
*/
@Override
public List<${ClassName}Vo> queryList(${ClassName}Bo bo) {
LambdaQueryWrapper<${ClassName}> lqw = buildQueryWrapper(bo);
return ${className}Mapper.selectVoList(lqw);
}
<#if enableUnique>
/**
* 校验${functionName}是否满足组合唯一约束
*
* @param bo ${functionName}
* @return 是否唯一
*/
@Override
public boolean checkUnique(${ClassName}Bo bo) {
boolean hasUniqueValue = true;
<#list uniqueColumns as column>
<#if column.javaType == 'String'>
hasUniqueValue = hasUniqueValue && StringUtils.isNotBlank(bo.get${column.capJavaField}());
<#else>
hasUniqueValue = hasUniqueValue && bo.get${column.capJavaField}() != null;
</#if>
</#list>
if (!hasUniqueValue) {
return true;
}
LambdaQueryWrapper<${ClassName}> lqw = Wrappers.lambdaQuery();
<#list uniqueColumns as column>
lqw.eq(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}());
</#list>
lqw.ne(bo.get${pkColumn.capJavaField}() != null, ${ClassName}::get${pkColumn.capJavaField}, bo.get${pkColumn.capJavaField}());
return !${className}Mapper.exists(lqw);
}
</#if>
private LambdaQueryWrapper<${ClassName}> buildQueryWrapper(${ClassName}Bo bo) {
<#if hasBetween>
Map<String, Object> params = bo.getParams();
</#if>
return QueryBuilder.lambda(${ClassName}.class)
<#list columns as column>
<#if column.query>
<#assign queryType = column.queryType>
<#assign javaType = column.javaType>
<#assign AttrName = column.capJavaField>
<#assign mpMethod = column.queryType?lower_case>
<#if queryType != 'BETWEEN'>
<#if javaType == 'String'>
<#assign condition = 'StringUtils.isNotBlank(bo.get'+AttrName+'())'>
<#if queryType == 'LIKE'>
.likeIfText(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#elseif queryType == 'EQ'>
.eqIfText(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#elseif queryType == 'NE'>
.neIfText(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#else>
.${mpMethod}(${condition}, ${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
</#if>
<#else>
<#assign condition = 'bo.get'+AttrName+'() != null'>
<#if queryType == 'EQ'>
.eqIfPresent(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#elseif queryType == 'NE'>
.neIfPresent(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#elseif queryType == 'GT'>
.gtIfPresent(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#elseif queryType == 'LT'>
.ltIfPresent(${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
<#else>
.${mpMethod}(${condition}, ${ClassName}::get${column.capJavaField}, bo.get${column.capJavaField}())
</#if>
</#if>
<#else>
.betweenParams(${ClassName}::get${column.capJavaField}, params, "begin${column.capJavaField}", "end${column.capJavaField}")
</#if>
</#if>
</#list>
<#if table.tree && "" != treeAncestorsField>
.orderByAsc(${ClassName}::get${treeAncestorsCap})
</#if>
<#if table.tree && "" != treeParentCode>
.orderByAsc(${ClassName}::get${treeParentCap})
</#if>
<#if table.tree && "" != treeOrderField>
.orderByAsc(${ClassName}::get${treeOrderCap})
<#elseif enableSort>
.orderByAsc(${ClassName}::get${sortColumn.capJavaField})
</#if>
.orderByAsc(${ClassName}::get${pkColumn.capJavaField})
.build();
}
/**
* 新增${functionName}
*
* @param bo ${functionName}
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(${ClassName}Bo bo) {
${ClassName} add = MapstructUtils.convert(bo, ${ClassName}.class);
<#if table.tree>
fillTreeMetaBeforeSave(add, false);
</#if>
validEntityBeforeSave(add);
boolean flag = ${className}Mapper.insert(add) > 0;
if (flag) {
bo.set${pkColumn.capJavaField}(add.get${pkColumn.capJavaField}());
}
return flag;
}
/**
* 修改${functionName}
*
* @param bo ${functionName}
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(${ClassName}Bo bo) {
${ClassName} update = MapstructUtils.convert(bo, ${ClassName}.class);
<#if table.tree>
fillTreeMetaBeforeSave(update, true);
</#if>
validEntityBeforeSave(update);
return ${className}Mapper.updateById(update) > 0;
}
<#if enableStatus>
/**
* 修改${functionName}状态
*
* @param ${pkColumn.javaField} 主键
* @param status 状态值
* @return 是否修改成功
*/
@Override
public Boolean updateStatus(${pkColumn.javaType} ${pkColumn.javaField}, ${statusColumn.javaType} status) {
return ${className}Mapper.lambda()
.set(${ClassName}::get${statusColumn.capJavaField}, status)
.eq(${ClassName}::get${pkColumn.capJavaField}, ${pkColumn.javaField})
.update();
}
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
*
* @param ${pkColumn.javaField} 主键
* @param sortValue 排序值
* @return 是否修改成功
*/
@Override
public Boolean updateSort(${pkColumn.javaType} ${pkColumn.javaField}, ${sortColumn.javaType} sortValue) {
return ${className}Mapper.lambda()
.set(${ClassName}::get${sortColumn.capJavaField}, sortValue)
.eq(${ClassName}::get${pkColumn.capJavaField}, ${pkColumn.javaField})
.update();
}
</#if>
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(${ClassName} entity) {
// 可在此扩展通用业务校验
}
<#if table.tree>
private void fillTreeMetaBeforeSave(${ClassName} entity, boolean updateMode) {
<#if "" != treeParentCode>
if (entity.get${treeParentCap}() == null) {
entity.set${treeParentCap}(${treeRootValueJavaLiteral});
}
if (ObjectUtil.equal(entity.get${pkColumn.capJavaField}(), entity.get${treeParentCap}())) {
throw new ServiceException("${functionName}父节点不能选择自身");
}
<#if "" != treeAncestorsField>
${ClassName} parent = null;
if (!ObjectUtil.equal(entity.get${treeParentCap}(), ${treeRootValueJavaLiteral})) {
parent = ${className}Mapper.selectById(entity.get${treeParentCap}());
if (ObjectUtil.isNull(parent)) {
throw new ServiceException("${functionName}父节点不存在");
}
}
if (updateMode && entity.get${pkColumn.capJavaField}() != null && ObjectUtil.isNotNull(parent)
&& containsAncestor(parent.get${treeAncestorsCap}(), entity.get${pkColumn.capJavaField}())) {
throw new ServiceException("不能选择当前节点或其子节点作为父节点");
}
String newAncestors = resolveAncestors(entity.get${treeParentCap}(), parent);
if (updateMode && entity.get${pkColumn.capJavaField}() != null) {
${ClassName} oldEntity = ${className}Mapper.selectById(entity.get${pkColumn.capJavaField}());
if (ObjectUtil.isNull(oldEntity)) {
throw new ServiceException("${functionName}不存在,无法修改");
}
String oldAncestors = oldEntity.get${treeAncestorsCap}();
entity.set${treeAncestorsCap}(newAncestors);
if (!StringUtils.equals(oldAncestors, newAncestors)) {
updateChildrenAncestors(entity.get${pkColumn.capJavaField}(), newAncestors, oldAncestors);
}
} else {
entity.set${treeAncestorsCap}(newAncestors);
}
</#if>
</#if>
}
<#if "" != treeAncestorsField>
private String resolveAncestors(${treeParentColumn.javaType} parentId, ${ClassName} parent) {
if (ObjectUtil.equal(parentId, ${treeRootValueJavaLiteral})) {
return "${treeRootValue}";
}
String parentAncestors = parent.get${treeAncestorsCap}();
if (StringUtils.isBlank(parentAncestors)) {
return String.valueOf(parentId);
}
return parentAncestors + StringUtils.SEPARATOR + parentId;
}
private void updateChildrenAncestors(${pkColumn.javaType} currentId, String newAncestors, String oldAncestors) {
List<${ClassName}> children = ${className}Mapper.lambda()
.select(${ClassName}::get${pkColumn.capJavaField}, ${ClassName}::get${treeAncestorsCap})
.findInSet(currentId, ${ClassName}::get${treeAncestorsCap})
.list();
List<${ClassName}> updateList = new ArrayList<>();
for (${ClassName} child : children) {
String ancestors = child.get${treeAncestorsCap}();
if (StringUtils.isBlank(ancestors)) {
continue;
}
${ClassName} update = new ${ClassName}();
update.set${pkColumn.capJavaField}(child.get${pkColumn.capJavaField}());
update.set${treeAncestorsCap}(StringUtils.replaceOnce(ancestors, oldAncestors, newAncestors));
updateList.add(update);
}
if (!updateList.isEmpty()) {
${className}Mapper.updateBatchById(updateList);
}
}
private boolean containsAncestor(String ancestors, ${pkColumn.javaType} nodeId) {
for (String item : StringUtils.splitList(ancestors)) {
if (StringUtils.equals(item, String.valueOf(nodeId))) {
return true;
}
}
return false;
}
</#if>
</#if>
/**
* 校验并批量删除${functionName}信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid) {
if (isValid) {
// 可在此扩展删除前业务校验
}
return ${className}Mapper.deleteByIds(ids) > 0;
}
}

View File

@@ -0,0 +1,61 @@
package ${packageName}.domain.vo;
<#list importList as import>
import ${import};
</#list>
import ${packageName}.domain.${ClassName};
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* ${functionName}视图对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = ${ClassName}.class)
public class ${ClassName}Vo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
<#list columns as column>
<#if column.list>
/**
* ${column.columnComment}
*/
<#assign parentheseIndex = column.columnComment?index_of("(")>
<#if column.dictType?has_content>
@ExcelProperty(value = "${column.columnLabel}", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "${column.dictType}")
<#elseif parentheseIndex != -1>
@ExcelProperty(value = "${column.columnLabel}", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "${column.readConverterExp()}")
<#else>
@ExcelProperty(value = "${column.columnLabel}")
</#if>
private ${column.javaType} ${column.javaField};
<#if column.htmlType == "imageUpload">
/**
* ${column.columnComment}Url
*/
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "${column.javaField}")
private String ${column.javaField}Url;
</#if>
</#if>
</#list>
}

View File

@@ -0,0 +1,94 @@
import type { <#if !table.tree>PageResult, </#if> R } from '@/api/types';
import request from '@/api/request';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from './types';
/**
* 查询${functionName}列表
*/
export function list${BusinessName}(query?: ${BusinessName}Query) {
return request<R<<#if table.tree>${BusinessName}VO[]<#else> PageResult<${BusinessName}VO></#if>>>({
url: '/${moduleName}/${businessName}/list',
method: 'get',
params: query
});
}
/**
* 查询${functionName}详细
*/
export function get${BusinessName}(${pkColumn.javaField}: string | number) {
return request<R<${BusinessName}VO>>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
});
}
/**
* 新增${functionName}
*/
export function add${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'post',
data
});
}
/**
* 修改${functionName}
*/
export function update${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'put',
data
});
}
<#if enableStatus>
/**
* 修改${functionName}状态
*/
export function change${BusinessName}Status(
${pkColumn.javaField}: string | number,
${statusField}: <#if statusColumn.javaType == 'Boolean'>boolean<#elseif statusColumn.javaType == 'Integer' || statusColumn.javaType == 'Long'>number<#else> string</#if>
) {
return request<R>({
url: '/${moduleName}/${businessName}/changeStatus',
method: 'put',
data: {
${pkColumn.javaField},
${statusField}
}
});
}
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
*/
export function update${BusinessName}Sort(
${pkColumn.javaField}: string | number,
${sortField}: <#if sortColumn.javaType == 'LocalDateTime' || sortColumn.javaType == 'String'>string<#else> number</#if>
) {
return request<R>({
url: '/${moduleName}/${businessName}/updateSort',
method: 'put',
data: {
${pkColumn.javaField},
${sortField}
}
});
}
</#if>
/**
* 删除${functionName}
*/
export function del${BusinessName}(${pkColumn.javaField}: string | number | Array<string | number>) {
return request<R>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
});
}

View File

@@ -0,0 +1,424 @@
import { DeleteOutlined<#if enableExport>, DownloadOutlined</#if>, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
<#if needCheckbox>
ProFormCheckbox,
</#if>
<#if needDateField>
ProFormDateTimePicker,
</#if>
<#if needDigit>
ProFormDigit,
</#if>
<#if needSelect>
ProFormSelect,
</#if>
<#if needTextArea>
ProFormTextArea,
</#if>
ProFormText,
ProFormTreeSelect,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message<#if enableStatus || needSwitchField>, Switch</#if><#if enableSort>, InputNumber</#if> } from 'antd';
import { useMemo, useRef, useState } from 'react';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
<#if needDict>
import DictTag from '@/components/common/DictTag';
</#if>
<#if needFileUpload>
import FileUpload from '@/components/common/FileUpload';
</#if>
<#if needImagePreview>
import ImagePreview from '@/components/common/ImagePreview';
</#if>
<#if needImageUpload>
import ImageUpload from '@/components/common/ImageUpload';
</#if>
<#if needEditor>
import RichTextEditor from '@/components/common/RichTextEditor';
</#if>
import RowActions from '@/components/common/RowActions';
<#if needDict>
import { useDict } from '@/hooks/useDict';
</#if>
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTableScroll } from '@/hooks/useTableScroll';
import { useTreeTableExpand } from '@/hooks/useTreeTableExpand';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
import { dictOptions } from '@/utils/dict';
</#if>
<#if enableStatus>
import { confirmAction } from '@/utils/modal';
</#if>
import { hasPermi } from '@/utils/permission';
import { filterTree<#if needDateField>, formatDateTimeFields</#if>, handleTree<#if needDateField>, toDayjsFields</#if> } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
${treeParentCode}: ${treeRootValueTsLiteral},
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk && column.htmlType == "checkbox">
${column.javaField}: [],
</#if>
</#list>
};
interface ${BusinessName}SelectNode {
title: string;
value: string | number;
children?: ${BusinessName}SelectNode[];
}
function toTreeSelectData(nodes: ${BusinessName}VO[]): ${BusinessName}SelectNode[] {
return nodes.map(node => ({
title: String(node.${treeName} || ''),
value: node.${treeCode},
children: node.children ? toTreeSelectData(node.children) : undefined
}));
}
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const { tableScroll } = useTableScroll(1000);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
const dicts = useDict(${dicts});
</#if>
const [treeOptions, setTreeOptions] = useState<${BusinessName}VO[]>([]);
const [tableRows, setTableRows] = useState<${BusinessName}VO[]>([]);
const { expandAll, expandedRowKeys, onExpandedRowsChange, syncExpandedRows, toggleExpandAll } =
useTreeTableExpand<${BusinessName}VO>(row => row.${treeCode});
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [modalTitle, setModalTitle] = useState('');
<#if enableExport>
const { updateExportParams, exportFile } = useTableExport();
</#if>
const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']);
const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']);
const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']);
<#if enableExport>
const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']);
</#if>
const treeSelectData = useMemo(
() => [{ title: '顶级节点', value: ${treeRootValueTsLiteral}, children: toTreeSelectData(treeOptions) }],
[treeOptions]
);
const loadTreeOptions = async (excludeId?: string | number) => {
const res = await list${BusinessName}();
const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}');
setTreeOptions(excludeId ? filterTree(rows, node => node.${treeCode} !== excludeId) : rows);
};
const openAdd = async (row?: ${BusinessName}VO) => {
await loadTreeOptions();
form.resetFields();
form.setFieldsValue({ ...default${BusinessName}Form, ${treeParentCode}: row?.${treeCode} || treeRootValueTsLiteral });
setModalTitle('添加${functionName}');
openModal();
};
const openEdit = async (row: ${BusinessName}VO) => {
await loadTreeOptions(row.${treeCode});
const res = await get${BusinessName}(row.${pkColumn.javaField});
const data = <#if needDateField>toDayjsFields({ ...res.data }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...res.data }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
</#if>
</#list>
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = <#if needDateField>formatDateTimeFields({ ...values }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...values }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
</#if>
</#list>
submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues);
message.success('操作成功');
form.resetFields();
actionRef.current?.reload();
return true;
};
const remove = async (row: ${BusinessName}VO) => {
await del${BusinessName}(row.${pkColumn.javaField});
message.success('删除成功');
actionRef.current?.reload();
};
<#if enableStatus>
const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => {
const previousStatus = row.${statusField};
const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue;
const text = checked ? '启用' : '停用';
try {
await confirmAction(`确认要"${r'${text}'}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${r'${text}'}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
</#if>
<#if enableSort>
const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => {
await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0);
message.success('排序更新成功');
actionRef.current?.reload();
};
</#if>
const columns: ProColumns<${BusinessName}VO>[] = [
<#list columns as column>
<#if column.list && !column.pk>
<#if enableStatus && statusField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
<#if column.dictType?has_content>
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
</#if>
render: (_, row) => (
<Switch
checked={row.${column.javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
<#elseif enableSort && sortField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${column.javaField} as number} onChange={value => handleSortChange(row, value)} />
},
<#elseif column.htmlType == "datetime">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}', valueType: 'dateTime', search: <#if column.query>true<#else> false</#if>, width: 170 },
<#elseif column.htmlType == "imageUpload">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${column.javaField}Url || String(row.${column.javaField} || '')} width={50} height={50} />
},
<#elseif column.dictColumn>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
render: (_, row) => <DictTag options={dicts.${column.dictType}} value={row.${column.javaField}} />
},
<#elseif column.htmlType == "switch">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${column.javaField} === ${column.switchActiveValue}} disabled />
},
<#else>
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}'<#if !column.query>, search: false</#if> },
</#if>
</#if>
</#list>
{
title: '操作',
valueType: 'option',
width: 130,
fixed: 'right',
render: (_, row) => (
<RowActions
actions={[
canEdit && { key: 'edit', label: '修改', icon: <EditOutlined />, onClick: () => openEdit(row) },
canAdd && { key: 'add', label: '新增', icon: <PlusOutlined />, onClick: () => openAdd(row) },
canRemove && {
key: 'delete',
label: '删除',
icon: <DeleteOutlined />,
danger: true,
confirm: `是否确认删除${functionName}编号为"${r'${row.'}${pkColumn.javaField}${r'}'}"的数据项?`,
onClick: () => remove(row)
}
]}
/>
)
}
];
return (
<PageContainer title="${functionName}">
<ProTable<${BusinessName}VO, ${BusinessName}Query>
actionRef={actionRef}
rowKey="${treeCode}"
columns={columns}
scroll={tableScroll}
pagination={false}
search={{ labelWidth: 90 }}
expandable={{
expandedRowKeys,
onExpandedRowsChange
}}
request={async params => {
const res = await list${BusinessName}(params);
const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}');
setTableRows(rows);
syncExpandedRows(rows, expandAll);
<#if enableExport>
updateExportParams(params);
</#if>
return { data: rows, total: rows.length, success: true };
}}
toolbar={{ title: '${functionName}列表' }}
toolBarRender={() => [
canAdd && (
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openAdd()}>
新增
</Button>
),
<Button
key="expand"
icon={<SortAscendingOutlined />}
onClick={() => toggleExpandAll(tableRows)}
>
展开/折叠
</Button><#if enableExport>,
canExport && (
<Button
key="export"
icon={<DownloadOutlined />}
onClick={() => exportFile('/${moduleName}/${businessName}/export', () => `${businessName}_${r'${Date.now()}'}.xlsx`)}
>
导出
</Button>
)</#if>
]}
/>
<ModalForm<${BusinessName}Form>
title={modalTitle}
open={modalOpen}
width={560}
form={form}
layout="vertical"
initialValues={default${BusinessName}Form}
modalProps={{ destroyOnHidden: true, onCancel: closeModal }}
onOpenChange={open => !open && closeModal()}
onFinish={submitForm}
>
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if column.javaField == treeParentCode>
<ProFormTreeSelect
name="${column.javaField}"
label="${column.columnLabel}"
<#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>
fieldProps={{
allowClear: true,
treeDefaultExpandAll: true,
treeData: treeSelectData,
placeholder: '请选择${column.columnLabel}'
}}
/>
<#elseif column.htmlType == "input">
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "textarea">
<ProFormTextArea name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "inputNumber">
<ProFormDigit name="${column.javaField}" label="${column.columnLabel}" min={0} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif (column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch") && column.dictType?has_content>
<ProFormSelect name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<ProFormCheckbox.Group name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "switch">
<Form.Item
name="${column.javaField}"
label="${column.columnLabel}"
valuePropName="checked"
getValueProps={value => ({ checked: value === ${column.switchActiveValue} })}
normalize={checked => (checked ? ${column.switchActiveValue} : ${column.switchInactiveValue})}
>
<Switch />
</Form.Item>
<#elseif column.htmlType == "datetime">
<ProFormDateTimePicker name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "imageUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<ImageUpload />
</Form.Item>
<#elseif column.htmlType == "fileUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<FileUpload />
</Form.Item>
<#elseif column.htmlType == "editor">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<RichTextEditor />
</Form.Item>
<#else>
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
</#if>
</#if>
</#list>
</ModalForm>
</PageContainer>
);
}

View File

@@ -0,0 +1,406 @@
import { DeleteOutlined<#if enableExport>, DownloadOutlined</#if>, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
<#if needCheckbox>
ProFormCheckbox,
</#if>
<#if needDateField>
ProFormDateTimePicker,
</#if>
<#if needDigit>
ProFormDigit,
</#if>
<#if needSelect>
ProFormSelect,
</#if>
<#if needTextArea>
ProFormTextArea,
</#if>
ProFormText,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message, Popconfirm<#if enableStatus || needSwitchField>, Switch</#if><#if enableSort>, InputNumber</#if> } from 'antd';
import { useRef, useState } from 'react';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
<#if needDict>
import DictTag from '@/components/common/DictTag';
</#if>
<#if needFileUpload>
import FileUpload from '@/components/common/FileUpload';
</#if>
<#if needImagePreview>
import ImagePreview from '@/components/common/ImagePreview';
</#if>
<#if needImageUpload>
import ImageUpload from '@/components/common/ImageUpload';
</#if>
<#if needEditor>
import RichTextEditor from '@/components/common/RichTextEditor';
</#if>
import RowActions from '@/components/common/RowActions';
<#if needDict>
import { useDict } from '@/hooks/useDict';
</#if>
<#if needDateRange>
import { useDateRangeQuery } from '@/hooks/useDateRangeQuery';
</#if>
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTableScroll } from '@/hooks/useTableScroll';
import { useTableSelection } from '@/hooks/useTableSelection';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
import { dictOptions } from '@/utils/dict';
</#if>
<#if enableStatus>
import { confirmAction } from '@/utils/modal';
</#if>
import { hasPermi } from '@/utils/permission';
import { <#if needDateField>formatDateTimeFields, toDayjsFields, </#if> toPageQuery, toTableData } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk && column.htmlType == "checkbox">
${column.javaField}: [],
</#if>
</#list>
};
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const { tableScroll } = useTableScroll(1000);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
const dicts = useDict(${dicts});
</#if>
const { ids, selectedOne, handleSelectionChange, clearSelection } = useTableSelection<${BusinessName}VO>(
row => row.${pkColumn.javaField}
);
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [modalTitle, setModalTitle] = useState('');
<#if enableExport>
const { updateExportParams, exportFile } = useTableExport();
</#if>
<#list columns as column>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
const { applyDateRange: apply${column.capJavaField}DateRange } = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']);
const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']);
const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']);
<#if enableExport>
const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']);
</#if>
const openAdd = () => {
form.resetFields();
form.setFieldsValue(default${BusinessName}Form);
setModalTitle('添加${functionName}');
openModal();
};
const openEdit = async (row?: ${BusinessName}VO) => {
const target = row || selectedOne;
if (!target?.${pkColumn.javaField}) return;
const res = await get${BusinessName}(target.${pkColumn.javaField});
const data = <#if needDateField>toDayjsFields({ ...res.data }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...res.data }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
</#if>
</#list>
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = <#if needDateField>formatDateTimeFields({ ...values }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...values }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
</#if>
</#list>
submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues);
message.success('操作成功');
form.resetFields();
actionRef.current?.reload();
return true;
};
const remove = async (row?: ${BusinessName}VO) => {
await del${BusinessName}(row?.${pkColumn.javaField} || ids);
message.success('删除成功');
clearSelection();
actionRef.current?.reloadAndRest?.();
};
<#if enableStatus>
const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => {
const previousStatus = row.${statusField};
const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue;
const text = checked ? '启用' : '停用';
try {
await confirmAction(`确认要"${r'${text}'}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${r'${text}'}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
</#if>
<#if enableSort>
const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => {
await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0);
message.success('排序更新成功');
actionRef.current?.reload();
};
</#if>
const columns: ProColumns<${BusinessName}VO>[] = [
<#list columns as column>
<#if column.list>
<#if enableStatus && statusField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
<#if column.dictType?has_content>
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
</#if>
render: (_, row) => (
<Switch
checked={row.${column.javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
<#elseif enableSort && sortField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${column.javaField} as number} onChange={value => handleSortChange(row, value)} />
},
<#elseif column.htmlType == "datetime">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}', valueType: 'dateTime', search: <#if column.query && column.queryType != "BETWEEN">true<#else> false</#if>, width: 170 },
<#elseif column.htmlType == "imageUpload">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${column.javaField}Url || String(row.${column.javaField} || '')} width={50} height={50} />
},
<#elseif column.dictColumn>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
render: (_, row) => <DictTag options={dicts.${column.dictType}} value={row.${column.javaField}} />
},
<#elseif column.htmlType == "switch">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${column.javaField} === ${column.switchActiveValue}} disabled />
},
<#else>
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}'<#if !column.query>, search: false</#if> },
</#if>
</#if>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}Range', valueType: 'dateTimeRange', hideInTable: true },
</#if>
</#list>
{
title: '操作',
valueType: 'option',
width: 92,
fixed: 'right',
render: (_, row) => (
<RowActions
actions={[
canEdit && { key: 'edit', label: '修改', icon: <EditOutlined />, onClick: () => openEdit(row) },
canRemove && {
key: 'delete',
label: '删除',
icon: <DeleteOutlined />,
danger: true,
confirm: `是否确认删除${functionName}编号为"${r'${row.'}${pkColumn.javaField}${r'}'}"的数据项?`,
onClick: () => remove(row)
}
]}
/>
)
}
];
return (
<PageContainer title="${functionName}">
<ProTable<${BusinessName}VO, ${BusinessName}Query<#if needDateRange> & Record<string, unknown></#if>>
actionRef={actionRef}
rowKey="${pkColumn.javaField}"
columns={columns}
scroll={tableScroll}
search={{ labelWidth: 90 }}
pagination={{ defaultPageSize: 10, showSizeChanger: true }}
rowSelection={{ selectedRowKeys: ids, onChange: handleSelectionChange }}
request={async params => {
<#if needDateRange>
let query = toPageQuery(params);
<#list columns as column>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
query = apply${column.capJavaField}DateRange(query, params.${column.javaField}Range as [string, string] | undefined);
delete query.${column.javaField}Range;
</#if>
</#list>
<#else>
const query = toPageQuery(params);
</#if>
<#if enableExport>
updateExportParams(query);
</#if>
const res = await list${BusinessName}(query);
return toTableData(res);
}}
toolbar={{ title: '${functionName}列表' }}
toolBarRender={() => [
canAdd && (
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={openAdd}>
新增
</Button>
),
canEdit && (
<Button key="edit" disabled={!selectedOne} icon={<EditOutlined />} onClick={() => openEdit()}>
修改
</Button>
),
canRemove && (
<Popconfirm key="delete" title={`是否确认删除${functionName}编号为"${r'${ids}'}"的数据项?`} onConfirm={() => remove()}>
<Button danger disabled={!ids.length} icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
)<#if enableExport>,
canExport && (
<Button
key="export"
icon={<DownloadOutlined />}
onClick={() => exportFile('/${moduleName}/${businessName}/export', () => `${businessName}_${r'${Date.now()}'}.xlsx`)}
>
导出
</Button>
)</#if>
]}
/>
<ModalForm<${BusinessName}Form>
title={modalTitle}
open={modalOpen}
width={560}
form={form}
layout="vertical"
initialValues={default${BusinessName}Form}
modalProps={{ destroyOnHidden: true, onCancel: closeModal }}
onOpenChange={open => !open && closeModal()}
onFinish={submitForm}
>
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if column.htmlType == "input">
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "textarea">
<ProFormTextArea name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "inputNumber">
<ProFormDigit name="${column.javaField}" label="${column.columnLabel}" min={0} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif (column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch") && column.dictType?has_content>
<ProFormSelect name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<ProFormCheckbox.Group name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "switch">
<Form.Item
name="${column.javaField}"
label="${column.columnLabel}"
valuePropName="checked"
getValueProps={value => ({ checked: value === ${column.switchActiveValue} })}
normalize={checked => (checked ? ${column.switchActiveValue} : ${column.switchInactiveValue})}
>
<Switch />
</Form.Item>
<#elseif column.htmlType == "datetime">
<ProFormDateTimePicker name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "imageUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<ImageUpload />
</Form.Item>
<#elseif column.htmlType == "fileUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<FileUpload />
</Form.Item>
<#elseif column.htmlType == "editor">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<RichTextEditor />
</Form.Item>
<#else>
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
</#if>
</#if>
</#list>
</ModalForm>
</PageContainer>
);
}

View File

@@ -0,0 +1,54 @@
import type { BaseEntity<#if !table.tree>, PageQuery</#if> } from '@/api/types';
export interface ${BusinessName}VO {
<#list columns as column>
<#if column.list || column.pk>
/**
* ${column.columnComment}
*/
${column.javaField}: ${column.tsType};
<#if column.htmlType == "imageUpload">
/**
* ${column.columnComment}Url
*/
${column.javaField}Url?: string;
</#if>
</#if>
</#list>
<#if table.tree>
/**
* 子对象
*/
children?: ${BusinessName}VO[];
</#if>
}
export interface ${BusinessName}Form extends BaseEntity {
<#list columns as column>
<#if column.insert || column.edit || column.pk>
/**
* ${column.columnComment}
*/
<#if column.htmlType == "checkbox">
${column.javaField}?: string | string[];
<#else>
${column.javaField}?: ${column.tsType};
</#if>
</#if>
</#list>
}
export interface ${BusinessName}Query<#if !table.tree> extends PageQuery</#if> {
<#list columns as column>
<#if column.query>
/**
* ${column.columnComment}
*/
${column.javaField}?: ${column.tsType};
</#if>
</#list>
/**
* 日期范围参数
*/
params?: Record<string, unknown>;
}

View File

@@ -0,0 +1,20 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[0]}, '${functionName}', ${parentMenuId}, 1, '${businessName}', '${moduleName}/${businessName}/index', 'N', 'Y', 'C', '0', '0', '${permissionPrefix}:list', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '${functionName}菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[1]}, '${functionName}查询', ${table.menuIds[0]}, 1, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:query', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[2]}, '${functionName}新增', ${table.menuIds[0]}, 2, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:add', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:edit', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
</#if>

View File

@@ -0,0 +1,20 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[0]}, '${functionName}', ${parentMenuId}, 1, '${businessName}', '${moduleName}/${businessName}/index', 'N', 'Y', 'C', '0', '0', '${permissionPrefix}:list', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '${functionName}菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[1]}, '${functionName}查询', ${table.menuIds[0]}, 1, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:query', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[2]}, '${functionName}新增', ${table.menuIds[0]}, 2, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:add', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:edit', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
</#if>

View File

@@ -0,0 +1,20 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[0]}, '${functionName}', ${parentMenuId}, 1, '${businessName}', '${moduleName}/${businessName}/index', 'N', 'Y', 'C', '0', '0', '${permissionPrefix}:list', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '${functionName}菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[1]}, '${functionName}查询', ${table.menuIds[0]}, 1, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:query', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[2]}, '${functionName}新增', ${table.menuIds[0]}, 2, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:add', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:edit', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
</#if>

View File

@@ -0,0 +1,20 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[0]}, '${functionName}', ${parentMenuId}, 1, '${businessName}', '${moduleName}/${businessName}/index', 'N', 'Y', 'C', '0', '0', '${permissionPrefix}:list', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '${functionName}菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[1]}, '${functionName}查询', ${table.menuIds[0]}, 1, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:query', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[2]}, '${functionName}新增', ${table.menuIds[0]}, 2, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:add', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:edit', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
</#if>

View File

@@ -0,0 +1,101 @@
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
<#if !table.tree>
import type { PageResult } from '@/api/types';
</#if>
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
/**
* 查询${functionName}列表
* @param query
* @returns {*}
*/
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
return request({
url: '/${moduleName}/${businessName}/list',
method: 'get',
params: query
});
};
/**
* 查询${functionName}详细
* @param ${pkColumn.javaField}
*/
export const get${BusinessName} = (${pkColumn.javaField}: string | number): AxiosPromise<${BusinessName}VO> => {
return request({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
});
};
/**
* 新增${functionName}
* @param data
*/
export const add${BusinessName} = (data: ${BusinessName}Form) => {
return request({
url: '/${moduleName}/${businessName}',
method: 'post',
data: data
});
};
/**
* 修改${functionName}
* @param data
*/
export const update${BusinessName} = (data: ${BusinessName}Form) => {
return request({
url: '/${moduleName}/${businessName}',
method: 'put',
data: data
});
};
<#if enableStatus>
/**
* 修改${functionName}状态
* @param ${pkColumn.javaField}
* @param status
*/
export const change${BusinessName}Status = (${pkColumn.javaField}: string | number, status: <#if statusColumn.javaType == 'Boolean'>boolean<#elseif statusColumn.javaType == 'String'>string<#else> number</#if>) => {
return request({
url: '/${moduleName}/${businessName}/changeStatus',
method: 'put',
data: {
${pkColumn.javaField},
${statusField}: status
}
});
};
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
* @param ${pkColumn.javaField}
* @param sortValue
*/
export const update${BusinessName}Sort = (${pkColumn.javaField}: string | number, sortValue: <#if sortColumn.javaType == 'String' || sortColumn.javaType == 'LocalDateTime'>string<#else> number</#if>) => {
return request({
url: '/${moduleName}/${businessName}/updateSort',
method: 'put',
data: {
${pkColumn.javaField},
${sortField}: sortValue
}
});
};
</#if>
/**
* 删除${functionName}
* @param ${pkColumn.javaField}
*/
export const del${BusinessName} = (${pkColumn.javaField}: string | number | Array<string | number>) => {
return request({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
});
};

View File

@@ -0,0 +1,691 @@
<template>
<div class="p-2 page-shell ${moduleName}-${businessName}-page">
<div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header>
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
<div><h3>筛选条件</h3></div>
</div>
</template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<#list columns as column>
<#if column.query>
<#if column.htmlType == "input" || column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="queryParams.${column.javaField}" placeholder="请输入${column.columnLabel}" clearable @keyup.enter="handleQuery" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="queryParams.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<#if column.javaType == "Boolean">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
<#elseif column.javaType == "Integer" || column.javaType == "Long">
<el-option label="开启" :value="0" />
<el-option label="关闭" :value="1" />
<#else>
<el-option label="开启" value="0" />
<el-option label="关闭" value="1" />
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType != "BETWEEN">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN">
<el-form-item label="${column.columnLabel}" style="width: 308px">
<el-date-picker
v-model="dateRange${column.capJavaField}"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
</el-form-item>
</#if>
</#if>
</#list>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover" class="table-panel">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<h3>${functionName}列表</h3>
</div>
<div class="toolbar-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
<#if enableExport>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['${moduleName}:${businessName}:export']">导出</el-button>
</#if>
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
</div>
</div>
</template>
<el-table
ref="${businessName}TableRef"
v-loading="loading"
class="data-table"
:data="${businessName}List"
row-key="${treeCode}"
border
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<#assign firstTreeListField = "">
<#list columns as tempColumn>
<#if !tempColumn.pk && tempColumn.list && "" != tempColumn.javaField && firstTreeListField == "">
<#assign firstTreeListField = tempColumn.javaField>
</#if>
</#list>
<#list columns as column>
<#if column.pk>
<#elseif enableStatus && statusField == column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
</#if>
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<#elseif enableSort && sortField == column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="160">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="160">
</#if>
<template #default="scope">
<#if column.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${column.javaField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "switch">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="120">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="120">
</#if>
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
disabled
/>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "datetime">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="180">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="180">
</#if>
<template #default="scope">
<span>{{ parseTime(scope.row.${column.javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "imageUpload">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}Url" width="100">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}Url" width="100">
</#if>
<template #default="scope">
<image-preview :src="scope.row.${column.javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
<#elseif column.list && column.dictColumn>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
</#if>
<template #default="scope">
<#if column.htmlType == "checkbox">
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField} ? scope.row.${column.javaField}.split(',') : []"/>
<#else>
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField}"/>
</#if>
</template>
</el-table-column>
<#elseif column.list && "" != column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" />
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
</#if>
</#if>
</#list>
<#if enableStatus && !statusColumn.list>
<el-table-column label="${statusColumn.columnComment}" align="center" prop="${statusField}">
<template #default="scope">
<el-switch
v-model="scope.row.${statusField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
</#if>
<#if enableSort && !sortColumn.list>
<el-table-column label="${sortColumn.columnComment}" align="center" prop="${sortField}" width="160">
<template #default="scope">
<#if sortColumn.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${sortField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${sortColumn.columnComment}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${sortField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
</#if>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />
</el-tooltip>
<el-tooltip content="新增" placement="top">
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${moduleName}:${businessName}:add']" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if "" != treeParentCode && column.javaField == treeParentCode>
<el-form-item label="${column.columnLabel}" prop="${treeParentCode}">
<el-tree-select
v-model="form.${treeParentCode}"
:data="${businessName}Options"
:props="{ value: '${treeCode}', label: '${treeName}', children: 'children' } as any"
value-key="${treeCode}"
placeholder="请选择${column.columnLabel}"
check-strictly
/>
</el-form-item>
<#elseif column.htmlType == "input">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="form.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif column.htmlType == "imageUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<image-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "fileUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<file-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "editor">
<el-form-item label="${column.columnLabel}">
<editor v-model="form.${column.javaField}" :min-height="192"/>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.label"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio
v-for="dict in ${column.dictType}"
:key="dict.value"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-switch
v-model="form.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
/>
</el-form-item>
<#elseif column.htmlType == "datetime">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="form.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" type="textarea" placeholder="请输入内容" />
</el-form-item>
</#if>
</#if>
</#list>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}" lang="ts">
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
import { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import { useLoading } from '@/hooks/async/useLoading';
import { useFormDialog } from '@/hooks/dialog/useFormDialog';
<#if needAddDateRange>
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
</#if>
import { useSearchReset } from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle';
import { useTreeTableExpand } from '@/hooks/tree/useTreeTableExpand';
<#if needDict>
import { useDict } from '@/utils/dict';
</#if>
import modal from '@/plugins/modal';
import { handleTree<#if needParseTime>, parseTime</#if> } from '@/utils/ruoyi';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>
<#if needDict>
const { ${dictsNoSymbol} } = toRefs<any>(useDict(${dicts}));
</#if>
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
type ${BusinessName}Option = {
${treeCode}: <#if treeParentColumn.javaType == 'String'>string<#else> number</#if>;
${treeName}: string;
children?: ${BusinessName}Option[];
};
const ${businessName}List = ref<${BusinessName}VO[]>([]);
const ${businessName}Options = ref<${BusinessName}Option[]>([]);
const all${BusinessName}Options = ref<${BusinessName}Option[]>([]);
const buttonLoading = ref(false);
const { showSearch } = useSearchToggle();
const { loading, setLoading, withLoading } = useLoading();
const queryFormRef = ref<ElFormInstance>();
const ${businessName}FormRef = ref<ElFormInstance>();
const ${businessName}TableRef = ref<ElTableInstance>();
const { isExpandAll, handleToggleExpandAll } = useTreeTableExpand<${BusinessName}VO>({
tableRef: ${businessName}TableRef,
data: ${businessName}List
});
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
const {
dateRange: dateRange${column.capJavaField},
applyDateRange: apply${column.capJavaField}DateRange,
resetDateRange: reset${column.capJavaField}DateRange
} = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const initFormData: ${BusinessName}Form = {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.htmlType == "checkbox">
${column.javaField}: [],
<#else>
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
const data = reactive<PageData<${BusinessName}Form, ${BusinessName}Query>>({
form: {...initFormData},
queryParams: {
<#list columns as column>
<#if column.query>
<#if column.htmlType != "datetime" || column.queryType != "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
params: {
<#list columns as column>
<#if column.query>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
},
rules: {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.required>
${column.javaField}: [
{ required: true, message: "${column.columnLabel}不能为空", trigger: <#if column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch" || column.htmlType == "inputNumber">"change"<#else>"blur"</#if> }
],
</#if>
</#if>
</#list>
}
});
const { queryParams, form, rules } = toRefs(data);
const { dialog, resetForm: reset, openDialog, showDialog, closeDialog } = useFormDialog({
form,
formRef: ${businessName}FormRef,
initialFormData: initFormData
});
/** 查询${functionName}列表 */
const getList = async () => {
await withLoading(async () => {
<#if needAddDateRange>
let params = queryParams.value;
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
params = apply${column.capJavaField}DateRange(params);
</#if>
</#list>
const res = await list${BusinessName}(params);
<#else>
const res = await list${BusinessName}(queryParams.value);
</#if>
const data = handleTree<${BusinessName}VO>(res.data, '${treeCode}', '${treeParentCode}');
if (data) {
${businessName}List.value = data;
}
});
};
/** 查询${functionName}下拉树结构 */
const getTreeselect = async (excludeId?: string | number) => {
const res = await list${BusinessName}();
const data: ${BusinessName}Option = { ${treeCode}: ${treeRootValueTsLiteral}, ${treeName}: '顶级节点', children: [] };
data.children = handleTree<${BusinessName}Option>(res.data, '${treeCode}', '${treeParentCode}');
all${BusinessName}Options.value = [data];
${businessName}Options.value = excludeId != null ? filterTreeOptions(all${BusinessName}Options.value, excludeId) : all${BusinessName}Options.value;
};
/** 取消按钮 */
const cancel = () => {
reset();
closeDialog();
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
const { resetQuery } = useSearchReset({
queryFormRef,
queryParams,
resetExtras: () => {
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
reset${column.capJavaField}DateRange();
</#if>
</#list>
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = (row?: Partial<${BusinessName}VO>) => {
openDialog('添加${functionName}');
getTreeselect();
if (row != null && row.${treeCode}) {
form.value.${treeParentCode} = row.${treeCode};
} else {
form.value.${treeParentCode} = ${treeRootValueTsLiteral};
}
};
/** 修改按钮操作 */
const handleUpdate = async (row: Partial<${BusinessName}VO>) => {
reset();
await getTreeselect(row.${treeCode});
if (row != null) {
form.value.${treeParentCode} = row.${treeParentCode};
}
const res = await get${BusinessName}(row.${pkColumn.javaField});
Object.assign(form.value, res.data);
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.split(",");
</#if>
</#list>
showDialog('修改${functionName}');
};
/** 提交按钮 */
const submitForm = () => {
${businessName}FormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.join(",");
</#if>
</#list>
if (form.value.${pkColumn.javaField}) {
await update${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
} else {
await add${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: Partial<${BusinessName}VO>) => {
await modal.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?');
setLoading(true);
await del${BusinessName}(row.${pkColumn.javaField}).finally(() => setLoading(false));
await getList();
modal.msgSuccess('删除成功');
};
const filterTreeOptions = (options: ${BusinessName}Option[], excludeId: string | number): ${BusinessName}Option[] => {
return options
.filter(item => item.${treeCode} !== excludeId)
.map(item => ({
...item,
children: item.children ? filterTreeOptions(item.children, excludeId) : []
}));
};
<#if enableStatus>
/** 状态修改 */
const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try {
await modal.confirm('确认要"' + text + '"吗?');
await change${BusinessName}Status(row.${pkColumn.javaField}, row.${statusField});
modal.msgSuccess(text + '成功');
} catch (err) {
row.${statusField} = row.${statusField} === ${statusField}ActiveValue ? ${statusField}InactiveValue : ${statusField}ActiveValue;
}
};
</#if>
<#if enableSort>
/** 排序调整 */
const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功');
} catch (err) {
await getList();
}
};
</#if>
<#if enableExport>
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'${moduleName}/${businessName}/export',
{
...queryParams.value
},
`${businessName}_${r'${new Date().getTime()}'}.xlsx`
);
};
</#if>
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,615 @@
<template>
<div class="p-2 page-shell ${moduleName}-${businessName}-page">
<div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header>
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
<div><h3>筛选条件</h3></div>
</div>
</template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<#list columns as column>
<#if column.query>
<#if column.htmlType == "input" || column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="queryParams.${column.javaField}" placeholder="请输入${column.columnLabel}" clearable @keyup.enter="handleQuery" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="queryParams.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<#if column.javaType == "Boolean">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
<#elseif column.javaType == "Integer" || column.javaType == "Long">
<el-option label="开启" :value="0" />
<el-option label="关闭" :value="1" />
<#else>
<el-option label="开启" value="0" />
<el-option label="关闭" value="1" />
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType != "BETWEEN">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
<el-form-item label="${column.columnLabel}" style="width: 308px">
<el-date-picker
v-model="dateRange${column.capJavaField}"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
</el-form-item>
</#if>
</#if>
</#list>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover" class="table-panel">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<h3>${functionName}列表</h3>
</div>
<div class="toolbar-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
<#if enableExport>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['${moduleName}:${businessName}:export']">导出</el-button>
</#if>
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
</div>
</div>
</template>
<el-table v-loading="loading" border class="data-table" :data="${businessName}List" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<#list columns as column>
<#if column.pk && column.list>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
<#elseif enableStatus && statusField == column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<#elseif enableSort && sortField == column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="160">
<template #default="scope">
<#if column.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${column.javaField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "switch">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
disabled
/>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "datetime">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.${column.javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "imageUpload">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}Url" width="100">
<template #default="scope">
<image-preview :src="scope.row.${column.javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
<#elseif column.list && column.dictColumn>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
<template #default="scope">
<#if column.htmlType == "checkbox">
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField} ? scope.row.${column.javaField}.split(',') : []"/>
<#else>
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField}"/>
</#if>
</template>
</el-table-column>
<#elseif column.list && "" != column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
</#if>
</#list>
<#if enableStatus && !statusColumn.list>
<el-table-column label="${statusColumn.columnComment}" align="center" prop="${statusField}">
<template #default="scope">
<el-switch
v-model="scope.row.${statusField}"
<#if statusColumn.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
</#if>
<#if enableSort && !sortColumn.list>
<el-table-column label="${sortColumn.columnComment}" align="center" prop="${sortField}" width="160">
<template #default="scope">
<#if sortColumn.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${sortField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${sortColumn.columnComment}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${sortField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
</#if>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if column.htmlType == "input">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="form.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif column.htmlType == "imageUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<image-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "fileUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<file-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "editor">
<el-form-item label="${column.columnLabel}">
<editor v-model="form.${column.javaField}" :min-height="192"/>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.label"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio
v-for="dict in ${column.dictType}"
:key="dict.value"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-switch
v-model="form.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
/>
</el-form-item>
<#elseif column.htmlType == "datetime">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="form.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择${column.columnLabel}">
</el-date-picker>
</el-form-item>
<#elseif column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" type="textarea" placeholder="请输入内容" />
</el-form-item>
</#if>
</#if>
</#list>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}" lang="ts">
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
import { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import { useLoading } from '@/hooks/async/useLoading';
import { useFormDialog } from '@/hooks/dialog/useFormDialog';
<#if needAddDateRange>
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
</#if>
import { useSearchReset } from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle';
import { useTableSelection } from '@/hooks/table/useTableSelection';
<#if needDict>
import { useDict } from '@/utils/dict';
</#if>
<#if needParseTime>
import { parseTime } from '@/utils/ruoyi';
</#if>
import modal from '@/plugins/modal';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>
<#if needDict>
const { ${dictsNoSymbol} } = toRefs<any>(useDict(${dicts}));
</#if>
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
const ${businessName}List = ref<${BusinessName}VO[]>([]);
const buttonLoading = ref(false);
const { loading, withLoading } = useLoading(true);
const { showSearch } = useSearchToggle();
const total = ref(0);
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
const {
dateRange: dateRange${column.capJavaField},
applyDateRange: apply${column.capJavaField}DateRange,
resetDateRange: reset${column.capJavaField}DateRange
} = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const queryFormRef = ref<ElFormInstance>();
const ${businessName}FormRef = ref<ElFormInstance>();
const initFormData: ${BusinessName}Form = {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.htmlType == "checkbox">
${column.javaField}: [],
<#else>
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
const data = reactive<PageData<${BusinessName}Form, ${BusinessName}Query>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
<#list columns as column>
<#if column.query>
<#if column.htmlType != "datetime" || column.queryType != "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
params: {
<#list columns as column>
<#if column.query>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
},
rules: {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.required>
${column.javaField}: [
{ required: true, message: "${column.columnLabel}不能为空", trigger: <#if column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch" || column.htmlType == "inputNumber">"change"<#else>"blur"</#if> }
],
</#if>
</#if>
</#list>
}
});
const { queryParams, form, rules } = toRefs(data);
const { ids, single, multiple, handleSelectionChange } = useTableSelection<${BusinessName}VO>(item => item.${pkColumn.javaField});
const { dialog, resetForm: reset, openDialog, showDialog, closeDialog } = useFormDialog({
form,
formRef: ${businessName}FormRef,
initialFormData: initFormData
});
/** 查询${functionName}列表 */
const getList = async () => {
await withLoading(async () => {
<#if needAddDateRange>
let params = queryParams.value;
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
params = apply${column.capJavaField}DateRange(params);
</#if>
</#list>
const res = await list${BusinessName}(params);
<#else>
const res = await list${BusinessName}(queryParams.value);
</#if>
${businessName}List.value = res.data?.rows;
total.value = res.data?.total;
});
};
/** 取消按钮 */
const cancel = () => {
reset();
closeDialog();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
const { resetQuery } = useSearchReset({
queryFormRef,
queryParams,
pageNumKey: 'pageNum',
pageSizeKey: 'pageSize',
initialPageSize: 10,
resetExtras: () => {
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
reset${column.capJavaField}DateRange();
</#if>
</#list>
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = () => {
openDialog('添加${functionName}');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: Partial<${BusinessName}VO>) => {
reset();
const _${pkColumn.javaField} = row?.${pkColumn.javaField} || ids.value[0];
const res = await get${BusinessName}(_${pkColumn.javaField});
Object.assign(form.value, res.data);
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.split(",");
</#if>
</#list>
showDialog('修改${functionName}');
};
/** 提交按钮 */
const submitForm = () => {
${businessName}FormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.join(",");
</#if>
</#list>
if (form.value.${pkColumn.javaField}) {
await update${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
} else {
await add${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
const _${pkColumn.javaField}s = row?.${pkColumn.javaField} || ids.value;
await modal.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?');
await del${BusinessName}(_${pkColumn.javaField}s);
modal.msgSuccess('删除成功');
await getList();
};
<#if enableExport>
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'${moduleName}/${businessName}/export',
{
...queryParams.value
},
`${businessName}_${r'${new Date().getTime()}'}.xlsx`
);
};
</#if>
<#if enableStatus>
/** 状态修改 */
const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try {
await modal.confirm('确认要"' + text + '"吗?');
await change${BusinessName}Status(row.${pkColumn.javaField}, row.${statusField});
modal.msgSuccess(text + '成功');
} catch (err) {
row.${statusField} = row.${statusField} === ${statusField}ActiveValue ? ${statusField}InactiveValue : ${statusField}ActiveValue;
}
};
</#if>
<#if enableSort>
/** 排序调整 */
const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功');
} catch (err) {
await getList();
}
};
</#if>
onMounted(() => {
getList();
});
</script>

View File

@@ -0,0 +1,48 @@
export interface ${BusinessName}VO {
<#list columns as column>
<#if column.list>
/**
* ${column.columnComment}
*/
${column.javaField}: ${column.tsType};
<#if column.htmlType == "imageUpload">
/**
* ${column.columnComment}Url
*/
${column.javaField}Url: string;
</#if>
</#if>
</#list>
<#if table.tree>
/**
* 子对象
*/
children: ${BusinessName}VO[];
</#if>
}
export interface ${BusinessName}Form extends BaseEntity {
<#list columns as column>
<#if column.insert || column.edit>
/**
* ${column.columnComment}
*/
${column.javaField}?: ${column.tsType};
</#if>
</#list>
}
export interface ${BusinessName}Query<#if !table.tree> extends PageQuery</#if> {
<#list columns as column>
<#if column.query>
/**
* ${column.columnComment}
*/
${column.javaField}?: ${column.tsType};
</#if>
</#list>
/**
* 日期范围参数
*/
params?: any;
}

View File

@@ -0,0 +1,6 @@
<?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="${packageName}.mapper.${ClassName}Mapper">
</mapper>

View File

@@ -0,0 +1,10 @@
# 代码生成
gen:
# 作者
author: Lion Li
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: org.dromara.system
# 自动去除表前缀,默认是false
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_

View File

@@ -0,0 +1,10 @@
<?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="org.dromara.gen.mapper.GenTableColumnMapper">
<resultMap type="org.dromara.gen.domain.GenTableColumn" id="GenTableColumnResult">
</resultMap>
</mapper>

View File

@@ -0,0 +1,9 @@
<?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="org.dromara.gen.mapper.GenTableMapper">
<select id="selectTableNameList" resultType="java.lang.String">
select table_name from gen_table where data_name = #{dataName,jdbcType=VARCHAR}
</select>
</mapper>

View File

@@ -0,0 +1,3 @@
java包使用 `.` 分割 resource 目录使用 `/` 分割
<br>
此文件目的 防止文件夹粘连找不到 `xml` 文件

View File

@@ -0,0 +1,35 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-job</artifactId>
<description>
任务调度
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- 调度模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-job</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,19 @@
package org.dromara.job.entity;
import java.math.BigDecimal;
/**
* 账单数据传输对象。
*
* @param billId 账单 ID
* @param billChannel 账单渠道
* @param billDate 账单日期
* @param billAmount 账单金额
*/
public record BillDTO(
Long billId,
String billChannel,
String billDate,
BigDecimal billAmount
) {
}

Some files were not shown because too many files have changed in this diff Show More