优化
This commit is contained in:
@@ -329,7 +329,7 @@ USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一客户编码',
|
||||
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
||||
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用',
|
||||
@@ -364,6 +364,15 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
KEY idx_iam_tenant_status (status, created_at)
|
||||
) ENGINE=InnoDB COMMENT='客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_number_sequence (
|
||||
sequence_key VARCHAR(64) NOT NULL COMMENT '业务编号类型',
|
||||
next_value BIGINT UNSIGNED NOT NULL COMMENT '下一待分配数字',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (sequence_key)
|
||||
) ENGINE=InnoDB COMMENT='IAM业务编号序列表';
|
||||
|
||||
INSERT IGNORE INTO iam_number_sequence(sequence_key, next_value) VALUES ('CUSTOMER_CODE', 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant_review (
|
||||
id BIGINT NOT NULL COMMENT '审核记录ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '被审核客户ID和数据隔离键',
|
||||
@@ -15186,7 +15195,7 @@ CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'reject_reason', '客户
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'sales_owner_name', '销售负责人姓名');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'service_package', '客户服务套餐');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'status', '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_code', '全局唯一客户编码');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_code', '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_name', '客户企业名称');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_type', '主体类型:MERCHANT企业客户');
|
||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'unified_social_credit_code', '统一社会信用代码');
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@
|
||||
- 新增模拟柜只保存智能装柜的 9 类计划渠道 channel_group_code;未关联已放舱订舱时 channel_code/channel_name 为空,关联后才冻结订舱的实际渠道。智能装柜全局工作台和订舱关联候选均按计划渠道组匹配;服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选支持按创建日期和预计到货日期闭区间筛选,状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入以响应 `canAssign` 为准,由有效柜次占用、剩余箱数、容量、组锁和版本等事实决定;人工加入允许订单国内仓及渠道与柜次不同,智能推荐允许跨国内仓但仍只选择柜次渠道。容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核事实必须经 `PendingReviewOrderClient` 重新核验 Order 权威 `SUBMITTED` 订单,待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。计划明细通过 `totalPackageCount/remainingPackageCount` 返回可调总量和余量:测量前取 Order 原始申报箱数,进入 `WAIT_SPLIT/LOADING` 后取最新已审核测量箱数;只读补充失败时保守回退当前已分配数,调整写入仍重新查询并校验权威数量。同柜不可拆组只有全部成员命中状态集合时才返回,且必须成员完整、满箱并作为单个分页与分配单元,不能跨页或拆分。一次性移除未拣明细时,普通未拣项直接移除;同柜组仅全组未拣时整组移除,任一成员已拣则整组保留,单事务只递增一次计划版本。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。
|
||||
- 预装进入 `CONFIRMING/CONFIRMED` 只固化确认快照,不得锁死尚未完成柜次的智能装柜分配;装柜计划仍处于 `EDITING/LOADING` 时可继续新增,未产生实物拣货的明细可调整或移除,已拣明细保持锁定。
|
||||
- 客户入库费用只在登录客户范围、测量审核通过、`feeStatus=SENT` 且存在已发布明细时可见;确认必须具备 `merchant:order:edit`,携带当前 `expectedFeeVersion` 与幂等键。成功后记录确认成员和时间;客户未确认或该票尚未装柜完成时,平台仍可修改箱规规则、生成新费用版本并重新发送,新版本必须清空旧版客户确认快照。仅当“客户已确认且该票已装柜完成”同时成立时最终冻结;装柜完成不得代替客户自动确认。旧版本、未发送或重复异参确认均返回冲突,不做兼容覆盖。
|
||||
- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。装柜允许跨国内仓,仓库与渠道限制统一由上游装柜编排负责;PDA 只按已生成任务读取各入仓单自身的 `CONFIRMED` 库位资源,不得再用柜次仓库过滤任务明细、库存快照或拣货写入。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。
|
||||
- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。PDA 首页从 Warehouse 主档选择启用国内仓,一件代发与装柜待办列表必须按所选 `warehouseId` 服务端过滤;装柜任务内部仍允许跨国内仓,仓库与渠道限制统一由上游装柜编排负责,PDA 只按已生成任务读取各入仓单自身的 `CONFIRMED` 库位资源,不得再用柜次仓库过滤任务明细、库存快照或拣货写入。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。
|
||||
- PDA 部分拣货生成二级子入库单时,数量必须守恒:`已拣数量 + 子单数量 = 拣货前主单可用数量`。子单只允许一层,不能继续拆分。
|
||||
- 首张称重图片确认时按 `max(已确认称重图片数, ceil(有效体积 / 默认托盘容量))` 尽力预留并确认整票库位块;有效体积先取入库单已落地的正数实测体积,否则对最新测量批次的正数 `totalVolumeCbm/originalVolumeCbm` 取较大值,再无有效值才查询 Order 最新客户申报体积,全部无效则拒绝。后续确认绑定最小空槽,所需槽数增长时通过 Warehouse `/blocks/expand` 原子追加,既有 `blockId/slotNo/reservationId/locationId/sharedOverflow` 不得变化。库位指引失败只记录警告,不阻断称重确认;测量提交后以最新已确认图片调用 `ensureForSubmission` 再次补齐和校验,失败同样不回滚已提交的测量主流程。Firstleg 必须显式保存 `weightResultId -> blockId/slotNo/reservationId/locationId/locationCode`,按 `confirmedAt + weightResultId` 稳定顺序绑定,禁止按列表下标推断;普通连续位不足的新增槽由 Warehouse 放入共享 A0,不阻断确认。
|
||||
- 删除称重图片只解除图片和托盘快照绑定,将槽位恢复为 `RESERVED_EMPTY`;槽位和整票块继续归当前入仓码。解绑失败时不得调用文件删除,文件删除失败时本地解绑和重量结果删除必须回滚。
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 获取平台仓库主数据,供虚拟柜校验国内中转仓使用。 */
|
||||
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseMasterDataClient",
|
||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouses")
|
||||
@@ -15,10 +17,17 @@ public interface WarehouseMasterDataClient {
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long warehouseId);
|
||||
|
||||
@GetMapping("/domestic")
|
||||
ApiResponse<List<WarehouseOption>> domestic(
|
||||
@RequestHeader("X-Internal-Service-Token") String token);
|
||||
|
||||
record Detail(Warehouse warehouse) {
|
||||
}
|
||||
|
||||
record Warehouse(Long id, Long tenantId, String warehouseCode, String internalCode, String warehouseName,
|
||||
String warehouseType, String status) {
|
||||
}
|
||||
|
||||
record WarehouseOption(Long id, String warehouseCode, String warehouseName) {
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -39,9 +39,10 @@ public class PlatformDropshipPalletizationController {
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<List<DropshipPalletizationDtos.PendingTask>> pending(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @Positive long warehouseId,
|
||||
@RequestParam(required = false) @Size(max = 64) String keyword) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.pending(user.userId(), keyword));
|
||||
return ApiResponse.success(service.pending(user.userId(), warehouseId, keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}")
|
||||
@@ -121,6 +122,21 @@ public class PlatformDropshipPalletizationController {
|
||||
return ApiResponse.success(service.seal(taskId, palletId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/pallets/{palletId}/print")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ResponseEntity<byte[]> printTaskPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@PathVariable @Positive long palletId,
|
||||
@RequestParam @Positive long warehouseId,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
Pallet pallet = service.task(taskId, warehouseId, user.userId()).pallets().stream()
|
||||
.filter(item -> item.palletId() == palletId).findFirst()
|
||||
.orElseThrow(() -> new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.NOT_FOUND, "托盘不存在"));
|
||||
return printService.generateDropshipPallet(pallet, requestId);
|
||||
}
|
||||
|
||||
@GetMapping("/inbound-orders/{inboundId}/pallets")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:list')")
|
||||
public ApiResponse<List<Pallet>> inboundPallets(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+13
-1
@@ -20,6 +20,8 @@ import java.util.List;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.baiyue.firstleg.service.PdaWarehouseCatalogService;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient.WarehouseOption;
|
||||
import com.baiyue.firstleg.loading.service.CabinetLoadingEvidenceService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -51,6 +53,8 @@ public class PlatformLoadingPdaController {
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
@Resource
|
||||
private PdaWarehouseCatalogService warehouseCatalog;
|
||||
@Resource
|
||||
private CabinetLoadingEvidenceService evidenceService;
|
||||
|
||||
@GetMapping("/tasks")
|
||||
@@ -66,10 +70,18 @@ public class PlatformLoadingPdaController {
|
||||
@GetMapping("/tasks/pending")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<List<PendingTask>> pendingTasks(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @Positive long warehouseId,
|
||||
@RequestParam(required = false)
|
||||
@Size(max = 64) String keyword) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.pendingTasks(user.userId(), keyword));
|
||||
return ApiResponse.success(service.pendingTasks(user.userId(), warehouseId, keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/warehouses")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<List<WarehouseOption>> warehouses(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(warehouseCatalog.activeDomesticWarehouses());
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}")
|
||||
|
||||
@@ -30,10 +30,12 @@ public interface DropshipPalletizationMapper {
|
||||
o.warehouse_name,t.total_cartons,t.counted_cartons,t.allocated_cartons bound_cartons,
|
||||
t.counted_cartons-t.allocated_cartons unbound_cartons,t.discrepancy_cartons,t.task_status,t.created_at
|
||||
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
|
||||
WHERE t.task_status IN ('PENDING','CLAIMED') AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
|
||||
WHERE t.task_status IN ('PENDING','CLAIMED') AND t.warehouse_id=#{warehouseId}
|
||||
AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
|
||||
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%') OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
|
||||
ORDER BY t.created_at,t.id""")
|
||||
List<PendingTask> pending(@Param("operatorId") long operatorId,@Param("keyword") String keyword);
|
||||
List<PendingTask> pending(@Param("operatorId") long operatorId,@Param("warehouseId") long warehouseId,
|
||||
@Param("keyword") String keyword);
|
||||
|
||||
@Select("""
|
||||
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
|
||||
|
||||
@@ -100,6 +100,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
|
||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
|
||||
WHERE task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED') AND booking.record_status = 'ACTIVE'
|
||||
AND booking.warehouse_id = #{warehouseId}
|
||||
AND (assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId})
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
||||
@@ -115,6 +116,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
</script>
|
||||
""")
|
||||
List<PendingTaskRow> selectPendingForOperator(@Param("operatorId") long operatorId,
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("keyword") String keyword);
|
||||
|
||||
class PendingTaskRow {
|
||||
|
||||
@@ -90,8 +90,8 @@ public class LoadingPickRepository {
|
||||
return tasks.selectLoadingByPlan(loadingPlanId, warehouseId);
|
||||
}
|
||||
|
||||
public List<PendingTaskRow> pendingTasks(long operatorId, String keyword) {
|
||||
return tasks.selectPendingForOperator(operatorId, keyword);
|
||||
public List<PendingTaskRow> pendingTasks(long operatorId, long warehouseId, String keyword) {
|
||||
return tasks.selectPendingForOperator(operatorId, warehouseId, keyword);
|
||||
}
|
||||
|
||||
public ContainerBookingEntity activeBookingByTask(long taskId) {
|
||||
|
||||
+9
-3
@@ -36,6 +36,7 @@ public class DropshipPalletizationService {
|
||||
return view(task);
|
||||
}
|
||||
if(!"CLAIMED".equals(task.getTaskStatus())||!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("当前任务不允许创建托盘");
|
||||
if(!Objects.equals(task.getCountedCartons(),task.getTotalCartons())) throw conflict("请先完成全部箱码清点,再生成托盘码");
|
||||
int expected=mapper.unboundCount(taskId);
|
||||
if(expected==0) throw conflict("当前没有待绑托箱,请先清点箱码");
|
||||
long palletId=IdWorker.getId();
|
||||
@@ -43,6 +44,10 @@ public class DropshipPalletizationService {
|
||||
mapper.insertPallet(palletId,taskId,task.getTenantId(),task.getInboundId(),task.getWarehouseId(),palletNo,operation,operatorId,operatorName);
|
||||
int bound=mapper.bindPending(taskId,palletId,operation,operatorId,operatorName);
|
||||
if(bound!=expected||mapper.adjustBound(taskId,operatorId,bound)!=1) throw conflict("待绑托箱已变化,请刷新后重试");
|
||||
if(mapper.seal(taskId,palletId,1,request.warehouseId(),operation,operatorId,operatorName)!=1)
|
||||
throw conflict("托盘完成状态已变化,请刷新后重试");
|
||||
if(mapper.completeTask(taskId)!=1 || mapper.completeInbound(taskId,task.getTenantId(),task.getInboundId(),operatorId)!=1)
|
||||
throw conflict("一件代发入库完成状态已变化,请刷新后重试");
|
||||
return view(requireTask(taskId,request.warehouseId()));
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ public class DropshipPalletizationService {
|
||||
mapper.insertTask(IdWorker.getId(),inbound.getTenantId(),inbound.getId(),inbound.getWarehouseId(),inbound.getTotalPackageCount());
|
||||
}
|
||||
|
||||
public List<PendingTask> pending(long operatorId,String keyword){ return mapper.pending(operatorId,normalize(keyword)); }
|
||||
public List<PendingTask> pending(long operatorId,long warehouseId,String keyword){ return mapper.pending(operatorId,warehouseId,normalize(keyword)); }
|
||||
|
||||
@Transactional
|
||||
public Task claim(long taskId,long warehouseId,long operatorId,String operatorName){
|
||||
@@ -99,8 +104,8 @@ public class DropshipPalletizationService {
|
||||
if(!mismatch && request.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
|
||||
if(mismatch && request.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
|
||||
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
|
||||
PalletRow pallet=StringUtils.hasText(request.palletNo())
|
||||
? getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName):null;
|
||||
if(StringUtils.hasText(request.palletNo())) throw invalid("逐箱清点阶段不能提前绑定托盘码");
|
||||
PalletRow pallet=null;
|
||||
long cartonId=IdWorker.getId();
|
||||
mapper.insertCarton(cartonId,taskId,pallet==null?null:pallet.getId(),task.getTenantId(),task.getInboundId(),
|
||||
label.getInboundDetailId(),label.getCartonLabelId(),label.getSkuCode(),label.getCargoName(),label.getBoxCode(),
|
||||
@@ -113,6 +118,7 @@ public class DropshipPalletizationService {
|
||||
@Transactional
|
||||
public Task bindPending(long taskId,BindPendingRequest request,long operatorId,String operatorName){
|
||||
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
||||
if(!Objects.equals(task.getCountedCartons(),task.getTotalCartons())) throw conflict("请先完成全部箱码清点,再绑定托盘码");
|
||||
String operation=operation(request.operationId());
|
||||
if(mapper.bindOperationCount(taskId,operation)>0) return view(requireTask(taskId,request.warehouseId()));
|
||||
PalletRow pallet=getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName);
|
||||
|
||||
+2
-1
@@ -115,7 +115,8 @@ public class InboundCartonLabelPrintService {
|
||||
|
||||
/** 一件代发托盘标签:托盘号、入仓码、SKU箱数和托盘二维码。 */
|
||||
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String requestId) {
|
||||
if (pallet == null || !StringUtils.hasText(pallet.palletNo()) || pallet.items().isEmpty()) {
|
||||
if (pallet == null || !"SEALED".equals(pallet.status())
|
||||
|| !StringUtils.hasText(pallet.palletNo()) || pallet.items().isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "只有已完成且包含货物的一件代发托盘可以打印");
|
||||
}
|
||||
String inboundCodes = pallet.items().stream().map(item -> item.inboundCode())
|
||||
|
||||
@@ -233,9 +233,9 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), warehouseId);
|
||||
}
|
||||
|
||||
public List<PendingTask> pendingTasks(long operatorId, String keyword) {
|
||||
public List<PendingTask> pendingTasks(long operatorId, long warehouseId, String keyword) {
|
||||
String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim() : null;
|
||||
return repository.pendingTasks(operatorId, normalizedKeyword).stream()
|
||||
return repository.pendingTasks(operatorId, warehouseId, normalizedKeyword).stream()
|
||||
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerBookingId(),
|
||||
row.getCabinetNo(), row.getContainerNo(),
|
||||
row.getSourceType(), row.getRecordStatus(), row.getMergedIntoContainerBookingId(),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient.WarehouseOption;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 为国内 PDA 提供启用中的国内仓选择项,主档仍由 Warehouse Service 唯一维护。 */
|
||||
@Service
|
||||
public class PdaWarehouseCatalogService {
|
||||
@Resource private WarehouseMasterDataClient warehouses;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
public List<WarehouseOption> activeDomesticWarehouses() {
|
||||
try {
|
||||
ApiResponse<List<WarehouseOption>> response = warehouses.domestic(internalToken);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw unavailable();
|
||||
}
|
||||
return response.data();
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException unavailable() {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "国内仓目录暂时不可用,请稍后重试");
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@ openapi: 3.0.3
|
||||
info:
|
||||
title: 一件代发国内清点组托 API
|
||||
version: 2.0.0
|
||||
description: 测量、计费后独立下发。箱码逐箱确认 SKU 实收数量;托盘码可先扫或后扫,封托后作为不可拆分柜单元。
|
||||
description: 测量、计费后独立下发。先逐箱确认 SKU 实收数量;全部箱码清点完成后才原子生成、绑定并封存正式 DP 托盘码,随后可打印托盘标签。
|
||||
paths:
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/pending:
|
||||
get:
|
||||
summary: 查询待领取或本人已领取任务
|
||||
x-required-authority: platform:firstleg:pda:view
|
||||
parameters: [{$ref: '#/components/parameters/WarehouseId'}, {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}}]
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}:
|
||||
get:
|
||||
@@ -33,8 +34,8 @@ paths:
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/cartons/count:
|
||||
post:
|
||||
summary: 逐箱确认 SKU 实收数量,可同时绑定当前托盘
|
||||
description: palletNo 为空时进入 COUNTED_UNBOUND;不为空时幂等获取或创建 OPEN 托盘并绑定。数量不一致必须 ACCEPT_VARIANCE 且填写说明。
|
||||
summary: 逐箱确认 SKU 实收数量
|
||||
description: 清点阶段只记录箱级实收并进入 COUNTED_UNBOUND,不得提前生成或绑定托盘码。数量不一致必须 ACCEPT_VARIANCE 且填写说明。
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CountCartonRequest'}}}}
|
||||
@@ -48,8 +49,8 @@ paths:
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/create-and-bind:
|
||||
post:
|
||||
summary: 系统生成 DP 托盘码并原子绑定全部待绑箱
|
||||
description: 仅当前仓库本人已领取的待组托任务可执行。没有待绑箱时不发码、不建空托。operationId 在同任务和仓库重放不重复创建;不同任务使用同幂等号拒绝。返回当前 Task。
|
||||
summary: 完成一件代发入库并生成正式 DP 托盘码
|
||||
description: 仅全部箱码已经清点的本人任务可执行;同一事务内生成 DP 托盘码、绑定全部箱、封托并把入仓单推进到待分柜。未完成清点或没有待绑箱时不发码、不建空托。operationId 重放不重复创建;返回已完成 Task。
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateAndBindRequest'}}}}
|
||||
@@ -72,6 +73,17 @@ paths:
|
||||
- {name: palletId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/SealPalletRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/print:
|
||||
get:
|
||||
summary: PDA 打印已封托的一件代发托盘码
|
||||
description: 仅当前仓库、当前操作员可访问;未封托托盘拒绝打印。返回 100mm × 100mm PDF。
|
||||
x-required-authority: platform:firstleg:pda:view
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: palletId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
- {$ref: '#/components/parameters/WarehouseId'}
|
||||
responses:
|
||||
'200': {description: 托盘码 PDF, content: {application/pdf: {schema: {type: string, format: binary}}}}
|
||||
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets:
|
||||
get:
|
||||
summary: Admin 查询入仓单权威托盘结果
|
||||
@@ -134,7 +146,6 @@ components:
|
||||
actualSkuQuantity: {type: integer, minimum: 1}
|
||||
discrepancyAction: {type: string, enum: [MATCH, ACCEPT_VARIANCE]}
|
||||
discrepancyRemark: {type: string, maxLength: 500, nullable: true}
|
||||
palletNo: {$ref: '#/components/schemas/NullablePalletNo'}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
BindPendingRequest:
|
||||
type: object
|
||||
|
||||
@@ -3,6 +3,17 @@ info:
|
||||
title: Phase 3 Container PDA Picking API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/platform/v1/first-leg/pda-loading/warehouses:
|
||||
get:
|
||||
summary: List active domestic warehouses available to the PDA operator
|
||||
responses:
|
||||
'200':
|
||||
description: Warehouse master-data options used to scope PDA pending-task lists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/WarehouseOption'}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks:
|
||||
get:
|
||||
summary: Find the active container picking task in one platform warehouse
|
||||
@@ -18,6 +29,7 @@ paths:
|
||||
get:
|
||||
summary: List active tasks available to the authenticated PDA operator
|
||||
parameters:
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
- {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
responses:
|
||||
'200':
|
||||
@@ -175,6 +187,13 @@ components:
|
||||
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
ItemId: {name: itemId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
schemas:
|
||||
WarehouseOption:
|
||||
type: object
|
||||
required: [id, warehouseCode, warehouseName]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
warehouseCode: {type: string}
|
||||
warehouseName: {type: string}
|
||||
Task:
|
||||
type: object
|
||||
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
|
||||
|
||||
+13
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -10,6 +12,17 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DropshipPalletizationSqlContractTest {
|
||||
@Test
|
||||
void pendingPdaTasksAreScopedByTheSelectedWarehouse() throws Exception {
|
||||
String sql = DropshipPalletizationMapper.class
|
||||
.getDeclaredMethod("pending", long.class, long.class, String.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql)
|
||||
.contains("t.warehouse_id=#{warehouseId}")
|
||||
.contains("t.assignee_id IS NULL OR t.assignee_id=#{operatorId}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void moduleAndUnifiedSchemasExposeCountingPalletAndWholePalletAssignmentFacts() throws Exception {
|
||||
String inbound = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
|
||||
+2
-1
@@ -37,12 +37,13 @@ class LoadingPickPdaTaskSqlContractTest {
|
||||
@Test
|
||||
void pendingTasksAreLimitedToUnassignedOrCurrentOperatorWithoutCustomerInput() throws Exception {
|
||||
String sql = LoadingPickTaskMapper.class
|
||||
.getDeclaredMethod("selectPendingForOperator", long.class, String.class)
|
||||
.getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql)
|
||||
.contains("task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED')")
|
||||
.contains("booking.record_status = 'ACTIVE'")
|
||||
.contains("booking.warehouse_id = #{warehouseId}")
|
||||
.contains("booking.source_type AS sourceType")
|
||||
.contains("booking.record_status AS recordStatus")
|
||||
.contains("booking.merged_into_container_booking_id AS mergedIntoContainerBookingId")
|
||||
|
||||
+20
-18
@@ -22,18 +22,27 @@ class DropshipPalletizationServiceTest {
|
||||
|
||||
DropshipPalletizationServiceTest(){ ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"documentNumbers",numbers); }
|
||||
|
||||
@Test void generatesAndBindsOnlyWhenPendingCartonsExist(){
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(task());
|
||||
@Test void generatesBindsSealsAndCompletesOnlyAfterEveryCartonIsCounted(){
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(readyToComplete());
|
||||
when(mapper.unboundCount(10L)).thenReturn(2); when(numbers.next("DP")).thenReturn("DP-260909-0001");
|
||||
when(mapper.bindPending(eq(10L),anyLong(),eq("create-1"),eq(7L),eq("张三"))).thenReturn(2);
|
||||
when(mapper.adjustBound(10L,7L,2)).thenReturn(1);
|
||||
when(mapper.seal(eq(10L),anyLong(),eq(1),eq(100L),eq("create-1"),eq(7L),eq("张三"))).thenReturn(1);
|
||||
when(mapper.completeTask(10L)).thenReturn(1); when(mapper.completeInbound(10L,1L,30L,7L)).thenReturn(1);
|
||||
service.createAndBind(10L,new com.baiyue.firstleg.dto.DropshipPalletizationDtos.CreateAndBindRequest(100L,"create-1"),7L,"张三");
|
||||
verify(mapper).insertPallet(anyLong(),eq(10L),eq(1L),eq(30L),eq(100L),eq("DP-260909-0001"),eq("create-1"),eq(7L),eq("张三"));
|
||||
verify(numbers,times(1)).next("DP");
|
||||
}
|
||||
|
||||
@Test void noPendingCartonsNeverAllocatesNumberOrEmptyPallet(){
|
||||
@Test void incompleteCountingNeverAllocatesAPalletNumber(){
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(task());
|
||||
assertThatThrownBy(()->service.createAndBind(10L,new com.baiyue.firstleg.dto.DropshipPalletizationDtos.CreateAndBindRequest(100L,"early"),7L,"张三"))
|
||||
.hasMessageContaining("完成全部箱码清点");
|
||||
verifyNoInteractions(numbers);
|
||||
}
|
||||
|
||||
@Test void noPendingCartonsNeverAllocatesNumberOrEmptyPallet(){
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(readyToComplete());
|
||||
assertThatThrownBy(()->service.createAndBind(10L,new com.baiyue.firstleg.dto.DropshipPalletizationDtos.CreateAndBindRequest(100L,"empty"),7L,"张三")).hasMessageContaining("没有待绑托箱");
|
||||
verifyNoInteractions(numbers);
|
||||
verify(mapper,never()).insertPallet(anyLong(),anyLong(),anyLong(),anyLong(),anyLong(),anyString(),anyString(),anyLong(),anyString());
|
||||
@@ -48,7 +57,7 @@ class DropshipPalletizationServiceTest {
|
||||
}
|
||||
|
||||
@Test void rejectsOtherOperatorAndCrossTaskReplay(){
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(task());
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(readyToComplete());
|
||||
var request=new com.baiyue.firstleg.dto.DropshipPalletizationDtos.CreateAndBindRequest(100L,"replay");
|
||||
assertThatThrownBy(()->service.createAndBind(10L,request,8L,"李四")).hasMessageContaining("当前操作员");
|
||||
PalletRow other=pallet(); other.setTaskId(11L); when(mapper.palletByOpenOperation("replay")).thenReturn(other);
|
||||
@@ -59,7 +68,7 @@ class DropshipPalletizationServiceTest {
|
||||
@Test void rejectsUnknownWarehouseAndChangedBindCount(){
|
||||
var request=new com.baiyue.firstleg.dto.DropshipPalletizationDtos.CreateAndBindRequest(100L,"new");
|
||||
assertThatThrownBy(()->service.createAndBind(10L,request,7L,"张三")).hasMessageContaining("当前仓库");
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(task());
|
||||
when(mapper.lockTask(10L,100L)).thenReturn(10L); when(mapper.task(10L,100L)).thenReturn(readyToComplete());
|
||||
when(mapper.unboundCount(10L)).thenReturn(2); when(numbers.next("DP")).thenReturn("DP-260909-0002");
|
||||
assertThatThrownBy(()->service.createAndBind(10L,request,7L,"张三")).hasMessageContaining("待绑托箱已变化");
|
||||
verify(mapper,never()).adjustBound(anyLong(),anyLong(),anyInt());
|
||||
@@ -87,23 +96,16 @@ class DropshipPalletizationServiceTest {
|
||||
verify(mapper,never()).insertCarton(anyLong(),anyLong(),any(),anyLong(),anyLong(),anyLong(),anyLong(),any(),any(),any(),anyInt(),anyInt(),any(),any(),any(),anyLong(),any());
|
||||
}
|
||||
|
||||
@Test void countsOneCartonDirectlyIntoScannedPallet(){
|
||||
TaskRow before=task(),after=task(); after.setCountedCartons(1); after.setAllocatedCartons(1);
|
||||
LabelRow label=label(); PalletRow pallet=pallet(); CartonRow carton=carton();
|
||||
when(mapper.task(10L,100L)).thenReturn(before,after);
|
||||
when(mapper.label(10L,"BOX-1")).thenReturn(label); when(mapper.palletByNo("DP-260908-0001")).thenReturn(pallet);
|
||||
when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton);
|
||||
when(mapper.pallet(10L,20L,100L)).thenReturn(pallet); when(mapper.cartons(20L)).thenReturn(java.util.List.of(carton));
|
||||
when(mapper.metrics(20L)).thenReturn(new DropshipPalletizationMapper.PalletMetrics(java.math.BigDecimal.ONE,java.math.BigDecimal.TEN));
|
||||
@Test void countingCannotExposeOrBindAPalletNumberEarly(){
|
||||
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.label(10L,"BOX-1")).thenReturn(label());
|
||||
var request=new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260908-0001","op-1");
|
||||
var result=service.countCarton(10L,request,7L,"张三");
|
||||
assertThat(result.carton().countStatus()).isEqualTo("BOUND");
|
||||
assertThat(result.pallet().status()).isEqualTo("OPEN");
|
||||
verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),eq(40L),eq(50L),eq("SKU-1"),
|
||||
eq("货物"),eq("BOX-1"),eq(12),eq(12),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三"));
|
||||
assertThatThrownBy(()->service.countCarton(10L,request,7L,"张三"))
|
||||
.hasMessageContaining("不能提前绑定托盘码");
|
||||
verify(mapper,never()).insertCarton(anyLong(),anyLong(),any(),anyLong(),anyLong(),anyLong(),anyLong(),any(),any(),any(),anyInt(),anyInt(),any(),any(),any(),anyLong(),any());
|
||||
}
|
||||
|
||||
private static TaskRow task(){ TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setVersion(1);return t; }
|
||||
private static TaskRow readyToComplete(){ TaskRow t=task();t.setCountedCartons(2);return t; }
|
||||
private static LabelRow label(){LabelRow l=new LabelRow();l.setCartonLabelId(50L);l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setUnitsPerCarton(12);l.setInboundCode("IN-1");l.setBoxCode("BOX-1");return l;}
|
||||
private static PalletRow pallet(){PalletRow p=new PalletRow();p.setId(20L);p.setTaskId(10L);p.setTenantId(1L);p.setInboundId(30L);p.setWarehouseId(100L);p.setPalletNo("DP-260908-0001");p.setPalletStatus("OPEN");p.setVersion(1);return p;}
|
||||
private static CartonRow carton(){CartonRow c=new CartonRow();c.setId(60L);c.setTaskId(10L);c.setPalletId(20L);c.setPalletNo("DP-260908-0001");c.setInboundId(30L);c.setInboundDetailId(40L);c.setSkuCode("SKU-1");c.setCargoName("货物");c.setBoxCode("BOX-1");c.setExpectedSkuQuantity(12);c.setActualSkuQuantity(12);c.setDiscrepancyStatus("MATCH");return c;}
|
||||
|
||||
+28
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,6 +37,25 @@ class InboundCartonLabelPrintServiceTest {
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropshipPalletLabelIsAvailableOnlyAfterSeal() {
|
||||
Pallet open = dropshipPallet("OPEN");
|
||||
assertThatThrownBy(() -> service.generateDropshipPallet(open, "request-open"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("已完成");
|
||||
verifyNoInteractions(print);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.eq("request-sealed"), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(new ResponseEntity<>(new byte[] {7, 8, 9}, headers,
|
||||
org.springframework.http.HttpStatus.OK));
|
||||
|
||||
ResponseEntity<byte[]> response = service.generateDropshipPallet(dropshipPallet("SEALED"), "request-sealed");
|
||||
assertThat(response.getBody()).containsExactly(7, 8, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardsStableLabelsAndOnlySafePdfHeaders() {
|
||||
when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch());
|
||||
@@ -159,4 +180,11 @@ class InboundCartonLabelPrintServiceTest {
|
||||
"PCHK2608040174", "MCC1", "CTNS:19-1", "洛杉矶普船海卡");
|
||||
return new InboundCartonLabels.Batch(70001L, 80001L, "PCHK2608040174", 19, List.of(label));
|
||||
}
|
||||
|
||||
private Pallet dropshipPallet(String status) {
|
||||
SkuItem item = new SkuItem(70001L, "PCHK2608040174", 2L,
|
||||
"SKU-1", "音箱", 1, 19, List.of("PCHK2608040174#19-1"));
|
||||
return new Pallet(90001L, "DP-260911-0001", status, 1, 1, 0,
|
||||
BigDecimal.ONE, BigDecimal.ONE, 1, List.of(item), List.of(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,9 +337,9 @@ class LoadingPickServiceTest {
|
||||
row.setAssignmentVersion(0);
|
||||
row.setItemCount(1);
|
||||
row.setPendingItemCount(0);
|
||||
when(repository.pendingTasks(9L, null)).thenReturn(List.of(row));
|
||||
when(repository.pendingTasks(9L, 200001L, null)).thenReturn(List.of(row));
|
||||
|
||||
var result = service.pendingTasks(9L, null).get(0);
|
||||
var result = service.pendingTasks(9L, 200001L, null).get(0);
|
||||
|
||||
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
||||
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PdaWarehouseCatalogServiceTest {
|
||||
@Test
|
||||
void returnsDomesticWarehouseMasterDataWithoutHardcodedOptions() {
|
||||
WarehouseMasterDataClient client = mock(WarehouseMasterDataClient.class);
|
||||
PdaWarehouseCatalogService service = new PdaWarehouseCatalogService();
|
||||
ReflectionTestUtils.setField(service, "warehouses", client);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||
var option = new WarehouseMasterDataClient.WarehouseOption(200001L, "CN-DONGGUAN-01", "东莞仓");
|
||||
when(client.domestic("token")).thenReturn(ApiResponse.success(List.of(option)));
|
||||
|
||||
assertThat(service.activeDomesticWarehouses()).containsExactly(option);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsClosedWhenWarehouseDirectoryIsUnavailable() {
|
||||
WarehouseMasterDataClient client = mock(WarehouseMasterDataClient.class);
|
||||
PdaWarehouseCatalogService service = new PdaWarehouseCatalogService();
|
||||
ReflectionTestUtils.setField(service, "warehouses", client);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||
when(client.domestic("token")).thenThrow(new IllegalStateException("down"));
|
||||
|
||||
assertThatThrownBy(service::activeDomesticWarehouses).isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
|
||||
| 目标 | 入口 |
|
||||
|---|---|
|
||||
| 客户资料/审核与企业联系信息 | `controller/PlatformTenantController.java`、`service/TenantService.java`;公司联系电话/邮箱属于企业资料,不作为登录标识且不要求跨企业唯一;审核必填资质仅营业执照与统一社会信用代码,法人身份证选填 |
|
||||
| 客户资料/审核与企业联系信息 | `controller/PlatformTenantController.java`、`service/TenantService.java`;企业客户编号由 `CustomerCodeRepository` 在创建事务内从 `00001` 起分配固定五位数字,平台技术主体仍为 `PLATFORM`;公司联系电话/邮箱属于企业资料,不作为登录标识且不要求跨企业唯一;审核必填资质仅营业执照与统一社会信用代码,法人身份证选填 |
|
||||
| 客户默认箱规与权重系数初始化、重试及补偿 | `service/CustomerDimensionRuleInitializationService.java`、`CustomerDimensionInitializationRelay.java`、`repository/CustomerDimensionInitializationRepository.java`、`client/CustomerDimensionRuleClient.java`、`model/CustomerDimensionInitializationTask.java`;持久任务异步调用 Firstleg `/internal/firstleg/v1/customer-dimension-configs/defaults`,尺寸配置和默认权重系数规则均返回成功后才标记 `READY` |
|
||||
| 商户成员、登录手机号与个人资料 | `controller/MerchantTenantController.java`、`service/MemberService.java`、`service/PersonalProfileService.java`、`client/AuthAccountClient.java`;成员手机号是可选登录标识,变更时同步 Auth、撤销该账号全部会话并审计,不与公司联系电话联动 |
|
||||
| 平台用户与海外 PDA 仓绑定 | `controller/PlatformUserManagementController.java`、`service/PlatformUserManagementService.java`、`dto/PlatformUserManagementDtos.java`、`entity/IamMembership.java`;`OVERSEAS_WAREHOUSE_PDA_OPERATOR` 必须且只能绑定一个 `overseasWarehouseId` |
|
||||
|
||||
@@ -110,7 +110,7 @@ public final class TenantDtos {
|
||||
@Schema(description = "Review type / 审核类型", example = "REGISTER") String reviewType,
|
||||
@Schema(description = "Review status / 审核状态", example = "PENDING") String status,
|
||||
@Schema(description = "Customer name / 客户名称", example = "深圳虫虫出海科技有限公司") String tenantName,
|
||||
@Schema(description = "Customer code / 客户编号", example = "TEN190000000000000002") String tenantCode,
|
||||
@Schema(description = "Customer code / 五位客户编号", example = "00001") String tenantCode,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") String contactEmail,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/** 在客户创建事务内串行分配固定五位客户编号。 */
|
||||
@Repository
|
||||
public class CustomerCodeRepository {
|
||||
private static final String SEQUENCE_KEY = "CUSTOMER_CODE";
|
||||
private static final long MAX_CUSTOMER_CODE = 99_999L;
|
||||
|
||||
@Resource
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public String nextCode() {
|
||||
jdbc.update("INSERT IGNORE INTO iam_number_sequence(sequence_key,next_value) VALUES (?,1)",
|
||||
SEQUENCE_KEY);
|
||||
Long value = jdbc.queryForObject(
|
||||
"SELECT next_value FROM iam_number_sequence WHERE sequence_key=? FOR UPDATE",
|
||||
Long.class, SEQUENCE_KEY);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户编号序列状态异常");
|
||||
}
|
||||
String code = format(value);
|
||||
int updated = jdbc.update(
|
||||
"UPDATE iam_number_sequence SET next_value=next_value+1 WHERE sequence_key=? AND next_value=?",
|
||||
SEQUENCE_KEY, value);
|
||||
if (updated != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户编号分配冲突,请重试");
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
static String format(long value) {
|
||||
if (value < 1) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户编号序列状态异常");
|
||||
}
|
||||
if (value > MAX_CUSTOMER_CODE) {
|
||||
throw new BizException(ResultCode.CONFLICT, "五位客户编号已用完");
|
||||
}
|
||||
return String.format(Locale.ROOT, "%05d", value);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import com.baiyue.iam.mapper.IamTenantMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantQualificationMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantReviewMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -43,6 +44,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private CustomerCodeRepository customerCodes;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
@@ -66,7 +68,7 @@ public class MerchantOnboardingService {
|
||||
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(tenantId);
|
||||
tenant.setTenantCode("TEN" + tenantId);
|
||||
tenant.setTenantCode(customerCodes.nextCode());
|
||||
tenant.setTenantName(request.companyName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
@@ -56,6 +57,8 @@ public class TenantService {
|
||||
@Resource
|
||||
private TenantRepository repository;
|
||||
@Resource
|
||||
private CustomerCodeRepository customerCodes;
|
||||
@Resource
|
||||
private ProfileResourceRepository profileRepository;
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@@ -165,7 +168,7 @@ public class TenantService {
|
||||
ensureAdministratorPhoneAvailable(contactPhone);
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(IdWorker.getId());
|
||||
tenant.setTenantCode("TEN" + tenant.getId());
|
||||
tenant.setTenantCode(customerCodes.nextCode());
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
|
||||
@@ -7,7 +7,7 @@ USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一客户编码',
|
||||
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
||||
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用、ARCHIVED已归档',
|
||||
@@ -43,6 +43,15 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
KEY idx_iam_tenant_status (status, created_at)
|
||||
) ENGINE=InnoDB COMMENT='客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_number_sequence (
|
||||
sequence_key VARCHAR(64) NOT NULL COMMENT '业务编号类型',
|
||||
next_value BIGINT UNSIGNED NOT NULL COMMENT '下一待分配数字',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (sequence_key)
|
||||
) ENGINE=InnoDB COMMENT='IAM业务编号序列表';
|
||||
|
||||
INSERT IGNORE INTO iam_number_sequence(sequence_key, next_value) VALUES ('CUSTOMER_CODE', 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_customer_dimension_initialization_task (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID及任务聚合ID',
|
||||
operator_id BIGINT NOT NULL COMMENT '触发初始化的操作人ID;系统补偿为0',
|
||||
|
||||
@@ -1113,7 +1113,7 @@ components:
|
||||
reviewType: {type: string, example: REGISTER}
|
||||
status: {type: string, enum: [PENDING]}
|
||||
tenantName: {type: string, description: 客户名称}
|
||||
tenantCode: {type: string, description: 客户编号}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001', description: 五位客户编号}
|
||||
contactName: {type: string, description: 联系人}
|
||||
contactPhone: {type: string, nullable: true, description: 联系电话}
|
||||
contactEmail: {type: string, format: email, nullable: true, description: 联系邮箱}
|
||||
@@ -1199,7 +1199,7 @@ components:
|
||||
required: [id, tenantCode, tenantName, status, dimensionInitializationStatus, servicePackage]
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
||||
tenantCode: {type: string}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}
|
||||
dimensionInitializationStatus: {type: string, enum: [PENDING, READY, FAILED], description: 客户默认尺寸初始化状态;未 READY 时依赖该配置的业务失败关闭}
|
||||
@@ -1237,7 +1237,7 @@ components:
|
||||
required: [id, tenantCode, tenantName, status]
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$'}
|
||||
tenantCode: {type: string}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||
contactName: {type: string, nullable: true}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CustomerCodeRepositoryTest {
|
||||
@Test
|
||||
void allocatesFiveDigitCodeFromOne() {
|
||||
assertEquals("00001", CustomerCodeRepository.format(1));
|
||||
assertEquals("99999", CustomerCodeRepository.format(99_999));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAllocationAfterFiveDigitCapacityIsExhausted() {
|
||||
BizException failure = assertThrows(BizException.class,
|
||||
() -> CustomerCodeRepository.format(100_000));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, failure.getResultCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerCodeSchemaContractTest {
|
||||
@Test
|
||||
void baselineAndUnifiedSqlInitializeCustomerCodeSequenceAtOne() throws Exception {
|
||||
String baseline = new ClassPathResource("db/iam.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Path unifiedPath = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(unifiedPath)) {
|
||||
unifiedPath = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
}
|
||||
String unified = Files.readString(unifiedPath, StandardCharsets.UTF_8);
|
||||
|
||||
assertSequenceDefinition(baseline);
|
||||
assertSequenceDefinition(unified);
|
||||
}
|
||||
|
||||
private static void assertSequenceDefinition(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("tenant_code VARCHAR(8) NOT NULL")
|
||||
.contains("CREATE TABLE IF NOT EXISTS iam_number_sequence")
|
||||
.contains("PRIMARY KEY (sequence_key)")
|
||||
.contains("INSERT IGNORE INTO iam_number_sequence(sequence_key, next_value) VALUES ('CUSTOMER_CODE', 1)");
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class CustomerListExportProviderServiceTest {
|
||||
private IamTenant tenant(long id) {
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(id);
|
||||
tenant.setTenantCode("TEN" + id);
|
||||
tenant.setTenantCode(String.format("%05d", id));
|
||||
tenant.setTenantName("柏悦物流" + id);
|
||||
tenant.setLegalName("柏悦物流有限公司");
|
||||
tenant.setUnifiedSocialCreditCode("91440000123456789X");
|
||||
|
||||
@@ -14,7 +14,9 @@ import com.baiyue.iam.mapper.IamTenantMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantQualificationMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantReviewMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
@@ -42,7 +44,14 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private SecurityEventOutboxService securityEvents;
|
||||
@MockBean private CustomerCodeRepository customerCodes;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
|
||||
@BeforeEach
|
||||
void allocateCustomerCode() {
|
||||
when(customerCodes.nextCode()).thenReturn("00001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientRegistrationCreatesOwnerAndAssignsGlobalMerchantOwnerRole() {
|
||||
|
||||
@@ -76,6 +85,7 @@ class MerchantOnboardingServiceTest {
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals("PENDING", result.dimensionInitializationStatus());
|
||||
assertEquals("PENDING", tenant.getValue().getDimensionInitializationStatus());
|
||||
assertEquals("00001", tenant.getValue().getTenantCode());
|
||||
assertEquals(result.tenantId(), tenant.getValue().getId());
|
||||
assertEquals("测试企业", tenant.getValue().getLegalName());
|
||||
assertEquals("张三", tenant.getValue().getLegalRepresentative());
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
@@ -21,6 +22,7 @@ import static org.mockito.Mockito.when;
|
||||
@SpringJUnitConfig(TenantService.class)
|
||||
class TenantCustomerExportTest {
|
||||
@MockBean private TenantRepository repository;
|
||||
@MockBean private CustomerCodeRepository customerCodes;
|
||||
@MockBean private ProfileResourceRepository profileRepository;
|
||||
@MockBean private MemberRepository memberRepository;
|
||||
@MockBean private AuthAccountClient authAccountClient;
|
||||
@@ -34,7 +36,7 @@ class TenantCustomerExportTest {
|
||||
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(101L);
|
||||
tenant.setTenantCode("TEN101");
|
||||
tenant.setTenantCode("00101");
|
||||
tenant.setTenantName("柏悦物流");
|
||||
tenant.setContactPhone("13800138000");
|
||||
tenant.setContactEmail("ops@example.com");
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.baiyue.iam.dto.TenantDtos.ResetCustomerOwnerPasswordRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
@@ -51,6 +52,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
class TenantServiceTest {
|
||||
@MockBean private TenantRepository repository;
|
||||
@MockBean private CustomerCodeRepository customerCodes;
|
||||
@MockBean private ProfileResourceRepository profileRepository;
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@@ -63,6 +65,7 @@ class TenantServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void allowUnusedAdministratorPhone() {
|
||||
when(customerCodes.nextCode()).thenReturn("00001");
|
||||
when(authClient.availability(eq("internal-token"), any()))
|
||||
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(true));
|
||||
}
|
||||
@@ -72,7 +75,7 @@ class TenantServiceTest {
|
||||
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(10001L);
|
||||
tenant.setTenantCode("TEN10001");
|
||||
tenant.setTenantCode("10001");
|
||||
tenant.setTenantName("Example Merchant");
|
||||
tenant.setStatus("ACTIVE");
|
||||
tenant.setLegalName("Example Merchant Ltd");
|
||||
@@ -91,7 +94,7 @@ class TenantServiceTest {
|
||||
var result = service.pageMerchants(1, 10, "Example", "ACTIVE");
|
||||
|
||||
assertEquals(1, result.total());
|
||||
assertEquals("TEN10001", result.records().get(0).tenantCode());
|
||||
assertEquals("10001", result.records().get(0).tenantCode());
|
||||
assertEquals("Alice", result.records().get(0).contactName());
|
||||
assertEquals("Sales Zhang", result.records().get(0).salesOwnerName());
|
||||
assertEquals(30, result.records().get(0).paymentTermDays());
|
||||
@@ -108,7 +111,7 @@ class TenantServiceTest {
|
||||
review.setReviewType("REGISTER");
|
||||
review.setCreatedAt(LocalDateTime.of(2026, 8, 17, 10, 27));
|
||||
IamTenant tenant = merchant(10001L, "PENDING");
|
||||
tenant.setTenantCode("TEN10001");
|
||||
tenant.setTenantCode("10001");
|
||||
tenant.setTenantName("Example Merchant");
|
||||
tenant.setContactName("Alice");
|
||||
tenant.setContactPhone("13800138000");
|
||||
@@ -124,7 +127,7 @@ class TenantServiceTest {
|
||||
assertEquals("REGISTER", result.get(0).reviewType());
|
||||
assertEquals("PENDING", result.get(0).status());
|
||||
assertEquals("Example Merchant", result.get(0).tenantName());
|
||||
assertEquals("TEN10001", result.get(0).tenantCode());
|
||||
assertEquals("10001", result.get(0).tenantCode());
|
||||
assertEquals("Alice", result.get(0).contactName());
|
||||
assertEquals("13800138000", result.get(0).contactPhone());
|
||||
assertEquals("alice@example.com", result.get(0).contactEmail());
|
||||
@@ -136,7 +139,7 @@ class TenantServiceTest {
|
||||
@Test
|
||||
void pageFinanceCustomersKeepsFullContactSummaryForInternalFinance() {
|
||||
IamTenant tenant = merchant(10001L, "ACTIVE");
|
||||
tenant.setTenantCode("TEN10001");
|
||||
tenant.setTenantCode("10001");
|
||||
tenant.setTenantName("Example Merchant");
|
||||
tenant.setContactName("Alice");
|
||||
tenant.setContactPhone("13800138000");
|
||||
@@ -145,7 +148,7 @@ class TenantServiceTest {
|
||||
|
||||
var result = service.pageFinanceCustomers(1, 10, "Alice", "ACTIVE", null);
|
||||
|
||||
assertEquals("TEN10001", result.records().get(0).tenantCode());
|
||||
assertEquals("10001", result.records().get(0).tenantCode());
|
||||
assertEquals("Alice", result.records().get(0).contactName());
|
||||
assertEquals("13800138000", result.records().get(0).contactPhone());
|
||||
verify(repository).pageFinanceCustomers(1, 10, "Alice", "ACTIVE", null);
|
||||
@@ -320,6 +323,7 @@ class TenantServiceTest {
|
||||
9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", created.tenant().status());
|
||||
assertEquals("00001", created.tenant().tenantCode());
|
||||
assertEquals("PENDING", created.tenant().dimensionInitializationStatus());
|
||||
assertEquals("Bob", created.tenant().legalRepresentative());
|
||||
assertEquals("基础版", created.tenant().servicePackage());
|
||||
@@ -515,7 +519,7 @@ class TenantServiceTest {
|
||||
private IamTenant merchant(long id, String status) {
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(id);
|
||||
tenant.setTenantCode("TEN" + id);
|
||||
tenant.setTenantCode(String.format("%05d", id));
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus(status);
|
||||
tenant.setDimensionInitializationStatus("READY");
|
||||
|
||||
+2
-1
@@ -22,5 +22,6 @@ public interface DomesticWarehouseDirectoryClient {
|
||||
record Warehouse(Long id, Long tenantId, String warehouseCode, String warehouseName,
|
||||
String warehouseType, String countryCode, String status) { }
|
||||
|
||||
record Address(String stateProvince) { }
|
||||
record Address(String addressType, String countryCode, String stateProvince, String city,
|
||||
String district, String postalCode, String addressLine1, String addressLine2) { }
|
||||
}
|
||||
|
||||
+40
-1
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
|
||||
import com.baiyue.lastleg.client.PrintPdfClient;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import feign.FeignException;
|
||||
@@ -13,6 +14,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -27,6 +29,7 @@ public class DispatchBolPrintService {
|
||||
|
||||
@Resource private DispatchService dispatches;
|
||||
@Resource private PrintPdfClient print;
|
||||
@Resource private DomesticWarehouseDirectoryClient warehouses;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
public GeneratedPdf generate(String dispatchNo, String requestId) {
|
||||
@@ -52,6 +55,7 @@ public class DispatchBolPrintService {
|
||||
}
|
||||
|
||||
private List<PrintPdfClient.Document> documents(DispatchResponse detail) {
|
||||
String shipFromAddress = warehouseAddress(detail.overseasWarehouseId());
|
||||
Map<String, List<DispatchResponse.BolLine>> groups = new LinkedHashMap<>();
|
||||
for (DispatchResponse.BolLine line : detail.bolLines()) groups.computeIfAbsent(text(line.fbaWarehouseCode()), ignored -> new ArrayList<>()).add(line);
|
||||
List<PrintPdfClient.Document> documents = new ArrayList<>();
|
||||
@@ -67,7 +71,7 @@ public class DispatchBolPrintService {
|
||||
List<PrintPdfClient.CargoRow> rows = lines.subList(offset, end).stream()
|
||||
.map(line -> new PrintPdfClient.CargoRow(text(line.containerNo()), text(line.palletNo()), text(line.fbaWarehouseCode()),
|
||||
text(line.warehouseNo()), text(line.fbaShipmentId()), text(line.poReferenceNo()), line.packageCount())).toList();
|
||||
documents.add(new PrintPdfClient.Document(text(detail.overseasWarehouseName()), "—", text(detail.carrierName()),
|
||||
documents.add(new PrintPdfClient.Document(text(detail.overseasWarehouseName()), shipFromAddress, text(detail.carrierName()),
|
||||
text(detail.fbaAppointmentNo()), dateTime(detail.estimatedArrivalAt()), text(detail.vehiclePlate()), "—",
|
||||
group.getKey(), destination, "—", text(detail.dispatchNo()), unique(lines, true), unique(lines, false),
|
||||
total, page + 1, pages, rows));
|
||||
@@ -77,6 +81,41 @@ public class DispatchBolPrintService {
|
||||
return List.copyOf(documents);
|
||||
}
|
||||
|
||||
private String warehouseAddress(String warehouseId) {
|
||||
long id;
|
||||
try {
|
||||
id = Long.parseLong(warehouseId == null ? "" : warehouseId.strip());
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "排车单未关联有效的海外仓");
|
||||
}
|
||||
try {
|
||||
var response = warehouses.activeWarehouse(id, internalToken);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库主数据暂不可用,无法生成BOL");
|
||||
}
|
||||
List<DomesticWarehouseDirectoryClient.Address> addresses = response.data().addresses();
|
||||
if (addresses == null || addresses.isEmpty()) return "—";
|
||||
DomesticWarehouseDirectoryClient.Address address = addresses.stream().filter(Objects::nonNull)
|
||||
.filter(value -> "OPERATING".equals(value.addressType())).findFirst()
|
||||
.orElseGet(() -> addresses.stream().filter(Objects::nonNull).findFirst().orElse(null));
|
||||
if (address == null) return "—";
|
||||
List<String> parts = new ArrayList<>();
|
||||
add(parts, address.addressLine1());
|
||||
add(parts, address.addressLine2());
|
||||
add(parts, address.city());
|
||||
add(parts, address.stateProvince());
|
||||
add(parts, address.postalCode());
|
||||
add(parts, address.countryCode());
|
||||
return parts.isEmpty() ? "—" : String.join(", ", parts);
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库主数据暂不可用,无法生成BOL");
|
||||
}
|
||||
}
|
||||
|
||||
private void add(List<String> parts, String value) {
|
||||
if (value != null && !value.isBlank()) parts.add(value.strip());
|
||||
}
|
||||
|
||||
static int pageCount(int rowCount) {
|
||||
if (rowCount <= FIRST_PAGE_ROWS) return 1;
|
||||
return 1 + (rowCount - FIRST_PAGE_ROWS + CONTINUATION_PAGE_ROWS - 1) / CONTINUATION_PAGE_ROWS;
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ class DomesticWarehouseDirectoryClientContractTest {
|
||||
"warehouseType:String", "countryCode:String", "status:String");
|
||||
assertThat(Arrays.stream(DomesticWarehouseDirectoryClient.Address.class.getRecordComponents())
|
||||
.map(value -> value.getName() + ":" + value.getType().getSimpleName()))
|
||||
.containsExactly("stateProvince:String");
|
||||
.containsExactly("addressType:String", "countryCode:String", "stateProvince:String", "city:String",
|
||||
"district:String", "postalCode:String", "addressLine1:String", "addressLine2:String");
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -1,10 +1,41 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
|
||||
import com.baiyue.lastleg.client.PrintPdfClient;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchBolPrintServiceTest {
|
||||
@Mock private DispatchService dispatches;
|
||||
@Mock private PrintPdfClient print;
|
||||
@Mock private DomesticWarehouseDirectoryClient warehouses;
|
||||
@InjectMocks private DispatchBolPrintService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-test-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesDenseFirstAndContinuationPageCapacities() {
|
||||
assertThat(DispatchBolPrintService.pageCount(1)).isEqualTo(1);
|
||||
@@ -15,4 +46,33 @@ class DispatchBolPrintServiceTest {
|
||||
assertThat(DispatchBolPrintService.pageCount(70)).isEqualTo(3);
|
||||
assertThat(DispatchBolPrintService.pageCount(71)).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesTheOperatingWarehouseAddressIntoTheBolRequest() {
|
||||
String warehouseId = "193000000000200003";
|
||||
DispatchResponse detail = new DispatchResponse(1L, "DS-WH-20260911-624449", "DEV-23653906",
|
||||
null, null, "五月开发验收车队", null, null, warehouseId, "洛杉矶仓", null,
|
||||
null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE",
|
||||
1, 1, 1, 0, 0, 1, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(),
|
||||
List.of(new DispatchResponse.BolLine("TSTU3539519", "DP-260911-0027", "ABE2", "ABE2",
|
||||
"NYEXX2609110214", null, null, 1)));
|
||||
var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "CA", "Los Angeles",
|
||||
null, "90001", "Los Angeles warehouse generic address pending maintenance", null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE");
|
||||
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
|
||||
when(warehouses.activeWarehouse(Long.parseLong(warehouseId), "internal-test-token"))
|
||||
.thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse, List.of(address))));
|
||||
when(print.generateDispatchBol(eq("internal-test-token"), eq("request-1"), any()))
|
||||
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
|
||||
.header("X-Print-Page-Count", "1").body("%PDF-".getBytes(StandardCharsets.US_ASCII)));
|
||||
|
||||
service.generate(detail.dispatchNo(), "request-1");
|
||||
|
||||
ArgumentCaptor<PrintPdfClient.Request> request = ArgumentCaptor.forClass(PrintPdfClient.Request.class);
|
||||
verify(print).generateDispatchBol(eq("internal-test-token"), eq("request-1"), request.capture());
|
||||
assertThat(request.getValue().documents()).singleElement().satisfies(document ->
|
||||
assertThat(document.shipFromAddress()).isEqualTo(
|
||||
"Los Angeles warehouse generic address pending maintenance, Los Angeles, CA, 90001, US"));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -91,7 +91,8 @@ class FbaWarehouseValidatorTest {
|
||||
return new DomesticWarehouseDirectoryClient.Detail(
|
||||
new DomesticWarehouseDirectoryClient.Warehouse(193000000000000001L, 0L, code, name,
|
||||
type, "US", status),
|
||||
List.of(new DomesticWarehouseDirectoryClient.Address("CA")));
|
||||
List.of(new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "CA", "Los Angeles",
|
||||
null, "90001", "123 Logistics Ave", null)));
|
||||
}
|
||||
|
||||
private void assertInvalid(org.assertj.core.api.ThrowableAssert.ThrowingCallable action) {
|
||||
|
||||
@@ -61,7 +61,8 @@ public class DispatchBolRenderer {
|
||||
line(s,u,105,top,105,bottom); line(s,u,left,50,right,50);
|
||||
blackHeader(s,u,f,"SHIP FROM",left,top,105,27); blackHeader(s,u,f,"CARRIER",105,top,right,27);
|
||||
blackHeader(s,u,f,"SHIP TO",left,50,105,56); blackHeader(s,u,f,"REFERENCE",105,50,right,56);
|
||||
text(s,u,f,d.shipFromName(),9.5f,left+2,33); text(s,u,f,"Address: " + d.shipFromAddress(),7.5f,left+2,40);
|
||||
text(s,u,f,d.shipFromName(),9.5f,left+2,33);
|
||||
wrappedText(s,u,f,"Address: " + d.shipFromAddress(),7.2f,left+2,40,91,2,4.5);
|
||||
text(s,u,f,"Carrier Name: " + d.carrierName(),7.5f,107,32); text(s,u,f,"ISA #: " + d.isaNumber(),7.5f,107,36.5);
|
||||
text(s,u,f,"Appointment time: " + d.appointmentTime(),7.5f,107,41); text(s,u,f,"Trailer #: " + d.trailerNumber(),7.5f,107,45.5);
|
||||
text(s,u,f,"Seal #: " + d.sealNumber(),7.5f,107,49);
|
||||
@@ -132,6 +133,21 @@ public class DispatchBolRenderer {
|
||||
private void fill(PDPageContentStream s,PdfUnits u,Color color,double x1,double y1,double x2,double y2)throws IOException{s.setNonStrokingColor(color);s.addRect(PdfUnits.mmToPt(x1),u.topDownMmToPdfY(y2),PdfUnits.mmToPt(x2-x1),PdfUnits.mmToPt(y2-y1));s.fill();}
|
||||
private void signature(PDPageContentStream s,PdfUnits u,PDFont f,double x,double y,String label)throws IOException{line(s,u,x,y,x+52,y);centeredCell(s,u,f,label,6.2f,x,x+52,y+8);}
|
||||
private void centered(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double centerX,double y)throws IOException{float w=f.getStringWidth(value)/1000*size;text(s,u,f,value,size,centerX-w/PdfUnits.mmToPt(1)/2,y);}
|
||||
private void wrappedText(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x,double y,double width,int maxLines,double lineHeight)throws IOException{
|
||||
String remaining=value==null?"—":value.strip();
|
||||
for(int lineIndex=0;lineIndex<maxLines&&!remaining.isEmpty();lineIndex++){
|
||||
String line=fitLine(f,remaining,size,PdfUnits.mmToPt(width));
|
||||
boolean truncated=line.length()<remaining.length();
|
||||
if(lineIndex==maxLines-1&&truncated)line=fit(f,remaining,size,PdfUnits.mmToPt(width));
|
||||
text(s,u,f,line,size,x,y+lineIndex*lineHeight);
|
||||
remaining=truncated?remaining.substring(line.length()).strip():"";
|
||||
}
|
||||
}
|
||||
private String fitLine(PDFont f,String value,float size,float max)throws IOException{
|
||||
if(f.getStringWidth(value)/1000*size<=max)return value;
|
||||
int end=value.length();while(end>0&&f.getStringWidth(value.substring(0,end))/1000*size>max)end--;
|
||||
int space=value.lastIndexOf(' ',end);return value.substring(0,space>0?space:end).stripTrailing();
|
||||
}
|
||||
private void centeredLines(PDPageContentStream s,PdfUnits u,PDFont f,String[] values,float size,double x1,double x2,double y)throws IOException{for(int i=0;i<values.length;i++)centeredCell(s,u,f,values[i],size,x1+.3,x2-.3,y+i*3.5);}
|
||||
private void centeredCell(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x1,double x2,double y)throws IOException{String fit=fit(f,value,size,PdfUnits.mmToPt(x2-x1));float w=f.getStringWidth(fit)/1000*size;text(s,u,f,fit,size,x1+(x2-x1-w/PdfUnits.mmToPt(1))/2,y);}
|
||||
private void right(PDPageContentStream s,PdfUnits u,PDFont f,String value,float size,double x,double y)throws IOException{float w=f.getStringWidth(value)/1000*size;text(s,u,f,value,size,x-w/PdfUnits.mmToPt(1),y);}
|
||||
|
||||
@@ -98,7 +98,7 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
return ok({ accessToken: 'client-token' })
|
||||
}
|
||||
if (method === 'GET' && requestPath === '/api/platform/finance/v1/accounts?page=1&pageSize=20&customerId=11') {
|
||||
return ok({ records: [{ customerId: '11', customerCode: 'TEN11', customerName: 'Fixture Account',
|
||||
return ok({ records: [{ customerId: '11', customerCode: '00011', customerName: 'Fixture Account',
|
||||
contactName: 'Fixture Contact', contactPhone: '13800000000', outstandingReceivable: 100,
|
||||
pendingBilling: 20, receivedInPeriod: 30, outstandingPayable: 40, status: 'ACTIVE',
|
||||
lastChangedAt: '2026-08-10T09:00:00' }] })
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
|
||||
| 目标 | 入口 |
|
||||
|---|---|
|
||||
| 仓库/地址/能力 | `controller/PlatformWarehouseController.java`、`MerchantWarehouseController.java`、`service/WarehouseApplicationService.java` |
|
||||
| 仓库/地址/能力 | `controller/PlatformWarehouseController.java`、`MerchantWarehouseController.java`、`service/WarehouseApplicationService.java`;国内 PDA 仓库目录由 `InternalWarehouseController.java` 的 `/internal/warehouses/domestic` 提供给 Firstleg |
|
||||
| FBA 目的仓公共主数据与 Admin 候选 | 权威表 `wh_warehouse` 的 `tenant_id=0 + warehouse_type=FBA` 记录;Admin 直接调用 `GET /api/platform/v1/warehouse-admin?warehouseType=FBA&status=ACTIVE&page=1&size=200`,权限 `platform:warehouse:list`,DTO `WarehouseSummaryResponse` 的 `id/warehouseCode/warehouseName/status/countryCode/primaryAddress.stateProvince` 分别作为 ID/编码/名称/状态/国家/区域 |
|
||||
| FBA 目的仓内部校验 | `InternalWarehouseController.java` 的 `GET /internal/warehouses/0/{warehouseId}/active`,使用内部服务令牌返回 `WarehouseDetailResponse`;调用方仍须校验 `warehouseType=FBA` 及 ID/编码/名称一致,不得把 `OVERSEAS` 实际收货仓或 `DOMESTIC` 中转仓当作 FBA 目的仓 |
|
||||
| 公共海外收货仓内部目录 | `controller/InternalWarehouseController.java` 的 `/internal/warehouses/shared-overseas`、`/shared-overseas/{warehouseId}`,响应 `dto/SharedOverseasWarehouseResponse.java`;只返回平台公共 `OVERSEAS + ACTIVE + RECEIVING` 仓,供海外资源服务按 warehouseId 引用 |
|
||||
|
||||
+8
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||
import com.baiyue.warehouse.dto.DomesticWarehouseOptionResponse;
|
||||
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||
import com.baiyue.warehouse.dto.SharedOverseasWarehouseResponse;
|
||||
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
||||
@@ -49,6 +50,13 @@ public class InternalWarehouseController {
|
||||
return ApiResponse.success(service.activeOverseasPartners());
|
||||
}
|
||||
|
||||
@GetMapping("/domestic")
|
||||
public ApiResponse<List<DomesticWarehouseOptionResponse>> domesticWarehouses(
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token) {
|
||||
verifyToken(token);
|
||||
return ApiResponse.success(service.activeDomesticWarehouses());
|
||||
}
|
||||
|
||||
@GetMapping("/shared-overseas")
|
||||
public ApiResponse<List<SharedOverseasWarehouseResponse>> sharedOverseasWarehouses(
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
|
||||
/** 国内 PDA 可选择的启用仓库主档。 */
|
||||
public record DomesticWarehouseOptionResponse(Long id, String warehouseCode, String warehouseName) {
|
||||
public static DomesticWarehouseOptionResponse of(WarehouseEntity warehouse) {
|
||||
return new DomesticWarehouseOptionResponse(
|
||||
warehouse.getId(), warehouse.getWarehouseCode(), warehouse.getWarehouseName());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||
import com.baiyue.warehouse.dto.DomesticWarehouseOptionResponse;
|
||||
import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||
import com.baiyue.warehouse.dto.WarehouseSummaryResponse;
|
||||
@@ -96,6 +97,14 @@ public class WarehouseApplicationService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<DomesticWarehouseOptionResponse> activeDomesticWarehouses() {
|
||||
return repository.findActiveByType(WarehouseSaveNormalizer.PUBLIC_TENANT_ID,
|
||||
WarehouseSaveNormalizer.TYPE_DOMESTIC)
|
||||
.stream()
|
||||
.map(DomesticWarehouseOptionResponse::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<SharedOverseasWarehouseResponse> activeSharedOverseasWarehouses() {
|
||||
return repository.findActiveByType(WarehouseSaveNormalizer.PUBLIC_TENANT_ID,
|
||||
WarehouseSaveNormalizer.TYPE_OVERSEAS)
|
||||
|
||||
+12
@@ -3,6 +3,7 @@ package com.baiyue.warehouse.controller;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||
import com.baiyue.warehouse.dto.DomesticWarehouseOptionResponse;
|
||||
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||
import com.baiyue.warehouse.dto.SharedOverseasWarehouseResponse;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
@@ -85,6 +86,17 @@ class InternalWarehouseControllerTest {
|
||||
verify(service).activeOverseasPartners();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listsActiveDomesticWarehouseOptionsForInternalCallers() {
|
||||
var option = new DomesticWarehouseOptionResponse(200001L, "CN-DONGGUAN-01", "东莞仓");
|
||||
when(service.activeDomesticWarehouses()).thenReturn(List.of(option));
|
||||
|
||||
var response = controller.domesticWarehouses(INTERNAL_TOKEN);
|
||||
|
||||
assertThat(response.data()).containsExactly(option);
|
||||
verify(service).activeDomesticWarehouses();
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesSharedOverseasReceivingDirectoryOnlyWithInternalToken() {
|
||||
var warehouse = new SharedOverseasWarehouseResponse(200003L, "US-LAX-01",
|
||||
|
||||
Reference in New Issue
Block a user