feat(格口管理): 新增可用暂存格口查询功能并优化密码生成逻辑

新增AvailableStorageCellDTO用于返回可用暂存格口信息
添加查询指定门店下可用暂存格口的接口
优化格口密码生成逻辑确保在门店内唯一
添加重复请求过滤防止并发问题
This commit is contained in:
dzq 2025-12-19 09:08:44 +08:00
parent 1ab3d7ea13
commit fa3637ffb5
6 changed files with 206 additions and 14 deletions

3
.gitignore vendored
View File

@ -50,4 +50,5 @@ nbdist/
/.svn
.trae
.trae
/.claude

View File

@ -10,6 +10,7 @@ import com.agileboot.domain.cabinet.cell.CabinetCellApplicationService;
import com.agileboot.domain.cabinet.cell.command.OpenCellByPasswordCommand;
import com.agileboot.domain.cabinet.cell.command.StoreItemToCellCommand;
import com.agileboot.domain.cabinet.cell.dto.CabinetCellDTO;
import com.agileboot.domain.cabinet.cell.dto.AvailableStorageCellDTO;
import com.agileboot.domain.cabinet.cell.db.CabinetCellEntity;
import com.agileboot.domain.cabinet.cell.model.CabinetCellModel;
import com.agileboot.domain.cabinet.cell.model.CabinetCellModelFactory;
@ -32,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import com.agileboot.domain.common.cache.CaffeineCacheService;
import java.util.Date;
import java.util.List;
@ -50,6 +52,7 @@ public class CabinetCellController {
private final SmartCabinetModelFactory smartCabinetModelFactory;
private final CabinetMainboardModelFactory cabinetMainboardModelFactory;
private final CabinetCellApplicationService cabinetCellApplicationService;
private final CaffeineCacheService caffeineCacheService;
/**
* 管理员获取机柜格口列表
@ -71,6 +74,26 @@ public class CabinetCellController {
return ResponseDTO.ok(smartCabinetApplicationService.getRentingCabinetDetail(shopId));
}
@Operation(summary = "获取可使用的暂存柜格口列表",
description = "查询条件is_rented=0未租用usage_status=1空闲available_status=1正常")
@GetMapping("/availableStorageCells")
public ResponseDTO<List<AvailableStorageCellDTO>> getAvailableStorageCells(@RequestParam Long shopId) {
try {
if (shopId == null) {
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, "shopId不能为空");
}
List<AvailableStorageCellDTO> result = smartCabinetApplicationService.getAvailableStorageCells(shopId);
return ResponseDTO.ok(result);
} catch (IllegalArgumentException e) {
log.error("获取可用暂存格口列表参数错误", e);
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_PARAMETERS_INVALID, e.getMessage());
} catch (Exception e) {
log.error("获取可用暂存格口列表失败", e);
throw new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "获取可用暂存格口列表失败");
}
}
/**
* 用户获取自己租用的机柜格口列表
* @param corpid
@ -184,7 +207,23 @@ public class CabinetCellController {
@Operation(summary = "存入物品分配格口")
@PostMapping("/storeItem")
public ResponseDTO<CabinetCellDTO> storeItem(@RequestBody StoreItemToCellCommand command) {
CabinetCellDTO result = cabinetCellApplicationService.storeItemToCell(command);
return ResponseDTO.ok(result);
// 构造缓存键
String cacheKey = "storeItem:" + command.getShopId() + ":" + command.getCellType();
// 重复请求过滤
Boolean cached = caffeineCacheService.apiCache.get(cacheKey);
if (cached != null && cached) {
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, "重复请求,请稍后重试"));
}
// 放入缓存标记请求已处理
caffeineCacheService.apiCache.put(cacheKey, Boolean.TRUE);
try {
CabinetCellDTO result = cabinetCellApplicationService.storeItemToCell(command);
return ResponseDTO.ok(result);
} catch (Exception e) {
log.error("storeItem error: ", e);
return ResponseDTO.fail(new ApiException(ErrorCode.Internal.INTERNAL_ERROR, e));
}
}
}

View File

@ -12,6 +12,7 @@ import com.agileboot.domain.cabinet.cell.command.UpdateCabinetCellCommand;
import com.agileboot.domain.cabinet.cell.command.StoreItemToCellCommand;
import com.agileboot.domain.cabinet.cell.db.CabinetCellEntity;
import com.agileboot.domain.cabinet.cell.db.CabinetCellService;
import com.agileboot.domain.cabinet.cell.db.CabinetCellMapper;
import com.agileboot.domain.cabinet.cell.dto.CabinetCellDTO;
import com.agileboot.domain.cabinet.cell.model.CabinetCellModel;
import com.agileboot.domain.cabinet.cell.model.CabinetCellModelFactory;
@ -47,6 +48,7 @@ public class CabinetCellApplicationService {
private final SmartCabinetModelFactory smartCabinetModelFactory;
private final SmartCabinetService smartCabinetService;
private final CabinetMainboardModelFactory cabinetMainboardModelFactory;
private final CabinetCellMapper cabinetCellMapper;
public PageDTO<CabinetCellWithOrderCountDTO> getCabinetCellList(SearchCabinetCellWithOrdersQuery<CabinetCellEntity> query) {
Page<CabinetCellWithOrderCountDTO> page = cabinetCellService.getCellListWithOrders(query);
@ -140,7 +142,7 @@ public class CabinetCellApplicationService {
public void openCellByPassword(OpenCellByPasswordCommand command) {
// 根据密码查找格口
CabinetCellEntity cell;
CabinetCellEntity cell = null;
if (command.getShopId() != null) {
// 在指定店铺中查找匹配密码的格口
// 先获取该店铺下的柜子ID列表
@ -161,12 +163,6 @@ public class CabinetCellApplicationService {
.eq(CabinetCellEntity::getPassword, command.getPassword())
.eq(CabinetCellEntity::getDeleted, false)
.one();
} else {
// 全局查找匹配密码的格口
cell = cabinetCellService.lambdaQuery()
.eq(CabinetCellEntity::getPassword, command.getPassword())
.eq(CabinetCellEntity::getDeleted, false)
.one();
}
if (cell == null) {
@ -225,17 +221,33 @@ public class CabinetCellApplicationService {
.eq(CabinetCellEntity::getUsageStatus, 1) // 空闲
.eq(CabinetCellEntity::getAvailableStatus, 1) // 正常
.eq(CabinetCellEntity::getDeleted, false)
.last("limit 1")
.one();
if (cell == null) {
throw new RuntimeException("找不到符合条件的可用格口");
}
// 生成4位随机数字密码
String password = generateFourDigitPassword();
// 加载格口模型并更新密码和状态
// 加载格口模型
CabinetCellModel cellModel = cabinetCellModelFactory.loadById(cell.getCellId());
// 生成4位随机数字密码并确保在shopId下不重复最多尝试20次
String password = null;
int maxAttempts = 20;
for (int i = 0; i < maxAttempts; i++) {
String candidate = generateFourDigitPassword();
int count = cabinetCellMapper.countByPasswordAndShopId(candidate, command.getShopId());
if (count == 0) {
password = candidate;
break;
}
}
if (password == null) {
throw new RuntimeException("无法生成唯一密码,请稍后重试");
}
// 更新密码和状态
cellModel.setPassword(password);
cellModel.setUsageStatus(2); // 设置为已占用
cellModel.updateById();

View File

@ -119,4 +119,10 @@ public interface CabinetCellMapper extends BaseMapper<CabinetCellEntity> {
"INNER JOIN ab98_user au ON so.name = au.name AND so.mobile = au.tel AND au.deleted = 0 " +
"${ew.customSqlSegment}")
List<CabinetCellEntity> selectRentedCellsByAb98UserIdAndCorpid(@Param(Constants.WRAPPER) Wrapper<?> queryWrapper);
@Select("SELECT COUNT(1) FROM cabinet_cell cc " +
"INNER JOIN smart_cabinet sc ON cc.cabinet_id = sc.cabinet_id " +
"WHERE cc.password = #{password} AND sc.shop_id = #{shopId} " +
"AND cc.deleted = 0 AND sc.deleted = 0")
int countByPasswordAndShopId(@Param("password") String password, @Param("shopId") Long shopId);
}

View File

@ -0,0 +1,83 @@
package com.agileboot.domain.cabinet.cell.dto;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.common.annotation.ExcelColumn;
import com.agileboot.common.annotation.ExcelSheet;
import com.agileboot.domain.cabinet.cell.db.CabinetCellEntity;
import com.agileboot.domain.cabinet.smartCabinet.db.SmartCabinetEntity;
import lombok.Data;
import java.math.BigDecimal;
/**
* 可用暂存格口DTO
* 用于返回可使用的暂存柜格口列表
*/
@ExcelSheet(name = "可用暂存格口列表")
@Data
public class AvailableStorageCellDTO {
public AvailableStorageCellDTO(CabinetCellEntity entity, SmartCabinetEntity cabinet) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
if (cabinet != null) {
this.cabinetName = cabinet.getCabinetName();
}
// 判断password是否为空
this.hasPassword = entity.getPassword() != null && !entity.getPassword().trim().isEmpty();
}
}
@ExcelColumn(name = "格口ID")
private Long cellId;
@ExcelColumn(name = "关联柜机ID")
private Long cabinetId;
@ExcelColumn(name = "柜子名称")
private String cabinetName;
@ExcelColumn(name = "关联主板ID")
private Long mainboardId;
@ExcelColumn(name = "格口号")
private Integer cellNo;
@ExcelColumn(name = "针脚序号")
private Integer pinNo;
@ExcelColumn(name = "库存数量")
private Integer stock;
@ExcelColumn(name = "格口租用价格")
private BigDecimal cellPrice;
@ExcelColumn(name = "是否已租用")
private Integer isRented;
@ExcelColumn(name = "格口类型")
private Integer cellType;
@ExcelColumn(name = "使用状态")
private Integer usageStatus;
@ExcelColumn(name = "可用状态")
private Integer availableStatus;
@ExcelColumn(name = "是否有密码")
private Boolean hasPassword;
@ExcelColumn(name = "关联商品ID")
private Long goodsId;
@ExcelColumn(name = "商品名称")
private String goodsName;
@ExcelColumn(name = "商品价格")
private BigDecimal price;
@ExcelColumn(name = "封面图URL")
private String coverImg;
// 注意不包含password字段
}

View File

@ -21,6 +21,7 @@ import com.agileboot.domain.cabinet.smartCabinet.command.UpdateSmartCabinetComma
import com.agileboot.domain.cabinet.smartCabinet.db.SmartCabinetEntity;
import com.agileboot.domain.cabinet.smartCabinet.db.SmartCabinetService;
import com.agileboot.domain.cabinet.smartCabinet.dto.SmartCabinetDTO;
import com.agileboot.domain.cabinet.cell.dto.AvailableStorageCellDTO;
import com.agileboot.domain.cabinet.smartCabinet.model.SmartCabinetModel;
import com.agileboot.domain.cabinet.smartCabinet.model.SmartCabinetModelFactory;
import com.agileboot.domain.cabinet.smartCabinet.query.SearchSmartCabinetQuery;
@ -33,8 +34,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@ -388,4 +392,51 @@ public class SmartCabinetApplicationService {
}
return result;
}
/**
* 获取可用暂存格口列表
* 查询条件is_rented=0未租用usage_status=1空闲available_status=1正常
*
* @param shopId 门店ID
* @return 可使用的暂存格口列表平铺结构不按柜子分组
*/
public List<AvailableStorageCellDTO> getAvailableStorageCells(Long shopId) {
// 参数校验
if (shopId == null) {
throw new IllegalArgumentException("shopId不能为空");
}
// 1. 查询shopId下的所有智能柜
QueryWrapper<SmartCabinetEntity> smartCabinetWrapper = new QueryWrapper<>();
smartCabinetWrapper.eq("shop_id", shopId)
.eq("deleted", false);
List<SmartCabinetEntity> smartCabinets = smartCabinetService.list(smartCabinetWrapper);
if (smartCabinets.isEmpty()) {
return new ArrayList<>();
}
// 创建柜子ID到柜子实体的映射
Map<Long, SmartCabinetEntity> cabinetMap = smartCabinets.stream()
.collect(Collectors.toMap(SmartCabinetEntity::getCabinetId, Function.identity()));
// 2. 提取所有cabinet_id
List<Long> cabinetIds = new ArrayList<>(cabinetMap.keySet());
// 3. 查询满足条件的格口
QueryWrapper<CabinetCellEntity> cellWrapper = new QueryWrapper<>();
cellWrapper.in("cabinet_id", cabinetIds)
.eq("is_rented", 0)
.eq("usage_status", 1)
.eq("available_status", 1)
.eq("deleted", false)
.orderByAsc("cell_id");
List<CabinetCellEntity> availableCells = cabinetCellService.list(cellWrapper);
// 4. 转换为DTO列表
return availableCells.stream()
.map(cell -> new AvailableStorageCellDTO(cell, cabinetMap.get(cell.getCabinetId())))
.collect(Collectors.toList());
}
}