Commit 04b7bde7 by Lizh

增加商品homesku编码统一生成逻辑和接口

parent edb49ea1
package com.jomalls.custom.app.enums;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
/**
* SKU生成枚举
* <p>
* 定义各业务模块的SKU生成规则:code(单据标识)、prefix(SKU前缀)、lock(Redis锁键)
*
* @author Lizh
* @date 2026-06-05
*/
@Getter
public enum SkuGenerateEnums {
/** 模板产品 */
TEMPLATE_PRODUCT("JM", "JM", "templateProductSku"),
/** 对账 */
RECONCILIATION("DZ", "DZ", "reconciliation"),
/** POD对账 */
POD_RECONCILIATION("PDZ", "PDZ", "podReconciliation"),
/** POD美国对账 */
POD_US_RECONCILIATION("USPDZ", "USPDZ", "podUsReconciliation"),
/** POD对账任务 */
POD_REC_TASK("PRT", "PRT", "podRecTask"),
/** POD美国对账任务 */
POD_US_REC_TASK("PURT", "PURT", "podUsRecTask");
/** 单据code,对应sys_bill_rule表的code字段 */
private final String code;
/** SKU前缀 */
private final String prefix;
/** Redis分布式锁的键名 */
private final String lock;
SkuGenerateEnums(String code, String prefix, String lock) {
this.code = code;
this.prefix = prefix;
this.lock = lock;
}
/**
* 根据code查找枚举
*
* @param code 单据code
* @return 对应的枚举,未找到返回null
*/
public static SkuGenerateEnums getByCode(String code) {
return Arrays.stream(values()).filter(e -> e.getCode().equals(code))
.findFirst().orElse(null);
}
/**
* 获取所有锁键列表(用于启动时清理锁)
*
* @return 锁键列表
*/
public static List<String> getLockList() {
return Arrays.stream(values()).map(SkuGenerateEnums::getLock).toList();
}
}
package com.jomalls.custom.app.service;
/**
* 单号规则 App Service 接口
*
* @author Lizh
* @date 2026-06-05
*/
public interface SysBillRuleService {
/**
* 根据单据code生成产品SKU编号
* <p>
* 生成规则:prefix + YYMMDD + 3位序号(如 JM250605001)
* 使用Redis分布式锁 + 数据库行锁保证并发安全,同一天内序号递增,跨天自动重置
*
* @param code 单据code(对应SkuGenerateEnums中的code字段)
* @return 生成的SKU编号字符串
*/
String getProductSku(String code);
}
package com.jomalls.custom.app.service.impl;
import com.jomalls.custom.app.enums.SkuGenerateEnums;
import com.jomalls.custom.app.exception.ServiceException;
import com.jomalls.custom.app.service.SysBillRuleService;
import com.jomalls.custom.app.utils.CustomAsserts;
import com.jomalls.custom.dal.entity.SysBillRuleEntity;
import com.jomalls.custom.domain.service.SysBillRuleDomainService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
/**
* SKU编码规则 App Service 实现
* <p>
* 并发安全采用两层防护:
* <ol>
* <li><b>Redisson 分布式锁(快速互斥)</b>:pub/sub 通知替代忙等,看门狗自动续期防止锁过期。</li>
* <li><b>MySQL FOR UPDATE 行锁(数据正确性兜底)</b>:事务内的行锁保证同一 code 的读写严格串行化。</li>
* </ol>
* 时序:获取 Redisson 锁(事务外) → DB 操作(TransactionTemplate 事务内) → 事务提交 → 释放锁(事务外)
*
* @author Lizh
* @date 2026-06-05
*/
@Slf4j
@Service
public class SysBillRuleServiceImpl implements SysBillRuleService {
/** 获取锁的最大等待时间 */
private static final long LOCK_WAIT_SECONDS = 5;
/** 隔天重置的起始数字 */
private static final String NEW_START_NUMBER = "1";
/** 补全数字的个数 */
private static final long PAD_LENGTH = 3;
/** 日期格式化器(线程安全,复用) */
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
private final SysBillRuleDomainService sysBillRuleDomainService;
private final TransactionTemplate transactionTemplate;
private final RedissonClient redissonClient;
@Autowired
public SysBillRuleServiceImpl(SysBillRuleDomainService sysBillRuleDomainService,
TransactionTemplate transactionTemplate,
RedissonClient redissonClient) {
this.sysBillRuleDomainService = sysBillRuleDomainService;
this.transactionTemplate = transactionTemplate;
this.redissonClient = redissonClient;
}
/**
* 根据单据code生成产品SKU编号
*
* @param code 单据code
* @return 生成的SKU编号
*/
@Override
public String getProductSku(String code) {
CustomAsserts.nonNull(code, "分类编码不能为空");
// 1. 查找对应的枚举配置
SkuGenerateEnums generateEnums = SkuGenerateEnums.getByCode(code);
if (generateEnums == null) {
throw new ServiceException("未找到对应的SKU生成规则,code: " + code);
}
// 2. 获取 Redisson 分布式锁
RLock lock = redissonClient.getLock(generateEnums.getLock());
boolean acquired = false;
try {
acquired = lock.tryLock(LOCK_WAIT_SECONDS, TimeUnit.SECONDS);
if (!acquired) {
log.error("获取Redisson锁超时, lockKey={}, waited={}s", generateEnums.getLock(), LOCK_WAIT_SECONDS);
throw new ServiceException("获取锁超时,请稍后重试");
}
// 3. 在独立事务中执行DB操作(编程式事务,不受 AOP 自调用代理限制)
return transactionTemplate.execute(status -> doGenerateSku(generateEnums));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("获取锁被中断");
} finally {
// 4. 事务已提交,释放锁(Redisson 自动校验线程归属)
if (acquired) {
try {
lock.unlock();
} catch (Exception e) {
log.warn("释放Redisson锁异常, lockKey={}", generateEnums.getLock(), e);
}
}
}
}
/**
* 核心SKU生成逻辑(在 TransactionTemplate 管理的事务中执行)
*
* @param generateEnums SKU生成枚举
* @return 生成的SKU编号
*/
private String doGenerateSku(SkuGenerateEnums generateEnums) {
String code = generateEnums.getCode();
// 查询单号规则(FOR UPDATE 行锁在事务期间持有)
SysBillRuleEntity billRule = sysBillRuleDomainService.getByCodeForUpdate(code);
if (billRule == null) {
throw new ServiceException("单号规则不存在,code: " + code);
}
// 生成日期前缀:prefix + YYMMDD
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String preStr = generateEnums.getPrefix() + dateStr;
// 确定当前序号
String currentNumber = billRule.getCurrentNumber();
if (billRule.getLastNo() == null || !billRule.getLastNo().startsWith(preStr)) {
log.debug("SKU序号跨天重置: code={}, lastNo={}, preStr={}", code, billRule.getLastNo(), preStr);
currentNumber = NEW_START_NUMBER;
} else if (currentNumber == null || currentNumber.isEmpty()) {
currentNumber = NEW_START_NUMBER;
}
// 补齐为3位数字
if (currentNumber.length() < PAD_LENGTH) {
currentNumber = padNumber(currentNumber);
}
// 拼接最终SKU
String sku = preStr + currentNumber;
// 更新单号规则
billRule.setLastNo(sku);
billRule.setCurrentNumber(String.valueOf(Integer.parseInt(currentNumber) + 1));
sysBillRuleDomainService.updateById(billRule);
log.debug("SKU生成成功: code={}, sku={}, nextNumber={}", code, sku, billRule.getCurrentNumber());
return sku;
}
/**
* 将数字字符串补齐到指定位数
*/
private String padNumber(String number) {
try {
return String.format("%0" + PAD_LENGTH + "d", Integer.parseInt(number));
} catch (NumberFormatException e) {
log.error("序号格式异常, number={}", number, e);
throw new ServiceException("单号规则的当前序号格式异常: " + number);
}
}
}
package com.jomalls.custom.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 单号规则 Entity
*
* @author Lizh
* @date 2026-06-05
*/
@Data
@TableName("sys_bill_rule")
public class SysBillRuleEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 单据名称
*/
@TableField("name")
private String name;
/**
* code
*/
@TableField("code")
private String code;
/**
* 类别简称
*/
@TableField("short_name")
private String shortName;
/**
* 日期类型(年月日 / 年月)
*/
@TableField("date_type")
private String dateType;
/**
* 样例
*/
@TableField("example")
private String example;
/**
* 分隔符
*/
@TableField("space_mark")
private String spaceMark;
/**
* 起始编号
*/
@TableField("start_number")
private String startNumber;
/**
* 当前编号
*/
@TableField("current_number")
private String currentNumber;
/**
* 启用标记
*/
@TableField("enable_flag")
private Integer enableFlag;
/**
* 上个单据号
*/
@TableField("last_no")
private String lastNo;
}
package com.jomalls.custom.dal.mapper;
import com.jomalls.custom.dal.entity.SysBillRuleEntity;
import com.jomalls.custom.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 单号规则 Mapper
*
* @author Lizh
* @date 2026-06-05
*/
@Mapper
public interface SysBillRuleMapper extends BaseMapper<SysBillRuleEntity> {
/**
* 通过code查询并加行锁(FOR UPDATE),防止并发更新
*
* @param code 单据code
* @return 单号规则实体
*/
SysBillRuleEntity selectByCodeForUpdate(@Param("code") String code);
}
package com.jomalls.custom.domain.service;
import com.jomalls.custom.dal.entity.SysBillRuleEntity;
import com.jomalls.custom.service.IBaseService;
/**
* 单号规则 Domain Service 接口
*
* @author Lizh
* @date 2026-06-05
*/
public interface SysBillRuleDomainService extends IBaseService<SysBillRuleEntity> {
/**
* 通过code查询单号规则(加行锁,用于并发场景下的SKU生成)
*
* @param code 单据code
* @return 单号规则实体
*/
SysBillRuleEntity getByCodeForUpdate(String code);
}
package com.jomalls.custom.domain.service.impl;
import com.jomalls.custom.dal.entity.SysBillRuleEntity;
import com.jomalls.custom.dal.mapper.SysBillRuleMapper;
import com.jomalls.custom.domain.service.SysBillRuleDomainService;
import com.jomalls.custom.service.impl.BaseServiceImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 单号规则 Domain Service 实现
*
* @author Lizh
* @date 2026-06-05
*/
@Service
public class SysBillRuleDomainServiceImpl
extends BaseServiceImpl<SysBillRuleMapper, SysBillRuleEntity>
implements SysBillRuleDomainService {
@Autowired
public SysBillRuleDomainServiceImpl(SqlSessionFactory sqlSessionFactory) {
super(sqlSessionFactory);
}
@Override
public SysBillRuleEntity getByCodeForUpdate(String code) {
return this.baseMapper.selectByCodeForUpdate(code);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jomalls.custom.dal.mapper.SysBillRuleMapper">
<resultMap type="com.jomalls.custom.dal.entity.SysBillRuleEntity" id="sysBillRuleMap">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="code" column="code"/>
<result property="shortName" column="short_name"/>
<result property="dateType" column="date_type"/>
<result property="example" column="example"/>
<result property="spaceMark" column="space_mark"/>
<result property="startNumber" column="start_number"/>
<result property="currentNumber" column="current_number"/>
<result property="enableFlag" column="enable_flag"/>
<result property="lastNo" column="last_no"/>
</resultMap>
<sql id="tableColumns">
id,
name,
code,
short_name,
date_type,
example,
space_mark,
start_number,
current_number,
enable_flag,
last_no
</sql>
<select id="selectByCodeForUpdate" resultMap="sysBillRuleMap">
SELECT
<include refid="tableColumns"/>
FROM sys_bill_rule
WHERE code = #{code}
FOR UPDATE
</select>
</mapper>
package com.jomalls.custom.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
......@@ -9,25 +13,40 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Author: Lizh
* @Date: 2026/6/2 15:27
* @Description: redis配置类
* @Version: 1.0
* Redis 配置类
* <p>
* 包含:RedisTemplate / StringRedisTemplate 序列化配置、Redisson 分布式锁客户端。
* Redisson 手动配置以避开 redisson-spring-boot-starter 与 Spring Boot 4.x 的兼容问题。
*
* @author Lizh
* @date 2026-06-02
*/
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host:localhost}")
private String host;
@Value("${spring.data.redis.port:6379}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Value("${spring.data.redis.database:0}")
private int database;
// ==================== RedisTemplate ====================
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key使用String序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value使用Jackson JSON序列化
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
......@@ -39,4 +58,21 @@ public class RedisConfig {
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
// ==================== Redisson ====================
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + host + ":" + port;
config.useSingleServer()
.setAddress(address)
.setPassword(password.isBlank() ? null : password)
.setDatabase(database)
.setConnectionPoolSize(16)
.setConnectionMinimumIdleSize(4);
return Redisson.create(config);
}
}
package com.jomalls.custom.webapp.controller;
import com.jomalls.custom.app.service.SysBillRuleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 单号规则 Controller
* <p>
* 提供SKU编号生成接口
*
* @author Lizh
* @date 2026-06-05
*/
@Slf4j
@RestController
@Tag(name = "/api/v2/product/homesku", description = "生成产品SKU编号")
@RequestMapping("/api/v2/product/homesku")
public class SysBillRuleController {
@Autowired
private SysBillRuleService sysBillRuleService;
/**
* 生成产品SKU编号
* <p>
* 根据单据code生成唯一的SKU编号,格式:prefix + YYMMDD + 3位序号(如 JM250605001)。
* 同一自然日内序号从001开始递增,跨天自动重置。
*
* @param body 请求体,包含 code 字段(如 "JM", "DZ", "PDZ", "USPDZ" 等)
* @return Map 包含生成的 sku 编号
*/
@Operation(summary = "生成产品SKU编号", description = "根据单据code生成唯一的SKU编号,格式:prefix+YYMMDD+3位序号")
@RequestMapping(value = "/{code}", method = RequestMethod.GET)
public String generateSku(
@Parameter(description = "请求体,code为单据标识(如JM/DZ/PDZ/USPDZ/PRT/PURT)", required = true)
@PathVariable String code) {
return sysBillRuleService.getProductSku(code);
}
}
......@@ -70,6 +70,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson 分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.40.2</version>
</dependency>
<!-- Lettuce连接池需要commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment