From d5af60142f143e2c75750a1cefdb6a2597f857cb Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Thu, 9 Jul 2026 21:44:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E9=85=8D=E7=BD=AE=E5=8A=A0=E5=AF=86=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E4=B8=8E=E6=95=8F=E6=84=9F=E5=AD=97=E6=AE=B5=E8=84=B1?= =?UTF-8?q?=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完善平台级移动端应用配置: app_config/notify_config 改 DataEncryptTypeHandler 加密入库, 返回脱敏, 保存时敏感键空值保留原值; 补建 pay_platform_mobile_app 表。 --- _config/sql/update-tables.sql | 43 +++++++++ .../app/mobile/convert/MobileAppConvert.java | 6 ++ .../payment/app/mobile/entity/MobileApp.java | 13 +-- .../app/mobile/service/MobileAppService.java | 96 +++++++++++++++++-- 4 files changed, 145 insertions(+), 13 deletions(-) diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index 75352249b..b4675f224 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -158,3 +158,46 @@ CREATE INDEX IF NOT EXISTS idx_pay_platform_wechat_message_record_send_time ON p -- rename iam_social_config -> iam_social_login_config (avoid naming clash with upcoming third-party platform management) ALTER TABLE IF EXISTS iam_social_config RENAME TO iam_social_login_config; + +-- ---------------------------- +-- 平台级移动端应用配置(按 app_type + platform 维度, 每组合一条) +-- app_config/notify_config 使用 text + DataEncryptTypeHandler 加密存储(密文非合法JSON, 故不用 jsonb) +-- ---------------------------- +CREATE TABLE IF NOT EXISTS pay_platform_mobile_app ( + id int8 NOT NULL, + app_type varchar(32) NOT NULL, + platform varchar(32) NOT NULL, + app_name varchar(64), + app_config text, + notify_config text, + binding_enabled bool NOT NULL DEFAULT false, + enabled bool NOT NULL DEFAULT true, + remark varchar(500), + creator int8, + create_time timestamptz(6), + last_modifier int8, + last_modified_time timestamptz(6), + version int4 NOT NULL DEFAULT 0, + deleted bool NOT NULL DEFAULT false, + CONSTRAINT pk_pay_platform_mobile_app PRIMARY KEY (id) +); + +COMMENT ON TABLE pay_platform_mobile_app IS '平台级移动端应用配置'; +COMMENT ON COLUMN pay_platform_mobile_app.id IS '主键'; +COMMENT ON COLUMN pay_platform_mobile_app.app_type IS '端类型: merchant-商户端 / admin-管理端 / cashier-收银台'; +COMMENT ON COLUMN pay_platform_mobile_app.platform IS '移动平台: wx_h5/wx_mini/alipay_mini/dy_mini/android/ios'; +COMMENT ON COLUMN pay_platform_mobile_app.app_name IS '应用名称(展示用)'; +COMMENT ON COLUMN pay_platform_mobile_app.app_config IS '平台特有密钥配置(JSON文本, AES-256-GCM加密存储)'; +COMMENT ON COLUMN pay_platform_mobile_app.notify_config IS '消息通知配置(JSON文本, AES-256-GCM加密存储)'; +COMMENT ON COLUMN pay_platform_mobile_app.binding_enabled IS '是否启用第三方账号用户绑定'; +COMMENT ON COLUMN pay_platform_mobile_app.enabled IS '是否启用'; +COMMENT ON COLUMN pay_platform_mobile_app.remark IS '备注'; +COMMENT ON COLUMN pay_platform_mobile_app.creator IS '创建者ID'; +COMMENT ON COLUMN pay_platform_mobile_app.create_time IS '创建时间'; +COMMENT ON COLUMN pay_platform_mobile_app.last_modifier IS '最后修改者ID'; +COMMENT ON COLUMN pay_platform_mobile_app.last_modified_time IS '最后修改时间'; +COMMENT ON COLUMN pay_platform_mobile_app.version IS '版本号(乐观锁)'; +COMMENT ON COLUMN pay_platform_mobile_app.deleted IS '逻辑删除标记'; + +CREATE UNIQUE INDEX IF NOT EXISTS uk_pay_platform_mobile_app_type_platform + ON pay_platform_mobile_app (app_type, platform) WHERE deleted = false; diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/convert/MobileAppConvert.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/convert/MobileAppConvert.java index 947a6a171..59b3bb73a 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/convert/MobileAppConvert.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/convert/MobileAppConvert.java @@ -3,11 +3,16 @@ package cn.daxpay.open.payment.app.mobile.convert; import cn.daxpay.open.payment.app.mobile.entity.MobileApp; import cn.daxpay.open.payment.app.mobile.param.MobileAppParam; import cn.daxpay.open.payment.app.mobile.result.MobileAppResult; +import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; /// # 移动端应用配置转换 +/// +/// copy 使用 IGNORE 策略, 前端未传的敏感字段(null)不会覆盖数据库原值, +/// 与前端 diffForm + 后端脱敏回显双重保护配合, 避免误清空。 @Mapper public interface MobileAppConvert { @@ -20,5 +25,6 @@ public interface MobileAppConvert { MobileApp toEntity(MobileAppParam param); /// 参数拷贝到实体(更新) + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) void copy(MobileAppParam param, @MappingTarget MobileApp entity); } diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/entity/MobileApp.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/entity/MobileApp.java index de66c9d80..900c0c34e 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/entity/MobileApp.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/entity/MobileApp.java @@ -4,7 +4,8 @@ import cn.daxpay.open.payment.app.mobile.convert.MobileAppConvert; import cn.daxpay.open.payment.app.mobile.result.MobileAppResult; 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.type.JsonbStringTypeHandler; +import cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,7 +14,7 @@ import lombok.experimental.Accessors; /// # 移动端应用配置 /// /// 平台级移动端应用配置, 按端类型(appType)+移动平台(platform)维度, 每组合一条记录。 -/// app_config 存储各平台特有的密钥配置(jsonb), notify_config 存储消息通知配置(jsonb)。 +/// app_config/notify_config 以 JSON 文本存储, 通过 [DataEncryptTypeHandler] AES-256-GCM 加密入库。 @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) @@ -31,12 +32,12 @@ public class MobileApp extends MpBaseEntity implements ToResult /// 应用名称(展示用) private String appName; - /// 平台特有密钥配置(jsonb原始JSON文本) - @com.baomidou.mybatisplus.annotation.TableField(typeHandler = JsonbStringTypeHandler.class) + /// 平台特有密钥配置(JSON文本, 加密存储) + @TableField(typeHandler = DataEncryptTypeHandler.class) private String appConfig; - /// 消息通知配置(jsonb原始JSON文本) - @com.baomidou.mybatisplus.annotation.TableField(typeHandler = JsonbStringTypeHandler.class) + /// 消息通知配置(JSON文本, 加密存储) + @TableField(typeHandler = DataEncryptTypeHandler.class) private String notifyConfig; /// 是否启用第三方账号用户绑定 diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/service/MobileAppService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/service/MobileAppService.java index a69db68aa..2d5c8c9b4 100644 --- a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/service/MobileAppService.java +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/app/mobile/service/MobileAppService.java @@ -5,44 +5,58 @@ import cn.daxpay.open.payment.app.mobile.dao.MobileAppManager; import cn.daxpay.open.payment.app.mobile.entity.MobileApp; import cn.daxpay.open.payment.app.mobile.param.MobileAppParam; import cn.daxpay.open.payment.app.mobile.result.MobileAppResult; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; import cn.daxpay.open.platform.core.exception.DataNotExistException; +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.Set; /// # 移动端应用配置服务 /// /// 平台级配置, 按端类型(appType)+移动平台(platform)维度管理。 +/// app_config/notify_config 经 [cn.daxpay.open.platform.common.mybatisplus.handler.encrypt.DataEncryptTypeHandler] 加密入库; +/// 返回前端时对 app_config 内敏感键(appSecret/privateKey/clientSecret)脱敏; +/// 保存时敏感键为空则保留库中原值(配合前端 diffForm)。 @Slf4j @Service @RequiredArgsConstructor public class MobileAppService { + /// app_config 中需要脱敏/保护的敏感键 + private static final Set SENSITIVE_KEYS = Set.of( + "appSecret", "privateKey", "alipayPublicKey", "clientSecret"); + private final MobileAppManager manager; /// 查询全部(前端按端类型分组展示卡片) public List findAll() { return manager.findAll().stream() - .map(MobileApp::toResult) + .map(this::toMaskedResult) .toList(); } /// 按端类型查询所有平台配置(端详情页Tab列表) public List findAllByAppType(String appType) { return manager.findAllByField(MobileApp::getAppType, appType).stream() - .map(MobileApp::toResult) + .map(this::toMaskedResult) .toList(); } /// 查询单条 public MobileAppResult findById(Long id) { // 通用: 移动端应用配置不存在 - return manager.findById(id) - .orElseThrow(() -> new DataNotExistException("error.mobile_app.notExist")) - .toResult(); + MobileApp entity = manager.findById(id) + .orElseThrow(() -> new DataNotExistException("error.mobile_app.notExist")); + return toMaskedResult(entity); } /// 保存(按端类型+平台组合 upsert) @@ -54,13 +68,15 @@ public class MobileAppService { .oneOpt(); if (existing.isPresent()) { var entity = existing.get(); + // 敏感键为空时保留库中原值, 避免 diffForm 跳过字段后被清空 + param.setAppConfig(mergeSensitiveJson(entity.getAppConfig(), param.getAppConfig())); MobileAppConvert.CONVERT.copy(param, entity); manager.updateById(entity); - return entity.toResult(); + return toMaskedResult(entity); } var entity = MobileAppConvert.CONVERT.toEntity(param); manager.save(entity); - return entity.toResult(); + return toMaskedResult(entity); } /// 更新启用状态 @@ -72,4 +88,70 @@ public class MobileAppService { entity.setEnabled(enabled); manager.updateById(entity); } + + /// 实体转结果并对 app_config 敏感键脱敏 + private MobileAppResult toMaskedResult(MobileApp entity) { + MobileAppResult result = entity.toResult(); + result.setAppConfig(maskSensitiveJson(result.getAppConfig())); + return result; + } + + /// 对 JSON 文本中的敏感键做脱敏(password 风格), 非 JSON 或空串原样返回 + private String maskSensitiveJson(String json) { + if (StrUtil.isBlank(json)) { + return json; + } + 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; + } + } + + /// 合并新旧 app_config: 新值中敏感键为空/null 时沿用旧值 + private String mergeSensitiveJson(String oldJson, String newJson) { + if (StrUtil.isBlank(newJson)) { + // 前端未传则整段保留旧值 + return oldJson; + } + if (StrUtil.isBlank(oldJson)) { + return newJson; + } + 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); + } catch (Exception e) { + log.warn("mergeSensitiveJson parse failed, use newJson: {}", e.getMessage()); + return newJson; + } + } }