From 6fc8c48677e6466b140e9e8a380e87edabb5294c Mon Sep 17 00:00:00 2001 From: smallchill Date: Sat, 18 Apr 2026 01:19:07 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9EBladeView?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E5=BA=8F=E5=88=97=E5=8C=96=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8C=89=E8=A7=92=E8=89=B2=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=8E=A7=E5=88=B6JSON=E8=BE=93=E5=87=BA=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/BladeViewRoleConfiguration.java | 46 +++++++ .../config/BladeViewAutoConfiguration.java | 57 +++++++++ .../tool/config/JacksonConfiguration.java | 47 +------- .../tool/jackson/BladeJacksonProperties.java | 27 +++++ .../core/tool/jackson/BladeRoleSupplier.java | 40 ++++++ .../core/tool/jackson/BladeView.java | 70 +++++++++++ .../BladeViewAnnotationIntrospector.java | 53 ++++++++ .../tool/jackson/BladeViewCustomizer.java | 52 ++++++++ .../core/tool/jackson/BladeViewResolver.java | 114 ++++++++++++++++++ .../tool/jackson/BladeViewResponseAdvice.java | 99 +++++++++++++++ .../core/tool/jackson/JsonUtil.java | 40 +++--- .../springblade/core/tool/jackson/Views.java | 57 +++++++++ .../springblade/core/mp/base/BaseEntity.java | 9 ++ 13 files changed, 653 insertions(+), 58 deletions(-) create mode 100644 blade-core-secure/src/main/java/org/springblade/core/secure/config/BladeViewRoleConfiguration.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeViewAutoConfiguration.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeRoleSupplier.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeView.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewAnnotationIntrospector.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewCustomizer.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResolver.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResponseAdvice.java create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/jackson/Views.java diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/config/BladeViewRoleConfiguration.java b/blade-core-secure/src/main/java/org/springblade/core/secure/config/BladeViewRoleConfiguration.java new file mode 100644 index 0000000..89cf182 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/config/BladeViewRoleConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.secure.config; + +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.jackson.BladeRoleSupplier; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +/** + * Jackson Views 角色提供者自动装配 + *

+ * 默认通过 {@link SecureUtil#getUserRole()} 获取当前用户角色名, + * 用户可自定义 {@link BladeRoleSupplier} Bean 来覆盖。 + *

+ * + * @author Chill + */ +@AutoConfiguration +public class BladeViewRoleConfiguration { + + /** + * 默认角色名称提供者 + *

使用 {@link ConditionalOnMissingBean} 允许用户自定义覆盖

+ */ + @Bean + @ConditionalOnMissingBean + public BladeRoleSupplier roleNameSupplier() { + return SecureUtil::getUserRole; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeViewAutoConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeViewAutoConfiguration.java new file mode 100644 index 0000000..eb529fc --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeViewAutoConfiguration.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.config; + +import org.springblade.core.tool.jackson.*; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +/** + * BladeView 视图序列化自动装配 + *

+ * 框架内置常驻加载,无需手动开启。 + * 当 Controller 方法未标注 {@link BladeView} 时不产生任何额外开销。 + * 所有 Bean 均支持 {@link ConditionalOnMissingBean},可由用户自定义覆盖。 + *

+ * + * @author Chill + */ +@AutoConfiguration(after = JacksonConfiguration.class) +public class BladeViewAutoConfiguration { + + /** + * 视图解析器 + */ + @Bean + @ConditionalOnMissingBean + public BladeViewResolver bladeViewResolver(BladeJacksonProperties properties, ObjectProvider viewCustomizer) { + BladeViewResolver resolver = new BladeViewResolver(properties.getView()); + viewCustomizer.orderedStream().forEach(customizer -> customizer.customize(resolver)); + return resolver; + } + + /** + * 响应拦截器 + */ + @Bean + @ConditionalOnMissingBean + public BladeViewResponseAdvice bladeViewResponseAdvice(BladeViewResolver viewResolver, ObjectProvider roleNameSupplier) { + return new BladeViewResponseAdvice(viewResolver, roleNameSupplier.getIfAvailable()); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java index a716598..d5aef00 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java @@ -15,70 +15,35 @@ */ package org.springblade.core.tool.config; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import lombok.AllArgsConstructor; import org.springblade.core.tool.jackson.BladeJacksonProperties; -import org.springblade.core.tool.jackson.BladeJavaTimeModule; -import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.jackson.JsonUtil; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; - -import java.text.SimpleDateFormat; -import java.time.ZoneId; -import java.util.Locale; -import java.util.TimeZone; /** * Jackson配置类 * * @author Chill */ -@AutoConfiguration -@AllArgsConstructor +@AutoConfiguration(before = JacksonAutoConfiguration.class) @ConditionalOnClass(ObjectMapper.class) -@AutoConfigureBefore(JacksonAutoConfiguration.class) @EnableConfigurationProperties(BladeJacksonProperties.class) public class JacksonConfiguration { @Bean @ConditionalOnMissingBean - public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { - builder.simpleDateFormat(DateUtil.PATTERN_DATETIME); - //创建ObjectMapper - ObjectMapper objectMapper = builder.createXmlMapper(false).build(); - //设置地点为中国 - objectMapper.setLocale(Locale.CHINA); - //去掉默认的时间戳格式 - objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - //设置为中国上海时区 - objectMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); - //序列化时,日期的统一格式 - objectMapper.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA)); - //序列化处理 - objectMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true); - objectMapper.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true); - objectMapper.findAndRegisterModules(); - //失败处理 - objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - //单引号处理 - objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); - //反序列化时,属性不存在的兼容处理 - objectMapper.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - //日期格式化 - objectMapper.registerModule(new BladeJavaTimeModule()); + public ObjectMapper objectMapper() { + //创建默认的ObjectMapper + ObjectMapper objectMapper = JsonUtil.getInstance(); //允许空字符串序列化为null对象 objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); + //自定义拓展配置 objectMapper.findAndRegisterModules(); return objectMapper; } diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java index ea37749..eb18b7f 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java @@ -19,6 +19,9 @@ import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.LinkedHashMap; +import java.util.Map; + /** * jackson 配置 * @@ -43,4 +46,28 @@ public class BladeJacksonProperties { */ private Boolean supportTextPlain = Boolean.FALSE; + /** + * 视图配置 + */ + private View view = new View(); + + /** + * Jackson Views 视图配置 + * + * @author Chill + */ + @Getter + @Setter + public static class View { + /** + * 动态模式的默认视图(角色获取不到时降级) + *

可选: summary / detail / admin / administrator

+ */ + private String defaultView = "summary"; + /** + * 角色 → 视图映射 + */ + private Map roleMapping = new LinkedHashMap<>(); + } + } diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeRoleSupplier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeRoleSupplier.java new file mode 100644 index 0000000..66872a2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeRoleSupplier.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +import java.util.function.Supplier; + +/** + * BladeView 角色名提供者 + *

+ * 用于 {@link BladeViewResponseAdvice} 动态模式下按角色解析视图。 + * 自定义时请实现本接口而非通用的 {@code Supplier}, + * 以避免与业务工程中其它 {@code Supplier} Bean 产生歧义。 + *

+ * + *

示例

+ *
+ * @Bean
+ * public BladeRoleSupplier myRoleSupplier() {
+ *     return () -> currentUserHolder.getRole();
+ * }
+ * 
+ * + * @author Chill + */ +@FunctionalInterface +public interface BladeRoleSupplier extends Supplier { +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeView.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeView.java new file mode 100644 index 0000000..ebf7b04 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeView.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +import java.lang.annotation.*; + +/** + * BladeX 视图控制注解(全场景统一) + *

+ * 用在字段上:标记该字段的可见视图层级(替代 {@code @JsonView}) + * 用在方法/类上:控制接口的视图过滤策略 + *

+ * + *

字段标注

+ *
+ * @BladeView(Views.Detail.class)
+ * private String tenantName;
+ * 
+ * + *

Controller 标注

+ *
+ * // 静态:明确指定视图
+ * @BladeView(Views.Summary.class)
+ * public R<List<UserVO>> list() { ... }
+ *
+ * // 动态:根据用户角色自动解析
+ * @BladeView
+ * public R<UserVO> detail() { ... }
+ *
+ * // 类级别默认(方法级可覆盖)
+ * @BladeView(Views.Admin.class)
+ * public class AdminController { ... }
+ * 
+ * + * @author Chill + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BladeView { + + /** + * 视图类型 + *

+ * 字段上:指定 Views.Summary / Detail / Admin / Administrator + * 方法上:指定具体视图 = 静态模式;默认 Auto.class = 动态模式 + *

+ */ + Class value() default Auto.class; + + /** + * 动态解析标记(仅用于 Controller 方法/类) + */ + interface Auto { + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewAnnotationIntrospector.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewAnnotationIntrospector.java new file mode 100644 index 0000000..06a9151 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewAnnotationIntrospector.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; + +import java.io.Serial; + +/** + * 自定义 Jackson 注解内省器 + *

+ * 让 Jackson 序列化引擎识别字段上的 {@link BladeView} 注解, + * 将其等同于标准的 {@code @JsonView} 处理。 + *

+ *

+ * 继承 {@link JacksonAnnotationIntrospector},所有标准 Jackson 注解 + * ({@code @JsonIgnore}、{@code @JsonSerialize}、{@code @JsonFormat} 等) + * 通过 super 正常工作。 + *

+ * + * @author Chill + */ +public class BladeViewAnnotationIntrospector extends JacksonAnnotationIntrospector { + + @Serial + private static final long serialVersionUID = 1L; + + @Override + public Class[] findViews(Annotated a) { + // 优先识别 @BladeView(仅处理非 Auto 的静态视图标注) + BladeView bladeView = _findAnnotation(a, BladeView.class); + if (bladeView != null && bladeView.value() != BladeView.Auto.class) { + return new Class[]{bladeView.value()}; + } + // 兼容:仍然识别标准 @JsonView + return super.findViews(a); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewCustomizer.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewCustomizer.java new file mode 100644 index 0000000..f2bd2c7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewCustomizer.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +/** + * 视图自定义扩展接口 + *

+ * 用户可通过实现此接口来注册自定义视图层级, + * 注册为 Spring Bean 即可生效。 + *

+ * + *

示例:新增超管以上的超级视图

+ *
+ * // 1. 定义自定义视图接口
+ * public interface SuperView extends Views.Administrator {}
+ *
+ * // 2. 注册到解析器
+ * @Bean
+ * public BladeViewCustomizer myViewCustomizer() {
+ *     return resolver -> resolver.registerView("super", SuperView.class, 4);
+ * }
+ *
+ * // 3. 在 YAML 中映射角色
+ * // blade.jackson.view.role-mapping.superadmin: super
+ * 
+ * + * @author Chill + */ +@FunctionalInterface +public interface BladeViewCustomizer { + + /** + * 自定义视图注册 + * + * @param resolver 视图解析器 + */ + void customize(BladeViewResolver resolver); + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResolver.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResolver.java new file mode 100644 index 0000000..60db3db --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResolver.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 视图解析器 — 视图名/角色名 → 视图 Class + *

+ * 内置四级视图:summary(0) → detail(1) → admin(2) → administrator(3) + * 可通过 {@link BladeViewCustomizer} 注册自定义视图层级 + *

+ * + * @author Chill + */ +public class BladeViewResolver { + + private final BladeJacksonProperties.View viewProperties; + + /** + * 视图名称 → 视图 Class 映射(线程安全) + */ + private final Map> viewMapping = new ConcurrentHashMap<>(); + + /** + * 视图名称 → 优先级映射(数字越大优先级越高,线程安全) + */ + private final Map priorityMapping = new ConcurrentHashMap<>(); + + public BladeViewResolver(BladeJacksonProperties.View viewProperties) { + this.viewProperties = viewProperties; + // 注册内置四级视图 + registerView("summary", Views.Summary.class, 0); + registerView("list", Views.Summary.class, 0); + registerView("detail", Views.Detail.class, 1); + registerView("admin", Views.Admin.class, 2); + registerView("administrator", Views.Administrator.class, 3); + } + + /** + * 注册自定义视图层级 + * + * @param name 视图名称(不区分大小写) + * @param viewClass 视图 Class(建议继承 {@link Views} 中的接口) + * @param priority 优先级(数字越大权限越高,内置:summary=0, detail=1, admin=2, administrator=3) + */ + public void registerView(String name, Class viewClass, int priority) { + viewMapping.put(name.toLowerCase(), viewClass); + priorityMapping.put(name.toLowerCase(), priority); + } + + /** + * 视图名称 → Class(未知名称降级到 Summary — 最小权限原则) + * + * @param viewName 视图名称 + * @return 视图 Class + */ + public Class resolve(String viewName) { + return viewMapping.getOrDefault(viewName.toLowerCase(), Views.Summary.class); + } + + /** + * 根据角色名解析视图(支持逗号分隔多角色,取最高权限) + * + * @param roleName 角色名(可逗号分隔) + * @return 视图 Class + */ + public Class resolveByRole(String roleName) { + if (roleName == null || roleName.isEmpty()) { + return resolve(viewProperties.getDefaultView()); + } + + Map mapping = viewProperties.getRoleMapping(); + String bestView = null; + int bestPriority = -1; + + for (String role : roleName.split(",")) { + String viewName = mapping.get(role.trim().toLowerCase()); + if (viewName != null) { + int priority = priorityMapping.getOrDefault(viewName.toLowerCase(), -1); + if (priority > bestPriority) { + bestPriority = priority; + bestView = viewName; + } + } + } + + return resolve(bestView != null ? bestView : viewProperties.getDefaultView()); + } + + /** + * 获取默认视图 + * + * @return 默认视图 Class + */ + public Class getDefaultView() { + return resolve(viewProperties.getDefaultView()); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResponseAdvice.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResponseAdvice.java new file mode 100644 index 0000000..0622e96 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeViewResponseAdvice.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +import lombok.RequiredArgsConstructor; +import net.dreamlu.mica.auto.annotation.AutoIgnore; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * {@link BladeView} 响应拦截器 + *

+ * 拦截 Controller 方法/类上的 {@link BladeView} 注解, + * 解析视图 Class 后包装为 {@link MappingJacksonValue} 交给 Jackson 处理。 + *

+ * + * @author Chill + */ +@AutoIgnore +@ControllerAdvice +@RequiredArgsConstructor +public class BladeViewResponseAdvice implements ResponseBodyAdvice { + + private final BladeViewResolver viewResolver; + + @Nullable + private final BladeRoleSupplier roleNameSupplier; + + @Override + public boolean supports(@NonNull MethodParameter returnType, + @NonNull Class> converterType) { + return returnType.hasMethodAnnotation(BladeView.class) + || returnType.getDeclaringClass().isAnnotationPresent(BladeView.class); + } + + @Override + public Object beforeBodyWrite(@Nullable Object body, + @NonNull MethodParameter returnType, + @NonNull MediaType selectedContentType, + @NonNull Class> selectedConverterType, + @NonNull ServerHttpRequest request, + @NonNull ServerHttpResponse response) { + if (body == null) { + return null; + } + + // 方法级优先于类级 + BladeView annotation = returnType.getMethodAnnotation(BladeView.class); + if (annotation == null) { + annotation = returnType.getDeclaringClass().getAnnotation(BladeView.class); + } + if (annotation == null) { + return body; + } + + Class viewClass = resolveViewClass(annotation); + + MappingJacksonValue container; + if (body instanceof MappingJacksonValue) { + container = (MappingJacksonValue) body; + } else { + container = new MappingJacksonValue(body); + } + container.setSerializationView(viewClass); + return container; + } + + private Class resolveViewClass(BladeView annotation) { + if (annotation.value() != BladeView.Auto.class) { + return annotation.value(); + } + if (roleNameSupplier != null) { + return viewResolver.resolveByRole(roleNameSupplier.get()); + } + return viewResolver.getDefaultView(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java index 5759390..da43db1 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.type.CollectionLikeType; @@ -32,6 +33,7 @@ import org.springframework.lang.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.Serial; import java.text.SimpleDateFormat; import java.time.ZoneId; import java.util.*; @@ -949,41 +951,45 @@ public class JsonUtil { } public static class JacksonObjectMapper extends ObjectMapper { + @Serial private static final long serialVersionUID = 4288193147502386170L; private static final Locale CHINA = Locale.CHINA; + public JacksonObjectMapper(ObjectMapper src) { + super(src); + } + public JacksonObjectMapper() { - super(); + // 通过 Builder 设置 Feature + super(JsonMapper.builder() + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) //默认视图包含 + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) //允许未转义的控制字符 + .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER) //允许反斜杠转义任意字符 + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) //允许单引号 + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) //允许空字符串反序列化为null对象 + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) //日期不序列化为时间戳 + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) //序列化空对象不报错 + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) //反序列化遇到未知属性不报错 + .build()); + //设置地点为中国 super.setLocale(CHINA); - //去掉默认的时间戳格式 - super.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); //设置为中国上海时区 super.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); //序列化时,日期的统一格式 super.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA)); - //序列化处理 - super.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true); - super.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true); - super.findAndRegisterModules(); - //失败处理 - super.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - super.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - //单引号处理 - super.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); - //反序列化时,属性不存在的兼容处理s - super.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); //日期格式化 super.registerModule(new BladeJavaTimeModule()); - //允许空字符串序列化为null对象 - super.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); + //视图注解内省器,让 Jackson 识别 @BladeView + super.setAnnotationIntrospector(new BladeViewAnnotationIntrospector()); + //注册模块 super.findAndRegisterModules(); } @Override public ObjectMapper copy() { - return super.copy(); + return new JacksonObjectMapper(this); } } diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/Views.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/Views.java new file mode 100644 index 0000000..43f7490 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/Views.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com). + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.tool.jackson; + +/** + * BladeX 全局视图层级定义 + *

+ * 四级视图:Summary → Detail → Admin → Administrator + * 继承即包含:高级视图自动包含所有低级视图的字段 + * 未标注 {@link BladeView} 的字段在任何视图下始终输出 + *

+ * + * @author Chill + */ +public final class Views { + + private Views() { + } + + /** + * 摘要视图 — 列表、下拉、搜索 + */ + public interface Summary { + } + + /** + * 详情视图 — 详情页、个人中心 + */ + public interface Detail extends Summary { + } + + /** + * 管理视图 — 普通管理员(admin) + */ + public interface Admin extends Detail { + } + + /** + * 超管视图 — 超级管理员(administrator) + */ + public interface Administrator extends Admin { + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java index 05fb66f..3ed3383 100644 --- a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java @@ -24,6 +24,8 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.springblade.core.tool.jackson.BladeView; +import org.springblade.core.tool.jackson.Views; import org.springblade.core.tool.utils.DateUtil; import org.springframework.format.annotation.DateTimeFormat; @@ -49,6 +51,7 @@ public class BaseEntity implements Serializable { /** * 创建人 */ + @BladeView(Views.Admin.class) @JsonSerialize(using = ToStringSerializer.class) @Schema(description = "创建人", hidden = true) private Long createUser; @@ -56,6 +59,7 @@ public class BaseEntity implements Serializable { /** * 创建部门 */ + @BladeView(Views.Admin.class) @JsonSerialize(using = ToStringSerializer.class) @Schema(description = "创建部门", hidden = true) private Long createDept; @@ -63,6 +67,7 @@ public class BaseEntity implements Serializable { /** * 创建时间 */ + @BladeView(Views.Detail.class) @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) @Schema(description = "创建时间", hidden = true) @@ -71,6 +76,7 @@ public class BaseEntity implements Serializable { /** * 更新人 */ + @BladeView(Views.Admin.class) @JsonSerialize(using = ToStringSerializer.class) @Schema(description = "更新人", hidden = true) private Long updateUser; @@ -78,6 +84,7 @@ public class BaseEntity implements Serializable { /** * 更新时间 */ + @BladeView(Views.Detail.class) @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) @Schema(description = "更新时间", hidden = true) @@ -86,12 +93,14 @@ public class BaseEntity implements Serializable { /** * 状态[1:正常] */ + @BladeView(Views.Detail.class) @Schema(description = "业务状态", hidden = true) private Integer status; /** * 状态[0:未删除,1:删除] */ + @BladeView(Views.Admin.class) @TableLogic @Schema(description = "是否已删除", hidden = true) private Integer isDeleted;