From 85dede385b659dab29c69ce2e404635fab78406e Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sat, 11 Jul 2026 10:21:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(system):=20=E5=A2=9E=E5=BC=BA=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=BA=94=E7=94=A8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=B8=8E=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完善 MobileApp 配置读写、错误与校验文案, 补充 mobile 相关能力。 --- .../i18n/en-US/error/mobile_app.json | 7 +- .../i18n/zh-CN/error/mobile_app.json | 7 +- .../config/mobile/MobileAppController.java | 9 + .../convert/mobile/MobileAppConvert.java | 16 +- .../system/dao/mobile/MobileAppManager.java | 18 + .../system/entity/mobile/MobileApp.java | 13 +- .../mobile/config/AlipayMiniAppConfig.java | 34 ++ .../system/mobile/config/DyMiniAppConfig.java | 18 + .../system/mobile/config/WxMiniAppConfig.java | 21 + .../convert/MobileAppConfigConvert.java | 46 +++ .../param/AlipayMiniAppConfigParam.java | 38 ++ .../config/param/DyMiniAppConfigParam.java | 20 + .../config/param/WxMiniAppConfigParam.java | 25 ++ .../result/AlipayMiniAppConfigResult.java | 39 ++ .../config/result/DyMiniAppConfigResult.java | 20 + .../config/result/WxMiniAppConfigResult.java | 23 ++ .../system/param/mobile/MobileAppParam.java | 38 +- .../system/result/mobile/MobileAppResult.java | 18 +- .../service/mobile/MobileAppService.java | 366 ++++++++++++++---- 19 files changed, 675 insertions(+), 101 deletions(-) create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/AlipayMiniAppConfig.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/WxMiniAppConfig.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/convert/MobileAppConfigConvert.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/AlipayMiniAppConfigParam.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/DyMiniAppConfigParam.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/WxMiniAppConfigParam.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/AlipayMiniAppConfigResult.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/DyMiniAppConfigResult.java create mode 100644 daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/WxMiniAppConfigResult.java diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/mobile_app.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/mobile_app.json index 7f32c0e5d..85c65e282 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/mobile_app.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/mobile_app.json @@ -1,5 +1,10 @@ { "notExist": "Mobile app configuration does not exist", + "notEnabled": "Mobile app configuration is disabled: {0}/{1}", "appTypeNotFound": "No matching mobile app type found: {0}", - "platformNotFound": "No matching mobile platform found: {0}" + "platformNotFound": "No matching mobile platform found: {0}", + "platformNotAllowed": "App type {0} does not support mobile platform {1}", + "configRequired": "Please submit app config for platform {0}", + "configPlatformMismatch": "App config does not match platform {0}", + "alipayCertRequired": "In certificate mode, please upload app cert, Alipay cert and root cert" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/mobile_app.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/mobile_app.json index 6ac745e97..0be48133a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/mobile_app.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/mobile_app.json @@ -1,5 +1,10 @@ { "notExist": "移动端应用配置不存在", + "notEnabled": "移动端应用配置未启用: {0}/{1}", "appTypeNotFound": "未找到对应的移动端应用端类型: {0}", - "platformNotFound": "未找到对应的移动端平台: {0}" + "platformNotFound": "未找到对应的移动端平台: {0}", + "platformNotAllowed": "端类型 {0} 不支持移动平台 {1}", + "configRequired": "请提交与平台 {0} 对应的应用配置", + "configPlatformMismatch": "应用配置与平台 {0} 不匹配", + "alipayCertRequired": "证书模式下请上传应用公钥证书、支付宝公钥证书与根证书" } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/mobile/MobileAppController.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/mobile/MobileAppController.java index a70998b32..5c9e93d8f 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/mobile/MobileAppController.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/mobile/MobileAppController.java @@ -53,6 +53,15 @@ public class MobileAppController { return Res.ok(mobileAppService.findById(id)); } + @PermCode(code = "view", nameCn = "移动端应用查看", nameEn = "Mobile App View") + @Operation(summary = "按端类型+平台查询(不存在返回 null)") + @GetMapping("/get-by-type-platform") + public Result findByAppTypeAndPlatform( + @NotBlank(message = "{validation.field.appType.notBlank}") String appType, + @NotBlank(message = "{validation.field.platform.notBlank}") String platform) { + return Res.ok(mobileAppService.findByAppTypeAndPlatform(appType, platform).orElse(null)); + } + @PermCode(code = "manage", nameCn = "移动端应用管理", nameEn = "Mobile App Manage") @Operation(summary = "保存(按端类型+平台 upsert)") @PostMapping("/save") diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/mobile/MobileAppConvert.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/mobile/MobileAppConvert.java index 99aba15c3..0a2cd036a 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/mobile/MobileAppConvert.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/mobile/MobileAppConvert.java @@ -5,26 +5,32 @@ import cn.daxpay.open.platform.system.param.mobile.MobileAppParam; import cn.daxpay.open.platform.system.result.mobile.MobileAppResult; import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; -/// # 移动端应用配置转换 +/// # 移动端应用配置外壳转换 /// -/// copy 使用 IGNORE 策略, 前端未传的敏感字段(null)不会覆盖数据库原值, -/// 与前端 diffForm + 后端脱敏回显双重保护配合, 避免误清空。 +/// 嵌套平台配置与 appConfig 密文由 [cn.daxpay.open.platform.system.service.mobile.MobileAppService] 处理, +/// 此处仅映射外壳字段。 @Mapper public interface MobileAppConvert { MobileAppConvert CONVERT = Mappers.getMapper(MobileAppConvert.class); - /// 实体转结果 + /// 实体转结果(嵌套配置由 Service 填充) + @Mapping(target = "wxMini", ignore = true) + @Mapping(target = "alipayMini", ignore = true) + @Mapping(target = "dyMini", ignore = true) MobileAppResult toResult(MobileApp entity); - /// 参数转实体(新增) + /// 参数转实体(新增, appConfig 由 Service 序列化后写入) + @Mapping(target = "appConfig", ignore = true) MobileApp toEntity(MobileAppParam param); /// 参数拷贝到实体(更新) @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + @Mapping(target = "appConfig", ignore = true) void copy(MobileAppParam param, @MappingTarget MobileApp entity); } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/mobile/MobileAppManager.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/mobile/MobileAppManager.java index 3edc0ae21..f3b308f40 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/mobile/MobileAppManager.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/mobile/MobileAppManager.java @@ -4,7 +4,25 @@ import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; import cn.daxpay.open.platform.system.entity.mobile.MobileApp; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + /// # 移动端应用配置管理器 @Repository public class MobileAppManager extends BaseManager { + + /// 按端类型 + 移动平台查询(唯一键) + public Optional findByAppTypeAndPlatform(String appType, String platform) { + return lambdaQuery() + .eq(MobileApp::getAppType, appType) + .eq(MobileApp::getPlatform, platform) + .oneOpt(); + } + + /// 按端类型查询全部平台配置 + public List findAllByAppType(String appType) { + return lambdaQuery() + .eq(MobileApp::getAppType, appType) + .list(); + } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/mobile/MobileApp.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/mobile/MobileApp.java index 08c3921bc..eb394fdc3 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/mobile/MobileApp.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/mobile/MobileApp.java @@ -3,6 +3,7 @@ package cn.daxpay.open.platform.system.entity.mobile; import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; +import cn.daxpay.open.platform.common.mybatisplus.handler.type.JsonbStringTypeHandler; import cn.daxpay.open.platform.system.convert.mobile.MobileAppConvert; import cn.daxpay.open.platform.system.result.mobile.MobileAppResult; import com.baomidou.mybatisplus.annotation.TableField; @@ -14,7 +15,8 @@ import lombok.experimental.Accessors; /// # 移动端应用配置 /// /// 平台级移动端应用配置, 按端类型(appType)+移动平台(platform)维度, 每组合一条记录。 -/// app_config/notify_config 以 JSON 文本存储, 通过 [DataEncryptTypeHandler] AES-256-GCM 加密入库。 +/// - app_config: text, 含密钥, 经 [DataEncryptTypeHandler] AES-256-GCM 加密入库 +/// - notify_config: jsonb, 通知模板/开关等非敏感配置, 经 [JsonbStringTypeHandler] 明文存储 @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) @@ -29,15 +31,12 @@ public class MobileApp extends MpBaseEntity implements ToResult /// @see cn.daxpay.open.platform.system.enums.MobilePlatformEnum private String platform; - /// 应用名称(展示用) - private String appName; - - /// 平台特有密钥配置(JSON文本, 加密存储) + /// 平台特有密钥配置(JSON文本, 加密存储为 text) @TableField(typeHandler = DataEncryptTypeHandler.class) private String appConfig; - /// 消息通知配置(JSON文本, 加密存储) - @TableField(typeHandler = DataEncryptTypeHandler.class) + /// 消息通知配置(jsonb, 非敏感, 明文; Java 侧为原始 JSON 字符串) + @TableField(typeHandler = JsonbStringTypeHandler.class) private String notifyConfig; /// 是否启用第三方账号用户绑定 diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/AlipayMiniAppConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/AlipayMiniAppConfig.java new file mode 100644 index 000000000..04ebb6389 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/AlipayMiniAppConfig.java @@ -0,0 +1,34 @@ +package cn.daxpay.open.platform.system.mobile.config; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 支付宝小程序应用配置(落库 JSON 形状) +/// +/// 序列化后写入 [cn.daxpay.open.platform.system.entity.mobile.MobileApp#appConfig] 并加密存储。 +/// authType 与平台支付宝认证一致: public_key / cert。 +@Data +@Accessors(chain = true) +public class AlipayMiniAppConfig { + + /// 小程序 AppId + private String appId; + + /// 鉴权方式: public_key(公钥) / cert(证书) + private String authType; + + /// 应用私钥 + private String privateKey; + + /// 支付宝公钥(公钥模式) + private String alipayPublicKey; + + /// 应用公钥证书(证书模式) + private String appCert; + + /// 支付宝公钥证书(证书模式) + private String alipayCert; + + /// 支付宝根证书(证书模式) + private String alipayRootCert; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java new file mode 100644 index 000000000..1abc80d46 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/DyMiniAppConfig.java @@ -0,0 +1,18 @@ +package cn.daxpay.open.platform.system.mobile.config; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 抖音小程序应用配置(落库 JSON 形状, 预留) +/// +/// 序列化后写入 [cn.daxpay.open.platform.system.entity.mobile.MobileApp#appConfig] 并加密存储。 +@Data +@Accessors(chain = true) +public class DyMiniAppConfig { + + /// 小程序 AppId + private String appId; + + /// 小程序 AppSecret + private String appSecret; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/WxMiniAppConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/WxMiniAppConfig.java new file mode 100644 index 000000000..1f3dab5aa --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/WxMiniAppConfig.java @@ -0,0 +1,21 @@ +package cn.daxpay.open.platform.system.mobile.config; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 微信小程序应用配置(落库 JSON 形状) +/// +/// 序列化后写入 [cn.daxpay.open.platform.system.entity.mobile.MobileApp#appConfig] 并加密存储。 +@Data +@Accessors(chain = true) +public class WxMiniAppConfig { + + /// 小程序 AppId + private String appId; + + /// 小程序 AppSecret + private String appSecret; + + /// 原始 ID(gh_ 开头, 可选) + private String originalId; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/convert/MobileAppConfigConvert.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/convert/MobileAppConfigConvert.java new file mode 100644 index 000000000..709156df2 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/convert/MobileAppConfigConvert.java @@ -0,0 +1,46 @@ +package cn.daxpay.open.platform.system.mobile.config.convert; + +import cn.daxpay.open.platform.system.mobile.config.AlipayMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.DyMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.WxMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.param.AlipayMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.DyMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.WxMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.result.AlipayMiniAppConfigResult; +import cn.daxpay.open.platform.system.mobile.config.result.DyMiniAppConfigResult; +import cn.daxpay.open.platform.system.mobile.config.result.WxMiniAppConfigResult; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/// # 移动端各平台应用配置转换 +/// +/// Param → Config 使用 IGNORE, 空敏感字段不覆盖目标(merge 时先 copy 旧值再 overlay)。 +@Mapper +public interface MobileAppConfigConvert { + + MobileAppConfigConvert CONVERT = Mappers.getMapper(MobileAppConfigConvert.class); + + WxMiniAppConfig toConfig(WxMiniAppConfigParam param); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void copy(WxMiniAppConfigParam param, @MappingTarget WxMiniAppConfig config); + + WxMiniAppConfigResult toResult(WxMiniAppConfig config); + + AlipayMiniAppConfig toConfig(AlipayMiniAppConfigParam param); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void copy(AlipayMiniAppConfigParam param, @MappingTarget AlipayMiniAppConfig config); + + AlipayMiniAppConfigResult toResult(AlipayMiniAppConfig config); + + DyMiniAppConfig toConfig(DyMiniAppConfigParam param); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void copy(DyMiniAppConfigParam param, @MappingTarget DyMiniAppConfig config); + + DyMiniAppConfigResult toResult(DyMiniAppConfig config); +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/AlipayMiniAppConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/AlipayMiniAppConfigParam.java new file mode 100644 index 000000000..0790b8847 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/AlipayMiniAppConfigParam.java @@ -0,0 +1,38 @@ +package cn.daxpay.open.platform.system.mobile.config.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 支付宝小程序应用配置入参 +/// +/// 敏感字段为空表示不更新; 公钥/证书材料互斥校验在 Service 按新建/更新处理。 +@Data +@Accessors(chain = true) +@Schema(title = "支付宝小程序应用配置参数") +public class AlipayMiniAppConfigParam { + + @NotBlank(message = "{validation.field.appId.notBlank}") + @Schema(description = "小程序 AppId") + private String appId; + + @NotBlank(message = "{validation.field.authType.notBlank}") + @Schema(description = "鉴权方式: public_key / cert") + private String authType; + + @Schema(description = "应用私钥(空则不更新)") + private String privateKey; + + @Schema(description = "支付宝公钥(公钥模式, 空则不更新)") + private String alipayPublicKey; + + @Schema(description = "应用公钥证书(证书模式, 空则不更新)") + private String appCert; + + @Schema(description = "支付宝公钥证书(证书模式, 空则不更新)") + private String alipayCert; + + @Schema(description = "支付宝根证书(证书模式, 空则不更新)") + private String alipayRootCert; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/DyMiniAppConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/DyMiniAppConfigParam.java new file mode 100644 index 000000000..269a63ee6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/DyMiniAppConfigParam.java @@ -0,0 +1,20 @@ +package cn.daxpay.open.platform.system.mobile.config.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 抖音小程序应用配置入参(预留) +@Data +@Accessors(chain = true) +@Schema(title = "抖音小程序应用配置参数") +public class DyMiniAppConfigParam { + + @NotBlank(message = "{validation.field.appId.notBlank}") + @Schema(description = "小程序 AppId") + private String appId; + + @Schema(description = "小程序 AppSecret(空则不更新)") + private String appSecret; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/WxMiniAppConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/WxMiniAppConfigParam.java new file mode 100644 index 000000000..892381e22 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/param/WxMiniAppConfigParam.java @@ -0,0 +1,25 @@ +package cn.daxpay.open.platform.system.mobile.config.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 微信小程序应用配置入参 +/// +/// 敏感字段 appSecret 为空表示不更新(配合前端 diffForm)。 +@Data +@Accessors(chain = true) +@Schema(title = "微信小程序应用配置参数") +public class WxMiniAppConfigParam { + + @NotBlank(message = "{validation.field.appId.notBlank}") + @Schema(description = "小程序 AppId") + private String appId; + + @Schema(description = "小程序 AppSecret(空则不更新)") + private String appSecret; + + @Schema(description = "原始 ID(gh_ 开头, 可选)") + private String originalId; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/AlipayMiniAppConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/AlipayMiniAppConfigResult.java new file mode 100644 index 000000000..9c44f71b7 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/AlipayMiniAppConfigResult.java @@ -0,0 +1,39 @@ +package cn.daxpay.open.platform.system.mobile.config.result; + +import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 支付宝小程序应用配置出参(敏感字段脱敏) +@Data +@Accessors(chain = true) +@Schema(title = "支付宝小程序应用配置") +public class AlipayMiniAppConfigResult { + + @Schema(description = "小程序 AppId") + private String appId; + + @Schema(description = "鉴权方式: public_key / cert") + private String authType; + + @SensitiveInfo + @Schema(description = "应用私钥") + private String privateKey; + + @SensitiveInfo + @Schema(description = "支付宝公钥") + private String alipayPublicKey; + + @SensitiveInfo + @Schema(description = "应用公钥证书") + private String appCert; + + @SensitiveInfo + @Schema(description = "支付宝公钥证书") + private String alipayCert; + + @SensitiveInfo + @Schema(description = "支付宝根证书") + private String alipayRootCert; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/DyMiniAppConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/DyMiniAppConfigResult.java new file mode 100644 index 000000000..9847c0121 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/DyMiniAppConfigResult.java @@ -0,0 +1,20 @@ +package cn.daxpay.open.platform.system.mobile.config.result; + +import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 抖音小程序应用配置出参(预留, 敏感字段脱敏) +@Data +@Accessors(chain = true) +@Schema(title = "抖音小程序应用配置") +public class DyMiniAppConfigResult { + + @Schema(description = "小程序 AppId") + private String appId; + + @SensitiveInfo + @Schema(description = "小程序 AppSecret") + private String appSecret; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/WxMiniAppConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/WxMiniAppConfigResult.java new file mode 100644 index 000000000..370e88696 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/mobile/config/result/WxMiniAppConfigResult.java @@ -0,0 +1,23 @@ +package cn.daxpay.open.platform.system.mobile.config.result; + +import cn.daxpay.open.platform.common.json.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 微信小程序应用配置出参(敏感字段脱敏) +@Data +@Accessors(chain = true) +@Schema(title = "微信小程序应用配置") +public class WxMiniAppConfigResult { + + @Schema(description = "小程序 AppId") + private String appId; + + @SensitiveInfo + @Schema(description = "小程序 AppSecret") + private String appSecret; + + @Schema(description = "原始 ID") + private String originalId; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/mobile/MobileAppParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/mobile/MobileAppParam.java index 04d3daf27..ca89e7289 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/mobile/MobileAppParam.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/mobile/MobileAppParam.java @@ -1,33 +1,53 @@ package cn.daxpay.open.platform.system.param.mobile; +import cn.daxpay.open.platform.system.mobile.config.param.AlipayMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.DyMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.WxMiniAppConfigParam; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.experimental.Accessors; /// # 移动端应用配置参数 /// -/// 保存时按 appType + platform 进行 upsert, 二者组合为唯一键。 +/// 保存时按 appType + platform 进行 upsert。平台密钥走嵌套强类型对象, 与 platform 一一对应: +/// - wx_mini → wxMini +/// - alipay_mini → alipayMini +/// - dy_mini → dyMini @Data @Accessors(chain = true) @Schema(title = "移动端应用配置参数") public class MobileAppParam { - @Schema(description = "主键(更新时必填)") + @Schema(description = "主键(更新时传)") private Long id; - @Schema(description = "端类型") + /// 端类型 + /// @see cn.daxpay.open.platform.system.enums.MobileAppTypeEnum + @NotBlank(message = "{validation.field.appType.notBlank}") + @Schema(description = "端类型: merchant/admin/cashier") private String appType; - @Schema(description = "移动平台") + /// 移动平台 + /// @see cn.daxpay.open.platform.system.enums.MobilePlatformEnum + @NotBlank(message = "{validation.field.platform.notBlank}") + @Schema(description = "移动平台: wx_h5/wx_mini/alipay_mini/dy_mini/android/ios") private String platform; - @Schema(description = "应用名称") - private String appName; + @Valid + @Schema(description = "微信小程序配置(platform=wx_mini 时必填)") + private WxMiniAppConfigParam wxMini; - @Schema(description = "平台特有密钥配置(JSON文本)") - private String appConfig; + @Valid + @Schema(description = "支付宝小程序配置(platform=alipay_mini 时必填)") + private AlipayMiniAppConfigParam alipayMini; - @Schema(description = "消息通知配置(JSON文本)") + @Valid + @Schema(description = "抖音小程序配置(platform=dy_mini 时必填, 预留)") + private DyMiniAppConfigParam dyMini; + + @Schema(description = "消息通知配置(jsonb 原始JSON文本, 明文非敏感)") private String notifyConfig; @Schema(description = "是否启用第三方账号用户绑定") diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/mobile/MobileAppResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/mobile/MobileAppResult.java index 7b43411e2..36e1308d7 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/mobile/MobileAppResult.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/mobile/MobileAppResult.java @@ -1,12 +1,17 @@ package cn.daxpay.open.platform.system.result.mobile; import cn.daxpay.open.platform.core.result.BaseResult; +import cn.daxpay.open.platform.system.mobile.config.result.AlipayMiniAppConfigResult; +import cn.daxpay.open.platform.system.mobile.config.result.DyMiniAppConfigResult; +import cn.daxpay.open.platform.system.mobile.config.result.WxMiniAppConfigResult; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; /// # 移动端应用配置结果 +/// +/// 平台密钥以嵌套强类型返回(敏感字段脱敏), 按 platform 仅填充对应嵌套。 @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) @@ -19,13 +24,16 @@ public class MobileAppResult extends BaseResult { @Schema(description = "移动平台") private String platform; - @Schema(description = "应用名称") - private String appName; + @Schema(description = "微信小程序配置") + private WxMiniAppConfigResult wxMini; - @Schema(description = "平台特有密钥配置(JSON文本)") - private String appConfig; + @Schema(description = "支付宝小程序配置") + private AlipayMiniAppConfigResult alipayMini; - @Schema(description = "消息通知配置(JSON文本)") + @Schema(description = "抖音小程序配置") + private DyMiniAppConfigResult dyMini; + + @Schema(description = "消息通知配置(jsonb 原始JSON文本, 明文非敏感)") private String notifyConfig; @Schema(description = "是否启用第三方账号用户绑定") diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/mobile/MobileAppService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/mobile/MobileAppService.java index 461f5613a..7a6767f34 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/mobile/MobileAppService.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/mobile/MobileAppService.java @@ -1,41 +1,64 @@ package cn.daxpay.open.platform.system.service.mobile; +import cn.daxpay.open.platform.capability.alipay.auth.config.AlipayAuthTypeEnum; import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; import cn.daxpay.open.platform.system.convert.mobile.MobileAppConvert; import cn.daxpay.open.platform.system.dao.mobile.MobileAppManager; import cn.daxpay.open.platform.system.entity.mobile.MobileApp; +import cn.daxpay.open.platform.system.enums.MobileAppTypeEnum; +import cn.daxpay.open.platform.system.enums.MobilePlatformEnum; +import cn.daxpay.open.platform.system.mobile.config.AlipayMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.DyMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.WxMiniAppConfig; +import cn.daxpay.open.platform.system.mobile.config.convert.MobileAppConfigConvert; +import cn.daxpay.open.platform.system.mobile.config.param.AlipayMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.DyMiniAppConfigParam; +import cn.daxpay.open.platform.system.mobile.config.param.WxMiniAppConfigParam; import cn.daxpay.open.platform.system.param.mobile.MobileAppParam; import cn.daxpay.open.platform.system.result.mobile.MobileAppResult; -import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import tools.jackson.core.type.TypeReference; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /// # 移动端应用配置服务 /// -/// 平台级配置, 按端类型(appType)+移动平台(platform)维度管理。 -/// app_config/notify_config 经 [cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler] 加密入库; -/// 返回前端时对 app_config 内敏感键(appSecret/privateKey/alipayPublicKey/证书等)脱敏; -/// 保存时敏感键为空则保留库中原值(配合前端 diffForm)。 +/// 平台级配置, 按端类型(appType)+移动平台(platform)维度管理, 每组合最多一条(唯一键)。 +/// - app_config: 强类型 Config 序列化后经 [DataEncryptTypeHandler] 加密入库 +/// - API 层: wxMini / alipayMini / dyMini 嵌套对象; notify_config 仍为明文 jsonb 字符串 +/// +/// ## 端类型与平台白名单 +/// - merchant: wx_h5 / wx_mini / alipay_mini / dy_mini +/// - admin: wx_mini / alipay_mini / dy_mini +/// - cashier: wx_mini / alipay_mini(抖音本期未开放) @Slf4j @Service @RequiredArgsConstructor public class MobileAppService { - /// app_config 中需要脱敏/保护的敏感键 - /// 含支付宝小程序公钥字段与证书模式三本证书 - private static final Set SENSITIVE_KEYS = Set.of( - "appSecret", "privateKey", "alipayPublicKey", "clientSecret", - "appCert", "alipayCert", "alipayRootCert"); + /// 各端类型允许配置的移动平台(本期开放范围) + private static final Map> ALLOWED_PLATFORMS = Map.of( + MobileAppTypeEnum.MERCHANT, Set.of( + MobilePlatformEnum.WX_H5, + MobilePlatformEnum.WX_MINI, + MobilePlatformEnum.ALIPAY_MINI, + MobilePlatformEnum.DY_MINI), + MobileAppTypeEnum.ADMIN, Set.of( + MobilePlatformEnum.WX_MINI, + MobilePlatformEnum.ALIPAY_MINI, + MobilePlatformEnum.DY_MINI), + MobileAppTypeEnum.CASHIER, Set.of( + MobilePlatformEnum.WX_MINI, + MobilePlatformEnum.ALIPAY_MINI)); private final MobileAppManager manager; @@ -46,37 +69,89 @@ public class MobileAppService { .toList(); } - /// 按端类型查询所有平台配置(端详情页Tab列表) + /// 按端类型查询所有平台配置(端详情页 Tab 列表) public List findAllByAppType(String appType) { - return manager.findAllByField(MobileApp::getAppType, appType).stream() + MobileAppTypeEnum.findByCode(appType); + return manager.findAllByAppType(appType).stream() .map(this::toMaskedResult) .toList(); } - /// 查询单条 + /// 查询单条(运营端, 脱敏) public MobileAppResult findById(Long id) { - // 通用: 移动端应用配置不存在 MobileApp entity = manager.findById(id) .orElseThrow(() -> new DataNotExistException("error.mobile_app.notExist")); return toMaskedResult(entity); } + /// 按端类型+平台查询(运营端, 脱敏; 不存在返回 empty) + public Optional findByAppTypeAndPlatform(String appType, String platform) { + MobileAppTypeEnum.findByCode(appType); + MobilePlatformEnum.findByCode(platform); + return manager.findByAppTypeAndPlatform(appType, platform) + .map(this::toMaskedResult); + } + + /// 运行时读取实体(不脱敏, appConfig 为解密后 JSON 明文) + public MobileApp getEntity(String appType, String platform) { + return manager.findByAppTypeAndPlatform(appType, platform) + .orElseThrow(() -> new DataNotExistException("error.mobile_app.notExist")); + } + + /// 运行时读取已启用的配置实体 + public MobileApp getEnabledEntity(String appType, String platform) { + MobileApp entity = getEntity(appType, platform); + if (!Boolean.TRUE.equals(entity.getEnabled())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.notEnabled", appType, platform); + } + return entity; + } + + /// 运行时: 解析微信小程序配置(明文密钥) + public WxMiniAppConfig getWxMiniConfig(String appType) { + return parseConfig(getEnabledEntity(appType, MobilePlatformEnum.WX_MINI.getCode()).getAppConfig(), + WxMiniAppConfig.class); + } + + /// 运行时: 解析支付宝小程序配置(明文密钥) + public AlipayMiniAppConfig getAlipayMiniConfig(String appType) { + return parseConfig(getEnabledEntity(appType, MobilePlatformEnum.ALIPAY_MINI.getCode()).getAppConfig(), + AlipayMiniAppConfig.class); + } + + /// 运行时: 解析抖音小程序配置(明文密钥) + public DyMiniAppConfig getDyMiniConfig(String appType) { + return parseConfig(getEnabledEntity(appType, MobilePlatformEnum.DY_MINI.getCode()).getAppConfig(), + DyMiniAppConfig.class); + } + /// 保存(按端类型+平台组合 upsert) @Transactional(rollbackFor = Exception.class) public MobileAppResult save(MobileAppParam param) { - var existing = manager.lambdaQuery() - .eq(MobileApp::getAppType, param.getAppType()) - .eq(MobileApp::getPlatform, param.getPlatform()) - .oneOpt(); + MobilePlatformEnum platform = validateSaveParam(param); + String appConfigJson = buildAppConfigJson(param, platform, + manager.findByAppTypeAndPlatform(param.getAppType(), param.getPlatform()) + .map(MobileApp::getAppConfig) + .orElse(null)); + + var existing = manager.findByAppTypeAndPlatform(param.getAppType(), param.getPlatform()); if (existing.isPresent()) { var entity = existing.get(); - // 敏感键为空时保留库中原值, 避免 diffForm 跳过字段后被清空 - param.setAppConfig(mergeSensitiveJson(entity.getAppConfig(), param.getAppConfig())); MobileAppConvert.CONVERT.copy(param, entity); + entity.setAppConfig(appConfigJson); manager.updateById(entity); return toMaskedResult(entity); } + var entity = MobileAppConvert.CONVERT.toEntity(param); + entity.setAppConfig(appConfigJson); + if (entity.getEnabled() == null) { + entity.setEnabled(true); + } + if (entity.getBindingEnabled() == null) { + entity.setBindingEnabled(false); + } manager.save(entity); return toMaskedResult(entity); } @@ -85,75 +160,220 @@ public class MobileAppService { @Transactional(rollbackFor = Exception.class) public void updateEnabled(Long id, Boolean enabled) { var entity = manager.findById(id) - // 通用: 移动端应用配置不存在 .orElseThrow(() -> new DataNotExistException("error.mobile_app.notExist")); entity.setEnabled(enabled); manager.updateById(entity); } - /// 实体转结果并对 app_config 敏感键脱敏 + /// 保存前校验白名单 + platform 与嵌套对象匹配 + private MobilePlatformEnum validateSaveParam(MobileAppParam param) { + MobileAppTypeEnum appType = MobileAppTypeEnum.findByCode(param.getAppType()); + MobilePlatformEnum platform = MobilePlatformEnum.findByCode(param.getPlatform()); + + Set allowed = ALLOWED_PLATFORMS.get(appType); + if (allowed == null || !allowed.contains(platform)) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.platformNotAllowed", + appType.getCode(), platform.getCode()); + } + + // 统计传入的嵌套配置数量, 必须恰好 1 个且与 platform 对应 + int nestedCount = 0; + if (param.getWxMini() != null) { + nestedCount++; + } + if (param.getAlipayMini() != null) { + nestedCount++; + } + if (param.getDyMini() != null) { + nestedCount++; + } + if (nestedCount != 1) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.configRequired", platform.getCode()); + } + + boolean match = switch (platform) { + case WX_MINI -> param.getWxMini() != null; + case ALIPAY_MINI -> param.getAlipayMini() != null; + case DY_MINI -> param.getDyMini() != null; + default -> false; + }; + if (!match) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.configPlatformMismatch", platform.getCode()); + } + return platform; + } + + /// 按平台合并并序列化 app_config JSON + private String buildAppConfigJson(MobileAppParam param, MobilePlatformEnum platform, String oldJson) { + return switch (platform) { + case WX_MINI -> { + WxMiniAppConfig merged = mergeWx(oldJson, param.getWxMini()); + validateWxForCreate(merged, oldJson); + yield JacksonUtil.toJson(merged); + } + case ALIPAY_MINI -> { + AlipayMiniAppConfig merged = mergeAlipay(oldJson, param.getAlipayMini()); + validateAlipayForCreate(merged, oldJson); + yield JacksonUtil.toJson(merged); + } + case DY_MINI -> { + DyMiniAppConfig merged = mergeDy(oldJson, param.getDyMini()); + validateDyForCreate(merged, oldJson); + yield JacksonUtil.toJson(merged); + } + default -> throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.configPlatformMismatch", platform.getCode()); + }; + } + + private WxMiniAppConfig mergeWx(String oldJson, WxMiniAppConfigParam param) { + WxMiniAppConfig config = parseConfigOrNew(oldJson, WxMiniAppConfig.class); + // appId 必填, 总是覆盖 + config.setAppId(param.getAppId()); + if (StrUtil.isNotBlank(param.getAppSecret())) { + config.setAppSecret(param.getAppSecret()); + } + // originalId 允许传空串清空 + if (param.getOriginalId() != null) { + config.setOriginalId(param.getOriginalId()); + } + return config; + } + + private AlipayMiniAppConfig mergeAlipay(String oldJson, AlipayMiniAppConfigParam param) { + AlipayMiniAppConfig config = parseConfigOrNew(oldJson, AlipayMiniAppConfig.class); + config.setAppId(param.getAppId()); + config.setAuthType(param.getAuthType()); + if (StrUtil.isNotBlank(param.getPrivateKey())) { + config.setPrivateKey(param.getPrivateKey()); + } + if (StrUtil.isNotBlank(param.getAlipayPublicKey())) { + config.setAlipayPublicKey(param.getAlipayPublicKey()); + } + if (StrUtil.isNotBlank(param.getAppCert())) { + config.setAppCert(param.getAppCert()); + } + if (StrUtil.isNotBlank(param.getAlipayCert())) { + config.setAlipayCert(param.getAlipayCert()); + } + if (StrUtil.isNotBlank(param.getAlipayRootCert())) { + config.setAlipayRootCert(param.getAlipayRootCert()); + } + return config; + } + + private DyMiniAppConfig mergeDy(String oldJson, DyMiniAppConfigParam param) { + DyMiniAppConfig config = parseConfigOrNew(oldJson, DyMiniAppConfig.class); + config.setAppId(param.getAppId()); + if (StrUtil.isNotBlank(param.getAppSecret())) { + config.setAppSecret(param.getAppSecret()); + } + return config; + } + + /// 新建时强制敏感材料齐全 + private void validateWxForCreate(WxMiniAppConfig config, String oldJson) { + if (StrUtil.isNotBlank(oldJson)) { + return; + } + if (StrUtil.isBlank(config.getAppSecret())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.appSecret.notBlank"); + } + } + + private void validateDyForCreate(DyMiniAppConfig config, String oldJson) { + if (StrUtil.isNotBlank(oldJson)) { + return; + } + if (StrUtil.isBlank(config.getAppSecret())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.appSecret.notBlank"); + } + } + + private void validateAlipayForCreate(AlipayMiniAppConfig config, String oldJson) { + if (StrUtil.isNotBlank(oldJson)) { + return; + } + if (StrUtil.isBlank(config.getPrivateKey())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.privateKey.notBlank"); + } + AlipayAuthTypeEnum authType = AlipayAuthTypeEnum.fromCode(config.getAuthType()); + if (authType.isCert()) { + if (StrUtil.isBlank(config.getAppCert()) + || StrUtil.isBlank(config.getAlipayCert()) + || StrUtil.isBlank(config.getAlipayRootCert())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "error.mobile_app.alipayCertRequired"); + } + } else if (StrUtil.isBlank(config.getAlipayPublicKey())) { + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, + "validation.field.alipayPublicKey.notBlank"); + } + } + + /// 实体 → 脱敏结果(填充对应嵌套配置) private MobileAppResult toMaskedResult(MobileApp entity) { - MobileAppResult result = entity.toResult(); - result.setAppConfig(maskSensitiveJson(result.getAppConfig())); + MobileAppResult result = MobileAppConvert.CONVERT.toResult(entity); + String json = entity.getAppConfig(); + if (StrUtil.isBlank(json) || StrUtil.isBlank(entity.getPlatform())) { + return result; + } + MobilePlatformEnum platform; + try { + platform = MobilePlatformEnum.findByCode(entity.getPlatform()); + } catch (Exception e) { + return result; + } + try { + switch (platform) { + case WX_MINI -> result.setWxMini( + MobileAppConfigConvert.CONVERT.toResult(parseConfig(json, WxMiniAppConfig.class))); + case ALIPAY_MINI -> result.setAlipayMini( + MobileAppConfigConvert.CONVERT.toResult(parseConfig(json, AlipayMiniAppConfig.class))); + case DY_MINI -> result.setDyMini( + MobileAppConfigConvert.CONVERT.toResult(parseConfig(json, DyMiniAppConfig.class))); + default -> { + } + } + } catch (Exception e) { + log.warn("parse appConfig for result failed, platform={}: {}", entity.getPlatform(), e.getMessage()); + } return result; } - /// 对 JSON 文本中的敏感键做脱敏(password 风格), 非 JSON 或空串原样返回 - private String maskSensitiveJson(String json) { + private T parseConfig(String json, Class type) { if (StrUtil.isBlank(json)) { - return json; + throw new DataNotExistException("error.mobile_app.notExist"); } - try { - Map map = JacksonUtil.toBean(json, new TypeReference>() {}); - if (map == null || map.isEmpty()) { - return json; - } - boolean changed = false; - for (String key : SENSITIVE_KEYS) { - Object val = map.get(key); - if (val instanceof String s && StrUtil.isNotBlank(s)) { - map.put(key, DesensitizedUtil.password(s)); - changed = true; - } - } - return changed ? JacksonUtil.toJson(map) : json; - } catch (Exception e) { - log.warn("maskSensitiveJson parse failed, return raw: {}", e.getMessage()); - return json; + T config = JacksonUtil.toBean(json, type); + if (config == null) { + throw new DataNotExistException("error.mobile_app.notExist"); } + return config; } - /// 合并新旧 app_config: 新值中敏感键为空/null 时沿用旧值 - private String mergeSensitiveJson(String oldJson, String newJson) { - if (StrUtil.isBlank(newJson)) { - // 前端未传则整段保留旧值 - return oldJson; + private T parseConfigOrNew(String json, Class type) { + if (StrUtil.isBlank(json)) { + try { + return type.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("create config failed: " + type.getName(), e); + } } - if (StrUtil.isBlank(oldJson)) { - return newJson; + T config = JacksonUtil.toBean(json, type); + if (config != null) { + return config; } try { - Map oldMap = JacksonUtil.toBean(oldJson, new TypeReference>() {}); - Map newMap = JacksonUtil.toBean(newJson, new TypeReference>() {}); - if (oldMap == null) { - oldMap = new LinkedHashMap<>(); - } - if (newMap == null) { - return oldJson; - } - for (String key : SENSITIVE_KEYS) { - Object newVal = newMap.get(key); - if (newVal == null || (newVal instanceof String s && StrUtil.isBlank(s))) { - Object oldVal = oldMap.get(key); - if (oldVal != null) { - newMap.put(key, oldVal); - } - } - } - return JacksonUtil.toJson(newMap); + return type.getDeclaredConstructor().newInstance(); } catch (Exception e) { - log.warn("mergeSensitiveJson parse failed, use newJson: {}", e.getMessage()); - return newJson; + throw new IllegalStateException("create config failed: " + type.getName(), e); } } }