积分配置重构实战:从键值对到实体化,前后端协同的丝滑升级 ✨

🚀 积分配置重构实战:从键值对到实体化,前后端协同的丝滑升级 ✨

各位开发者伙伴们,大家好!👋 在系统迭代的过程中,我们经常会遇到数据模型需要优化和重构的场景。今天,我将带大家回顾一次“积分配置”功能的重构之旅。我们是如何从原先基于 config_key/config_value 的灵活但分散的存储方式,升级为更结构化、类型化的实体字段存储,并在这个过程中协调前后端代码,最终实现功能完美落地和测试通过的。让我们一起看看这次重构中的关键决策和技术细节吧!🛠️

📖 重构亮点与成果总结

方面重构前 (旧)重构后 (新)收益与效果
🎯 数据模型SystemConfig 使用 config_key, config_value 存储多个配置项SystemConfig 直接包含 exchangeRate, maxDeductionLimit 等类型化字段数据结构更清晰,类型安全,减少了运行时的转换和解析错误。
⚙️ 后端逻辑Service 层需遍历配置项列表,逐个处理Service 层处理单个包含所有配置的 DTO (Data Transfer Object, 数据传输对象),逻辑更集中代码更简洁,维护性提高,减少了对 config_key 硬编码的依赖。
📡 前后端交互前端提交配置项数组 (List<SystemConfigPayloadItem>)前端提交单个配置对象 (PointsConfigPayload)API (Application Programming Interface, 应用程序编程接口) 接口更符合单一职责,数据传输更高效。
🛡️ 数据校验依赖后端对 config_value 的解析和转换DTO (Data Transfer Object, 数据传输对象) 和实体层均可利用 Bean Validation 进行声明式校验校验更前置、更规范,提升了数据质量。
🗄️ 数据库system_config 表唯一约束为 (config_key, mini_program_id)system_config 表唯一约束调整为 (mini_program_id) (每个小程序/全局只有一条配置记录)数据库结构更符合新的业务模型,查询效率可能有所提升。
用户体验(前端表单未大改) 保持了原有的直观输入方式(前端表单未大改) 用户无感知底层重构,体验一致在优化后端和数据结构的同时,保证了前端用户体验的平滑过渡。

🎨 前端先行:points-config-form.vue 的适配调整

前端的 Vue 组件在这次重构中,主要调整了 handleSubmit 方法中构造请求 payload 的逻辑,以适应后端新的 API (Application Programming Interface, 应用程序编程接口) 期望。

关键的前端代码 (handleSubmit 部分):

// points-config-form.vue
private async handleSubmit() {
  try {
    // ...表单引用等...

    // ✨ 构造新的单一 Payload 对象
    const payload: PointsConfigPayload = {
      appId: this.form.appId || null,
      currencyCode: this.form.currency,
      // 积分兑换率: 前端输入 "100" (100积分=1元), 转换为 "0.0100" (1积分=0.01元)
      exchangeRate: (1 / this.form.exchangeRate).toFixed(4),
      // 抵扣上限: 前端输入 "30" (30%), 转换为 "0.3000" (30%)
      deductionLimit: (this.form.deductionLimit / 100).toFixed(4)
    };

    this.loading = true;
    const res: any = await savePointsConfigs(payload); // 调用新的 API
    // ...后续成功失败处理...
  } catch (error) {
    // ...错误处理...
  } finally {
    this.loading = false;
  }
}

前端调整解读:

  • 单一 Payload: 不再构造配置项数组,而是创建一个名为 PointsConfigPayload 的对象。
  • 数据转换:
    • exchangeRate: 用户在界面输入的是“多少积分等于1元钱”中的“多少积分”(例如100),后端实体需要的是“1积分等于多少钱”(例如0.01)。前端在提交时完成了 1 / userInput 的计算,并格式化为4位小数的字符串。
    • deductionLimit: 用户输入百分比数值(例如30代表30%),前端转换为小数形式(例如0.3000)的字符串。
  • currencyCodeappId: 保持传递,appId 为空时传递 null 表示全局配置。

这样的调整使得前端的职责更清晰:收集用户输入,进行必要的格式化和初步转换,然后将结构化的数据发送给后端。

🔧 后端大改造:迎接新的数据结构

后端的核心改动在于 SystemConfig 实体本身,以及随之而来的 PointsConfigService、DTO (Data Transfer Object, 数据传输对象) 和 Repository 的调整。

1. 新的 SystemConfig 实体

// SystemConfig.java (关键部分)
@Data
@Entity
@Table(name = "system_config")
public class SystemConfig extends BaseEntity {
    // 积分兑换率 (1积分等于多少人民币,例如:0.01表示100积分=1元)
    @NotNull @DecimalMin(value = "0.0", inclusive = false)
    @Column(name = "exchange_rate", nullable = false, precision = 10, scale = 4)
    private BigDecimal exchangeRate;

    // 积分抵扣上限比例 (例如:0.3表示30%)
    @NotNull @DecimalMin(value = "0.0") @DecimalMax(value = "1.0")
    @Column(name = "max_deduction_limit", nullable = false, precision = 5, scale = 4)
    private BigDecimal maxDeductionLimit;

    @Column(name = "description", length = 255)
    private String description;

    @Column(name = "mini_program_id") // NULL 表示全局配置
    private Integer miniProgramId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "mini_program_id", insertable = false, updatable = false)
    private MiniProgramConfig miniProgramConfig;
}

这个实体直接定义了 exchangeRatemaxDeductionLimit 字段,类型为 BigDecimal,并附带了 Bean Validation 注解。

2. 新的 DTO (Data Transfer Object, 数据传输对象): PointsConfigPayload.java

用于接收前端发送的 JSON (JavaScript Object Notation, JavaScript 对象表示法) 数据。

// PointsConfigPayload.java
@Data
public class PointsConfigPayload {
    private String appId;
    @NotBlank private String currencyCode;
    @NotBlank @Pattern(regexp = "^[0-9]+(\\.[0-9]{1,4})?$") // 校验小数格式
    private String exchangeRate;
    @NotBlank @Pattern(regexp = "^[0-9]+(\\.[0-9]{1,4})?$")
    private String deductionLimit;
}

3. 更新后的 SystemConfigRepository

查询方法调整为基于 miniProgramId

// SystemConfigRepository.java
public interface SystemConfigRepository extends JpaRepository<SystemConfig, Integer> {
    Optional<SystemConfig> findByMiniProgramId(Integer miniProgramId);
    Optional<SystemConfig> findByMiniProgramIdIsNull();
}

4. 重构核心:PointsConfigService.java

服务层的逻辑变化最大,现在它处理的是一个代表完整配置的单一对象。

核心处理流程图:

无效
有效
是 (特定小程序)
否 (全局配置)
开始
接收 PointsConfigPayload
和管理员ID
管理员ID有效?
返回未登录错误
校验币种代码
(payload.currencyCode)
payload.appId 是否存在?
根据 appId 查找
MiniProgramConfig
获取小程序数据库ID
(actualMiniProgramDbId)
校验管理员
对该小程序的配置权限
根据 actualMiniProgramDbId
查找 SystemConfig
校验管理员
全局配置权限
查找全局 SystemConfig
(miniProgramId IS NULL)
SystemConfig 是否存在?
获取现有 SystemConfig 实例
创建新的 SystemConfig 实例
设置 miniProgramId
数据转换与赋值
将 payload 中的 exchangeRate (String)
转为 BigDecimal 并设置
将 payload 中的 deductionLimit (String)
转为 BigDecimal 并设置
生成/设置 description
(可选)手动触发
Bean Validation 校验
保存 SystemConfig 到数据库
(INSERT 或 UPDATE)
返回成功
结束

PointsConfigService 关键逻辑:

// PointsConfigService.java (核心部分)
@Transactional
public void saveOrUpdatePointsConfiguration(PointsConfigPayload payload, Integer currentAdminId) {
    // ... 前置校验 (adminId, payload 非空) ...

    // 1. 校验币种 (来自 payload.getCurrencyCode())
    Currency validCurrency = validateAndGetCurrency(payload.getCurrencyCode());

    // 2. 处理小程序 ID 及权限校验
    Integer actualMiniProgramDbId = null;
    if (StringUtils.hasText(payload.getAppId())) {
        MiniProgramConfig targetMiniProgram = miniProgramConfigRepository.findByAppId(payload.getAppId())
            .orElseThrow(/* ... */);
        actualMiniProgramDbId = targetMiniProgram.getId();
        checkAdminPermissionForMiniProgram(currentAdminId, actualMiniProgramDbId, payload.getAppId());
    } else {
        checkAdminPermissionForGlobalConfig(currentAdminId);
    }

    // 3. 查找或创建唯一的 SystemConfig 记录
    SystemConfig configToSave;
    Optional<SystemConfig> existingConfigOpt = (actualMiniProgramDbId == null)
        ? systemConfigRepository.findByMiniProgramIdIsNull()
        : systemConfigRepository.findByMiniProgramId(actualMiniProgramDbId);

    if (existingConfigOpt.isPresent()) {
        configToSave = existingConfigOpt.get();
    } else {
        configToSave = new SystemConfig();
        configToSave.setMiniProgramId(actualMiniProgramDbId);
    }

    // 4. 设置字段值 (注意数据类型转换)
    // 前端已将 exchangeRate 转为 "0.0100" 格式的字符串
    configToSave.setExchangeRate(new BigDecimal(payload.getExchangeRate()));
    // 前端已将 deductionLimit 转为 "0.3000" 格式的字符串
    configToSave.setMaxDeductionLimit(new BigDecimal(payload.getDeductionLimit()));

    // 5. 生成描述 (示例)
    // (根据 validCurrency 和转换后的 BigDecimal 值生成描述)
    // ...
    configToSave.setDescription(/* ... 生成的描述 ... */);

    // 6. (可选) 手动触发实体校验
    // Set<ConstraintViolation<SystemConfig>> violations = validator.validate(configToSave);
    // if (!violations.isEmpty()) { throw new MyRuntimeException(...); }

    systemConfigRepository.save(configToSave);
}

后端调整解读:

  • 单一入口方法: saveOrUpdatePointsConfiguration 现在接收 PointsConfigPayload
  • 查询逻辑简化: 直接通过 miniProgramId (或 miniProgramId IS NULL) 查找唯一的 SystemConfig 记录。
  • 数据赋值:
    • payload 中获取 exchangeRatedeductionLimit 字符串。
    • 使用 new BigDecimal() 将它们转换为 BigDecimal 类型。由于前端已经完成了数值含义的转换(例如,从 “100” 转为 “0.0100”),后端可以直接使用这些字符串构造 BigDecimal
  • 描述生成: description 字段需要根据新的实体字段和币种信息来动态生成,确保其准确反映当前配置。
  • Bean Validation: 实体 SystemConfig 上的 @NotNull, @DecimalMin, @DecimalMax 等注解会在 systemConfigRepository.save() 持久化时自动触发校验。也可以如代码中注释所示,注入 Validator 进行手动提前校验。

📊 交互时序图 (Sequence Diagram - 重构后)

"前端 Vue 组件""PointsConfigController""PointsConfigService""MiniProgramConfigRepository""AdminMiniProgramRepository""CurrencyRepository""SystemConfigRepository""数据库"POST /save (PointsConfigPayload, adminId)saveOrUpdatePointsConfiguration(payload, adminId)findByCode(payload.currencyCode)Optional<Currency>校验币种findByAppId(payload.appId)Optional<MiniProgramConfig>获取 miniProgramDbIdexistsByAdminIdAndMiniProgramId(adminId, miniProgramDbId)boolean (权限检查)findByMiniProgramId(miniProgramDbId)Optional<SystemConfig>checkAdminPermissionForGlobalConfig(adminId)findByMiniProgramIdIsNull()Optional<SystemConfig>alt[如果 payload.appId 存在][全局配置]准备 SystemConfig 实例 (更新或新建)设置 exchangeRate, maxDeductionLimit (String to BigDecimal)生成 descriptionsave(SystemConfig)INSERT/UPDATE system_config操作结果保存后的 SystemConfig处理结果 (成功/异常)BaseResult (响应)"前端 Vue 组件""PointsConfigController""PointsConfigService""MiniProgramConfigRepository""AdminMiniProgramRepository""CurrencyRepository""SystemConfigRepository""数据库"

🔄 系统配置状态图 (State Diagram - SystemConfig 记录)

对于每个 mini_program_id(或全局),SystemConfig 记录的状态依然是从“不存在”到“存在/活动”。

"配置记录初始不存在
(针对特定小程序或全局)"
"首次保存配置
(saveOrUpdatePointsConfiguration)"
"更新现有配置
(saveOrUpdatePointsConfiguration)"
NonExistent
Active
通过服务首次为该小程序/全局保存时,
记录从无到有。
当该小程序/全局的配置已存在,
服务会更新其字段值。

🏛️ 类图 (Class Diagram - 关键部分)

"uses"
"consumes"
"uses"
"manages"
PointsConfigPayload
-String appId
-String currencyCode
-String exchangeRate
-String deductionLimit
PointsConfigController
+savePointsConfiguration(payload, adminId)
PointsConfigService
+saveOrUpdatePointsConfiguration(payload, adminId)
SystemConfig
+BigDecimal exchangeRate
+BigDecimal maxDeductionLimit
+String description
+Integer miniProgramId
«interface»
SystemConfigRepository
+findByMiniProgramId(miniProgramId)
+findByMiniProgramIdIsNull()
+save(entity)

方法签名详细说明(简化版,具体类型参考代码):

  • PointsConfigController.savePointsConfiguration(PointsConfigPayload payload, Integer adminId): 返回 BaseResult
  • PointsConfigService.saveOrUpdatePointsConfiguration(PointsConfigPayload payload, Integer adminId): 返回 void
  • SystemConfigRepository.findByMiniProgramId(Integer miniProgramId): 返回 Optional<SystemConfig>
  • SystemConfigRepository.findByMiniProgramIdIsNull(): 返回 Optional<SystemConfig>
  • SystemConfigRepository.save(SystemConfig entity): 返回 SystemConfig

💡 英文缩写全称及中文解释

  • API: Application Programming Interface (应用程序编程接口)
  • AppID: Application Identifier (应用程序标识符)
  • CSS: Cascading Style Sheets (层叠样式表)
  • DDL: Data Definition Language (数据定义语言)
  • DOM: Document Object Model (文档对象模型)
  • DTO: Data Transfer Object (数据传输对象)
  • FK: Foreign Key (外键)
  • HTTP: HyperText Transfer Protocol (超文本传输协议)
  • ID: Identifier (标识符)
  • JPA: Jakarta Persistence API (formerly Java Persistence API) (Jakarta 持久化应用程序接口)
  • JSON: JavaScript Object Notation (JavaScript 对象表示法)
  • ORM: Object-Relational Mapping (对象关系映射)
  • PK: Primary Key (主键)
  • SCSS: Sassy CSS (一种 CSS (Cascading Style Sheets, 层叠样式表) 预处理器)
  • SQL: Structured Query Language (结构化查询语言)
  • UI: User Interface (用户界面)
  • UML: Unified Modeling Language (统一建模语言)
  • Vue: 一款流行的渐进式 JavaScript 框架。

🧠 思维导图 (Markdown 格式)

在这里插入图片描述

🎉 重构成功,未来可期!

通过这次前后端协同的重构,我们不仅优化了数据模型,提升了代码质量,还保证了功能的稳定和用户体验的平滑。这是一个很好的例子,说明了在系统演进过程中,适时地对核心数据结构和相关逻辑进行重构是非常有价值的。

感谢大家的阅读,希望这次的分享能对你在类似场景下的工作有所启发!如果你对这次重构有任何疑问或更好的建议,欢迎在评论区交流!👇 Happy Refactoring! 💻🌟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值