🚀 积分配置重构实战:从键值对到实体化,前后端协同的丝滑升级 ✨
各位开发者伙伴们,大家好!👋 在系统迭代的过程中,我们经常会遇到数据模型需要优化和重构的场景。今天,我将带大家回顾一次“积分配置”功能的重构之旅。我们是如何从原先基于 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)的字符串。
currencyCode和appId: 保持传递,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;
}
这个实体直接定义了 exchangeRate 和 maxDeductionLimit 字段,类型为 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
服务层的逻辑变化最大,现在它处理的是一个代表完整配置的单一对象。
核心处理流程图:
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中获取exchangeRate和deductionLimit字符串。 - 使用
new BigDecimal()将它们转换为BigDecimal类型。由于前端已经完成了数值含义的转换(例如,从 “100” 转为 “0.0100”),后端可以直接使用这些字符串构造BigDecimal。
- 从
- 描述生成:
description字段需要根据新的实体字段和币种信息来动态生成,确保其准确反映当前配置。 - Bean Validation: 实体
SystemConfig上的@NotNull,@DecimalMin,@DecimalMax等注解会在systemConfigRepository.save()持久化时自动触发校验。也可以如代码中注释所示,注入Validator进行手动提前校验。
📊 交互时序图 (Sequence Diagram - 重构后)
🔄 系统配置状态图 (State Diagram - SystemConfig 记录)
对于每个 mini_program_id(或全局),SystemConfig 记录的状态依然是从“不存在”到“存在/活动”。
🏛️ 类图 (Class Diagram - 关键部分)
方法签名详细说明(简化版,具体类型参考代码):
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! 💻🌟
155

被折叠的 条评论
为什么被折叠?



