一、系统概述 叮咚买菜作为生鲜电商平台,商品迭代是核心业务功能之一,涉及商品上架、下架、信息更新、价格调整等操作。系统需要实现完整的商品生命周期管理,并保留详细的迭代记录以支持审计、分析和回滚需求。 二、商品迭代记录核心需求 1.完整记录:记录所有商品变更操作(创建、更新、
一、系统概述
叮咚买菜作为生鲜电商平台,商品迭代是核心业务功能之一,涉及商品上架、下架、信息更新、价格调整等操作。系统需要实现完整的商品生命周期管理,并保留详细的迭代记录以支持审计、分析和回滚需求。
二、商品迭代记录核心需求
1. 完整记录:记录所有商品变更操作(创建、更新、删除)
2. 变更追溯:支持查看任意时间点的商品状态
3. 操作审计:记录操作者、操作时间、变更内容
4. 数据分析:支持基于变更历史的分析
5. 版本回滚:在必要时恢复商品到历史版本
三、技术实现方案
1. 数据库设计
主表设计:
```sql
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_code VARCHAR(32) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
category_id BIGINT NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL,
status TINYINT NOT NULL COMMENT 1-上架 2-下架 3-删除,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
-- 其他商品字段...
INDEX idx_code (product_code),
INDEX idx_status (status)
);
```
变更历史表设计:
```sql
CREATE TABLE product_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
operation_type TINYINT NOT NULL COMMENT 1-创建 2-更新 3-删除,
operator_id BIGINT NOT NULL COMMENT 操作者ID,
operator_name VARCHAR(50) NOT NULL COMMENT 操作者名称,
change_content JSON NOT NULL COMMENT 变更内容JSON,
before_content JSON COMMENT 变更前内容JSON,
operation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_product (product_id),
INDEX idx_operation_time (operation_time)
);
```
2. 核心实现逻辑
商品服务层实现:
```java
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductHistoryRepository historyRepository;
@Transactional
public Product createProduct(ProductDTO dto, Long operatorId, String operatorName) {
Product product = convertToEntity(dto);
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
// 保存商品
Product saved = productRepository.save(product);
// 记录创建历史
ProductHistory history = new ProductHistory();
history.setProductId(saved.getId());
history.setOperationType(1); // 创建
history.setOperatorId(operatorId);
history.setOperatorName(operatorName);
history.setChangeContent(JSON.toJSONString(dto));
historyRepository.save(history);
return saved;
}
@Transactional
public Product updateProduct(Long productId, ProductDTO dto, Long operatorId, String operatorName) {
// 获取当前商品
Product current = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
// 记录变更前内容
ProductHistory history = new ProductHistory();
history.setProductId(productId);
history.setOperationType(2); // 更新
history.setOperatorId(operatorId);
history.setOperatorName(operatorName);
history.setBeforeContent(JSON.toJSONString(current));
// 更新商品
BeanUtils.copyProperties(dto, current, "id", "createdAt");
current.setUpdatedAt(LocalDateTime.now());
productRepository.save(current);
// 记录变更后内容
history.setChangeContent(JSON.toJSONString(dto));
historyRepository.save(history);
return current;
}
// 其他方法...
}
```
3. 变更内容记录策略
1. 全量记录:每次变更记录完整的商品数据(适合数据量小的场景)
2. 差异记录:只记录变更的字段(实现较复杂但节省存储)
3. 混合策略:创建时全量记录,更新时差异记录
差异记录实现示例:
```java
public Map
getChangedFields(Product original, Product updated) {
Map changes = new HashMap<>();
if (!Objects.equals(original.getName(), updated.getName())) {
changes.put("name", updated.getName());
}
if (!Objects.equals(original.getPrice(), updated.getPrice())) {
changes.put("price", updated.getPrice());
}
// 其他字段比较...
return changes.isEmpty() ? null : changes;
}
```
4. 查询实现
历史记录查询接口:
```java
@GetMapping("/products/{id}/history")
public List getProductHistory(@PathVariable Long id,
@RequestParam(required = false) LocalDateTime startTime,
@RequestParam(required = false) LocalDateTime endTime) {
// 构建查询条件
Specification spec = (root, query, cb) -> {
List predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("productId"), id));
if (startTime != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("operationTime"), startTime));
}
if (endTime != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("operationTime"), endTime));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
// 执行查询并按时间倒序排序
List histories = historyRepository.findAll(spec,
Sort.by(Sort.Direction.DESC, "operationTime"));
return histories.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
```
5. 版本回滚实现
```java
@Transactional
public Product rollbackToVersion(Long productId, Long historyId, Long operatorId, String operatorName) {
// 获取历史记录
ProductHistory history = historyRepository.findById(historyId)
.orElseThrow(() -> new RuntimeException("历史记录不存在"));
if (!history.getProductId().equals(productId)) {
throw new RuntimeException("历史记录与商品不匹配");
}
// 获取当前商品
Product current = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
// 记录当前状态作为新的历史记录
ProductHistory newHistory = new ProductHistory();
newHistory.setProductId(productId);
newHistory.setOperationType(2); // 标记为回滚操作
newHistory.setOperatorId(operatorId);
newHistory.setOperatorName(operatorName);
newHistory.setBeforeContent(JSON.toJSONString(current));
// 解析历史数据并更新商品
ProductDTO dto = JSON.parseObject(history.getChangeContent(), ProductDTO.class);
BeanUtils.copyProperties(dto, current, "id", "createdAt");
current.setUpdatedAt(LocalDateTime.now());
productRepository.save(current);
// 保存新的历史记录
newHistory.setChangeContent(JSON.toJSONString(dto));
historyRepository.save(newHistory);
return current;
}
```
四、高级功能扩展
1. 变更通知:当关键字段变更时发送通知(如价格变更通知运营)
2. 变更审批:重要商品变更需要审批流程
3. 变更影响分析:分析商品变更对订单、库存等的影响
4. 数据快照:定期生成商品数据快照用于备份和分析
5. 变更可视化:提供商品变更时间轴可视化界面
五、性能优化建议
1. 异步记录历史:对于高频更新场景,可采用消息队列异步记录历史
2. 批量插入:批量操作时合并历史记录插入
3. 分区表:对历史表按时间分区提高查询性能
4. 冷热数据分离:将旧历史数据迁移到低成本存储
六、安全考虑
1. 操作权限控制:只有特定角色可以修改商品信息
2. 数据脱敏:敏感信息(如成本价)在历史记录中脱敏
3. 防篡改:历史记录不可修改,可考虑添加数字签名
通过以上方案,叮咚买菜系统可以实现完善的商品迭代记录功能,满足业务运营、审计和数据分析的需求。