mirror of
https://gitee.com/smallc/blade-tool.git
synced 2026-08-25 04:31:17 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df67adcf3e | ||
|
|
583fd83b88 | ||
|
|
9bceec2067 | ||
|
|
3d93b3d587 |
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Release-V5.0.0-blue.svg" alt="Downloads">
|
<img src="https://img.shields.io/badge/Release-V5.0.1-blue.svg" alt="Downloads">
|
||||||
<img src="https://img.shields.io/badge/JDK-21+-green.svg" alt="Build Status">
|
<img src="https://img.shields.io/badge/JDK-21+-green.svg" alt="Build Status">
|
||||||
<img src="https://img.shields.io/badge/license-LGPL%20v3-blue.svg" alt="Build Status">
|
<img src="https://img.shields.io/badge/license-LGPL%20v3-blue.svg" alt="Build Status">
|
||||||
<img src="https://img.shields.io/badge/Spring%20Cloud-2025.1-blue.svg" alt="Coverage Status">
|
<img src="https://img.shields.io/badge/Spring%20Cloud-2025.1-blue.svg" alt="Coverage Status">
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ mybatis-plus:
|
||||||
swagger:
|
swagger:
|
||||||
title: SpringBlade 接口文档系统
|
title: SpringBlade 接口文档系统
|
||||||
description: SpringBlade 接口文档系统
|
description: SpringBlade 接口文档系统
|
||||||
version: 5.0.0
|
version: 5.0.1
|
||||||
license: Powered By SpringBlade
|
license: Powered By SpringBlade
|
||||||
licenseUrl: https://bladex.vip
|
licenseUrl: https://bladex.vip
|
||||||
terms-of-service-url: https://bladex.vip
|
terms-of-service-url: https://bladex.vip
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,16 @@
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--
|
||||||
|
Jackson 3 库本体。Spring Cloud 与 Sentinel 的多个组件已按「Jackson 3 在场」编写条件装配,
|
||||||
|
缺席时的表现从静默不注册(Sentinel 规则数据源转换器)到容器启动失败(Feign 分页模型扩展、
|
||||||
|
Sentinel 熔断规则刷新监听器)不等,且都不属于本工程可修的范围。此处只引入库,
|
||||||
|
不引入 spring-boot-jackson 自动配置模块,序列化管线仍由 Jackson 2 承载,机制见父 POM 的门禁注释。
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>tools.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- Admin -->
|
<!-- Admin -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.codecentric</groupId>
|
<groupId>de.codecentric</groupId>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ public class SecureUtil {
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public static boolean isAdministrator() {
|
public static boolean isAdministrator() {
|
||||||
return StringUtil.containsAny(getUserRole(), RoleConstant.ADMINISTRATOR);
|
return hasRole(RoleConstant.ADMINISTRATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,7 +183,17 @@ public class SecureUtil {
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public static boolean isAdmin() {
|
public static boolean isAdmin() {
|
||||||
return StringUtil.containsAny(getUserRole(), RoleConstant.ADMIN);
|
return hasRole(RoleConstant.ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定当前会话是否持有指定角色
|
||||||
|
*
|
||||||
|
* @param role 角色别名
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
private static boolean hasRole(String role) {
|
||||||
|
return CollectionUtil.contains(Func.toStrArray(getUserRole()), role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package org.springblade.core.tool.jackson;
|
package org.springblade.core.tool.jackson;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
|
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
|
@ -32,12 +33,18 @@ import java.time.LocalTime;
|
||||||
/**
|
/**
|
||||||
* java 8 时间默认序列化
|
* java 8 时间默认序列化
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
|
* 仅覆盖 LocalDateTime、LocalDate、LocalTime 的格式,其余类型由官方 {@link JavaTimeModule} 处理。
|
||||||
|
* 模块名须显式声明:SimpleModule 以 Version 的 artifactId 为模块标识,沿用 jsr310 的 PackageVersion
|
||||||
|
* 会与官方模块同名,被去重逻辑判为重复注册而互相顶替。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
* @author L.cm
|
* @author L.cm
|
||||||
*/
|
*/
|
||||||
public class BladeJavaTimeModule extends SimpleModule {
|
public class BladeJavaTimeModule extends SimpleModule {
|
||||||
|
|
||||||
public BladeJavaTimeModule() {
|
public BladeJavaTimeModule() {
|
||||||
super(PackageVersion.VERSION);
|
super("BladeJavaTimeModule", PackageVersion.VERSION);
|
||||||
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtil.DATETIME_FORMAT));
|
this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtil.DATETIME_FORMAT));
|
||||||
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtil.DATE_FORMAT));
|
this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtil.DATE_FORMAT));
|
||||||
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtil.TIME_FORMAT));
|
this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtil.TIME_FORMAT));
|
||||||
|
|
|
||||||
|
|
@ -979,12 +979,12 @@ public class JsonUtil {
|
||||||
super.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
|
super.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
|
||||||
//序列化时,日期的统一格式
|
//序列化时,日期的统一格式
|
||||||
super.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA));
|
super.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA));
|
||||||
//日期格式化
|
|
||||||
super.registerModule(new BladeJavaTimeModule());
|
|
||||||
//视图注解内省器,让 Jackson 识别 @BladeView
|
//视图注解内省器,让 Jackson 识别 @BladeView
|
||||||
super.setAnnotationIntrospector(new BladeViewAnnotationIntrospector());
|
super.setAnnotationIntrospector(new BladeViewAnnotationIntrospector());
|
||||||
//注册模块
|
//注册模块
|
||||||
super.findAndRegisterModules();
|
super.findAndRegisterModules();
|
||||||
|
//日期格式化,须后于官方模块注册以覆盖其格式
|
||||||
|
super.registerModule(new BladeJavaTimeModule());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ public class CodeGenerator {
|
||||||
/**
|
/**
|
||||||
* 基础业务字段
|
* 基础业务字段
|
||||||
*/
|
*/
|
||||||
public static String[] SUPER_ENTITY_COLUMNS = {"create_time", "create_user", "update_time", "update_user", "status", "is_deleted"};
|
public static String[] SUPER_ENTITY_COLUMNS = {"create_time", "create_user", "create_dept", "update_time", "update_user", "status", "is_deleted"};
|
||||||
/**
|
/**
|
||||||
* 是否包含包装器
|
* 是否包含包装器
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package org.springblade.develop.support;
|
package org.springblade.develop.support;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||||
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
|
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
|
||||||
import com.baomidou.mybatisplus.generator.config.OutputFile;
|
import com.baomidou.mybatisplus.generator.config.OutputFile;
|
||||||
import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
|
import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
|
||||||
|
import com.baomidou.mybatisplus.generator.config.builder.Entity;
|
||||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||||
import com.baomidou.mybatisplus.generator.config.rules.DateType;
|
import com.baomidou.mybatisplus.generator.config.rules.DateType;
|
||||||
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
|
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
|
||||||
|
|
@ -148,25 +150,33 @@ public class BladeCodeGenerator {
|
||||||
customFile.put("wrapper.java", "/templates/api/wrapper.java.vm");
|
customFile.put("wrapper.java", "/templates/api/wrapper.java.vm");
|
||||||
}
|
}
|
||||||
if (Func.isNotBlank(packageWebDir)) {
|
if (Func.isNotBlank(packageWebDir)) {
|
||||||
customFile.put("api.js", "/templates/saber/api.js.vm");
|
customFile.put("api.ts", "/templates/saber/api.ts.vm");
|
||||||
customFile.put("crud.vue", "/templates/saber/crud.vue.vm");
|
customFile.put("crud.vue", "/templates/saber/crud.vue.vm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无基础业务字段时实体不再继承 BaseEntity,服务层须同步降级为 MyBatis-Plus 原生接口(3.5.17 起位于 spring 包)
|
||||||
|
String superServiceClass = hasSuperEntity ? "org.springblade.core.mp.base.BaseService" : "com.baomidou.mybatisplus.spring.service.IService";
|
||||||
|
String superServiceImplClass = hasSuperEntity ? "org.springblade.core.mp.base.BaseServiceImpl" : "com.baomidou.mybatisplus.spring.service.impl.ServiceImpl";
|
||||||
|
|
||||||
FastAutoGenerator.create(url, username, password)
|
FastAutoGenerator.create(url, username, password)
|
||||||
.globalConfig(builder -> builder.author(props.getProperty("author")).dateType(DateType.TIME_PACK).enableSwagger().outputDir(getOutputDir()).disableOpenDir())
|
.globalConfig(builder -> builder.author(props.getProperty("author")).dateType(DateType.TIME_PACK).enableSwagger().outputDir(getOutputDir()).disableOpenDir())
|
||||||
.packageConfig(builder -> builder.parent(packageName).controller("controller").entity("pojo.entity").service("service").serviceImpl("service.impl").mapper("mapper").xml("mapper"))
|
.packageConfig(builder -> builder.parent(packageName).controller("controller").entity("pojo.entity").service("service").serviceImpl("service.impl").mapper("mapper").xml("mapper"))
|
||||||
.strategyConfig(builder -> builder.addTablePrefix(tablePrefix).addInclude(includeTables).addExclude(excludeTables)
|
.strategyConfig(builder -> {
|
||||||
.entityBuilder().naming(NamingStrategy.underline_to_camel).columnNaming(NamingStrategy.underline_to_camel).enableLombok().superClass("org.springblade.core.mp.base.BaseEntity").addSuperEntityColumns(superEntityColumns).enableFileOverride()
|
Entity.Builder entityBuilder = builder.addTablePrefix(tablePrefix).addInclude(includeTables).addExclude(excludeTables)
|
||||||
.javaTemplate("/templates/api/entity.java.vm")
|
.entityBuilder().naming(NamingStrategy.underline_to_camel).columnNaming(NamingStrategy.underline_to_camel).enableLombok().idType(IdType.ASSIGN_ID).enableFileOverride()
|
||||||
.serviceBuilder().superServiceClass("org.springblade.core.mp.base.BaseService").superServiceImplClass("org.springblade.core.mp.base.BaseServiceImpl").formatServiceFileName("I%sService").formatServiceImplFileName("%sServiceImpl").enableFileOverride()
|
.javaTemplate("/templates/api/entity.java.vm");
|
||||||
|
if (hasSuperEntity) {
|
||||||
|
entityBuilder.superClass("org.springblade.core.mp.base.BaseEntity").addSuperEntityColumns(superEntityColumns);
|
||||||
|
}
|
||||||
|
entityBuilder.serviceBuilder().superServiceClass(superServiceClass).superServiceImplClass(superServiceImplClass).formatServiceFileName("I%sService").formatServiceImplFileName("%sServiceImpl").enableFileOverride()
|
||||||
.serviceTemplate("/templates/api/service.java.vm")
|
.serviceTemplate("/templates/api/service.java.vm")
|
||||||
.serviceImplTemplate("/templates/api/serviceImpl.java.vm")
|
.serviceImplTemplate("/templates/api/serviceImpl.java.vm")
|
||||||
.mapperBuilder().mapperAnnotation(Mapper.class).enableBaseResultMap().enableBaseColumnList().formatMapperFileName("%sMapper").formatXmlFileName("%sMapper").enableFileOverride()
|
.mapperBuilder().mapperAnnotation(Mapper.class).enableBaseResultMap().enableBaseColumnList().formatMapperFileName("%sMapper").formatXmlFileName("%sMapper").enableFileOverride()
|
||||||
.mapperTemplate("/templates/api/mapper.java.vm")
|
.mapperTemplate("/templates/api/mapper.java.vm")
|
||||||
.mapperXmlTemplate("/templates/api/mapper.xml.vm")
|
.mapperXmlTemplate("/templates/api/mapper.xml.vm")
|
||||||
.controllerBuilder().superClass("org.springblade.core.boot.ctrl.BladeController").formatFileName("%sController").enableRestStyle().enableHyphenStyle().enableFileOverride()
|
.controllerBuilder().superClass("org.springblade.core.boot.ctrl.BladeController").formatFileName("%sController").enableRestStyle().enableHyphenStyle().enableFileOverride()
|
||||||
.template("/templates/api/controller.java.vm")
|
.template("/templates/api/controller.java.vm");
|
||||||
)
|
})
|
||||||
.injectionConfig(builder -> builder.beforeOutputFile(
|
.injectionConfig(builder -> builder.beforeOutputFile(
|
||||||
(tableInfo, objectMap) -> System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size())
|
(tableInfo, objectMap) -> System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size())
|
||||||
).customMap(customMap).customFile(customFile)
|
).customMap(customMap).customFile(customFile)
|
||||||
|
|
@ -202,8 +212,8 @@ public class BladeCodeGenerator {
|
||||||
outputPath = getOutputDir() + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "wrapper" + StringPool.SLASH + entityName + "Wrapper" + StringPool.DOT_JAVA;
|
outputPath = getOutputDir() + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "wrapper" + StringPool.SLASH + entityName + "Wrapper" + StringPool.DOT_JAVA;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtil.equals(key, "api.js")) {
|
if (StringUtil.equals(key, "api.ts")) {
|
||||||
outputPath = getOutputWebDir() + StringPool.SLASH + "api" + StringPool.SLASH + servicePackage.toLowerCase() + StringPool.SLASH + entityNameLower + ".js";
|
outputPath = getOutputWebDir() + StringPool.SLASH + "api" + StringPool.SLASH + servicePackage.toLowerCase() + StringPool.SLASH + entityNameLower + ".ts";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtil.equals(key, "crud.vue")) {
|
if (StringUtil.equals(key, "crud.vue")) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||||
* <p>
|
* <p>
|
||||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
* <p>
|
* <p>
|
||||||
* http://www.gnu.org/licenses/lgpl.html
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
* <p>
|
* <p>
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
@ -15,35 +15,30 @@
|
||||||
*/
|
*/
|
||||||
package $!{package.Controller};
|
package $!{package.Controller};
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import jakarta.validation.Valid;
|
#if($!{superControllerClassPackage})
|
||||||
|
import $!{superControllerClassPackage};
|
||||||
|
#end
|
||||||
import org.springblade.core.mp.support.Condition;
|
import org.springblade.core.mp.support.Condition;
|
||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.swagger.annotation.ApiOrder;
|
import org.springblade.core.swagger.annotation.ApiOrder;
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
#if($!{superEntityClass})
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
#end
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import $!{package.Entity}.$!{entity};
|
import $!{package.Entity}.$!{entity};
|
||||||
#set($voPackage=$package.Entity.replace("entity","vo"))
|
#set($voPackage=$package.Entity.replace("entity","vo"))
|
||||||
import $!{voPackage}.$!{entity}VO;
|
import $!{voPackage}.$!{entity}VO;
|
||||||
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
|
|
||||||
#if($!{hasWrapper})
|
#if($!{hasWrapper})
|
||||||
|
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
|
||||||
import $!{wrapperPackage}.$!{entity}Wrapper;
|
import $!{wrapperPackage}.$!{entity}Wrapper;
|
||||||
#end
|
#end
|
||||||
import $!{package.Service}.$!{table.serviceName};
|
import $!{package.Service}.$!{table.serviceName};
|
||||||
#if($!{superControllerClassPackage})
|
import org.springframework.web.bind.annotation.*;
|
||||||
import $!{superControllerClassPackage};
|
|
||||||
#end
|
import jakarta.validation.Valid;
|
||||||
#if(!$!{superEntityClass})
|
|
||||||
#end
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $!{table.comment} 控制器
|
* $!{table.comment} 控制器
|
||||||
|
|
@ -84,7 +79,6 @@ public class $!{table.controllerName} {
|
||||||
IPage<$!{entity}> pages = $!{table.entityPath}Service.page(Condition.getPage(query), Condition.getQueryWrapper($!{table.entityPath}));
|
IPage<$!{entity}> pages = $!{table.entityPath}Service.page(Condition.getPage(query), Condition.getQueryWrapper($!{table.entityPath}));
|
||||||
return R.data($!{entity}Wrapper.build().pageVO(pages));
|
return R.data($!{entity}Wrapper.build().pageVO(pages));
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
/**
|
/**
|
||||||
* 详情
|
* 详情
|
||||||
|
|
@ -144,8 +138,7 @@ public class $!{table.controllerName} {
|
||||||
return R.status($!{table.entityPath}Service.saveOrUpdate($!{table.entityPath}));
|
return R.status($!{table.entityPath}Service.saveOrUpdate($!{table.entityPath}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#if($!{superEntityClass})
|
#if($!{superEntityClass})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除 $!{table.comment}
|
* 删除 $!{table.comment}
|
||||||
*/
|
*/
|
||||||
|
|
@ -155,8 +148,7 @@ public class $!{table.controllerName} {
|
||||||
return R.status($!{table.entityPath}Service.deleteLogic(Func.toLongList(ids)));
|
return R.status($!{table.entityPath}Service.deleteLogic(Func.toLongList(ids)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除 $!{table.comment}
|
* 删除 $!{table.comment}
|
||||||
*/
|
*/
|
||||||
|
|
@ -166,6 +158,5 @@ public class $!{table.controllerName} {
|
||||||
return R.status($!{table.entityPath}Service.removeByIds(Func.toLongList(ids)));
|
return R.status($!{table.entityPath}Service.removeByIds(Func.toLongList(ids)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#end
|
#end
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,21 @@
|
||||||
|
#set($hasTenant = false)
|
||||||
|
#set($hasKeyField = false)
|
||||||
|
#set($hasLongKey = false)
|
||||||
|
#foreach($field in $!{table.fields})
|
||||||
|
#if($!{field.name} == $!{tenantColumn})
|
||||||
|
#set($hasTenant = true)
|
||||||
|
#end
|
||||||
|
#if($!{field.keyFlag})
|
||||||
|
#set($hasKeyField = true)
|
||||||
|
#end
|
||||||
|
#if($!{field.keyFlag} && $!{field.propertyType} == "Long")
|
||||||
|
#set($hasLongKey = true)
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
#set($useTenantEntity = false)
|
||||||
|
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
|
||||||
|
#set($useTenantEntity = true)
|
||||||
|
#end
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -16,8 +34,20 @@
|
||||||
package $!{package.Entity};
|
package $!{package.Entity};
|
||||||
|
|
||||||
#foreach($pkg in $!{table.importPackages})
|
#foreach($pkg in $!{table.importPackages})
|
||||||
|
#if($useTenantEntity && $!{pkg} == "org.springblade.core.mp.base.BaseEntity")
|
||||||
|
import org.springblade.core.mp.base.TenantEntity;
|
||||||
|
#elseif($!{pkg} == "java.io.Serializable" && $!{superEntityClass} && !$!{activeRecord})
|
||||||
|
## 继承父类时不直接实现 Serializable,跳过强制注入的导入
|
||||||
|
#elseif(!$hasKeyField && ($!{pkg} == "com.baomidou.mybatisplus.annotation.IdType" || $!{pkg} == "com.baomidou.mybatisplus.annotation.TableId"))
|
||||||
|
## 主键列归入公共字段时实体内不产出主键注解,跳过全局 idType 注入的导入
|
||||||
|
#else
|
||||||
import $!{pkg};
|
import $!{pkg};
|
||||||
#end
|
#end
|
||||||
|
#end
|
||||||
|
#if($hasLongKey)
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
#end
|
||||||
#if($!{entityLombokModel})
|
#if($!{entityLombokModel})
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
@ -46,7 +76,7 @@ import java.io.Serial;
|
||||||
@Schema(description = #if ("$!{table.comment}"=="")"$!{entity}对象"#else"$!{table.comment}"#end)
|
@Schema(description = #if ("$!{table.comment}"=="")"$!{entity}对象"#else"$!{table.comment}"#end)
|
||||||
#end
|
#end
|
||||||
#if($!{superEntityClass})
|
#if($!{superEntityClass})
|
||||||
public class $!{entity} extends $!{superEntityClass}#if($!{activeRecord})<$!{entity}>#end {
|
public class $!{entity} extends #if($useTenantEntity)TenantEntity#{else}$!{superEntityClass}#end#if($!{activeRecord})<$!{entity}>#end {
|
||||||
#elseif($!{activeRecord})
|
#elseif($!{activeRecord})
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class $!{entity} extends Model<$!{entity}> {
|
public class $!{entity} extends Model<$!{entity}> {
|
||||||
|
|
@ -59,7 +89,7 @@ public class $!{entity} implements Serializable {
|
||||||
|
|
||||||
## ---------- BEGIN 字段循环遍历 ----------
|
## ---------- BEGIN 字段循环遍历 ----------
|
||||||
#foreach($field in $!{table.fields})
|
#foreach($field in $!{table.fields})
|
||||||
#if($!{field.name}!=$!{tenantColumn})
|
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
|
||||||
#if($!{field.keyFlag})
|
#if($!{field.keyFlag})
|
||||||
#set($keyPropertyName=$!{field.propertyName})
|
#set($keyPropertyName=$!{field.propertyName})
|
||||||
#end
|
#end
|
||||||
|
|
@ -80,6 +110,9 @@ public class $!{entity} implements Serializable {
|
||||||
#elseif($!{field.convert})
|
#elseif($!{field.convert})
|
||||||
@TableId("$!{field.name}")
|
@TableId("$!{field.name}")
|
||||||
#end
|
#end
|
||||||
|
#if($!{field.propertyType} == "Long")
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
#end
|
||||||
## 普通字段
|
## 普通字段
|
||||||
#elseif($!{field.fill})
|
#elseif($!{field.fill})
|
||||||
## ----- 存在字段填充设置 -----
|
## ----- 存在字段填充设置 -----
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package $!{package.Mapper};
|
package $!{package.Mapper};
|
||||||
|
|
||||||
|
import $!{superMapperClassPackage};
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import $!{package.Entity}.$!{entity};
|
import $!{package.Entity}.$!{entity};
|
||||||
#set($voPackage=$package.Entity.replace("entity","vo"))
|
#set($voPackage=$package.Entity.replace("entity","vo"))
|
||||||
import $!{voPackage}.$!{entity}VO;
|
import $!{voPackage}.$!{entity}VO;
|
||||||
import $!{superMapperClassPackage};
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
#set($hasTenant = false)
|
||||||
|
#foreach($field in $!{table.fields})
|
||||||
|
#if($!{field.name} == $!{tenantColumn})
|
||||||
|
#set($hasTenant = true)
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
#set($useTenantEntity = false)
|
||||||
|
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
|
||||||
|
#set($useTenantEntity = true)
|
||||||
|
#end
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="$!{package.Mapper}.$!{table.mapperName}">
|
<mapper namespace="$!{package.Mapper}.$!{table.mapperName}">
|
||||||
|
|
@ -19,7 +29,7 @@
|
||||||
<result column="$!{field.name}" property="$!{field.propertyName}"/>
|
<result column="$!{field.name}" property="$!{field.propertyName}"/>
|
||||||
#end
|
#end
|
||||||
#foreach($field in $!{table.fields})
|
#foreach($field in $!{table.fields})
|
||||||
#if(!$!{field.keyFlag} && $!{field.name} != $!{tenantColumn})##生成普通字段
|
#if(!$!{field.keyFlag} && ($!{field.name} != $!{tenantColumn} || !$useTenantEntity))##生成普通字段
|
||||||
<result column="$!{field.name}" property="$!{field.propertyName}"/>
|
<result column="$!{field.name}" property="$!{field.propertyName}"/>
|
||||||
#end
|
#end
|
||||||
#end
|
#end
|
||||||
|
|
@ -28,7 +38,11 @@
|
||||||
#end
|
#end
|
||||||
|
|
||||||
<select id="select$!{entity}Page" resultMap="$!{table.entityPath}ResultMap">
|
<select id="select$!{entity}Page" resultMap="$!{table.entityPath}ResultMap">
|
||||||
|
#if($!{superEntityClass})
|
||||||
select * from $!{table.name} where is_deleted = 0
|
select * from $!{table.name} where is_deleted = 0
|
||||||
|
#else
|
||||||
|
select * from $!{table.name}
|
||||||
|
#end
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package $!{package.Service};
|
package $!{package.Service};
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import $!{superServiceClassPackage};
|
||||||
import $!{package.Entity}.$!{entity};
|
import $!{package.Entity}.$!{entity};
|
||||||
#set($voPackage=$package.Entity.replace("entity","vo"))
|
#set($voPackage=$package.Entity.replace("entity","vo"))
|
||||||
import $!{voPackage}.$!{entity}VO;
|
import $!{voPackage}.$!{entity}VO;
|
||||||
import $!{superServiceClassPackage};
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $!{table.comment} 服务类
|
* $!{table.comment} 服务类
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package $!{package.ServiceImpl};
|
package $!{package.ServiceImpl};
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import $!{superServiceImplClassPackage};
|
||||||
import $!{package.Entity}.$!{entity};
|
import $!{package.Entity}.$!{entity};
|
||||||
#set($voPackage=$package.Entity.replace("entity","vo"))
|
#set($voPackage=$package.Entity.replace("entity","vo"))
|
||||||
import $!{voPackage}.$!{entity}VO;
|
import $!{voPackage}.$!{entity}VO;
|
||||||
import $!{package.Mapper}.$!{table.mapperName};
|
import $!{package.Mapper}.$!{table.mapperName};
|
||||||
import $!{package.Service}.$!{table.serviceName};
|
import $!{package.Service}.$!{table.serviceName};
|
||||||
import $!{superServiceImplClassPackage};
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* $!{table.comment} 服务实现类
|
* $!{table.comment} 服务实现类
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
|
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
|
||||||
package $!{wrapperPackage};
|
package $!{wrapperPackage};
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import org.springblade.core.mp.support.BaseEntityWrapper;
|
import org.springblade.core.mp.support.BaseEntityWrapper;
|
||||||
import org.springblade.core.tool.utils.BeanUtil;
|
import org.springblade.core.tool.utils.BeanUtil;
|
||||||
import $!{package.Entity}.$!{entity};
|
import $!{package.Entity}.$!{entity};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
|
#set($hasTenant = false)
|
||||||
|
#foreach($field in $!{table.fields})
|
||||||
|
#if($!{field.name} == $!{tenantColumn})
|
||||||
|
#set($hasTenant = true)
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
#set($useTenantEntity = false)
|
||||||
|
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
|
||||||
|
#set($useTenantEntity = true)
|
||||||
|
#end
|
||||||
<template>
|
<template>
|
||||||
<basic-container>
|
<basic-container>
|
||||||
<avue-crud :option="option"
|
<avue-crud :option="option"
|
||||||
:table-loading="loading"
|
:table-loading="loading"
|
||||||
:data="data"
|
:data="data"
|
||||||
ref="crudRef"
|
:page="page"
|
||||||
v-model="form"
|
|
||||||
v-model:search="search"
|
|
||||||
v-model:page="page"
|
|
||||||
:permission="permissionList"
|
:permission="permissionList"
|
||||||
:before-open="beforeOpen"
|
:before-open="beforeOpen"
|
||||||
|
v-model="form"
|
||||||
|
ref="crudRef"
|
||||||
@row-update="rowUpdate"
|
@row-update="rowUpdate"
|
||||||
@row-save="rowSave"
|
@row-save="rowSave"
|
||||||
@row-del="rowDel"
|
@row-del="rowDel"
|
||||||
|
|
@ -17,6 +26,7 @@
|
||||||
@selection-change="selectionChange"
|
@selection-change="selectionChange"
|
||||||
@current-change="currentChange"
|
@current-change="currentChange"
|
||||||
@size-change="sizeChange"
|
@size-change="sizeChange"
|
||||||
|
@refresh-change="refreshChange"
|
||||||
@on-load="onLoad">
|
@on-load="onLoad">
|
||||||
<template #menu-left>
|
<template #menu-left>
|
||||||
<el-button type="danger"
|
<el-button type="danger"
|
||||||
|
|
@ -41,8 +51,8 @@ import { validData } from '@/utils/util';
|
||||||
interface $!{entity}Entity {
|
interface $!{entity}Entity {
|
||||||
id: string;
|
id: string;
|
||||||
#foreach($field in $!{table.fields})
|
#foreach($field in $!{table.fields})
|
||||||
#if($!{field.name}!=$!{tenantColumn})
|
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
|
||||||
#if($!{field.propertyName} != "id")
|
#if(!$!{field.keyFlag})
|
||||||
#if($!{field.propertyType} == "Integer" || $!{field.propertyType} == "Long" || $!{field.propertyType} == "Short" || $!{field.propertyType} == "Byte" || $!{field.propertyType} == "Double" || $!{field.propertyType} == "Float" || $!{field.propertyType} == "BigDecimal")
|
#if($!{field.propertyType} == "Integer" || $!{field.propertyType} == "Long" || $!{field.propertyType} == "Short" || $!{field.propertyType} == "Byte" || $!{field.propertyType} == "Double" || $!{field.propertyType} == "Float" || $!{field.propertyType} == "BigDecimal")
|
||||||
$!{field.propertyName}?: number;
|
$!{field.propertyName}?: number;
|
||||||
#elseif($!{field.propertyType} == "Boolean")
|
#elseif($!{field.propertyType} == "Boolean")
|
||||||
|
|
@ -65,14 +75,13 @@ const permission = computed(() => store.getters.permission);
|
||||||
// 表格实例与数据状态
|
// 表格实例与数据状态
|
||||||
const crudRef = ref();
|
const crudRef = ref();
|
||||||
const form = ref<$!{entity}Form>({});
|
const form = ref<$!{entity}Form>({});
|
||||||
const search = ref<Partial<$!{entity}Entity>>({});
|
|
||||||
const data = ref<$!{entity}Entity[]>([]);
|
const data = ref<$!{entity}Entity[]>([]);
|
||||||
const selectionList = ref<$!{entity}Entity[]>([]);
|
const selectionList = ref<$!{entity}Entity[]>([]);
|
||||||
const query = ref<Partial<$!{entity}Entity>>({});
|
const query = ref<Partial<$!{entity}Entity>>({});
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
// 分页参数(整体替换,需用 ref)
|
// 分页参数
|
||||||
const page = ref({
|
const page = reactive({
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
@ -85,16 +94,17 @@ const ids = computed(() => selectionList.value.map(ele => ele.id).join(','));
|
||||||
const option = reactive({
|
const option = reactive({
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
calcHeight: 210,
|
calcHeight: 210,
|
||||||
|
tip: false,
|
||||||
searchShow: true,
|
searchShow: true,
|
||||||
searchMenuSpan: 6,
|
searchMenuSpan: 6,
|
||||||
tip: false,
|
|
||||||
border: true,
|
border: true,
|
||||||
index: true,
|
index: true,
|
||||||
viewBtn: true,
|
viewBtn: true,
|
||||||
selection: true,
|
selection: true,
|
||||||
column: [
|
column: [
|
||||||
#foreach($field in $!{table.fields})
|
#foreach($field in $!{table.fields})
|
||||||
#if($!{field.name}!=$!{tenantColumn})
|
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
|
||||||
|
#if(!$!{field.keyFlag})
|
||||||
{
|
{
|
||||||
label: '$!{field.comment}',
|
label: '$!{field.comment}',
|
||||||
prop: '$!{field.propertyName}',
|
prop: '$!{field.propertyName}',
|
||||||
|
|
@ -105,6 +115,7 @@ const option = reactive({
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
#end
|
#end
|
||||||
|
#end
|
||||||
#end
|
#end
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -121,9 +132,9 @@ const permissionList = computed(() => ({
|
||||||
const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Partial<$!{entity}Entity> = {}) => {
|
const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Partial<$!{entity}Entity> = {}) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getList(pageData.currentPage, pageData.pageSize, Object.assign(params, query.value)).then(res => {
|
getList(pageData.currentPage, pageData.pageSize, Object.assign(params, query.value)).then(res => {
|
||||||
const list = res.data.data;
|
const listData = res.data.data;
|
||||||
page.value.total = list.total;
|
page.total = listData.total;
|
||||||
data.value = list.records;
|
data.value = listData.records;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
selectionClear();
|
selectionClear();
|
||||||
});
|
});
|
||||||
|
|
@ -132,25 +143,30 @@ const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Par
|
||||||
// 条件检索
|
// 条件检索
|
||||||
const searchChange = (params: Partial<$!{entity}Entity>, done: () => void) => {
|
const searchChange = (params: Partial<$!{entity}Entity>, done: () => void) => {
|
||||||
query.value = params;
|
query.value = params;
|
||||||
page.value.currentPage = 1;
|
page.currentPage = 1;
|
||||||
onLoad(page.value, params);
|
onLoad(page, params);
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置检索条件
|
// 重置检索条件
|
||||||
const searchReset = () => {
|
const searchReset = () => {
|
||||||
query.value = {};
|
query.value = {};
|
||||||
onLoad(page.value);
|
onLoad(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换页码
|
// 切换页码
|
||||||
const currentChange = (currentPage: number) => {
|
const currentChange = (currentPage: number) => {
|
||||||
page.value.currentPage = currentPage;
|
page.currentPage = currentPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调整每页条数
|
// 调整每页条数
|
||||||
const sizeChange = (pageSize: number) => {
|
const sizeChange = (pageSize: number) => {
|
||||||
page.value.pageSize = pageSize;
|
page.pageSize = pageSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新当前列表
|
||||||
|
const refreshChange = () => {
|
||||||
|
onLoad(page, query.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 记录当前选中行
|
// 记录当前选中行
|
||||||
|
|
@ -168,7 +184,7 @@ const selectionClear = () => {
|
||||||
const rowSave = (row: $!{entity}Form, done: () => void, loading: () => void) => {
|
const rowSave = (row: $!{entity}Form, done: () => void, loading: () => void) => {
|
||||||
add(row).then(() => {
|
add(row).then(() => {
|
||||||
done();
|
done();
|
||||||
onLoad(page.value);
|
onLoad(page);
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: '操作成功!',
|
message: '操作成功!',
|
||||||
|
|
@ -183,7 +199,7 @@ const rowSave = (row: $!{entity}Form, done: () => void, loading: () => void) =>
|
||||||
const rowUpdate = (row: $!{entity}Form, index: number, done: () => void, loading: () => void) => {
|
const rowUpdate = (row: $!{entity}Form, index: number, done: () => void, loading: () => void) => {
|
||||||
update(row).then(() => {
|
update(row).then(() => {
|
||||||
done();
|
done();
|
||||||
onLoad(page.value);
|
onLoad(page);
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: '操作成功!',
|
message: '操作成功!',
|
||||||
|
|
@ -205,7 +221,7 @@ const rowDel = (row: $!{entity}Entity) => {
|
||||||
return remove(row.id);
|
return remove(row.id);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onLoad(page.value);
|
onLoad(page);
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: '操作成功!',
|
message: '操作成功!',
|
||||||
|
|
@ -228,7 +244,7 @@ const handleDelete = () => {
|
||||||
return remove(ids.value);
|
return remove(ids.value);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onLoad(page.value);
|
onLoad(page);
|
||||||
ElMessage({
|
ElMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: '操作成功!',
|
message: '操作成功!',
|
||||||
|
|
@ -237,7 +253,7 @@ const handleDelete = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开弹窗前置处理:编辑与查看回填详情
|
// 打开弹窗前加载编辑/查看详情
|
||||||
const beforeOpen = (done: () => void, type: string) => {
|
const beforeOpen = (done: () => void, type: string) => {
|
||||||
if (['edit', 'view'].includes(type)) {
|
if (['edit', 'view'].includes(type)) {
|
||||||
getDetail(form.value.id).then(res => {
|
getDetail(form.value.id).then(res => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springblade.core.loadbalancer.config;
|
||||||
|
|
||||||
|
import net.dreamlu.mica.auto.annotation.AutoEnvPostProcessor;
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
|
import org.springframework.boot.EnvironmentPostProcessor;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.env.DefaultPropertiesPropertySource;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feign 分页模型 JSON 支持的装配兼容处理
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Feign 为 Spring Data 的分页与排序模型提供的两个 JSON 扩展仅有 Jackson 3 实现,
|
||||||
|
* 而其装配条件本应校验的 Jackson 模块基类在 Jackson 3 中已改名,上游条件退化为恒成立,
|
||||||
|
* 于是只要分页与排序模型在类路径上就会装配,Jackson 3 缺席时中断容器启动。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 故仅在 Jackson 3 缺席时给出关闭的默认值,该默认值优先级最低,下游可显式开启;
|
||||||
|
* Jackson 3 在场时不再干预,交由上游条件判定。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@AutoEnvPostProcessor
|
||||||
|
public class FeignJacksonEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feign 分页模型 JSON 支持开关
|
||||||
|
*/
|
||||||
|
private static final String FEIGN_JACKSON_KEY = "spring.cloud.openfeign.autoconfiguration.jackson.enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson 3 模块基类,上述两个扩展的直接父类
|
||||||
|
*/
|
||||||
|
private static final String JACKSON_MODULE_CLASS = "tools.jackson.databind.JacksonModule";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postProcessEnvironment(@NonNull ConfigurableEnvironment environment, SpringApplication application) {
|
||||||
|
if (ClassUtils.isPresent(JACKSON_MODULE_CLASS, application.getClassLoader())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> defaults = Map.of(FEIGN_JACKSON_KEY, Boolean.FALSE.toString());
|
||||||
|
DefaultPropertiesPropertySource.addOrMerge(defaults, environment.getPropertySources());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return Ordered.LOWEST_PRECEDENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import org.springblade.core.secure.utils.SecureUtil;
|
||||||
import org.springblade.core.tenant.exception.TenantException;
|
import org.springblade.core.tenant.exception.TenantException;
|
||||||
import org.springblade.core.tool.utils.CollectionUtil;
|
import org.springblade.core.tool.utils.CollectionUtil;
|
||||||
import org.springblade.core.tool.utils.ReflectUtil;
|
import org.springblade.core.tool.utils.ReflectUtil;
|
||||||
|
import org.springblade.core.tool.utils.StringUtil;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
@ -100,7 +101,8 @@ public class TenantGuard {
|
||||||
/**
|
/**
|
||||||
* 提交时(新增 / 修改)的租户绑定守卫
|
* 提交时(新增 / 修改)的租户绑定守卫
|
||||||
* <p>
|
* <p>
|
||||||
* 新增(id 为空):超管放行,非超管强制写入当前会话 tenantId,避免前端注入。<br/>
|
* 新增(id 为空):非超管强制写入当前会话 tenantId 避免前端注入;超管保留指定归属租户的能力,
|
||||||
|
* 入参未指定时兜底为会话 tenantId。<br/>
|
||||||
* 修改(id 不空):调用 {@link #verify} 校验归属,并把已存在实体的 tenantId 回写到入参,防止 update 篡改。
|
* 修改(id 不空):调用 {@link #verify} 校验归属,并把已存在实体的 tenantId 回写到入参,防止 update 篡改。
|
||||||
*
|
*
|
||||||
* @param service MyBatis-Plus IService 实例
|
* @param service MyBatis-Plus IService 实例
|
||||||
|
|
@ -112,13 +114,13 @@ public class TenantGuard {
|
||||||
public static <T> void bindTenant(IService<T> service, T entity, EntityType entityType) {
|
public static <T> void bindTenant(IService<T> service, T entity, EntityType entityType) {
|
||||||
Long id = idOf(entity);
|
Long id = idOf(entity);
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
if (!SecureUtil.isAdministrator()) {
|
bindTenantOnCreate(entity);
|
||||||
bindTenantId(entity, SecureUtil.getTenantId());
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
T existEntity = verify(service, id, entityType);
|
T existEntity = verify(service, id, entityType);
|
||||||
if (existEntity == null) {
|
if (existEntity == null) {
|
||||||
|
// 超管提交了库中不存在的 id,saveOrUpdate 将退化为新增,按新增路径绑定归属
|
||||||
|
bindTenantOnCreate(entity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 非超管路径下 verify 已确保 existEntity.tenantId 等于会话 tenantId,直接复用避免重复反射
|
// 非超管路径下 verify 已确保 existEntity.tenantId 等于会话 tenantId,直接复用避免重复反射
|
||||||
|
|
@ -126,6 +128,19 @@ public class TenantGuard {
|
||||||
bindTenantId(entity, tenantId);
|
bindTenantId(entity, tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增路径的租户归属绑定
|
||||||
|
* <p>
|
||||||
|
* 非超管一律以会话 tenantId 覆盖入参,杜绝伪造归属;超管保留跨租户建数据的能力,仅在入参未携带
|
||||||
|
* tenantId 时兜底为会话 tenantId —— 业务表单未必向超管暴露租户选择项,不兜底则记录会以空归属落库,
|
||||||
|
* 脱离所有租户的数据范围且无法被租户条件检索。
|
||||||
|
*/
|
||||||
|
private static void bindTenantOnCreate(Object entity) {
|
||||||
|
if (!SecureUtil.isAdministrator() || StringUtil.isBlank(tenantIdOf(entity))) {
|
||||||
|
bindTenantId(entity, SecureUtil.getTenantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验批量 ids 全部归属当前会话租户,并返回查询出的实体列表
|
* 校验批量 ids 全部归属当前会话租户,并返回查询出的实体列表
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -143,12 +158,14 @@ public class TenantGuard {
|
||||||
if (CollectionUtil.isEmpty(ids)) {
|
if (CollectionUtil.isEmpty(ids)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<T> list = service.listByIds(ids);
|
// 去重入参主键,避免重复 id 导致查询行数不等于入参数量而误判为越权
|
||||||
|
List<Long> distinctIds = ids.stream().distinct().toList();
|
||||||
|
List<T> list = service.listByIds(distinctIds);
|
||||||
if (SecureUtil.isAdministrator()) {
|
if (SecureUtil.isAdministrator()) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
String currentTenantId = SecureUtil.getTenantId();
|
String currentTenantId = SecureUtil.getTenantId();
|
||||||
if (list.size() != ids.size() || list.stream().anyMatch(entity -> !currentTenantId.equals(tenantIdOf(entity)))) {
|
if (list.size() != distinctIds.size() || list.stream().anyMatch(entity -> !currentTenantId.equals(tenantIdOf(entity)))) {
|
||||||
throw new TenantException("无权操作非本租户的" + entityType.label());
|
throw new TenantException("无权操作非本租户的" + entityType.label());
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,7 @@ public class JacksonConfiguration {
|
||||||
|
|
||||||
> ⚠️【务必看清,这是升级中最隐蔽的一个坑】Spring 7 的 `WebMvcConfigurationSupport.getMessageConverters()` 执行顺序是:先 `configureMessageConverters(list)` → 若 list 仍为空才 `addDefaultHttpMessageConverters(list)`(这一步才注册 ByteArray/String/Resource/ResourceRegion/Form 以及 JSON 转换器)→ 最后 `extendMessageConverters(list)`。
|
> ⚠️【务必看清,这是升级中最隐蔽的一个坑】Spring 7 的 `WebMvcConfigurationSupport.getMessageConverters()` 执行顺序是:先 `configureMessageConverters(list)` → 若 list 仍为空才 `addDefaultHttpMessageConverters(list)`(这一步才注册 ByteArray/String/Resource/ResourceRegion/Form 以及 JSON 转换器)→ 最后 `extendMessageConverters(list)`。
|
||||||
> 若重写 `configureMessageConverters` 并向 list 添加任何转换器,list 变为非空,`addDefaultHttpMessageConverters` 会被**整段跳过**,MVC 最终只剩下你加入的这一个转换器——文件下载、`byte[]`/图片/PDF、纯文本、表单请求体全部 406/`HttpMessageNotWritableException` 失败。**编译与纯 JSON 接口都正常,极难在冒烟测试中发现。**
|
> 若重写 `configureMessageConverters` 并向 list 添加任何转换器,list 变为非空,`addDefaultHttpMessageConverters` 会被**整段跳过**,MVC 最终只剩下你加入的这一个转换器——文件下载、`byte[]`/图片/PDF、纯文本、表单请求体全部 406/`HttpMessageNotWritableException` 失败。**编译与纯 JSON 接口都正常,极难在冒烟测试中发现。**
|
||||||
> 正解是重写 `extendMessageConverters`:它在默认列表构建完成之后回调,拿到的是**完整**的默认转换器,再原地替换 JSON(因 §5.5⑤ 已强制 `preferred-json-mapper=jackson2`,默认 JSON 转换器为 `MappingJackson2HttpMessageConverter`,属 `AbstractJackson2HttpMessageConverter`)与 String 转换器即可,其余转换器原样保留。
|
> 正解是重写 `extendMessageConverters`:它在默认列表构建完成之后回调,拿到的是**完整**的默认转换器,再原地替换 JSON(因 §5.5⑤ 已在托管层剥离 Jackson 3,默认 JSON 转换器为 `MappingJackson2HttpMessageConverter`,属 `AbstractJackson2HttpMessageConverter`)与 String 转换器即可,其余转换器原样保留。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -387,7 +387,28 @@ public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||||
|
|
||||||
> 与 Jackson 无关的 `org.springframework.lang.Nullable/NonNull` 在 Spring 7 也被弃用(官方推 JSpecify),那是一处独立、面广的迁移,不在本表范围,别混入 Jackson 的 `@SuppressWarnings`。
|
> 与 Jackson 无关的 `org.springframework.lang.Nullable/NonNull` 在 Spring 7 也被弃用(官方推 JSpecify),那是一处独立、面广的迁移,不在本表范围,别混入 Jackson 的 `@SuppressWarnings`。
|
||||||
|
|
||||||
**⑤ 【关键】强制 preferred-json-mapper = jackson2**——新增一个 `LauncherService`,否则 Boot 4 默认注册 Jackson 3 转换器,§5.5③ 的 `instanceof AbstractJackson2HttpMessageConverter` 命不中,Blade 的 JSON 处理静默失效:
|
**⑤ 【关键】让 Boot 4 选中 Jackson 2**——Boot 的择一逻辑写在 `org.springframework.boot.http.converter.autoconfigure` 下,读源码可见是**两条并列通路,满足其一即可**(`Jackson2HttpMessageConvertersConfiguration.PreferJackson2OrJacksonUnavailableCondition` 是 `AnyNestedCondition`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Jackson 3 分支——三个条件同时成立才注册(javap 实测 JacksonJsonHttpMessageConverterConfiguration)
|
||||||
|
@ConditionalOnClass(tools.jackson.databind.json.JsonMapper.class)
|
||||||
|
@ConditionalOnBean(tools.jackson.databind.json.JsonMapper.class) // ← 关键:要 Bean,不只是类
|
||||||
|
@ConditionalOnProperty(name = "spring.http.converters.preferred-json-mapper",
|
||||||
|
havingValue = "jackson", matchIfMissing = true)
|
||||||
|
|
||||||
|
// Jackson 2 分支——以下任一命中即生效
|
||||||
|
@ConditionalOnProperty(name = "...preferred-json-mapper", havingValue = "jackson2") // 通路 A
|
||||||
|
@ConditionalOnMissingBean(JacksonJsonHttpMessageConvertersCustomizer.class) // 通路 B
|
||||||
|
```
|
||||||
|
|
||||||
|
- **通路 B(主)**:Jackson 3 分支要的是**容器里有 `JsonMapper` Bean**,而该 Bean 只由 `spring-boot-jackson` 自动配置模块产出;托管层已把该模块摘掉(见 §5.5⑥),Bean 不存在 → Jackson 3 分支不命中 → Jackson 2 分支的 `@ConditionalOnMissingBean` 成立。这是**编译期事实**,不依赖启动路径。
|
||||||
|
- **通路 A(兜底)**:`LauncherService` 设 `preferred-json-mapper=jackson2`,覆盖下游未继承 blade 托管层、`spring-boot-jackson` 漏进 classpath 的场景。
|
||||||
|
|
||||||
|
> ⚠️ 别把「剥离 Jackson 3」理解成「classpath 上不能有 `tools.jackson`」——**决定管线的是 `JsonMapper` Bean 在不在,不是 Jackson 3 的类在不在**。Jackson 3 的库照常引入不会翻转管线,反而是必需的,理由见 §5.5⑧。
|
||||||
|
|
||||||
|
> 两条都留的原因:通路 A 仅在 `BladeApplication` 启动路径执行,`@SpringBootTest`、blade-gateway(不含 blade-core-tool)覆盖不到;且 Jackson 3 的 **XML 转换器分支**(`JacksonXmlHttpMessageConverterConfiguration`)只有 `@ConditionalOnClass` + `@ConditionalOnBean`,**不受 `preferred-json-mapper` 约束**,属性通路结构性覆盖不到。
|
||||||
|
>
|
||||||
|
> 关于失效程度需说清:因 §5.5③ 的 `if (!jsonReplaced) converters.add(0, bladeJson)` 兜底,即使两条通路全漏、Jackson 3 转换器占位,blade 转换器仍会插到队首优先命中,**MVC 服务端不会失效**;真正会静默回落到 Jackson 3 的是 `RestClient`/`RestTemplate` 客户端转换器与 WebFlux 编解码器。
|
||||||
|
|
||||||
新增 `blade-core-tool/.../config/JacksonLauncherServiceImpl.java`:
|
新增 `blade-core-tool/.../config/JacksonLauncherServiceImpl.java`:
|
||||||
|
|
||||||
|
|
@ -408,6 +429,92 @@ public class JacksonLauncherServiceImpl implements LauncherService {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**⑥ 【主通路】托管层剥离 Jackson 3 的自动配置模块(只剥模块,不剥库)**——在父 POM `dependencyManagement` 把 `spring-boot-jackson` 从传递链上摘掉,容器里便不会出现 `JsonMapper` Bean,§5.5⑤ 的通路 B 恒成立。**剥离对象仅限这个自动配置模块,`tools.jackson` 系的库本体照常引入**(理由见 §5.5⑧)。**需要两个条目,缺一不可**:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 主路径:webmvc、restclient 等多个 starter 都经由 starter-jackson 传递 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jackson</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-jackson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<!-- springdoc 走的是另一条路,webflux-ui / webmvc-ui 两个坐标都要单独排 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>3.0.3</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-jackson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 为什么 springdoc 必须单独排:`springdoc-openapi-starter-common:3.0.3` **直接依赖** `org.springframework.boot:spring-boot-jackson`,而不是经 `spring-boot-starter-jackson` 传递,上面第一个条目的 `<exclusions>` 只约束它自己的子树,管不到 springdoc。可用 `mvn dependency:tree` 单独验证(注意:在完整工程里跑,Maven 的 nearest-wins 会把 springdoc 分支下的重复节点省略,须用只含 springdoc 的最小 POM 或加 `-Dverbose` 才看得见)。
|
||||||
|
>
|
||||||
|
> springdoc 内部仅 Spring Data REST 支持类引用 Jackson 3,且被 `@ConditionalOnClass(RepositoryRestConfiguration)` 守卫,排除无副作用。
|
||||||
|
>
|
||||||
|
> 子模块**不需要**再写一遍局部 `<exclusions>`:子模块继承父 `dependencyManagement`,下游工程经 `blade-core-bom` 展开同样继承,重复声明纯属冗余。
|
||||||
|
|
||||||
|
**⑦ 序列化管线门禁**——托管层排除是"当前依赖树"的快照,第三方库升级随时可能开出新的引入路径(springdoc 直连就是活例)。父 POM `<build><plugins>` 挂一条 enforcer 规则,把漏排从**运行期静默回落**提前成**构建期失败**,并顺带校验托管层排除是否随 `blade-core-bom` 正确传导:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
|
<version>${maven.enforcer.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>ban-jackson3-autoconfigure</id>
|
||||||
|
<goals><goal>enforce</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<bannedDependencies>
|
||||||
|
<excludes>
|
||||||
|
<exclude>org.springframework.boot:spring-boot-jackson</exclude>
|
||||||
|
<exclude>org.springframework.boot:spring-boot-jackson-test</exclude>
|
||||||
|
</excludes>
|
||||||
|
</bannedDependencies>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **封禁对象只有自动配置模块**,`tools.jackson` 系构件不在范围内——门禁守的是"`JsonMapper` Bean 不许出现",不是"Jackson 3 的类不许出现";把库一起禁掉会连带打死一批按「Jackson 3 在场」编写的第三方组件(见 §5.5⑧)。`spring-boot-jackson` / `spring-boot-jackson-test` 均为精确坐标匹配,**不会**误伤 `spring-boot-jackson2`。
|
||||||
|
>
|
||||||
|
> 该门禁 Boot 单体 / Cloud 微服务两个下游工程需各自挂一份——它们以 Boot BOM import 方式引用 blade-tool,并非以其为 parent,`<build><plugins>` 不会继承过去。
|
||||||
|
|
||||||
|
**⑧ 【必读】Jackson 3 的库本体必须留着(Cloud 侧尤其)**——⑥ 只剥自动配置模块,而 `tools.jackson` 的库是随该模块传递进来的,剥掉模块库也就跟着没了。若不补回来,凡是**编译期直接 `import tools.jackson.*`** 的第三方组件都会踩雷:轻则条件不命中静默降级,重则 `NoClassDefFoundError: tools/jackson/databind/ObjectMapper` 打断启动。Boot 单体不受影响(这些组件只存在于 Cloud 技术栈),Cloud 侧实测清单:
|
||||||
|
|
||||||
|
| 组件 | 触发点 | 缺席后果 |
|
||||||
|
|---|---|---|
|
||||||
|
| `spring-cloud-circuitbreaker-sentinel` | `CircuitBreakerRuleChangeListener#afterSingletonsInstantiated` → `SentinelFeignClientProperties#copy()` 内 `new tools.jackson.databind.ObjectMapper()` | **容器 refresh 末尾抛错,服务起不来**;所有单例都建完了才炸,日志看着像启动成功 |
|
||||||
|
| `spring-cloud-gateway-server-webflux` | `RemoveJsonAttributesResponseBodyGatewayFilterFactory` 的实例字段 `private ObjectMapper mapper = new ObjectMapper()`,该 Bean 由 `@ConditionalOnEnabledFilter` 默认开启 | **网关起不来** |
|
||||||
|
| `spring-cloud-openfeign-core` | `PageJacksonModule` / `SortJacksonModule`,装配条件校验的模块基类在 Jackson 3 改名后退化为恒成立 | **容器启动失败**;见 `FeignJacksonEnvPostProcessor` 的兜底 |
|
||||||
|
| `spring-boot-admin-client` | `RestClientRegistrationClientConfig#registrationClient` 形参 `ObjectProvider<tools.jackson.databind.json.JsonMapper>` | 配了 `spring.boot.admin.client.url` 才装配,装配即失败 |
|
||||||
|
| `spring-cloud-alibaba-sentinel-datasource` | `JsonConverter` / `XmlConverter` | 静默不注册,Sentinel 规则数据源转换能力消失 |
|
||||||
|
| `spring-cloud-starter-alibaba-sentinel` | `SentinelAutoConfiguration$SentinelConverterConfiguration` | 自带 `@ConditionalOnClass` 守卫,整块跳过,同上静默降级 |
|
||||||
|
|
||||||
|
这些都不属于本工程可修的范围,逐个关开关是打地鼠——上表还只是**当前版本**的快照,三方库一升级又会冒出新的。所以口径是**留库**:
|
||||||
|
|
||||||
|
- `blade-core-cloud` 引 `tools.jackson.core:jackson-databind`,覆盖全部微服务(经 `blade-core-boot` 传递)。
|
||||||
|
- `blade-gateway` 不含 `blade-core-cloud`,需在自己的 POM 里单独引一份,否则会被上表第二行打挂。
|
||||||
|
- 版本由 Boot BOM 托管(`spring-boot-dependencies` 已 import `tools.jackson:jackson-bom`),不必自设版本属性。
|
||||||
|
|
||||||
|
留库**不会**翻转管线:§5.5⑤ 已实测 Jackson 3 转换器分支要的是 `@ConditionalOnBean(JsonMapper)`,而该 Bean 只由被剥离的 `spring-boot-jackson` 产出。`FeignJacksonEnvPostProcessor` 也据此加了 `ClassUtils.isPresent("tools.jackson.databind.JacksonModule")` 守卫——Jackson 3 在场就不干预,交回上游条件判定,只在缺席时才给出关闭默认值。
|
||||||
|
|
||||||
|
> 排查手法(升级三方库后建议复跑):对依赖树里的 jar 逐个扫常量池,`tools/jackson` 命中即为硬引用嫌疑,再回源码看有没有条件守卫。用 Python 的 `zipfile` 扫,别用 `strings`——macOS 的 `strings` 会把 `.class` 的 `0xCAFEBABE` magic 误判成 Mach-O fat 二进制,扫出空结果。
|
||||||
|
|
||||||
### 5.6 Redis pub/sub 容器与 Boot 4.1 新增 `@RedisListener` 自动配置的竞争
|
### 5.6 Redis pub/sub 容器与 Boot 4.1 新增 `@RedisListener` 自动配置的竞争
|
||||||
|
|
||||||
**背景**:Boot 4.1 的 `spring-boot-data-redis` 新增了注解驱动监听自动配置 `DataRedisAnnotationDrivenConfiguration`(由 `DataRedisAutoConfiguration` `@Import`,条件 `@ConditionalOnClass({org.springframework.data.redis.annotation.EnableRedisListeners, org.springframework.messaging.Message})`),用于支撑新的 `@RedisListener` 注解。它会注册一个 `@Bean(name = "redisMessageListenerContainer")` + `@ConditionalOnMissingBean` 的默认容器。
|
**背景**:Boot 4.1 的 `spring-boot-data-redis` 新增了注解驱动监听自动配置 `DataRedisAnnotationDrivenConfiguration`(由 `DataRedisAutoConfiguration` `@Import`,条件 `@ConditionalOnClass({org.springframework.data.redis.annotation.EnableRedisListeners, org.springframework.messaging.Message})`),用于支撑新的 `@RedisListener` 注解。它会注册一个 `@Bean(name = "redisMessageListenerContainer")` + `@ConditionalOnMissingBean` 的默认容器。
|
||||||
|
|
@ -652,7 +759,7 @@ import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
|
||||||
|
|
||||||
**⑥ 安全放行(springdoc 路径)**:springdoc UI 与规格端点需匿名可达。框架默认放行清单 `blade-core-secure` 的 `SecureRegistry.defaultExcludePatterns` 已含 `/v3/api-docs/**`、`/swagger-ui/**`、`/swagger-ui.html`(`/swagger-ui/**` 为 UI 静态资源,`/swagger-ui.html` 为跳转入口),下游 servlet 应用继承即可。若应用另有自建放行清单(Boot 单体的 `BladeConfiguration`、Cloud 网关的 `AuthProvider`),须同步补齐这三项,并去掉 `/doc.html`、`/swagger-resources/**` 等 knife4j / springfox 旧路径。
|
**⑥ 安全放行(springdoc 路径)**:springdoc UI 与规格端点需匿名可达。框架默认放行清单 `blade-core-secure` 的 `SecureRegistry.defaultExcludePatterns` 已含 `/v3/api-docs/**`、`/swagger-ui/**`、`/swagger-ui.html`(`/swagger-ui/**` 为 UI 静态资源,`/swagger-ui.html` 为跳转入口),下游 servlet 应用继承即可。若应用另有自建放行清单(Boot 单体的 `BladeConfiguration`、Cloud 网关的 `AuthProvider`),须同步补齐这三项,并去掉 `/doc.html`、`/swagger-resources/**` 等 knife4j / springfox 旧路径。
|
||||||
|
|
||||||
**⑦ 代码生成器模板**:`blade-starter-develop/src/main/resources/templates/controller.java.vm` 里的 `@ApiOperationSupport` import 与注解一并删除——它是 `.vm` 资源,下游按 `*.java` 批量清理的脚本覆盖不到,须单独处理,否则该生成器产出的 Controller 引用已移除的 knife4j 包、编译不过(排序改用 §9⑧ 的 `@ApiOrder`)。删干净后**还须在模板 `@Tag` 上方补一行无参 `@ApiOrder` + 其 import**(`org.springblade.core.swagger.annotation.ApiOrder`),让生成的 Controller 与手写控制器一致带排序(见下「落地约定」);下游 Boot(`src/{main,test}/resources/templates/`)、Cloud(`blade-ops/blade-develop/src/{main,test}/resources/templates/`)的同名模板副本一并补。
|
**⑦ 代码生成器模板**:`blade-starter-develop/src/main/resources/templates/controller.java.vm` 里的 `@ApiOperationSupport` import 与注解一并删除——它是 `.vm` 资源,下游按 `*.java` 批量清理的脚本覆盖不到,须单独处理,否则该生成器产出的 Controller 引用已移除的 knife4j 包、编译不过(排序改用 §9⑧ 的 `@ApiOrder`)。删干净后**还须在模板 `@Tag` 上方补一行无参 `@ApiOrder` + 其 import**(`org.springblade.core.swagger.annotation.ApiOrder`),让生成的 Controller 与手写控制器一致带排序(见下「落地约定」);下游 Boot(`src/main/resources/templates/`)、Cloud(`blade-ops/blade-develop/src/main/resources/templates/`)的同名模板副本一并补。`src/test/resources/templates/` 下的旧扁平路径 `.vm` 副本自模板重组为 `api/` 子目录后不再被加载,应直接删除,test 侧仅保留 `code.properties` 作为数据源配置覆盖点。
|
||||||
|
|
||||||
**⑧ 接口排序:`@ApiOrder`(替代 knife4j 的 `@ApiOperationSupport(order = n)`)**
|
**⑧ 接口排序:`@ApiOrder`(替代 knife4j 的 `@ApiOperationSupport(order = n)`)**
|
||||||
|
|
||||||
|
|
@ -664,7 +771,7 @@ springdoc / OpenAPI 3 无接口排序原生注解,blade-starter-swagger 新增 `
|
||||||
|
|
||||||
实现在 `SwaggerAutoConfiguration`:`GlobalOperationCustomizer` 读注解写入 `x-order` 扩展(类级用字节码行号取声明序,反射不保证方法顺序)并记录「tag → 类级序号」;`GlobalOpenApiCustomizer` 按 `x-order` 重排 `paths`、按类级序号重排顶层 `tags`。swagger-ui 仅在两个 sorter 未设时保留 spec 顺序(§9⑤),并须保持 `springdoc.writer-with-order-by-keys` 默认 `false`。
|
实现在 `SwaggerAutoConfiguration`:`GlobalOperationCustomizer` 读注解写入 `x-order` 扩展(类级用字节码行号取声明序,反射不保证方法顺序)并记录「tag → 类级序号」;`GlobalOpenApiCustomizer` 按 `x-order` 重排 `paths`、按类级序号重排顶层 `tags`。swagger-ui 仅在两个 sorter 未设时保留 spec 顺序(§9⑤),并须保持 `springdoc.writer-with-order-by-keys` 默认 `false`。
|
||||||
|
|
||||||
**落地约定(本次统一)**:三工程**每个带 `@Tag` 的控制器**均在 `@Tag` 上方补一行**无参 `@ApiOrder`**(值取默认 `Integer.MAX_VALUE`),方法体只保留 `@Operation`——效果是分组内接口按**源码声明顺序**展示、各 `@Tag` 分组之间因类级序号相等而回落 tag 名字母序;仅当个别接口要打破源码顺序时,才在该方法上补 `@ApiOrder(n)` 精确覆盖。删 knife4j 排序注解(§3.1 / §4.2)后**只删不补**会让接口在 springdoc UI 下按字母序、丢失原有顺序,故「删旧 + 补 `@ApiOrder`」是同一件事的两半。代码生成器模板 `controller.java.vm`(blade-tool `blade-starter-develop` 源模板 + 下游 Boot/Cloud `blade-develop` 的 main/test 副本,共 5 份)同步在 `@Tag` 上方生成 `@ApiOrder`,新生成的 Controller 开箱即带排序。
|
**落地约定(本次统一)**:三工程**每个带 `@Tag` 的控制器**均在 `@Tag` 上方补一行**无参 `@ApiOrder`**(值取默认 `Integer.MAX_VALUE`),方法体只保留 `@Operation`——效果是分组内接口按**源码声明顺序**展示、各 `@Tag` 分组之间因类级序号相等而回落 tag 名字母序;仅当个别接口要打破源码顺序时,才在该方法上补 `@ApiOrder(n)` 精确覆盖。删 knife4j 排序注解(§3.1 / §4.2)后**只删不补**会让接口在 springdoc UI 下按字母序、丢失原有顺序,故「删旧 + 补 `@ApiOrder`」是同一件事的两半。代码生成器模板 `controller.java.vm`(blade-tool `blade-starter-develop` 源模板 + 下游 Boot/Cloud `blade-develop` 的 main 副本,共 3 份)同步在 `@Tag` 上方生成 `@ApiOrder`,新生成的 Controller 开箱即带排序。
|
||||||
|
|
||||||
> 提示:类级 `@ApiOrder(n)` 的 `value` 用作 `@Tag` 分组之间的排序序号(`SwaggerAutoConfiguration#recordTagOrder` 写入、`GlobalOpenApiCustomizer` 据此重排顶层 `tags`);分组内各接口不受此值影响,按源码声明顺序排列。本次全量采用**无参**形式,`value` 恒为默认值、各分组落 tag 名字母序;如需固定分组顺序,给对应控制器标 `@ApiOrder(n)` 即可。
|
> 提示:类级 `@ApiOrder(n)` 的 `value` 用作 `@Tag` 分组之间的排序序号(`SwaggerAutoConfiguration#recordTagOrder` 写入、`GlobalOpenApiCustomizer` 据此重排顶层 `tags`);分组内各接口不受此值影响,按源码声明顺序排列。本次全量采用**无参**形式,`value` 恒为默认值、各分组落 tag 名字母序;如需固定分组顺序,给对应控制器标 `@ApiOrder(n)` 即可。
|
||||||
|
|
||||||
|
|
|
||||||
75
pom.xml
75
pom.xml
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- Blade-Tool Version -->
|
<!-- Blade-Tool Version -->
|
||||||
<revision>5.0.0</revision>
|
<revision>5.0.1</revision>
|
||||||
<module.name>org.springblade.blade.tool</module.name>
|
<module.name>org.springblade.blade.tool</module.name>
|
||||||
<!-- jdk Version -->
|
<!-- jdk Version -->
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
<druid.version>1.2.28</druid.version>
|
<druid.version>1.2.28</druid.version>
|
||||||
<spring.retry.version>2.0.13</spring.retry.version>
|
<spring.retry.version>2.0.13</spring.retry.version>
|
||||||
<httpclient.version>4.5.14</httpclient.version>
|
<httpclient.version>4.5.14</httpclient.version>
|
||||||
|
<maven.enforcer.version>3.6.3</maven.enforcer.version>
|
||||||
|
|
||||||
<spring.boot.version>4.1.0</spring.boot.version>
|
<spring.boot.version>4.1.0</spring.boot.version>
|
||||||
<spring.boot.admin.version>4.1.1</spring.boot.admin.version>
|
<spring.boot.admin.version>4.1.1</spring.boot.admin.version>
|
||||||
|
|
@ -127,6 +128,23 @@
|
||||||
<version>${mica.auto.version}</version>
|
<version>${mica.auto.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--
|
||||||
|
业务序列化由 Jackson 2 承载,中间件按各自条件使用 Jackson 3。
|
||||||
|
消息转换器改用哪一代,取决于容器中是否存在 JsonMapper Bean,该 Bean 只由
|
||||||
|
spring-boot-jackson 产出,故此处只剥离该模块,Jackson 3 的库照常引入。
|
||||||
|
webmvc、restclient 等多个 starter 均经 spring-boot-starter-jackson 传递,在此一并收口。
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jackson</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-jackson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
<!-- blade tool -->
|
<!-- blade tool -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springblade</groupId>
|
<groupId>org.springblade</groupId>
|
||||||
|
|
@ -380,15 +398,28 @@
|
||||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||||
<version>2.2.52</version>
|
<version>2.2.52</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- springdoc 直连 spring-boot-jackson,不受上方 starter 条目的排除约束,须单独剥离 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.0.3</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-jackson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.0.3</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-jackson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- protostuff -->
|
<!-- protostuff -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
@ -420,6 +451,12 @@
|
||||||
<artifactId>easy-captcha</artifactId>
|
<artifactId>easy-captcha</artifactId>
|
||||||
<version>1.6.2</version>
|
<version>1.6.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- fastjson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<version>2.0.64</version>
|
||||||
|
</dependency>
|
||||||
<!-- fastexcel -->
|
<!-- fastexcel -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.idev.excel</groupId>
|
<groupId>cn.idev.excel</groupId>
|
||||||
|
|
@ -532,6 +569,42 @@
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<!--
|
||||||
|
序列化管线门禁:把托管层排除的漏排转为构建期失败。
|
||||||
|
封禁对象仅为 Jackson 3 的自动配置模块,其库不在范围内;
|
||||||
|
spring-boot-jackson2 为精确坐标匹配,同样不受影响。
|
||||||
|
-->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
|
<version>${maven.enforcer.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>ban-jackson3-autoconfigure</id>
|
||||||
|
<goals>
|
||||||
|
<goal>enforce</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<bannedDependencies>
|
||||||
|
<excludes>
|
||||||
|
<exclude>org.springframework.boot:spring-boot-jackson</exclude>
|
||||||
|
<exclude>org.springframework.boot:spring-boot-jackson-test</exclude>
|
||||||
|
</excludes>
|
||||||
|
<message>
|
||||||
|
检测到 spring-boot-jackson 进入依赖树。该模块会注册 JsonMapper Bean,
|
||||||
|
使 Boot 4 改用 Jackson 3 的消息转换器,
|
||||||
|
@BladeView 视图过滤、大数转字符串、日期格式等序列化约定将静默失效。
|
||||||
|
请执行 mvn dependency:tree -Dincludes='org.springframework.boot:spring-boot-jackson'
|
||||||
|
定位引入路径,并在父 POM dependencyManagement 对应条目补充排除。
|
||||||
|
注意:tools.jackson 系构件本身不在封禁范围,可以正常引入。
|
||||||
|
</message>
|
||||||
|
</bannedDependencies>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user