Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17

This commit is contained in:
YunaiV
2026-07-13 00:27:03 +08:00
152 changed files with 11924 additions and 768 deletions
+14 -28
View File
@@ -19,12 +19,24 @@
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<spring-ai.version>1.1.5</spring-ai.version>
<spring-ai.version>1.1.8</spring-ai.version>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
@@ -111,38 +123,12 @@
<artifactId>spring-ai-starter-model-stability-ai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<!-- 智谱 GLM -->
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-minimax</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<!-- 通义千问 -->
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${alibaba-ai.version}</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springaicommunity</groupId>
<artifactId>qianfan-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<!-- 月之暗面 -->
<groupId>org.springaicommunity</groupId>
<artifactId>moonshot-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 向量存储:https://db-engines.com/en/ranking/vector+dbms -->
<dependency>
<!-- Qdrant:https://qdrant.tech/ -->
@@ -260,4 +246,4 @@
</dependency>
</dependencies>
</project>
</project>
@@ -106,6 +106,20 @@ public class AiChatConversationController {
conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0))));
}
@GetMapping("/get")
@Operation(summary = "获得对话", description = "用于【对话管理】菜单")
@Parameter(name = "id", required = true, description = "对话编号", example = "1024")
@PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
@TransMethodResult
public CommonResult<AiChatConversationRespVO> getChatConversation(@RequestParam("id") Long id) {
AiChatConversationDO conversation = chatConversationService.getChatConversation(id);
AiChatConversationRespVO respVO = BeanUtils.toBean(conversation, AiChatConversationRespVO.class);
if (respVO != null) {
respVO.setMessageCount(chatMessageService.getChatMessageCount(id));
}
return success(respVO);
}
@Operation(summary = "管理员删除对话")
@DeleteMapping("/delete-by-admin")
@Parameter(name = "id", required = true, description = "对话编号", example = "1024")
@@ -131,7 +131,7 @@ public class AiChatMessageController {
@GetMapping("/page")
@Operation(summary = "获得消息分页", description = "用于【对话管理】菜单")
@PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
@PreAuthorize("@ss.hasPermission('ai:chat-message:query')")
public CommonResult<PageResult<AiChatMessageRespVO>> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
PageResult<AiChatMessageDO> pageResult = chatMessageService.getChatMessagePage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
@@ -145,6 +145,20 @@ public class AiChatMessageController {
role -> respVO.setRoleName(role.getName()))));
}
@GetMapping("/get")
@Operation(summary = "获得消息", description = "用于【对话管理】菜单")
@Parameter(name = "id", required = true, description = "消息编号", example = "1024")
@PreAuthorize("@ss.hasPermission('ai:chat-message:query')")
public CommonResult<AiChatMessageRespVO> getChatMessage(@RequestParam("id") Long id) {
AiChatMessageDO message = chatMessageService.getChatMessage(id);
AiChatMessageRespVO respVO = BeanUtils.toBean(message, AiChatMessageRespVO.class);
if (respVO != null && respVO.getRoleId() != null) {
AiChatRoleDO role = chatRoleService.getChatRole(respVO.getRoleId());
respVO.setRoleName(role != null ? role.getName() : null);
}
return success(respVO);
}
@Operation(summary = "管理员删除消息")
@DeleteMapping("/delete-by-admin")
@Parameter(name = "id", required = true, description = "消息编号", example = "1024")
@@ -119,6 +119,15 @@ public class AiImageController {
return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得绘画")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:image:query')")
public CommonResult<AiImageRespVO> getImage(@RequestParam("id") Long id) {
AiImageDO image = imageService.getImage(id);
return success(BeanUtils.toBean(image, AiImageRespVO.class));
}
@PutMapping("/update")
@Operation(summary = "更新绘画")
@PreAuthorize("@ss.hasPermission('ai:image:update')")
@@ -136,4 +145,4 @@ public class AiImageController {
return success(true);
}
}
}
@@ -46,6 +46,15 @@ public class AiMindMapController {
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得思维导图")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
public CommonResult<AiMindMapRespVO> getMindMap(@RequestParam("id") Long id) {
AiMindMapDO mindMap = mindMapService.getMindMap(id);
return success(BeanUtils.toBean(mindMap, AiMindMapRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得思维导图分页")
@PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
@@ -78,6 +78,15 @@ public class AiMusicController {
return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得音乐")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:music:query')")
public CommonResult<AiMusicRespVO> getMusic(@RequestParam("id") Long id) {
AiMusicDO music = musicService.getMusic(id);
return success(BeanUtils.toBean(music, AiMusicRespVO.class));
}
@DeleteMapping("/delete")
@Operation(summary = "删除音乐")
@Parameter(name = "id", description = "编号", required = true)
@@ -46,6 +46,15 @@ public class AiWriteController {
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得写作")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:write:query')")
public CommonResult<AiWriteRespVO> getWrite(@RequestParam("id") Long id) {
AiWriteDO write = writeService.getWrite(id);
return success(BeanUtils.toBean(write, AiWriteRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得写作分页")
@PreAuthorize("@ss.hasPermission('ai:write:query')")
@@ -12,6 +12,7 @@ public interface ErrorCodeConstants {
// ========== API 密钥 1-040-000-000 ==========
ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在");
ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!");
ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI 配置({})无法解析,请检查环境变量或配置项");
// ========== API 模型 1-040-001-000 ==========
ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!");
@@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
MOONSHOT("Moonshot", "月之暗面"), // KIMI
BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
STEP_FUN("StepFun", "阶跃星辰"), // 阶跃星辰
// ========== 国外平台 ==========
@@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
SUNO("Suno", "Suno"), // Suno AI
GROK("Grok","Grok"), // Grok
GROK("Grok", "Grok"), // Grok
;
@@ -10,10 +10,15 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
@@ -44,7 +49,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Optional;
/**
* 芋道 AI 自动配置
@@ -113,13 +117,13 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.defaultOptions(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
@@ -168,11 +172,8 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
}
// 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL
if (StrUtil.isEmpty(properties.getBaseUrl())) {
properties.setBaseUrl(
StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
: HunYuanChatModel.BASE_URL);
properties.setBaseUrl(HunYuanChatModel.BASE_URL);
}
// 创建 DeepSeekChatModel、HunYuanChatModel 对象
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
@@ -203,25 +204,15 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
}
OpenAiApi.Builder builder = OpenAiApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL_V1)
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey());
if ("x1".equals(properties.getModel())) {
builder.baseUrl(XingHuoChatModel.BASE_URL_V2)
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(OpenAiChatOptions.builder()
return XingHuoChatModel.builder()
.apiKey(properties.getApiKey())
.options(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
// TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!!
.toolCallingManager(getToolCallingManager())
.build();
return new XingHuoChatModel(openAiChatModel);
}
@Bean
@@ -235,12 +226,13 @@ public class AiAutoConfiguration {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.completionsPath(BaiChuanChatModel.COMPLETE_PATH)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.defaultOptions(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
@@ -248,7 +240,123 @@ public class AiAutoConfiguration {
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new BaiChuanChatModel(openAiChatModel);
return new BaiChuanChatModel(deepSeekChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true")
public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan();
return buildYiYanChatClient(properties);
}
public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(YiYanChatModel.MODEL_DEFAULT);
}
return new YiYanChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL),
null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true")
public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu();
return buildZhiPuChatClient(properties);
}
public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(ZhiPuChatModel.MODEL_DEFAULT);
}
DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL))
.apiKey(properties.getApiKey())
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.build();
return new ZhiPuChatModel(deepSeekChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true")
public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax();
return buildMiniMaxChatClient(properties);
}
public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(MiniMaxChatModel.MODEL_DEFAULT);
}
return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL),
null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true")
public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot();
return buildMoonshotChatClient(properties);
}
public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(MoonshotChatModel.MODEL_DEFAULT);
}
return new MoonshotChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL),
MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true")
public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun();
return buildStepFunChatClient(properties);
}
public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(StepFunChatModel.MODEL_DEFAULT);
}
return new StepFunChatModel(buildDeepSeekCompatibleChatModel(
StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL),
StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
}
private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath,
String apiKey, String model,
Double temperature, Integer maxTokens,
Double topP) {
DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder()
.baseUrl(baseUrl)
.apiKey(apiKey);
if (StrUtil.isNotEmpty(completionsPath)) {
apiBuilder.completionsPath(completionsPath);
}
return DeepSeekChatModel.builder()
.deepSeekApi(apiBuilder.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(model)
.temperature(temperature)
.maxTokens(maxTokens)
.topP(topP)
.build())
.toolCallingManager(getToolCallingManager())
.build();
}
@Bean
@@ -264,14 +372,20 @@ public class AiAutoConfiguration {
return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
}
public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
@Bean
@ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true")
public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok();
return buildGrokChatClient(properties);
}
public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(GrokChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(Optional.ofNullable(properties.getBaseUrl())
.orElse(GrokChatModel.BASE_URL))
.baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL))
.completionsPath(GrokChatModel.COMPLETE_PATH)
.apiKey(properties.getApiKey())
.build())
@@ -283,7 +397,7 @@ public class AiAutoConfiguration {
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DouBaoChatModel(openAiChatModel);
return new GrokChatModel(openAiChatModel);
}
// ========== RAG 相关 ==========
@@ -320,4 +434,4 @@ public class AiAutoConfiguration {
return List.of(ToolCallbacks.from(personService));
}
}
}
@@ -43,6 +43,36 @@ public class YudaoAiProperties {
*/
private BaiChuan baichuan;
/**
* 文心一言
*/
private YiYan yiyan;
/**
* 智谱
*/
private ZhiPu zhipu;
/**
* MiniMax
*/
private MiniMax minimax;
/**
* 月之暗面
*/
private Moonshot moonshot;
/**
* 阶跃星辰
*/
private StepFun stepfun;
/**
* Grok
*/
private Grok grok;
/**
* Midjourney 绘图
*/
@@ -116,9 +146,7 @@ public class YudaoAiProperties {
public static class XingHuo {
private String enable;
private String appId;
private String appKey;
private String secretKey;
private String apiKey;
private String model;
private Double temperature;
@@ -140,6 +168,76 @@ public class YudaoAiProperties {
}
@Data
public static class YiYan {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class ZhiPu {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class MiniMax {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class Moonshot {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class StepFun {
private String enable;
private String apiKey;
private String baseUrl;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class Midjourney {
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.func.Func0;
import cn.hutool.core.util.ArrayUtil;
@@ -15,14 +14,21 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
@@ -40,18 +46,6 @@ import io.milvus.client.MilvusServiceClient;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import lombok.SneakyThrows;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanEmbeddingModel;
import org.springaicommunity.qianfan.QianFanEmbeddingOptions;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;
import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.chat.model.ChatModel;
@@ -63,27 +57,17 @@ import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
@@ -114,13 +98,9 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.ai.zhipuai.*;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.web.client.RestClient;
import redis.clients.jedis.JedisPooled;
import java.io.File;
@@ -131,7 +111,6 @@ import java.util.Timer;
import java.util.TimerTask;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
/**
* AI Model 模型工厂的实现类
@@ -141,7 +120,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
public class AiModelFactoryImpl implements AiModelFactory {
@Override
public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) {
public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
// noinspection EnhancedSwitchMigration
@@ -164,6 +145,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildMiniMaxChatModel(apiKey, url);
case MOONSHOT:
return buildMoonshotChatModel(apiKey, url);
case STEP_FUN:
return buildStepFunChatModel(apiKey, url);
case XING_HUO:
return buildXingHuoChatModel(apiKey);
case BAI_CHUAN:
@@ -179,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case OLLAMA:
return buildOllamaChatModel(url);
case GROK:
return buildGrokChatModel(apiKey,url);
return buildGrokChatModel(apiKey, url);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@@ -193,7 +176,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case TONG_YI:
return SpringUtil.getBean(DashScopeChatModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanChatModel.class);
return SpringUtil.getBean(YiYanChatModel.class);
case DEEP_SEEK:
return SpringUtil.getBean(DeepSeekChatModel.class);
case DOU_BAO:
@@ -203,11 +186,13 @@ public class AiModelFactoryImpl implements AiModelFactory {
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowChatModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiChatModel.class);
return SpringUtil.getBean(ZhiPuChatModel.class);
case MINI_MAX:
return SpringUtil.getBean(MiniMaxChatModel.class);
case MOONSHOT:
return SpringUtil.getBean(MoonshotChatModel.class);
case STEP_FUN:
return SpringUtil.getBean(StepFunChatModel.class);
case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN:
@@ -222,6 +207,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(GeminiChatModel.class);
case OLLAMA:
return SpringUtil.getBean(OllamaChatModel.class);
case GROK:
return SpringUtil.getBean(GrokChatModel.class);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@@ -233,10 +220,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
switch (platform) {
case TONG_YI:
return SpringUtil.getBean(DashScopeImageModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanImageModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiImageModel.class);
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowImageModel.class);
case OPENAI:
@@ -249,19 +232,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) {
public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) {
String apiKey = resolveSpringPlaceholders(rawApiKey);
String url = resolveSpringPlaceholders(rawUrl);
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return buildTongYiImagesModel(apiKey);
case YI_YAN:
return buildQianFanImageModel(apiKey);
case ZHI_PU:
return buildZhiPuAiImageModel(apiKey, url);
case OPENAI:
return buildOpenAiImageModel(apiKey, url);
case SILICON_FLOW:
return buildSiliconFlowImageModel(apiKey,url);
return buildSiliconFlowImageModel(apiKey, url);
case STABLE_DIFFUSION:
return buildStabilityAiImageModel(apiKey, url);
default:
@@ -270,9 +251,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
url);
public MidjourneyApi getOrCreateMidjourneyApi(String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(),
apiKey, url);
return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
.getMidjourney();
@@ -281,25 +264,23 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
@Override
public SunoApi getOrCreateSunoApi(String apiKey, String url) {
public SunoApi getOrCreateSunoApi(String rawApiKey, String rawUrl) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
}
@Override
@SuppressWarnings("EnhancedSwitchMigration")
public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) {
public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String rawApiKey, String rawUrl, String model) {
final String apiKey = resolveSpringPlaceholders(rawApiKey);
final String url = resolveSpringPlaceholders(rawUrl);
String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model);
return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
switch (platform) {
case TONG_YI:
return buildTongYiEmbeddingModel(apiKey, model);
case YI_YAN:
return buildYiYanEmbeddingModel(apiKey, model);
case ZHI_PU:
return buildZhiPuEmbeddingModel(apiKey, url, model);
case MINI_MAX:
return buildMiniMaxEmbeddingModel(apiKey, url, model);
case OPENAI:
return buildOpenAiEmbeddingModel(apiKey, url, model);
case AZURE_OPENAI:
@@ -341,6 +322,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
}
private static String resolveSpringPlaceholders(String value) {
// yml 配置的占位符由 Spring 自动解析;DB 里保存的 ${xxx} 需要在这里手动解析。
return AiUtils.resolveSpringPlaceholders(value);
}
// ========== 各种创建 spring-ai 客户端的方法 ==========
/**
@@ -367,30 +353,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
.build();
}
/**
* 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法
*/
private static QianFanChatModel buildYiYanChatModel(String key) {
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
return new QianFanChatModel(qianFanApi);
}
/**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法
*/
private QianFanImageModel buildQianFanImageModel(String key) {
// TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey);
return new QianFanImageModel(qianFanApi);
private ChatModel buildYiYanChatModel(String apiKey) {
YudaoAiProperties.YiYan properties = new YudaoAiProperties.YiYan()
.setApiKey(apiKey);
return new AiAutoConfiguration().buildYiYanChatClient(properties);
}
/**
@@ -435,62 +401,47 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
/**
* 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法
* 可参考 {@link AiAutoConfiguration#zhiPuChatClient(YudaoAiProperties)}
*/
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
getObservationRegistry().getIfAvailable());
private ZhiPuChatModel buildZhiPuChatModel(String apiKey, String url) {
YudaoAiProperties.ZhiPu properties = new YudaoAiProperties.ZhiPu()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildZhiPuChatClient(properties);
}
/**
* 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法
*/
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
: new ZhiPuAiImageApi(url, apiKey, RestClient.builder());
return new ZhiPuAiImageModel(zhiPuAiApi);
}
/**
* 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法
* 可参考 {@link AiAutoConfiguration#miniMaxChatClient(YudaoAiProperties)}
*/
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE);
YudaoAiProperties.MiniMax properties = new YudaoAiProperties.MiniMax()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildMiniMaxChatClient(properties);
}
/**
* 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法
* 可参考 {@link AiAutoConfiguration#moonshotChatClient(YudaoAiProperties)}
*/
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()
.apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
moonshotApiBuilder.baseUrl(url);
}
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
return MoonshotChatModel.builder()
.moonshotApi(moonshotApiBuilder.build())
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
YudaoAiProperties.Moonshot properties = new YudaoAiProperties.Moonshot()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildMoonshotChatClient(properties);
}
/**
* 可参考 {@link AiAutoConfiguration#stepFunChatClient(YudaoAiProperties)}
*/
private StepFunChatModel buildStepFunChatModel(String apiKey, String url) {
YudaoAiProperties.StepFun properties = new YudaoAiProperties.StepFun()
.setBaseUrl(url).setApiKey(apiKey);
return new AiAutoConfiguration().buildStepFunChatClient(properties);
}
/**
* 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)}
*/
private static XingHuoChatModel buildXingHuoChatModel(String key) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
private static XingHuoChatModel buildXingHuoChatModel(String apiKey) {
YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
.setAppKey(keys.get(0)).setSecretKey(keys.get(1));
.setApiKey(apiKey).setModel(XingHuoChatModel.MODEL_DEFAULT);
return new AiAutoConfiguration().buildXingHuoChatClient(properties);
}
@@ -590,7 +541,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new StabilityAiImageModel(stabilityAiApi);
}
private ChatModel buildGrokChatModel(String apiKey,String url) {
private GrokChatModel buildGrokChatModel(String apiKey, String url) {
YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok()
.setBaseUrl(url)
.setApiKey(apiKey);
@@ -608,41 +559,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
}
/**
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
*/
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
zhiPuAiApiBuilder.baseUrl(url);
}
ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build();
return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions);
}
/**
* 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法
*/
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build();
return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions);
}
/**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法
*/
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build();
return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions);
}
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build();
@@ -6,7 +6,7 @@ import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
@@ -20,26 +20,28 @@ public class BaiChuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.baichuan-ai.com";
public static final String MODEL_DEFAULT = "Baichuan4-Turbo";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "Baichuan-M3";
/**
* 兼容 OpenAI 接口,进行复用
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final OpenAiChatModel openAiChatModel;
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -20,7 +20,7 @@ public class DouBaoChatModel implements ChatModel {
public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
public static final String COMPLETE_PATH = "/v3/chat/completions";
public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115";
public static final String MODEL_DEFAULT = "doubao-seed-2-1-turbo-260628";
/**
* 兼容 OpenAI 接口,进行复用
@@ -19,7 +19,7 @@ public class GrokChatModel implements ChatModel {
public static final String BASE_URL = "https://api.x.ai";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "grok-4-fast-reasoning";
public static final String MODEL_DEFAULT = "grok-4.3";
/**
* 兼容 OpenAI 接口,进行复用
@@ -9,10 +9,9 @@ import org.springframework.ai.chat.prompt.Prompt;
import reactor.core.publisher.Flux;
/**
* 腾云混元 {@link ChatModel} 实现类
* 腾讯混元 {@link ChatModel} 实现类
*
* 1. 混元大模型:基于 <a href="https://cloud.tencent.com/document/product/1729/111007">知识引擎原子能力</a> 实现
* 2. 知识引擎原子能力:基于 <a href="https://cloud.tencent.com/document/product/1772/115969">知识引擎原子能力</a> 实现
* 基于 <a href="https://cloud.tencent.com/document/product/1823/132252">TokenHub OpenAI 兼容接口</a> 实现
*
* @author fansili
*/
@@ -20,14 +19,10 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor
public class HunYuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
public static final String BASE_URL = "https://tokenhub.tencentmaas.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "hunyuan-turbo";
public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com";
public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3";
public static final String MODEL_DEFAULT = "hy3-preview";
/**
* 兼容 OpenAI 接口,进行复用
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* MiniMax {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MiniMaxChatModel implements ChatModel {
public static final String BASE_URL = "https://api.minimaxi.com/v1";
public static final String MODEL_DEFAULT = "MiniMax-M3";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 月之暗面 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class MoonshotChatModel implements ChatModel {
public static final String BASE_URL = "https://api.moonshot.cn";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "kimi-k2.6";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 阶跃星辰 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class StepFunChatModel implements ChatModel {
public static final String BASE_URL = "https://api.stepfun.com";
public static final String COMPLETE_PATH = "/v1/chat/completions";
public static final String MODEL_DEFAULT = "step-3.7-flash";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo;
import lombok.RequiredArgsConstructor;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
/**
@@ -14,37 +17,140 @@ import reactor.core.publisher.Flux;
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class XingHuoChatModel implements ChatModel {
public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";
public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com";
public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions";
/**
* 星火 X2
*
* @see <a href="https://spark-api-open.xf-yun.com/x2/chat/completions">接口地址</a>
*/
public static final String MODEL_X2 = "x2";
/**
* 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite
* 星火 X2 Flash
*
* @see <a href="https://spark-api-open.xf-yun.com/agent/v1/chat/completions">接口地址</a>
*/
public static final String MODEL_DEFAULT = "4.0Ultra";
public static final String MODEL_X2_FLASH = "x2-flash";
private static final String BASE_URL_X2 = "https://spark-api-open.xf-yun.com/x2";
private static final String BASE_URL_X2_FLASH = "https://spark-api-open.xf-yun.com/agent/v1";
public static final String MODEL_DEFAULT = MODEL_X2_FLASH;
private static String getBaseUrl(String model) {
if (MODEL_X2_FLASH.equals(model)) {
return BASE_URL_X2_FLASH;
}
return BASE_URL_X2;
}
private final String apiKey;
private final DeepSeekChatOptions options;
/**
* v1 兼容 OpenAI 接口,进行复用
* 兼容 OpenAI 接口,进行复用
*/
private final ChatModel openAiChatModelV1;
private final ChatModel chatModelX2;
private final ChatModel chatModelX2Flash;
private XingHuoChatModel(String apiKey, DeepSeekChatOptions options) {
this.apiKey = apiKey;
this.options = options;
this.chatModelX2 = buildChatModel(MODEL_X2);
this.chatModelX2Flash = buildChatModel(MODEL_X2_FLASH);
}
public static Builder builder() {
return new Builder();
}
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModelV1.call(prompt);
return getChatModel(prompt).call(buildApiPrompt(prompt));
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModelV1.stream(prompt);
return getChatModel(prompt).stream(buildApiPrompt(prompt));
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModelV1.getDefaultOptions();
return options;
}
private ChatModel getChatModel(Prompt prompt) {
String model = options.getModel();
ChatOptions options = prompt.getOptions();
if (options != null && isBusinessModel(options.getModel())) {
model = options.getModel();
}
return getChatModel(model);
}
private ChatModel getChatModel(String model) {
if (MODEL_X2_FLASH.equals(model)) {
return chatModelX2Flash;
}
return chatModelX2;
}
private ChatModel buildChatModel(String model) {
return DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(getBaseUrl(model))
.apiKey(apiKey)
.build())
.defaultOptions(new DeepSeekChatOptions.Builder(options).model("spark-x").build())
.build();
}
private static Prompt buildApiPrompt(Prompt prompt) {
ChatOptions options = prompt.getOptions();
if (options == null) {
return prompt;
}
if (!(options instanceof DeepSeekChatOptions)) {
return prompt;
}
return Prompt.builder()
.messages(prompt.getInstructions())
.chatOptions(new DeepSeekChatOptions.Builder((DeepSeekChatOptions) options).model("spark-x").build())
.build();
}
private static boolean isBusinessModel(String model) {
return MODEL_X2.equals(model) || MODEL_X2_FLASH.equals(model);
}
public static final class Builder {
private String apiKey;
private DeepSeekChatOptions options;
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public Builder options(DeepSeekChatOptions options) {
this.options = options;
return this;
}
public XingHuoChatModel build() {
DeepSeekChatOptions options = this.options != null ? this.options : DeepSeekChatOptions.builder().build();
if (StrUtil.isEmpty(options.getModel())) {
options = new DeepSeekChatOptions.Builder(options).model(MODEL_DEFAULT).build();
}
return new XingHuoChatModel(apiKey, options);
}
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 文心一言 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class YiYanChatModel implements ChatModel {
public static final String BASE_URL = "https://qianfan.baidubce.com/v2";
public static final String MODEL_DEFAULT = "ernie-5.1";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import reactor.core.publisher.Flux;
/**
* 智谱 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class ZhiPuChatModel implements ChatModel {
public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
public static final String MODEL_DEFAULT = "glm-5.2";
/**
* 兼容 OpenAI 接口,复用 DeepSeek 客户端
*/
private final DeepSeekChatModel deepSeekChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return deepSeekChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return deepSeekChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return deepSeekChatModel.getDefaultOptions();
}
}
@@ -45,6 +45,14 @@ public interface AiChatMessageService {
*/
List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId);
/**
* 获得聊天消息
*
* @param id 消息编号
* @return 聊天消息
*/
AiChatMessageDO getChatMessage(Long id);
/**
* 删除消息
*
@@ -68,6 +76,14 @@ public interface AiChatMessageService {
*/
void deleteChatMessageByAdmin(Long id);
/**
* 获得聊天对话的消息数量
*
* @param conversationId 对话编号
* @return 消息数量
*/
Integer getChatMessageCount(Long conversationId);
/**
* 获得聊天对话的消息数量 Map
*
@@ -524,6 +524,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return chatMessageMapper.selectListByConversationId(conversationId);
}
@Override
public AiChatMessageDO getChatMessage(Long id) {
return chatMessageMapper.selectById(id);
}
@Override
public void deleteChatMessage(Long id, Long userId) {
// 1. 校验消息存在
@@ -557,6 +562,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
chatMessageMapper.deleteById(id);
}
@Override
public Integer getChatMessageCount(Long conversationId) {
return chatMessageMapper.selectCount(AiChatMessageDO::getConversationId, conversationId).intValue();
}
@Override
public Map<Long, Integer> getChatMessageCountMap(Collection<Long> conversationIds) {
return chatMessageMapper.selectCountMapByConversationId(conversationIds);
@@ -29,14 +29,12 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService {
.model(model.getModel()).n(1)
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder()
.model(model.getModel()).N(1)
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) {
return ZhiPuAiImageOptions.builder()
.model(model.getModel())
.build();
}
throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform());
}
@@ -30,6 +30,14 @@ public interface AiMindMapService {
*/
void deleteMindMap(Long id);
/**
* 获得思维导图
*
* @param id 编号
* @return 思维导图
*/
AiMindMapDO getMindMap(Long id);
/**
* 获得思维导图分页
*
@@ -153,6 +153,11 @@ public class AiMindMapServiceImpl implements AiMindMapService {
}
}
@Override
public AiMindMapDO getMindMap(Long id) {
return mindMapMapper.selectById(id);
}
@Override
public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
return mindMapMapper.selectPage(pageReqVO);
@@ -30,6 +30,14 @@ public interface AiWriteService {
*/
void deleteWrite(Long id);
/**
* 获得写作
*
* @param id 编号
* @return AI 写作
*/
AiWriteDO getWrite(Long id);
/**
* 获得写作分页
*
@@ -170,6 +170,11 @@ public class AiWriteServiceImpl implements AiWriteService {
}
}
@Override
public AiWriteDO getWrite(Long id) {
return writeMapper.selectById(id);
}
@Override
public PageResult<AiWriteDO> getWritePage(AiWritePageReqVO pageReqVO) {
return writeMapper.selectPage(pageReqVO);
@@ -8,8 +8,7 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
@@ -17,14 +16,16 @@ import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.core.env.Environment;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED;
/**
* Spring AI 工具类
*
@@ -35,6 +36,34 @@ public class AiUtils {
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
/**
* 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY}
*
* @param value 待解析的配置值
* @return 解析后的配置值
*/
public static String resolveSpringPlaceholders(String value) {
if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) {
return value;
}
try {
return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value);
} catch (IllegalArgumentException ex) {
throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value);
}
}
/**
* 校验 API Key,避免集成测试使用默认占位值发起调用。
*
* @param apiKey API Key
*/
public static void validateApiKey(String apiKey) {
if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) {
throw new IllegalStateException("apiKey 不能为空");
}
}
/**
* 通义千问支持多模态的模型
*
@@ -42,7 +71,8 @@ public class AiUtils {
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a>
*/
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
// qwen3.5 / 3.6 系列(统一多模态主干)
// qwen3.5 / 3.6 / 3.7 系列(统一多模态主干)
"qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash",
"qwen3.6-plus", "qwen3.6-flash",
"qwen3.5-plus", "qwen3.5-flash",
// qwen-vl 视觉理解
@@ -72,27 +102,21 @@ public class AiUtils {
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK:
case DOU_BAO: // 复用 DeepSeek 客户端
case HUN_YUAN: // 复用 DeepSeek 客户端
case SILICON_FLOW: // 复用 DeepSeek 客户端
case YI_YAN: // 复用 DeepSeek 客户端
case ZHI_PU: // 复用 DeepSeek 客户端
case XING_HUO: // 复用 DeepSeek 客户端
case MINI_MAX: // 复用 DeepSeek 客户端
case MOONSHOT: // 复用 DeepSeek 客户端
case BAI_CHUAN: // 复用 DeepSeek 客户端
case STEP_FUN: // 复用 DeepSeek 客户端
return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OPENAI:
case GEMINI: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
case GROK: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
@@ -159,4 +183,4 @@ public class AiUtils {
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
}
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link BaiChuanChatModel} 集成测试
@@ -23,22 +27,27 @@ import java.util.List;
*/
public class BaiChuanChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
private static final String API_KEY = SystemUtil.get("BAICHUAN_API_KEY",
"sk-xxxx"); // 按需改成你的百川 API Key
private static final String MODEL = SystemUtil.get("BAICHUAN_MODEL",
BaiChuanChatModel.MODEL_DEFAULT);
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey
.completionsPath(BaiChuanChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api)
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build();
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel);
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -48,11 +57,13 @@ public class BaiChuanChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -61,7 +72,10 @@ public class BaiChuanChatModelTests {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@@ -14,6 +15,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link DeepSeekChatModel} 集成测试
@@ -22,12 +26,17 @@ import java.util.List;
*/
public class DeepSeekChatModelTests {
private static final String API_KEY = SystemUtil.get("DEEPSEEK_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("DEEPSEEK_MODEL",
"deepseek-v4-flash");
private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey
.apiKey(API_KEY) // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model("deepseek-chat") // 模型
.model(MODEL) // 模型
.temperature(0.7)
.build())
.build();
@@ -35,6 +44,7 @@ public class DeepSeekChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -43,12 +53,13 @@ public class DeepSeekChatModelTests {
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -63,11 +74,12 @@ public class DeepSeekChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-reasoner")
.model(MODEL)
.build();
// 调用
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link DouBaoChatModel} 集成测试
@@ -23,19 +27,19 @@ import java.util.List;
*/
public class DouBaoChatModelTests {
/**
* 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力!
*/
private static final String API_KEY = SystemUtil.get("DOUBAO_API_KEY",
"sk-xxxx"); // 按需改成你的豆包 API Key
private static final String MODEL = SystemUtil.get("DOUBAO_MODEL",
DouBaoChatModel.MODEL_DEFAULT);
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model("doubao-1-5-lite-32k-250115") // 模型(doubao)
// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao)
// .model("deepseek-r1-250120") // 模型(deepseek)
.model(MODEL)
.temperature(0.7)
.build())
.build();
@@ -45,6 +49,7 @@ public class DouBaoChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -54,11 +59,13 @@ public class DouBaoChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -67,25 +74,26 @@ public class DouBaoChatModelTests {
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("doubao-seed-1-6-thinking-250715")
.build();
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link GrokChatModel} 集成测试
*
* @author 芋道源码
*/
public class GrokChatModelTests {
private static final String API_KEY = SystemUtil.get("GROK_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("GROK_MODEL",
GrokChatModel.MODEL_DEFAULT);
private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(GrokChatModel.BASE_URL)
.completionsPath(GrokChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -15,6 +16,9 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link HunYuanChatModel} 集成测试
@@ -23,14 +27,19 @@ import java.util.List;
*/
public class HunYuanChatModelTests {
private static final String API_KEY = SystemUtil.get("HUNYUAN_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("HUNYUAN_MODEL",
HunYuanChatModel.MODEL_DEFAULT);
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.BASE_URL)
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.apiKey(API_KEY) // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(HunYuanChatModel.MODEL_DEFAULT) // 模型
.model(MODEL) // 模型
.temperature(0.7)
.build())
.build();
@@ -40,6 +49,7 @@ public class HunYuanChatModelTests {
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -48,12 +58,13 @@ public class HunYuanChatModelTests {
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -68,12 +79,12 @@ public class HunYuanChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("hunyuan-a13b")
// .model("hunyuan-turbos-latest")
.model(MODEL)
.build();
// 调用
@@ -85,66 +96,4 @@ public class HunYuanChatModelTests {
}).then().block();
}
private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3")
.model("deepseek-r1") // 模型("deepseek-r1")
.temperature(0.7)
.build())
.build();
private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel);
@Test
@Disabled
public void testCall_deepseek() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = deepSeekChatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
}
@Test
@Disabled
public void testStream_deepseek() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
}
@Test
@Disabled
public void testStream_deepseek_thinking() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("deepseek-r1")
.build();
// 调用
Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
}).then().block();
}
}
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link MiniMaxChatModel} 的集成测试
@@ -22,14 +27,24 @@ import java.util.List;
*/
public class MiniMaxChatModelTests {
private final MiniMaxChatModel chatModel = new MiniMaxChatModel(
new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥
MiniMaxChatOptions.builder()
.model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型
.build());
private static final String API_KEY = SystemUtil.get("MINIMAX_API_KEY",
"sk-xxxx"); // 按需改成你的 MiniMax API Key
private static final String MODEL = SystemUtil.get("MINIMAX_MODEL",
MiniMaxChatModel.MODEL_DEFAULT);
private final MiniMaxChatModel chatModel = new MiniMaxChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(MiniMaxChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -39,12 +54,13 @@ public class MiniMaxChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -55,19 +71,19 @@ public class MiniMaxChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
.model("MiniMax-M1")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@@ -75,7 +91,7 @@ public class MiniMaxChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@@ -1,19 +1,24 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link MoonshotChatModel} 的集成测试
@@ -22,18 +27,27 @@ import java.util.List;
*/
public class MoonshotChatModelTests {
private final MoonshotChatModel chatModel = MoonshotChatModel.builder()
.moonshotApi(MoonshotApi.builder()
.apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥
private static final String API_KEY = SystemUtil.get("MOONSHOT_API_KEY",
"sk-xxxx"); // 按需改成你的 Moonshot API Key
private static final String MODEL = SystemUtil.get("MOONSHOT_MODEL",
MoonshotChatModel.MODEL_DEFAULT);
private final MoonshotChatModel chatModel = new MoonshotChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(MoonshotChatModel.BASE_URL)
.completionsPath(MoonshotChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(MoonshotChatOptions.builder()
.model("kimi-k2-0711-preview") // 模型
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(1D)
.build())
.build();
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -43,12 +57,13 @@ public class MoonshotChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -59,20 +74,19 @@ public class MoonshotChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
MoonshotChatOptions options = MoonshotChatOptions.builder()
// .model("kimi-k2-0711-preview")
.model("kimi-thinking-preview")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@@ -80,7 +94,7 @@ public class MoonshotChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link StepFunChatModel} 集成测试
*
* @author 芋道源码
*/
public class StepFunChatModelTests {
private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY",
"sk-xxxx");
private static final String MODEL = SystemUtil.get("STEPFUN_MODEL",
StepFunChatModel.MODEL_DEFAULT);
private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(StepFunChatModel.BASE_URL)
.completionsPath(StepFunChatModel.COMPLETE_PATH)
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
// System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
System.out.println(response.getResult() != null ? response.getResult().getOutput() : null);
}).then().block();
}
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link XingHuoChatModel} 集成测试
@@ -23,24 +25,23 @@ import java.util.List;
*/
public class XingHuoChatModelTests {
private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL_V2)
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)
.apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
.build())
.defaultOptions(DeepSeekChatOptions.builder()
// .model("generalv3.5") // 模型
.model("x1") // 模型
.temperature(0.7)
.build())
.build();
private static final String API_KEY = SystemUtil.get("XINGHUO_API_KEY",
"sk-xxxx"); // 按需改成你的讯飞星火 API Key
private static final String MODEL = SystemUtil.get("XINGHUO_MODEL",
XingHuoChatModel.MODEL_DEFAULT);
private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel);
private final XingHuoChatModel chatModel = XingHuoChatModel.builder()
.apiKey(API_KEY)
.options(DeepSeekChatOptions.builder()
.model(MODEL)
.temperature(0.7)
.build())
.build();
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -55,6 +56,7 @@ public class XingHuoChatModelTests {
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -69,11 +71,12 @@ public class XingHuoChatModelTests {
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model("x1")
.model(MODEL)
.build();
// 调用
@@ -81,7 +84,7 @@ public class XingHuoChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@@ -1,62 +1,76 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展
/**
* {@link QianFanChatModel} 的集成测试
* {@link YiYanChatModel} 的集成测试
*
* @author fansili
*/
public class YiYanChatModelTests {
private final QianFanChatModel chatModel = new QianFanChatModel(
new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥
QianFanChatOptions.builder()
.model("ERNIE-4.5-8K-Preview")
.build()
);
private static final String API_KEY = SystemUtil.get("YIYAN_API_KEY",
"sk-xxxx"); // 按需改成你的文心一言 API Key
private static final String MODEL = SystemUtil.get("YIYAN_MODEL",
YiYanChatModel.MODEL_DEFAULT);
private final YiYanChatModel chatModel = new YiYanChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(YiYanChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.build())
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
// TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
// TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名!
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ?"));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
}
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link ZhiPuAiChatModel} 的集成测试
* {@link ZhiPuChatModel} 的集成测试
*
* @author 芋道源码
*/
public class ZhiPuAiChatModelTests {
private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(
ZhiPuAiApi.builder().apiKey("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR").build(), // 密钥
ZhiPuAiChatOptions.builder()
.model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型
private static final String API_KEY = SystemUtil.get("ZHIPU_API_KEY",
"sk-xxxx"); // 按需改成你的智谱 API Key
private static final String MODEL = SystemUtil.get("ZHIPU_MODEL",
ZhiPuChatModel.MODEL_DEFAULT);
private final ZhiPuChatModel chatModel = new ZhiPuChatModel(DeepSeekChatModel.builder()
.deepSeekApi(DeepSeekApi.builder()
.baseUrl(ZhiPuChatModel.BASE_URL)
.apiKey(API_KEY)
.build())
.defaultOptions(DeepSeekChatOptions.builder()
.model(MODEL)
.build()
);
).build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests {
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}
@Test
@Disabled
public void testStream() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
@Test
@Disabled
public void testStream_thinking() {
validateApiKey(API_KEY);
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
.model("GLM-4.5")
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.model(MODEL)
.build();
// 调用
@@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests {
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response);
System.out.println(response.getResult().getOutput());
System.out.println(Objects.requireNonNull(response.getResult()).getOutput());
}).then().block();
}
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImageOptions;
@@ -9,6 +10,8 @@ import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.openai.api.OpenAiImageApi;
import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey;
/**
* {@link OpenAiImageModel} 集成测试类
*
@@ -16,17 +19,20 @@ import org.springframework.ai.openai.api.OpenAiImageApi;
*/
public class OpenAiImageModelTests {
private static final String API_KEY = SystemUtil.get("OPENAI_API_KEY", "sk-xxxx");
private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
.baseUrl("https://api.holdai.top") // apiKey
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD")
.apiKey(API_KEY)
.build());
@Test
@Disabled
public void testCall() {
validateApiKey(API_KEY);
// 准备参数
ImageOptions options = OpenAiImageOptions.builder()
.model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
.model("gpt-image-1")
.height(256).width(256)
.build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options);
@@ -1,43 +0,0 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;
// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 <https://github.com/spring-projects/spring-ai/issues/2179> 进展
/**
* {@link QianFanImageModel} 集成测试类
*/
public class QianFanImageTests {
private final QianFanImageModel imageModel = new QianFanImageModel(
new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥
@Test
@Disabled
public void testCall() {
// 准备参数
// 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576
QianFanImageOptions imageOptions = QianFanImageOptions.builder()
.model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())
.width(1024).height(1024)
.N(1)
.build();
ImagePrompt prompt = new ImagePrompt("good", imageOptions);
// 方法调用
ImageResponse response = imageModel.call(prompt);
// 打印结果
String b64Json = response.getResult().getOutput().getB64Json();
System.out.println(response);
viewImage(b64Json);
}
}
@@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
/**
* {@link ZhiPuAiImageModel} 集成测试
*/
public class ZhiPuAiImageModelTests {
private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel(
new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥
);
@Test
@Disabled
public void testCall() {
// 准备参数
ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder()
.model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue())
.build();
ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions);
// 方法调用
ImageResponse response = imageModel.call(prompt);
// 打印结果
System.out.println(response);
}
}
@@ -48,6 +48,10 @@ public interface ErrorCodeConstants {
ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_001_004_010, "主表(id={})定义不存在,请检查");
ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_001_004_011, "子表的字段(id={})不存在,请检查");
ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_001_004_012, "主表生成代码失败,原因:它没有子表");
ErrorCode CODEGEN_MASTER_TABLE_NAME_DUPLICATE = new ErrorCode(1_001_004_013,
"主子表规范化类名({})重复,请调整主表或子表类名");
ErrorCode CODEGEN_MASTER_TABLE_FIELD_DUPLICATE = new ErrorCode(1_001_004_014,
"主子表属性名({})重复,请调整主表字段、子表类名或关联关系");
// ========== 文件配置 1-001-006-000 ==========
ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在");
@@ -48,6 +48,9 @@ import java.util.*;
import static cn.hutool.core.map.MapUtil.getStr;
import static cn.hutool.core.text.CharSequenceUtil.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_FIELD_DUPLICATE;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_MASTER_TABLE_NAME_DUPLICATE;
/**
* 代码生成的引擎,用于具体生成代码
@@ -152,6 +155,16 @@ public class CodegenEngine {
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index_tree.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/breadcrumb_tree.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/breadcrumb.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_master.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_erp.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form_sub_erp.vue"),
vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCase}/form/index.vue"))
// VUE3_VBEN2_ANTD_SCHEMA
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -380,12 +393,17 @@ public class CodegenEngine {
// 2. 执行生成
Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
Map<String, String> generatedSources = Maps.newHashMapWithExpectedSize(templates.size());
templates.forEach((vmPath, filePath) -> {
// 2.1 特殊:主子表专属逻辑
if (isSubTemplate(vmPath)) {
generateSubCode(table, subTables, result, vmPath, filePath, bindingMap);
// 2.1 特殊:主子表、Uniapp 树表使用独立页面模版
if (isVue3AdminUniappTemplate(vmPath) && !matchVue3AdminUniappTemplate(table, vmPath)) {
return;
// 2.2 特殊:树表专属逻辑
}
// 2.2 特殊:主子表专属逻辑
if (isSubTemplate(vmPath)) {
generateSubCode(table, subTables, result, generatedSources, vmPath, filePath, bindingMap);
return;
// 2.3 特殊:树表专属逻辑
} else if (isPageReqVOTemplate(vmPath)) {
// 减少多余的类生成,例如说 PageVO.java 类
if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
@@ -402,23 +420,36 @@ public class CodegenEngine {
return;
}
}
// 2.3 默认生成
generateCode(result, vmPath, filePath, bindingMap);
// 2.4 默认生成
generateCode(result, generatedSources, vmPath, filePath, bindingMap);
});
return result;
}
private void generateCode(Map<String, String> result, String vmPath,
private void generateCode(Map<String, String> result, Map<String, String> generatedSources, String vmPath,
String filePath, Map<String, Object> bindingMap) {
filePath = formatFilePath(filePath, bindingMap);
registerGeneratedSource(generatedSources, filePath, vmPath, bindingMap);
String content = templateEngine.getTemplate(vmPath).render(bindingMap);
// 格式化代码
content = prettyCode(content, vmPath);
result.put(filePath, content);
}
@VisibleForTesting
static void registerGeneratedSource(Map<String, String> generatedSources, String filePath, String vmPath,
Map<String, Object> bindingMap) {
String source = vmPath
+ (bindingMap.containsKey("subIndex") ? "[subIndex=" + bindingMap.get("subIndex") + "]" : "");
String previousSource = generatedSources.putIfAbsent(filePath, source);
if (previousSource != null) {
throw new IllegalStateException(String.format("生成文件路径重复:%s,来源模板:%s、%s",
filePath, previousSource, source));
}
}
private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables,
Map<String, String> result, String vmPath,
Map<String, String> result, Map<String, String> generatedSources, String vmPath,
String filePath, Map<String, Object> bindingMap) {
// 没有子表,所以不生成
if (CollUtil.isEmpty(subTables)) {
@@ -441,7 +472,7 @@ public class CodegenEngine {
// 逐个生成
for (int i = 0; i < subTables.size(); i++) {
bindingMap.put("subIndex", i);
generateCode(result, vmPath, filePath, bindingMap);
generateCode(result, generatedSources, vmPath, filePath, bindingMap);
}
bindingMap.remove("subIndex");
}
@@ -482,6 +513,13 @@ public class CodegenEngine {
if (StrUtil.count(content, "DICT_TYPE.") == 0) {
content = StrUtils.removeLineContains(content, "DICT_TYPE");
}
// Vue3 Admin UniApp:清理空白行,并补充文件末尾换行,满足 ESLint 格式要求
if (vmPath.contains("vue3_admin_uniapp")) {
content = content.replaceAll("(?m)^[\\t ]+$", "");
if (!content.endsWith("\n")) {
content += "\n";
}
}
return content;
}
@@ -533,6 +571,14 @@ public class CodegenEngine {
List<String> subClassNameVars = new ArrayList<>();
List<String> simpleClassNameUnderlineCases = new ArrayList<>();
List<String> subSimpleClassNameStrikeCases = new ArrayList<>();
Set<String> masterSymbols = new HashSet<>();
masterSymbols.add(simpleClassName);
boolean mergeSubTableFields = ObjectUtils.equalsAny(table.getTemplateType(),
CodegenTemplateTypeEnum.MASTER_NORMAL.getType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
Set<String> masterFieldSymbols = new HashSet<>();
if (mergeSubTableFields) {
columns.forEach(column -> masterFieldSymbols.add(column.getJavaField()));
}
for (int i = 0; i < subTables.size(); i++) {
CodegenTableDO subTable = subTables.get(i);
List<CodegenColumnDO> subColumns = subColumnsList.get(i);
@@ -543,9 +589,18 @@ public class CodegenEngine {
subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type
// className 相关
String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName()));
if (!masterSymbols.add(subSimpleClassName)) {
throw exception(CODEGEN_MASTER_TABLE_NAME_DUPLICATE, subSimpleClassName);
}
String subClassNameVar = lowerFirst(subSimpleClassName);
String subFieldName = subClassNameVar
+ (Boolean.TRUE.equals(subTable.getSubJoinMany()) ? "s" : "");
if (mergeSubTableFields && !masterFieldSymbols.add(subFieldName)) {
throw exception(CODEGEN_MASTER_TABLE_FIELD_DUPLICATE, subFieldName);
}
subSimpleClassNames.add(subSimpleClassName);
simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type
subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量
subClassNameVars.add(subClassNameVar); // 将 DictType 转换成 dictType,用于变量
subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type
}
bindingMap.put("subPrimaryColumns", subPrimaryColumns);
@@ -734,6 +789,33 @@ public class CodegenEngine {
return path.contains("_sub");
}
private static boolean isVue3AdminUniappTemplate(String path) {
return path.contains("codegen/vue3_admin_uniapp/");
}
/**
* 匹配 Uniapp 不同表类型的专用页面模版
*/
private static boolean matchVue3AdminUniappTemplate(CodegenTableDO table, String path) {
boolean tree = CodegenTemplateTypeEnum.isTree(table.getTemplateType());
boolean masterSubmit = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())
|| ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
boolean masterErp = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType());
if (path.contains("_tree")) {
return tree;
}
if (path.contains("detail_master")) {
return masterSubmit;
}
if (path.contains("_erp")) {
return masterErp;
}
if (tree && path.endsWith("views/index.vue.vm")) {
return false;
}
return !(masterSubmit || masterErp) || !path.endsWith("views/detail/index.vue.vm");
}
private static boolean isPageReqVOTemplate(String path) {
return path.contains("pageReqVO");
}
@@ -1,36 +1,80 @@
#if ($table.templateType != 2)
import type { PageParam, PageResult } from '@/http/types'
#end
import { http } from '@/http/http'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || ($table.templateType != 11 && $column.id == $subJoinColumn.id))
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: Date
#elseif(${javaType} == "boolean")
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey)
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
#if ($table.templateType == 2)
children?: ${simpleClassName}[]
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
${subClassNameVar}#if($subTable.subJoinMany)s#end?: ${subSimpleClassName}#if($subTable.subJoinMany)[]#else | null#end
#end
#end
}
#if ($table.templateType == 2)
/** 获取${table.classComment}列表 */
export function get${simpleClassName}List(params?: Record<string, any>) {
return http.get<${simpleClassName}[]>('/${table.moduleName}/${simpleClassName_strikeCase}/list', params)
}
#else
/** 获取${table.classComment}分页列表 */
export function get${simpleClassName}Page(params: PageParam) {
return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
}
#end
/** 获取${table.classComment}详情 */
export function get${simpleClassName}(id: ${primaryTsType}) {
@@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) {
/** 创建${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}) {
return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
return http.post<${primaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
}
/** 更新${table.classComment} */
@@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) {
export function delete${simpleClassName}(id: ${primaryTsType}) {
return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`)
}
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
#set ($subPrimaryTsType = "number")
#else
#set ($subPrimaryTsType = "string")
#end
#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
#set ($subJoinTsType = "number")
#else
#set ($subJoinTsType = "string")
#end
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
// ==================== 子表:${subTable.classComment} ====================
#if ($table.templateType == 11)
/** 获取${subTable.classComment}分页列表 */
export function get${subSimpleClassName}Page(params: PageParam & { ${subJoinColumn.javaField}: ${subJoinTsType} }) {
return http.get<PageResult<${subSimpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/page', params)
}
/** 获取${subTable.classComment}详情 */
export function get${subSimpleClassName}(id: ${subPrimaryTsType}) {
return http.get<${subSimpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get?id=${id}`)
}
/** 创建${subTable.classComment} */
export function create${subSimpleClassName}(data: ${subSimpleClassName}) {
return http.post<${subPrimaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/create', data)
}
/** 更新${subTable.classComment} */
export function update${subSimpleClassName}(data: ${subSimpleClassName}) {
return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/update', data)
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: ${subPrimaryTsType}) {
return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/delete?id=${id}`)
}
#elseif ($subTable.subJoinMany)
/** 获取${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
return http.get<${subSimpleClassName}[]>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
}
#else
/** 获取${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
return http.get<${subSimpleClassName} | null>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
}
#end
#end
@@ -0,0 +1,87 @@
<template>
<view class="bg-white px-24rpx py-16rpx">
<scroll-view scroll-x class="whitespace-nowrap">
<view class="inline-flex items-center">
<view
class="flex items-center text-28rpx"
:class="items.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
@click="handleClick(-1)"
>
<text>全部${table.classComment}</text>
</view>
<template v-for="(item, index) in items" :key="item.id">
<wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
<view
class="flex items-center text-28rpx"
:class="index < items.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
@click="handleClick(index)"
>
<text>{{ item.name }}</text>
</view>
</template>
</view>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
interface BreadcrumbItem {
id: number
name: string
}
const props = defineProps<{
modelValue: number
}>()
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
const items = ref<BreadcrumbItem[]>([]) // 面包屑数据
/** 监听外部值变化 */
watch(() => props.modelValue, (value) => {
if (value === 0) {
items.value = []
}
})
/** 点击面包屑 */
function handleClick(index: number) {
if (index === -1) {
reset()
return
}
if (index < items.value.length - 1) {
items.value = items.value.slice(0, index + 1)
emit('update:modelValue', items.value[index].id)
}
}
/** 进入子层级 */
function enter(item: BreadcrumbItem) {
items.value.push(item)
emit('update:modelValue', item.id)
}
/** 返回上一层级 */
function back(): boolean {
if (items.value.length === 0) {
return false
}
items.value.pop()
emit('update:modelValue', items.value.at(-1)?.id ?? 0)
return true
}
/** 重置面包屑 */
function reset() {
items.value = []
emit('update:modelValue', 0)
}
defineExpose({ back, enter, reset })
</script>
@@ -145,7 +145,7 @@ const formData = reactive({
#else
${column.javaField}: undefined as number | undefined,
#end
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}: undefined as number | undefined,
#elseif(${javaType} == "boolean")
${column.javaField}: undefined as boolean | undefined,
@@ -174,7 +174,7 @@ const placeholder = computed(() => {
conditions.push(`${comment}:#[[${]]#getDictLabel(DICT_TYPE.${dictType.toUpperCase()}, formData.${javaField})#[[}]]#`)
}
#else
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
if (formData.${javaField} !== undefined) {
conditions.push(`${comment}:#[[${]]#formData.${javaField}#[[}]]#`)
}
@@ -39,7 +39,7 @@
</wd-button>
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
@@ -49,6 +49,12 @@
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#foreach($column in $columns)
#if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
@@ -64,6 +70,7 @@
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
@@ -76,7 +83,7 @@ import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{
id?: number | any
id?: ${primaryTsType}
}>()
definePage({
@@ -87,6 +94,7 @@ definePage({
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 详情数据
const deleting = ref(false) // 删除状态
@@ -117,28 +125,27 @@ function handleEdit() {
}
/** 删除${table.classComment} */
function handleDelete() {
async function handleDelete() {
if (!props.id) {
return
}
uni.showModal({
title: '提示',
content: '确定要删除该${table.classComment}吗?',
success: async (res) => {
if (!res.confirm) {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
},
})
try {
await dialog.confirm({
title: '提示',
msg: '确定要删除该${table.classComment}吗?',
})
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 初始化 */
@@ -0,0 +1,258 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar title="${table.classComment}详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="${table.classComment}信息">
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${column.columnComment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
#else
<wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
#set ($pageSize = 10)
#if (!$subTable.subJoinMany)
#set ($pageSize = 1)
#end
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}({{ ${subClassNameVar}Total }})</text>
<wd-button
#if (!$subTable.subJoinMany)
v-if="${subClassNameVar}Total === 0 && hasAccessByCodes(['${permissionPrefix}:create'])"
#else
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
#end
size="small" type="primary" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="${subClassNameVar}PagingRef"
v-model="${subClassNameVar}List"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="${pageSize}"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无${subTable.classComment}"
@query="query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List"
>
<view
v-for="item in ${subClassNameVar}List"
:key="item.${subPrimaryColumn.javaField}"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
</view>
#end
#end
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
#end
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button v-if="hasAccessByCodes(['${permissionPrefix}:update'])" class="flex-1" type="warning" @click="handleEdit">
编辑
</wd-button>
<wd-button v-if="hasAccessByCodes(['${permissionPrefix}:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#if ($column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type {
${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { onShow } from '@dcloudio/uni-app'
import { nextTick, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
delete${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
delete${subSimpleClassName},
#end
get${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
get${subSimpleClassName}Page,
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{ id?: ${primaryTsType} }>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 主表数据
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
const ${subClassNameVar}List = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}列表
const ${subClassNameVar}Total = ref(0) // ${subTable.classComment}总数
const ${subClassNameVar}PagingRef = ref<any>() // ${subTable.classComment}分页组件引用
#end
const deleting = ref(false) // 主表删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载主表 */
async function loadMain() {
if (props.id) {
formData.value = await get${simpleClassName}(props.id)
}
}
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
/** 查询${subTable.classComment}列表 */
async function query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List(pageNo: number, pageSize: number) {
if (!props.id) {
${subClassNameVar}PagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await get${subSimpleClassName}Page({ ${subJoinColumn.javaField}: props.id, pageNo, pageSize })
${subClassNameVar}Total.value = data.total
${subClassNameVar}PagingRef.value?.completeByTotal(data.list, data.total)
} catch {
${subClassNameVar}PagingRef.value?.complete(false)
}
}
/** 打开${subTable.classComment}表单 */
function open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item?: ${subSimpleClassName}) {
const query = [
`${subJoinColumn.javaField}=#[[${]]#props.id#[[}]]#`,
item?.${subPrimaryColumn.javaField} ? `id=#[[${]]#item.${subPrimaryColumn.javaField}#[[}]]#` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCases.get($index)}/form/index?#[[${]]#query#[[}]]#` })
}
/** 删除${subTable.classComment} */
async function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item: ${subSimpleClassName}) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${subTable.classComment}吗?' })
} catch {
return
}
await delete${subSimpleClassName}(item.${subPrimaryColumn.javaField}!)
toast.success('删除成功')
${subClassNameVar}PagingRef.value?.reload()
}
#end
/** 编辑主表 */
function handleEdit() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
}
/** 删除主表 */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑和子表返回后刷新) */
onShow(() => {
loadMain()
nextTick(() => {
#foreach ($subClassNameVar in $subClassNameVars)
${subClassNameVar}PagingRef.value?.reload()
#end
})
})
</script>
@@ -0,0 +1,239 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="${table.classComment}信息">
#foreach($column in $columns)
#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
#if ($column.dictType && "" != $column.dictType)
<wd-cell title="${column.columnComment}">
<dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
</wd-cell>
#elseif ($column.javaType == "LocalDateTime")
<wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
#else
<wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
#end
#end
#end
</wd-cell-group>
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
${subTable.classComment}#if($subTable.subJoinMany)({{ ${subClassNameVar}s.length }})#end
</view>
<view class="px-24rpx">
#if ($subTable.subJoinMany)
<view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view
v-for="item in ${subClassNameVar}s"
:key="item.${subPrimaryColumn.javaField}"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
</view>
#end
#end
</view>
#else
<view class="rounded-12rpx bg-white p-24rpx shadow-sm">
<template v-if="${subClassNameVar}">
#foreach ($column in $subColumns)
#if ($column.listOperationResult && !$column.primaryKey)
<view class="mb-8rpx text-28rpx text-[#666]">
${column.columnComment}:{{ ${subClassNameVar}.${column.javaField} ?? '-' }}
</view>
#end
#end
</template>
<view v-else class="text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
</view>
#end
</view>
#end
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
class="flex-1" type="warning" @click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasDateTime = 0)
#foreach($column in $columns)
#if ($column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#end
#if ($column.javaType == "LocalDateTime")
#set ($hasDateTime = 1)
#end
#end
import type {
${simpleClassName},
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
delete${simpleClassName},
get${simpleClassName},
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName},
#else
get${subSimpleClassName}By${SubJoinColumnName},
#end
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDateTime == 1)
import { formatDateTime } from '@/utils/date'
#end
const props = defineProps<{
id?: ${primaryTsType}
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<${simpleClassName}>() // 主表数据
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
const ${subClassNameVar}s = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}数据
#else
const ${subClassNameVar} = ref<${subSimpleClassName} | null>(null) // ${subTable.classComment}数据
#end
#end
const deleting = ref(false) // 删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
}
/** 加载主表和子表详情 */
async function getDetail() {
if (!props.id || deleting.value) {
return
}
const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
get${simpleClassName}(props.id),
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
#else
get${subSimpleClassName}By${SubJoinColumnName}(props.id),
#end
#end
])
formData.value = data
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
${subClassNameVar}s.value = ${subClassNameVar}Data || []
#else
${subClassNameVar}.value = ${subClassNameVar}Data || null
#end
#end
}
/** 编辑${table.classComment} */
function handleEdit() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
}
/** 删除${table.classComment} */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
} catch {
return
}
deleting.value = true
try {
await delete${simpleClassName}(props.id)
toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑返回后刷新) */
onShow(() => {
getDetail()
})
</script>
@@ -12,34 +12,48 @@
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多
#if ($column.htmlType == "select" && $dictType && "" != $dictType)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#set ($comment = $column.columnComment)
#set ($dictMethod = "getDictOptions")
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
## 树表的父级选择
#if ($table.templateType == 2 && $column.id == $treeParentColumn.id)
<yd-tree-select
v-model="formData.${javaField}"
:data="treeData"
label="${comment}"
prop="${javaField}"
:props="{ label: '${treeNameColumn.javaField}', value: '${primaryColumn.javaField}' }"
placeholder="请选择${comment}"
/>
## 下拉选择(字典)→ yd-form-picker,内部按 dictType 解析选项,适合选项较多
#elseif ($column.htmlType == "select" && $dictType && "" != $dictType)
<yd-form-picker
v-model="formData.${javaField}"
label="${comment}"
label-width="180rpx"
prop="${javaField}"
#if ($javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${dictType.toUpperCase()}"
#if ($javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${comment}"
/>
## 单选(字典)→ radio-group,适合选项较少
#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
## 单选(字典)→ radio-group,适合选项较少
#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-radio-group v-model="formData.${javaField}" type="button">
<wd-radio
@@ -51,70 +65,78 @@
</wd-radio>
</wd-radio-group>
</wd-form-item>
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
## 数字类型(无字典)
#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-input-number
v-model="formData.${javaField}"
#if (${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
#if ($column.nullable)
allow-null
#end
#if (${javaType.toLowerCase()} == "float" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
## 布尔类型
#elseif (${javaType.toLowerCase()} == "boolean")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
<wd-switch v-model="formData.${javaField}" />
</wd-form-item>
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
## 日期时间类型
#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "date")
#if (${javaType.toLowerCase()} == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-form-item
title="${comment}"
title-width="180rpx"
prop="${javaField}"
is-link
#if ($pickerType == "datetime")
:value="formatDateTime(formData.${javaField})"
:value="formData.${javaField} ? formatDateTime(formData.${javaField}) : ''"
#else
:value="formatDate(formData.${javaField})"
:value="formData.${javaField} ? formatDate(formData.${javaField}) : ''"
#end
placeholder="请选择${comment}"
@click="${javaField}Visible = true"
/>
<wd-datetime-picker
v-model="formData.${javaField}"
v-model:visible="${javaField}Visible"
:model-value="formData.${javaField} || Date.now()"
type="${pickerType}"
@update:model-value="value => formData.${javaField} = value"
/>
## 文本域
#elseif ($column.htmlType == "textarea")
## 文本域
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-textarea
v-model="formData.${javaField}"
clearable
placeholder="请输入${comment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
show-word-limit
/>
</wd-form-item>
## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)
#elseif ($column.htmlType == "imageUpload")
## 图片上传 → yd-upload-img(包 wd-form-item 才能注册到 wd-form 做校验/错误展示)
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<yd-upload-img v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
## 文件上传 → yd-upload-file
#elseif ($column.htmlType == "fileUpload")
## 文件上传 → yd-upload-file
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<yd-upload-file v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
<yd-upload-file v-model="formData.${javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
## 默认:文本输入
#else
## 默认:文本输入
#else
<wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
<wd-input
v-model="formData.${javaField}"
@@ -122,11 +144,277 @@
placeholder="请输入${comment}"
/>
</wd-form-item>
#end
#end
#end
#end
#end
</wd-cell-group>
</wd-form>
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
<!-- 子表:${subTable.classComment} -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}</text>
#if ($subTable.subJoinMany)
<wd-button size="small" type="primary" variant="plain" @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}">
添加
</wd-button>
#else
<wd-button
v-if="!${subClassNameVar}"
size="small"
type="primary"
variant="plain"
@click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
>
添加
</wd-button>
#end
</view>
<view class="px-24rpx">
#if ($subTable.subJoinMany)
<view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view
v-for="(item, index) in ${subClassNameVar}s"
:key="index"
class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-16rpx flex justify-end">
<wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index)">
删除
</wd-button>
</view>
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#set ($dictMethod = "getDictOptions")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($column.javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($column.javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="item.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-radio-group v-model="item.${column.javaField}" type="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
#elseif ($javaType == "boolean")
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-switch v-model="item.${column.javaField}" />
</wd-form-item>
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input-number
v-model="item.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($subPickerType = "date")
#if ($javaType == "localdatetime")
#set ($subPickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
is-link
#if ($javaType == "localdatetime")
:value="item.${column.javaField} ? formatDateTime(item.${column.javaField}) : ''"
#else
:value="item.${column.javaField} ? formatDate(item.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`]"
:model-value="item.${column.javaField} || Date.now()"
type="${subPickerType}"
@update:model-value="value => item.${column.javaField} = value"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-textarea
v-model="item.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
</wd-form-item>
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-img v-model="item.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-file v-model="item.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input v-model="item.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
</wd-form-item>
#end
#end
#end
</view>
#else
<view v-if="!${subClassNameVar}" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
暂无${subTable.classComment}
</view>
<view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#set ($dictMethod = "getDictOptions")
#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
#set ($dictMethod = "getIntDictOptions")
#elseif ($column.javaType == "String")
#set ($dictMethod = "getStrDictOptions")
#elseif ($column.javaType == "Boolean")
#set ($dictMethod = "getBoolDictOptions")
#end
#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="${subClassNameVar}.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-radio-group v-model="${subClassNameVar}.${column.javaField}" type="button">
<wd-radio
v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
#elseif ($javaType == "boolean")
<wd-form-item title="${column.columnComment}" title-width="180rpx" center>
<wd-switch v-model="${subClassNameVar}.${column.javaField}" />
</wd-form-item>
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input-number
v-model="${subClassNameVar}.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
</wd-form-item>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($subPickerType = "date")
#if ($javaType == "localdatetime")
#set ($subPickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
is-link
#if ($javaType == "localdatetime")
:value="${subClassNameVar}.${column.javaField} ? formatDateTime(${subClassNameVar}.${column.javaField}) : ''"
#else
:value="${subClassNameVar}.${column.javaField} ? formatDate(${subClassNameVar}.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="subDateVisible['${subClassNameVar}.${column.javaField}'] = true"
/>
<wd-datetime-picker
v-model:visible="subDateVisible['${subClassNameVar}.${column.javaField}']"
:model-value="${subClassNameVar}.${column.javaField} || Date.now()"
type="${subPickerType}"
@update:model-value="value => ${subClassNameVar}.${column.javaField} = value"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-textarea
v-model="${subClassNameVar}.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
</wd-form-item>
#elseif ($column.htmlType == "imageUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-img v-model="${subClassNameVar}.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#elseif ($column.htmlType == "fileUpload")
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<yd-upload-file v-model="${subClassNameVar}.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
</wd-form-item>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx">
<wd-input v-model="${subClassNameVar}.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
</wd-form-item>
#end
#end
#end
</view>
#end
</view>
#end
#end
</view>
<!-- 底部保存按钮 -->
@@ -146,7 +434,7 @@
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
@@ -154,54 +442,113 @@ import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
#set ($hasDict = 0)
#set ($hasDate = 0)
#set ($hasDateTime = 0)
#set ($hasSubDate = 0)
#set ($hasGetDictOptions = 0)
#set ($hasGetIntDictOptions = 0)
#set ($hasGetStrDictOptions = 0)
#set ($hasGetBoolDictOptions = 0)
#foreach ($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subColumns in $subColumnsList)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ($column.htmlType == "select" || $column.htmlType == "radio")
&& $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
#set ($hasGetIntDictOptions = 1)
#elseif ($column.htmlType == "radio" && $column.javaType == "String")
#set ($hasGetStrDictOptions = 1)
#elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $column.javaType == "Boolean")
#set ($hasGetBoolDictOptions = 1)
#elseif ($column.htmlType == "radio")
#set ($hasGetDictOptions = 1)
#end
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
#set ($hasDate = 1)
#set ($hasSubDate = 1)
#end
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
&& ${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#set ($hasSubDate = 1)
#end
#end
#end
#end
import type {
${simpleClassName},
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subSimpleClassName in $subSimpleClassNames)
${subSimpleClassName},
#end
#end
} from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
#if ($table.templateType == 10 || $table.templateType == 12)
import {
create${simpleClassName},
get${simpleClassName},
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName},
#else
get${subSimpleClassName}By${SubJoinColumnName},
#end
#end
update${simpleClassName},
} from '@/api/${table.moduleName}/${table.businessName}'
#elseif ($table.templateType == 2)
import { create${simpleClassName}, get${simpleClassName}, get${simpleClassName}List, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#else
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
#end
#if ($hasGetDictOptions == 1 || $hasGetIntDictOptions == 1 || $hasGetStrDictOptions == 1 || $hasGetBoolDictOptions == 1)
#set ($dictImportNames = "")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
#set ($dictImportNames = "")
#if ($hasGetDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
#end
#if ($hasGetIntDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
#end
#if ($hasGetStrDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
#end
#if ($hasGetBoolDictOptions == 1)
#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
#end
#set ($dictImportNames = $dictImportNames.trim())
#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
import { $dictImportNames } from '@/hooks/useDict'
#end
import { delay, navigateBackPlus } from '@/utils'
@@ -209,21 +556,27 @@ import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDate == 1 || $hasDateTime == 1)
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
import { $dateImportNames } from '@/utils/date'
#end
import { createFormSchema } from '@/utils/wot'
#if ($table.templateType == 2)
import { handleTree } from '@/utils/tree'
#end
const props = defineProps<{
id?: ${primaryTsType} | any
id?: ${primaryTsType}
#if ($table.templateType == 2)
${treeParentColumn.javaField}?: ${primaryTsType}
#end
}>()
definePage({
@@ -236,42 +589,165 @@ definePage({
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
const formLoading = ref(false) // 表单提交状态
const formData = ref<${simpleClassName}>({
const formData = ref<Partial<${simpleClassName}>>({
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if ($column.primaryKey)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if ($column.primaryKey)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#elseif($table.templateType == 2 && $column.id == $treeParentColumn.id)
${column.javaField}: props.${column.javaField} ?? 0,
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
${column.javaField}: undefined,
#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "float" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
#if($column.nullable)
${column.javaField}: undefined,
#else
${column.javaField}: 0,
#elseif(${javaType} == "boolean")
#end
#elseif(${javaType} == "boolean")
${column.javaField}: false,
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}: undefined,
#else
#else
${column.javaField}: '',
#end
#end
#end
#end
#end
}) // 表单数据
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
const ${subClassNameVar}s = ref<Partial<${subSimpleClassName}>[]>([]) // ${subTable.classComment}数据
#else
const ${subClassNameVar} = ref<Partial<${subSimpleClassName}> | null>(null) // ${subTable.classComment}数据
#end
#end
#end
#foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
#end
#end
#end
#if ($hasSubDate == 1)
const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
#end
const formSchema = createFormSchema({
#foreach($column in $columns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end
#end
#end
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
#if ($table.templateType == 2)
const treeData = ref<${simpleClassName}[]>([]) // 父级树形选项
#end
#if ($table.templateType == 10 || $table.templateType == 12)
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
/** 添加${subTable.classComment} */
function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
${subClassNameVar}s.value.push({
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
})
}
/** 删除${subTable.classComment} */
function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index: number) {
${subClassNameVar}s.value.splice(index, 1)
}
#else
/** 添加${subTable.classComment} */
function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
${subClassNameVar}.value = {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
}
}
#end
#end
/** 校验主子表数据 */
function isEmptySubValue(value: unknown) {
return value === undefined || value === null || value === ''
}
function validateSubTables() {
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subColumns = $subColumnsList.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#if ($subTable.subJoinMany)
for (let index = 0; index < ${subClassNameVar}s.value.length; index++) {
const item = ${subClassNameVar}s.value[index]!
#foreach ($column in $subColumns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
if (isEmptySubValue(item.${column.javaField})) {
toast.warning(`${subTable.classComment}第#[[${]]#index + 1#[[}]]#项的${column.columnComment}不能为空`)
return false
}
#end
#end
}
#else
if (${subClassNameVar}.value) {
#foreach ($column in $subColumns)
#set ($javaFieldLower = $column.javaField.toLowerCase())
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
&& $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
if (isEmptySubValue(${subClassNameVar}.value.${column.javaField})) {
toast.warning('${subTable.classComment}的${column.columnComment}不能为空')
return false
}
#end
#end
}
#end
#end
return true
}
#end
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
@@ -282,7 +758,34 @@ async function getDetail() {
if (!props.id) {
return
}
#if ($table.templateType == 10 || $table.templateType == 12)
const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
get${simpleClassName}(props.id),
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn = $subJoinColumns.get($index))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
#if ($subTable.subJoinMany)
get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
#else
get${subSimpleClassName}By${SubJoinColumnName}(props.id),
#end
#end
])
formData.value = data
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany)
${subClassNameVar}s.value = ${subClassNameVar}Data || []
#else
${subClassNameVar}.value = ${subClassNameVar}Data || null
#end
#end
#else
formData.value = await get${simpleClassName}(props.id)
#end
}
/** 提交表单 */
@@ -291,14 +794,31 @@ async function handleSubmit() {
if (!valid) {
return
}
#if ($table.templateType == 10 || $table.templateType == 12)
if (!validateSubTables()) {
return
}
#end
formLoading.value = true
try {
#if ($table.templateType == 10 || $table.templateType == 12)
const data = {
...formData.value,
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
${subClassNameVar}#if($subTable.subJoinMany)s#end: ${subClassNameVar}#if($subTable.subJoinMany)s#end.value,
#end
} as ${simpleClassName}
#else
const data = formData.value as ${simpleClassName}
#end
if (props.id) {
await update${simpleClassName}(formData.value)
await update${simpleClassName}(data)
toast.success('修改成功')
} else {
await create${simpleClassName}(formData.value)
await create${simpleClassName}(data)
toast.success('新增成功')
}
uni.$emit('${table.moduleName}:${table.businessName}:reload')
@@ -309,7 +829,16 @@ async function handleSubmit() {
}
/** 初始化 */
onMounted(() => {
onMounted(async () => {
#if ($table.templateType == 2)
const data = await get${simpleClassName}List()
treeData.value = [{
${primaryColumn.javaField}: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
${treeParentColumn.javaField}: 0,
children: handleTree(data, '${primaryColumn.javaField}', '${treeParentColumn.javaField}'),
} as ${simpleClassName}]
#end
getDetail()
})
</script>
@@ -0,0 +1,250 @@
#set ($subTable = $subTables.get($subIndex))
#set ($subColumns = $subColumnsList.get($subIndex))
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex))
#set ($subJoinColumn = $subJoinColumns.get($subIndex))
#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "float" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
#set ($subPrimaryTsType = "number")
#else
#set ($subPrimaryTsType = "string")
#end
#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "float" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
#set ($subJoinTsType = "number")
#else
#set ($subJoinTsType = "string")
#end
#set ($hasDict = 0)
#set ($hasBoolDict = 0)
#set ($hasDate = 0)
#set ($hasDateTime = 0)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
#set ($hasDict = 1)
#if ($column.javaType == "Boolean")
#set ($hasBoolDict = 1)
#end
#end
#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate")
#set ($hasDate = 1)
#end
#if (${column.javaType.toLowerCase()} == "localdatetime")
#set ($hasDateTime = 1)
#end
#end
#end
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($javaType = $column.javaType.toLowerCase())
#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
<yd-form-picker
v-model="formData.${column.javaField}"
label="${column.columnComment}"
label-width="180rpx"
prop="${column.javaField}"
#if ($column.javaType == "Boolean")
:columns="getBoolDictOptions(DICT_TYPE.${column.dictType.toUpperCase()})"
#else
:dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
#if ($column.javaType == "String")
dict-kind="str"
#end
#end
placeholder="请选择${column.columnComment}"
/>
#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
#set ($pickerType = "date")
#if ($javaType == "localdatetime")
#set ($pickerType = "datetime")
#end
<wd-form-item
title="${column.columnComment}"
title-width="180rpx"
prop="${column.javaField}"
is-link
#if ($javaType == "localdatetime")
:value="formData.${column.javaField} ? formatDateTime(formData.${column.javaField}) : ''"
#else
:value="formData.${column.javaField} ? formatDate(formData.${column.javaField}) : ''"
#end
placeholder="请选择${column.columnComment}"
@click="${column.javaField}Visible = true"
/>
<wd-datetime-picker
v-model:visible="${column.javaField}Visible"
:model-value="formData.${column.javaField} || Date.now()"
type="${pickerType}"
@update:model-value="value => formData.${column.javaField} = value"
/>
#else
<wd-form-item title="${column.columnComment}" title-width="180rpx" prop="${column.javaField}">
#if ($javaType == "boolean")
<wd-switch v-model="formData.${column.javaField}" />
#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
<wd-input-number
v-model="formData.${column.javaField}"
#if($column.nullable)
allow-null
#end
#if ($javaType == "float" || $javaType == "double" || $javaType == "bigdecimal")
:precision="2"
input-type="number"
#end
:min="0"
/>
#elseif ($column.htmlType == "textarea" || $column.htmlType == "editor")
<wd-textarea
v-model="formData.${column.javaField}"
clearable
placeholder="请输入${column.columnComment}"
#if ($column.htmlType == "editor")
:maxlength="2000"
#else
:maxlength="200"
#end
/>
#elseif ($column.htmlType == "imageUpload")
<yd-upload-img v-model="formData.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
#elseif ($column.htmlType == "fileUpload")
<yd-upload-file v-model="formData.${column.javaField}" :limit="1" directory="${table.moduleName}/${table.businessName}" />
#else
<wd-input v-model="formData.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
#end
</wd-form-item>
#end
#end
#end
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { ${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { create${subSimpleClassName}, get${subSimpleClassName}, update${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { delay, navigateBackPlus } from '@/utils'
#if ($hasBoolDict == 1)
import { getBoolDictOptions } from '@/hooks/useDict'
#end
#if ($hasDict == 1)
import { DICT_TYPE } from '@/utils/constants'
#end
#if ($hasDate == 1 || $hasDateTime == 1)
#set ($dateImportNames = "")
#if ($hasDate == 1)
#set ($dateImportNames = "${dateImportNames}formatDate, ")
#end
#if ($hasDateTime == 1)
#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
#end
#set ($dateImportNames = $dateImportNames.trim())
#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
import { $dateImportNames } from '@/utils/date'
#end
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: ${subPrimaryTsType}
${subJoinColumn.javaField}: ${subJoinTsType}
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${subTable.classComment}' : '新增${subTable.classComment}')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<${subSimpleClassName}>>({
#foreach ($column in $subColumns)
#if ($column.primaryKey)
${column.javaField}: undefined,
#elseif ($column.id == $subJoinColumn.id)
${column.javaField}: props.${column.javaField},
#elseif ($column.createOperation || $column.updateOperation)
#set ($javaType = $column.javaType.toLowerCase())
#if((($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType) || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
${column.javaField}: undefined,
#else
${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "float" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte") 0#elseif($javaType == "boolean") false#else ''#end,
#end
#end
#end
}) // 表单数据
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id
&& (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
#end
#end
const formSchema = createFormSchema({
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey)
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end
#end
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载${subTable.classComment}详情 */
async function getDetail() {
if (props.id) {
formData.value = await get${subSimpleClassName}(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
${subJoinColumn.javaField}: props.${subJoinColumn.javaField} ?? formData.value.${subJoinColumn.javaField},
} as ${subSimpleClassName}
if (props.id) {
await update${subSimpleClassName}(data)
toast.success('修改成功')
} else {
await create${subSimpleClassName}(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -0,0 +1,151 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="${table.classComment}管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 面包屑导航 -->
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
<!-- ${table.classComment}列表 -->
<view class="p-24rpx">
<view
v-for="item in currentList"
:key="item.${primaryColumn.javaField}"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
>
<view class="p-24rpx" @click="handleDetail(item)">
<view class="flex items-center justify-between">
<view class="flex min-w-0 items-center">
<view class="mr-16rpx h-48rpx w-48rpx shrink-0 flex items-center justify-center rounded-8rpx bg-[#1890ff]">
<wd-icon name="folder" size="20px" color="#fff" />
</view>
<view class="truncate text-32rpx text-[#333] font-semibold">
{{ item.${treeNameColumn.javaField} }}
</view>
</view>
<view
v-if="item.children?.length"
class="ml-16rpx shrink-0 flex items-center"
@click.stop="handleEnterChildren(item)"
>
<text class="text-24rpx text-[#1890ff]">子级 ({{ item.children.length }})</text>
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
<wd-empty icon="content" tip="暂无${table.classComment}数据" />
</view>
</view>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "float" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onUnload } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { get${simpleClassName}List } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { findChildren, handleTree } from '@/utils/tree'
import Breadcrumb from './components/breadcrumb.vue'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const loading = ref(false) // 列表加载状态
const list = ref<${simpleClassName}[]>([]) // 完整树形列表
const currentParentId = ref<${primaryTsType}>(0) // 当前层级的父节点编号
const currentList = computed(() => currentParentId.value === 0
? list.value
: findChildren(list.value, currentParentId.value, '${primaryColumn.javaField}')) // 当前层级列表
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>() // 面包屑组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页或上一层级 */
function handleBack() {
if (!breadcrumbRef.value?.back()) {
navigateBackPlus()
}
}
/** 进入子层级 */
function handleEnterChildren(item: ${simpleClassName}) {
breadcrumbRef.value?.enter({ id: item.${primaryColumn.javaField}!, name: item.${treeNameColumn.javaField} })
}
/** 查询${table.classComment}列表 */
async function getList() {
loading.value = true
try {
list.value = handleTree(await get${simpleClassName}List(queryParams.value), '${primaryColumn.javaField}', '${treeParentColumn.javaField}')
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
currentParentId.value = 0
breadcrumbRef.value?.reset()
getList()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 新增${table.classComment} */
function handleAdd() {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?${treeParentColumn.javaField}=#[[${]]#currentParentId.value#[[}]]#` })
}
/** 查看详情 */
function handleDetail(item: ${simpleClassName}) {
uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#` })
}
/** 初始化 */
onMounted(() => {
getList()
uni.$on('${table.moduleName}:${table.businessName}:reload', getList)
})
/** 卸载 */
onUnload(() => {
uni.$off('${table.moduleName}:${table.businessName}:reload', getList)
})
</script>
@@ -157,13 +157,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
String lastFilePath = StrUtil.subAfter(filePath, '/', true);
String ext = StrUtil.subAfter(lastFilePath, '.', true);
String name = StrUtil.subBefore(lastFilePath, '.', true);
String contentPath = ext + '/' + name;
// 同名文件(如 index.vue 同时出现在 列表/form/detail)会撞名,撞名时前缀补上级目录区分;仍撞则加序号兜底,避免快照互相覆盖
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 parentDir = StrUtil.subAfter(StrUtil.subBefore(filePath, '/' + lastFilePath, true), '/', true);
contentPath = ext + '/' + parentDir + '/' + name;
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 + '/' + parentDir + '/' + name + '-' + i;
contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name + '-' + i;
}
}
usedContentPaths.add(contentPath);
@@ -1,5 +1,6 @@
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;
@@ -7,10 +8,14 @@ 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;
/**
@@ -59,4 +64,315 @@ public class CodegenEngineUniappTest extends CodegenEngineAbstractTest {
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,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" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
}, {
"contentPath" : "vue/student-contact/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"
}, {
"contentPath" : "vue/student-teacher/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-teacher/form/index.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,125 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 学生联系人信息 */
export interface StudentContact {
id?: number
studentId: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生班主任信息 */
export interface StudentTeacher {
id?: number
studentId: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生信息 */
export interface Student {
id?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video: string
memo: string
createTime?: number
}
/** 获取学生分页列表 */
export function getStudentPage(params: PageParam) {
return http.get<PageResult<Student>>('/infra/student/page', params)
}
/** 获取学生详情 */
export function getStudent(id: number) {
return http.get<Student>(`/infra/student/get?id=${id}`)
}
/** 创建学生 */
export function createStudent(data: Student) {
return http.post<number>('/infra/student/create', data)
}
/** 更新学生 */
export function updateStudent(data: Student) {
return http.put<boolean>('/infra/student/update', data)
}
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete<boolean>(`/infra/student/delete?id=${id}`)
}
// ==================== 子表:学生联系人 ====================
/** 获取学生联系人分页列表 */
export function getStudentContactPage(params: PageParam & { studentId: number }) {
return http.get<PageResult<StudentContact>>('/infra/student/student-contact/page', params)
}
/** 获取学生联系人详情 */
export function getStudentContact(id: number) {
return http.get<StudentContact>(`/infra/student/student-contact/get?id=${id}`)
}
/** 创建学生联系人 */
export function createStudentContact(data: StudentContact) {
return http.post<number>('/infra/student/student-contact/create', data)
}
/** 更新学生联系人 */
export function updateStudentContact(data: StudentContact) {
return http.put<boolean>('/infra/student/student-contact/update', data)
}
/** 删除学生联系人 */
export function deleteStudentContact(id: number) {
return http.delete<boolean>(`/infra/student/student-contact/delete?id=${id}`)
}
// ==================== 子表:学生班主任 ====================
/** 获取学生班主任分页列表 */
export function getStudentTeacherPage(params: PageParam & { studentId: number }) {
return http.get<PageResult<StudentTeacher>>('/infra/student/student-teacher/page', params)
}
/** 获取学生班主任详情 */
export function getStudentTeacher(id: number) {
return http.get<StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`)
}
/** 创建学生班主任 */
export function createStudentTeacher(data: StudentTeacher) {
return http.post<number>('/infra/student/student-teacher/create', data)
}
/** 更新学生班主任 */
export function updateStudentTeacher(data: StudentTeacher) {
return http.put<boolean>('/infra/student/student-teacher/update', data)
}
/** 删除学生班主任 */
export function deleteStudentTeacher(id: number) {
return http.delete<boolean>(`/infra/student/student-teacher/delete?id=${id}`)
}
@@ -0,0 +1,342 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar title="学生详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<view class="pb-160rpx">
<!-- 主表详情 -->
<wd-cell-group border title="学生信息">
<wd-cell title="编号" :value="formData?.id ?? '-'" />
<wd-cell title="名字" :value="formData?.name ?? '-'" />
<wd-cell title="简介" :value="formData?.description ?? '-'" />
<wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
<wd-cell title="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
</wd-cell>
<wd-cell title="是否有效">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
</wd-cell>
<wd-cell title="头像" :value="formData?.avatar ?? '-'" />
<wd-cell title="附件" :value="formData?.video ?? '-'" />
<wd-cell title="备注" :value="formData?.memo ?? '-'" />
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
</wd-cell-group>
<!-- 子表:学生联系人 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生联系人({{ studentContactTotal }})</text>
<wd-button
v-if="hasAccessByCodes(['infra:student:create'])"
size="small" type="primary" variant="plain" @click="openStudentContact()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="studentContactPagingRef"
v-model="studentContactList"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="10"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无学生联系人"
@query="queryStudentContactList"
>
<view
v-for="item in studentContactList"
:key="item.id"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ item.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ item.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ item.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ item.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ item.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ item.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ item.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ item.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ item.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ item.createTime ?? '-' }}
</view>
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="openStudentContact(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
<!-- 子表:学生班主任 -->
<view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
<text class="text-28rpx text-[#333] font-semibold">学生班主任({{ studentTeacherTotal }})</text>
<wd-button
v-if="studentTeacherTotal === 0 && hasAccessByCodes(['infra:student:create'])"
size="small" type="primary" variant="plain" @click="openStudentTeacher()"
>
添加
</wd-button>
</view>
<view class="px-24rpx">
<z-paging
ref="studentTeacherPagingRef"
v-model="studentTeacherList"
:fixed="false"
:auto="false"
height="560rpx"
:default-page-size="1"
:refresher-enabled="false"
:inside-more="true"
:to-bottom-loading-more-enabled="false"
loading-more-default-text="点击加载更多"
empty-view-text="暂无学生班主任"
@query="queryStudentTeacherList"
>
<view
v-for="item in studentTeacherList"
:key="item.id"
class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
>
<view class="mb-8rpx text-28rpx text-[#666]">
学生编号:{{ item.studentId ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
名字:{{ item.name ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
简介:{{ item.description ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
出生日期:{{ item.birthday ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
性别:{{ item.sex ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
是否有效:{{ item.enabled ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
头像:{{ item.avatar ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
附件:{{ item.video ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
备注:{{ item.memo ?? '-' }}
</view>
<view class="mb-8rpx text-28rpx text-[#666]">
创建时间:{{ item.createTime ?? '-' }}
</view>
<view class="mt-16rpx flex justify-end gap-16rpx">
<wd-button size="small" variant="plain" @click="openStudentTeacher(item)">
编辑
</wd-button>
<wd-button size="small" type="danger" variant="plain" @click="removeStudentTeacher(item)">
删除
</wd-button>
</view>
</view>
</z-paging>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button v-if="hasAccessByCodes(['infra:student:update'])" class="flex-1" type="warning" @click="handleEdit">
编辑
</wd-button>
<wd-button v-if="hasAccessByCodes(['infra:student:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import type {
Student,
StudentContact,
StudentTeacher,
} from '@/api/infra/demo'
import { onShow } from '@dcloudio/uni-app'
import { nextTick, ref } from 'vue'
import { useDialog } from '@wot-ui/ui/components/wd-dialog'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import {
deleteStudent,
deleteStudentContact,
deleteStudentTeacher,
getStudent,
getStudentContactPage,
getStudentTeacherPage,
} from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{ id?: number }>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const { hasAccessByCodes } = useAccess()
const dialog = useDialog()
const toast = useToast()
const formData = ref<Student>() // 主表数据
const studentContactList = ref<StudentContact[]>([]) // 学生联系人列表
const studentContactTotal = ref(0) // 学生联系人总数
const studentContactPagingRef = ref<any>() // 学生联系人分页组件引用
const studentTeacherList = ref<StudentTeacher[]>([]) // 学生班主任列表
const studentTeacherTotal = ref(0) // 学生班主任总数
const studentTeacherPagingRef = ref<any>() // 学生班主任分页组件引用
const deleting = ref(false) // 主表删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载主表 */
async function loadMain() {
if (props.id) {
formData.value = await getStudent(props.id)
}
}
/** 查询学生联系人列表 */
async function queryStudentContactList(pageNo: number, pageSize: number) {
if (!props.id) {
studentContactPagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await getStudentContactPage({ studentId: props.id, pageNo, pageSize })
studentContactTotal.value = data.total
studentContactPagingRef.value?.completeByTotal(data.list, data.total)
} catch {
studentContactPagingRef.value?.complete(false)
}
}
/** 打开学生联系人表单 */
function openStudentContact(item?: StudentContact) {
const query = [
`studentId=${props.id}`,
item?.id ? `id=${item.id}` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-infra/demo/student-contact/form/index?${query}` })
}
/** 删除学生联系人 */
async function removeStudentContact(item: StudentContact) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生联系人吗?' })
} catch {
return
}
await deleteStudentContact(item.id!)
toast.success('删除成功')
studentContactPagingRef.value?.reload()
}
/** 查询学生班主任列表 */
async function queryStudentTeacherList(pageNo: number, pageSize: number) {
if (!props.id) {
studentTeacherPagingRef.value?.completeByTotal([], 0)
return
}
try {
const data = await getStudentTeacherPage({ studentId: props.id, pageNo, pageSize })
studentTeacherTotal.value = data.total
studentTeacherPagingRef.value?.completeByTotal(data.list, data.total)
} catch {
studentTeacherPagingRef.value?.complete(false)
}
}
/** 打开学生班主任表单 */
function openStudentTeacher(item?: StudentTeacher) {
const query = [
`studentId=${props.id}`,
item?.id ? `id=${item.id}` : '',
].filter(Boolean).join('&')
uni.navigateTo({ url: `/pages-infra/demo/student-teacher/form/index?${query}` })
}
/** 删除学生班主任 */
async function removeStudentTeacher(item: StudentTeacher) {
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生班主任吗?' })
} catch {
return
}
await deleteStudentTeacher(item.id!)
toast.success('删除成功')
studentTeacherPagingRef.value?.reload()
}
/** 编辑主表 */
function handleEdit() {
uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
}
/** 删除主表 */
async function handleDelete() {
if (!props.id) {
return
}
try {
await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
} catch {
return
}
deleting.value = true
try {
await deleteStudent(props.id)
toast.success('删除成功')
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
deleting.value = false
}
}
/** 进入页面加载(含编辑和子表返回后刷新) */
onShow(() => {
loadMain()
nextTick(() => {
studentContactPagingRef.value?.reload()
studentTeacherPagingRef.value?.reload()
})
})
</script>
@@ -0,0 +1,193 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input
v-model="formData.name"
clearable
placeholder="请输入名字"
/>
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
show-word-limit
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
<wd-radio-group v-model="formData.enabled" type="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
show-word-limit
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type {
Student,
} from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudent, getStudent, updateStudent } from '@/api/infra/demo'
import { getBoolDictOptions } from '@/hooks/useDict'
import { delay, navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<Student>>({
id: undefined,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: '',
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
video: [{ required: true, message: '附件不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载学生详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await getStudent(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = formData.value as Student
if (props.id) {
await updateStudent(data)
toast.success('修改成功')
} else {
await createStudent(data)
toast.success('新增成功')
}
uni.$emit('infra:demo:reload')
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,169 @@
<template>
<view class="yd-page-container yd-page-container-paging">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 学生列表 -->
<z-paging
ref="pagingRef"
v-model="list"
:fixed="false"
class="min-h-0 flex-1"
:default-page-size="10"
:refresher-enabled="true"
:inside-more="true"
:loading-more-default-as-loading="true"
empty-view-text="暂无学生数据"
@query="queryList"
>
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.name }}
</view>
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">简介:</text>
<text class="line-clamp-1">{{ item.description }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">出生日期:</text>
<text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">是否有效:</text>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">头像:</text>
<text class="line-clamp-1">{{ item.avatar }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">附件:</text>
<text class="line-clamp-1">{{ item.video }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">备注:</text>
<text class="line-clamp-1">{{ item.memo }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">创建时间:</text>
<text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
</view>
</view>
</view>
</z-paging>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['infra:student:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { getStudentPage } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const list = ref<Student[]>([]) // 列表数据
const pagingRef = ref<any>() // 分页组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询学生列表 */
async function queryList(pageNo: number, pageSize: number) {
try {
const params = {
...queryParams.value,
pageNo,
pageSize,
}
const data = await getStudentPage(params)
pagingRef.value?.completeByTotal(data.list, data.total)
} catch {
pagingRef.value?.complete(false)
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
reload()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 重新加载 */
function reload() {
pagingRef.value?.reload()
}
/** 新增学生 */
function handleAdd() {
uni.navigateTo({
url: '/pages-infra/demo/form/index',
})
}
/** 查看详情 */
function handleDetail(item: Student) {
uni.navigateTo({
url: `/pages-infra/demo/detail/index?id=${item.id}`,
})
}
/** 初始化 */
onMounted(() => {
uni.$on('infra:demo:reload', reload)
})
/** 卸载 */
onUnload(() => {
uni.$off('infra:demo:reload', reload)
})
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,116 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
名字
</view>
<wd-input
v-model="formData.name"
placeholder="请输入名字"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
出生日期
</view>
<wd-input
v-model="formData.birthday"
placeholder="请输入出生日期"
clearable
/>
</view>
<yd-search-picker
v-model="formData.sex"
label="性别"
:columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
all-option
/>
<yd-search-picker
v-model="formData.enabled"
label="是否有效"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
all-option
/>
<yd-search-date-range v-model="formData.createTime" label="创建时间" />
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
import { formatDate, formatDateRange } from '@/utils/date'
import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({
name: undefined as string | undefined,
birthday: undefined as string | undefined,
sex: undefined as number | undefined,
enabled: undefined as boolean | undefined,
createTime: [undefined, undefined] as [number | undefined, number | undefined],
}) // 搜索表单数据
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
if (formData.name) {
conditions.push(`名字:${formData.name}`)
}
if (formData.birthday) {
conditions.push(`出生日期:${formData.birthday}`)
}
if (formData.sex !== undefined) {
conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
}
if (formData.enabled !== undefined) {
conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
})
/** 搜索按钮操作 */
function handleSearch() {
visible.value = false
emit('search', {
...formData,
createTime: formatDateRange(formData.createTime),
})
}
/** 重置按钮操作 */
function handleReset() {
formData.name = undefined
formData.birthday = undefined
formData.sex = undefined
formData.enabled = undefined
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
}
</script>
@@ -0,0 +1,167 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input v-model="formData.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<yd-form-picker
v-model="formData.enabled"
label="是否有效"
label-width="180rpx"
prop="enabled"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
placeholder="请选择是否有效"
/>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { StudentContact } from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudentContact, getStudentContact, updateStudentContact } from '@/api/infra/demo'
import { delay, navigateBackPlus } from '@/utils'
import { getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
studentId: number
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生联系人' : '新增学生联系人')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<StudentContact>>({
id: undefined,
studentId: props.studentId,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
studentId: [{ required: true, message: '学生编号不能为空' }],
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载学生联系人详情 */
async function getDetail() {
if (props.id) {
formData.value = await getStudentContact(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
studentId: props.studentId ?? formData.value.studentId,
} as StudentContact
if (props.id) {
await updateStudentContact(data)
toast.success('修改成功')
} else {
await createStudentContact(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -0,0 +1,167 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
<!-- 表单区域 -->
<wd-form ref="formRef" :model="formData" :schema="formSchema">
<wd-cell-group border>
<wd-form-item title="名字" title-width="180rpx" prop="name">
<wd-input v-model="formData.name" clearable placeholder="请输入名字" />
</wd-form-item>
<wd-form-item title="简介" title-width="180rpx" prop="description">
<wd-textarea
v-model="formData.description"
clearable
placeholder="请输入简介"
:maxlength="200"
/>
</wd-form-item>
<wd-form-item
title="出生日期"
title-width="180rpx"
prop="birthday"
is-link
:value="formData.birthday ? formatDateTime(formData.birthday) : ''"
placeholder="请选择出生日期"
@click="birthdayVisible = true"
/>
<wd-datetime-picker
v-model:visible="birthdayVisible"
:model-value="formData.birthday || Date.now()"
type="datetime"
@update:model-value="value => formData.birthday = value"
/>
<yd-form-picker
v-model="formData.sex"
label="性别"
label-width="180rpx"
prop="sex"
:dict-type="DICT_TYPE.SYSTEM_USER_SEX"
placeholder="请选择性别"
/>
<yd-form-picker
v-model="formData.enabled"
label="是否有效"
label-width="180rpx"
prop="enabled"
:columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
placeholder="请选择是否有效"
/>
<wd-form-item title="头像" title-width="180rpx" prop="avatar">
<yd-upload-img v-model="formData.avatar" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="附件" title-width="180rpx" prop="video">
<yd-upload-file v-model="formData.video" :limit="1" directory="infra/demo" />
</wd-form-item>
<wd-form-item title="备注" title-width="180rpx" prop="memo">
<wd-textarea
v-model="formData.memo"
clearable
placeholder="请输入备注"
:maxlength="2000"
/>
</wd-form-item>
</wd-cell-group>
</wd-form>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
import type { StudentTeacher } from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from '@wot-ui/ui/components/wd-toast'
import { createStudentTeacher, getStudentTeacher, updateStudentTeacher } from '@/api/infra/demo'
import { delay, navigateBackPlus } from '@/utils'
import { getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import { createFormSchema } from '@/utils/wot'
const props = defineProps<{
id?: number
studentId: number
}>()
definePage({
style: { navigationBarTitleText: '', navigationStyle: 'custom' },
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生班主任' : '新增学生班主任')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Partial<StudentTeacher>>({
id: undefined,
studentId: props.studentId,
name: '',
description: '',
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: '',
video: undefined,
memo: '',
}) // 表单数据
const birthdayVisible = ref(false) // 出生日期选择器状态
const formSchema = createFormSchema({
studentId: [{ required: true, message: '学生编号不能为空' }],
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
}) // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 加载学生班主任详情 */
async function getDetail() {
if (props.id) {
formData.value = await getStudentTeacher(props.id)
}
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
const data = {
...formData.value,
studentId: props.studentId ?? formData.value.studentId,
} as StudentTeacher
if (props.id) {
await updateStudentTeacher(data)
toast.success('修改成功')
} else {
await createStudentTeacher(data)
toast.success('新增成功')
}
delay(handleBack)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
@@ -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" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.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,87 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 学生联系人信息 */
export interface StudentContact {
id?: number
studentId?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生班主任信息 */
export interface StudentTeacher {
id?: number
studentId?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video?: string
memo: string
createTime?: number
}
/** 学生信息 */
export interface Student {
id?: number
name: string
description: string
birthday: number
sex: number
enabled: boolean
avatar: string
video: string
memo: string
createTime?: number
studentContacts?: StudentContact[]
studentTeacher?: StudentTeacher | null
}
/** 获取学生分页列表 */
export function getStudentPage(params: PageParam) {
return http.get<PageResult<Student>>('/infra/student/page', params)
}
/** 获取学生详情 */
export function getStudent(id: number) {
return http.get<Student>(`/infra/student/get?id=${id}`)
}
/** 创建学生 */
export function createStudent(data: Student) {
return http.post<number>('/infra/student/create', data)
}
/** 更新学生 */
export function updateStudent(data: Student) {
return http.put<boolean>('/infra/student/update', data)
}
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete<boolean>(`/infra/student/delete?id=${id}`)
}
// ==================== 子表:学生联系人 ====================
/** 获取学生联系人列表 */
export function getStudentContactListByStudentId(studentId: number) {
return http.get<StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`)
}
// ==================== 子表:学生班主任 ====================
/** 获取学生班主任 */
export function getStudentTeacherByStudentId(studentId: number) {
return http.get<StudentTeacher | null>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`)
}

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