优化
This commit is contained in:
@@ -331,6 +331,7 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
|||||||
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
||||||
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
||||||
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
||||||
|
customer_nickname VARCHAR(32) NOT NULL COMMENT '客户昵称:非法定展示及打印使用,创建时默认取公司名称中文拼音大写首字母',
|
||||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户',
|
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已停用',
|
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用',
|
||||||
legal_name VARCHAR(128) NULL COMMENT '企业法定名称',
|
legal_name VARCHAR(128) NULL COMMENT '企业法定名称',
|
||||||
@@ -1245,15 +1246,24 @@ SET @ddl = IF(
|
|||||||
);
|
);
|
||||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @customer_nickname_ddl = (SELECT IF(
|
||||||
|
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||||
|
AND table_name='iam_tenant' AND column_name='customer_nickname'),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE iam_tenant ADD COLUMN customer_nickname VARCHAR(32) NULL COMMENT ''客户昵称:非法定展示及打印使用,创建时默认取公司名称中文拼音大写首字母'' AFTER tenant_name'));
|
||||||
|
PREPARE customer_nickname_stmt FROM @customer_nickname_ddl;
|
||||||
|
EXECUTE customer_nickname_stmt;
|
||||||
|
DEALLOCATE PREPARE customer_nickname_stmt;
|
||||||
|
|
||||||
INSERT INTO iam_tenant (
|
INSERT INTO iam_tenant (
|
||||||
id, tenant_code, tenant_name, tenant_type, status, legal_name,
|
id, tenant_code, tenant_name, customer_nickname, tenant_type, status, legal_name,
|
||||||
unified_social_credit_code, contact_name, contact_phone, contact_email,
|
unified_social_credit_code, contact_name, contact_phone, contact_email,
|
||||||
approved_at, approved_by, created_by, deleted, version
|
approved_at, approved_by, created_by, deleted, version
|
||||||
) VALUES
|
) VALUES
|
||||||
(0, 'PLATFORM', '虫虫出海平台', 'PLATFORM', 'ACTIVE', '虫虫出海平台',
|
(0, 'PLATFORM', '虫虫出海平台', '平台', 'PLATFORM', 'ACTIVE', '虫虫出海平台',
|
||||||
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0)
|
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
tenant_code = VALUES(tenant_code), tenant_name = VALUES(tenant_name), tenant_type = VALUES(tenant_type),
|
tenant_code = VALUES(tenant_code), tenant_name = VALUES(tenant_name), customer_nickname = VALUES(customer_nickname), tenant_type = VALUES(tenant_type),
|
||||||
status = 'ACTIVE', legal_name = VALUES(legal_name),
|
status = 'ACTIVE', legal_name = VALUES(legal_name),
|
||||||
unified_social_credit_code = VALUES(unified_social_credit_code),
|
unified_social_credit_code = VALUES(unified_social_credit_code),
|
||||||
contact_name = VALUES(contact_name), contact_phone = VALUES(contact_phone),
|
contact_name = VALUES(contact_name), contact_phone = VALUES(contact_phone),
|
||||||
@@ -15256,6 +15266,7 @@ 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', 'status', '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用');
|
||||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_code', '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM');
|
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_name', '客户企业名称');
|
||||||
|
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'customer_nickname', '客户昵称:非法定展示及打印使用,创建时默认取公司名称中文拼音大写首字母');
|
||||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_type', '主体类型:MERCHANT企业客户');
|
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_type', '主体类型:MERCHANT企业客户');
|
||||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'unified_social_credit_code', '统一社会信用代码');
|
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'unified_social_credit_code', '统一社会信用代码');
|
||||||
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'updated_at', '更新时间');
|
CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'updated_at', '更新时间');
|
||||||
@@ -15848,6 +15859,9 @@ BEGIN
|
|||||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||||
WHERE table_schema='baiyue_iam' AND table_name='iam_tenant'
|
WHERE table_schema='baiyue_iam' AND table_name='iam_tenant'
|
||||||
AND column_name='dimension_initialization_status') <> 1
|
AND column_name='dimension_initialization_status') <> 1
|
||||||
|
OR (SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema='baiyue_iam' AND table_name='iam_tenant'
|
||||||
|
AND column_name='customer_nickname') <> 1
|
||||||
OR (SELECT COUNT(*) FROM information_schema.tables
|
OR (SELECT COUNT(*) FROM information_schema.tables
|
||||||
WHERE table_schema='baiyue_iam'
|
WHERE table_schema='baiyue_iam'
|
||||||
AND table_name='iam_customer_dimension_initialization_task') <> 1
|
AND table_name='iam_customer_dimension_initialization_task') <> 1
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ public interface IamCustomerSnapshotClient {
|
|||||||
@RequestParam(value = "status", required = false) String status,
|
@RequestParam(value = "status", required = false) String status,
|
||||||
@RequestParam(value = "customerId", required = false) Long customerId);
|
@RequestParam(value = "customerId", required = false) Long customerId);
|
||||||
|
|
||||||
record Customer(long id, String tenantCode, String tenantName, String status,
|
record Customer(long id, String tenantCode, String tenantName, String customerNickname, String status,
|
||||||
String contactName, String contactPhone) {}
|
String contactName, String contactPhone) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,16 @@ public interface PrintPdfClient {
|
|||||||
record DropshipPalletA4PdfRequest(String templateCode, PageSpec pageSpec,
|
record DropshipPalletA4PdfRequest(String templateCode, PageSpec pageSpec,
|
||||||
String downloadFileName, List<DropshipPalletDocument> documents) {}
|
String downloadFileName, List<DropshipPalletDocument> documents) {}
|
||||||
|
|
||||||
record DropshipPalletDocument(String palletNo, String inboundCodes, int cartonCount,
|
record DropshipPalletDocument(String palletNo, String destinationWarehouse,
|
||||||
int skuCount, BigDecimal weightKg, BigDecimal volumeCbm,
|
int palletSequence, int totalPallets,
|
||||||
String barcodeContent, List<DropshipPalletSkuRow> skuRows) {}
|
String barcodeContent,
|
||||||
|
List<DropshipPalletInboundRow> inboundRows,
|
||||||
|
List<DropshipPalletSkuRow> skuRows) {}
|
||||||
|
|
||||||
record DropshipPalletSkuRow(String inboundCode, String skuCode, String cargoName,
|
record DropshipPalletInboundRow(String inboundCode, String customerNickname,
|
||||||
int cartonCount, int skuQuantity) {}
|
int cartonCount, int itemQuantity) {}
|
||||||
|
|
||||||
|
record DropshipPalletSkuRow(String skuCode, int cartonCount,
|
||||||
|
int itemQuantity, String unitsPerCarton) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,58 @@ public interface DropshipPalletizationMapper {
|
|||||||
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
|
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
|
||||||
WHERE c.pallet_id=#{palletId}""")
|
WHERE c.pallet_id=#{palletId}""")
|
||||||
PalletMetrics metrics(@Param("palletId") long palletId);
|
PalletMetrics metrics(@Param("palletId") long palletId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT c.tenant_id,c.inbound_id,o.inbound_code,
|
||||||
|
COALESCE(NULLIF(o.destination_name,''),NULLIF(o.destination_code,''),'—') destination_warehouse,
|
||||||
|
COUNT(DISTINCT c.id) carton_count,
|
||||||
|
SUM(COALESCE(q.item_quantity,c.actual_sku_quantity,0)) item_quantity
|
||||||
|
FROM fl_dropship_pallet_carton c
|
||||||
|
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
|
||||||
|
LEFT JOIN (SELECT carton_id,SUM(actual_quantity) item_quantity
|
||||||
|
FROM fl_dropship_pallet_carton_sku GROUP BY carton_id) q ON q.carton_id=c.id
|
||||||
|
WHERE c.pallet_id=#{palletId}
|
||||||
|
GROUP BY c.tenant_id,c.inbound_id,o.inbound_code,o.destination_name,o.destination_code
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.tenant_id,p.inbound_id,o.inbound_code,
|
||||||
|
COALESCE(NULLIF(o.destination_name,''),NULLIF(o.destination_code,''),'—') destination_warehouse,
|
||||||
|
0 carton_count,0 item_quantity
|
||||||
|
FROM fl_dropship_pallet p
|
||||||
|
JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
|
||||||
|
WHERE p.id=#{palletId}
|
||||||
|
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=p.id)
|
||||||
|
ORDER BY inbound_code
|
||||||
|
""")
|
||||||
|
List<PrintInboundRow> printInboundRows(@Param("palletId") long palletId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT sku_code,COUNT(DISTINCT carton_id) carton_count,SUM(item_quantity) item_quantity,
|
||||||
|
MIN(item_quantity) min_units_per_carton,MAX(item_quantity) max_units_per_carton
|
||||||
|
FROM (
|
||||||
|
SELECT c.id carton_id,s.sku_code,SUM(s.actual_quantity) item_quantity
|
||||||
|
FROM fl_dropship_pallet_carton c
|
||||||
|
JOIN fl_dropship_pallet_carton_sku s ON s.carton_id=c.id
|
||||||
|
WHERE c.pallet_id=#{palletId}
|
||||||
|
GROUP BY c.id,s.sku_code
|
||||||
|
UNION ALL
|
||||||
|
SELECT c.id carton_id,c.sku_code,c.actual_sku_quantity item_quantity
|
||||||
|
FROM fl_dropship_pallet_carton c
|
||||||
|
WHERE c.pallet_id=#{palletId}
|
||||||
|
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton_sku s WHERE s.carton_id=c.id)
|
||||||
|
) pallet_sku
|
||||||
|
GROUP BY sku_code ORDER BY sku_code
|
||||||
|
""")
|
||||||
|
List<PrintSkuRow> printSkuRows(@Param("palletId") long palletId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT (SELECT COUNT(*) FROM fl_dropship_pallet listed
|
||||||
|
WHERE listed.task_id=target.task_id AND listed.id<=target.id) sequence_no,
|
||||||
|
(SELECT COUNT(*) FROM fl_dropship_pallet listed
|
||||||
|
WHERE listed.task_id=target.task_id) total
|
||||||
|
FROM fl_dropship_pallet target
|
||||||
|
WHERE target.id=#{palletId}
|
||||||
|
""")
|
||||||
|
PalletPosition palletPosition(@Param("palletId") long palletId);
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
|
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
|
||||||
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
|
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
|
||||||
@@ -244,5 +296,8 @@ public interface DropshipPalletizationMapper {
|
|||||||
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
|
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
|
||||||
@Getter @Setter class CartonSkuRow { private Long inboundDetailId; private String skuCode,cargoName,discrepancyStatus,discrepancyRemark; private Integer expectedQuantity,actualQuantity,lineNo; }
|
@Getter @Setter class CartonSkuRow { private Long inboundDetailId; private String skuCode,cargoName,discrepancyStatus,discrepancyRemark; private Integer expectedQuantity,actualQuantity,lineNo; }
|
||||||
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
|
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
|
||||||
|
@Getter @Setter class PrintInboundRow { private Long tenantId,inboundId; private String inboundCode,destinationWarehouse; private Integer cartonCount,itemQuantity; }
|
||||||
|
@Getter @Setter class PrintSkuRow { private String skuCode; private Integer cartonCount,itemQuantity,minUnitsPerCarton,maxUnitsPerCarton; }
|
||||||
|
record PalletPosition(int sequenceNo,int total) {}
|
||||||
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
|
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,21 @@ public class IamCustomerSnapshotService {
|
|||||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "客户主数据服务暂不可用");
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "客户主数据服务暂不可用");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String customerNickname(long customerId) {
|
||||||
|
try {
|
||||||
|
var response = client.page(internalToken, 1, 2, null, null, customerId);
|
||||||
|
if (response == null || !response.successful() || response.data() == null
|
||||||
|
|| response.data().records() == null || response.data().records().size() != 1) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "客户昵称不存在,请先完善客户资料");
|
||||||
|
}
|
||||||
|
var customer = response.data().records().get(0);
|
||||||
|
if (customer.id() != customerId || !StringUtils.hasText(customer.customerNickname())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "客户昵称不存在,请先完善客户资料");
|
||||||
|
}
|
||||||
|
return customer.customerNickname().trim();
|
||||||
|
} catch (FeignException exception) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "客户主数据服务暂不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-13
@@ -5,9 +5,11 @@ import com.baiyue.common.core.api.ResultCode;
|
|||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.firstleg.client.PrintPdfClient;
|
import com.baiyue.firstleg.client.PrintPdfClient;
|
||||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||||
|
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import feign.FeignException;
|
import feign.FeignException;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -19,6 +21,9 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
||||||
|
|
||||||
/** Allocates stable carton codes and forwards immutable label facts to Print. */
|
/** Allocates stable carton codes and forwards immutable label facts to Print. */
|
||||||
@@ -34,6 +39,8 @@ public class InboundCartonLabelPrintService {
|
|||||||
private final InboundCartonLabelService labelService;
|
private final InboundCartonLabelService labelService;
|
||||||
private final PrintPdfClient printClient;
|
private final PrintPdfClient printClient;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
@Resource private DropshipPalletizationMapper palletizationMapper;
|
||||||
|
@Resource private IamCustomerSnapshotService customerSnapshots;
|
||||||
|
|
||||||
@Value("${baiyue.security.internal-service-token}")
|
@Value("${baiyue.security.internal-service-token}")
|
||||||
private String internalToken;
|
private String internalToken;
|
||||||
@@ -115,7 +122,7 @@ public class InboundCartonLabelPrintService {
|
|||||||
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
|
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一件代发 A4 托盘信息单:托盘号、Code 128 一维码和 SKU 归托明细。 */
|
/** 一件代发 A4 托盘信息单:托盘、目的仓、入仓码和 SKU 汇总。 */
|
||||||
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String inboundCode, String requestId) {
|
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String inboundCode, String requestId) {
|
||||||
return generateDropshipPallets(List.of(pallet), inboundCode, requestId);
|
return generateDropshipPallets(List.of(pallet), inboundCode, requestId);
|
||||||
}
|
}
|
||||||
@@ -125,13 +132,14 @@ public class InboundCartonLabelPrintService {
|
|||||||
if (pallets == null || pallets.isEmpty()) {
|
if (pallets == null || pallets.isEmpty()) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印");
|
throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印");
|
||||||
}
|
}
|
||||||
|
Map<Long, String> customerNicknameCache = new HashMap<>();
|
||||||
List<PrintPdfClient.DropshipPalletDocument> documents = pallets.stream()
|
List<PrintPdfClient.DropshipPalletDocument> documents = pallets.stream()
|
||||||
.map(pallet -> dropshipPalletDocument(pallet, inboundCode)).toList();
|
.map(pallet -> dropshipPalletDocument(pallet, customerNicknameCache)).toList();
|
||||||
String fileName = pallets.size() == 1
|
String fileName = pallets.size() == 1
|
||||||
? pallets.get(0).palletNo() + "-A4托盘信息单.pdf"
|
? pallets.get(0).palletNo() + "-A4托盘信息单.pdf"
|
||||||
: inboundCode + "-" + pallets.size() + "托-A4托盘信息单.pdf";
|
: inboundCode + "-" + pallets.size() + "托-A4托盘信息单.pdf";
|
||||||
PrintPdfClient.DropshipPalletA4PdfRequest request = new PrintPdfClient.DropshipPalletA4PdfRequest(
|
PrintPdfClient.DropshipPalletA4PdfRequest request = new PrintPdfClient.DropshipPalletA4PdfRequest(
|
||||||
"DROPSHIP_PALLET_A4_V1", new PrintPdfClient.PageSpec(A4_WIDTH_MM, A4_HEIGHT_MM),
|
"DROPSHIP_PALLET_A4_V2", new PrintPdfClient.PageSpec(A4_WIDTH_MM, A4_HEIGHT_MM),
|
||||||
fileName, documents);
|
fileName, documents);
|
||||||
try {
|
try {
|
||||||
return requirePdf(printClient.generateDropshipPalletA4Documents(internalToken, requestId, request));
|
return requirePdf(printClient.generateDropshipPalletA4Documents(internalToken, requestId, request));
|
||||||
@@ -140,20 +148,42 @@ public class InboundCartonLabelPrintService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PrintPdfClient.DropshipPalletDocument dropshipPalletDocument(Pallet pallet, String inboundCode) {
|
private PrintPdfClient.DropshipPalletDocument dropshipPalletDocument(
|
||||||
if (pallet == null || !StringUtils.hasText(pallet.palletNo())) {
|
Pallet pallet, Map<Long, String> customerNicknameCache) {
|
||||||
|
if (pallet == null || pallet.palletId() == null || !StringUtils.hasText(pallet.palletNo())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印");
|
throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印");
|
||||||
}
|
}
|
||||||
String inboundCodes = pallet.items().stream().map(item -> item.inboundCode())
|
List<DropshipPalletizationMapper.PrintInboundRow> inboundFacts =
|
||||||
.filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining("/"));
|
palletizationMapper.printInboundRows(pallet.palletId());
|
||||||
if (!StringUtils.hasText(inboundCodes)) inboundCodes = inboundCode;
|
if (inboundFacts == null || inboundFacts.isEmpty()) {
|
||||||
List<PrintPdfClient.DropshipPalletSkuRow> skuRows = pallet.items().stream()
|
throw new BizException(ResultCode.CONFLICT, "托盘未关联入仓单,不能打印");
|
||||||
.map(item -> new PrintPdfClient.DropshipPalletSkuRow(item.inboundCode(), item.skuCode(),
|
}
|
||||||
item.cargoName(), number(item.cartonCount()), number(item.skuQuantity())))
|
List<PrintPdfClient.DropshipPalletInboundRow> inboundRows = inboundFacts.stream()
|
||||||
|
.map(row -> new PrintPdfClient.DropshipPalletInboundRow(
|
||||||
|
row.getInboundCode(),
|
||||||
|
customerNicknameCache.computeIfAbsent(row.getTenantId(), customerSnapshots::customerNickname),
|
||||||
|
number(row.getCartonCount()), number(row.getItemQuantity())))
|
||||||
.toList();
|
.toList();
|
||||||
|
String destinationWarehouse = inboundFacts.stream()
|
||||||
|
.map(DropshipPalletizationMapper.PrintInboundRow::getDestinationWarehouse)
|
||||||
|
.filter(StringUtils::hasText).distinct().collect(Collectors.joining(" / "));
|
||||||
|
if (!StringUtils.hasText(destinationWarehouse)) destinationWarehouse = "—";
|
||||||
|
List<PrintPdfClient.DropshipPalletSkuRow> skuRows = palletizationMapper.printSkuRows(pallet.palletId())
|
||||||
|
.stream().map(row -> new PrintPdfClient.DropshipPalletSkuRow(
|
||||||
|
row.getSkuCode(), number(row.getCartonCount()), number(row.getItemQuantity()),
|
||||||
|
unitsPerCarton(row))).toList();
|
||||||
|
DropshipPalletizationMapper.PalletPosition position =
|
||||||
|
palletizationMapper.palletPosition(pallet.palletId());
|
||||||
return new PrintPdfClient.DropshipPalletDocument(
|
return new PrintPdfClient.DropshipPalletDocument(
|
||||||
pallet.palletNo(), inboundCodes, number(pallet.cartonCount()), number(pallet.skuCount()),
|
pallet.palletNo(), destinationWarehouse,
|
||||||
pallet.weightKg(), pallet.volumeCbm(), pallet.palletNo(), skuRows);
|
position == null ? 1 : position.sequenceNo(), position == null ? 1 : position.total(),
|
||||||
|
pallet.palletNo(), inboundRows, skuRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unitsPerCarton(DropshipPalletizationMapper.PrintSkuRow row) {
|
||||||
|
int min = number(row.getMinUnitsPerCarton());
|
||||||
|
int max = number(row.getMaxUnitsPerCarton());
|
||||||
|
return min == max ? Integer.toString(min) : min + "-" + max;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int number(Integer value) {
|
private int number(Integer value) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ paths:
|
|||||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/print:
|
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/print:
|
||||||
get:
|
get:
|
||||||
summary: PDA 获取预生成托盘码的 A4 托盘信息单
|
summary: PDA 获取预生成托盘码的 A4 托盘信息单
|
||||||
description: 测量审核通过并生成托盘码后即可获取;每个托盘固定一页 A4,包含托盘号及其 Code 128 一维码、入仓码、箱数以及已归托 SKU、商品名称和实点数量,不展示作业状态。空托显示暂无已归托 SKU。仅当前仓库、当前操作员可访问。
|
description: 测量审核通过并生成托盘码后即可获取;每个托盘固定一页 A4,包含托盘号及其 Code 128 一维码、目的仓、托盘序号、入仓码/客户昵称/箱数/件数汇总以及 SKU/箱数/件数/每箱件数汇总,不展示作业状态、重量或体积。空托显示暂无已归托 SKU。仅当前仓库、当前操作员可访问。
|
||||||
x-required-authority: platform:firstleg:pda:view
|
x-required-authority: platform:firstleg:pda:view
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/TaskId'}
|
- {$ref: '#/components/parameters/TaskId'}
|
||||||
@@ -91,7 +91,7 @@ paths:
|
|||||||
responses: {'200': {description: OK}}
|
responses: {'200': {description: OK}}
|
||||||
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/{palletId}/print:
|
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/{palletId}/print:
|
||||||
get:
|
get:
|
||||||
summary: 获取包含托盘码、入仓码和 SKU 实收汇总的 A4 托盘信息单
|
summary: 获取包含托盘码、目的仓、入仓码和 SKU 汇总的 A4 托盘信息单
|
||||||
x-required-authority: platform:firstleg:inbound:list
|
x-required-authority: platform:firstleg:inbound:list
|
||||||
responses: {'200': {description: PDF}}
|
responses: {'200': {description: PDF}}
|
||||||
components:
|
components:
|
||||||
|
|||||||
+12
-2
@@ -32,7 +32,7 @@ class IamCustomerSnapshotServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void resolvesExactlyOneActiveCustomerAndUsesServerName() {
|
void resolvesExactlyOneActiveCustomerAndUsesServerName() {
|
||||||
var customer = new IamCustomerSnapshotClient.Customer(
|
var customer = new IamCustomerSnapshotClient.Customer(
|
||||||
7L, "C-7", "Canonical Customer", "ACTIVE", null, null);
|
7L, "C-7", "Canonical Customer", "Short Name", "ACTIVE", null, null);
|
||||||
when(client.page("internal-token", 1, 2, null, "ACTIVE", 7L))
|
when(client.page("internal-token", 1, 2, null, "ACTIVE", 7L))
|
||||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(customer), 1, 2, 1, 1)));
|
.thenReturn(ApiResponse.success(new PageResult<>(List.of(customer), 1, 2, 1, 1)));
|
||||||
|
|
||||||
@@ -50,10 +50,20 @@ class IamCustomerSnapshotServiceTest {
|
|||||||
.isInstanceOf(BizException.class).hasMessageContaining("不存在或未启用");
|
.isInstanceOf(BizException.class).hasMessageContaining("不存在或未启用");
|
||||||
|
|
||||||
var customer = new IamCustomerSnapshotClient.Customer(
|
var customer = new IamCustomerSnapshotClient.Customer(
|
||||||
7L, "C-7", "Canonical Customer", "ACTIVE", null, null);
|
7L, "C-7", "Canonical Customer", "Short Name", "ACTIVE", null, null);
|
||||||
when(client.page("internal-token", 1, 2, null, "ACTIVE", 7L))
|
when(client.page("internal-token", 1, 2, null, "ACTIVE", 7L))
|
||||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(customer), 1, 2, 1, 1)));
|
.thenReturn(ApiResponse.success(new PageResult<>(List.of(customer), 1, 2, 1, 1)));
|
||||||
assertThatThrownBy(() -> service.active(7L, "Forged Customer"))
|
assertThatThrownBy(() -> service.active(7L, "Forged Customer"))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
|
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesCustomerNicknameForPalletDocument() {
|
||||||
|
var customer = new IamCustomerSnapshotClient.Customer(
|
||||||
|
7L, "C-7", "Canonical Customer", " Short Name ", "ACTIVE", null, null);
|
||||||
|
when(client.page("internal-token", 1, 2, null, null, 7L))
|
||||||
|
.thenReturn(ApiResponse.success(new PageResult<>(List.of(customer), 1, 2, 1, 1)));
|
||||||
|
|
||||||
|
assertThat(service.customerNickname(7L)).isEqualTo("Short Name");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-4
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.client.PrintPdfClient;
|
|||||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
||||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
|
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
|
||||||
|
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -29,17 +30,22 @@ import static org.mockito.Mockito.when;
|
|||||||
class InboundCartonLabelPrintServiceTest {
|
class InboundCartonLabelPrintServiceTest {
|
||||||
private final InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
|
private final InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
|
||||||
private final PrintPdfClient print = mock(PrintPdfClient.class);
|
private final PrintPdfClient print = mock(PrintPdfClient.class);
|
||||||
|
private final DropshipPalletizationMapper palletization = mock(DropshipPalletizationMapper.class);
|
||||||
|
private final IamCustomerSnapshotService customerSnapshots = mock(IamCustomerSnapshotService.class);
|
||||||
private InboundCartonLabelPrintService service;
|
private InboundCartonLabelPrintService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
service = new InboundCartonLabelPrintService(labels, print, new ObjectMapper());
|
service = new InboundCartonLabelPrintService(labels, print, new ObjectMapper());
|
||||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||||
|
ReflectionTestUtils.setField(service, "palletizationMapper", palletization);
|
||||||
|
ReflectionTestUtils.setField(service, "customerSnapshots", customerSnapshots);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void dropshipPalletLabelIsAvailableBeforeSeal() {
|
void dropshipPalletLabelIsAvailableBeforeSeal() {
|
||||||
Pallet open = dropshipPallet("OPEN");
|
Pallet open = dropshipPallet("OPEN");
|
||||||
|
stubPalletFacts(90001L, 1, 2);
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||||
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
|
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
|
||||||
@@ -54,17 +60,25 @@ class InboundCartonLabelPrintServiceTest {
|
|||||||
ArgumentCaptor.forClass(PrintPdfClient.DropshipPalletA4PdfRequest.class);
|
ArgumentCaptor.forClass(PrintPdfClient.DropshipPalletA4PdfRequest.class);
|
||||||
verify(print).generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.eq("internal-token"),
|
verify(print).generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
org.mockito.ArgumentMatchers.eq("request-open"), request.capture());
|
org.mockito.ArgumentMatchers.eq("request-open"), request.capture());
|
||||||
assertThat(request.getValue().templateCode()).isEqualTo("DROPSHIP_PALLET_A4_V1");
|
assertThat(request.getValue().templateCode()).isEqualTo("DROPSHIP_PALLET_A4_V2");
|
||||||
assertThat(request.getValue().pageSpec().widthMm()).isEqualByComparingTo("210");
|
assertThat(request.getValue().pageSpec().widthMm()).isEqualByComparingTo("210");
|
||||||
assertThat(request.getValue().pageSpec().heightMm()).isEqualByComparingTo("297");
|
assertThat(request.getValue().pageSpec().heightMm()).isEqualByComparingTo("297");
|
||||||
assertThat(request.getValue().documents()).singleElement().satisfies(document -> {
|
assertThat(request.getValue().documents()).singleElement().satisfies(document -> {
|
||||||
assertThat(document.palletNo()).isEqualTo("DP-260911-0001");
|
assertThat(document.palletNo()).isEqualTo("DP-260911-0001");
|
||||||
assertThat(document.inboundCodes()).isEqualTo("PCHK2608040174");
|
assertThat(document.destinationWarehouse()).isEqualTo("纽约 N.JDF");
|
||||||
|
assertThat(document.palletSequence()).isEqualTo(1);
|
||||||
|
assertThat(document.totalPallets()).isEqualTo(2);
|
||||||
|
assertThat(document.inboundRows()).singleElement().satisfies(row -> {
|
||||||
|
assertThat(row.inboundCode()).isEqualTo("PCHK2608040174");
|
||||||
|
assertThat(row.customerNickname()).isEqualTo("小虫科技");
|
||||||
|
assertThat(row.cartonCount()).isEqualTo(1);
|
||||||
|
assertThat(row.itemQuantity()).isEqualTo(19);
|
||||||
|
});
|
||||||
assertThat(document.skuRows()).singleElement().satisfies(row -> {
|
assertThat(document.skuRows()).singleElement().satisfies(row -> {
|
||||||
assertThat(row.skuCode()).isEqualTo("SKU-1");
|
assertThat(row.skuCode()).isEqualTo("SKU-1");
|
||||||
assertThat(row.cargoName()).isEqualTo("音箱");
|
|
||||||
assertThat(row.cartonCount()).isEqualTo(1);
|
assertThat(row.cartonCount()).isEqualTo(1);
|
||||||
assertThat(row.skuQuantity()).isEqualTo(19);
|
assertThat(row.itemQuantity()).isEqualTo(19);
|
||||||
|
assertThat(row.unitsPerCarton()).isEqualTo("19");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,6 +88,8 @@ class InboundCartonLabelPrintServiceTest {
|
|||||||
Pallet first = dropshipPallet("OPEN");
|
Pallet first = dropshipPallet("OPEN");
|
||||||
Pallet second = new Pallet(90002L, "DP-260911-0002", "OPEN", 0, 0, 0,
|
Pallet second = new Pallet(90002L, "DP-260911-0002", "OPEN", 0, 0, 0,
|
||||||
BigDecimal.ZERO, BigDecimal.ZERO, 1, List.of(), List.of(), null);
|
BigDecimal.ZERO, BigDecimal.ZERO, 1, List.of(), List.of(), null);
|
||||||
|
stubPalletFacts(90001L, 1, 2);
|
||||||
|
stubPalletFacts(90002L, 2, 2);
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||||
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
|
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
|
||||||
@@ -225,4 +241,27 @@ class InboundCartonLabelPrintServiceTest {
|
|||||||
return new Pallet(90001L, "DP-260911-0001", status, 1, 1, 0,
|
return new Pallet(90001L, "DP-260911-0001", status, 1, 1, 0,
|
||||||
BigDecimal.ONE, BigDecimal.ONE, 1, List.of(item), List.of(), null);
|
BigDecimal.ONE, BigDecimal.ONE, 1, List.of(item), List.of(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void stubPalletFacts(long palletId, int sequence, int total) {
|
||||||
|
DropshipPalletizationMapper.PrintInboundRow inbound =
|
||||||
|
new DropshipPalletizationMapper.PrintInboundRow();
|
||||||
|
inbound.setTenantId(10001L);
|
||||||
|
inbound.setInboundId(70001L);
|
||||||
|
inbound.setInboundCode("PCHK2608040174");
|
||||||
|
inbound.setDestinationWarehouse("纽约 N.JDF");
|
||||||
|
inbound.setCartonCount(palletId == 90001L ? 1 : 0);
|
||||||
|
inbound.setItemQuantity(palletId == 90001L ? 19 : 0);
|
||||||
|
when(palletization.printInboundRows(palletId)).thenReturn(List.of(inbound));
|
||||||
|
DropshipPalletizationMapper.PrintSkuRow sku = new DropshipPalletizationMapper.PrintSkuRow();
|
||||||
|
sku.setSkuCode("SKU-1");
|
||||||
|
sku.setCartonCount(1);
|
||||||
|
sku.setItemQuantity(19);
|
||||||
|
sku.setMinUnitsPerCarton(19);
|
||||||
|
sku.setMaxUnitsPerCarton(19);
|
||||||
|
when(palletization.printSkuRows(palletId))
|
||||||
|
.thenReturn(palletId == 90001L ? List.of(sku) : List.of());
|
||||||
|
when(palletization.palletPosition(palletId))
|
||||||
|
.thenReturn(new DropshipPalletizationMapper.PalletPosition(sequence, total));
|
||||||
|
when(customerSnapshots.customerNickname(10001L)).thenReturn("小虫科技");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>
|
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>
|
||||||
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
|
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
|
||||||
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
|
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
|
||||||
|
<dependency><groupId>com.belerweb</groupId><artifactId>pinyin4j</artifactId><version>2.5.1</version></dependency>
|
||||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
|
||||||
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>
|
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>
|
||||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
|
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public final class CustomerExportDtos {
|
|||||||
public record CustomerExportBatch(List<CustomerExportRow> records, Long nextCursor, boolean hasMore) { }
|
public record CustomerExportBatch(List<CustomerExportRow> records, Long nextCursor, boolean hasMore) { }
|
||||||
|
|
||||||
public record CustomerExportRow(
|
public record CustomerExportRow(
|
||||||
String id, String tenantCode, String tenantName, String legalName,
|
String id, String tenantCode, String tenantName, String customerNickname, String legalName,
|
||||||
String unifiedSocialCreditCode, String enterpriseType, LocalDate establishedDate,
|
String unifiedSocialCreditCode, String enterpriseType, LocalDate establishedDate,
|
||||||
BigDecimal registeredCapital, String registeredCapitalCurrency, String registeredAddress,
|
BigDecimal registeredCapital, String registeredCapitalCurrency, String registeredAddress,
|
||||||
String contactName, String contactPhone, String contactEmail, String contactWechat,
|
String contactName, String contactPhone, String contactEmail, String contactWechat,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public final class TenantDtos {
|
|||||||
@Schema(name = "TenantRegisterRequest", description = "Enterprise tenant registration / 企业租户注册资料")
|
@Schema(name = "TenantRegisterRequest", description = "Enterprise tenant registration / 企业租户注册资料")
|
||||||
public record RegisterRequest(
|
public record RegisterRequest(
|
||||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||||
|
@Schema(description = "Customer nickname; defaults to uppercase pinyin initials of Chinese characters in tenantName / 客户昵称,不填时默认取公司名称各中文字符的拼音大写首字母", example = "SZWL") @Size(max = 32) String customerNickname,
|
||||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||||
@Schema(description = "Legal representative (optional) / 法定代表人(选填)", example = "张三") @Size(max = 64) String legalRepresentative,
|
@Schema(description = "Legal representative (optional) / 法定代表人(选填)", example = "张三") @Size(max = 64) String legalRepresentative,
|
||||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||||
@@ -57,6 +58,7 @@ public final class TenantDtos {
|
|||||||
@Schema(name = "PlatformTenantUpdateRequest", description = "Platform maintained merchant tenant profile")
|
@Schema(name = "PlatformTenantUpdateRequest", description = "Platform maintained merchant tenant profile")
|
||||||
public record UpdateTenantRequest(
|
public record UpdateTenantRequest(
|
||||||
@NotBlank String tenantName,
|
@NotBlank String tenantName,
|
||||||
|
@Schema(description = "Customer nickname used by non-legal displays and printing / 非法定展示及打印使用的客户昵称") @NotBlank @Size(max = 32) String customerNickname,
|
||||||
@NotBlank String legalName,
|
@NotBlank String legalName,
|
||||||
@Size(max = 64) String legalRepresentative,
|
@Size(max = 64) String legalRepresentative,
|
||||||
@NotBlank String contactName,
|
@NotBlank String contactName,
|
||||||
@@ -134,7 +136,7 @@ public final class TenantDtos {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
|
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
|
||||||
public record TenantResponse(Long id, String tenantCode, String tenantName, String status,
|
public record TenantResponse(Long id, String tenantCode, String tenantName, String customerNickname, String status,
|
||||||
String dimensionInitializationStatus,
|
String dimensionInitializationStatus,
|
||||||
String legalName, String legalRepresentative, String unifiedSocialCreditCode, String contactName,
|
String legalName, String legalRepresentative, String unifiedSocialCreditCode, String contactName,
|
||||||
String contactPhone, String contactEmail, String contactWechat,
|
String contactPhone, String contactEmail, String contactWechat,
|
||||||
@@ -144,6 +146,6 @@ public final class TenantDtos {
|
|||||||
String internalRemark, LocalDateTime createdAt, LocalDateTime approvedAt) { }
|
String internalRemark, LocalDateTime createdAt, LocalDateTime approvedAt) { }
|
||||||
|
|
||||||
/** Full customer contact summary for trusted internal finance read models. */
|
/** Full customer contact summary for trusted internal finance read models. */
|
||||||
public record FinanceCustomerSummary(Long id, String tenantCode, String tenantName, String status,
|
public record FinanceCustomerSummary(Long id, String tenantCode, String tenantName, String customerNickname, String status,
|
||||||
String contactName, String contactPhone) { }
|
String contactName, String contactPhone) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class IamTenant extends BaseEntity {
|
|||||||
@TableId private Long id;
|
@TableId private Long id;
|
||||||
private String tenantCode;
|
private String tenantCode;
|
||||||
private String tenantName;
|
private String tenantName;
|
||||||
|
private String customerNickname;
|
||||||
private String tenantType;
|
private String tenantType;
|
||||||
private String status;
|
private String status;
|
||||||
private String dimensionInitializationStatus;
|
private String dimensionInitializationStatus;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class CustomerListExportProviderService {
|
|||||||
column("id", "客户ID", ExportColumnType.STRING, 20),
|
column("id", "客户ID", ExportColumnType.STRING, 20),
|
||||||
column("tenantCode", "客户编码", ExportColumnType.STRING, 18),
|
column("tenantCode", "客户编码", ExportColumnType.STRING, 18),
|
||||||
column("tenantName", "公司名称", ExportColumnType.STRING, 28),
|
column("tenantName", "公司名称", ExportColumnType.STRING, 28),
|
||||||
|
column("customerNickname", "客户昵称", ExportColumnType.STRING, 14),
|
||||||
column("legalName", "营业执照企业名称", ExportColumnType.STRING, 28),
|
column("legalName", "营业执照企业名称", ExportColumnType.STRING, 28),
|
||||||
column("unifiedSocialCreditCode", "统一社会信用代码", ExportColumnType.STRING, 24),
|
column("unifiedSocialCreditCode", "统一社会信用代码", ExportColumnType.STRING, 24),
|
||||||
column("enterpriseType", "企业类型", ExportColumnType.STRING, 16),
|
column("enterpriseType", "企业类型", ExportColumnType.STRING, 16),
|
||||||
@@ -177,6 +178,7 @@ public class CustomerListExportProviderService {
|
|||||||
row.put("id", String.valueOf(tenant.getId()));
|
row.put("id", String.valueOf(tenant.getId()));
|
||||||
row.put("tenantCode", tenant.getTenantCode());
|
row.put("tenantCode", tenant.getTenantCode());
|
||||||
row.put("tenantName", tenant.getTenantName());
|
row.put("tenantName", tenant.getTenantName());
|
||||||
|
row.put("customerNickname", tenant.getCustomerNickname());
|
||||||
row.put("legalName", tenant.getLegalName());
|
row.put("legalName", tenant.getLegalName());
|
||||||
row.put("unifiedSocialCreditCode", tenant.getUnifiedSocialCreditCode());
|
row.put("unifiedSocialCreditCode", tenant.getUnifiedSocialCreditCode());
|
||||||
row.put("enterpriseType", tenant.getEnterpriseType());
|
row.put("enterpriseType", tenant.getEnterpriseType());
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public class MerchantOnboardingService {
|
|||||||
tenant.setId(tenantId);
|
tenant.setId(tenantId);
|
||||||
tenant.setTenantCode(customerCodes.nextCode());
|
tenant.setTenantCode(customerCodes.nextCode());
|
||||||
tenant.setTenantName(request.companyName());
|
tenant.setTenantName(request.companyName());
|
||||||
|
tenant.setCustomerNickname(TenantService.resolveCustomerNickname(null, request.companyName()));
|
||||||
tenant.setTenantType("MERCHANT");
|
tenant.setTenantType("MERCHANT");
|
||||||
tenant.setStatus("PROFILE_REQUIRED");
|
tenant.setStatus("PROFILE_REQUIRED");
|
||||||
tenant.setDimensionInitializationStatus("PENDING");
|
tenant.setDimensionInitializationStatus("PENDING");
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import feign.FeignException;
|
import feign.FeignException;
|
||||||
|
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -170,6 +171,7 @@ public class TenantService {
|
|||||||
tenant.setId(IdWorker.getId());
|
tenant.setId(IdWorker.getId());
|
||||||
tenant.setTenantCode(customerCodes.nextCode());
|
tenant.setTenantCode(customerCodes.nextCode());
|
||||||
tenant.setTenantName(request.tenantName());
|
tenant.setTenantName(request.tenantName());
|
||||||
|
tenant.setCustomerNickname(resolveCustomerNickname(request.customerNickname(), request.tenantName()));
|
||||||
tenant.setTenantType("MERCHANT");
|
tenant.setTenantType("MERCHANT");
|
||||||
tenant.setStatus("PROFILE_REQUIRED");
|
tenant.setStatus("PROFILE_REQUIRED");
|
||||||
tenant.setDimensionInitializationStatus("PENDING");
|
tenant.setDimensionInitializationStatus("PENDING");
|
||||||
@@ -222,7 +224,7 @@ public class TenantService {
|
|||||||
public PageResult<FinanceCustomerSummary> pageFinanceCustomers(long page, long size, String keyword, String status, Long customerId) {
|
public PageResult<FinanceCustomerSummary> pageFinanceCustomers(long page, long size, String keyword, String status, Long customerId) {
|
||||||
PageResult<IamTenant> source = repository.pageFinanceCustomers(page, size, keyword, status, customerId);
|
PageResult<IamTenant> source = repository.pageFinanceCustomers(page, size, keyword, status, customerId);
|
||||||
return new PageResult<>(source.records().stream().map(tenant -> new FinanceCustomerSummary(
|
return new PageResult<>(source.records().stream().map(tenant -> new FinanceCustomerSummary(
|
||||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getCustomerNickname(), tenant.getStatus(),
|
||||||
tenant.getContactName(), tenant.getContactPhone())).toList(),
|
tenant.getContactName(), tenant.getContactPhone())).toList(),
|
||||||
source.page(), source.size(), source.total(), source.pages());
|
source.page(), source.size(), source.total(), source.pages());
|
||||||
}
|
}
|
||||||
@@ -239,7 +241,7 @@ public class TenantService {
|
|||||||
|
|
||||||
private CustomerExportRow exportRow(IamTenant tenant) {
|
private CustomerExportRow exportRow(IamTenant tenant) {
|
||||||
return new CustomerExportRow(String.valueOf(tenant.getId()), tenant.getTenantCode(),
|
return new CustomerExportRow(String.valueOf(tenant.getId()), tenant.getTenantCode(),
|
||||||
tenant.getTenantName(), tenant.getLegalName(), tenant.getUnifiedSocialCreditCode(),
|
tenant.getTenantName(), tenant.getCustomerNickname(), tenant.getLegalName(), tenant.getUnifiedSocialCreditCode(),
|
||||||
tenant.getEnterpriseType(), tenant.getEstablishedDate(), tenant.getRegisteredCapital(),
|
tenant.getEnterpriseType(), tenant.getEstablishedDate(), tenant.getRegisteredCapital(),
|
||||||
tenant.getRegisteredCapitalCurrency(), tenant.getRegisteredAddress(), tenant.getContactName(),
|
tenant.getRegisteredCapitalCurrency(), tenant.getRegisteredAddress(), tenant.getContactName(),
|
||||||
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
||||||
@@ -390,6 +392,7 @@ public class TenantService {
|
|||||||
ensureNotArchived(tenant);
|
ensureNotArchived(tenant);
|
||||||
String contactPhone = normalizeContactPhone(request.contactPhone());
|
String contactPhone = normalizeContactPhone(request.contactPhone());
|
||||||
tenant.setTenantName(request.tenantName());
|
tenant.setTenantName(request.tenantName());
|
||||||
|
tenant.setCustomerNickname(requireCustomerNickname(request.customerNickname()));
|
||||||
tenant.setLegalName(request.legalName());
|
tenant.setLegalName(request.legalName());
|
||||||
tenant.setLegalRepresentative(normalizeOptional(request.legalRepresentative()));
|
tenant.setLegalRepresentative(normalizeOptional(request.legalRepresentative()));
|
||||||
tenant.setContactName(request.contactName());
|
tenant.setContactName(request.contactName());
|
||||||
@@ -489,6 +492,33 @@ public class TenantService {
|
|||||||
return value == null || value.isBlank() ? null : value.trim();
|
return value == null || value.isBlank() ? null : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String resolveCustomerNickname(String nickname, String tenantName) {
|
||||||
|
if (nickname != null && !nickname.isBlank()) return nickname.trim();
|
||||||
|
String normalizedName = tenantName == null ? "" : tenantName.trim();
|
||||||
|
if (normalizedName.isEmpty()) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "公司名称不能为空");
|
||||||
|
}
|
||||||
|
StringBuilder initials = new StringBuilder();
|
||||||
|
normalizedName.codePoints()
|
||||||
|
.filter(codePoint -> Character.UnicodeScript.of(codePoint) == Character.UnicodeScript.HAN)
|
||||||
|
.filter(Character::isBmpCodePoint)
|
||||||
|
.forEach(codePoint -> {
|
||||||
|
String[] pinyin = PinyinHelper.toHanyuPinyinStringArray((char) codePoint);
|
||||||
|
if (pinyin != null && pinyin.length > 0 && !pinyin[0].isBlank()) {
|
||||||
|
initials.append(Character.toUpperCase(pinyin[0].charAt(0)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!initials.isEmpty()) return initials.substring(0, Math.min(initials.length(), 32));
|
||||||
|
return new String(Character.toChars(normalizedName.codePointAt(0))).toUpperCase(java.util.Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireCustomerNickname(String nickname) {
|
||||||
|
if (nickname == null || nickname.isBlank()) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "客户昵称不能为空");
|
||||||
|
}
|
||||||
|
return nickname.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureAdministratorPhoneAvailable(String phone) {
|
private void ensureAdministratorPhoneAvailable(String phone) {
|
||||||
if (!administratorPhoneAvailable(phone)) {
|
if (!administratorPhoneAvailable(phone)) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "管理员登录手机号已被其他账号使用");
|
throw new BizException(ResultCode.CONFLICT, "管理员登录手机号已被其他账号使用");
|
||||||
@@ -522,7 +552,7 @@ public class TenantService {
|
|||||||
|
|
||||||
private TenantResponse response(IamTenant tenant) {
|
private TenantResponse response(IamTenant tenant) {
|
||||||
return new TenantResponse(
|
return new TenantResponse(
|
||||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getCustomerNickname(), tenant.getStatus(),
|
||||||
tenant.getDimensionInitializationStatus(),
|
tenant.getDimensionInitializationStatus(),
|
||||||
tenant.getLegalName(), tenant.getLegalRepresentative(), tenant.getUnifiedSocialCreditCode(), tenant.getContactName(),
|
tenant.getLegalName(), tenant.getLegalRepresentative(), tenant.getUnifiedSocialCreditCode(), tenant.getContactName(),
|
||||||
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
||||||
@@ -545,7 +575,7 @@ public class TenantService {
|
|||||||
|
|
||||||
private TenantResponse summaryResponse(IamTenant tenant) {
|
private TenantResponse summaryResponse(IamTenant tenant) {
|
||||||
TenantResponse response = response(tenant);
|
TenantResponse response = response(tenant);
|
||||||
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.status(),
|
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.customerNickname(), response.status(),
|
||||||
response.dimensionInitializationStatus(),
|
response.dimensionInitializationStatus(),
|
||||||
response.legalName(), response.legalRepresentative(), response.unifiedSocialCreditCode(), response.contactName(),
|
response.legalName(), response.legalRepresentative(), response.unifiedSocialCreditCode(), response.contactName(),
|
||||||
maskPhone(response.contactPhone()), maskEmail(response.contactEmail()), response.contactWechat(),
|
maskPhone(response.contactPhone()), maskEmail(response.contactEmail()), response.contactWechat(),
|
||||||
@@ -556,7 +586,7 @@ public class TenantService {
|
|||||||
|
|
||||||
private TenantResponse merchantVisibleResponse(IamTenant tenant) {
|
private TenantResponse merchantVisibleResponse(IamTenant tenant) {
|
||||||
TenantResponse response = response(tenant);
|
TenantResponse response = response(tenant);
|
||||||
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.status(),
|
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.customerNickname(), response.status(),
|
||||||
response.dimensionInitializationStatus(),
|
response.dimensionInitializationStatus(),
|
||||||
response.legalName(), response.legalRepresentative(), response.unifiedSocialCreditCode(), response.contactName(),
|
response.legalName(), response.legalRepresentative(), response.unifiedSocialCreditCode(), response.contactName(),
|
||||||
response.contactPhone(), response.contactEmail(), response.contactWechat(), response.servicePackage(), response.salesOwnerName(),
|
response.contactPhone(), response.contactEmail(), response.contactWechat(), response.servicePackage(), response.salesOwnerName(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
|||||||
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
||||||
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
||||||
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
||||||
|
customer_nickname VARCHAR(32) NOT NULL COMMENT '客户昵称:非法定展示及打印使用,创建时默认取公司名称中文拼音大写首字母',
|
||||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户',
|
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已归档',
|
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用、ARCHIVED已归档',
|
||||||
dimension_initialization_status VARCHAR(16) NOT NULL DEFAULT 'READY' COMMENT '默认尺寸初始化状态:PENDING待处理、READY已就绪、FAILED失败',
|
dimension_initialization_status VARCHAR(16) NOT NULL DEFAULT 'READY' COMMENT '默认尺寸初始化状态:PENDING待处理、READY已就绪、FAILED失败',
|
||||||
|
|||||||
@@ -1306,11 +1306,12 @@ components:
|
|||||||
pages: {type: integer, minimum: 0}
|
pages: {type: integer, minimum: 0}
|
||||||
TenantResponse:
|
TenantResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [id, tenantCode, tenantName, status, dimensionInitializationStatus, servicePackage]
|
required: [id, tenantCode, tenantName, customerNickname, status, dimensionInitializationStatus, servicePackage]
|
||||||
properties:
|
properties:
|
||||||
id: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
id: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
||||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||||
tenantName: {type: string}
|
tenantName: {type: string}
|
||||||
|
customerNickname: {type: string, maxLength: 32, description: 客户昵称;非法定展示及打印使用}
|
||||||
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}
|
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}
|
||||||
dimensionInitializationStatus: {type: string, enum: [PENDING, READY, FAILED], description: 客户默认尺寸初始化状态;未 READY 时依赖该配置的业务失败关闭}
|
dimensionInitializationStatus: {type: string, enum: [PENDING, READY, FAILED], description: 客户默认尺寸初始化状态;未 READY 时依赖该配置的业务失败关闭}
|
||||||
legalName: {type: string}
|
legalName: {type: string}
|
||||||
@@ -1344,11 +1345,12 @@ components:
|
|||||||
updatedAt: {type: string, format: date-time}
|
updatedAt: {type: string, format: date-time}
|
||||||
FinanceCustomerSummary:
|
FinanceCustomerSummary:
|
||||||
type: object
|
type: object
|
||||||
required: [id, tenantCode, tenantName, status]
|
required: [id, tenantCode, tenantName, customerNickname, status]
|
||||||
properties:
|
properties:
|
||||||
id: {type: string, pattern: '^\d+$'}
|
id: {type: string, pattern: '^\d+$'}
|
||||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||||
tenantName: {type: string}
|
tenantName: {type: string}
|
||||||
|
customerNickname: {type: string, maxLength: 32, description: 非法定展示及打印使用的客户昵称}
|
||||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||||
contactName: {type: string, nullable: true}
|
contactName: {type: string, nullable: true}
|
||||||
contactPhone: {type: string, nullable: true}
|
contactPhone: {type: string, nullable: true}
|
||||||
@@ -1459,6 +1461,7 @@ components:
|
|||||||
required: [tenantName, legalName, contactName, contactPhone, servicePackage]
|
required: [tenantName, legalName, contactName, contactPhone, servicePackage]
|
||||||
properties:
|
properties:
|
||||||
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
|
customerNickname: {type: string, maxLength: 32, example: SZWL, description: 选填;不填时默认取公司名称各中文字符的拼音大写首字母,没有中文时取首个非空字符并转大写}
|
||||||
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
legalRepresentative: {type: string, maxLength: 64, nullable: true, example: 张三, description: 选填}
|
legalRepresentative: {type: string, maxLength: 64, nullable: true, example: 张三, description: 选填}
|
||||||
contactName: {type: string, example: 李四}
|
contactName: {type: string, example: 李四}
|
||||||
@@ -1486,9 +1489,10 @@ components:
|
|||||||
contactEmail: {type: string, format: email}
|
contactEmail: {type: string, format: email}
|
||||||
UpdateTenantRequest:
|
UpdateTenantRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [tenantName, legalName, contactName, contactPhone]
|
required: [tenantName, customerNickname, legalName, contactName, contactPhone]
|
||||||
properties:
|
properties:
|
||||||
tenantName: {type: string}
|
tenantName: {type: string}
|
||||||
|
customerNickname: {type: string, maxLength: 32, description: 客户昵称;非法定展示及打印使用}
|
||||||
legalName: {type: string}
|
legalName: {type: string}
|
||||||
legalRepresentative: {type: string, maxLength: 64, nullable: true, description: 选填}
|
legalRepresentative: {type: string, maxLength: 64, nullable: true, description: 选填}
|
||||||
contactName: {type: string}
|
contactName: {type: string}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ class CustomerProfileMigrationContractTest {
|
|||||||
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
|
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
assertThat(sql)
|
assertThat(sql)
|
||||||
|
.contains("customer_nickname VARCHAR(32)")
|
||||||
|
.contains("ALTER TABLE iam_tenant ADD COLUMN customer_nickname VARCHAR(32)")
|
||||||
.contains("ALTER TABLE iam_tenant MODIFY COLUMN status")
|
.contains("ALTER TABLE iam_tenant MODIFY COLUMN status")
|
||||||
.contains("DEFAULT 'PROFILE_REQUIRED'")
|
.contains("DEFAULT 'PROFILE_REQUIRED'")
|
||||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_at'")
|
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_at'")
|
||||||
|
|||||||
@@ -50,13 +50,14 @@ class CustomerListExportProviderServiceTest {
|
|||||||
assertEquals(1, response.schemaVersion());
|
assertEquals(1, response.schemaVersion());
|
||||||
assertThat(response.schemaHash()).matches("sha256:[0-9a-f]{64}");
|
assertThat(response.schemaHash()).matches("sha256:[0-9a-f]{64}");
|
||||||
assertThat(response.snapshotToken()).matches("v1\\.103\\.[A-Za-z0-9_-]+");
|
assertThat(response.snapshotToken()).matches("v1\\.103\\.[A-Za-z0-9_-]+");
|
||||||
assertEquals(20, response.columns().size());
|
assertEquals(21, response.columns().size());
|
||||||
assertEquals("tenantCode", response.columns().get(1).key());
|
assertEquals("tenantCode", response.columns().get(1).key());
|
||||||
assertEquals(ExportColumnType.DECIMAL,
|
assertEquals(ExportColumnType.DECIMAL,
|
||||||
response.columns().stream().filter(column -> "registeredCapital".equals(column.key()))
|
response.columns().stream().filter(column -> "registeredCapital".equals(column.key()))
|
||||||
.findFirst().orElseThrow().type());
|
.findFirst().orElseThrow().type());
|
||||||
assertEquals(2, response.rows().size());
|
assertEquals(2, response.rows().size());
|
||||||
assertEquals("101", response.rows().get(0).get("id"));
|
assertEquals("101", response.rows().get(0).get("id"));
|
||||||
|
assertEquals("柏", response.rows().get(0).get("customerNickname"));
|
||||||
assertEquals(new BigDecimal("1000000.00"), response.rows().get(0).get("registeredCapital"));
|
assertEquals(new BigDecimal("1000000.00"), response.rows().get(0).get("registeredCapital"));
|
||||||
assertEquals("启用", response.rows().get(0).get("status"));
|
assertEquals("启用", response.rows().get(0).get("status"));
|
||||||
assertEquals("102", response.nextCursor());
|
assertEquals("102", response.nextCursor());
|
||||||
@@ -142,6 +143,7 @@ class CustomerListExportProviderServiceTest {
|
|||||||
tenant.setId(id);
|
tenant.setId(id);
|
||||||
tenant.setTenantCode(String.format("%05d", id));
|
tenant.setTenantCode(String.format("%05d", id));
|
||||||
tenant.setTenantName("柏悦物流" + id);
|
tenant.setTenantName("柏悦物流" + id);
|
||||||
|
tenant.setCustomerNickname("柏");
|
||||||
tenant.setLegalName("柏悦物流有限公司");
|
tenant.setLegalName("柏悦物流有限公司");
|
||||||
tenant.setUnifiedSocialCreditCode("91440000123456789X");
|
tenant.setUnifiedSocialCreditCode("91440000123456789X");
|
||||||
tenant.setEnterpriseType("有限责任公司");
|
tenant.setEnterpriseType("有限责任公司");
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class TenantServiceTest {
|
|||||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||||
|
|
||||||
var updated = service.updateMerchant(10001L,
|
var updated = service.updateMerchant(10001L,
|
||||||
new UpdateTenantRequest("New name", "New legal", "Bob", "Alice", "13800138000", "alice@example.com",
|
new UpdateTenantRequest("New name", "新", "New legal", "Bob", "Alice", "13800138000", "alice@example.com",
|
||||||
"专业版", "alice_wechat", "Sales Zhang", 45, "LIMITED_COMPANY", LocalDate.of(2020, 1, 2),
|
"专业版", "alice_wechat", "Sales Zhang", 45, "LIMITED_COMPANY", LocalDate.of(2020, 1, 2),
|
||||||
new BigDecimal("5000000.00"), "CNY", "深圳市南山区示例路 1 号", "一期重点客户"));
|
new BigDecimal("5000000.00"), "CNY", "深圳市南山区示例路 1 号", "一期重点客户"));
|
||||||
var disabled = service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED"));
|
var disabled = service.changeMerchantStatus(10001L, new ChangeTenantStatusRequest("DISABLED"));
|
||||||
@@ -187,7 +187,7 @@ class TenantServiceTest {
|
|||||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||||
|
|
||||||
var updated = service.updateMerchant(10001L,
|
var updated = service.updateMerchant(10001L,
|
||||||
new UpdateTenantRequest("New name", "New legal", "Bob", "Alice", "13900139000", null,
|
new UpdateTenantRequest("New name", "新", "New legal", "Bob", "Alice", "13900139000", null,
|
||||||
"基础版", null, null, 30, null, null, null, "CNY", null, null));
|
"基础版", null, null, 30, null, null, null, "CNY", null, null));
|
||||||
|
|
||||||
assertEquals("13900139000", updated.contactPhone());
|
assertEquals("13900139000", updated.contactPhone());
|
||||||
@@ -282,7 +282,7 @@ class TenantServiceTest {
|
|||||||
when(repository.findTenant(10001L)).thenReturn(archived);
|
when(repository.findTenant(10001L)).thenReturn(archived);
|
||||||
|
|
||||||
BizException platformFailure = assertThrows(BizException.class, () -> service.updateMerchant(10001L,
|
BizException platformFailure = assertThrows(BizException.class, () -> service.updateMerchant(10001L,
|
||||||
new UpdateTenantRequest("New name", "New legal", "Bob", "Alice", "13800138000", null,
|
new UpdateTenantRequest("New name", "新", "New legal", "Bob", "Alice", "13800138000", null,
|
||||||
"基础版", null, null, 30, null, null, null, "CNY", null, null)));
|
"基础版", null, null, 30, null, null, null, "CNY", null, null)));
|
||||||
BizException merchantFailure = assertThrows(BizException.class, () -> service.update(10001L,
|
BizException merchantFailure = assertThrows(BizException.class, () -> service.update(10001L,
|
||||||
new UpdateProfileRequest("New name", "New legal", "Bob", "Alice",
|
new UpdateProfileRequest("New name", "New legal", "Bob", "Alice",
|
||||||
@@ -317,7 +317,7 @@ class TenantServiceTest {
|
|||||||
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
||||||
|
|
||||||
var created = service.register(new RegisterRequest(
|
var created = service.register(new RegisterRequest(
|
||||||
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
"Example Merchant", null, "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||||
"alice@example.com", "基础版", "alice_wechat", "Sales Zhang", 30, "LIMITED_COMPANY",
|
"alice@example.com", "基础版", "alice_wechat", "Sales Zhang", 30, "LIMITED_COMPANY",
|
||||||
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
|
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
|
||||||
9001L);
|
9001L);
|
||||||
@@ -325,6 +325,7 @@ class TenantServiceTest {
|
|||||||
assertEquals("PROFILE_REQUIRED", created.tenant().status());
|
assertEquals("PROFILE_REQUIRED", created.tenant().status());
|
||||||
assertEquals("00001", created.tenant().tenantCode());
|
assertEquals("00001", created.tenant().tenantCode());
|
||||||
assertEquals("PENDING", created.tenant().dimensionInitializationStatus());
|
assertEquals("PENDING", created.tenant().dimensionInitializationStatus());
|
||||||
|
assertEquals("E", created.tenant().customerNickname());
|
||||||
assertEquals("Bob", created.tenant().legalRepresentative());
|
assertEquals("Bob", created.tenant().legalRepresentative());
|
||||||
assertEquals("基础版", created.tenant().servicePackage());
|
assertEquals("基础版", created.tenant().servicePackage());
|
||||||
assertEquals("merchant_81", created.administratorUsername());
|
assertEquals("merchant_81", created.administratorUsername());
|
||||||
@@ -337,13 +338,21 @@ class TenantServiceTest {
|
|||||||
verify(repository, never()).insertReview(any());
|
verify(repository, never()).insertReview(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customerNicknameDefaultsToFirstChineseCharacterAndAcceptsManualValue() {
|
||||||
|
assertEquals("SZWLYXGS", TenantService.resolveCustomerNickname(null, "ABC 深圳物流有限公司"));
|
||||||
|
assertEquals("SZWL", TenantService.resolveCustomerNickname(null, "深圳物流"));
|
||||||
|
assertEquals("A", TenantService.resolveCustomerNickname(" ", "ABC Logistics"));
|
||||||
|
assertEquals("虫虫", TenantService.resolveCustomerNickname(" 虫虫 ", "深圳物流有限公司"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformRegistrationFailsWhenDurableInitializationIntentCannotBePersisted() {
|
void platformRegistrationFailsWhenDurableInitializationIntentCannotBePersisted() {
|
||||||
doThrow(new BizException(ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
|
doThrow(new BizException(ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
|
||||||
.when(dimensionRuleInitialization).initialize(any(Long.class), any(Long.class));
|
.when(dimensionRuleInitialization).initialize(any(Long.class), any(Long.class));
|
||||||
|
|
||||||
BizException failure = assertThrows(BizException.class, () -> service.register(new RegisterRequest(
|
BizException failure = assertThrows(BizException.class, () -> service.register(new RegisterRequest(
|
||||||
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
"Example Merchant", null, "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||||
"alice@example.com", "基础版", null, null, 30, null,
|
"alice@example.com", "基础版", null, null, 30, null,
|
||||||
null, null, "CNY", null, null), 9001L));
|
null, null, "CNY", null, null), 9001L));
|
||||||
|
|
||||||
@@ -357,7 +366,7 @@ class TenantServiceTest {
|
|||||||
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
.when(provisioning).provisionPlatformCreatedTenant(any(), eq(9001L));
|
||||||
|
|
||||||
var created = service.register(new RegisterRequest(
|
var created = service.register(new RegisterRequest(
|
||||||
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
"Example Merchant", null, "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||||
"alice@example.com", "基础版", null, null, 30, null,
|
"alice@example.com", "基础版", null, null, 30, null,
|
||||||
null, null, "CNY", null, null), 9001L);
|
null, null, "CNY", null, null), 9001L);
|
||||||
|
|
||||||
@@ -524,6 +533,7 @@ class TenantServiceTest {
|
|||||||
tenant.setStatus(status);
|
tenant.setStatus(status);
|
||||||
tenant.setDimensionInitializationStatus("READY");
|
tenant.setDimensionInitializationStatus("READY");
|
||||||
tenant.setTenantName("Example Merchant");
|
tenant.setTenantName("Example Merchant");
|
||||||
|
tenant.setCustomerNickname("E");
|
||||||
tenant.setLegalName("Example Merchant Ltd");
|
tenant.setLegalName("Example Merchant Ltd");
|
||||||
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
||||||
tenant.setContactName("Alice");
|
tenant.setContactName("Alice");
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- 本模块是无数据库的无状态 PDF 渲染服务;调用方传入完整 `labels[]` 或 `documents[]`,模块在内存中生成一次性多页 PDF 并同步返回。
|
- 本模块是无数据库的无状态 PDF 渲染服务;调用方传入完整 `labels[]` 或 `documents[]`,模块在内存中生成一次性多页 PDF 并同步返回。
|
||||||
- 不保存模板、任务、历史或文件,不连接数据库、File 服务或打印机,不反查或修正业务数据。
|
- 不保存模板、任务、历史或文件,不连接数据库、File 服务或打印机,不反查或修正业务数据。
|
||||||
- 当前支持 `INBOUND_MARK_V1`(`100mm x 100mm`)、`DROPSHIP_PALLET_A4_V1`、`SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1` 和 `BILL_OF_LADING_CONFIRMATION_A4_V1`(后三者均为 `210mm x 297mm`);一件代发托盘模板每托固定一页,展示托盘 Code 128 一维码、最多两行入仓码和最多 34 行完整 SKU 归托信息,不展示作业状态。新尺寸或布局必须使用新模板编码,不得静默改变既有 V1。
|
- 当前支持 `INBOUND_MARK_V1`(`100mm x 100mm`)、`DROPSHIP_PALLET_A4_V2`、`SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1` 和 `BILL_OF_LADING_CONFIRMATION_A4_V1`(后三者均为 `210mm x 297mm`);一件代发托盘模板每托固定一页,展示托盘 Code 128 一维码、目的仓、最多 10 行入仓码/客户昵称汇总和最多 34 行 SKU 汇总,不展示作业状态、重量或体积。新尺寸或布局必须使用新模板编码。
|
||||||
|
|
||||||
## 快速索引
|
## 快速索引
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- 服务发现名:`print-service`。
|
- 服务发现名:`print-service`。
|
||||||
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
|
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
|
||||||
- 一件代发 A4 托盘信息单:`POST /internal/print/v1/pdfs/dropship-pallet-a4-documents`;模板固定为 `DROPSHIP_PALLET_A4_V1`,每托一页 A4,包含托盘 Code 128 一维码、最多两行入仓码、总箱数和最多 34 行 SKU/商品名称/箱数/实点数量,不展示作业状态。
|
- 一件代发 A4 托盘信息单:`POST /internal/print/v1/pdfs/dropship-pallet-a4-documents`;模板固定为 `DROPSHIP_PALLET_A4_V2`,每托一页 A4,包含托盘 Code 128 一维码、目的仓、托盘序号、最多 10 行入仓码/客户昵称汇总和最多 34 行 SKU 汇总,不展示作业状态、重量或体积。
|
||||||
- A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。
|
- A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。
|
||||||
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V2`,每个 document 对应一张 A4、包含 1-26 行明细。调用方首张按 18 行、续页按 26 行稳定分页;同页连续相同柜号和托盘号由 Print 纵向合并显示。
|
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V2`,每个 document 对应一张 A4、包含 1-26 行明细。调用方首张按 18 行、续页按 26 行稳定分页;同页连续相同柜号和托盘号由 Print 纵向合并显示。
|
||||||
- 本地直连:`http://127.0.0.1:9207`。
|
- 本地直连:`http://127.0.0.1:9207`。
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@ import java.util.List;
|
|||||||
|
|
||||||
/** 一件代发托盘 A4 信息单的内部渲染契约。 */
|
/** 一件代发托盘 A4 信息单的内部渲染契约。 */
|
||||||
public final class DropshipPalletA4PdfDtos {
|
public final class DropshipPalletA4PdfDtos {
|
||||||
public static final String TEMPLATE = "DROPSHIP_PALLET_A4_V1";
|
public static final String TEMPLATE = "DROPSHIP_PALLET_A4_V2";
|
||||||
|
|
||||||
private DropshipPalletA4PdfDtos() {}
|
private DropshipPalletA4PdfDtos() {}
|
||||||
|
|
||||||
@@ -14,10 +14,14 @@ public final class DropshipPalletA4PdfDtos {
|
|||||||
|
|
||||||
public record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
|
public record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
|
||||||
|
|
||||||
public record Document(String palletNo, String inboundCodes, int cartonCount,
|
public record Document(String palletNo, String destinationWarehouse,
|
||||||
int skuCount, BigDecimal weightKg, BigDecimal volumeCbm,
|
int palletSequence, int totalPallets,
|
||||||
String barcodeContent, List<SkuRow> skuRows) {}
|
String barcodeContent, List<InboundRow> inboundRows,
|
||||||
|
List<SkuRow> skuRows) {}
|
||||||
|
|
||||||
public record SkuRow(String inboundCode, String skuCode, String cargoName,
|
public record InboundRow(String inboundCode, String customerNickname,
|
||||||
int cartonCount, int skuQuantity) {}
|
int cartonCount, int itemQuantity) {}
|
||||||
|
|
||||||
|
public record SkuRow(String skuCode, int cartonCount,
|
||||||
|
int itemQuantity, String unitsPerCarton) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,18 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|||||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/** 每个托盘一页 A4,Code 128 托盘码与完整 SKU 归托信息同页展示。 */
|
/** 每个托盘一页 A4:托盘码、目的仓、入仓码汇总和 SKU 汇总。 */
|
||||||
@Component
|
@Component
|
||||||
public class DropshipPalletA4Renderer {
|
public class DropshipPalletA4Renderer {
|
||||||
private static final float[] COLS = {8, 18, 57, 98, 161, 181, 202};
|
private static final Color BLUE = new Color(20, 89, 156);
|
||||||
private static final String[] HEADERS = {"序号", "入仓码", "SKU", "商品名称", "箱数", "实点数量"};
|
private static final Color LIGHT_BLUE = new Color(225, 239, 251);
|
||||||
private static final double TABLE_TOP = 105;
|
private static final Color TABLE_HEADER = new Color(238, 242, 247);
|
||||||
private static final double HEADER_HEIGHT = 8;
|
private static final Color RED = new Color(180, 43, 36);
|
||||||
private static final double REGULAR_ROW_HEIGHT = 7;
|
private static final float[] INBOUND_COLS = {18, 27, 79, 146, 169, 192};
|
||||||
private static final double DENSE_ROW_HEIGHT = 5.2;
|
private static final String[] INBOUND_HEADERS = {"#", "入仓码", "客户", "箱数", "件数"};
|
||||||
private static final float BARCODE_WIDTH_PT = PdfUnits.mmToPt(158);
|
private static final float[] SKU_COLS = {18, 132, 154, 176, 192};
|
||||||
private static final float BARCODE_HEIGHT_PT = PdfUnits.mmToPt(22);
|
private static final String[] SKU_HEADERS = {"SKU", "箱数", "件数", "每箱件数"};
|
||||||
|
|
||||||
@Resource private PdfBarcodeRenderer barcodeRenderer;
|
@Resource private PdfBarcodeRenderer barcodeRenderer;
|
||||||
|
|
||||||
public void renderPage(PDDocument pdf, PDFont font, DropshipPalletA4PdfDtos.Document data,
|
public void renderPage(PDDocument pdf, PDFont font, DropshipPalletA4PdfDtos.Document data,
|
||||||
@@ -32,93 +33,144 @@ public class DropshipPalletA4Renderer {
|
|||||||
pdf.addPage(page);
|
pdf.addPage(page);
|
||||||
PdfUnits units = new PdfUnits(297);
|
PdfUnits units = new PdfUnits(297);
|
||||||
try (PDPageContentStream stream = new PDPageContentStream(pdf, page)) {
|
try (PDPageContentStream stream = new PDPageContentStream(pdf, page)) {
|
||||||
stream.setNonStrokingColor(Color.BLACK);
|
stream.setLineWidth(PdfUnits.mmToPt(.25));
|
||||||
stream.setLineWidth(PdfUnits.mmToPt(.3));
|
border(stream, units);
|
||||||
centered(stream, units, font, "一件代发托盘信息单", 15, 105, 13);
|
colorText(stream, units, font, "一件代发备货托", 14, 18, 22, BLUE);
|
||||||
centered(stream, units, font, data.palletNo(), 22, 105, 25);
|
rightText(stream, units, font,
|
||||||
barcode(stream, units, barcodeMatrix);
|
"第 " + data.palletSequence() + " 托 / 共 " + data.totalPallets() + " 托",
|
||||||
fill(stream, units, new Color(245, 247, 250), 8, 58, 202, 94);
|
10, 192, 22, new Color(75, 75, 75));
|
||||||
inboundCodes(stream, units, font, data.inboundCodes());
|
|
||||||
line(stream, units, 8, 78, 202, 78);
|
barcodeRenderer.draw(stream, barcodeMatrix, PdfUnits.mmToPt(18), units.topDownMmToPdfY(51),
|
||||||
metric(stream, units, font, "总箱数", Integer.toString(data.cartonCount()), 8, 56.5);
|
PdfUnits.mmToPt(80), PdfUnits.mmToPt(22));
|
||||||
metric(stream, units, font, "SKU 数", Integer.toString(data.skuCount()), 56.5, 105);
|
fittedText(stream, units, font, data.palletNo(), 25, 105, 44, 87, Color.BLACK);
|
||||||
metric(stream, units, font, "重量", decimal(data.weightKg()) + " kg", 105, 153.5);
|
|
||||||
metric(stream, units, font, "体积", decimal(data.volumeCbm()) + " m³", 153.5, 202);
|
fill(stream, units, LIGHT_BLUE, 10, 57, 200, 82);
|
||||||
line(stream, units, 56.5, 78, 56.5, 94);
|
colorText(stream, units, font, "目的仓", 7.5f, 18, 66, BLUE);
|
||||||
line(stream, units, 105, 78, 105, 94);
|
fittedText(stream, units, font, data.destinationWarehouse(), 20, 18, 77, 174, BLUE);
|
||||||
line(stream, units, 153.5, 78, 153.5, 94);
|
|
||||||
line(stream, units, 8, 99, 202, 99);
|
int rowCount = data.inboundRows().size() + 1 + Math.max(1, data.skuRows().size());
|
||||||
text(stream, units, font, "SKU 明细", 11, 8, 104);
|
double rowHeight = rowCount > 36 ? 2.9 : rowCount > 24 ? 3.6 : 6.0;
|
||||||
table(stream, units, font, data.skuRows());
|
float rowFont = rowCount > 36 ? 4.4f : rowCount > 24 ? 5.0f : 7.0f;
|
||||||
|
|
||||||
|
int inboundCartons = data.inboundRows().stream().mapToInt(DropshipPalletA4PdfDtos.InboundRow::cartonCount).sum();
|
||||||
|
int inboundItems = data.inboundRows().stream().mapToInt(DropshipPalletA4PdfDtos.InboundRow::itemQuantity).sum();
|
||||||
|
colorText(stream, units, font,
|
||||||
|
"入仓码信息 · 共 " + data.inboundRows().size() + " 票",
|
||||||
|
9, 18, 92, BLUE);
|
||||||
|
double inboundBottom = inboundTable(stream, units, font, data.inboundRows(),
|
||||||
|
inboundCartons, inboundItems, 97, rowHeight, rowFont);
|
||||||
|
|
||||||
|
double skuTitleY = inboundBottom + 8;
|
||||||
|
colorText(stream, units, font, "SKU 数:" + data.skuRows().size(), 9, 18, skuTitleY, new Color(70, 70, 70));
|
||||||
|
skuTable(stream, units, font, data.skuRows(), skuTitleY + 5, rowHeight, rowFont);
|
||||||
|
|
||||||
|
line(stream, units, 10, 280, 200, 280);
|
||||||
|
colorText(stream, units, font, "拆托 / 合托后此标作废,须重打", 8.5f, 18, 288, RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void barcode(PDPageContentStream stream, PdfUnits units, BitMatrix matrix) throws IOException {
|
private double inboundTable(PDPageContentStream s, PdfUnits u, PDFont f,
|
||||||
float left = PdfUnits.mmToPt(26);
|
List<DropshipPalletA4PdfDtos.InboundRow> rows,
|
||||||
float bottom = units.topDownMmToPdfY(54);
|
int totalCartons, int totalItems, double top,
|
||||||
barcodeRenderer.draw(stream, matrix, left, bottom, BARCODE_WIDTH_PT, BARCODE_HEIGHT_PT);
|
double rowHeight, float rowFont) throws IOException {
|
||||||
|
double headerHeight = 7;
|
||||||
|
double bottom = top + headerHeight + (rows.size() + 1) * rowHeight;
|
||||||
|
tableGrid(s, u, INBOUND_COLS, top, bottom, headerHeight, rows.size() + 1, rowHeight);
|
||||||
|
tableHeaders(s, u, f, INBOUND_COLS, INBOUND_HEADERS, top, headerHeight);
|
||||||
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
var row = rows.get(index);
|
||||||
|
String[] values = {Integer.toString(index + 1), row.inboundCode(), row.customerNickname(),
|
||||||
|
Integer.toString(row.cartonCount()), Integer.toString(row.itemQuantity())};
|
||||||
|
tableRow(s, u, f, INBOUND_COLS, values, top + headerHeight + index * rowHeight,
|
||||||
|
rowHeight, rowFont);
|
||||||
|
}
|
||||||
|
String[] totals = {"计", "跨 " + rows.size() + " 票合计", "", Integer.toString(totalCartons),
|
||||||
|
Integer.toString(totalItems)};
|
||||||
|
tableRow(s, u, f, INBOUND_COLS, totals, top + headerHeight + rows.size() * rowHeight,
|
||||||
|
rowHeight, rowFont);
|
||||||
|
return bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void table(PDPageContentStream stream, PdfUnits units, PDFont font,
|
private void skuTable(PDPageContentStream s, PdfUnits u, PDFont f,
|
||||||
List<DropshipPalletA4PdfDtos.SkuRow> rows) throws IOException {
|
List<DropshipPalletA4PdfDtos.SkuRow> rows, double top,
|
||||||
|
double rowHeight, float rowFont) throws IOException {
|
||||||
|
double headerHeight = 7;
|
||||||
int visibleRows = Math.max(1, rows.size());
|
int visibleRows = Math.max(1, rows.size());
|
||||||
double rowHeight = rows.size() > 24 ? DENSE_ROW_HEIGHT : REGULAR_ROW_HEIGHT;
|
double bottom = top + headerHeight + visibleRows * rowHeight;
|
||||||
float rowFontSize = rows.size() > 24 ? 5.3f : 6.4f;
|
tableGrid(s, u, SKU_COLS, top, bottom, headerHeight, visibleRows, rowHeight);
|
||||||
double headerBottom = TABLE_TOP + HEADER_HEIGHT;
|
tableHeaders(s, u, f, SKU_COLS, SKU_HEADERS, top, headerHeight);
|
||||||
double bottom = headerBottom + visibleRows * rowHeight;
|
|
||||||
fill(stream, units, new Color(235, 239, 245), COLS[0], TABLE_TOP, COLS[COLS.length - 1], headerBottom);
|
|
||||||
for (float x : COLS) line(stream, units, x, TABLE_TOP, x, bottom);
|
|
||||||
for (int boundary = 0; boundary <= visibleRows; boundary++) {
|
|
||||||
line(stream, units, COLS[0], headerBottom + boundary * rowHeight,
|
|
||||||
COLS[COLS.length - 1], headerBottom + boundary * rowHeight);
|
|
||||||
}
|
|
||||||
line(stream, units, COLS[0], TABLE_TOP, COLS[COLS.length - 1], TABLE_TOP);
|
|
||||||
for (int column = 0; column < HEADERS.length; column++) {
|
|
||||||
centeredCell(stream, units, font, HEADERS[column], 7.2f,
|
|
||||||
COLS[column] + .5, COLS[column + 1] - .5, TABLE_TOP + 5.5);
|
|
||||||
}
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
centeredCell(stream, units, font, "暂无已归托 SKU", 7.2f, COLS[0], COLS[COLS.length - 1], headerBottom + 4.8);
|
tableRow(s, u, f, SKU_COLS, new String[]{"暂无已归托 SKU", "", "", ""},
|
||||||
|
top + headerHeight, rowHeight, rowFont);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
DropshipPalletA4PdfDtos.SkuRow row = rows.get(rowIndex);
|
var row = rows.get(index);
|
||||||
String[] values = {Integer.toString(rowIndex + 1), row.inboundCode(), row.skuCode(), row.cargoName(),
|
String[] values = {row.skuCode(), Integer.toString(row.cartonCount()),
|
||||||
Integer.toString(row.cartonCount()), Integer.toString(row.skuQuantity())};
|
Integer.toString(row.itemQuantity()), row.unitsPerCarton()};
|
||||||
double baseline = headerBottom + rowIndex * rowHeight + rowHeight * .7;
|
tableRow(s, u, f, SKU_COLS, values, top + headerHeight + index * rowHeight,
|
||||||
for (int column = 0; column < values.length; column++) {
|
rowHeight, rowFont);
|
||||||
centeredCell(stream, units, font, values[column], rowFontSize,
|
|
||||||
COLS[column] + .7, COLS[column + 1] - .7, baseline);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String decimal(java.math.BigDecimal value) {
|
private void tableGrid(PDPageContentStream s, PdfUnits u, float[] columns, double top,
|
||||||
return value == null ? "0" : value.stripTrailingZeros().toPlainString();
|
double bottom, double headerHeight, int rows, double rowHeight) throws IOException {
|
||||||
}
|
fill(s, u, TABLE_HEADER, columns[0], top, columns[columns.length - 1], top + headerHeight);
|
||||||
private void inboundCodes(PDPageContentStream s, PdfUnits u, PDFont f, String value) throws IOException {
|
for (float column : columns) line(s, u, column, top, column, bottom);
|
||||||
text(s, u, f, "入仓码:", 8, 14, 66);
|
line(s, u, columns[0], top, columns[columns.length - 1], top);
|
||||||
List<String> lines = wrap(f, value, 8, PdfUnits.mmToPt(163), 2);
|
line(s, u, columns[0], top + headerHeight, columns[columns.length - 1], top + headerHeight);
|
||||||
for (int index = 0; index < lines.size(); index++) {
|
for (int index = 1; index <= rows; index++) {
|
||||||
text(s, u, f, lines.get(index), 8, 33, 66 + index * 6);
|
line(s, u, columns[0], top + headerHeight + index * rowHeight,
|
||||||
|
columns[columns.length - 1], top + headerHeight + index * rowHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void metric(PDPageContentStream s, PdfUnits u, PDFont f, String label, String value,
|
|
||||||
double x1, double x2) throws IOException {
|
private void tableHeaders(PDPageContentStream s, PdfUnits u, PDFont f, float[] columns,
|
||||||
centeredCell(s, u, f, label, 7.2f, x1 + 1, x2 - 1, 84);
|
String[] headers, double top, double headerHeight) throws IOException {
|
||||||
centeredCell(s, u, f, value, 10.5f, x1 + 1, x2 - 1, 91);
|
for (int index = 0; index < headers.length; index++) {
|
||||||
|
centeredCell(s, u, f, headers[index], 7, columns[index] + .5,
|
||||||
|
columns[index + 1] - .5, top + headerHeight * .68);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private void centered(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
|
||||||
double centerX, double y) throws IOException {
|
private void tableRow(PDPageContentStream s, PdfUnits u, PDFont f, float[] columns,
|
||||||
float width = f.getStringWidth(value) / 1000 * size;
|
String[] values, double top, double height, float size) throws IOException {
|
||||||
text(s, u, f, value, size, centerX - width / PdfUnits.mmToPt(1) / 2, y);
|
for (int index = 0; index < values.length; index++) {
|
||||||
|
centeredCell(s, u, f, values[index], size, columns[index] + .7,
|
||||||
|
columns[index + 1] - .7, top + height * .7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void border(PDPageContentStream s, PdfUnits u) throws IOException {
|
||||||
|
s.setStrokingColor(BLUE);
|
||||||
|
s.setLineWidth(PdfUnits.mmToPt(.55));
|
||||||
|
s.addRect(PdfUnits.mmToPt(10), u.topDownMmToPdfY(290), PdfUnits.mmToPt(190), PdfUnits.mmToPt(280));
|
||||||
|
s.stroke();
|
||||||
|
s.setStrokingColor(new Color(140, 140, 140));
|
||||||
|
s.setLineWidth(PdfUnits.mmToPt(.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fittedText(PDPageContentStream s, PdfUnits u, PDFont f, String value,
|
||||||
|
float preferredSize, double x, double y, double widthMm,
|
||||||
|
Color color) throws IOException {
|
||||||
|
float size = preferredSize;
|
||||||
|
while (size > 9 && f.getStringWidth(value) / 1000 * size > PdfUnits.mmToPt(widthMm)) size -= .5f;
|
||||||
|
colorText(s, u, f, fit(f, value, size, PdfUnits.mmToPt(widthMm)), size, x, y, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rightText(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
||||||
|
double right, double y, Color color) throws IOException {
|
||||||
|
double widthMm = f.getStringWidth(value) / 1000 * size / PdfUnits.mmToPt(1);
|
||||||
|
colorText(s, u, f, value, size, right - widthMm, y, color);
|
||||||
|
}
|
||||||
|
|
||||||
private void centeredCell(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
private void centeredCell(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
||||||
double x1, double x2, double y) throws IOException {
|
double x1, double x2, double y) throws IOException {
|
||||||
String fitted = fit(f, value, size, PdfUnits.mmToPt(x2 - x1));
|
String fitted = fit(f, value, size, PdfUnits.mmToPt(x2 - x1));
|
||||||
float width = f.getStringWidth(fitted) / 1000 * size;
|
float width = f.getStringWidth(fitted) / 1000 * size;
|
||||||
text(s, u, f, fitted, size, x1 + (x2 - x1 - width / PdfUnits.mmToPt(1)) / 2, y);
|
text(s, u, f, fitted, size, x1 + (x2 - x1 - width / PdfUnits.mmToPt(1)) / 2, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fit(PDFont font, String raw, float size, float maxWidth) throws IOException {
|
private String fit(PDFont font, String raw, float size, float maxWidth) throws IOException {
|
||||||
String value = raw == null || raw.isBlank() ? "—" : raw;
|
String value = raw == null || raw.isBlank() ? "—" : raw;
|
||||||
if (font.getStringWidth(value) / 1000 * size <= maxWidth) return value;
|
if (font.getStringWidth(value) / 1000 * size <= maxWidth) return value;
|
||||||
@@ -127,26 +179,7 @@ public class DropshipPalletA4Renderer {
|
|||||||
while (end > 0 && font.getStringWidth(value.substring(0, end) + ellipsis) / 1000 * size > maxWidth) end--;
|
while (end > 0 && font.getStringWidth(value.substring(0, end) + ellipsis) / 1000 * size > maxWidth) end--;
|
||||||
return end == 0 ? ellipsis : value.substring(0, end) + ellipsis;
|
return end == 0 ? ellipsis : value.substring(0, end) + ellipsis;
|
||||||
}
|
}
|
||||||
private List<String> wrap(PDFont font, String raw, float size, float maxWidth,
|
|
||||||
int maxLines) throws IOException {
|
|
||||||
String remaining = raw == null || raw.isBlank() ? "—" : raw;
|
|
||||||
List<String> lines = new java.util.ArrayList<>();
|
|
||||||
while (!remaining.isEmpty() && lines.size() < maxLines) {
|
|
||||||
if (font.getStringWidth(remaining) / 1000 * size <= maxWidth) {
|
|
||||||
lines.add(remaining);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
int split = remaining.length();
|
|
||||||
while (split > 0 && font.getStringWidth(remaining.substring(0, split)) / 1000 * size > maxWidth) split--;
|
|
||||||
int delimiter = remaining.lastIndexOf('/', split - 1);
|
|
||||||
if (delimiter > 0) split = delimiter + 1;
|
|
||||||
String line = remaining.substring(0, Math.max(1, split));
|
|
||||||
remaining = remaining.substring(Math.max(1, split));
|
|
||||||
if (lines.size() == maxLines - 1 && !remaining.isEmpty()) line = fit(font, line + remaining, size, maxWidth);
|
|
||||||
lines.add(line);
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
private void fill(PDPageContentStream s, PdfUnits u, Color color, double x1, double y1,
|
private void fill(PDPageContentStream s, PdfUnits u, Color color, double x1, double y1,
|
||||||
double x2, double y2) throws IOException {
|
double x2, double y2) throws IOException {
|
||||||
s.setNonStrokingColor(color);
|
s.setNonStrokingColor(color);
|
||||||
@@ -154,14 +187,27 @@ public class DropshipPalletA4Renderer {
|
|||||||
s.fill();
|
s.fill();
|
||||||
s.setNonStrokingColor(Color.BLACK);
|
s.setNonStrokingColor(Color.BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void colorText(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
||||||
|
double x, double y, Color color) throws IOException {
|
||||||
|
s.setNonStrokingColor(color);
|
||||||
|
text(s, u, f, value, size, x, y);
|
||||||
|
s.setNonStrokingColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
private void text(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
private void text(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
|
||||||
double x, double y) throws IOException {
|
double x, double y) throws IOException {
|
||||||
s.beginText(); s.setFont(f, size); s.newLineAtOffset(PdfUnits.mmToPt(x), u.topDownMmToPdfY(y));
|
s.beginText();
|
||||||
s.showText(value == null ? "—" : value); s.endText();
|
s.setFont(f, size);
|
||||||
|
s.newLineAtOffset(PdfUnits.mmToPt(x), u.topDownMmToPdfY(y));
|
||||||
|
s.showText(value == null ? "—" : value);
|
||||||
|
s.endText();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void line(PDPageContentStream s, PdfUnits u, double x1, double y1, double x2,
|
private void line(PDPageContentStream s, PdfUnits u, double x1, double y1, double x2,
|
||||||
double y2) throws IOException {
|
double y2) throws IOException {
|
||||||
s.moveTo(PdfUnits.mmToPt(x1), u.topDownMmToPdfY(y1));
|
s.moveTo(PdfUnits.mmToPt(x1), u.topDownMmToPdfY(y1));
|
||||||
s.lineTo(PdfUnits.mmToPt(x2), u.topDownMmToPdfY(y2)); s.stroke();
|
s.lineTo(PdfUnits.mmToPt(x2), u.topDownMmToPdfY(y2));
|
||||||
|
s.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.springframework.stereotype.Service;
|
|||||||
public class DropshipPalletA4PdfService {
|
public class DropshipPalletA4PdfService {
|
||||||
private static final BigDecimal A4_WIDTH = new BigDecimal("210");
|
private static final BigDecimal A4_WIDTH = new BigDecimal("210");
|
||||||
private static final BigDecimal A4_HEIGHT = new BigDecimal("297");
|
private static final BigDecimal A4_HEIGHT = new BigDecimal("297");
|
||||||
|
private static final int MAX_INBOUND_ROWS = 10;
|
||||||
private static final int MAX_SKU_ROWS = 34;
|
private static final int MAX_SKU_ROWS = 34;
|
||||||
private static final Set<Character> FORBIDDEN_FILE_CHARS = Set.of('/', '\\', ':', '*', '?', '"', '<', '>', '|');
|
private static final Set<Character> FORBIDDEN_FILE_CHARS = Set.of('/', '\\', ':', '*', '?', '"', '<', '>', '|');
|
||||||
|
|
||||||
@@ -67,21 +68,28 @@ public class DropshipPalletA4PdfService {
|
|||||||
|
|
||||||
private DropshipPalletA4PdfDtos.Document normalizeDocument(DropshipPalletA4PdfDtos.Document value, int index) {
|
private DropshipPalletA4PdfDtos.Document normalizeDocument(DropshipPalletA4PdfDtos.Document value, int index) {
|
||||||
if (value == null) invalid("documents[" + index + "] is required");
|
if (value == null) invalid("documents[" + index + "] is required");
|
||||||
if (value.cartonCount() < 0 || value.skuCount() < 0 || negative(value.weightKg()) || negative(value.volumeCbm())) invalid("documents[" + index + "] totals are invalid");
|
if (value.palletSequence() <= 0 || value.totalPallets() <= 0
|
||||||
|
|| value.palletSequence() > value.totalPallets()) invalid("documents[" + index + "] pallet position is invalid");
|
||||||
|
if (value.inboundRows() == null || value.inboundRows().isEmpty()
|
||||||
|
|| value.inboundRows().size() > MAX_INBOUND_ROWS) invalid("documents[" + index + "].inboundRows exceeds the one-page A4 capacity");
|
||||||
if (value.skuRows() == null || value.skuRows().size() > MAX_SKU_ROWS) invalid("documents[" + index + "].skuRows exceeds the one-page A4 capacity");
|
if (value.skuRows() == null || value.skuRows().size() > MAX_SKU_ROWS) invalid("documents[" + index + "].skuRows exceeds the one-page A4 capacity");
|
||||||
|
List<DropshipPalletA4PdfDtos.InboundRow> inboundRows = new ArrayList<>();
|
||||||
|
for (DropshipPalletA4PdfDtos.InboundRow row : value.inboundRows()) {
|
||||||
|
if (row == null || row.cartonCount() < 0 || row.itemQuantity() < 0) invalid("inbound row is invalid");
|
||||||
|
inboundRows.add(new DropshipPalletA4PdfDtos.InboundRow(text(row.inboundCode(), 80),
|
||||||
|
text(row.customerNickname(), 80), row.cartonCount(), row.itemQuantity()));
|
||||||
|
}
|
||||||
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
||||||
for (DropshipPalletA4PdfDtos.SkuRow row : value.skuRows()) {
|
for (DropshipPalletA4PdfDtos.SkuRow row : value.skuRows()) {
|
||||||
if (row == null || row.cartonCount() < 0 || row.skuQuantity() < 0) invalid("SKU row is invalid");
|
if (row == null || row.cartonCount() < 0 || row.itemQuantity() < 0) invalid("SKU row is invalid");
|
||||||
rows.add(new DropshipPalletA4PdfDtos.SkuRow(text(row.inboundCode(), 80), text(row.skuCode(), 128),
|
rows.add(new DropshipPalletA4PdfDtos.SkuRow(text(row.skuCode(), 128), row.cartonCount(),
|
||||||
text(row.cargoName(), 200), row.cartonCount(), row.skuQuantity()));
|
row.itemQuantity(), text(row.unitsPerCarton(), 30)));
|
||||||
}
|
}
|
||||||
return new DropshipPalletA4PdfDtos.Document(text(value.palletNo(), 80), text(value.inboundCodes(), 300),
|
return new DropshipPalletA4PdfDtos.Document(text(value.palletNo(), 80),
|
||||||
value.cartonCount(), value.skuCount(), decimal(value.weightKg()), decimal(value.volumeCbm()),
|
text(value.destinationWarehouse(), 200), value.palletSequence(), value.totalPallets(),
|
||||||
barcode(value.barcodeContent()), List.copyOf(rows));
|
barcode(value.barcodeContent()), List.copyOf(inboundRows), List.copyOf(rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal decimal(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
|
|
||||||
private boolean negative(BigDecimal value) { return value != null && value.signum() < 0; }
|
|
||||||
private String barcode(String value) {
|
private String barcode(String value) {
|
||||||
String normalized = text(value, 80);
|
String normalized = text(value, 80);
|
||||||
if (normalized.codePoints().anyMatch(codePoint -> codePoint < 0x20 || codePoint > 0x7e)) invalid("barcodeContent must contain printable ASCII only");
|
if (normalized.codePoints().anyMatch(codePoint -> codePoint < 0x20 || codePoint > 0x7e)) invalid("barcodeContent must contain printable ASCII only");
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
required: [templateCode, pageSpec, downloadFileName, documents]
|
required: [templateCode, pageSpec, downloadFileName, documents]
|
||||||
properties:
|
properties:
|
||||||
templateCode: { type: string, enum: [DROPSHIP_PALLET_A4_V1] }
|
templateCode: { type: string, enum: [DROPSHIP_PALLET_A4_V2] }
|
||||||
pageSpec: { $ref: '#/components/schemas/FreightA4PageSpec' }
|
pageSpec: { $ref: '#/components/schemas/FreightA4PageSpec' }
|
||||||
downloadFileName: { type: string, maxLength: 120 }
|
downloadFileName: { type: string, maxLength: 120 }
|
||||||
documents:
|
documents:
|
||||||
@@ -290,33 +290,43 @@ components:
|
|||||||
items: { $ref: '#/components/schemas/DropshipPalletA4Document' }
|
items: { $ref: '#/components/schemas/DropshipPalletA4Document' }
|
||||||
DropshipPalletA4Document:
|
DropshipPalletA4Document:
|
||||||
type: object
|
type: object
|
||||||
required: [palletNo, inboundCodes, cartonCount, skuCount, weightKg, volumeCbm, barcodeContent, skuRows]
|
required: [palletNo, destinationWarehouse, palletSequence, totalPallets, barcodeContent, inboundRows, skuRows]
|
||||||
properties:
|
properties:
|
||||||
palletNo: { type: string, maxLength: 80 }
|
palletNo: { type: string, maxLength: 80 }
|
||||||
inboundCodes: { type: string, maxLength: 300 }
|
destinationWarehouse: { type: string, maxLength: 200 }
|
||||||
cartonCount: { type: integer, minimum: 0 }
|
palletSequence: { type: integer, minimum: 1 }
|
||||||
skuCount: { type: integer, minimum: 0 }
|
totalPallets: { type: integer, minimum: 1 }
|
||||||
weightKg: { type: number, minimum: 0 }
|
|
||||||
volumeCbm: { type: number, minimum: 0 }
|
|
||||||
barcodeContent:
|
barcodeContent:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 80
|
maxLength: 80
|
||||||
pattern: '^[\\x20-\\x7E]+$'
|
pattern: '^[\\x20-\\x7E]+$'
|
||||||
description: Code 128 一维码内容,当前固定为稳定托盘码
|
description: Code 128 一维码内容,当前固定为稳定托盘码
|
||||||
|
inboundRows:
|
||||||
|
type: array
|
||||||
|
minItems: 1
|
||||||
|
maxItems: 10
|
||||||
|
items: { $ref: '#/components/schemas/DropshipPalletInboundRow' }
|
||||||
skuRows:
|
skuRows:
|
||||||
type: array
|
type: array
|
||||||
minItems: 0
|
minItems: 0
|
||||||
maxItems: 34
|
maxItems: 34
|
||||||
items: { $ref: '#/components/schemas/DropshipPalletSkuRow' }
|
items: { $ref: '#/components/schemas/DropshipPalletSkuRow' }
|
||||||
DropshipPalletSkuRow:
|
DropshipPalletInboundRow:
|
||||||
type: object
|
type: object
|
||||||
required: [inboundCode, skuCode, cargoName, cartonCount, skuQuantity]
|
required: [inboundCode, customerNickname, cartonCount, itemQuantity]
|
||||||
properties:
|
properties:
|
||||||
inboundCode: { type: string, maxLength: 80 }
|
inboundCode: { type: string, maxLength: 80 }
|
||||||
skuCode: { type: string, maxLength: 128 }
|
customerNickname: { type: string, maxLength: 80 }
|
||||||
cargoName: { type: string, maxLength: 200 }
|
|
||||||
cartonCount: { type: integer, minimum: 0 }
|
cartonCount: { type: integer, minimum: 0 }
|
||||||
skuQuantity: { type: integer, minimum: 0 }
|
itemQuantity: { type: integer, minimum: 0 }
|
||||||
|
DropshipPalletSkuRow:
|
||||||
|
type: object
|
||||||
|
required: [skuCode, cartonCount, itemQuantity, unitsPerCarton]
|
||||||
|
properties:
|
||||||
|
skuCode: { type: string, maxLength: 128 }
|
||||||
|
cartonCount: { type: integer, minimum: 0 }
|
||||||
|
itemQuantity: { type: integer, minimum: 0 }
|
||||||
|
unitsPerCarton: { type: string, maxLength: 30 }
|
||||||
FreightA4PageSpec:
|
FreightA4PageSpec:
|
||||||
type: object
|
type: object
|
||||||
required: [widthMm, heightMm]
|
required: [widthMm, heightMm]
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ class InternalPdfControllerTest {
|
|||||||
.contains("name: X-Internal-Service-Token")
|
.contains("name: X-Internal-Service-Token")
|
||||||
.contains("SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1:")
|
.contains("SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1:")
|
||||||
.contains("BILL_OF_LADING_CONFIRMATION_A4_V1:")
|
.contains("BILL_OF_LADING_CONFIRMATION_A4_V1:")
|
||||||
.contains("DROPSHIP_PALLET_A4_V1")
|
.contains("DROPSHIP_PALLET_A4_V2")
|
||||||
.contains("additionalProperties: false")
|
.contains("additionalProperties: false")
|
||||||
.contains("minItems: 1")
|
.contains("minItems: 1")
|
||||||
.contains("required: [marksAndNos, quantity, packaging, descriptionOfGoods, grossWeight, measurement]")
|
.contains("required: [marksAndNos, quantity, packaging, descriptionOfGoods, grossWeight, measurement]")
|
||||||
@@ -268,23 +268,26 @@ class InternalPdfControllerTest {
|
|||||||
private String validDropshipPalletA4RequestJson() {
|
private String validDropshipPalletA4RequestJson() {
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"templateCode": "DROPSHIP_PALLET_A4_V1",
|
"templateCode": "DROPSHIP_PALLET_A4_V2",
|
||||||
"pageSpec": {"widthMm": 210, "heightMm": 297},
|
"pageSpec": {"widthMm": 210, "heightMm": 297},
|
||||||
"downloadFileName": "DP-260911-0034-A4托盘信息单.pdf",
|
"downloadFileName": "DP-260911-0034-A4托盘信息单.pdf",
|
||||||
"documents": [{
|
"documents": [{
|
||||||
"palletNo": "DP-260911-0034",
|
"palletNo": "DP-260911-0034",
|
||||||
"inboundCodes": "NYEXX2609110249",
|
"destinationWarehouse": "纽约 N.JDF",
|
||||||
"cartonCount": 3,
|
"palletSequence": 1,
|
||||||
"skuCount": 1,
|
"totalPallets": 3,
|
||||||
"weightKg": 90,
|
|
||||||
"volumeCbm": 0.534,
|
|
||||||
"barcodeContent": "DP-260911-0034",
|
"barcodeContent": "DP-260911-0034",
|
||||||
"skuRows": [{
|
"inboundRows": [{
|
||||||
"inboundCode": "NYEXX2609110249",
|
"inboundCode": "NYEXX2609110249",
|
||||||
"skuCode": "SKU-001",
|
"customerNickname": "小虫科技",
|
||||||
"cargoName": "蓝牙音箱",
|
|
||||||
"cartonCount": 3,
|
"cartonCount": 3,
|
||||||
"skuQuantity": 90
|
"itemQuantity": 90
|
||||||
|
}],
|
||||||
|
"skuRows": [{
|
||||||
|
"skuCode": "SKU-001",
|
||||||
|
"cartonCount": 3,
|
||||||
|
"itemQuantity": 90,
|
||||||
|
"unitsPerCarton": "30"
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-15
@@ -27,11 +27,12 @@ class DropshipPalletA4PdfServiceGenerationTest {
|
|||||||
@Test
|
@Test
|
||||||
void generatesOneA4PageWithPalletBarcodeAndSkuDetails() throws Exception {
|
void generatesOneA4PageWithPalletBarcodeAndSkuDetails() throws Exception {
|
||||||
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
||||||
"DP-260911-0034", "NYEXX2609110249", 3, 2,
|
"DP-260911-0034", "纽约 N.JDF", 1, 3, "DP-260911-0034",
|
||||||
new BigDecimal("90"), new BigDecimal("0.534"), "DP-260911-0034",
|
List.of(new DropshipPalletA4PdfDtos.InboundRow(
|
||||||
|
"NYEXX2609110249", "小虫科技", 3, 90)),
|
||||||
List.of(
|
List.of(
|
||||||
new DropshipPalletA4PdfDtos.SkuRow("NYEXX2609110249", "SKU-001", "蓝牙音箱", 2, 40),
|
new DropshipPalletA4PdfDtos.SkuRow("SKU-001", 2, 40, "20"),
|
||||||
new DropshipPalletA4PdfDtos.SkuRow("NYEXX2609110249", "SKU-002", "充电线", 1, 50)));
|
new DropshipPalletA4PdfDtos.SkuRow("SKU-002", 1, 50, "50")));
|
||||||
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
|
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
|
||||||
DropshipPalletA4PdfDtos.TEMPLATE,
|
DropshipPalletA4PdfDtos.TEMPLATE,
|
||||||
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
|
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
|
||||||
@@ -43,24 +44,28 @@ class DropshipPalletA4PdfServiceGenerationTest {
|
|||||||
assertThat(pdf.getPage(0).getMediaBox().getWidth()).isCloseTo(PdfUnits.mmToPt(210), org.assertj.core.data.Offset.offset(.002f));
|
assertThat(pdf.getPage(0).getMediaBox().getWidth()).isCloseTo(PdfUnits.mmToPt(210), org.assertj.core.data.Offset.offset(.002f));
|
||||||
assertThat(pdf.getPage(0).getMediaBox().getHeight()).isCloseTo(PdfUnits.mmToPt(297), org.assertj.core.data.Offset.offset(.002f));
|
assertThat(pdf.getPage(0).getMediaBox().getHeight()).isCloseTo(PdfUnits.mmToPt(297), org.assertj.core.data.Offset.offset(.002f));
|
||||||
assertThat(new PDFTextStripper().getText(pdf))
|
assertThat(new PDFTextStripper().getText(pdf))
|
||||||
.contains("DP-260911-0034", "NYEXX2609110249", "SKU 明细", "SKU-001", "SKU-002", "40", "50")
|
.contains("DP-260911-0034", "N.JDF", "NYEXX2609110249",
|
||||||
|
"SKU-001", "SKU-002", "40", "50")
|
||||||
.containsOnlyOnce("DP-260911-0034")
|
.containsOnlyOnce("DP-260911-0034")
|
||||||
.doesNotContain("状态", "待封托", "已封托");
|
.doesNotContain("状态", "待封托", "已封托", "重量", "体积");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void keepsTenInboundCodesAndThirtyTwoSkuRowsOnOneA4Page() throws Exception {
|
void keepsTenInboundCodesAndThirtyTwoSkuRowsOnOneA4Page() throws Exception {
|
||||||
List<String> inboundCodes = new ArrayList<>();
|
List<DropshipPalletA4PdfDtos.InboundRow> inboundRows = new ArrayList<>();
|
||||||
for (int index = 1; index <= 10; index++) inboundCodes.add("NYEXX260911" + String.format("%04d", 240 + index));
|
for (int index = 1; index <= 10; index++) {
|
||||||
|
inboundRows.add(new DropshipPalletA4PdfDtos.InboundRow(
|
||||||
|
"NYEXX260911" + String.format("%04d", 240 + index), "客户" + index, 2, index * 5));
|
||||||
|
}
|
||||||
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
||||||
for (int index = 1; index <= 32; index++) {
|
for (int index = 1; index <= 32; index++) {
|
||||||
rows.add(new DropshipPalletA4PdfDtos.SkuRow(inboundCodes.get((index - 1) % inboundCodes.size()),
|
rows.add(new DropshipPalletA4PdfDtos.SkuRow(
|
||||||
"SKU-" + String.format("%03d", index), "测试商品" + index, 2, index * 5));
|
"SKU-" + String.format("%03d", index), 2, index * 5, "5-10"));
|
||||||
}
|
}
|
||||||
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
||||||
"DP-260911-0034", String.join("/", inboundCodes), 64, 32,
|
"DP-260911-0034", "纽约 N.JDF", 1, 1,
|
||||||
new BigDecimal("860.5"), new BigDecimal("6.728"), "DP-260911-0034", rows);
|
"DP-260911-0034", inboundRows, rows);
|
||||||
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
|
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
|
||||||
DropshipPalletA4PdfDtos.TEMPLATE,
|
DropshipPalletA4PdfDtos.TEMPLATE,
|
||||||
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
|
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
|
||||||
@@ -77,11 +82,11 @@ class DropshipPalletA4PdfServiceGenerationTest {
|
|||||||
void rejectsSkuRowsThatCannotFitOnOneA4Page() {
|
void rejectsSkuRowsThatCannotFitOnOneA4Page() {
|
||||||
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
List<DropshipPalletA4PdfDtos.SkuRow> rows = new ArrayList<>();
|
||||||
for (int index = 0; index < 35; index++) {
|
for (int index = 0; index < 35; index++) {
|
||||||
rows.add(new DropshipPalletA4PdfDtos.SkuRow("INBOUND", "SKU-" + index, "商品", 1, 1));
|
rows.add(new DropshipPalletA4PdfDtos.SkuRow("SKU-" + index, 1, 1, "1"));
|
||||||
}
|
}
|
||||||
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
|
||||||
"DP-260911-0034", "INBOUND", 35, 35,
|
"DP-260911-0034", "纽约 N.JDF", 1, 1, "DP-260911-0034",
|
||||||
BigDecimal.ZERO, BigDecimal.ZERO, "DP-260911-0034", rows);
|
List.of(new DropshipPalletA4PdfDtos.InboundRow("INBOUND", "客户", 35, 35)), rows);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.generate(new DropshipPalletA4PdfDtos.Request(
|
assertThatThrownBy(() -> service.generate(new DropshipPalletA4PdfDtos.Request(
|
||||||
DropshipPalletA4PdfDtos.TEMPLATE,
|
DropshipPalletA4PdfDtos.TEMPLATE,
|
||||||
|
|||||||
Reference in New Issue
Block a user