forked from wangziqi/ruoyi-vue-pro
🎉 Initial commit
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.ftp;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.extra.ftp.FtpMode;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClientConfig;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* {@link FtpFileClient} 集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class FtpFileClientTest {
|
||||
|
||||
// docker run -d \
|
||||
// -p 2121:21 -p 30000-30009:30000-30009 \
|
||||
// -e FTP_USER=foo \
|
||||
// -e FTP_PASS=pass \
|
||||
// -e PASV_ADDRESS=127.0.0.1 \
|
||||
// -e PASV_MIN_PORT=30000 \
|
||||
// -e PASV_MAX_PORT=30009 \
|
||||
// -v $(pwd)/ftp-data:/home/vsftpd \
|
||||
// fauria/vsftpd
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
FtpFileClientConfig config = new FtpFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/home/ftp");
|
||||
config.setHost("127.0.0.1");
|
||||
config.setPort(2121);
|
||||
config.setUsername("foo");
|
||||
config.setPassword("pass");
|
||||
config.setMode(FtpMode.Passive.name());
|
||||
FtpFileClient client = new FtpFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path, "image/jpeg");
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
if (false) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
}
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.local;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class LocalFileClientTest {
|
||||
|
||||
@TempDir
|
||||
public File tempDir;
|
||||
|
||||
@Test
|
||||
public void testUpload_success() {
|
||||
// 准备参数
|
||||
LocalFileClient client = createClient();
|
||||
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
|
||||
String path = "avatar/test.txt";
|
||||
|
||||
// 调用
|
||||
String url = client.upload(content, path, "text/plain");
|
||||
|
||||
// 断言
|
||||
assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/test.txt", url);
|
||||
assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path)));
|
||||
assertArrayEquals(content, client.getContent(path));
|
||||
|
||||
// 删除
|
||||
client.delete(path);
|
||||
assertFalse(FileUtil.exist(new File(tempDir, path)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpload_encodeUrlPath() {
|
||||
// 准备参数
|
||||
LocalFileClient client = createClient();
|
||||
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
|
||||
String path = "avatar/中文 100%+文件.txt";
|
||||
|
||||
// 调用
|
||||
String url = client.upload(content, path, "text/plain");
|
||||
|
||||
// 断言
|
||||
assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.txt", url);
|
||||
assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpload_pathInvalid() {
|
||||
// 准备参数
|
||||
LocalFileClient client = createClient();
|
||||
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertThrows(ServiceException.class, () -> client.upload(content, "../test.txt", "text/plain"));
|
||||
assertFalse(FileUtil.exist(new File(tempDir.getParentFile(), "test.txt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
LocalFileClientConfig config = new LocalFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/Users/yunai/file_test");
|
||||
LocalFileClient client = new LocalFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path, "image/jpeg");
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
client.delete(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testGetContent_notFound() {
|
||||
// 创建客户端
|
||||
LocalFileClientConfig config = new LocalFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/Users/yunai/file_test");
|
||||
LocalFileClient client = new LocalFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
byte[] content = client.getContent(randomString());
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private LocalFileClient createClient() {
|
||||
LocalFileClientConfig config = new LocalFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath(tempDir.getAbsolutePath());
|
||||
LocalFileClient client = new LocalFileClient(0L, config);
|
||||
client.init();
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.s3;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig;
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public class S3FileClientTest {
|
||||
|
||||
@Test
|
||||
public void testPresignGetUrl_publicAccess_encodeUrlPath() {
|
||||
// 准备参数
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
config.setDomain("https://static.iocoder.cn");
|
||||
config.setEnablePublicAccess(true);
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
|
||||
// 调用
|
||||
String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300);
|
||||
|
||||
// 断言
|
||||
assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPresignGetUrl_publicAccess_decodeDomainUrl() {
|
||||
// 准备参数
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
config.setDomain("https://static.iocoder.cn");
|
||||
config.setEnablePublicAccess(true);
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
|
||||
// 调用
|
||||
String result = client.presignGetUrl("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg?token=1", 300);
|
||||
|
||||
// 断言
|
||||
assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPresignGetUrl_privateAccess_rawPath() {
|
||||
// 准备参数
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
config.setAccessKey("admin");
|
||||
config.setAccessSecret("password");
|
||||
config.setBucket("yudao");
|
||||
config.setDomain("http://127.0.0.1:9000/yudao");
|
||||
config.setEndpoint("http://127.0.0.1:9000");
|
||||
config.setEnablePathStyleAccess(true);
|
||||
config.setEnablePublicAccess(false);
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
client.init();
|
||||
|
||||
// 调用
|
||||
String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300);
|
||||
|
||||
// 断言
|
||||
assertTrue(result.contains("/yudao/avatar/%E4%B8%AD%E6%96%87%20100%25"));
|
||||
assertTrue(result.contains("%E6%96%87%E4%BB%B6.jpg"));
|
||||
assertFalse(result.contains("%25E4%25B8%25AD"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // MinIO,如果要集成测试,可以注释本行
|
||||
public void testMinIO() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey("admin");
|
||||
config.setAccessSecret("password");
|
||||
config.setBucket("yudaoyuanma");
|
||||
config.setDomain(null);
|
||||
// 默认 9000 endpoint
|
||||
config.setEndpoint("http://127.0.0.1:9000");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
|
||||
public void testAliyun() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
|
||||
config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY"));
|
||||
config.setBucket("yunai-aoteman");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn
|
||||
// 默认北京的 endpoint
|
||||
config.setEndpoint("oss-cn-beijing.aliyuncs.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
|
||||
public void testQCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
|
||||
config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY"));
|
||||
config.setBucket("aoteman-1255880240");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("cos.ap-shanghai.myqcloud.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 七牛云存储,如果要集成测试,可以注释本行
|
||||
public void testQiniu() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
|
||||
config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8");
|
||||
config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP");
|
||||
config.setBucket("ruoyi-vue-pro");
|
||||
config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn
|
||||
config.setEnablePathStyleAccess(false);
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("s3-cn-south-1.qiniucs.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 七牛云存储(读私有桶),如果要集成测试,可以注释本行
|
||||
public void testQiniu_privateGet() {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
|
||||
config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8");
|
||||
config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP");
|
||||
config.setBucket("ruoyi-vue-pro-private");
|
||||
config.setDomain("http://t151glocd.hn-bkt.clouddn.com"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn
|
||||
config.setEnablePathStyleAccess(false);
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("s3-cn-south-1.qiniucs.com");
|
||||
|
||||
// 校验配置
|
||||
ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
|
||||
// 创建 Client
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
client.init();
|
||||
// 执行生成 URL 签名
|
||||
String path = "output.png";
|
||||
String presignedUrl = client.presignGetUrl(path, 300);
|
||||
System.out.println(presignedUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 华为云存储,如果要集成测试,可以注释本行
|
||||
public void testHuaweiCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
|
||||
config.setBucket("yudao");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
private void testExecuteUpload(S3FileClientConfig config) {
|
||||
// 校验配置
|
||||
ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
|
||||
// 创建 Client
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path, "image/jpeg");
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
// 读取文件
|
||||
if (true) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes.length);
|
||||
}
|
||||
// 删除文件
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.sftp;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* {@link SftpFileClient} 集成测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SftpFileClientTest {
|
||||
|
||||
// docker run -p 2222:22 -d \
|
||||
// -v $(pwd)/sftp-data:/home/foo/upload \
|
||||
// atmoz/sftp \
|
||||
// foo:pass:1001
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
SftpFileClientConfig config = new SftpFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/upload"); // 注意,这个是相对路径,不是实际 linux 上的路径!!!
|
||||
config.setHost("127.0.0.1");
|
||||
config.setPort(2222);
|
||||
config.setUsername("foo");
|
||||
config.setPassword("pass");
|
||||
SftpFileClient client = new SftpFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path, "image/jpeg");
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
if (false) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
}
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.framework.file.core.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link FileTypeUtils} 的单元测试
|
||||
*/
|
||||
public class FileTypeUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testWriteAttachment_contentDispositionEncodeFilename() throws Exception {
|
||||
// 准备参数
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// 调用
|
||||
FileTypeUtils.writeAttachment(response, "中文 100%+文件.txt", content);
|
||||
|
||||
// 断言
|
||||
assertEquals("attachment;filename=\"__ 100%+__.txt\";"
|
||||
+ "filename*=UTF-8''%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.txt",
|
||||
response.getHeader("Content-Disposition"));
|
||||
assertArrayEquals(content, response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.infra.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.generator.query.DefaultQuery;
|
||||
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
|
||||
import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DefaultDatabaseQueryTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:oracle:thin:@127.0.0.1:1521:xe",
|
||||
// "root", "123456").build();
|
||||
DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro",
|
||||
"root", "123456").build();
|
||||
// StrategyConfig strategyConfig = new StrategyConfig.Builder().build();
|
||||
|
||||
ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, null, null, null, null);
|
||||
|
||||
DefaultQuery query = new DefaultQuery(builder);
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
List<TableInfo> tableInfos = query.queryTables();
|
||||
for (TableInfo tableInfo : tableInfos) {
|
||||
if (StrUtil.startWithAny(tableInfo.getName().toLowerCase(), "act_", "flw_", "qrtz_")) {
|
||||
continue;
|
||||
}
|
||||
System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 1;", tableInfo.getName()));
|
||||
// System.out.println(String.format("DELETE FROM %s WHERE deleted = '1';", tableInfo.getName()));
|
||||
}
|
||||
System.out.println(tableInfos.size());
|
||||
System.out.println(System.currentTimeMillis() - time);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnSaveReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
|
||||
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
|
||||
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
|
||||
import cn.iocoder.yudao.module.infra.service.db.DataSourceConfigService;
|
||||
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link CodegenServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(CodegenServiceImpl.class)
|
||||
public class CodegenServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private CodegenServiceImpl codegenService;
|
||||
|
||||
@Resource
|
||||
private CodegenTableMapper codegenTableMapper;
|
||||
@Resource
|
||||
private CodegenColumnMapper codegenColumnMapper;
|
||||
|
||||
@MockitoBean
|
||||
private DatabaseTableService databaseTableService;
|
||||
@MockitoBean
|
||||
private DataSourceConfigService dataSourceConfigService;
|
||||
|
||||
@MockitoBean
|
||||
private CodegenBuilder codegenBuilder;
|
||||
@MockitoBean
|
||||
private CodegenEngine codegenEngine;
|
||||
|
||||
@MockitoBean
|
||||
private CodegenProperties codegenProperties;
|
||||
|
||||
@Test
|
||||
public void testCreateCodegenList() {
|
||||
// 准备参数
|
||||
String author = randomString();
|
||||
CodegenCreateListReqVO reqVO = randomPojo(CodegenCreateListReqVO.class,
|
||||
o -> o.setDataSourceConfigId(1L).setTableNames(Collections.singletonList("t_yunai")));
|
||||
// mock 方法(TableInfo)
|
||||
TableInfo tableInfo = mock(TableInfo.class);
|
||||
when(databaseTableService.getTable(eq(1L), eq("t_yunai")))
|
||||
.thenReturn(tableInfo);
|
||||
when(tableInfo.getComment()).thenReturn("芋艿");
|
||||
// mock 方法(TableInfo fields)
|
||||
TableField field01 = mock(TableField.class);
|
||||
when(field01.getComment()).thenReturn("主键");
|
||||
TableField field02 = mock(TableField.class);
|
||||
when(field02.getComment()).thenReturn("名字");
|
||||
List<TableField> fields = Arrays.asList(field01, field02);
|
||||
when(tableInfo.getFields()).thenReturn(fields);
|
||||
// mock 方法(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class);
|
||||
when(codegenBuilder.buildTable(same(tableInfo))).thenReturn(table);
|
||||
// mock 方法(CodegenColumnDO)
|
||||
List<CodegenColumnDO> columns = randomPojoList(CodegenColumnDO.class);
|
||||
when(codegenBuilder.buildColumns(eq(table.getId()), same(fields)))
|
||||
.thenReturn(columns);
|
||||
// mock 方法(CodegenProperties)
|
||||
when(codegenProperties.getFrontType()).thenReturn(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType());
|
||||
|
||||
// 调用
|
||||
List<Long> result = codegenService.createCodegenList(author, reqVO);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
// 断言(CodegenTableDO)
|
||||
CodegenTableDO dbTable = codegenTableMapper.selectList().get(0);
|
||||
assertPojoEquals(table, dbTable);
|
||||
assertEquals(1L, dbTable.getDataSourceConfigId());
|
||||
assertEquals(CodegenSceneEnum.ADMIN.getScene(), dbTable.getScene());
|
||||
assertEquals(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), dbTable.getFrontType());
|
||||
assertEquals(author, dbTable.getAuthor());
|
||||
// 断言(CodegenColumnDO)
|
||||
List<CodegenColumnDO> dbColumns = codegenColumnMapper.selectList();
|
||||
assertEquals(columns.size(), dbColumns.size());
|
||||
assertTrue(dbColumns.get(0).getPrimaryKey());
|
||||
for (int i = 0; i < dbColumns.size(); i++) {
|
||||
assertPojoEquals(columns.get(i), dbColumns.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateTableInfo() {
|
||||
// 情况一
|
||||
assertServiceException(() -> codegenService.validateTableInfo(null),
|
||||
CODEGEN_IMPORT_TABLE_NULL);
|
||||
// 情况二
|
||||
TableInfo tableInfo = mock(TableInfo.class);
|
||||
assertServiceException(() -> codegenService.validateTableInfo(tableInfo),
|
||||
CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL);
|
||||
// 情况三
|
||||
when(tableInfo.getComment()).thenReturn("芋艿");
|
||||
assertServiceException(() -> codegenService.validateTableInfo(tableInfo),
|
||||
CODEGEN_IMPORT_COLUMNS_NULL);
|
||||
// 情况四
|
||||
TableField field = mock(TableField.class);
|
||||
when(field.getName()).thenReturn("name");
|
||||
when(tableInfo.getFields()).thenReturn(Collections.singletonList(field));
|
||||
assertServiceException(() -> codegenService.validateTableInfo(tableInfo),
|
||||
CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCodegen_notExists() {
|
||||
// 准备参数
|
||||
CodegenUpdateReqVO updateReqVO = randomPojo(CodegenUpdateReqVO.class);
|
||||
// mock 方法
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.updateCodegen(updateReqVO),
|
||||
CODEGEN_TABLE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCodegen_sub_masterNotExists() {
|
||||
// mock 数据
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table);
|
||||
// 准备参数
|
||||
CodegenUpdateReqVO updateReqVO = randomPojo(CodegenUpdateReqVO.class,
|
||||
o -> o.getTable().setId(table.getId())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()));
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.updateCodegen(updateReqVO),
|
||||
CODEGEN_MASTER_TABLE_NOT_EXISTS, updateReqVO.getTable().getMasterTableId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCodegen_sub_columnNotExists() {
|
||||
// mock 数据
|
||||
CodegenTableDO subTable = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(subTable);
|
||||
// mock 数据(master)
|
||||
CodegenTableDO masterTable = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType())
|
||||
.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(masterTable);
|
||||
// 准备参数
|
||||
CodegenUpdateReqVO updateReqVO = randomPojo(CodegenUpdateReqVO.class,
|
||||
o -> o.getTable().setId(subTable.getId())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setMasterTableId(masterTable.getId()));
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.updateCodegen(updateReqVO),
|
||||
CODEGEN_SUB_COLUMN_NOT_EXISTS, updateReqVO.getTable().getSubJoinColumnId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCodegen_success() {
|
||||
// mock 数据
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setTemplateType(CodegenTemplateTypeEnum.ONE.getType())
|
||||
.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table);
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
|
||||
codegenColumnMapper.insert(column01);
|
||||
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
|
||||
codegenColumnMapper.insert(column02);
|
||||
// 准备参数
|
||||
CodegenUpdateReqVO updateReqVO = randomPojo(CodegenUpdateReqVO.class,
|
||||
o -> o.getTable().setId(table.getId())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType())
|
||||
.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
CodegenColumnSaveReqVO columnVO01 = randomPojo(CodegenColumnSaveReqVO.class,
|
||||
o -> o.setId(column01.getId()).setTableId(table.getId()));
|
||||
CodegenColumnSaveReqVO columnVO02 = randomPojo(CodegenColumnSaveReqVO.class,
|
||||
o -> o.setId(column02.getId()).setTableId(table.getId()));
|
||||
updateReqVO.setColumns(Arrays.asList(columnVO01, columnVO02));
|
||||
|
||||
// 调用
|
||||
codegenService.updateCodegen(updateReqVO);
|
||||
// 断言
|
||||
CodegenTableDO dbTable = codegenTableMapper.selectById(table.getId());
|
||||
assertPojoEquals(updateReqVO.getTable(), dbTable);
|
||||
List<CodegenColumnDO> dbColumns = codegenColumnMapper.selectList();
|
||||
assertEquals(2, dbColumns.size());
|
||||
assertPojoEquals(columnVO01, dbColumns.get(0));
|
||||
assertPojoEquals(columnVO02, dbColumns.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSyncCodegenFromDB() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class, o -> o.setTableName("t_yunai")
|
||||
.setDataSourceConfigId(1L).setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table);
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setColumnName("id").setPrimaryKey(true).setOrdinalPosition(0));
|
||||
codegenColumnMapper.insert(column01);
|
||||
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setColumnName("name").setOrdinalPosition(1));
|
||||
codegenColumnMapper.insert(column02);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
// mock 方法(TableInfo)
|
||||
TableInfo tableInfo = mock(TableInfo.class);
|
||||
when(databaseTableService.getTable(eq(1L), eq("t_yunai")))
|
||||
.thenReturn(tableInfo);
|
||||
when(tableInfo.getComment()).thenReturn("芋艿");
|
||||
// mock 方法(TableInfo fields)
|
||||
TableField field01 = mock(TableField.class);
|
||||
when(field01.getComment()).thenReturn("主键");
|
||||
TableField field03 = mock(TableField.class);
|
||||
when(field03.getComment()).thenReturn("分类");
|
||||
List<TableField> fields = Arrays.asList(field01, field03);
|
||||
when(tableInfo.getFields()).thenReturn(fields);
|
||||
when(databaseTableService.getTable(eq(1L), eq("t_yunai")))
|
||||
.thenReturn(tableInfo);
|
||||
// mock 方法(CodegenTableDO)
|
||||
List<CodegenColumnDO> newColumns = randomPojoList(CodegenColumnDO.class, 2);
|
||||
when(codegenBuilder.buildColumns(eq(table.getId()), argThat(tableFields -> {
|
||||
assertEquals(2, tableFields.size());
|
||||
assertSame(tableInfo.getFields(), tableFields);
|
||||
return true;
|
||||
}))).thenReturn(newColumns);
|
||||
|
||||
// 调用
|
||||
codegenService.syncCodegenFromDB(tableId);
|
||||
// 断言
|
||||
List<CodegenColumnDO> dbColumns = codegenColumnMapper.selectList();
|
||||
assertEquals(newColumns.size(), dbColumns.size());
|
||||
assertPojoEquals(newColumns.get(0), dbColumns.get(0));
|
||||
assertPojoEquals(newColumns.get(1), dbColumns.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteCodegen_notExists() {
|
||||
assertServiceException(() -> codegenService.deleteCodegen(randomLongId()),
|
||||
CODEGEN_TABLE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteCodegen_success() {
|
||||
// mock 数据
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table);
|
||||
CodegenColumnDO column = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
|
||||
codegenColumnMapper.insert(column);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用
|
||||
codegenService.deleteCodegen(tableId);
|
||||
// 断言
|
||||
assertNull(codegenTableMapper.selectById(tableId));
|
||||
assertEquals(0, codegenColumnMapper.selectList().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCodegenTableList() {
|
||||
// mock 数据
|
||||
CodegenTableDO table01 = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table01);
|
||||
CodegenTableDO table02 = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(table02);
|
||||
// 准备参数
|
||||
Long dataSourceConfigId = table01.getDataSourceConfigId();
|
||||
|
||||
// 调用
|
||||
List<CodegenTableDO> result = codegenService.getCodegenTableList(dataSourceConfigId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertPojoEquals(table01, result.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCodegenTablePage() {
|
||||
// mock 数据
|
||||
CodegenTableDO tableDO = randomPojo(CodegenTableDO.class, o -> {
|
||||
o.setTableName("t_yunai");
|
||||
o.setTableComment("芋艿");
|
||||
o.setClassName("SystemYunai");
|
||||
o.setCreateTime(buildTime(2021, 3, 10));
|
||||
}).setScene(CodegenSceneEnum.ADMIN.getScene());
|
||||
codegenTableMapper.insert(tableDO);
|
||||
// 测试 tableName 不匹配
|
||||
codegenTableMapper.insert(cloneIgnoreId(tableDO, o -> o.setTableName(randomString())));
|
||||
// 测试 tableComment 不匹配
|
||||
codegenTableMapper.insert(cloneIgnoreId(tableDO, o -> o.setTableComment(randomString())));
|
||||
// 测试 className 不匹配
|
||||
codegenTableMapper.insert(cloneIgnoreId(tableDO, o -> o.setClassName(randomString())));
|
||||
// 测试 createTime 不匹配
|
||||
codegenTableMapper.insert(cloneIgnoreId(tableDO, logDO -> logDO.setCreateTime(buildTime(2021, 4, 10))));
|
||||
// 准备参数
|
||||
CodegenTablePageReqVO reqVO = new CodegenTablePageReqVO();
|
||||
reqVO.setTableName("yunai");
|
||||
reqVO.setTableComment("芋");
|
||||
reqVO.setClassName("Yunai");
|
||||
reqVO.setCreateTime(buildBetweenTime(2021, 3, 1, 2021, 3, 31));
|
||||
|
||||
// 调用
|
||||
PageResult<CodegenTableDO> pageResult = codegenService.getCodegenTablePage(reqVO);
|
||||
// 断言,只查到了一条符合条件的
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(tableDO, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCodegenTable() {
|
||||
// mock 数据
|
||||
CodegenTableDO tableDO = randomPojo(CodegenTableDO.class, o -> o.setScene(CodegenSceneEnum.ADMIN.getScene()));
|
||||
codegenTableMapper.insert(tableDO);
|
||||
// 准备参数
|
||||
Long id = tableDO.getId();
|
||||
|
||||
// 调用
|
||||
CodegenTableDO result = codegenService.getCodegenTable(id);
|
||||
// 断言
|
||||
assertPojoEquals(tableDO, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCodegenColumnListByTableId() {
|
||||
// mock 数据
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class);
|
||||
codegenColumnMapper.insert(column01);
|
||||
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class);
|
||||
codegenColumnMapper.insert(column02);
|
||||
// 准备参数
|
||||
Long tableId = column01.getTableId();
|
||||
|
||||
// 调用
|
||||
List<CodegenColumnDO> result = codegenService.getCodegenColumnListByTableId(tableId);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertPojoEquals(column01, result.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_tableNotExists() {
|
||||
assertServiceException(() -> codegenService.generationCodes(randomLongId()),
|
||||
CODEGEN_TABLE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_columnNotExists() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
|
||||
codegenTableMapper.insert(table);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.generationCodes(tableId),
|
||||
CODEGEN_COLUMN_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_sub_tableNotExists() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
|
||||
codegenTableMapper.insert(table);
|
||||
// mock 数据(CodegenColumnDO)
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
|
||||
codegenColumnMapper.insert(column01);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.generationCodes(tableId),
|
||||
CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_sub_columnNotExists() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
|
||||
codegenTableMapper.insert(table);
|
||||
// mock 数据(CodegenColumnDO)
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()));
|
||||
codegenColumnMapper.insert(column01);
|
||||
// mock 数据(sub CodegenTableDO)
|
||||
CodegenTableDO subTable = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setMasterTableId(table.getId()));
|
||||
codegenTableMapper.insert(subTable);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> codegenService.generationCodes(tableId),
|
||||
CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_one_success() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType()));
|
||||
codegenTableMapper.insert(table);
|
||||
// mock 数据(CodegenColumnDO)
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setOrdinalPosition(1));
|
||||
codegenColumnMapper.insert(column01);
|
||||
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setOrdinalPosition(2));
|
||||
codegenColumnMapper.insert(column02);
|
||||
// mock 数据(DataSourceConfigDO)
|
||||
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
|
||||
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
|
||||
// mock 执行生成
|
||||
Map<String, String> codes = MapUtil.of(randomString(), randomString());
|
||||
when(codegenEngine.execute(eq(DbType.MYSQL), eq(table), argThat(columns -> {
|
||||
assertEquals(2, columns.size());
|
||||
assertEquals(column01, columns.get(0));
|
||||
assertEquals(column02, columns.get(1));
|
||||
return true;
|
||||
}), isNull(), isNull())).thenReturn(codes);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenService.generationCodes(tableId);
|
||||
// 断言
|
||||
assertSame(codes, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerationCodes_master_success() {
|
||||
// mock 数据(CodegenTableDO)
|
||||
CodegenTableDO table = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
|
||||
codegenTableMapper.insert(table);
|
||||
// mock 数据(CodegenColumnDO)
|
||||
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setOrdinalPosition(1));
|
||||
codegenColumnMapper.insert(column01);
|
||||
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
|
||||
.setOrdinalPosition(2));
|
||||
codegenColumnMapper.insert(column02);
|
||||
// mock 数据(sub CodegenTableDO)
|
||||
CodegenTableDO subTable = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setMasterTableId(table.getId())
|
||||
.setSubJoinColumnId(1024L));
|
||||
codegenTableMapper.insert(subTable);
|
||||
// mock 数据(sub CodegenColumnDO)
|
||||
CodegenColumnDO subColumn01 = randomPojo(CodegenColumnDO.class, o -> o.setId(1024L).setTableId(subTable.getId()));
|
||||
codegenColumnMapper.insert(subColumn01);
|
||||
// mock 数据(DataSourceConfigDO)
|
||||
when(dataSourceConfigService.getDataSourceConfig(eq(table.getDataSourceConfigId())))
|
||||
.thenReturn(randomPojo(DataSourceConfigDO.class, o -> o.setUrl("jdbc:mysql://")));
|
||||
// mock 执行生成
|
||||
Map<String, String> codes = MapUtil.of(randomString(), randomString());
|
||||
when(codegenEngine.execute(eq(DbType.MYSQL), eq(table), argThat(columns -> {
|
||||
assertEquals(2, columns.size());
|
||||
assertEquals(column01, columns.get(0));
|
||||
assertEquals(column02, columns.get(1));
|
||||
return true;
|
||||
}), argThat(tables -> {
|
||||
assertEquals(1, tables.size());
|
||||
assertPojoEquals(subTable, tables.get(0));
|
||||
return true;
|
||||
}), argThat(columns -> {
|
||||
assertEquals(1, columns.size());
|
||||
assertPojoEquals(subColumn01, columns.size());
|
||||
return true;
|
||||
}))).thenReturn(codes);
|
||||
// 准备参数
|
||||
Long tableId = table.getId();
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenService.generationCodes(tableId);
|
||||
// 断言
|
||||
assertSame(codes, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDatabaseTableList() {
|
||||
// 准备参数
|
||||
Long dataSourceConfigId = randomLongId();
|
||||
String name = randomString();
|
||||
String comment = randomString();
|
||||
// mock 方法
|
||||
TableInfo tableInfo01 = mock(TableInfo.class);
|
||||
when(tableInfo01.getName()).thenReturn("t_yunai");
|
||||
when(tableInfo01.getComment()).thenReturn("芋艿");
|
||||
TableInfo tableInfo02 = mock(TableInfo.class);
|
||||
when(tableInfo02.getName()).thenReturn("t_yunai_02");
|
||||
when(tableInfo02.getComment()).thenReturn("芋艿_02");
|
||||
when(databaseTableService.getTableList(eq(dataSourceConfigId), eq(name), eq(comment)))
|
||||
.thenReturn(ListUtil.toList(tableInfo01, tableInfo02));
|
||||
// mock 数据
|
||||
CodegenTableDO tableDO = randomPojo(CodegenTableDO.class,
|
||||
o -> o.setScene(CodegenSceneEnum.ADMIN.getScene())
|
||||
.setTableName("t_yunai_02")
|
||||
.setDataSourceConfigId(dataSourceConfigId));
|
||||
codegenTableMapper.insert(tableDO);
|
||||
|
||||
// 调用
|
||||
List<DatabaseTableRespVO> result = codegenService.getDatabaseTableList(dataSourceConfigId, name, comment);
|
||||
// 断言
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("t_yunai", result.get(0).getName());
|
||||
assertEquals("芋艿", result.get(0).getComment());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.baomidou.mybatisplus.generator.config.rules.IColumnType;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CodegenBuilderTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private CodegenBuilder codegenBuilder;
|
||||
|
||||
@Test
|
||||
public void testBuildTable() {
|
||||
// 准备参数
|
||||
TableInfo tableInfo = mock(TableInfo.class);
|
||||
// mock 方法
|
||||
when(tableInfo.getName()).thenReturn("system_user");
|
||||
when(tableInfo.getComment()).thenReturn("用户");
|
||||
|
||||
// 调用
|
||||
CodegenTableDO table = codegenBuilder.buildTable(tableInfo);
|
||||
// 断言
|
||||
assertEquals("system_user", table.getTableName());
|
||||
assertEquals("用户", table.getTableComment());
|
||||
assertEquals("system", table.getModuleName());
|
||||
assertEquals("user", table.getBusinessName());
|
||||
assertEquals("User", table.getClassName());
|
||||
assertEquals("用户", table.getClassComment());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildColumns() {
|
||||
// 准备参数
|
||||
Long tableId = randomLongId();
|
||||
TableField tableField = mock(TableField.class);
|
||||
List<TableField> tableFields = Collections.singletonList(tableField);
|
||||
// mock 方法
|
||||
TableField.MetaInfo metaInfo = mock(TableField.MetaInfo.class);
|
||||
when(tableField.getMetaInfo()).thenReturn(metaInfo);
|
||||
when(metaInfo.getJdbcType()).thenReturn(JdbcType.BIGINT);
|
||||
when(tableField.getComment()).thenReturn("编号");
|
||||
when(tableField.isKeyFlag()).thenReturn(true);
|
||||
IColumnType columnType = mock(IColumnType.class);
|
||||
when(tableField.getColumnType()).thenReturn(columnType);
|
||||
when(columnType.getType()).thenReturn("Long");
|
||||
when(tableField.getName()).thenReturn("id2");
|
||||
when(tableField.getPropertyName()).thenReturn("id");
|
||||
|
||||
// 调用
|
||||
List<CodegenColumnDO> columns = codegenBuilder.buildColumns(tableId, tableFields);
|
||||
// 断言
|
||||
assertEquals(1, columns.size());
|
||||
CodegenColumnDO column = columns.get(0);
|
||||
assertEquals(tableId, column.getTableId());
|
||||
assertEquals("id2", column.getColumnName());
|
||||
assertEquals("BIGINT", column.getDataType());
|
||||
assertEquals("编号", column.getColumnComment());
|
||||
assertFalse(column.getNullable());
|
||||
assertTrue(column.getPrimaryKey());
|
||||
assertEquals(1, column.getOrdinalPosition());
|
||||
assertEquals("Long", column.getJavaType());
|
||||
assertEquals("id", column.getJavaField());
|
||||
assertNull(column.getDictType());
|
||||
assertNotNull(column.getExample());
|
||||
assertFalse(column.getCreateOperation());
|
||||
assertTrue(column.getUpdateOperation());
|
||||
assertFalse(column.getListOperation());
|
||||
assertEquals("=", column.getListOperationCondition());
|
||||
assertTrue(column.getListOperationResult());
|
||||
assertEquals("input", column.getHtmlType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanitizeComment() {
|
||||
// 1. null / 空字符串:原样返回
|
||||
assertNull(codegenBuilder.sanitizeComment(null));
|
||||
assertEquals("", codegenBuilder.sanitizeComment(""));
|
||||
// 2. 无英文引号:原样返回
|
||||
assertEquals("无引号注释", codegenBuilder.sanitizeComment("无引号注释"));
|
||||
// 3. 含英文双引号:替换为中文左双引号
|
||||
assertEquals("含“双“引号", codegenBuilder.sanitizeComment("含\"双\"引号"));
|
||||
// 4. 含英文单引号:替换为中文左单引号
|
||||
assertEquals("含‘单‘引号", codegenBuilder.sanitizeComment("含'单'引号"));
|
||||
// 5. 双 / 单引号混合
|
||||
assertEquals("“混‘搭“‘", codegenBuilder.sanitizeComment("\"混'搭\"'"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.ZipUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Spy;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的单元测试抽象基类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
|
||||
|
||||
/**
|
||||
* 测试文件资源目录
|
||||
*/
|
||||
private String resourcesPath = "";
|
||||
|
||||
@InjectMocks
|
||||
protected CodegenEngine codegenEngine;
|
||||
|
||||
@Spy
|
||||
protected CodegenProperties codegenProperties = new CodegenProperties()
|
||||
.setBasePackage("cn.iocoder.yudao")
|
||||
.setVoType(CodegenVOTypeEnum.VO.getType())
|
||||
.setDeleteBatchEnable(true)
|
||||
.setUnitTestEnable(true)
|
||||
.setImportEnable(false);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
codegenEngine.setJakartaEnable(true); // 强制使用 jakarta,保证单测可以基于 jakarta 断言
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 获取测试文件 resources 路径,writeResult 调试用
|
||||
String absolutePath = FileUtil.getAbsolutePath("application-unit-test.yaml");
|
||||
resourcesPath = absolutePath.split("/target")[0] + "/src/test/resources/codegen/";
|
||||
}
|
||||
|
||||
protected static CodegenTableDO getTable(String name) {
|
||||
String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
|
||||
return JsonUtils.parseObject(content, "table", CodegenTableDO.class);
|
||||
}
|
||||
|
||||
protected static List<CodegenColumnDO> getColumnList(String name) {
|
||||
String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
|
||||
List<CodegenColumnDO> list = JsonUtils.parseArray(content, "columns", CodegenColumnDO.class);
|
||||
list.forEach(column -> {
|
||||
if (column.getNullable() == null) {
|
||||
column.setNullable(false);
|
||||
}
|
||||
if (column.getCreateOperation() == null) {
|
||||
column.setCreateOperation(false);
|
||||
}
|
||||
if (column.getUpdateOperation() == null) {
|
||||
column.setUpdateOperation(false);
|
||||
}
|
||||
if (column.getListOperation() == null) {
|
||||
column.setListOperation(false);
|
||||
}
|
||||
if (column.getListOperationResult() == null) {
|
||||
column.setListOperationResult(false);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成断言数据的开关,命令行加 {@code -Dcodegen.regenerate=true} 启用
|
||||
*/
|
||||
private static final boolean REGENERATE = Boolean.parseBoolean(System.getProperty("codegen.regenerate", "false"));
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected void assertResult(Map<String, String> result, String path) {
|
||||
if (REGENERATE) {
|
||||
writeResult(result, resourcesPath + path);
|
||||
return;
|
||||
}
|
||||
String assertContent = ResourceUtil.readUtf8Str("codegen/" + path + "/assert.json");
|
||||
List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
|
||||
Set<String> expectedFiles = asserts.stream()
|
||||
.map(m -> (String) m.get("filePath"))
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
|
||||
// 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格
|
||||
asserts.forEach(assertMap -> {
|
||||
String contentPath = (String) assertMap.get("contentPath");
|
||||
String filePath = (String) assertMap.get("filePath");
|
||||
String expected = normalizeLineEndings(ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath));
|
||||
String actual = result.get(filePath);
|
||||
assertEquals(expected, normalizeLineEndings(actual), filePath + ":不匹配");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一换行符并忽略文件末尾换行,避免 Windows/Unix 落盘差异导致快照断言失败
|
||||
*/
|
||||
private static String normalizeLineEndings(String content) {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
content = content.replace("\r\n", "\n").replace('\r', '\n');
|
||||
return content.replaceAll("\\n+\\z", "");
|
||||
}
|
||||
|
||||
// ==================== 调试专用 ====================
|
||||
|
||||
/**
|
||||
* 【调试使用】将生成的代码,写入到文件
|
||||
*
|
||||
* @param result 生成的代码
|
||||
* @param path 写入文件的路径
|
||||
*/
|
||||
protected void writeFile(Map<String, String> result, String path) {
|
||||
// 生成压缩包
|
||||
String[] paths = result.keySet().toArray(new String[0]);
|
||||
ByteArrayInputStream[] ins = result.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ZipUtil.zip(outputStream, paths, ins);
|
||||
// 写入文件
|
||||
FileUtil.writeBytes(outputStream.toByteArray(), path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【调试使用】将生成的结果,写入到文件
|
||||
*
|
||||
* @param result 生成的代码
|
||||
* @param basePath 写入文件的路径(绝对路径)
|
||||
*/
|
||||
protected void writeResult(Map<String, String> result, String basePath) {
|
||||
// 写入文件内容
|
||||
List<Map<String, String>> asserts = new ArrayList<>();
|
||||
Set<String> usedContentPaths = new LinkedHashSet<>();
|
||||
result.forEach((filePath, fileContent) -> {
|
||||
String lastFilePath = StrUtil.subAfter(filePath, '/', true);
|
||||
String ext = StrUtil.subAfter(lastFilePath, '.', true);
|
||||
String name = StrUtil.subBefore(lastFilePath, '.', true);
|
||||
String parentPath = StrUtil.subBefore(filePath, '/' + lastFilePath, true);
|
||||
String parentDir = StrUtil.subAfter(parentPath, '/', true);
|
||||
// index.vue 按页面语义稳定归档,避免结果迭代顺序导致列表页和表单页的快照路径互换
|
||||
String contentPath = "index".equals(name) && ("form".equals(parentDir) || "detail".equals(parentDir))
|
||||
? ext + '/' + parentDir + '/' + name : ext + '/' + name;
|
||||
// ERP 多个子表的 form/index.vue 仍可能撞名,继续补充祖父目录区分
|
||||
if (usedContentPaths.contains(contentPath)) {
|
||||
String grandParentDir = StrUtil.subAfter(StrUtil.subBefore(parentPath, '/' + parentDir, true), '/', true);
|
||||
contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name;
|
||||
for (int i = 2; usedContentPaths.contains(contentPath); i++) {
|
||||
contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name + '-' + i;
|
||||
}
|
||||
}
|
||||
usedContentPaths.add(contentPath);
|
||||
asserts.add(MapUtil.<String, String>builder().put("filePath", filePath)
|
||||
.put("contentPath", contentPath).build());
|
||||
FileUtil.writeUtf8String(fileContent, basePath + "/" + contentPath);
|
||||
});
|
||||
// 写入 assert.json 文件
|
||||
FileUtil.writeUtf8String(JsonUtils.toJsonPrettyString(asserts), basePath + "/assert.json");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 Admin Uniapp + Wot UI 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineUniappTest extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue3_admin_uniapp_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_dictSearch() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用并断言数字、布尔字典
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
String searchForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue");
|
||||
assertFalse(searchForm.contains("<wd-radio"));
|
||||
assertTrue(searchForm.contains("all-option"));
|
||||
assertTrue(searchForm.contains(":columns=\"getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)\""));
|
||||
assertTrue(searchForm.contains(":columns=\"getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)\""));
|
||||
|
||||
// 调用并断言字符串字典
|
||||
columns.stream().filter(column -> "sex".equals(column.getJavaField())).findFirst().orElseThrow()
|
||||
.setJavaType("String");
|
||||
result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
searchForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue");
|
||||
assertTrue(searchForm.contains(":columns=\"getStrDictOptions(DICT_TYPE.SYSTEM_USER_SEX)\""));
|
||||
assertTrue(searchForm.contains("sex: undefined as string | undefined,"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue3_admin_uniapp_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_semantics() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow()
|
||||
.setJavaType("Float").setHtmlType("input");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
|
||||
assertTrue(api.contains("birthday: number"));
|
||||
assertTrue(api.contains("createTime?: number"));
|
||||
assertTrue(api.contains("memo: number"));
|
||||
assertFalse(api.contains("createTime?: Date"));
|
||||
String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue");
|
||||
assertTrue(form.contains("v-model=\"formData.memo\""));
|
||||
assertTrue(form.contains(":precision=\"2\""));
|
||||
String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
|
||||
assertTrue(detail.contains("type=\"danger\""));
|
||||
assertFalse(detail.contains("type=\"error\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_stringPrimaryKey() {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
columns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
|
||||
// 准备子表
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
contactColumns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
|
||||
contactColumns.stream().filter(column -> Objects.equals(column.getId(), 100L))
|
||||
.findFirst().orElseThrow().setJavaType("String");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
List.of(contactTable), List.of(contactColumns));
|
||||
// 断言
|
||||
String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
|
||||
assertTrue(api.contains("return http.post<string>('/infra/student/create', data)"));
|
||||
assertTrue(api.contains("studentId: string"));
|
||||
assertTrue(api.contains("getStudentContact(id: string)"));
|
||||
assertTrue(api.contains("deleteStudentContact(id: string)"));
|
||||
String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
|
||||
assertFalse(detail.contains("Number(props.id)"));
|
||||
String subForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
|
||||
assertTrue(subForm.contains("id?: string"));
|
||||
assertTrue(subForm.contains("studentId: string"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_treeSearch() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
String index = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue");
|
||||
assertTrue(index.contains("currentParentId.value === 0\n ? list.value"));
|
||||
assertFalse(index.contains("list.value.filter"));
|
||||
String breadcrumb = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue");
|
||||
assertTrue(breadcrumb.contains("modelValue: number"));
|
||||
assertFalse(breadcrumb.contains("TreeValue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateErpSymbol() {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备两个归一化名称相同的子表
|
||||
CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
|
||||
List<CodegenColumnDO> erpItemColumns = getColumnList("contact");
|
||||
|
||||
// 调用
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns)));
|
||||
// 断言
|
||||
assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterSymbol() {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备两个规范化名称相同的子表
|
||||
CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
|
||||
List<CodegenColumnDO> erpItemColumns = getColumnList("contact");
|
||||
|
||||
// 调用并断言
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns)));
|
||||
assertTrue(exception.getMessage().contains("主子表规范化类名(OrderItem)重复"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterFieldNormal() {
|
||||
testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_NORMAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterFieldInner() {
|
||||
testExecute_duplicateMasterField(CodegenTemplateTypeEnum.MASTER_INNER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterFieldWithMainNormal() {
|
||||
testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_NORMAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterFieldWithMainInner() {
|
||||
testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum.MASTER_INNER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_duplicateMasterFieldErp() {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备属性名同为 items 的子表:Item 一对多、Items 一对一
|
||||
CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false);
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(crmItemTable, erpItemsTable),
|
||||
Arrays.asList(getColumnList("contact"), getColumnList("contact")));
|
||||
// 断言
|
||||
assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/item/form/index.vue"));
|
||||
assertTrue(result.containsKey("yudao-ui-admin-uniapp/src/pages-infra/demo/items/form/index.vue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterGeneratedSource_duplicatePath() {
|
||||
// 准备参数
|
||||
Map<String, String> generatedSources = new HashMap<>();
|
||||
Map<String, Object> bindingMap = new HashMap<>();
|
||||
bindingMap.put("subIndex", 0);
|
||||
CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap);
|
||||
bindingMap.put("subIndex", 1);
|
||||
|
||||
// 调用
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> CodegenEngine.registerGeneratedSource(generatedSources, "same.vue", "form_sub.vm", bindingMap));
|
||||
// 断言
|
||||
assertTrue(exception.getMessage().contains("生成文件路径重复:same.vue"));
|
||||
assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=0]"));
|
||||
assertTrue(exception.getMessage().contains("form_sub.vm[subIndex=1]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_masterNormal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue3_admin_uniapp_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_masterInner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue3_admin_uniapp_master_inner");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_masterErp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue3_admin_uniapp_master_erp");
|
||||
}
|
||||
|
||||
private void testExecute_duplicateMasterField(CodegenTemplateTypeEnum templateType) {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备属性名同为 items 的子表:Item 一对多、Items 一对一
|
||||
CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
CodegenTableDO erpItemsTable = getTable("contact").setModuleName("erp").setClassName("ErpItems")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
|
||||
List<CodegenColumnDO> erpItemsColumns = getColumnList("contact");
|
||||
|
||||
// 调用并断言
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(crmItemTable, erpItemsTable), Arrays.asList(crmItemColumns, erpItemsColumns)));
|
||||
assertTrue(exception.getMessage().contains("主子表属性名(items)重复"));
|
||||
}
|
||||
|
||||
private void testExecute_duplicateMasterFieldWithMain(CodegenTemplateTypeEnum templateType) {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
columns.stream().filter(column -> "memo".equals(column.getJavaField())).findFirst().orElseThrow()
|
||||
.setJavaField("items");
|
||||
// 准备一对多子表 Item,生成属性名 items
|
||||
CodegenTableDO itemTable = getTable("contact").setModuleName("crm").setClassName("CrmItem")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
|
||||
// 调用并断言
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
List.of(itemTable), List.of(getColumnList("contact"))));
|
||||
assertTrue(exception.getMessage().contains("主子表属性名(items)重复"));
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备主表
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备一对多子表
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备一对一子表
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
if (templateType != CodegenTemplateTypeEnum.MASTER_ERP) {
|
||||
String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
|
||||
assertTrue(api.contains("studentTeacher?: StudentTeacher | null"));
|
||||
String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue");
|
||||
assertTrue(form.contains("ref<Partial<StudentTeacher> | null>(null)"));
|
||||
assertTrue(form.contains("studentTeacher.value = studentTeacherData || null"));
|
||||
assertTrue(form.contains(":model-value=\"item.birthday || Date.now()\""));
|
||||
assertTrue(form.contains("v-model=\"item.sex\""));
|
||||
assertTrue(form.contains("<yd-upload-img v-model=\"item.avatar\""));
|
||||
assertTrue(form.contains("<yd-upload-file v-model=\"item.video\""));
|
||||
assertTrue(form.contains("<yd-upload-file v-model=\"item.video\" :limit=\"1\""));
|
||||
assertTrue(form.contains("v-model=\"item.description\""));
|
||||
assertTrue(form.contains("v-model=\"item.memo\""));
|
||||
assertTrue(form.contains("birthday: undefined"));
|
||||
assertTrue(form.contains("function validateSubTables()"));
|
||||
} else {
|
||||
String contactForm = result.get(
|
||||
"yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
|
||||
assertTrue(contactForm.contains(":model-value=\"formData.birthday || Date.now()\""));
|
||||
assertTrue(contactForm.contains("v-model=\"formData.sex\""));
|
||||
assertTrue(contactForm.contains("<yd-upload-img v-model=\"formData.avatar\""));
|
||||
assertTrue(contactForm.contains("<yd-upload-file v-model=\"formData.video\""));
|
||||
assertTrue(contactForm.contains("<yd-upload-file v-model=\"formData.video\" :limit=\"1\""));
|
||||
assertTrue(contactForm.contains("v-model=\"formData.description\""));
|
||||
assertTrue(contactForm.contains("v-model=\"formData.memo\""));
|
||||
assertTrue(contactForm.contains("birthday: undefined"));
|
||||
assertTrue(contactForm.contains(":columns=\"getBoolDictOptions"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Antdv Next 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdvNextTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Pattern ANTDV_NEXT_VALUE_IMPORT_PATTERN = Pattern.compile(
|
||||
"import\\s*\\{([^}]*)}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
|
||||
private static final Pattern RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile("<RangePicker\\b");
|
||||
private static final Pattern RANGE_PICKER_HELPER_BINDING_PATTERN = Pattern.compile(
|
||||
"\\bv-bind\\s*=\\s*([\"'])\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)\\s*\\1");
|
||||
private static final Pattern RANGE_PICKER_HELPER_CALL_PATTERN = Pattern.compile(
|
||||
"\\bgetRangePickerDefaultProps\\s*\\(\\s*\\)");
|
||||
private static final Pattern RANGE_PICKER_HELPER_IMPORT_PATTERN = Pattern.compile(
|
||||
"import\\s*\\{[^}]*\\bgetRangePickerDefaultProps\\b[^}]*}"
|
||||
+ "\\s*from\\s*'#/utils(?:/rangePickerProps)?';?", Pattern.DOTALL);
|
||||
private static final Pattern SCHEMA_RANGE_PICKER_COMPONENT_PATTERN = Pattern.compile(
|
||||
"\\bcomponent\\s*:\\s*([\"'])RangePicker\\1");
|
||||
private static final Pattern SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN = Pattern.compile(
|
||||
"\\.\\.\\.\\s*getRangePickerDefaultProps\\s*\\(\\s*\\)");
|
||||
|
||||
private static final String IMPORT_FORM_PATH =
|
||||
"yudao-ui-admin-vben/src/views/infra/demo/modules/import-form.vue";
|
||||
private static final String SCHEMA_DATA_PATH =
|
||||
"yudao-ui-admin-vben/src/views/infra/demo/data.ts";
|
||||
private static final String STUDENT_CONTACT_FORM_PATH =
|
||||
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue";
|
||||
private static final String STUDENT_TEACHER_FORM_PATH =
|
||||
"yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue";
|
||||
|
||||
private static final List<String> INNER_TEMPLATE_PATHS = Arrays.asList(
|
||||
"codegen/vue3_vben5_antdv_next/general/views/modules/form_sub_inner.vue.vm",
|
||||
"codegen/vue3_vben5_antdv_next/general/views/modules/list_sub_inner.vue.vm",
|
||||
"codegen/vue3_vben5_antdv_next/schema/views/modules/form_sub_inner.vue.vm",
|
||||
"codegen/vue3_vben5_antdv_next/schema/views/modules/list_sub_inner.vue.vm");
|
||||
|
||||
@Test
|
||||
public void testExecute_schemaMasterNormal() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_NORMAL);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertSchemaRangePickerGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_schemaMasterErp() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_ERP);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertSchemaRangePickerGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_schemaMasterInner() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_INNER);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertSchemaRangePickerGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalTree() {
|
||||
// 调用
|
||||
Map<String, String> result = executeTree(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType());
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertRuleGenerated(result);
|
||||
assertComponentGenerated(result, "FormItem");
|
||||
assertComponentGenerated(result, "TreeSelect");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalMasterNormal() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_NORMAL);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertRuleGenerated(result);
|
||||
assertGeneralCrudComponentsGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalMasterNormal_oneToOneJoinColumnOnlySelect() {
|
||||
// 调用
|
||||
Map<String, String> result = executeGeneralMasterNormalWithOneToOneJoinSelect();
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
String content = result.get(STUDENT_TEACHER_FORM_PATH);
|
||||
assertNotNull(content, STUDENT_TEACHER_FORM_PATH + ":未生成一对一子表表单");
|
||||
assertTrue(content.contains("<Select v-model:value=\"formData.studentId\""),
|
||||
STUDENT_TEACHER_FORM_PATH + ":应渲染 join 列的 Select 组件");
|
||||
assertTrue(content.contains("<SelectOption"), STUDENT_TEACHER_FORM_PATH + ":应渲染 SelectOption 组件");
|
||||
assertRootValueImport(STUDENT_TEACHER_FORM_PATH, content, "\\bSelectOption\\b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalMasterNormal_oneToManyJoinColumnOnlySelect() {
|
||||
// 调用
|
||||
Map<String, String> result = executeGeneralMasterNormalWithOneToManyJoinSelect();
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
String content = result.get(STUDENT_CONTACT_FORM_PATH);
|
||||
assertNotNull(content, STUDENT_CONTACT_FORM_PATH + ":未生成一对多子表表单");
|
||||
assertTrue(content.contains("<VxeTable"), STUDENT_CONTACT_FORM_PATH + ":应生成一对多子表表格");
|
||||
assertFalse(content.contains("<VxeColumn field=\"studentId\""),
|
||||
STUDENT_CONTACT_FORM_PATH + ":不应生成 join 列");
|
||||
assertFalse(content.contains("<Select v-model:value=\"row.studentId\""),
|
||||
STUDENT_CONTACT_FORM_PATH + ":不应渲染 join 列的 Select 组件");
|
||||
assertFalse(content.contains("<SelectOption"), STUDENT_CONTACT_FORM_PATH + ":不应渲染 SelectOption 组件");
|
||||
assertFalse(createRootImportPattern("import\\s*\\{", "\\bSelectOption\\b").matcher(content).find(),
|
||||
STUDENT_CONTACT_FORM_PATH + ":不应导入 SelectOption 组件");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalMasterInner() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_INNER);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertRuleGenerated(result);
|
||||
assertGeneralCrudComponentsGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_generalMasterErp() {
|
||||
// 调用
|
||||
Map<String, String> result = executeMaster(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType(),
|
||||
CodegenTemplateTypeEnum.MASTER_ERP);
|
||||
|
||||
// 断言
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
assertRuleGenerated(result);
|
||||
assertGeneralCrudComponentsGenerated(result);
|
||||
assertTabsGenerated(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_oneImportEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
|
||||
// 调用并断言
|
||||
assertImportFormCompatibleWithAntdvNext(
|
||||
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_SCHEMA.getType()));
|
||||
assertImportFormCompatibleWithAntdvNext(
|
||||
executeOne(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInnerTemplatesParseAntdvNext() {
|
||||
INNER_TEMPLATE_PATHS.forEach(path -> {
|
||||
String template = ResourceUtil.readUtf8Str(path);
|
||||
assertFalse(template.contains("codegen/vue3_vben5_antd/"), path + ":不应引用 Ant Design Vue 模板");
|
||||
assertTrue(template.contains("codegen/vue3_vben5_antdv_next/"), path + ":应引用 Antdv Next 模板");
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> executeOne(Integer frontType) {
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(frontType)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
|
||||
}
|
||||
|
||||
private Map<String, String> executeTree(Integer frontType) {
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(frontType)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("category"), null, null);
|
||||
}
|
||||
|
||||
private Map<String, String> executeGeneralMasterNormalWithOneToOneJoinSelect() {
|
||||
return executeGeneralMasterNormalWithJoinSelect("teacher", 200L, false);
|
||||
}
|
||||
|
||||
private Map<String, String> executeGeneralMasterNormalWithOneToManyJoinSelect() {
|
||||
return executeGeneralMasterNormalWithJoinSelect("contact", 100L, true);
|
||||
}
|
||||
|
||||
private Map<String, String> executeGeneralMasterNormalWithJoinSelect(String subTableName,
|
||||
Long subJoinColumnId,
|
||||
boolean subJoinMany) {
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
|
||||
CodegenTableDO subTable = getTable(subTableName)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL.getType())
|
||||
.setSubJoinColumnId(subJoinColumnId).setSubJoinMany(subJoinMany);
|
||||
List<CodegenColumnDO> subColumns = getColumnList(subTableName);
|
||||
// 稀疏场景:join 列是唯一的 select,避免其它 select 掩盖 join 列对导入判定的影响。
|
||||
subColumns.forEach(column -> {
|
||||
if (subJoinColumnId.equals(column.getId())) {
|
||||
column.setHtmlType("select");
|
||||
column.setDictType("");
|
||||
} else if ("select".equals(column.getHtmlType())) {
|
||||
column.setHtmlType("input");
|
||||
}
|
||||
});
|
||||
return codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
|
||||
List.of(subTable), List.of(subColumns));
|
||||
}
|
||||
|
||||
private Map<String, String> executeMaster(Integer frontType, CodegenTemplateTypeEnum templateType) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(frontType)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(frontType)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(frontType)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
return codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
}
|
||||
|
||||
private static void assertGeneratedCodeCompatibleWithAntdvNext(Map<String, String> result) {
|
||||
assertTrue(result.values().stream().anyMatch(content -> content.contains("from 'antdv-next")),
|
||||
"生成结果应使用 antdv-next");
|
||||
result.forEach((filePath, content) -> {
|
||||
assertFalse(content.contains("ant-design-vue"), filePath + ":不应使用 ant-design-vue");
|
||||
assertFalse(content.contains("antdv-next/es/"), filePath + ":不应使用未导出的 antdv-next 深层路径");
|
||||
assertFalse(content.contains("<Form.Item"), filePath + ":不应使用 Form.Item");
|
||||
assertFalse(content.contains("</Form.Item>"), filePath + ":不应使用 Form.Item");
|
||||
assertRootValueImportsSorted(filePath, content);
|
||||
assertRootValueImportMatchesUsage(filePath, content,
|
||||
"\\bDateRangePicker\\s+as\\s+RangePicker\\b", "<RangePicker\\b", "RangePicker");
|
||||
assertRootValueImportMatchesUsage(filePath, content,
|
||||
"\\bFormItem\\b", "<FormItem\\b", "FormItem");
|
||||
assertRootValueImportMatchesUsage(filePath, content,
|
||||
"\\bSelectOption\\b", "<SelectOption\\b", "SelectOption");
|
||||
assertRootValueImportMatchesUsage(filePath, content,
|
||||
"\\bTabPane\\b", "<TabPane\\b", "TabPane");
|
||||
assertRootValueImportMatchesUsage(filePath, content,
|
||||
"\\bTabs\\b", "<Tabs\\b", "Tabs");
|
||||
assertRootTypeImportMatchesUsage(filePath, content,
|
||||
"\\bRule\\b", "\\bRule\\b", "Rule");
|
||||
assertRangePickerHelperImportMatchesUsage(filePath, content);
|
||||
if (filePath.endsWith(".vue")) {
|
||||
assertRangePickerHelperBindingMatchesComponent(filePath, content);
|
||||
} else if (filePath.endsWith(".ts")) {
|
||||
assertSchemaRangePickerHelperMatchesComponent(filePath, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertImportFormCompatibleWithAntdvNext(Map<String, String> result) {
|
||||
assertGeneratedCodeCompatibleWithAntdvNext(result);
|
||||
String content = result.get(IMPORT_FORM_PATH);
|
||||
assertNotNull(content, IMPORT_FORM_PATH + ":未生成导入表单");
|
||||
assertTrue(content.contains("<Upload"), IMPORT_FORM_PATH + ":应生成 Upload 组件");
|
||||
assertRootValueImport(IMPORT_FORM_PATH, content, "\\bUpload\\b");
|
||||
assertTrue(content.contains("beforeUpload(file: File)"), IMPORT_FORM_PATH + ":上传参数应使用原生 File 类型");
|
||||
assertFalse(content.contains("UploadFile"), IMPORT_FORM_PATH + ":不应使用上传列表项类型 UploadFile");
|
||||
assertFalse(content.contains("as unknown as File"), IMPORT_FORM_PATH + ":不应通过双重断言转换上传文件");
|
||||
assertFalse(content.contains("FileType"), IMPORT_FORM_PATH + ":不应继续使用旧 FileType");
|
||||
}
|
||||
|
||||
private static void assertTabsGenerated(Map<String, String> result) {
|
||||
assertComponentGenerated(result, "Tabs");
|
||||
assertComponentGenerated(result, "TabPane");
|
||||
}
|
||||
|
||||
private static void assertGeneralCrudComponentsGenerated(Map<String, String> result) {
|
||||
assertComponentGenerated(result, "FormItem");
|
||||
assertComponentGenerated(result, "RangePicker");
|
||||
assertComponentGenerated(result, "SelectOption");
|
||||
}
|
||||
|
||||
private static void assertRuleGenerated(Map<String, String> result) {
|
||||
assertTrue(result.values().stream().map(CodegenEngineVben5AntdvNextTest::removeAntdvNextRootImports)
|
||||
.anyMatch(content -> Pattern.compile("\\bRule\\b").matcher(content).find()),
|
||||
"生成结果应包含 Rule 类型校验规则");
|
||||
}
|
||||
|
||||
private static void assertSchemaRangePickerGenerated(Map<String, String> result) {
|
||||
String content = result.get(SCHEMA_DATA_PATH);
|
||||
assertNotNull(content, SCHEMA_DATA_PATH + ":未生成 Schema 配置");
|
||||
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
|
||||
assertTrue(componentCount > 0, SCHEMA_DATA_PATH + ":应生成 RangePicker 组件配置");
|
||||
}
|
||||
|
||||
private static void assertComponentGenerated(Map<String, String> result, String component) {
|
||||
assertTrue(result.values().stream().anyMatch(content -> content.contains("<" + component)),
|
||||
"生成结果应包含 " + component + " 组件");
|
||||
}
|
||||
|
||||
private static void assertRootValueImportMatchesUsage(String filePath, String content,
|
||||
String expectedImportRegex, String usageRegex,
|
||||
String bindingName) {
|
||||
assertRootImportMatchesUsage(filePath, content, "import\\s*\\{", expectedImportRegex,
|
||||
usageRegex, bindingName);
|
||||
}
|
||||
|
||||
private static void assertRootTypeImportMatchesUsage(String filePath, String content,
|
||||
String expectedImportRegex, String usageRegex,
|
||||
String bindingName) {
|
||||
assertRootImportMatchesUsage(filePath, content, "import\\s+type\\s*\\{", expectedImportRegex,
|
||||
usageRegex, bindingName);
|
||||
}
|
||||
|
||||
private static void assertRootImportMatchesUsage(String filePath, String content, String importPrefix,
|
||||
String expectedImportRegex, String usageRegex,
|
||||
String bindingName) {
|
||||
boolean imported = createRootImportPattern(importPrefix, expectedImportRegex).matcher(content).find();
|
||||
boolean used = Pattern.compile(usageRegex).matcher(removeAntdvNextRootImports(content)).find();
|
||||
assertEquals(used, imported, filePath + ":" + bindingName + (used ? " 缺少合法根导入" : " 存在孤立根导入"));
|
||||
}
|
||||
|
||||
private static String removeAntdvNextRootImports(String content) {
|
||||
Pattern pattern = Pattern.compile("import\\s+(?:type\\s+)?\\{[^}]*}\\s*from\\s*'antdv-next';?",
|
||||
Pattern.DOTALL);
|
||||
return pattern.matcher(content).replaceAll("");
|
||||
}
|
||||
|
||||
private static void assertRootValueImport(String filePath, String content, String expectedRegex) {
|
||||
assertRootImport(filePath, content, "import\\s*\\{", expectedRegex);
|
||||
}
|
||||
|
||||
private static void assertRootValueImportsSorted(String filePath, String content) {
|
||||
var matcher = ANTDV_NEXT_VALUE_IMPORT_PATTERN.matcher(content);
|
||||
while (matcher.find()) {
|
||||
List<String> actual = Arrays.stream(matcher.group(1).split(","))
|
||||
.map(String::trim)
|
||||
.filter(item -> !item.isEmpty())
|
||||
.map(item -> item.replaceFirst("^.*\\s+as\\s+", ""))
|
||||
.toList();
|
||||
List<String> expected = actual.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList();
|
||||
assertEquals(expected, actual, filePath + ":antdv-next 命名导入未按 natural 顺序排列");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertRangePickerHelperImportMatchesUsage(String filePath, String content) {
|
||||
boolean imported = RANGE_PICKER_HELPER_IMPORT_PATTERN.matcher(content).find();
|
||||
boolean called = RANGE_PICKER_HELPER_CALL_PATTERN.matcher(content).find();
|
||||
assertEquals(called, imported,
|
||||
filePath + ":getRangePickerDefaultProps() 调用与导入不一致");
|
||||
}
|
||||
|
||||
private static void assertRangePickerHelperBindingMatchesComponent(String filePath, String content) {
|
||||
long componentCount = countMatches(RANGE_PICKER_COMPONENT_PATTERN, content);
|
||||
long bindingCount = countMatches(RANGE_PICKER_HELPER_BINDING_PATTERN, content);
|
||||
assertEquals(componentCount, bindingCount,
|
||||
filePath + ":RangePicker 与 getRangePickerDefaultProps() 绑定数量不一致");
|
||||
}
|
||||
|
||||
private static void assertSchemaRangePickerHelperMatchesComponent(String filePath, String content) {
|
||||
long componentCount = countMatches(SCHEMA_RANGE_PICKER_COMPONENT_PATTERN, content);
|
||||
long helperSpreadCount = countMatches(SCHEMA_RANGE_PICKER_HELPER_SPREAD_PATTERN, content);
|
||||
assertEquals(componentCount, helperSpreadCount,
|
||||
filePath + ":Schema RangePicker 与 getRangePickerDefaultProps() 展开数量不一致");
|
||||
}
|
||||
|
||||
private static long countMatches(Pattern pattern, String content) {
|
||||
return pattern.matcher(content).results().count();
|
||||
}
|
||||
|
||||
private static void assertRootImport(String filePath, String content, String importPrefix, String expectedRegex) {
|
||||
Pattern pattern = createRootImportPattern(importPrefix, expectedRegex);
|
||||
assertTrue(pattern.matcher(content).find(), filePath + ":缺少合法的 antdv-next 根导入 " + expectedRegex);
|
||||
}
|
||||
|
||||
private static Pattern createRootImportPattern(String importPrefix, String expectedRegex) {
|
||||
return Pattern.compile(importPrefix + "[^}]*" + expectedRegex
|
||||
+ "[^}]*}\\s*from\\s*'antdv-next';?", Pattern.DOTALL);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue2 + Element UI 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + "/vue2_one");
|
||||
// 断言
|
||||
assertResult(result, "/vue2_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue2_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + "/vue2_tree");
|
||||
// 断言
|
||||
assertResult(result, "/vue2_tree");
|
||||
// writeFile(result, "/Users/yunai/test/demo66.zip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_master_normal() {
|
||||
testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue2_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_master_erp() {
|
||||
testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue2_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_master_inner() {
|
||||
testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue2_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_vue2_master(CodegenTemplateTypeEnum templateType,
|
||||
String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + path);
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
// writeFile(result, "/Users/yunai/test/demo11.zip");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue2 + Element Plus 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + "/vue3_one");
|
||||
// 断言
|
||||
assertResult(result, "/vue3_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue3_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + "/vue3_tree");
|
||||
// 断言
|
||||
assertResult(result, "/vue3_tree");
|
||||
// writeFile(result, "/Users/yunai/test/demo66.zip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_master_normal() {
|
||||
testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue3_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_master_erp() {
|
||||
testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue3_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_master_inner() {
|
||||
testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue3_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_vue3_master(CodegenTemplateTypeEnum templateType,
|
||||
String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 生成测试文件
|
||||
//writeResult(result, resourcesPath + path);
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
// writeFile(result, "/Users/yunai/test/demo11.zip");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package cn.iocoder.yudao.module.infra.service.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.config.vo.ConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.config.ConfigMapper;
|
||||
import cn.iocoder.yudao.module.infra.enums.config.ConfigTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.randomEle;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@Import(ConfigServiceImpl.class)
|
||||
public class ConfigServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private ConfigServiceImpl configService;
|
||||
|
||||
@Resource
|
||||
private ConfigMapper configMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateConfig_success() {
|
||||
// 准备参数
|
||||
ConfigSaveReqVO reqVO = randomPojo(ConfigSaveReqVO.class)
|
||||
.setId(null); // 防止 id 被赋值,导致唯一性校验失败
|
||||
|
||||
// 调用
|
||||
Long configId = configService.createConfig(reqVO);
|
||||
// 断言
|
||||
assertNotNull(configId);
|
||||
// 校验记录的属性是否正确
|
||||
ConfigDO config = configMapper.selectById(configId);
|
||||
assertPojoEquals(reqVO, config, "id");
|
||||
assertEquals(ConfigTypeEnum.CUSTOM.getType(), config.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateConfig_success() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO();
|
||||
configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
ConfigSaveReqVO reqVO = randomPojo(ConfigSaveReqVO.class, o -> {
|
||||
o.setId(dbConfig.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
// 调用
|
||||
configService.updateConfig(reqVO);
|
||||
// 校验是否更新正确
|
||||
ConfigDO config = configMapper.selectById(reqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(reqVO, config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteConfig_success() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO(o -> {
|
||||
o.setType(ConfigTypeEnum.CUSTOM.getType()); // 只能删除 CUSTOM 类型
|
||||
});
|
||||
configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbConfig.getId();
|
||||
|
||||
// 调用
|
||||
configService.deleteConfig(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(configMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteConfig_canNotDeleteSystemType() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO(o -> {
|
||||
o.setType(ConfigTypeEnum.SYSTEM.getType()); // SYSTEM 不允许删除
|
||||
});
|
||||
configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbConfig.getId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> configService.deleteConfig(id), CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateConfigExists_success() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfigDO = randomConfigDO();
|
||||
configMapper.insert(dbConfigDO);// @Sql: 先插入出一条存在的数据
|
||||
|
||||
// 调用成功
|
||||
configService.validateConfigExists(dbConfigDO.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateConfigExist_notExists() {
|
||||
assertServiceException(() -> configService.validateConfigExists(randomLongId()), CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateConfigKeyUnique_success() {
|
||||
// 调用,成功
|
||||
configService.validateConfigKeyUnique(randomLongId(), randomString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateConfigKeyUnique_keyDuplicateForCreate() {
|
||||
// 准备参数
|
||||
String key = randomString();
|
||||
// mock 数据
|
||||
configMapper.insert(randomConfigDO(o -> o.setConfigKey(key)));
|
||||
|
||||
// 调用,校验异常
|
||||
assertServiceException(() -> configService.validateConfigKeyUnique(null, key),
|
||||
CONFIG_KEY_DUPLICATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateConfigKeyUnique_keyDuplicateForUpdate() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
String key = randomString();
|
||||
// mock 数据
|
||||
configMapper.insert(randomConfigDO(o -> o.setConfigKey(key)));
|
||||
|
||||
// 调用,校验异常
|
||||
assertServiceException(() -> configService.validateConfigKeyUnique(id, key),
|
||||
CONFIG_KEY_DUPLICATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfigPage() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO(o -> { // 等会查询到
|
||||
o.setName("芋艿");
|
||||
o.setConfigKey("yunai");
|
||||
o.setType(ConfigTypeEnum.SYSTEM.getType());
|
||||
o.setCreateTime(buildTime(2021, 2, 1));
|
||||
});
|
||||
configMapper.insert(dbConfig);
|
||||
// 测试 name 不匹配
|
||||
configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setName("土豆")));
|
||||
// 测试 key 不匹配
|
||||
configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setConfigKey("tudou")));
|
||||
// 测试 type 不匹配
|
||||
configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setType(ConfigTypeEnum.CUSTOM.getType())));
|
||||
// 测试 createTime 不匹配
|
||||
configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setCreateTime(buildTime(2021, 1, 1))));
|
||||
// 准备参数
|
||||
ConfigPageReqVO reqVO = new ConfigPageReqVO();
|
||||
reqVO.setName("艿");
|
||||
reqVO.setKey("nai");
|
||||
reqVO.setType(ConfigTypeEnum.SYSTEM.getType());
|
||||
reqVO.setCreateTime(buildBetweenTime(2021, 1, 15, 2021, 2, 15));
|
||||
|
||||
// 调用
|
||||
PageResult<ConfigDO> pageResult = configService.getConfigPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbConfig, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfig() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO();
|
||||
configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbConfig.getId();
|
||||
|
||||
// 调用
|
||||
ConfigDO config = configService.getConfig(id);
|
||||
// 断言
|
||||
assertNotNull(config);
|
||||
assertPojoEquals(dbConfig, config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConfigByKey() {
|
||||
// mock 数据
|
||||
ConfigDO dbConfig = randomConfigDO();
|
||||
configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
String key = dbConfig.getConfigKey();
|
||||
|
||||
// 调用
|
||||
ConfigDO config = configService.getConfigByKey(key);
|
||||
// 断言
|
||||
assertNotNull(config);
|
||||
assertPojoEquals(dbConfig, config);
|
||||
}
|
||||
|
||||
// ========== 随机对象 ==========
|
||||
|
||||
@SafeVarargs
|
||||
private static ConfigDO randomConfigDO(Consumer<ConfigDO>... consumers) {
|
||||
Consumer<ConfigDO> consumer = (o) -> {
|
||||
o.setType(randomEle(ConfigTypeEnum.values()).getType()); // 保证 key 的范围
|
||||
};
|
||||
return RandomUtils.randomPojo(ConfigDO.class, ArrayUtils.append(consumer, consumers));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package cn.iocoder.yudao.module.infra.service.db;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.crypto.symmetric.AES;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.EncryptTypeHandler;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.db.vo.DataSourceConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.db.DataSourceConfigMapper;
|
||||
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
|
||||
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DataSourceConfigServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(DataSourceConfigServiceImpl.class)
|
||||
public class DataSourceConfigServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private DataSourceConfigServiceImpl dataSourceConfigService;
|
||||
|
||||
@Resource
|
||||
private DataSourceConfigMapper dataSourceConfigMapper;
|
||||
|
||||
@MockitoBean
|
||||
private AES aes;
|
||||
|
||||
@MockitoBean
|
||||
private DynamicDataSourceProperties dynamicDataSourceProperties;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// mock 一个空实现的 StringEncryptor,避免 EncryptTypeHandler 报错
|
||||
ReflectUtil.setFieldValue(EncryptTypeHandler.class, "aes", aes);
|
||||
when(aes.encryptBase64(anyString())).then((Answer<String>) invocation -> invocation.getArgument(0));
|
||||
when(aes.decryptStr(anyString())).then((Answer<String>) invocation -> invocation.getArgument(0));
|
||||
|
||||
// mock DynamicDataSourceProperties
|
||||
when(dynamicDataSourceProperties.getPrimary()).thenReturn("primary");
|
||||
DataSourceProperty dataSourceProperty = new DataSourceProperty();
|
||||
dataSourceProperty.setUrl("http://localhost:3306");
|
||||
dataSourceProperty.setUsername("yunai");
|
||||
dataSourceProperty.setPassword("tudou");
|
||||
when(dynamicDataSourceProperties.getDatasource()).thenReturn(MapUtil.of("primary", dataSourceProperty));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateDataSourceConfig_success() {
|
||||
try (MockedStatic<JdbcUtils> databaseUtilsMock = mockStatic(JdbcUtils.class)) {
|
||||
// 准备参数
|
||||
DataSourceConfigSaveReqVO reqVO = randomPojo(DataSourceConfigSaveReqVO.class)
|
||||
.setId(null); // 避免 id 被设置
|
||||
// mock 方法
|
||||
databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()),
|
||||
eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true);
|
||||
|
||||
// 调用
|
||||
Long dataSourceConfigId = dataSourceConfigService.createDataSourceConfig(reqVO);
|
||||
// 断言
|
||||
assertNotNull(dataSourceConfigId);
|
||||
// 校验记录的属性是否正确
|
||||
DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(dataSourceConfigId);
|
||||
assertPojoEquals(reqVO, dataSourceConfig, "id");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateDataSourceConfig_success() {
|
||||
try (MockedStatic<JdbcUtils> databaseUtilsMock = mockStatic(JdbcUtils.class)) {
|
||||
// mock 数据
|
||||
DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
|
||||
dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
DataSourceConfigSaveReqVO reqVO = randomPojo(DataSourceConfigSaveReqVO.class, o -> {
|
||||
o.setId(dbDataSourceConfig.getId()); // 设置更新的 ID
|
||||
});
|
||||
// mock 方法
|
||||
databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()),
|
||||
eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true);
|
||||
|
||||
// 调用
|
||||
dataSourceConfigService.updateDataSourceConfig(reqVO);
|
||||
// 校验是否更新正确
|
||||
DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(reqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(reqVO, dataSourceConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateDataSourceConfig_notExists() {
|
||||
// 准备参数
|
||||
DataSourceConfigSaveReqVO reqVO = randomPojo(DataSourceConfigSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> dataSourceConfigService.updateDataSourceConfig(reqVO), DATA_SOURCE_CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteDataSourceConfig_success() {
|
||||
// mock 数据
|
||||
DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
|
||||
dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbDataSourceConfig.getId();
|
||||
|
||||
// 调用
|
||||
dataSourceConfigService.deleteDataSourceConfig(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(dataSourceConfigMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteDataSourceConfig_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> dataSourceConfigService.deleteDataSourceConfig(id), DATA_SOURCE_CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test // 测试使用 password 查询,可以查询到数据
|
||||
public void testSelectPassword() {
|
||||
// mock 数据
|
||||
DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
|
||||
dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据
|
||||
|
||||
// 调用
|
||||
DataSourceConfigDO result = dataSourceConfigMapper.selectOne(DataSourceConfigDO::getPassword,
|
||||
EncryptTypeHandler.encrypt(dbDataSourceConfig.getPassword()));
|
||||
assertPojoEquals(dbDataSourceConfig, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataSourceConfig_master() {
|
||||
// 准备参数
|
||||
Long id = 0L;
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id);
|
||||
// 断言
|
||||
assertEquals(id, dataSourceConfig.getId());
|
||||
assertEquals("primary", dataSourceConfig.getName());
|
||||
assertEquals("http://localhost:3306", dataSourceConfig.getUrl());
|
||||
assertEquals("yunai", dataSourceConfig.getUsername());
|
||||
assertEquals("tudou", dataSourceConfig.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataSourceConfig_normal() {
|
||||
// mock 数据
|
||||
DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
|
||||
dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbDataSourceConfig.getId();
|
||||
|
||||
// 调用
|
||||
DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id);
|
||||
// 断言
|
||||
assertPojoEquals(dbDataSourceConfig, dataSourceConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataSourceConfigList() {
|
||||
// mock 数据
|
||||
DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
|
||||
dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
|
||||
// 调用
|
||||
List<DataSourceConfigDO> dataSourceConfigList = dataSourceConfigService.getDataSourceConfigList();
|
||||
// 断言
|
||||
assertEquals(2, dataSourceConfigList.size());
|
||||
// master
|
||||
assertEquals(0L, dataSourceConfigList.get(0).getId());
|
||||
assertEquals("primary", dataSourceConfigList.get(0).getName());
|
||||
assertEquals("http://localhost:3306", dataSourceConfigList.get(0).getUrl());
|
||||
assertEquals("yunai", dataSourceConfigList.get(0).getUsername());
|
||||
assertEquals("tudou", dataSourceConfigList.get(0).getPassword());
|
||||
// normal
|
||||
assertPojoEquals(dbDataSourceConfig, dataSourceConfigList.get(1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.iocoder.yudao.module.infra.service.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Import(DatabaseTableServiceImpl.class)
|
||||
public class DatabaseTableServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private DatabaseTableServiceImpl databaseTableService;
|
||||
|
||||
@MockitoBean
|
||||
private DataSourceConfigService dataSourceConfigService;
|
||||
|
||||
@Test
|
||||
public void testGetTableList() {
|
||||
// 准备参数
|
||||
Long dataSourceConfigId = randomLongId();
|
||||
// mock 方法
|
||||
DataSourceConfigDO dataSourceConfig = new DataSourceConfigDO().setUsername("sa").setPassword("")
|
||||
.setUrl("jdbc:h2:mem:testdb");
|
||||
when(dataSourceConfigService.getDataSourceConfig(eq(dataSourceConfigId)))
|
||||
.thenReturn(dataSourceConfig);
|
||||
|
||||
// 调用
|
||||
List<TableInfo> tables = databaseTableService.getTableList(dataSourceConfigId,
|
||||
"config", "参数");
|
||||
// 断言
|
||||
assertEquals(1, tables.size());
|
||||
assertTableInfo(tables.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTable() {
|
||||
// 准备参数
|
||||
Long dataSourceConfigId = randomLongId();
|
||||
// mock 方法
|
||||
DataSourceConfigDO dataSourceConfig = new DataSourceConfigDO().setUsername("sa").setPassword("")
|
||||
.setUrl("jdbc:h2:mem:testdb");
|
||||
when(dataSourceConfigService.getDataSourceConfig(eq(dataSourceConfigId)))
|
||||
.thenReturn(dataSourceConfig);
|
||||
|
||||
// 调用
|
||||
TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, "infra_config");
|
||||
// 断言
|
||||
assertTableInfo(tableInfo);
|
||||
}
|
||||
|
||||
private void assertTableInfo(TableInfo tableInfo) {
|
||||
assertEquals("infra_config", tableInfo.getName());
|
||||
assertEquals("参数配置表", tableInfo.getComment());
|
||||
assertEquals(13, tableInfo.getFields().size());
|
||||
// id 字段
|
||||
TableField idField = tableInfo.getFields().get(0);
|
||||
assertEquals("id", idField.getName());
|
||||
assertEquals(JdbcType.BIGINT, idField.getMetaInfo().getJdbcType());
|
||||
assertEquals("编号", idField.getComment());
|
||||
assertFalse(idField.getMetaInfo().isNullable());
|
||||
assertTrue(idField.isKeyFlag());
|
||||
assertTrue(idField.isKeyIdentityFlag());
|
||||
assertEquals(DbColumnType.LONG, idField.getColumnType());
|
||||
assertEquals("id", idField.getPropertyName());
|
||||
// name 字段
|
||||
TableField nameField = tableInfo.getFields().get(3);
|
||||
assertEquals("name", nameField.getName());
|
||||
assertEquals(JdbcType.VARCHAR, nameField.getMetaInfo().getJdbcType());
|
||||
assertEquals("名字", nameField.getComment());
|
||||
assertFalse(nameField.getMetaInfo().isNullable());
|
||||
assertFalse(nameField.isKeyFlag());
|
||||
assertFalse(nameField.isKeyIdentityFlag());
|
||||
assertEquals(DbColumnType.STRING, nameField.getColumnType());
|
||||
assertEquals("name", nameField.getPropertyName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package cn.iocoder.yudao.module.infra.service.file;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.config.FileConfigSaveReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileConfigDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientConfig;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClientFactory;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.enums.FileStorageEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.Data;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.randomEle;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link FileConfigServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(FileConfigServiceImpl.class)
|
||||
public class FileConfigServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private FileConfigServiceImpl fileConfigService;
|
||||
|
||||
@Resource
|
||||
private FileConfigMapper fileConfigMapper;
|
||||
|
||||
@MockitoBean
|
||||
private Validator validator;
|
||||
@MockitoBean
|
||||
private FileClientFactory fileClientFactory;
|
||||
|
||||
@Test
|
||||
public void testCreateFileConfig_success() {
|
||||
// 准备参数
|
||||
Map<String, Object> config = MapUtil.<String, Object>builder().put("basePath", "/yunai")
|
||||
.put("domain", "https://www.iocoder.cn").build();
|
||||
FileConfigSaveReqVO reqVO = randomPojo(FileConfigSaveReqVO.class,
|
||||
o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()).setConfig(config))
|
||||
.setId(null); // 避免 id 被赋值
|
||||
|
||||
// 调用
|
||||
Long fileConfigId = fileConfigService.createFileConfig(reqVO);
|
||||
// 断言
|
||||
assertNotNull(fileConfigId);
|
||||
// 校验记录的属性是否正确
|
||||
FileConfigDO fileConfig = fileConfigMapper.selectById(fileConfigId);
|
||||
assertPojoEquals(reqVO, fileConfig, "id", "config");
|
||||
assertFalse(fileConfig.getMaster());
|
||||
assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
|
||||
assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
|
||||
// 验证 cache
|
||||
assertNull(fileConfigService.getClientCache().getIfPresent(fileConfigId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateFileConfig_success() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomPojo(FileConfigDO.class, o -> o.setStorage(FileStorageEnum.LOCAL.getStorage())
|
||||
.setConfig(new LocalFileClientConfig().setBasePath("/yunai").setDomain("https://www.iocoder.cn")));
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
FileConfigSaveReqVO reqVO = randomPojo(FileConfigSaveReqVO.class, o -> {
|
||||
o.setId(dbFileConfig.getId()); // 设置更新的 ID
|
||||
o.setStorage(FileStorageEnum.LOCAL.getStorage());
|
||||
Map<String, Object> config = MapUtil.<String, Object>builder().put("basePath", "/yunai2")
|
||||
.put("domain", "https://doc.iocoder.cn").build();
|
||||
o.setConfig(config);
|
||||
});
|
||||
|
||||
// 调用
|
||||
fileConfigService.updateFileConfig(reqVO);
|
||||
// 校验是否更新正确
|
||||
FileConfigDO fileConfig = fileConfigMapper.selectById(reqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(reqVO, fileConfig, "config");
|
||||
assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath());
|
||||
assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain());
|
||||
// 验证 cache
|
||||
assertNull(fileConfigService.getClientCache().getIfPresent(fileConfig.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateFileConfig_notExists() {
|
||||
// 准备参数
|
||||
FileConfigSaveReqVO reqVO = randomPojo(FileConfigSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> fileConfigService.updateFileConfig(reqVO), FILE_CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateFileConfigMaster_success() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
FileConfigDO masterFileConfig = randomFileConfigDO().setMaster(true);
|
||||
fileConfigMapper.insert(masterFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
|
||||
// 调用
|
||||
fileConfigService.updateFileConfigMaster(dbFileConfig.getId());
|
||||
// 断言数据
|
||||
assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster());
|
||||
assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster());
|
||||
// 验证 cache
|
||||
assertNull(fileConfigService.getClientCache().getIfPresent(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateFileConfigMaster_notExists() {
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> fileConfigService.updateFileConfigMaster(randomLongId()), FILE_CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFileConfig_success() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbFileConfig.getId();
|
||||
|
||||
// 调用
|
||||
fileConfigService.deleteFileConfig(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(fileConfigMapper.selectById(id));
|
||||
// 验证 cache
|
||||
assertNull(fileConfigService.getClientCache().getIfPresent(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFileConfig_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFileConfig_master() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(true);
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbFileConfig.getId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_DELETE_FAIL_MASTER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileConfigPage() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setName("芋道源码")
|
||||
.setStorage(FileStorageEnum.LOCAL.getStorage());
|
||||
dbFileConfig.setCreateTime(LocalDateTimeUtil.parse("2020-01-23", DatePattern.NORM_DATE_PATTERN));// 等会查询到
|
||||
fileConfigMapper.insert(dbFileConfig);
|
||||
// 测试 name 不匹配
|
||||
fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setName("源码")));
|
||||
// 测试 storage 不匹配
|
||||
fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setStorage(FileStorageEnum.DB.getStorage())));
|
||||
// 测试 createTime 不匹配
|
||||
fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setCreateTime(LocalDateTimeUtil.parse("2020-11-23", DatePattern.NORM_DATE_PATTERN))));
|
||||
// 准备参数
|
||||
FileConfigPageReqVO reqVO = new FileConfigPageReqVO();
|
||||
reqVO.setName("芋道");
|
||||
reqVO.setStorage(FileStorageEnum.LOCAL.getStorage());
|
||||
reqVO.setCreateTime((new LocalDateTime[]{buildTime(2020, 1, 1),
|
||||
buildTime(2020, 1, 24)}));
|
||||
|
||||
// 调用
|
||||
PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbFileConfig, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileConfig() throws Exception {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbFileConfig.getId();
|
||||
// mock 获得 Client
|
||||
FileClient fileClient = mock(FileClient.class);
|
||||
when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient);
|
||||
when(fileClient.upload(any(), any(), any())).thenReturn("https://www.iocoder.cn");
|
||||
|
||||
// 调用,并断言
|
||||
assertEquals("https://www.iocoder.cn", fileConfigService.testFileConfig(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileConfig() {
|
||||
// mock 数据
|
||||
FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false);
|
||||
fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbFileConfig.getId();
|
||||
|
||||
// 调用,并断言
|
||||
assertPojoEquals(dbFileConfig, fileConfigService.getFileConfig(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileClient() {
|
||||
// mock 数据
|
||||
FileConfigDO fileConfig = randomFileConfigDO().setMaster(false);
|
||||
fileConfigMapper.insert(fileConfig);
|
||||
// 准备参数
|
||||
Long id = fileConfig.getId();
|
||||
// mock 获得 Client
|
||||
FileClient fileClient = new LocalFileClient(id, new LocalFileClientConfig());
|
||||
when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient);
|
||||
|
||||
// 调用,并断言
|
||||
assertSame(fileClient, fileConfigService.getFileClient(id));
|
||||
// 断言缓存
|
||||
verify(fileClientFactory).createOrUpdateFileClient(eq(id), eq(fileConfig.getStorage()),
|
||||
eq(fileConfig.getConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMasterFileClient() {
|
||||
// mock 数据
|
||||
FileConfigDO fileConfig = randomFileConfigDO().setMaster(true);
|
||||
fileConfigMapper.insert(fileConfig);
|
||||
// 准备参数
|
||||
Long id = fileConfig.getId();
|
||||
// mock 获得 Client
|
||||
FileClient fileClient = new LocalFileClient(id, new LocalFileClientConfig());
|
||||
when(fileClientFactory.getFileClient(eq(fileConfig.getId()))).thenReturn(fileClient);
|
||||
|
||||
// 调用,并断言
|
||||
assertSame(fileClient, fileConfigService.getMasterFileClient());
|
||||
// 断言缓存
|
||||
verify(fileClientFactory).createOrUpdateFileClient(eq(fileConfig.getId()), eq(fileConfig.getStorage()),
|
||||
eq(fileConfig.getConfig()));
|
||||
}
|
||||
|
||||
private FileConfigDO randomFileConfigDO() {
|
||||
return randomPojo(FileConfigDO.class).setStorage(randomEle(FileStorageEnum.values()).getStorage())
|
||||
.setConfig(new EmptyFileClientConfig());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EmptyFileClientConfig implements FileClientConfig, Serializable {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package cn.iocoder.yudao.module.infra.service.file;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
|
||||
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@Import({FileServiceImpl.class})
|
||||
public class FileServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private FileServiceImpl fileService;
|
||||
|
||||
@Resource
|
||||
private FileMapper fileMapper;
|
||||
|
||||
@MockitoBean
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilePage() {
|
||||
// mock 数据
|
||||
FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到
|
||||
o.setPath("yunai");
|
||||
o.setType("image/jpg");
|
||||
o.setCreateTime(buildTime(2021, 1, 15));
|
||||
});
|
||||
fileMapper.insert(dbFile);
|
||||
// 测试 path 不匹配
|
||||
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou")));
|
||||
// 测试 type 不匹配
|
||||
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
|
||||
o.setType("image/png");
|
||||
}));
|
||||
// 测试 createTime 不匹配
|
||||
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> {
|
||||
o.setCreateTime(buildTime(2020, 1, 15));
|
||||
}));
|
||||
// 准备参数
|
||||
FilePageReqVO reqVO = new FilePageReqVO();
|
||||
reqVO.setPath("yunai");
|
||||
reqVO.setType("jp");
|
||||
reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 1, 10), buildTime(2021, 1, 20)}));
|
||||
|
||||
// 调用
|
||||
PageResult<FileDO> pageResult = fileService.getFilePage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* content、name、directory、type 都非空
|
||||
*/
|
||||
@Test
|
||||
public void testCreateFile_success_01() throws Exception {
|
||||
// 准备参数
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String name = "单测文件名";
|
||||
String directory = randomString();
|
||||
String type = "image/jpeg";
|
||||
// mock Master 文件客户端
|
||||
FileClient client = mock(FileClient.class);
|
||||
when(fileConfigService.getMasterFileClient()).thenReturn(client);
|
||||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/" + name + ".jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
when(client.getId()).thenReturn(10L);
|
||||
// 调用
|
||||
String result = fileService.createFile(content, name, directory, type);
|
||||
// 断言
|
||||
assertEquals(result, url);
|
||||
// 校验数据
|
||||
FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
|
||||
assertEquals(10L, file.getConfigId());
|
||||
assertEquals(pathRef.get(), file.getPath());
|
||||
assertEquals(url, file.getUrl());
|
||||
assertEquals(type, file.getType());
|
||||
assertEquals(content.length, file.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* content 非空,其它都空
|
||||
*/
|
||||
@Test
|
||||
public void testCreateFile_success_02() throws Exception {
|
||||
// 准备参数
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
// mock Master 文件客户端
|
||||
String type = "image/jpeg";
|
||||
FileClient client = mock(FileClient.class);
|
||||
when(fileConfigService.getMasterFileClient()).thenReturn(client);
|
||||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches("\\d{8}/\\d+/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e\\.jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
when(client.getId()).thenReturn(10L);
|
||||
// 调用
|
||||
String result = fileService.createFile(content, null, null, null);
|
||||
// 断言
|
||||
assertEquals(result, url);
|
||||
// 校验数据
|
||||
FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
|
||||
assertEquals(10L, file.getConfigId());
|
||||
assertEquals(pathRef.get(), file.getPath());
|
||||
assertEquals(url, file.getUrl());
|
||||
assertEquals(type, file.getType());
|
||||
assertEquals(content.length, file.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFile_success() throws Exception {
|
||||
// mock 数据
|
||||
FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg"));
|
||||
fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据
|
||||
// mock Master 文件客户端
|
||||
FileClient client = mock(FileClient.class);
|
||||
when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
|
||||
// 准备参数
|
||||
Long id = dbFile.getId();
|
||||
|
||||
// 调用
|
||||
fileService.deleteFile(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(fileMapper.selectById(id));
|
||||
// 校验调用
|
||||
verify(client).delete(eq("tudou.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFile_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteFile_pathInvalid() {
|
||||
// mock 数据
|
||||
FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("../tudou.jpg"));
|
||||
fileMapper.insert(dbFile);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.deleteFile(dbFile.getId()), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileContent() throws Exception {
|
||||
// 准备参数
|
||||
Long configId = 10L;
|
||||
String path = "tudou.jpg";
|
||||
// mock 方法
|
||||
FileClient client = mock(FileClient.class);
|
||||
when(fileConfigService.getFileClient(eq(10L))).thenReturn(client);
|
||||
byte[] content = new byte[]{};
|
||||
when(client.getContent(eq("tudou.jpg"))).thenReturn(content);
|
||||
|
||||
// 调用
|
||||
byte[] result = fileService.getFileContent(configId, path);
|
||||
// 断言
|
||||
assertSame(result, content);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileContent_pathInvalid() {
|
||||
// 准备参数
|
||||
Long configId = 10L;
|
||||
String path = "../tudou.jpg";
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.getFileContent(configId, path), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileByConfigIdAndPath() {
|
||||
// mock 数据
|
||||
FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("avatar/中文 100%+文件.jpg"));
|
||||
fileMapper.insert(dbFile);
|
||||
FileDO latestFile = ObjectUtils.cloneIgnoreId(dbFile, o -> o.setName("最新文件名.jpg"));
|
||||
fileMapper.insert(latestFile);
|
||||
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("avatar/other.jpg")));
|
||||
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setConfigId(20L)));
|
||||
|
||||
// 调用
|
||||
FileDO result = fileService.getFileByConfigIdAndPath(10L, "avatar/中文 100%+文件.jpg");
|
||||
|
||||
// 断言
|
||||
AssertUtils.assertPojoEquals(latestFile, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateFileByPresignedPath_success() {
|
||||
// 准备参数
|
||||
FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
|
||||
o.setPath("avatar/test.jpg");
|
||||
o.setName("test.jpg");
|
||||
o.setUrl("https://www.iocoder.cn/test.jpg?token=123");
|
||||
});
|
||||
|
||||
// 调用
|
||||
Long fileId = fileService.createFile(reqVO);
|
||||
|
||||
// 断言
|
||||
FileDO file = fileMapper.selectById(fileId);
|
||||
assertEquals("avatar/test.jpg", file.getPath());
|
||||
assertEquals("test.jpg", file.getName());
|
||||
assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateFileByPresignedPath_nameInvalid() {
|
||||
// 准备参数
|
||||
FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
|
||||
o.setPath("avatar/test.jpg");
|
||||
o.setName("../test.jpg");
|
||||
});
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateFileByPresignedPath_pathInvalid() {
|
||||
// 准备参数
|
||||
FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
|
||||
o.setPath("../test.jpg");
|
||||
o.setName("test.jpg");
|
||||
});
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_AllEnabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
// 包含日期格式:8 位数字,如 20240517
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_PrefixEnabled_SuffixDisabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
// 包含日期格式:8 位数字,如 20240517
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_PrefixDisabled_SuffixEnabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_AllDisabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test.jpg
|
||||
assertEquals(directory + "/" + name, path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_NoExtension() {
|
||||
// 准备参数
|
||||
String name = "test";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_DirectoryNull() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = null;
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_AllEnabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_PrefixDisabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_NoExtension() {
|
||||
// 准备参数
|
||||
String name = "test";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_FileNameInvalid() {
|
||||
// 准备参数
|
||||
String name = "../test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_DirectoryInvalid() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "../avatar";
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_DirectoryEmpty() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cn.iocoder.yudao.module.infra.service.job;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.job.vo.log.JobLogPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobLogDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper;
|
||||
import cn.iocoder.yudao.module.infra.enums.job.JobLogStatusEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Import(JobLogServiceImpl.class)
|
||||
public class JobLogServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private JobLogServiceImpl jobLogService;
|
||||
@Resource
|
||||
private JobLogMapper jobLogMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateJobLog() {
|
||||
// 准备参数
|
||||
JobLogDO reqVO = randomPojo(JobLogDO.class, o -> o.setExecuteIndex(1));
|
||||
|
||||
// 调用
|
||||
Long id = jobLogService.createJobLog(reqVO.getJobId(), reqVO.getBeginTime(),
|
||||
reqVO.getHandlerName(), reqVO.getHandlerParam(), reqVO.getExecuteIndex());
|
||||
// 断言
|
||||
assertNotNull(id);
|
||||
// 校验记录的属性是否正确
|
||||
JobLogDO job = jobLogMapper.selectById(id);
|
||||
assertEquals(JobLogStatusEnum.RUNNING.getStatus(), job.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobLogResultAsync_success() {
|
||||
// mock 数据
|
||||
JobLogDO log = randomPojo(JobLogDO.class, o -> {
|
||||
o.setExecuteIndex(1);
|
||||
o.setStatus(JobLogStatusEnum.RUNNING.getStatus());
|
||||
});
|
||||
jobLogMapper.insert(log);
|
||||
// 准备参数
|
||||
Long logId = log.getId();
|
||||
LocalDateTime endTime = randomLocalDateTime();
|
||||
Integer duration = randomInteger();
|
||||
boolean success = true;
|
||||
String result = randomString();
|
||||
|
||||
// 调用
|
||||
jobLogService.updateJobLogResultAsync(logId, endTime, duration, success, result);
|
||||
// 校验记录的属性是否正确
|
||||
JobLogDO dbLog = jobLogMapper.selectById(log.getId());
|
||||
assertEquals(endTime, dbLog.getEndTime());
|
||||
assertEquals(duration, dbLog.getDuration());
|
||||
assertEquals(JobLogStatusEnum.SUCCESS.getStatus(), dbLog.getStatus());
|
||||
assertEquals(result, dbLog.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobLogResultAsync_failure() {
|
||||
// mock 数据
|
||||
JobLogDO log = randomPojo(JobLogDO.class, o -> {
|
||||
o.setExecuteIndex(1);
|
||||
o.setStatus(JobLogStatusEnum.RUNNING.getStatus());
|
||||
});
|
||||
jobLogMapper.insert(log);
|
||||
// 准备参数
|
||||
Long logId = log.getId();
|
||||
LocalDateTime endTime = randomLocalDateTime();
|
||||
Integer duration = randomInteger();
|
||||
boolean success = false;
|
||||
String result = randomString();
|
||||
|
||||
// 调用
|
||||
jobLogService.updateJobLogResultAsync(logId, endTime, duration, success, result);
|
||||
// 校验记录的属性是否正确
|
||||
JobLogDO dbLog = jobLogMapper.selectById(log.getId());
|
||||
assertEquals(endTime, dbLog.getEndTime());
|
||||
assertEquals(duration, dbLog.getDuration());
|
||||
assertEquals(JobLogStatusEnum.FAILURE.getStatus(), dbLog.getStatus());
|
||||
assertEquals(result, dbLog.getResult());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanJobLog() {
|
||||
// mock 数据
|
||||
JobLogDO log01 = randomPojo(JobLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-3))))
|
||||
.setExecuteIndex(1);
|
||||
jobLogMapper.insert(log01);
|
||||
JobLogDO log02 = randomPojo(JobLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-1))))
|
||||
.setExecuteIndex(1);
|
||||
jobLogMapper.insert(log02);
|
||||
// 准备参数
|
||||
Integer exceedDay = 2;
|
||||
Integer deleteLimit = 1;
|
||||
|
||||
// 调用
|
||||
Integer count = jobLogService.cleanJobLog(exceedDay, deleteLimit);
|
||||
// 断言
|
||||
assertEquals(1, count);
|
||||
List<JobLogDO> logs = jobLogMapper.selectList();
|
||||
assertEquals(1, logs.size());
|
||||
// TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
|
||||
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetJobLog() {
|
||||
// mock 数据
|
||||
JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> o.setExecuteIndex(1));
|
||||
jobLogMapper.insert(dbJobLog);
|
||||
// 准备参数
|
||||
Long id = dbJobLog.getId();
|
||||
|
||||
// 调用
|
||||
JobLogDO jobLog = jobLogService.getJobLog(id);
|
||||
// 断言
|
||||
assertPojoEquals(dbJobLog, jobLog);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetJobPage() {
|
||||
// mock 数据
|
||||
JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> {
|
||||
o.setExecuteIndex(1);
|
||||
o.setHandlerName("handlerName 单元测试");
|
||||
o.setStatus(JobLogStatusEnum.SUCCESS.getStatus());
|
||||
o.setBeginTime(buildTime(2021, 1, 8));
|
||||
o.setEndTime(buildTime(2021, 1, 8));
|
||||
});
|
||||
jobLogMapper.insert(dbJobLog);
|
||||
// 测试 jobId 不匹配
|
||||
jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setJobId(randomLongId())));
|
||||
// 测试 handlerName 不匹配
|
||||
jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString())));
|
||||
// 测试 beginTime 不匹配
|
||||
jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setBeginTime(buildTime(2021, 1, 7))));
|
||||
// 测试 endTime 不匹配
|
||||
jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setEndTime(buildTime(2021, 1, 9))));
|
||||
// 测试 status 不匹配
|
||||
jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setStatus(JobLogStatusEnum.FAILURE.getStatus())));
|
||||
// 准备参数
|
||||
JobLogPageReqVO reqVo = new JobLogPageReqVO();
|
||||
reqVo.setJobId(dbJobLog.getJobId());
|
||||
reqVo.setHandlerName("单元");
|
||||
reqVo.setBeginTime(dbJobLog.getBeginTime());
|
||||
reqVo.setEndTime(dbJobLog.getEndTime());
|
||||
reqVo.setStatus(JobLogStatusEnum.SUCCESS.getStatus());
|
||||
|
||||
// 调用
|
||||
PageResult<JobLogDO> pageResult = jobLogService.getJobLogPage(reqVo);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbJobLog, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package cn.iocoder.yudao.module.infra.service.job;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper;
|
||||
import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
|
||||
import cn.iocoder.yudao.module.infra.job.job.JobLogCleanJob;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@Import(JobServiceImpl.class)
|
||||
public class JobServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private JobServiceImpl jobService;
|
||||
@Resource
|
||||
private JobMapper jobMapper;
|
||||
@MockitoBean
|
||||
private SchedulerManager schedulerManager;
|
||||
|
||||
@MockitoBean
|
||||
private JobLogCleanJob jobLogCleanJob;
|
||||
|
||||
@Test
|
||||
public void testCreateJob_cronExpressionValid() {
|
||||
// 准备参数。Cron 表达式为 String 类型,默认随机字符串。
|
||||
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.createJob(reqVO), JOB_CRON_EXPRESSION_VALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateJob_jobHandlerExists() throws SchedulerException {
|
||||
// 准备参数 指定 Cron 表达式
|
||||
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"));
|
||||
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
|
||||
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
|
||||
.thenReturn(jobLogCleanJob);
|
||||
|
||||
// 调用
|
||||
jobService.createJob(reqVO);
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateJob_success() throws SchedulerException {
|
||||
// 准备参数 指定 Cron 表达式
|
||||
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"))
|
||||
.setId(null);
|
||||
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
|
||||
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
|
||||
.thenReturn(jobLogCleanJob);
|
||||
|
||||
// 调用
|
||||
Long jobId = jobService.createJob(reqVO);
|
||||
// 断言
|
||||
assertNotNull(jobId);
|
||||
// 校验记录的属性是否正确
|
||||
JobDO job = jobMapper.selectById(jobId);
|
||||
assertPojoEquals(reqVO, job, "id");
|
||||
assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus());
|
||||
// 校验调用
|
||||
verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()),
|
||||
eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJob_jobNotExists(){
|
||||
// 准备参数
|
||||
JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"));
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.updateJob(reqVO), JOB_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJob_onlyNormalStatus(){
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.INIT.getStatus()));
|
||||
jobMapper.insert(job);
|
||||
// 准备参数
|
||||
JobSaveReqVO updateReqVO = randomPojo(JobSaveReqVO.class, o -> {
|
||||
o.setId(job.getId());
|
||||
o.setCronExpression("0 0/1 * * * ? *");
|
||||
});
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.updateJob(updateReqVO),
|
||||
JOB_UPDATE_ONLY_NORMAL_STATUS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJob_success() throws SchedulerException {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()));
|
||||
jobMapper.insert(job);
|
||||
// 准备参数
|
||||
JobSaveReqVO updateReqVO = randomPojo(JobSaveReqVO.class, o -> {
|
||||
o.setId(job.getId());
|
||||
o.setCronExpression("0 0/1 * * * ? *");
|
||||
});
|
||||
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
|
||||
springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(updateReqVO.getHandlerName())))
|
||||
.thenReturn(jobLogCleanJob);
|
||||
|
||||
// 调用
|
||||
jobService.updateJob(updateReqVO);
|
||||
// 校验记录的属性是否正确
|
||||
JobDO updateJob = jobMapper.selectById(updateReqVO.getId());
|
||||
assertPojoEquals(updateReqVO, updateJob);
|
||||
// 校验调用
|
||||
verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()),
|
||||
eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobStatus_changeStatusInvalid() {
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.updateJobStatus(1L, JobStatusEnum.INIT.getStatus()),
|
||||
JOB_CHANGE_STATUS_INVALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobStatus_changeStatusEquals() {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()));
|
||||
jobMapper.insert(job);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> jobService.updateJobStatus(job.getId(), job.getStatus()),
|
||||
JOB_CHANGE_STATUS_EQUALS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobStatus_stopSuccess() throws SchedulerException {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()));
|
||||
jobMapper.insert(job);
|
||||
|
||||
// 调用
|
||||
jobService.updateJobStatus(job.getId(), JobStatusEnum.STOP.getStatus());
|
||||
// 校验记录的属性是否正确
|
||||
JobDO dbJob = jobMapper.selectById(job.getId());
|
||||
assertEquals(JobStatusEnum.STOP.getStatus(), dbJob.getStatus());
|
||||
// 校验调用
|
||||
verify(schedulerManager).pauseJob(eq(job.getHandlerName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateJobStatus_normalSuccess() throws SchedulerException {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.STOP.getStatus()));
|
||||
jobMapper.insert(job);
|
||||
|
||||
// 调用
|
||||
jobService.updateJobStatus(job.getId(), JobStatusEnum.NORMAL.getStatus());
|
||||
// 校验记录的属性是否正确
|
||||
JobDO dbJob = jobMapper.selectById(job.getId());
|
||||
assertEquals(JobStatusEnum.NORMAL.getStatus(), dbJob.getStatus());
|
||||
// 校验调用
|
||||
verify(schedulerManager).resumeJob(eq(job.getHandlerName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTriggerJob_success() throws SchedulerException {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class);
|
||||
jobMapper.insert(job);
|
||||
|
||||
// 调用
|
||||
jobService.triggerJob(job.getId());
|
||||
// 校验调用
|
||||
verify(schedulerManager).triggerJob(eq(job.getId()),
|
||||
eq(job.getHandlerName()), eq(job.getHandlerParam()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteJob_success() throws SchedulerException {
|
||||
// mock 数据
|
||||
JobDO job = randomPojo(JobDO.class);
|
||||
jobMapper.insert(job);
|
||||
|
||||
// 调用
|
||||
jobService.deleteJob(job.getId());
|
||||
// 校验不存在
|
||||
assertNull(jobMapper.selectById(job.getId()));
|
||||
// 校验调用
|
||||
verify(schedulerManager).deleteJob(eq(job.getHandlerName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetJobPage() {
|
||||
// mock 数据
|
||||
JobDO dbJob = randomPojo(JobDO.class, o -> {
|
||||
o.setName("定时任务测试");
|
||||
o.setHandlerName("handlerName 单元测试");
|
||||
o.setStatus(JobStatusEnum.INIT.getStatus());
|
||||
});
|
||||
jobMapper.insert(dbJob);
|
||||
// 测试 name 不匹配
|
||||
jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setName("土豆")));
|
||||
// 测试 status 不匹配
|
||||
jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus())));
|
||||
// 测试 handlerName 不匹配
|
||||
jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString())));
|
||||
// 准备参数
|
||||
JobPageReqVO reqVo = new JobPageReqVO();
|
||||
reqVo.setName("定时");
|
||||
reqVo.setStatus(JobStatusEnum.INIT.getStatus());
|
||||
reqVo.setHandlerName("单元");
|
||||
|
||||
// 调用
|
||||
PageResult<JobDO> pageResult = jobService.getJobPage(reqVo);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbJob, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetJob() {
|
||||
// mock 数据
|
||||
JobDO dbJob = randomPojo(JobDO.class);
|
||||
jobMapper.insert(dbJob);
|
||||
// 调用
|
||||
JobDO job = jobService.getJob(dbJob.getId());
|
||||
// 断言
|
||||
assertPojoEquals(dbJob, job);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package cn.iocoder.yudao.module.infra.service.logger;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiAccessLogCreateReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiAccessLogMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@Import(ApiAccessLogServiceImpl.class)
|
||||
public class ApiAccessLogServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogServiceImpl apiAccessLogService;
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogMapper apiAccessLogMapper;
|
||||
|
||||
@Test
|
||||
public void testGetApiAccessLogPage() {
|
||||
ApiAccessLogDO apiAccessLogDO = randomPojo(ApiAccessLogDO.class, o -> {
|
||||
o.setUserId(2233L);
|
||||
o.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||
o.setApplicationName("yudao-test");
|
||||
o.setRequestUrl("foo");
|
||||
o.setBeginTime(buildTime(2021, 3, 13));
|
||||
o.setDuration(1000);
|
||||
o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode());
|
||||
});
|
||||
apiAccessLogMapper.insert(apiAccessLogDO);
|
||||
// 测试 userId 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserId(3344L)));
|
||||
// 测试 userType 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserType(UserTypeEnum.MEMBER.getValue())));
|
||||
// 测试 applicationName 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setApplicationName("test")));
|
||||
// 测试 requestUrl 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setRequestUrl("bar")));
|
||||
// 测试 beginTime 不匹配:构造一个早期时间 2021-02-06 00:00:00
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setBeginTime(buildTime(2021, 2, 6))));
|
||||
// 测试 duration 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setDuration(100)));
|
||||
// 测试 resultCode 不匹配
|
||||
apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setResultCode(2)));
|
||||
// 准备参数
|
||||
ApiAccessLogPageReqVO reqVO = new ApiAccessLogPageReqVO();
|
||||
reqVO.setUserId(2233L);
|
||||
reqVO.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||
reqVO.setApplicationName("yudao-test");
|
||||
reqVO.setRequestUrl("foo");
|
||||
reqVO.setBeginTime(buildBetweenTime(2021, 3, 13, 2021, 3, 13));
|
||||
reqVO.setDuration(1000);
|
||||
reqVO.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode());
|
||||
|
||||
// 调用
|
||||
PageResult<ApiAccessLogDO> pageResult = apiAccessLogService.getApiAccessLogPage(reqVO);
|
||||
// 断言,只查到了一条符合条件的
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(apiAccessLogDO, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanJobLog() {
|
||||
// mock 数据
|
||||
ApiAccessLogDO log01 = randomPojo(ApiAccessLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-3))));
|
||||
apiAccessLogMapper.insert(log01);
|
||||
ApiAccessLogDO log02 = randomPojo(ApiAccessLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-1))));
|
||||
apiAccessLogMapper.insert(log02);
|
||||
// 准备参数
|
||||
Integer exceedDay = 2;
|
||||
Integer deleteLimit = 1;
|
||||
|
||||
// 调用
|
||||
Integer count = apiAccessLogService.cleanAccessLog(exceedDay, deleteLimit);
|
||||
// 断言
|
||||
assertEquals(1, count);
|
||||
List<ApiAccessLogDO> logs = apiAccessLogMapper.selectList();
|
||||
assertEquals(1, logs.size());
|
||||
// TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
|
||||
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateApiAccessLog() {
|
||||
// 准备参数
|
||||
ApiAccessLogCreateReqDTO createDTO = randomPojo(ApiAccessLogCreateReqDTO.class);
|
||||
|
||||
// 调用
|
||||
apiAccessLogService.createApiAccessLog(createDTO);
|
||||
// 断言
|
||||
ApiAccessLogDO apiAccessLogDO = apiAccessLogMapper.selectOne(null);
|
||||
assertPojoEquals(createDTO, apiAccessLogDO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package cn.iocoder.yudao.module.infra.service.logger;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiErrorLogDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper;
|
||||
import cn.iocoder.yudao.module.infra.enums.logger.ApiErrorLogProcessStatusEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.randomEle;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Import(ApiErrorLogServiceImpl.class)
|
||||
public class ApiErrorLogServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private ApiErrorLogServiceImpl apiErrorLogService;
|
||||
|
||||
@Resource
|
||||
private ApiErrorLogMapper apiErrorLogMapper;
|
||||
|
||||
@Test
|
||||
public void testGetApiErrorLogPage() {
|
||||
// mock 数据
|
||||
ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class, o -> {
|
||||
o.setUserId(2233L);
|
||||
o.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||
o.setApplicationName("yudao-test");
|
||||
o.setRequestUrl("foo");
|
||||
o.setExceptionTime(buildTime(2021, 3, 13));
|
||||
o.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
|
||||
});
|
||||
apiErrorLogMapper.insert(apiErrorLogDO);
|
||||
// 测试 userId 不匹配
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setUserId(3344L)));
|
||||
// 测试 userType 不匹配
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setUserType(UserTypeEnum.MEMBER.getValue())));
|
||||
// 测试 applicationName 不匹配
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setApplicationName("test")));
|
||||
// 测试 requestUrl 不匹配
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setRequestUrl("bar")));
|
||||
// 测试 exceptionTime 不匹配:构造一个早期时间 2021-02-06 00:00:00
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setExceptionTime(buildTime(2021, 2, 6))));
|
||||
// 测试 progressStatus 不匹配
|
||||
apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus())));
|
||||
// 准备参数
|
||||
ApiErrorLogPageReqVO reqVO = new ApiErrorLogPageReqVO();
|
||||
reqVO.setUserId(2233L);
|
||||
reqVO.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||
reqVO.setApplicationName("yudao-test");
|
||||
reqVO.setRequestUrl("foo");
|
||||
reqVO.setExceptionTime(buildBetweenTime(2021, 3, 1, 2021, 3, 31));
|
||||
reqVO.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
|
||||
|
||||
// 调用
|
||||
PageResult<ApiErrorLogDO> pageResult = apiErrorLogService.getApiErrorLogPage(reqVO);
|
||||
// 断言,只查到了一条符合条件的
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(apiErrorLogDO, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateApiErrorLog() {
|
||||
// 准备参数
|
||||
ApiErrorLogCreateReqDTO createDTO = randomPojo(ApiErrorLogCreateReqDTO.class);
|
||||
|
||||
// 调用
|
||||
apiErrorLogService.createApiErrorLog(createDTO);
|
||||
// 断言
|
||||
ApiErrorLogDO apiErrorLogDO = apiErrorLogMapper.selectOne(null);
|
||||
assertPojoEquals(createDTO, apiErrorLogDO);
|
||||
assertEquals(ApiErrorLogProcessStatusEnum.INIT.getStatus(), apiErrorLogDO.getProcessStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateApiErrorLogProcess_success() {
|
||||
// 准备参数
|
||||
ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class,
|
||||
o -> o.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()));
|
||||
apiErrorLogMapper.insert(apiErrorLogDO);
|
||||
// 准备参数
|
||||
Long id = apiErrorLogDO.getId();
|
||||
Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus();
|
||||
Long processUserId = randomLongId();
|
||||
|
||||
// 调用
|
||||
apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId);
|
||||
// 断言
|
||||
ApiErrorLogDO dbApiErrorLogDO = apiErrorLogMapper.selectById(apiErrorLogDO.getId());
|
||||
assertEquals(processStatus, dbApiErrorLogDO.getProcessStatus());
|
||||
assertEquals(processUserId, dbApiErrorLogDO.getProcessUserId());
|
||||
assertNotNull(dbApiErrorLogDO.getProcessTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateApiErrorLogProcess_processed() {
|
||||
// 准备参数
|
||||
ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class,
|
||||
o -> o.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()));
|
||||
apiErrorLogMapper.insert(apiErrorLogDO);
|
||||
// 准备参数
|
||||
Long id = apiErrorLogDO.getId();
|
||||
Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus();
|
||||
Long processUserId = randomLongId();
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() ->
|
||||
apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId),
|
||||
API_ERROR_LOG_PROCESSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateApiErrorLogProcess_notFound() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus();
|
||||
Long processUserId = randomLongId();
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() ->
|
||||
apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId),
|
||||
API_ERROR_LOG_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanJobLog() {
|
||||
// mock 数据
|
||||
ApiErrorLogDO log01 = randomPojo(ApiErrorLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-3))));
|
||||
apiErrorLogMapper.insert(log01);
|
||||
ApiErrorLogDO log02 = randomPojo(ApiErrorLogDO.class, o -> o.setCreateTime(addTime(Duration.ofDays(-1))));
|
||||
apiErrorLogMapper.insert(log02);
|
||||
// 准备参数
|
||||
Integer exceedDay = 2;
|
||||
Integer deleteLimit = 1;
|
||||
|
||||
// 调用
|
||||
Integer count = apiErrorLogService.cleanErrorLog(exceedDay, deleteLimit);
|
||||
// 断言
|
||||
assertEquals(1, count);
|
||||
List<ApiErrorLogDO> logs = apiErrorLogMapper.selectList();
|
||||
assertEquals(1, logs.size());
|
||||
// TODO @芋艿:createTime updateTime 被屏蔽,仅 win11 会复现,建议后续修复。
|
||||
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
spring:
|
||||
main:
|
||||
lazy-initialization: true # 开启懒加载,加快速度
|
||||
banner-mode: off # 单元测试,禁用 Banner
|
||||
|
||||
--- #################### 数据库相关配置 ####################
|
||||
|
||||
spring:
|
||||
# 数据源配置项
|
||||
datasource:
|
||||
name: ruoyi-vue-pro
|
||||
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
druid:
|
||||
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
|
||||
initial-size: 1 # 单元测试,配置为 1,提升启动速度
|
||||
sql:
|
||||
init:
|
||||
schema-locations: classpath:/sql/create_tables.sql
|
||||
encoding: UTF-8
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1 # 地址
|
||||
port: 16379 # 端口(单元测试,使用 16379 端口)
|
||||
database: 0 # 数据库索引
|
||||
|
||||
mybatis-plus:
|
||||
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
|
||||
type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
--- #################### 配置中心相关配置 ####################
|
||||
|
||||
--- #################### 服务保障相关配置 ####################
|
||||
|
||||
# Lock4j 配置项(单元测试,禁用 Lock4j)
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
info:
|
||||
base-package: cn.iocoder.yudao
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"table": {
|
||||
"id": 10,
|
||||
"scene" : 1,
|
||||
"parentMenuId" : 888,
|
||||
"tableName" : "infra_category",
|
||||
"tableComment" : "分类表",
|
||||
"moduleName" : "infra",
|
||||
"businessName" : "demo",
|
||||
"className" : "InfraCategory",
|
||||
"classComment" : "分类",
|
||||
"author" : "芋道源码",
|
||||
"treeParentColumnId" : 22,
|
||||
"treeNameColumnId" : 11
|
||||
},
|
||||
"columns": [ {
|
||||
"columnName" : "id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "编号",
|
||||
"primaryKey" : true,
|
||||
"javaType" : "Long",
|
||||
"javaField" : "id",
|
||||
"example" : "1024",
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"id" : 11,
|
||||
"columnName" : "name",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "名字",
|
||||
"javaType" : "String",
|
||||
"javaField" : "name",
|
||||
"example" : "芋头",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "LIKE",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "input"
|
||||
}, {
|
||||
"id" : 22,
|
||||
"columnName" : "description",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "父编号",
|
||||
"javaType" : "Long",
|
||||
"javaField" : "parentId",
|
||||
"example" : "2048",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
} ]
|
||||
}
|
||||
143
yudao-module-infra/src/test/resources/codegen/table/contact.json
Normal file
143
yudao-module-infra/src/test/resources/codegen/table/contact.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"table": {
|
||||
"scene" : 1,
|
||||
"tableName" : "infra_student_contact",
|
||||
"tableComment" : "学生联系人表",
|
||||
"moduleName" : "infra",
|
||||
"businessName" : "demo",
|
||||
"className" : "InfraStudentContact",
|
||||
"classComment" : "学生联系人",
|
||||
"author" : "芋道源码"
|
||||
},
|
||||
"columns": [ {
|
||||
"columnName" : "id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "编号",
|
||||
"primaryKey" : true,
|
||||
"javaType" : "Long",
|
||||
"javaField" : "id",
|
||||
"example" : "1024",
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"id" : 100,
|
||||
"columnName" : "student_id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "学生编号",
|
||||
"javaType" : "Long",
|
||||
"javaField" : "studentId",
|
||||
"example" : "2048",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"columnName" : "name",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "名字",
|
||||
"javaType" : "String",
|
||||
"javaField" : "name",
|
||||
"example" : "芋头",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "LIKE",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "input"
|
||||
}, {
|
||||
"columnName" : "description",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "简介",
|
||||
"javaType" : "String",
|
||||
"javaField" : "description",
|
||||
"example" : "我是介绍",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "textarea"
|
||||
}, {
|
||||
"columnName" : "birthday",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "出生日期",
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "birthday",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
}, {
|
||||
"columnName" : "sex",
|
||||
"dataType" : "INTEGER",
|
||||
"columnComment" : "性别",
|
||||
"javaType" : "Integer",
|
||||
"javaField" : "sex",
|
||||
"dictType" : "system_user_sex",
|
||||
"example" : "1",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "select"
|
||||
}, {
|
||||
"columnName" : "enabled",
|
||||
"dataType" : "BOOLEAN",
|
||||
"columnComment" : "是否有效",
|
||||
"javaType" : "Boolean",
|
||||
"javaField" : "enabled",
|
||||
"dictType" : "infra_boolean_string",
|
||||
"example" : "true",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "radio"
|
||||
}, {
|
||||
"columnName" : "avatar",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "头像",
|
||||
"javaType" : "String",
|
||||
"javaField" : "avatar",
|
||||
"example" : "https://www.iocoder.cn/1.png",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "imageUpload"
|
||||
}, {
|
||||
"columnName" : "video",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "附件",
|
||||
"nullable" : true,
|
||||
"javaType" : "String",
|
||||
"javaField" : "video",
|
||||
"example" : "https://www.iocoder.cn/1.mp4",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "fileUpload"
|
||||
}, {
|
||||
"columnName" : "memo",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "备注",
|
||||
"javaType" : "String",
|
||||
"javaField" : "memo",
|
||||
"example" : "我是备注",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "editor"
|
||||
}, {
|
||||
"columnName" : "create_time",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "创建时间",
|
||||
"nullable" : true,
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "createTime",
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "BETWEEN",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
} ]
|
||||
}
|
||||
134
yudao-module-infra/src/test/resources/codegen/table/student.json
Normal file
134
yudao-module-infra/src/test/resources/codegen/table/student.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"table": {
|
||||
"id": 1,
|
||||
"scene" : 1,
|
||||
"parentMenuId" : 888,
|
||||
"tableName" : "infra_student",
|
||||
"tableComment" : "学生表",
|
||||
"moduleName" : "infra",
|
||||
"businessName" : "demo",
|
||||
"className" : "InfraStudent",
|
||||
"classComment" : "学生",
|
||||
"author" : "芋道源码"
|
||||
},
|
||||
"columns": [ {
|
||||
"id" : 100,
|
||||
"columnName" : "id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "编号",
|
||||
"primaryKey" : true,
|
||||
"javaType" : "Long",
|
||||
"javaField" : "id",
|
||||
"example" : "1024",
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"columnName" : "name",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "名字",
|
||||
"javaType" : "String",
|
||||
"javaField" : "name",
|
||||
"example" : "芋头",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "LIKE",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "input"
|
||||
}, {
|
||||
"columnName" : "description",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "简介",
|
||||
"javaType" : "String",
|
||||
"javaField" : "description",
|
||||
"example" : "我是介绍",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "textarea"
|
||||
}, {
|
||||
"columnName" : "birthday",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "出生日期",
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "birthday",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
}, {
|
||||
"columnName" : "sex",
|
||||
"dataType" : "INTEGER",
|
||||
"columnComment" : "性别",
|
||||
"javaType" : "Integer",
|
||||
"javaField" : "sex",
|
||||
"dictType" : "system_user_sex",
|
||||
"example" : "1",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "select"
|
||||
}, {
|
||||
"columnName" : "enabled",
|
||||
"dataType" : "BOOLEAN",
|
||||
"columnComment" : "是否有效",
|
||||
"javaType" : "Boolean",
|
||||
"javaField" : "enabled",
|
||||
"dictType" : "infra_boolean_string",
|
||||
"example" : "true",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "radio"
|
||||
}, {
|
||||
"columnName" : "avatar",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "头像",
|
||||
"javaType" : "String",
|
||||
"javaField" : "avatar",
|
||||
"example" : "https://www.iocoder.cn/1.png",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "imageUpload"
|
||||
}, {
|
||||
"columnName" : "video",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "附件",
|
||||
"javaType" : "String",
|
||||
"javaField" : "video",
|
||||
"example" : "https://www.iocoder.cn/1.mp4",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "fileUpload"
|
||||
}, {
|
||||
"columnName" : "memo",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "备注",
|
||||
"javaType" : "String",
|
||||
"javaField" : "memo",
|
||||
"example" : "我是备注",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "editor"
|
||||
}, {
|
||||
"columnName" : "create_time",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "创建时间",
|
||||
"nullable" : true,
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "createTime",
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "BETWEEN",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
} ]
|
||||
}
|
||||
143
yudao-module-infra/src/test/resources/codegen/table/teacher.json
Normal file
143
yudao-module-infra/src/test/resources/codegen/table/teacher.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"table": {
|
||||
"scene" : 1,
|
||||
"tableName" : "infra_student_teacher",
|
||||
"tableComment" : "学生班主任表",
|
||||
"moduleName" : "infra",
|
||||
"businessName" : "demo",
|
||||
"className" : "InfraStudentTeacher",
|
||||
"classComment" : "学生班主任",
|
||||
"author" : "芋道源码"
|
||||
},
|
||||
"columns": [ {
|
||||
"columnName" : "id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "编号",
|
||||
"primaryKey" : true,
|
||||
"javaType" : "Long",
|
||||
"javaField" : "id",
|
||||
"example" : "1024",
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"id" : 200,
|
||||
"columnName" : "student_id",
|
||||
"dataType" : "BIGINT",
|
||||
"columnComment" : "学生编号",
|
||||
"javaType" : "Long",
|
||||
"javaField" : "studentId",
|
||||
"example" : "2048",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true
|
||||
}, {
|
||||
"columnName" : "name",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "名字",
|
||||
"javaType" : "String",
|
||||
"javaField" : "name",
|
||||
"example" : "芋头",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "LIKE",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "input"
|
||||
}, {
|
||||
"columnName" : "description",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "简介",
|
||||
"javaType" : "String",
|
||||
"javaField" : "description",
|
||||
"example" : "我是介绍",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "textarea"
|
||||
}, {
|
||||
"columnName" : "birthday",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "出生日期",
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "birthday",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
}, {
|
||||
"columnName" : "sex",
|
||||
"dataType" : "INTEGER",
|
||||
"columnComment" : "性别",
|
||||
"javaType" : "Integer",
|
||||
"javaField" : "sex",
|
||||
"dictType" : "system_user_sex",
|
||||
"example" : "1",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "select"
|
||||
}, {
|
||||
"columnName" : "enabled",
|
||||
"dataType" : "BOOLEAN",
|
||||
"columnComment" : "是否有效",
|
||||
"javaType" : "Boolean",
|
||||
"javaField" : "enabled",
|
||||
"dictType" : "infra_boolean_string",
|
||||
"example" : "true",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "=",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "radio"
|
||||
}, {
|
||||
"columnName" : "avatar",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "头像",
|
||||
"javaType" : "String",
|
||||
"javaField" : "avatar",
|
||||
"example" : "https://www.iocoder.cn/1.png",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "imageUpload"
|
||||
}, {
|
||||
"columnName" : "video",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "附件",
|
||||
"nullable" : true,
|
||||
"javaType" : "String",
|
||||
"javaField" : "video",
|
||||
"example" : "https://www.iocoder.cn/1.mp4",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "fileUpload"
|
||||
}, {
|
||||
"columnName" : "memo",
|
||||
"dataType" : "VARCHAR",
|
||||
"columnComment" : "备注",
|
||||
"javaType" : "String",
|
||||
"javaField" : "memo",
|
||||
"example" : "我是备注",
|
||||
"createOperation" : true,
|
||||
"updateOperation" : true,
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "editor"
|
||||
}, {
|
||||
"columnName" : "create_time",
|
||||
"dataType" : "DATE",
|
||||
"columnComment" : "创建时间",
|
||||
"nullable" : true,
|
||||
"javaType" : "LocalDateTime",
|
||||
"javaField" : "createTime",
|
||||
"listOperation" : true,
|
||||
"listOperationCondition" : "BETWEEN",
|
||||
"listOperationResult" : true,
|
||||
"htmlType" : "datetime"
|
||||
} ]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
@@ -0,0 +1,6 @@
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
|
||||
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
|
||||
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生联系人 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_contact")
|
||||
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentContactDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生联系人 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
|
||||
|
||||
default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
|
||||
.eq(InfraStudentContactDO::getStudentId, studentId)
|
||||
.orderByDesc(InfraStudentContactDO::getId));
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
|
||||
|
||||
@Tag(name = "管理后台 - 学生")
|
||||
@RestController
|
||||
@RequestMapping("/infra/student")
|
||||
@Validated
|
||||
public class InfraStudentController {
|
||||
|
||||
@Resource
|
||||
private InfraStudentService studentService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:create')")
|
||||
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
|
||||
return success(studentService.createStudent(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:update')")
|
||||
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
|
||||
studentService.updateStudent(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除学生")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
|
||||
studentService.deleteStudent(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
|
||||
InfraStudentDO student = studentService.getStudent(id);
|
||||
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得学生分页")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
|
||||
BeanUtils.toBean(list, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@GetMapping("/student-contact/page")
|
||||
@Operation(summary = "获得学生联系人分页")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
|
||||
@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentContactPage(pageReqVO, studentId));
|
||||
}
|
||||
|
||||
@PostMapping("/student-contact/create")
|
||||
@Operation(summary = "创建学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:create')")
|
||||
public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
|
||||
return success(studentService.createStudentContact(studentContact));
|
||||
}
|
||||
|
||||
@PutMapping("/student-contact/update")
|
||||
@Operation(summary = "更新学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:update')")
|
||||
public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
|
||||
studentService.updateStudentContact(studentContact);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-contact/delete")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@Operation(summary = "删除学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
|
||||
studentService.deleteStudentContact(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-contact/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentContactListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-contact/get")
|
||||
@Operation(summary = "获得学生联系人")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
|
||||
return success(studentService.getStudentContact(id));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@GetMapping("/student-teacher/page")
|
||||
@Operation(summary = "获得学生班主任分页")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
|
||||
@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
|
||||
}
|
||||
|
||||
@PostMapping("/student-teacher/create")
|
||||
@Operation(summary = "创建学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:create')")
|
||||
public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
|
||||
return success(studentService.createStudentTeacher(studentTeacher));
|
||||
}
|
||||
|
||||
@PutMapping("/student-teacher/update")
|
||||
@Operation(summary = "更新学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:update')")
|
||||
public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
|
||||
studentService.updateStudentTeacher(studentTeacher);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-teacher/delete")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@Operation(summary = "删除学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
|
||||
studentService.deleteStudentTeacher(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-teacher/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentTeacherListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-teacher/get")
|
||||
@Operation(summary = "获得学生班主任")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
|
||||
return success(studentService.getStudentTeacher(id));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student")
|
||||
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
|
||||
/**
|
||||
* 学生 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
|
||||
|
||||
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
|
||||
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
|
||||
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
|
||||
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
|
||||
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
|
||||
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(InfraStudentDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", example = "1")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", example = "true")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
||||
|
||||
@Schema(description = "管理后台 - 学生 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class InfraStudentRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@ExcelProperty("编号")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@ExcelProperty("名字")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@ExcelProperty("简介")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@ExcelProperty(value = "性别", converter = DictConvert.class)
|
||||
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
|
||||
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@ExcelProperty("头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@ExcelProperty("附件")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@ExcelProperty("备注")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
|
||||
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
|
||||
@Data
|
||||
public class InfraStudentSaveReqVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@NotEmpty(message = "名字不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@NotEmpty(message = "简介不能为空")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "出生日期不能为空")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "性别不能为空")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@NotNull(message = "是否有效不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@NotEmpty(message = "头像不能为空")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@NotEmpty(message = "附件不能为空")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@NotEmpty(message = "备注不能为空")
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
|
||||
/**
|
||||
* 学生 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface InfraStudentService {
|
||||
|
||||
/**
|
||||
* 创建学生
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新学生
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除学生
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 学生
|
||||
*/
|
||||
InfraStudentDO getStudent(Long id);
|
||||
|
||||
/**
|
||||
* 获得学生分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 学生分页
|
||||
*/
|
||||
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/**
|
||||
* 获得学生联系人分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @param studentId 学生编号
|
||||
* @return 学生联系人分页
|
||||
*/
|
||||
PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
|
||||
|
||||
/**
|
||||
* 创建学生联系人
|
||||
*
|
||||
* @param studentContact 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createStudentContact(@Valid InfraStudentContactDO studentContact);
|
||||
|
||||
/**
|
||||
* 更新学生联系人
|
||||
*
|
||||
* @param studentContact 更新信息
|
||||
*/
|
||||
void updateStudentContact(@Valid InfraStudentContactDO studentContact);
|
||||
|
||||
/**
|
||||
* 删除学生联系人
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteStudentContact(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生联系人
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentContactListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生联系人
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 学生联系人
|
||||
*/
|
||||
InfraStudentContactDO getStudentContact(Long id);
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/**
|
||||
* 获得学生班主任分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @param studentId 学生编号
|
||||
* @return 学生班主任分页
|
||||
*/
|
||||
PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
|
||||
|
||||
/**
|
||||
* 创建学生班主任
|
||||
*
|
||||
* @param studentTeacher 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
|
||||
|
||||
/**
|
||||
* 更新学生班主任
|
||||
*
|
||||
* @param studentTeacher 更新信息
|
||||
*/
|
||||
void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
|
||||
|
||||
/**
|
||||
* 删除学生班主任
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteStudentTeacher(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生班主任
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentTeacherListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生班主任
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 学生班主任
|
||||
*/
|
||||
InfraStudentTeacherDO getStudentTeacher(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 学生 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class InfraStudentServiceImpl implements InfraStudentService {
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
@Resource
|
||||
private InfraStudentContactMapper studentContactMapper;
|
||||
@Resource
|
||||
private InfraStudentTeacherMapper studentTeacherMapper;
|
||||
|
||||
@Override
|
||||
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
|
||||
// 插入
|
||||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
// 返回
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateStudentExists(updateReqVO.getId());
|
||||
// 更新
|
||||
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
|
||||
studentMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudent(Long id) {
|
||||
// 校验存在
|
||||
validateStudentExists(id);
|
||||
// 删除
|
||||
studentMapper.deleteById(id);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentId(id);
|
||||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentDO getStudent(Long id) {
|
||||
return studentMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
|
||||
return studentMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@Override
|
||||
public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
|
||||
return studentContactMapper.selectPage(pageReqVO, studentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createStudentContact(InfraStudentContactDO studentContact) {
|
||||
studentContact.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentContactMapper.insert(studentContact);
|
||||
return studentContact.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStudentContact(InfraStudentContactDO studentContact) {
|
||||
// 校验存在
|
||||
validateStudentContactExists(studentContact.getId());
|
||||
// 更新
|
||||
studentContact.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentContactMapper.updateById(studentContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContact(Long id) {
|
||||
// 删除
|
||||
studentContactMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContactListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentContactMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentContactDO getStudentContact(Long id) {
|
||||
return studentContactMapper.selectById(id);
|
||||
}
|
||||
|
||||
private void validateStudentContactExists(Long id) {
|
||||
if (studentContactMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_CONTACT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentId(Long studentId) {
|
||||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
|
||||
return studentTeacherMapper.selectPage(pageReqVO, studentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
|
||||
// 校验是否已经存在
|
||||
if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
|
||||
throw exception(STUDENT_TEACHER_EXISTS);
|
||||
}
|
||||
// 插入
|
||||
studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentTeacherMapper.insert(studentTeacher);
|
||||
return studentTeacher.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
|
||||
// 校验存在
|
||||
validateStudentTeacherExists(studentTeacher.getId());
|
||||
// 更新
|
||||
studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.updateById(studentTeacher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacher(Long id) {
|
||||
// 删除
|
||||
studentTeacherMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacherListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentTeacherMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentTeacherDO getStudentTeacher(Long id) {
|
||||
return studentTeacherMapper.selectById(id);
|
||||
}
|
||||
|
||||
private void validateStudentTeacherExists(Long id) {
|
||||
if (studentTeacherMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_TEACHER_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentId(Long studentId) {
|
||||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link InfraStudentServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(InfraStudentServiceImpl.class)
|
||||
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private InfraStudentServiceImpl studentService;
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateStudent_success() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
|
||||
|
||||
// 调用
|
||||
Long studentId = studentService.createStudent(createReqVO);
|
||||
// 断言
|
||||
assertNotNull(studentId);
|
||||
// 校验记录的属性是否正确
|
||||
InfraStudentDO student = studentMapper.selectById(studentId);
|
||||
assertPojoEquals(createReqVO, student, "id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
|
||||
o.setId(dbStudent.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
// 调用
|
||||
studentService.updateStudent(updateReqVO);
|
||||
// 校验是否更新正确
|
||||
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(updateReqVO, student);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_notExists() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbStudent.getId();
|
||||
|
||||
// 调用
|
||||
studentService.deleteStudent(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(studentMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
|
||||
public void testGetStudentPage() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
|
||||
o.setName(null);
|
||||
o.setBirthday(null);
|
||||
o.setSex(null);
|
||||
o.setEnabled(null);
|
||||
o.setCreateTime(null);
|
||||
});
|
||||
studentMapper.insert(dbStudent);
|
||||
// 测试 name 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
|
||||
// 测试 birthday 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
|
||||
// 测试 sex 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
|
||||
// 测试 enabled 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
|
||||
// 测试 createTime 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
|
||||
// 准备参数
|
||||
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
|
||||
reqVO.setName(null);
|
||||
reqVO.setBirthday(null);
|
||||
reqVO.setSex(null);
|
||||
reqVO.setEnabled(null);
|
||||
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
|
||||
|
||||
// 调用
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbStudent, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生班主任 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_teacher")
|
||||
@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentTeacherDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生班主任 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
|
||||
|
||||
default PageResult<InfraStudentTeacherDO> selectPage(PageParam reqVO, Long studentId) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentTeacherDO>()
|
||||
.eq(InfraStudentTeacherDO::getStudentId, studentId)
|
||||
.orderByDesc(InfraStudentTeacherDO::getId));
|
||||
}
|
||||
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
|
||||
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
|
||||
CREATE TABLE IF NOT EXISTS "infra_student" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"name" varchar NOT NULL,
|
||||
"description" varchar NOT NULL,
|
||||
"birthday" varchar NOT NULL,
|
||||
"sex" int NOT NULL,
|
||||
"enabled" bit NOT NULL,
|
||||
"avatar" varchar NOT NULL,
|
||||
"video" varchar NOT NULL,
|
||||
"memo" varchar NOT NULL,
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT '学生表';
|
||||
|
||||
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
|
||||
DELETE FROM "infra_student";
|
||||
@@ -0,0 +1,54 @@
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人分页 */
|
||||
export function getStudentContactPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentContact>>(`/infra/student/student-contact/page`, { params });
|
||||
}
|
||||
/** 新增学生联系人 */
|
||||
export function createStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.post(`/infra/student/student-contact/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生联系人 */
|
||||
export function updateStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.put(`/infra/student/student-contact/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
export function deleteStudentContact(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
export function deleteStudentContactList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生联系人 */
|
||||
export function getStudentContact(id: number) {
|
||||
return requestClient.get<StudentApi.StudentContact>(`/infra/student/student-contact/get?id=${id}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任分页 */
|
||||
export function getStudentTeacherPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentTeacher>>(`/infra/student/student-teacher/page`, { params });
|
||||
}
|
||||
/** 新增学生班主任 */
|
||||
export function createStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.post(`/infra/student/student-teacher/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生班主任 */
|
||||
export function updateStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.put(`/infra/student/student-teacher/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
export function deleteStudentTeacher(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
export function deleteStudentTeacherList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacher(id: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,358 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { ref, h, reactive, onMounted, nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
|
||||
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'ant-design-vue';
|
||||
import StudentForm from './modules/form.vue';
|
||||
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
|
||||
import StudentContactList from './modules/student-contact-list.vue'
|
||||
import StudentTeacherList from './modules/student-teacher-list.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const selectStudent = ref<StudentApi.Student>();
|
||||
async function onCellClick({ row }: { row: StudentApi.Student }) {
|
||||
selectStudent.value = row
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.Student[]>([]) // 列表的数据
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
const data = await getStudentPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: StudentApi.Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: StudentApi.Student) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudent(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
const data = await exportStudent(queryParams);
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
v-access:code="['infra:student:export']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
@cell-click="onCellClick"
|
||||
:row-config="{
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
isCurrent: true,
|
||||
}"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentContact, createStudentContact, updateStudentContact } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生联系人'])
|
||||
: $t('ui.actionTitle.create', ['学生联系人']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentContact>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentContact;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentContact(data) : createStudentContact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentContact>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentContact(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,335 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentContact, deleteStudentContactList, getStudentContactPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentContactForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生联系人 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生联系人 */
|
||||
function handleEdit(row: StudentApi.StudentContact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
async function handleDelete(row: StudentApi.StudentContact) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContact(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContactList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentContact[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentContactPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentTeacher, createStudentTeacher, updateStudentTeacher } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生班主任'])
|
||||
: $t('ui.actionTitle.create', ['学生班主任']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentTeacher>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentTeacher;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentTeacher(data) : createStudentTeacher(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentTeacher>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentTeacher(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,335 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentTeacher, deleteStudentTeacherList, getStudentTeacherPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentTeacherForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生班主任 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生班主任 */
|
||||
function handleEdit(row: StudentApi.StudentTeacher) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
async function handleDelete(row: StudentApi.StudentTeacher) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacher(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacherList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentTeacher[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentTeacher[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentTeacherPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
|
||||
|
||||
<!--
|
||||
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
|
||||
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
|
||||
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
|
||||
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
|
||||
-->
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,73 @@
|
||||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
@@ -0,0 +1,3 @@
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生联系人 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_contact")
|
||||
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentContactDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生联系人 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
|
||||
|
||||
default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
|
||||
return selectList(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
|
||||
|
||||
@Tag(name = "管理后台 - 学生")
|
||||
@RestController
|
||||
@RequestMapping("/infra/student")
|
||||
@Validated
|
||||
public class InfraStudentController {
|
||||
|
||||
@Resource
|
||||
private InfraStudentService studentService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:create')")
|
||||
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
|
||||
return success(studentService.createStudent(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:update')")
|
||||
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
|
||||
studentService.updateStudent(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除学生")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
|
||||
studentService.deleteStudent(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
|
||||
InfraStudentDO student = studentService.getStudent(id);
|
||||
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得学生分页")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
|
||||
BeanUtils.toBean(list, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@GetMapping("/student-contact/list-by-student-id")
|
||||
@Operation(summary = "获得学生联系人列表")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentContactListByStudentId(studentId));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@GetMapping("/student-teacher/get-by-student-id")
|
||||
@Operation(summary = "获得学生班主任")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentTeacherByStudentId(studentId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student")
|
||||
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
|
||||
/**
|
||||
* 学生 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
|
||||
|
||||
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
|
||||
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
|
||||
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
|
||||
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
|
||||
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
|
||||
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(InfraStudentDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", example = "1")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", example = "true")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
||||
|
||||
@Schema(description = "管理后台 - 学生 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class InfraStudentRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@ExcelProperty("编号")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@ExcelProperty("名字")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@ExcelProperty("简介")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@ExcelProperty(value = "性别", converter = DictConvert.class)
|
||||
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
|
||||
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@ExcelProperty("头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@ExcelProperty("附件")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@ExcelProperty("备注")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
|
||||
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
|
||||
@Data
|
||||
public class InfraStudentSaveReqVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@NotEmpty(message = "名字不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@NotEmpty(message = "简介不能为空")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "出生日期不能为空")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "性别不能为空")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@NotNull(message = "是否有效不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@NotEmpty(message = "头像不能为空")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@NotEmpty(message = "附件不能为空")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@NotEmpty(message = "备注不能为空")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "学生联系人列表")
|
||||
private List<InfraStudentContactDO> studentContacts;
|
||||
|
||||
@Schema(description = "学生班主任")
|
||||
private InfraStudentTeacherDO studentTeacher;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
|
||||
/**
|
||||
* 学生 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface InfraStudentService {
|
||||
|
||||
/**
|
||||
* 创建学生
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新学生
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除学生
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 学生
|
||||
*/
|
||||
InfraStudentDO getStudent(Long id);
|
||||
|
||||
/**
|
||||
* 获得学生分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 学生分页
|
||||
*/
|
||||
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/**
|
||||
* 获得学生联系人列表
|
||||
*
|
||||
* @param studentId 学生编号
|
||||
* @return 学生联系人列表
|
||||
*/
|
||||
List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/**
|
||||
* 获得学生班主任
|
||||
*
|
||||
* @param studentId 学生编号
|
||||
* @return 学生班主任
|
||||
*/
|
||||
InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 学生 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class InfraStudentServiceImpl implements InfraStudentService {
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
@Resource
|
||||
private InfraStudentContactMapper studentContactMapper;
|
||||
@Resource
|
||||
private InfraStudentTeacherMapper studentTeacherMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
|
||||
// 插入
|
||||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
|
||||
// 插入子表
|
||||
createStudentContactList(student.getId(), createReqVO.getStudentContacts());
|
||||
createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
|
||||
// 返回
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateStudentExists(updateReqVO.getId());
|
||||
// 更新
|
||||
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
|
||||
studentMapper.updateById(updateObj);
|
||||
|
||||
// 更新子表
|
||||
updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
|
||||
updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudent(Long id) {
|
||||
// 校验存在
|
||||
validateStudentExists(id);
|
||||
// 删除
|
||||
studentMapper.deleteById(id);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentId(id);
|
||||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentDO getStudent(Long id) {
|
||||
return studentMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
|
||||
return studentMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@Override
|
||||
public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
|
||||
return studentContactMapper.selectListByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
studentContactMapper.insertBatch(list);
|
||||
}
|
||||
|
||||
private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
|
||||
List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
|
||||
boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
|
||||
if (same) {
|
||||
newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
|
||||
}
|
||||
return same;
|
||||
});
|
||||
|
||||
// 第二步,批量添加、修改、删除
|
||||
if (CollUtil.isNotEmpty(diffList.get(0))) {
|
||||
studentContactMapper.insertBatch(diffList.get(0));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(1))) {
|
||||
studentContactMapper.updateBatch(diffList.get(1));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(2))) {
|
||||
studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentId(Long studentId) {
|
||||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
|
||||
return studentTeacherMapper.selectByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
|
||||
if (studentTeacher == null) {
|
||||
return;
|
||||
}
|
||||
studentTeacher.setStudentId(studentId);
|
||||
studentTeacherMapper.insert(studentTeacher);
|
||||
}
|
||||
|
||||
private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
|
||||
if (studentTeacher == null) {
|
||||
return;
|
||||
}
|
||||
studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.insertOrUpdate(studentTeacher);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentId(Long studentId) {
|
||||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link InfraStudentServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(InfraStudentServiceImpl.class)
|
||||
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private InfraStudentServiceImpl studentService;
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateStudent_success() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
|
||||
|
||||
// 调用
|
||||
Long studentId = studentService.createStudent(createReqVO);
|
||||
// 断言
|
||||
assertNotNull(studentId);
|
||||
// 校验记录的属性是否正确
|
||||
InfraStudentDO student = studentMapper.selectById(studentId);
|
||||
assertPojoEquals(createReqVO, student, "id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
|
||||
o.setId(dbStudent.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
// 调用
|
||||
studentService.updateStudent(updateReqVO);
|
||||
// 校验是否更新正确
|
||||
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(updateReqVO, student);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_notExists() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbStudent.getId();
|
||||
|
||||
// 调用
|
||||
studentService.deleteStudent(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(studentMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
|
||||
public void testGetStudentPage() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
|
||||
o.setName(null);
|
||||
o.setBirthday(null);
|
||||
o.setSex(null);
|
||||
o.setEnabled(null);
|
||||
o.setCreateTime(null);
|
||||
});
|
||||
studentMapper.insert(dbStudent);
|
||||
// 测试 name 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
|
||||
// 测试 birthday 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
|
||||
// 测试 sex 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
|
||||
// 测试 enabled 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
|
||||
// 测试 createTime 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
|
||||
// 准备参数
|
||||
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
|
||||
reqVO.setName(null);
|
||||
reqVO.setBirthday(null);
|
||||
reqVO.setSex(null);
|
||||
reqVO.setEnabled(null);
|
||||
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
|
||||
|
||||
// 调用
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbStudent, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生班主任 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_teacher")
|
||||
@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentTeacherDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生班主任 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
|
||||
|
||||
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
|
||||
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
|
||||
CREATE TABLE IF NOT EXISTS "infra_student" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"name" varchar NOT NULL,
|
||||
"description" varchar NOT NULL,
|
||||
"birthday" varchar NOT NULL,
|
||||
"sex" int NOT NULL,
|
||||
"enabled" bit NOT NULL,
|
||||
"avatar" varchar NOT NULL,
|
||||
"video" varchar NOT NULL,
|
||||
"memo" varchar NOT NULL,
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT '学生表';
|
||||
|
||||
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
|
||||
DELETE FROM "infra_student";
|
||||
@@ -0,0 +1,54 @@
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
studentcontacts?: StudentContact[]
|
||||
studentteacher?: StudentTeacher
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人列表 */
|
||||
export function getStudentContactListByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacherByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
/** 子表的表单 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const studentContactFormRef = ref<InstanceType<typeof StudentContactForm>>()
|
||||
const studentTeacherFormRef = ref<InstanceType<typeof StudentTeacherForm>>()
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
// 校验子表单
|
||||
try {
|
||||
await studentTeacherFormRef.value?.validate()
|
||||
} catch (e) {
|
||||
subTabsName.value = 'studentTeacher'
|
||||
return
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
// 拼接子表的数据
|
||||
data.studentContacts = studentContactFormRef.value?.getData();
|
||||
data.studentTeacher = studentTeacherFormRef.value?.getValues();
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactForm ref="studentContactFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,351 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { ref, h, reactive, onMounted, nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
|
||||
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'ant-design-vue';
|
||||
import StudentForm from './modules/form.vue';
|
||||
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
|
||||
import StudentContactList from './modules/student-contact-list.vue'
|
||||
import StudentTeacherList from './modules/student-teacher-list.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('studentContact')
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.Student[]>([]) // 列表的数据
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
const data = await getStudentPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: StudentApi.Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: StudentApi.Student) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudent(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
const data = await exportStudent(queryParams);
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
v-access:code="['infra:student:export']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<!-- 子表的列表 -->
|
||||
<VxeColumn type="expand" width="60">
|
||||
<template #content="{ row }">
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName" class="mx-8">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactList :student-id="row?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherList :student-id="row?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { message, Tabs, Form, Input, Textarea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import { Plus } from "@vben/icons";
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { getStudentContactListByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
const tableRef = ref<VxeTableInstance>();
|
||||
|
||||
/** 添加学生联系人 */
|
||||
async function handleAdd() {
|
||||
await tableRef.value?.insertAt({} as StudentApi.StudentContact, -1);
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
async function handleDelete(row: StudentApi.StudentContact) {
|
||||
await tableRef.value?.remove(row);
|
||||
}
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): StudentApi.StudentContact[] => {
|
||||
const data = list.value as StudentApi.StudentContact[];
|
||||
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
|
||||
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
|
||||
return [
|
||||
...data.filter(
|
||||
(row) => !removeRecords.some((removed) => removed.id === row.id),
|
||||
),
|
||||
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
list.value = await getStudentContactListByStudentId(props.studentId!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
|
||||
<VxeColumn field="name" title="名字" align="center">
|
||||
<template #default="{ row }">
|
||||
<Input v-model:value="row.name" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="description" title="简介" align="center">
|
||||
<template #default="{ row }">
|
||||
<Textarea v-model:value="row.description" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{ row }">
|
||||
<DatePicker
|
||||
v-model:value="row.birthday"
|
||||
:showTime="true"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
valueFormat='x'
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{ row }">
|
||||
<Select v-model:value="row.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{ row }">
|
||||
<RadioGroup v-model:value="row.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center">
|
||||
<template #default="{ row }">
|
||||
<ImageUpload v-model:value="row.avatar" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="video" title="附件" align="center">
|
||||
<template #default="{ row }">
|
||||
<FileUpload v-model:value="row.video" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="memo" title="备注" align="center">
|
||||
<template #default="{ row }">
|
||||
<Textarea v-model:value="row.memo" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{ row }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<div class="flex justify-center mt-4">
|
||||
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['infra:student:create']">
|
||||
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
||||
import { getStudentContactListByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
list.value = await getStudentContactListByStudentId(props.studentId!);
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card title="学生联系人列表">
|
||||
<VxeTable
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { message, Tabs, Form, Input, Textarea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentTeacher>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => await formRef.value?.validate(),
|
||||
getValues: ()=> formData.value,
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
formData.value = await getStudentTeacherByStudentId(props.studentId!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
class="mx-4"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
||||
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentTeacher[]>([]) // 列表的数据
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
list.value = [await getStudentTeacherByStudentId(props.studentId!)];
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card title="学生班主任列表">
|
||||
<VxeTable
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
|
||||
|
||||
<!--
|
||||
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
|
||||
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
|
||||
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
|
||||
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
|
||||
-->
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,67 @@
|
||||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
} ]
|
||||
@@ -0,0 +1,3 @@
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生联系人 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_contact")
|
||||
@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentContactDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生联系人 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
|
||||
|
||||
default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
|
||||
return selectList(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
|
||||
|
||||
@Tag(name = "管理后台 - 学生")
|
||||
@RestController
|
||||
@RequestMapping("/infra/student")
|
||||
@Validated
|
||||
public class InfraStudentController {
|
||||
|
||||
@Resource
|
||||
private InfraStudentService studentService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:create')")
|
||||
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
|
||||
return success(studentService.createStudent(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:update')")
|
||||
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
|
||||
studentService.updateStudent(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除学生")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
|
||||
studentService.deleteStudent(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
|
||||
InfraStudentDO student = studentService.getStudent(id);
|
||||
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得学生分页")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
|
||||
BeanUtils.toBean(list, InfraStudentRespVO.class));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@GetMapping("/student-contact/list-by-student-id")
|
||||
@Operation(summary = "获得学生联系人列表")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentContactListByStudentId(studentId));
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@GetMapping("/student-teacher/get-by-student-id")
|
||||
@Operation(summary = "获得学生班主任")
|
||||
@Parameter(name = "studentId", description = "学生编号")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:query')")
|
||||
public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
|
||||
return success(studentService.getStudentTeacherByStudentId(studentId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student")
|
||||
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
|
||||
/**
|
||||
* 学生 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
|
||||
|
||||
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
|
||||
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
|
||||
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
|
||||
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
|
||||
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
|
||||
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(InfraStudentDO::getId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", example = "1")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", example = "true")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
||||
|
||||
@Schema(description = "管理后台 - 学生 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class InfraStudentRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@ExcelProperty("编号")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@ExcelProperty("名字")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@ExcelProperty("简介")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("出生日期")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@ExcelProperty(value = "性别", converter = DictConvert.class)
|
||||
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
|
||||
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@ExcelProperty("头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@ExcelProperty("附件")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@ExcelProperty("备注")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
|
||||
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
|
||||
@Data
|
||||
public class InfraStudentSaveReqVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
|
||||
@NotEmpty(message = "名字不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
|
||||
@NotEmpty(message = "简介不能为空")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "出生日期不能为空")
|
||||
private LocalDateTime birthday;
|
||||
|
||||
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "性别不能为空")
|
||||
private Integer sex;
|
||||
|
||||
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@NotNull(message = "是否有效不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
|
||||
@NotEmpty(message = "头像不能为空")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
|
||||
@NotEmpty(message = "附件不能为空")
|
||||
private String video;
|
||||
|
||||
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
|
||||
@NotEmpty(message = "备注不能为空")
|
||||
private String memo;
|
||||
|
||||
@Schema(description = "学生联系人列表")
|
||||
private List<InfraStudentContactDO> studentContacts;
|
||||
|
||||
@Schema(description = "学生班主任")
|
||||
private InfraStudentTeacherDO studentTeacher;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
|
||||
/**
|
||||
* 学生 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface InfraStudentService {
|
||||
|
||||
/**
|
||||
* 创建学生
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新学生
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除学生
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 学生
|
||||
*/
|
||||
InfraStudentDO getStudent(Long id);
|
||||
|
||||
/**
|
||||
* 获得学生分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 学生分页
|
||||
*/
|
||||
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/**
|
||||
* 获得学生联系人列表
|
||||
*
|
||||
* @param studentId 学生编号
|
||||
* @return 学生联系人列表
|
||||
*/
|
||||
List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/**
|
||||
* 获得学生班主任
|
||||
*
|
||||
* @param studentId 学生编号
|
||||
* @return 学生班主任
|
||||
*/
|
||||
InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 学生 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class InfraStudentServiceImpl implements InfraStudentService {
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
@Resource
|
||||
private InfraStudentContactMapper studentContactMapper;
|
||||
@Resource
|
||||
private InfraStudentTeacherMapper studentTeacherMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
|
||||
// 插入
|
||||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
|
||||
// 插入子表
|
||||
createStudentContactList(student.getId(), createReqVO.getStudentContacts());
|
||||
createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
|
||||
// 返回
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateStudentExists(updateReqVO.getId());
|
||||
// 更新
|
||||
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
|
||||
studentMapper.updateById(updateObj);
|
||||
|
||||
// 更新子表
|
||||
updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
|
||||
updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudent(Long id) {
|
||||
// 校验存在
|
||||
validateStudentExists(id);
|
||||
// 删除
|
||||
studentMapper.deleteById(id);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentId(id);
|
||||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentDO getStudent(Long id) {
|
||||
return studentMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
|
||||
return studentMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
@Override
|
||||
public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
|
||||
return studentContactMapper.selectListByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
studentContactMapper.insertBatch(list);
|
||||
}
|
||||
|
||||
private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
|
||||
List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
|
||||
boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
|
||||
if (same) {
|
||||
newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
|
||||
}
|
||||
return same;
|
||||
});
|
||||
|
||||
// 第二步,批量添加、修改、删除
|
||||
if (CollUtil.isNotEmpty(diffList.get(0))) {
|
||||
studentContactMapper.insertBatch(diffList.get(0));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(1))) {
|
||||
studentContactMapper.updateBatch(diffList.get(1));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(2))) {
|
||||
studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentId(Long studentId) {
|
||||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
|
||||
return studentTeacherMapper.selectByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
|
||||
if (studentTeacher == null) {
|
||||
return;
|
||||
}
|
||||
studentTeacher.setStudentId(studentId);
|
||||
studentTeacherMapper.insert(studentTeacher);
|
||||
}
|
||||
|
||||
private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
|
||||
if (studentTeacher == null) {
|
||||
return;
|
||||
}
|
||||
studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.insertOrUpdate(studentTeacher);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentId(Long studentId) {
|
||||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.*;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link InfraStudentServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Import(InfraStudentServiceImpl.class)
|
||||
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private InfraStudentServiceImpl studentService;
|
||||
|
||||
@Resource
|
||||
private InfraStudentMapper studentMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateStudent_success() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
|
||||
|
||||
// 调用
|
||||
Long studentId = studentService.createStudent(createReqVO);
|
||||
// 断言
|
||||
assertNotNull(studentId);
|
||||
// 校验记录的属性是否正确
|
||||
InfraStudentDO student = studentMapper.selectById(studentId);
|
||||
assertPojoEquals(createReqVO, student, "id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
|
||||
o.setId(dbStudent.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
// 调用
|
||||
studentService.updateStudent(updateReqVO);
|
||||
// 校验是否更新正确
|
||||
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(updateReqVO, student);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateStudent_notExists() {
|
||||
// 准备参数
|
||||
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_success() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
|
||||
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbStudent.getId();
|
||||
|
||||
// 调用
|
||||
studentService.deleteStudent(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(studentMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteStudent_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
|
||||
public void testGetStudentPage() {
|
||||
// mock 数据
|
||||
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
|
||||
o.setName(null);
|
||||
o.setBirthday(null);
|
||||
o.setSex(null);
|
||||
o.setEnabled(null);
|
||||
o.setCreateTime(null);
|
||||
});
|
||||
studentMapper.insert(dbStudent);
|
||||
// 测试 name 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
|
||||
// 测试 birthday 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
|
||||
// 测试 sex 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
|
||||
// 测试 enabled 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
|
||||
// 测试 createTime 不匹配
|
||||
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
|
||||
// 准备参数
|
||||
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
|
||||
reqVO.setName(null);
|
||||
reqVO.setBirthday(null);
|
||||
reqVO.setSex(null);
|
||||
reqVO.setEnabled(null);
|
||||
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
|
||||
|
||||
// 调用
|
||||
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbStudent, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 学生班主任 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("infra_student_teacher")
|
||||
@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InfraStudentTeacherDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 学生编号
|
||||
*/
|
||||
private Long studentId;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
private LocalDateTime birthday;
|
||||
/**
|
||||
* 性别
|
||||
*
|
||||
* 枚举 {@link TODO system_user_sex 对应的类}
|
||||
*/
|
||||
private Integer sex;
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean enabled;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 附件
|
||||
*/
|
||||
private String video;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 学生班主任 Mapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Mapper
|
||||
public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
|
||||
|
||||
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
|
||||
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
|
||||
CREATE TABLE IF NOT EXISTS "infra_student" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"name" varchar NOT NULL,
|
||||
"description" varchar NOT NULL,
|
||||
"birthday" varchar NOT NULL,
|
||||
"sex" int NOT NULL,
|
||||
"enabled" bit NOT NULL,
|
||||
"avatar" varchar NOT NULL,
|
||||
"video" varchar NOT NULL,
|
||||
"memo" varchar NOT NULL,
|
||||
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT '学生表';
|
||||
|
||||
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
|
||||
DELETE FROM "infra_student";
|
||||
@@ -0,0 +1,54 @@
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
studentcontacts?: StudentContact[]
|
||||
studentteacher?: StudentTeacher
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人列表 */
|
||||
export function getStudentContactListByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacherByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
/** 子表的表单 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const studentContactFormRef = ref<InstanceType<typeof StudentContactForm>>()
|
||||
const studentTeacherFormRef = ref<InstanceType<typeof StudentTeacherForm>>()
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
// 校验子表单
|
||||
try {
|
||||
await studentTeacherFormRef.value?.validate()
|
||||
} catch (e) {
|
||||
subTabsName.value = 'studentTeacher'
|
||||
return
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
// 拼接子表的数据
|
||||
data.studentContacts = studentContactFormRef.value?.getData();
|
||||
data.studentTeacher = studentTeacherFormRef.value?.getValues();
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactForm ref="studentContactFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user