feat(ops): refactor-order-operations

This commit is contained in:
lzh
2026-01-19 13:32:23 +08:00
parent 5419a949d4
commit 568d37a0be
31 changed files with 2806 additions and 1456 deletions

View File

@@ -58,10 +58,10 @@ public interface OrderLifecycleManager {
OrderTransitionResult enqueue(OrderTransitionRequest request);
/**
* 工单出队并派单QUEUED → DISPATCHED
* 工单出队并派单:PENDING/QUEUED → DISPATCHED
* <p>
* 状态转换:
* - 工单状态QUEUED → DISPATCHED
* - 工单状态:PENDING/QUEUED → DISPATCHED
* - 队列状态WAITING → PROCESSING
*
* @param request 状态转换请求

View File

@@ -0,0 +1,93 @@
package com.viewsh.module.ops.infrastructure.code;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
/**
* 工单编号生成器
* <p>
* 格式:{业务前缀}-{日期}-{序号}
* 例如CLEAN-20250119-0001, SECURITY-20250119-0001
* <p>
* 特性:
* - 使用 Redis 保证序号唯一性
* - 序号每日自动重置(按日期分 key
* - 不同业务类型独立计数
*
* @author lzh
*/
@Slf4j
@Service
public class OrderCodeGenerator {
private static final String KEY_PREFIX = "ops:order:code:";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final long DEFAULT_EXPIRE_DAYS = 7; // key 默认保留7天
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成工单编号
*
* @param businessType 业务类型CLEAN、SECURITY、FACILITIES
* @return 工单编号,格式:{业务类型}-{日期}-{4位序号}
*/
public String generate(String businessType) {
if (businessType == null || businessType.isEmpty()) {
throw new IllegalArgumentException("Business type cannot be null or empty");
}
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
// Redis 自增并获取新值
Long seq = stringRedisTemplate.opsForValue().increment(key);
// 首次创建时设置过期时间
if (seq != null && seq == 1) {
stringRedisTemplate.expire(key, DEFAULT_EXPIRE_DAYS, TimeUnit.DAYS);
}
if (seq == null) {
throw new RuntimeException("Failed to generate order code for business type: " + businessType);
}
String orderCode = String.format("%s-%s-%04d", businessType, dateStr, seq);
log.debug("生成工单编号: businessType={}, orderCode={}", businessType, orderCode);
return orderCode;
}
/**
* 获取指定业务类型当天的当前序号
*
* @param businessType 业务类型
* @return 当前序号如果不存在返回0
*/
public long getCurrentSeq(String businessType) {
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
String value = stringRedisTemplate.opsForValue().get(key);
return value == null ? 0 : Long.parseLong(value);
}
/**
* 重置指定业务类型当天的序号(仅供测试或特殊场景使用)
*
* @param businessType 业务类型
*/
public void reset(String businessType) {
String dateStr = LocalDate.now().format(DATE_FORMATTER);
String key = KEY_PREFIX + businessType + ":" + dateStr;
stringRedisTemplate.delete(key);
log.warn("重置工单编号序号: businessType={}, date={}", businessType, dateStr);
}
}

View File

@@ -0,0 +1,55 @@
package com.viewsh.module.ops.infrastructure.id;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 工单ID生成器
* <p>
* 使用雪花算法,保证分布式环境下的全局唯一性。
* 适用于工单主键生成。
*
* @author lzh
*/
@Slf4j
@Component
public class OrderIdGenerator {
private final OrderIdProperties orderIdProperties;
private SnowflakeIdGenerator snowflake;
/**
* 构造函数,注入配置
*/
public OrderIdGenerator(OrderIdProperties orderIdProperties) {
this.orderIdProperties = orderIdProperties;
}
/**
* 初始化雪花算法生成器
*/
@PostConstruct
public void init() {
int datacenterId = orderIdProperties.getDatacenterId();
int machineId = orderIdProperties.getMachineId();
this.snowflake = new SnowflakeIdGenerator(datacenterId, machineId);
log.info("OrderIdGenerator初始化完成: datacenterId={}, machineId={}",
datacenterId, machineId);
}
/**
* 生成工单ID
*
* @return 唯一的Long类型ID
*/
public Long generate() {
if (snowflake == null) {
throw new IllegalStateException("OrderIdGenerator 尚未初始化");
}
return snowflake.nextId();
}
}

View File

@@ -0,0 +1,28 @@
package com.viewsh.module.ops.infrastructure.id;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 工单ID生成器<E68890><E599A8>
*
* @author lzh
*/
@Data
@Component
@ConfigurationProperties(prefix = "ops.order")
public class OrderIdProperties {
/**
* 数据中心ID0-31
* 默认值1
*/
private int datacenterId = 1;
/**
* 机器ID0-31
* 默认值1
*/
private int machineId = 1;
}

View File

@@ -0,0 +1,184 @@
package com.viewsh.module.ops.infrastructure.id;
/**
* 雪花算法ID生成器
* <p>
* ID<49><44><EFBFBD>64位Long
* - 1位符号位永远为0
* - 41位时间戳毫秒级可用69年
* - 5位数据中心ID0-31
* - 5位机器ID0-31
* - 12位序列号毫秒内计数0-4095
* <p>
* 特性:
* - 分布式环境全局唯一
* - 时间有序
* - 高性能单机每毫秒可生成4096个ID
*
* @author lzh
*/
public class SnowflakeIdGenerator {
/**
* 起始时间戳2024-01-01 00:00:00
* 可根据项目实际情况调整
*/
private static final long EPOCH = 1704067200000L;
/**
* 各部分位数
*/
private static final long DATACENTER_ID_BITS = 5L;
private static final long MACHINE_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
/**
* 各部分最大值
*/
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS); // 31
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
/**
* 各部分位移
*/
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATACENTER_ID_BITS;
/**
* 数据中心ID
*/
private final long datacenterId;
/**
* 机器ID
*/
private final long machineId;
/**
* 序列号
*/
private long sequence = 0L;
/**
* 上次生成ID的时间戳
*/
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param datacenterId 数据中心ID0-31
* @param machineId 机器ID0-31
* @throws IllegalArgumentException 如果ID超出范围
*/
public SnowflakeIdGenerator(long datacenterId, long machineId) {
if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID) {
throw new IllegalArgumentException(
String.format("Datacenter ID must be between 0 and %d", MAX_DATACENTER_ID));
}
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException(
String.format("Machine ID must be between 0 and %d", MAX_MACHINE_ID));
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 生成下一个ID线程安全
*
* @return 唯一的Long类型ID
*/
public synchronized long nextId() {
long timestamp = getCurrentTimestamp();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 少量时钟回拨,等待
try {
Thread.sleep(offset << 1);
timestamp = getCurrentTimestamp();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
}
} catch (InterruptedException e) {
throw new RuntimeException("Clock moved backwards. Waiting interrupted", e);
}
} else {
throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
}
}
// 同一毫秒内,序列号自增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
/**
* 获取当前时间戳
*/
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
/**
* 等待下一毫秒
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
/**
* 解析ID获取时间戳信息
*
* @param id 雪花算法生成的ID
* @return 原始时间戳(毫秒)
*/
public static long parseTimestamp(long id) {
return ((id >> TIMESTAMP_SHIFT) & ~(-1L << 41L)) + EPOCH;
}
/**
* 解析ID获取数据中心ID
*
* @param id 雪花算法生成的ID
* @return 数据中心ID
*/
public static long parseDatacenterId(long id) {
return (id >> DATACENTER_ID_SHIFT) & MAX_DATACENTER_ID;
}
/**
* 解析ID获取机器ID
*
* @param id 雪花算法生成的ID
* @return 机器ID
*/
public static long parseMachineId(long id) {
return (id >> MACHINE_ID_SHIFT) & MAX_MACHINE_ID;
}
}

View File

@@ -0,0 +1,164 @@
package com.viewsh.module.ops.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单详情 VO基类
* <p>
* 支持多态,具体业务类型可继承此类添加扩展字段
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetailVO {
/**
* 工单ID
*/
private Long id;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单类型
*/
private String orderType;
/**
* 来源类型
*/
private String sourceType;
/**
* 工单标题
*/
private String title;
/**
* 工单描述
*/
private String description;
/**
* 优先级
*/
private Integer priority;
/**
* 工单状态
*/
private String status;
/**
* 区域ID
*/
private Long areaId;
/**
* 具体位置描述
*/
private String location;
/**
* 加急原因
*/
private String urgentReason;
/**
* 当前执行人ID
*/
private Long assigneeId;
/**
* 当前执行人姓名
*/
private String assigneeName;
/**
* 巡检员ID
*/
private Long inspectorId;
/**
* 巡检员姓名
*/
private String inspectorName;
/**
* 工单开始时间
*/
private LocalDateTime startTime;
/**
* 工单结束时间
*/
private LocalDateTime endTime;
/**
* 验收评分
*/
private Integer qualityScore;
/**
* 验收评语
*/
private String qualityComment;
/**
* 响应耗时(秒)
*/
private Integer responseSeconds;
/**
* 完成耗时(秒)
*/
private Integer completionSeconds;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 触发来源
*/
private String triggerSource;
/**
* 触发规则ID
*/
private Long triggerRuleId;
/**
* 触发设备ID
*/
private Long triggerDeviceId;
/**
* 触发设备Key
*/
private String triggerDeviceKey;
/**
* 扩展信息Map形式通用字段
*/
@Builder.Default
private Map<String, Object> extInfo = new java.util.HashMap<>();
}

View File

@@ -0,0 +1,46 @@
package com.viewsh.module.ops.service;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import java.util.Map;
/**
* 工单扩展表查询处理器接口
* <p>
* 各业务模块实现此接口,提供对应业务类型的扩展信息加载能力。
* 例如:
* - environment-biz 实现 CleanOrderExtQueryHandler
* - security-biz 实现 SecurityOrderExtQueryHandler
*
* @author lzh
*/
public interface OrderExtQueryHandler {
/**
* 是否支持此业务类型
*
* @param orderType 工单类型CLEAN、SECURITY、FACILITIES
* @return true-支持false-不支持
*/
boolean supports(String orderType);
/**
* 为汇总VO填充扩展信息
* <p>
* 用于分页查询场景,将扩展信息填充到 extInfo Map 中
*
* @param vo 汇总VO
* @param orderId 工单ID
*/
void enrichWithExtInfo(OrderSummaryVO vo, Long orderId);
/**
* 构建详情VO
* <p>
* 用于详情查询场景返回包含完整扩展信息的详情VO
*
* @param order 工单主表数据
* @return 详情VO
*/
OrderDetailVO buildDetailVO(OpsOrderDO order);
}

View File

@@ -0,0 +1,220 @@
package com.viewsh.module.ops.service;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import java.util.Map;
/**
* 工单中心查询服务接口
* <p>
* 职责:
* 1. 综合分页查询(支持所有业务类型)
* 2. 工单详情查询(含完整扩展信息)
* 3. 工单统计信息
*
* @author lzh
*/
public interface OrderQueryService {
/**
* 分页查询工单(支持所有业务类型)
* <p>
* 查询主表数据后,自动调用对应的扩展查询处理器填充扩展信息
*
* @param query 查询条件
* @return 分页结果
*/
PageResult<OrderSummaryVO> queryPage(OrderQuery query);
/**
* 查询工单详情(含完整扩展信息)
* <p>
* 根据工单的业务类型,调用对应的扩展查询处理器加载完整扩展信息
*
* @param orderId 工单ID
* @return 详情VO
*/
OrderDetailVO getDetail(Long orderId);
/**
* 获取工单统计信息
* <p>
* 按业务类型分组返回工单数量统计
*
* @param groupBy 分组维度status按状态统计null只按类型统计
* @return 统计结果
*/
Map<String, Object> getStats(String groupBy);
/**
* 工单查询条件
*/
class OrderQuery {
/**
* 页码从1开始
*/
private Integer page = 1;
/**
* 每页大小
*/
private Integer size = 20;
/**
* 工单类型(可选)
*/
private String orderType;
/**
* 工单状态(可选)
*/
private String status;
/**
* 优先级(可选)
*/
private Integer priority;
/**
* 区域ID可选
*/
private Long areaId;
/**
* 执行人ID可选
*/
private Long assigneeId;
/**
* 工单编号模糊查询(可选)
*/
private String orderCode;
/**
* 标题模糊查询(可选)
*/
private String title;
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public String getOrderType() {
return orderType;
}
public void setOrderType(String orderType) {
this.orderType = orderType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
public Long getAreaId() {
return areaId;
}
public void setAreaId(Long areaId) {
this.areaId = areaId;
}
public Long getAssigneeId() {
return assigneeId;
}
public void setAssigneeId(Long assigneeId) {
this.assigneeId = assigneeId;
}
public String getOrderCode() {
return orderCode;
}
public void setOrderCode(String orderCode) {
this.orderCode = orderCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public static OrderQuery builder() {
return new OrderQuery();
}
public OrderQuery withPage(Integer page) {
this.page = page;
return this;
}
public OrderQuery withSize(Integer size) {
this.size = size;
return this;
}
public OrderQuery withOrderType(String orderType) {
this.orderType = orderType;
return this;
}
public OrderQuery withStatus(String status) {
this.status = status;
return this;
}
public OrderQuery withPriority(Integer priority) {
this.priority = priority;
return this;
}
public OrderQuery withAreaId(Long areaId) {
this.areaId = areaId;
return this;
}
public OrderQuery withAssigneeId(Long assigneeId) {
this.assigneeId = assigneeId;
return this;
}
public OrderQuery withOrderCode(String orderCode) {
this.orderCode = orderCode;
return this;
}
public OrderQuery withTitle(String title) {
this.title = title;
return this;
}
}
}

View File

@@ -0,0 +1,186 @@
package com.viewsh.module.ops.service;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 工单中心查询服务实现
*
* @author lzh
*/
@Slf4j
@Service
public class OrderQueryServiceImpl implements OrderQueryService {
@Resource
private OpsOrderMapper opsOrderMapper;
/**
* 扩展查询处理器列表(由各业务模块注入)
*/
private final List<OrderExtQueryHandler> extQueryHandlers;
public OrderQueryServiceImpl(List<OrderExtQueryHandler> extQueryHandlers) {
this.extQueryHandlers = extQueryHandlers;
log.info("OrderQueryService 初始化,注册 {} 个扩展查询处理器", extQueryHandlers.size());
}
@Override
public PageResult<OrderSummaryVO> queryPage(OrderQuery query) {
// 1. 构建查询条件
LambdaQueryWrapperX<OpsOrderDO> wrapper = new LambdaQueryWrapperX<OpsOrderDO>()
.eqIfPresent(OpsOrderDO::getOrderType, query.getOrderType())
.eqIfPresent(OpsOrderDO::getStatus, query.getStatus())
.eqIfPresent(OpsOrderDO::getPriority, query.getPriority())
.eqIfPresent(OpsOrderDO::getAreaId, query.getAreaId())
.eqIfPresent(OpsOrderDO::getAssigneeId, query.getAssigneeId())
.likeIfPresent(OpsOrderDO::getOrderCode, query.getOrderCode())
.likeIfPresent(OpsOrderDO::getTitle, query.getTitle())
.orderByDesc(OpsOrderDO::getCreateTime);
// 2. 分页查询主表
PageResult<OpsOrderDO> pageResult = opsOrderMapper.selectPage(query.getPage(), query.getSize(), wrapper);
// 3. <20><><EFBFBD>换为 VO 并填充扩展信息
List<OrderSummaryVO> voList = pageResult.getList().stream()
.map(this::convertToSummaryVO)
.toList();
return new PageResult<>(voList, pageResult.getTotal());
}
@Override
public OrderDetailVO getDetail(Long orderId) {
// 1. 查询主表
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("工单不存在: " + orderId);
}
// 2. 找到对应的扩展查询处理器
OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType());
// 3. 构建详情VO
if (handler != null) {
return handler.buildDetailVO(order);
} else {
// 没有对应的扩展查询处理器返回基础VO
return convertToDetailVO(order);
}
}
@Override
public Map<String, Object> getStats(String groupBy) {
Map<String, Object> result = new HashMap<>();
if ("status".equals(groupBy)) {
// 按类型和状态统计
List<OpsOrderDO> allOrders = opsOrderMapper.selectList();
Map<String, Map<String, Long>> typeStatusMap = new HashMap<>();
for (OpsOrderDO order : allOrders) {
String type = order.getOrderType();
String status = order.getStatus();
typeStatusMap.computeIfAbsent(type, k -> new HashMap<>())
.merge(status, 1L, Long::sum);
}
result.putAll(typeStatusMap);
} else {
// 只按类型统计
List<OpsOrderDO> allOrders = opsOrderMapper.selectList();
Map<String, Long> typeCountMap = new HashMap<>();
for (OpsOrderDO order : allOrders) {
String type = order.getOrderType();
typeCountMap.merge(type, 1L, Long::sum);
}
result.putAll(typeCountMap);
}
return result;
}
/**
* 转换为汇总VO并填充扩展信息
*/
private OrderSummaryVO convertToSummaryVO(OpsOrderDO order) {
OrderSummaryVO vo = OrderSummaryVO.builder()
.id(order.getId())
.orderCode(order.getOrderCode())
.orderType(order.getOrderType())
.title(order.getTitle())
.description(order.getDescription())
.priority(order.getPriority())
.status(order.getStatus())
.areaId(order.getAreaId())
.location(order.getLocation())
.assigneeId(order.getAssigneeId())
.startTime(order.getStartTime())
.endTime(order.getEndTime())
.createTime(order.getCreateTime())
.build();
// 填充扩展信息
OrderExtQueryHandler handler = findExtQueryHandler(order.getOrderType());
if (handler != null) {
handler.enrichWithExtInfo(vo, order.getId());
}
return vo;
}
/**
* 转换为详情VO基础版本不含扩展信息
*/
private OrderDetailVO convertToDetailVO(OpsOrderDO order) {
return OrderDetailVO.builder()
.id(order.getId())
.orderCode(order.getOrderCode())
.orderType(order.getOrderType())
.sourceType(order.getSourceType())
.title(order.getTitle())
.description(order.getDescription())
.priority(order.getPriority())
.status(order.getStatus())
.areaId(order.getAreaId())
.location(order.getLocation())
.urgentReason(order.getUrgentReason())
.assigneeId(order.getAssigneeId())
.inspectorId(order.getInspectorId())
.startTime(order.getStartTime())
.endTime(order.getEndTime())
.qualityScore(order.getQualityScore())
.qualityComment(order.getQualityComment())
.responseSeconds(order.getResponseSeconds())
.completionSeconds(order.getCompletionSeconds())
.createTime(order.getCreateTime())
.updateTime(order.getUpdateTime())
.triggerSource(order.getTriggerSource())
.triggerRuleId(order.getTriggerRuleId())
.triggerDeviceId(order.getTriggerDeviceId())
.triggerDeviceKey(order.getTriggerDeviceKey())
.build();
}
/**
* 查找对应的扩展查询处理器
*/
private OrderExtQueryHandler findExtQueryHandler(String orderType) {
return extQueryHandlers.stream()
.filter(handler -> handler.supports(orderType))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,103 @@
package com.viewsh.module.ops.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 工单汇总信息 VO
* <p>
* 用于工单中心分页查询包含主表信息和扩展信息Map形式
*
* @author lzh
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSummaryVO {
/**
* 工单ID
*/
private Long id;
/**
* 工单编号
*/
private String orderCode;
/**
* 工单类型CLEAN/SECURITY/FACILITIES/SERVICE
*/
private String orderType;
/**
* 工单标题
*/
private String title;
/**
* 工单描述
*/
private String description;
/**
* 优先级0=P0/1=P1/2=P2
*/
private Integer priority;
/**
* 工单状态
*/
private String status;
/**
* 区域ID
*/
private Long areaId;
/**
* 具体位置描述
*/
private String location;
/**
* 当前执行人ID
*/
private Long assigneeId;
/**
* 当前执行人姓名(冗余)
*/
private String assigneeName;
/**
* 工单开始时间
*/
private LocalDateTime startTime;
/**
* 工单结束时间
*/
private LocalDateTime endTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 扩展信息Map形式
* <p>
* 根据工单类型不同,包含不同的扩展字段:
* - CLEAN: expectedDuration, cleaningType, difficultyLevel
* - SECURITY: route, checkpoint, patrolTime
*/
@Builder.Default
private Map<String, Object> extInfo = new java.util.HashMap<>();
}

View File

@@ -0,0 +1,182 @@
package com.viewsh.module.ops.infrastructure.code;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 工单编号生成器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
class OrderCodeGeneratorTest {
@Mock
private StringRedisTemplate stringRedisTemplate;
@Mock
private ValueOperations<String, String> valueOperations;
private OrderCodeGenerator orderCodeGenerator;
private static final String DATE_FORMAT = "yyyyMMdd";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
@BeforeEach
void setUp() {
orderCodeGenerator = new OrderCodeGenerator();
ReflectionTestUtils.setField(orderCodeGenerator, "stringRedisTemplate", stringRedisTemplate);
// Mock stringRedisTemplate.opsForValue()
lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
}
@Test
void testGenerate_FirstOrderOfToday() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-0001", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_SecondOrderOfToday() {
// Given
when(valueOperations.increment(anyString())).thenReturn(2L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-0002", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_DifferentBusinessTypes() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String cleanCode = orderCodeGenerator.generate("CLEAN");
String securityCode = orderCodeGenerator.generate("SECURITY");
// Then
assertTrue(cleanCode.startsWith("CLEAN-"));
assertTrue(securityCode.startsWith("SECURITY-"));
assertNotEquals(cleanCode, securityCode);
}
@Test
void testGenerate_LargeSequenceNumber() {
// Given
when(valueOperations.increment(anyString())).thenReturn(9999L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then
String expectedDate = LocalDate.now().format(DATE_FORMAT);
String expected = String.format("CLEAN-%s-9999", expectedDate);
assertEquals(expected, orderCode);
}
@Test
void testGenerate_NullBusinessType_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate(null));
}
@Test
void testGenerate_EmptyBusinessType_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> orderCodeGenerator.generate(""));
}
@Test
void testGenerate_SetsExpirationOnFirstUse() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
orderCodeGenerator.generate("CLEAN");
// Then
verify(stringRedisTemplate).expire(anyString(), eq(7L), any());
}
@Test
void testGetCurrentSeq_NoExistingRecords_ReturnsZero() {
// Given
when(valueOperations.get(anyString())).thenReturn(null);
// When
long seq = orderCodeGenerator.getCurrentSeq("CLEAN");
// Then
assertEquals(0, seq);
}
@Test
void testGetCurrentSeq_ExistingRecords_ReturnsValue() {
// Given
when(valueOperations.get(anyString())).thenReturn("5");
// When
long seq = orderCodeGenerator.getCurrentSeq("CLEAN");
// Then
assertEquals(5, seq);
}
@Test
void testReset_DeletesKey() {
// When
orderCodeGenerator.reset("CLEAN");
// Then
verify(stringRedisTemplate).delete(contains("CLEAN"));
}
@Test
void testGenerate_FormatConsistency() {
// Given
when(valueOperations.increment(anyString())).thenReturn(1L);
// When
String orderCode = orderCodeGenerator.generate("CLEAN");
// Then: 验证格式为 {TYPE}-{DATE}-{SEQUENCE}
String[] parts = orderCode.split("-");
assertEquals(3, parts.length);
assertEquals("CLEAN", parts[0]);
// 验证日期部分是8位数字
assertEquals(8, parts[1].length());
assertTrue(parts[1].matches("\\d{8}"));
// 验证序号部分是4位数字
assertEquals(4, parts[2].length());
assertTrue(parts[2].matches("\\d{4}"));
}
}

View File

@@ -0,0 +1,178 @@
package com.viewsh.module.ops.infrastructure.id;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/**
* 雪花算法ID生成器测试
*
* @author lzh
*/
class SnowflakeIdGeneratorTest {
private SnowflakeIdGenerator snowflake;
@BeforeEach
void setUp() {
snowflake = new SnowflakeIdGenerator(1, 1);
}
@Test
void testNextId_ReturnsUniqueId() {
// When
Long id1 = snowflake.nextId();
Long id2 = snowflake.nextId();
// Then
assertNotNull(id1);
assertNotNull(id2);
assertNotEquals(id1, id2);
assertTrue(id2 > id1, "ID应该递增");
}
@Test
void testNextId_GeneratesIncreasingIds() {
// Given
Long previousId = 0L;
// When: 生成100个ID
for (int i = 0; i < 100; i++) {
Long currentId = snowflake.nextId();
// Then
assertTrue(currentId > previousId, "ID应该严格递增");
previousId = currentId;
}
}
@Test
void testConstructor_ValidDatacenterId_Success() {
// When & Then: 测试边界值
assertDoesNotThrow(() -> new SnowflakeIdGenerator(0, 0));
assertDoesNotThrow(() -> new SnowflakeIdGenerator(31, 31));
}
@Test
void testConstructor_InvalidDatacenterId_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(-1, 1));
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(32, 1));
}
@Test
void testConstructor_InvalidMachineId_ThrowsException() {
// When & Then
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, -1));
assertThrows(IllegalArgumentException.class, () -> new SnowflakeIdGenerator(1, 32));
}
@Test
void testNextId_DifferentDatacenterOrMachine_GeneratesDifferentIds() {
// Given
SnowflakeIdGenerator generator1 = new SnowflakeIdGenerator(1, 1);
SnowflakeIdGenerator generator2 = new SnowflakeIdGenerator(1, 2);
SnowflakeIdGenerator generator3 = new SnowflakeIdGenerator(2, 1);
// When
Long id1 = generator1.nextId();
Long id2 = generator2.nextId();
Long id3 = generator3.nextId();
// Then: 不同数据中心或机器生成的ID应该不同
assertNotEquals(id1, id2);
assertNotEquals(id1, id3);
assertNotEquals(id2, id3);
}
@Test
void testNextId_Concurrent_ThreadSafe() throws InterruptedException {
// Given
int threadCount = 10;
int idsPerThread = 1000;
Set<Long> allIds = Collections.newSetFromMap(new ConcurrentHashMap<>());
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
// When: 多线程并发生成ID
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
for (int j = 0; j < idsPerThread; j++) {
Long id = snowflake.nextId();
boolean isNew = allIds.add(id);
assertTrue(isNew, "并发生成ID应该唯一");
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
// Then: 验证ID唯一性
assertEquals(threadCount * idsPerThread, allIds.size());
}
@RepeatedTest(10)
void testNextId_HighThroughput_NoDuplicateInSameMillisecond() {
// Given
Set<Long> ids = new HashSet<>();
// When: 快速生成100个ID
for (int i = 0; i < 100; i++) {
ids.add(snowflake.nextId());
}
// Then: 同一毫秒内应该生成不同的ID
assertEquals(100, ids.size());
}
@Test
void testParseTimestamp() {
// Given
Long id = snowflake.nextId();
long beforeParse = System.currentTimeMillis();
// When
long timestamp = SnowflakeIdGenerator.parseTimestamp(id);
long afterParse = System.currentTimeMillis();
// Then: 解析的时间戳应该接近当前时间
assertTrue(timestamp >= beforeParse - 1000, "时间戳应该接近当前时间");
assertTrue(timestamp <= afterParse + 1000, "时间戳应该接近当前时间");
}
@Test
void testParseDatacenterId() {
// Given
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10);
Long id = generator.nextId();
// When
long datacenterId = SnowflakeIdGenerator.parseDatacenterId(id);
// Then
assertEquals(5, datacenterId);
}
@Test
void testParseMachineId() {
// Given
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(5, 10);
Long id = generator.nextId();
// When
long machineId = SnowflakeIdGenerator.parseMachineId(id);
// Then
assertEquals(10, machineId);
}
}