优化
This commit is contained in:
+41
-1
@@ -6,10 +6,12 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyPageQuery;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyImportResult;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanySaveRequest;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.StatusRequest;
|
||||
import com.baiyue.warehouse.entity.ExpressCompanyEntity;
|
||||
import com.baiyue.warehouse.service.ExpressCompanyQueryService;
|
||||
import com.baiyue.warehouse.service.ExpressCompanyImportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -19,6 +21,10 @@ import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -28,6 +34,11 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** 平台快递公司管理接口。 */
|
||||
@Validated
|
||||
@@ -36,9 +47,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/v1/express-companies")
|
||||
public class PlatformExpressCompanyController {
|
||||
private final ExpressCompanyQueryService service;
|
||||
private final ExpressCompanyImportService importService;
|
||||
|
||||
public PlatformExpressCompanyController(ExpressCompanyQueryService service) {
|
||||
public PlatformExpressCompanyController(ExpressCompanyQueryService service, ExpressCompanyImportService importService) {
|
||||
this.service = service;
|
||||
this.importService = importService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -57,6 +70,33 @@ public class PlatformExpressCompanyController {
|
||||
return ApiResponse.success(service.page(tenantId, query));
|
||||
}
|
||||
|
||||
@GetMapping("/import-template")
|
||||
@PreAuthorize("hasAuthority('platform:express:import')")
|
||||
@Operation(summary = "下载快递公司导入模板", description = "下载当前版本的 XLSX 标准模板")
|
||||
public ResponseEntity<byte[]> importTemplate(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
requirePlatform(user);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDisposition(ContentDisposition.attachment()
|
||||
.filename("express-company-import-template-v1.xlsx", StandardCharsets.UTF_8).build());
|
||||
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
return ResponseEntity.ok().headers(headers).body(importService.template());
|
||||
}
|
||||
|
||||
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:express:import')")
|
||||
@Operation(summary = "批量导入快递公司", description = "使用标准 XLSX 模板整批校验并新增快递公司")
|
||||
public ApiResponse<ExpressCompanyImportResult> importCompanies(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @Min(1) long tenantId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
requirePlatform(user);
|
||||
try {
|
||||
return ApiResponse.success(importService.importFile(tenantId, file.getOriginalFilename(), file.getBytes()));
|
||||
} catch (IOException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "导入文件读取失败");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('platform:express:list')")
|
||||
@Operation(summary = "查询快递公司详情", description = "查询目标租户内的快递公司详情")
|
||||
|
||||
+41
-1
@@ -7,9 +7,11 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerSaveRequest;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerImportResult;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.StatusRequest;
|
||||
import com.baiyue.warehouse.entity.PartnerEntity;
|
||||
import com.baiyue.warehouse.service.PlatformPartnerQueryService;
|
||||
import com.baiyue.warehouse.service.PartnerImportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -28,6 +30,15 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** 平台往来单位管理接口。 */
|
||||
@Validated
|
||||
@@ -36,9 +47,38 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequestMapping("/api/platform/v1/partners")
|
||||
public class PlatformPartnerController {
|
||||
private final PlatformPartnerQueryService service;
|
||||
private final PartnerImportService importService;
|
||||
|
||||
public PlatformPartnerController(PlatformPartnerQueryService service) {
|
||||
public PlatformPartnerController(PlatformPartnerQueryService service, PartnerImportService importService) {
|
||||
this.service = service;
|
||||
this.importService = importService;
|
||||
}
|
||||
|
||||
@GetMapping("/import-template")
|
||||
@PreAuthorize("hasAuthority('platform:counterparty:import')")
|
||||
@Operation(summary = "下载往来单位导入模板", description = "下载当前版本的 XLSX 标准模板")
|
||||
public ResponseEntity<byte[]> importTemplate(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
requirePlatform(user);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDisposition(ContentDisposition.attachment()
|
||||
.filename("partner-import-template-v1.xlsx", StandardCharsets.UTF_8).build());
|
||||
headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
return ResponseEntity.ok().headers(headers).body(importService.template());
|
||||
}
|
||||
|
||||
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:counterparty:import')")
|
||||
@Operation(summary = "批量导入往来单位", description = "使用标准 XLSX 模板整批校验并新增往来单位")
|
||||
public ApiResponse<PartnerImportResult> importPartners(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @Min(1) long tenantId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
requirePlatform(user);
|
||||
try {
|
||||
return ApiResponse.success(importService.importFile(tenantId, file.getOriginalFilename(), file.getBytes()));
|
||||
} catch (IOException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "导入文件读取失败");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
||||
@@ -8,6 +8,8 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 平台基础资料请求契约。 */
|
||||
public final class MasterDataQueries {
|
||||
private MasterDataQueries() {
|
||||
@@ -63,8 +65,8 @@ public final class MasterDataQueries {
|
||||
@NotBlank @Size(max = 50) String partnerCode,
|
||||
@Schema(description = "往来单位名称", example = "深圳示例贸易有限公司")
|
||||
@NotBlank @Size(max = 128) String partnerName,
|
||||
@Schema(description = "往来单位类型", allowableValues = {"CUSTOMER", "SUPPLIER", "BOTH"})
|
||||
@NotBlank @Pattern(regexp = "^(CUSTOMER|SUPPLIER|BOTH)$") String partnerType,
|
||||
@Schema(description = "兼容旧客户端的单值类型", allowableValues = {"CUSTOMER", "SUPPLIER", "BOTH", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
|
||||
@Pattern(regexp = "^(CUSTOMER|SUPPLIER|BOTH|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String partnerType,
|
||||
@Schema(description = "联系人", example = "张三")
|
||||
@NotBlank @Size(max = 64) String contactName,
|
||||
@Schema(description = "联系电话", example = "13800138000")
|
||||
@@ -84,7 +86,24 @@ public final class MasterDataQueries {
|
||||
@Schema(description = "邮政编码", example = "518000")
|
||||
@Size(max = 20) String postalCode,
|
||||
@Schema(description = "状态", allowableValues = {"ACTIVE", "INACTIVE"})
|
||||
@NotBlank @Pattern(regexp = "^(ACTIVE|INACTIVE)$") String status
|
||||
@NotBlank @Pattern(regexp = "^(ACTIVE|INACTIVE)$") String status,
|
||||
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
|
||||
@Size(max = 6) List<@Pattern(regexp = "^(CUSTOMER|SUPPLIER|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String> types,
|
||||
@Schema(description = "备注") @Size(max = 500) String remark
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "PartnerImportError", description = "往来单位导入错误")
|
||||
public record PartnerImportError(int row, String field, String message) {
|
||||
}
|
||||
|
||||
@Schema(name = "PartnerImportResult", description = "往来单位批量导入结果")
|
||||
public record PartnerImportResult(
|
||||
String fileName,
|
||||
int totalRows,
|
||||
int importedRows,
|
||||
boolean success,
|
||||
List<PartnerImportError> errors
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -107,6 +126,24 @@ public final class MasterDataQueries {
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "ExpressCompanyImportError", description = "快递公司导入错误")
|
||||
public record ExpressCompanyImportError(
|
||||
@Schema(description = "数据行号,表头后的第一行数据为 4") int row,
|
||||
@Schema(description = "字段名称") String field,
|
||||
@Schema(description = "错误原因") String message
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "ExpressCompanyImportResult", description = "快递公司批量导入结果")
|
||||
public record ExpressCompanyImportResult(
|
||||
@Schema(description = "原始文件名") String fileName,
|
||||
@Schema(description = "数据行总数") int totalRows,
|
||||
@Schema(description = "成功写入行数") int importedRows,
|
||||
@Schema(description = "是否成功") boolean success,
|
||||
@Schema(description = "逐行错误") List<ExpressCompanyImportError> errors
|
||||
) {
|
||||
}
|
||||
|
||||
@Schema(name = "MasterDataStatusRequest", description = "基础资料状态变更请求")
|
||||
public record StatusRequest(
|
||||
@Schema(description = "目标状态", allowableValues = {"ACTIVE", "INACTIVE"})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.warehouse.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
@@ -8,6 +9,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 仓储域内的租户往来单位配置。 */
|
||||
@Schema(name = "Partner", description = "租户维度往来单位主数据")
|
||||
@Getter @Setter
|
||||
@@ -17,12 +20,17 @@ public class PartnerEntity extends BaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
@Schema(description = "所属租户 ID", example = "10001")
|
||||
private Long tenantId;
|
||||
@Schema(description = "所属业务数据范围 ID,由服务端维护", example = "10001")
|
||||
private Long dataScopeId;
|
||||
@Schema(description = "租户内唯一往来单位编码", example = "CUS-001")
|
||||
private String partnerCode;
|
||||
@Schema(description = "往来单位名称", example = "深圳示例贸易有限公司")
|
||||
private String partnerName;
|
||||
@Schema(description = "类型", example = "CUSTOMER", allowableValues = {"CUSTOMER", "SUPPLIER", "BOTH"})
|
||||
private String partnerType;
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
|
||||
private List<String> types;
|
||||
@Schema(description = "联系人")
|
||||
private String contactName;
|
||||
@Schema(description = "联系电话")
|
||||
@@ -43,4 +51,6 @@ public class PartnerEntity extends BaseEntity {
|
||||
private String postalCode;
|
||||
@Schema(description = "状态", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE"})
|
||||
private String status;
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.warehouse.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/** 往来单位与稳定业务类型的关联。 */
|
||||
@Getter
|
||||
@Setter
|
||||
public class PartnerTypeEntity {
|
||||
private Long partnerId;
|
||||
private String partnerType;
|
||||
}
|
||||
@@ -1,4 +1,42 @@
|
||||
package com.baiyue.warehouse.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.PartnerEntity;
|
||||
public interface PartnerMapper extends BaseMapper<PartnerEntity> { }
|
||||
import com.baiyue.warehouse.entity.PartnerTypeEntity;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface PartnerMapper extends BaseMapper<PartnerEntity> {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT partner_id AS partnerId, partner_type AS partnerType
|
||||
FROM wh_partner_type
|
||||
WHERE partner_id IN
|
||||
<foreach collection="partnerIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY partner_id, partner_type
|
||||
</script>
|
||||
""")
|
||||
List<PartnerTypeEntity> selectTypes(@Param("partnerIds") Collection<Long> partnerIds);
|
||||
|
||||
@Select("SELECT partner_id FROM wh_partner_type WHERE tenant_id = #{tenantId} AND partner_type = #{type}")
|
||||
List<Long> selectPartnerIdsByType(@Param("tenantId") long tenantId, @Param("type") String type);
|
||||
|
||||
@Delete("DELETE FROM wh_partner_type WHERE partner_id = #{partnerId}")
|
||||
void deleteTypes(@Param("partnerId") long partnerId);
|
||||
|
||||
@Insert("""
|
||||
<script>
|
||||
INSERT INTO wh_partner_type(partner_id, tenant_id, data_scope_id, partner_type)
|
||||
VALUES
|
||||
<foreach collection="types" item="type" separator=",">
|
||||
(#{partnerId}, #{tenantId}, #{dataScopeId}, #{type})
|
||||
</foreach>
|
||||
</script>
|
||||
""")
|
||||
void insertTypes(@Param("partnerId") long partnerId, @Param("tenantId") long tenantId,
|
||||
@Param("dataScopeId") long dataScopeId, @Param("types") Collection<String> types);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ public class ExpressCompanyRepository {
|
||||
.ne(excludedId != null, ExpressCompanyEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
|
||||
public boolean existsName(long tenantId, String name, Long excludedId) {
|
||||
return mapper.selectCount(new LambdaQueryWrapper<ExpressCompanyEntity>()
|
||||
.eq(ExpressCompanyEntity::getTenantId, tenantId)
|
||||
.eq(ExpressCompanyEntity::getCompanyName, name)
|
||||
.ne(excludedId != null, ExpressCompanyEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
|
||||
public void insert(ExpressCompanyEntity entity) {
|
||||
mapper.insert(entity);
|
||||
}
|
||||
|
||||
@@ -9,29 +9,46 @@ import com.baiyue.warehouse.mapper.PartnerMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class PartnerRepository {
|
||||
private final PartnerMapper mapper;
|
||||
public PartnerRepository(PartnerMapper mapper) { this.mapper = mapper; }
|
||||
public List<PartnerEntity> activeCandidates(long tenantId, String keyword, String type) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<PartnerEntity>()
|
||||
List<Long> typedIds = StringUtils.hasText(type)
|
||||
? typedIds(tenantId, type)
|
||||
: java.util.stream.Stream.concat(
|
||||
typedIds(tenantId, "SHIPPER").stream(), typedIds(tenantId, "CONSIGNEE").stream())
|
||||
.distinct().toList();
|
||||
if (typedIds.isEmpty()) return List.of();
|
||||
List<PartnerEntity> records = mapper.selectList(new LambdaQueryWrapper<PartnerEntity>()
|
||||
.eq(PartnerEntity::getTenantId, tenantId).eq(PartnerEntity::getStatus, "ACTIVE")
|
||||
.eq(StringUtils.hasText(type), PartnerEntity::getPartnerType, type)
|
||||
.in(PartnerEntity::getId, typedIds)
|
||||
.and(StringUtils.hasText(keyword), q -> q.like(PartnerEntity::getPartnerCode, keyword)
|
||||
.or().like(PartnerEntity::getPartnerName, keyword).or().like(PartnerEntity::getContactName, keyword))
|
||||
.orderByAsc(PartnerEntity::getPartnerName));
|
||||
hydrateTypes(records);
|
||||
return records;
|
||||
}
|
||||
public PartnerEntity findActive(long tenantId, long id) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<PartnerEntity>()
|
||||
PartnerEntity entity = mapper.selectOne(new LambdaQueryWrapper<PartnerEntity>()
|
||||
.eq(PartnerEntity::getTenantId, tenantId).eq(PartnerEntity::getId, id)
|
||||
.eq(PartnerEntity::getStatus, "ACTIVE"));
|
||||
hydrateTypes(entity == null ? List.of() : List.of(entity));
|
||||
return entity;
|
||||
}
|
||||
|
||||
public PageResult<PartnerEntity> page(long tenantId, PartnerPageQuery request) {
|
||||
List<Long> typedIds = typedIds(tenantId, request.type());
|
||||
if (StringUtils.hasText(request.type()) && typedIds.isEmpty()) {
|
||||
return new PageResult<>(List.of(), request.page(), request.size(), 0, 0);
|
||||
}
|
||||
LambdaQueryWrapper<PartnerEntity> wrapper = new LambdaQueryWrapper<PartnerEntity>()
|
||||
.eq(PartnerEntity::getTenantId, tenantId)
|
||||
.eq(StringUtils.hasText(request.type()), PartnerEntity::getPartnerType, request.type())
|
||||
.in(StringUtils.hasText(request.type()), PartnerEntity::getId, typedIds)
|
||||
.eq(StringUtils.hasText(request.status()), PartnerEntity::getStatus, request.status())
|
||||
.and(StringUtils.hasText(request.keyword()), query -> query
|
||||
.like(PartnerEntity::getPartnerCode, request.keyword())
|
||||
@@ -40,13 +57,17 @@ public class PartnerRepository {
|
||||
.or().like(PartnerEntity::getContactPhone, request.keyword()))
|
||||
.orderByAsc(PartnerEntity::getPartnerName)
|
||||
.orderByAsc(PartnerEntity::getId);
|
||||
return PageResult.from(mapper.selectPage(new Page<>(request.page(), request.size()), wrapper));
|
||||
PageResult<PartnerEntity> result = PageResult.from(mapper.selectPage(new Page<>(request.page(), request.size()), wrapper));
|
||||
hydrateTypes(result.records());
|
||||
return result;
|
||||
}
|
||||
|
||||
public PartnerEntity findOwned(long tenantId, long id) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<PartnerEntity>()
|
||||
PartnerEntity entity = mapper.selectOne(new LambdaQueryWrapper<PartnerEntity>()
|
||||
.eq(PartnerEntity::getTenantId, tenantId)
|
||||
.eq(PartnerEntity::getId, id));
|
||||
hydrateTypes(entity == null ? List.of() : List.of(entity));
|
||||
return entity;
|
||||
}
|
||||
|
||||
public boolean existsCode(long tenantId, String code, Long excludedId) {
|
||||
@@ -58,9 +79,40 @@ public class PartnerRepository {
|
||||
|
||||
public void insert(PartnerEntity entity) {
|
||||
mapper.insert(entity);
|
||||
replaceTypes(entity);
|
||||
}
|
||||
|
||||
public void save(PartnerEntity entity) {
|
||||
mapper.updateById(entity);
|
||||
replaceTypes(entity);
|
||||
}
|
||||
|
||||
private List<Long> typedIds(long tenantId, String type) {
|
||||
return StringUtils.hasText(type) ? mapper.selectPartnerIdsByType(tenantId, type) : List.of();
|
||||
}
|
||||
|
||||
private void replaceTypes(PartnerEntity entity) {
|
||||
mapper.deleteTypes(entity.getId());
|
||||
Collection<String> types = entity.getTypes();
|
||||
if (types != null && !types.isEmpty()) {
|
||||
mapper.insertTypes(entity.getId(), entity.getTenantId(), entity.getDataScopeId(), types);
|
||||
}
|
||||
}
|
||||
|
||||
private void hydrateTypes(List<PartnerEntity> records) {
|
||||
if (records.isEmpty()) return;
|
||||
Map<Long, List<String>> grouped = new LinkedHashMap<>();
|
||||
mapper.selectTypes(records.stream().map(PartnerEntity::getId).toList())
|
||||
.forEach(item -> grouped.computeIfAbsent(item.getPartnerId(), ignored -> new java.util.ArrayList<>())
|
||||
.add(item.getPartnerType()));
|
||||
records.forEach(record -> {
|
||||
List<String> types = grouped.get(record.getId());
|
||||
record.setTypes(types == null || types.isEmpty() ? legacyTypes(record.getPartnerType()) : List.copyOf(types));
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String> legacyTypes(String type) {
|
||||
if ("BOTH".equals(type)) return List.of("CUSTOMER", "SUPPLIER");
|
||||
return StringUtils.hasText(type) ? List.of(type) : List.of();
|
||||
}
|
||||
}
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyImportError;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyImportResult;
|
||||
import com.baiyue.warehouse.entity.ExpressCompanyEntity;
|
||||
import com.baiyue.warehouse.repository.ExpressCompanyRepository;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
/** 标准模板和快递公司批量导入。 */
|
||||
@Service
|
||||
public class ExpressCompanyImportService {
|
||||
private static final int MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
private static final int MAX_ROWS = 1000;
|
||||
private static final String TEMPLATE_VERSION = "1";
|
||||
private static final String[] HEADERS = {
|
||||
"快递公司编码*", "快递公司名称*", "客服电话", "官方网站", "轨迹查询地址模板", "状态*", "备注"
|
||||
};
|
||||
|
||||
private final ExpressCompanyRepository repository;
|
||||
|
||||
public ExpressCompanyImportService(ExpressCompanyRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public byte[] template() {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
XSSFSheet sheet = workbook.createSheet("快递公司");
|
||||
Row version = sheet.createRow(0);
|
||||
version.createCell(0).setCellValue("模板版本");
|
||||
version.createCell(1).setCellValue(TEMPLATE_VERSION);
|
||||
Row instruction = sheet.createRow(1);
|
||||
instruction.createCell(0).setCellValue("说明");
|
||||
instruction.createCell(1).setCellValue("仅新增;编码、名称、状态必填;状态填写 ACTIVE 或 INACTIVE");
|
||||
Row header = sheet.createRow(2);
|
||||
for (int i = 0; i < HEADERS.length; i++) header.createCell(i).setCellValue(HEADERS[i]);
|
||||
for (int i = 0; i < HEADERS.length; i++) sheet.autoSizeColumn(i);
|
||||
workbook.write(output);
|
||||
return output.toByteArray();
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("生成快递公司导入模板失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "EXPRESS_COMPANY_IMPORT", resourceType = "EXPRESS_COMPANY_IMPORT", description = "批量导入快递公司", captureBefore = false)
|
||||
public ExpressCompanyImportResult importFile(long tenantId, String fileName, byte[] content) {
|
||||
String safeFileName = fileName == null || fileName.isBlank() ? "未命名文件.xlsx" : fileName;
|
||||
if (content == null || content.length == 0) {
|
||||
return failure(safeFileName, 0, new ExpressCompanyImportError(0, "文件", "文件不能为空"));
|
||||
}
|
||||
if (content.length > MAX_FILE_BYTES) {
|
||||
return failure(safeFileName, 0, new ExpressCompanyImportError(0, "文件", "文件不能超过 5MB"));
|
||||
}
|
||||
if (!safeFileName.toLowerCase(Locale.ROOT).endsWith(".xlsx")) {
|
||||
return failure(safeFileName, 0, new ExpressCompanyImportError(0, "文件", "只支持 .xlsx 文件"));
|
||||
}
|
||||
|
||||
List<ImportRow> rows = new ArrayList<>();
|
||||
List<ExpressCompanyImportError> errors = new ArrayList<>();
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
if (workbook.getNumberOfSheets() == 0) {
|
||||
return failure(safeFileName, 0, new ExpressCompanyImportError(0, "模板", "缺少快递公司工作表"));
|
||||
}
|
||||
XSSFSheet sheet = workbook.getSheetAt(0);
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
if (!"模板版本".equals(value(sheet.getRow(0), 0, formatter))
|
||||
|| !TEMPLATE_VERSION.equals(value(sheet.getRow(0), 1, formatter))) {
|
||||
errors.add(new ExpressCompanyImportError(1, "模板版本", "模板版本不匹配,请下载最新模板"));
|
||||
}
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
if (!HEADERS[i].equals(value(sheet.getRow(2), i, formatter))) {
|
||||
errors.add(new ExpressCompanyImportError(3, "表头", "第 " + (i + 1) + " 列应为“" + HEADERS[i] + "”"));
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) return new ExpressCompanyImportResult(safeFileName, 0, 0, false, errors);
|
||||
|
||||
for (int rowIndex = 3; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (isBlank(row, formatter)) continue;
|
||||
if (rows.size() >= MAX_ROWS) {
|
||||
errors.add(new ExpressCompanyImportError(rowIndex + 1, "文件", "单次最多导入 1000 行"));
|
||||
break;
|
||||
}
|
||||
ImportRow importRow = new ImportRow(rowIndex + 1,
|
||||
value(row, 0, formatter), value(row, 1, formatter), value(row, 2, formatter),
|
||||
value(row, 3, formatter), value(row, 4, formatter), value(row, 5, formatter), value(row, 6, formatter));
|
||||
rows.add(importRow);
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return failure(safeFileName, rows.size(), new ExpressCompanyImportError(0, "文件", "文件格式无法读取,请使用标准 XLSX 模板"));
|
||||
}
|
||||
if (rows.isEmpty() && errors.isEmpty()) {
|
||||
errors.add(new ExpressCompanyImportError(0, "文件", "文件中没有可导入的数据行"));
|
||||
}
|
||||
for (ImportRow row : rows) validate(tenantId, row, rows, errors);
|
||||
if (!errors.isEmpty()) return new ExpressCompanyImportResult(safeFileName, rows.size(), 0, false, errors);
|
||||
|
||||
try {
|
||||
for (ImportRow row : rows) repository.insert(toEntity(tenantId, row));
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return failure(safeFileName, rows.size(), new ExpressCompanyImportError(0, "编码或名称", "与现有快递公司重复,整批数据未写入"));
|
||||
}
|
||||
return new ExpressCompanyImportResult(safeFileName, rows.size(), rows.size(), true, List.of());
|
||||
}
|
||||
|
||||
private void validate(long tenantId, ImportRow row, List<ImportRow> rows, List<ExpressCompanyImportError> errors) {
|
||||
if (row.code.isBlank()) errors.add(error(row, "快递公司编码", "不能为空"));
|
||||
if (row.code.length() > 50) errors.add(error(row, "快递公司编码", "不能超过 50 个字符"));
|
||||
if (row.name.isBlank()) errors.add(error(row, "快递公司名称", "不能为空"));
|
||||
if (row.name.length() > 128) errors.add(error(row, "快递公司名称", "不能超过 128 个字符"));
|
||||
if (row.phone.length() > 32) errors.add(error(row, "客服电话", "不能超过 32 个字符"));
|
||||
if (row.website.length() > 255 || !validUrl(row.website)) errors.add(error(row, "官方网站", "必须是 http:// 或 https:// 开头的网址"));
|
||||
if (row.trackingTemplate.length() > 500) errors.add(error(row, "轨迹查询地址模板", "不能超过 500 个字符"));
|
||||
if (!"ACTIVE".equals(row.status) && !"INACTIVE".equals(row.status)) errors.add(error(row, "状态", "只能填写 ACTIVE 或 INACTIVE"));
|
||||
if (row.remark.length() > 500) errors.add(error(row, "备注", "不能超过 500 个字符"));
|
||||
for (ImportRow previous : rows) {
|
||||
if (previous == row) break;
|
||||
if (!row.code.isBlank() && previous.code.equalsIgnoreCase(row.code)) {
|
||||
errors.add(error(row, "快递公司编码", "与文件内其他行重复"));
|
||||
}
|
||||
if (!row.name.isBlank() && previous.name.equalsIgnoreCase(row.name)) {
|
||||
errors.add(error(row, "快递公司名称", "与文件内其他行重复"));
|
||||
}
|
||||
}
|
||||
if (!row.code.isBlank() && repository.existsCode(tenantId, row.code, null)) errors.add(error(row, "快递公司编码", "与现有档案重复"));
|
||||
if (!row.name.isBlank() && repository.existsName(tenantId, row.name, null)) errors.add(error(row, "快递公司名称", "与现有档案重复"));
|
||||
}
|
||||
|
||||
private static ExpressCompanyEntity toEntity(long tenantId, ImportRow row) {
|
||||
ExpressCompanyEntity entity = new ExpressCompanyEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setCompanyCode(row.code);
|
||||
entity.setCompanyName(row.name);
|
||||
entity.setServicePhone(nullable(row.phone));
|
||||
entity.setWebsite(nullable(row.website));
|
||||
entity.setTrackingUrlTemplate(nullable(row.trackingTemplate));
|
||||
entity.setStatus(row.status);
|
||||
entity.setRemark(nullable(row.remark));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static String value(Row row, int index, DataFormatter formatter) {
|
||||
if (row == null) return "";
|
||||
Cell cell = row.getCell(index);
|
||||
return cell == null ? "" : formatter.formatCellValue(cell).trim();
|
||||
}
|
||||
|
||||
private static boolean isBlank(Row row, DataFormatter formatter) {
|
||||
if (row == null) return true;
|
||||
for (int i = 0; i < HEADERS.length; i++) if (!value(row, i, formatter).isBlank()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean validUrl(String value) {
|
||||
if (value.isBlank()) return true;
|
||||
try {
|
||||
URI uri = new URI(value);
|
||||
return ("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme())) && uri.getHost() != null;
|
||||
} catch (URISyntaxException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullable(String value) { return value.isBlank() ? null : value; }
|
||||
private static ExpressCompanyImportError error(ImportRow row, String field, String message) {
|
||||
return new ExpressCompanyImportError(row.row, field, message);
|
||||
}
|
||||
private static ExpressCompanyImportResult failure(String fileName, int rows, ExpressCompanyImportError error) {
|
||||
return new ExpressCompanyImportResult(fileName, rows, 0, false, List.of(error));
|
||||
}
|
||||
|
||||
private record ImportRow(int row, String code, String name, String phone, String website,
|
||||
String trackingTemplate, String status, String remark) { }
|
||||
}
|
||||
+10
-2
@@ -33,13 +33,14 @@ public class ExpressCompanyQueryService {
|
||||
@BusinessAudit(event = "EXPRESS_COMPANY_CREATE", resourceType = "EXPRESS_COMPANY", description = "新增快递公司", captureBefore = false)
|
||||
public Long create(long tenantId, ExpressCompanySaveRequest request) {
|
||||
ensureCodeUnique(tenantId, request.companyCode(), null);
|
||||
ensureNameUnique(tenantId, request.companyName(), null);
|
||||
ExpressCompanyEntity entity = new ExpressCompanyEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
copy(entity, request);
|
||||
try {
|
||||
repository.insert(entity);
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "快递公司编码已存在");
|
||||
throw new BizException(ResultCode.CONFLICT, "快递公司编码或名称已存在");
|
||||
}
|
||||
return entity.getId();
|
||||
}
|
||||
@@ -49,11 +50,12 @@ public class ExpressCompanyQueryService {
|
||||
public void update(long tenantId, long id, ExpressCompanySaveRequest request) {
|
||||
ExpressCompanyEntity entity = owned(tenantId, id);
|
||||
ensureCodeUnique(tenantId, request.companyCode(), id);
|
||||
ensureNameUnique(tenantId, request.companyName(), id);
|
||||
copy(entity, request);
|
||||
try {
|
||||
repository.save(entity);
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "快递公司编码已存在");
|
||||
throw new BizException(ResultCode.CONFLICT, "快递公司编码或名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +82,12 @@ public class ExpressCompanyQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNameUnique(long tenantId, String name, Long excludedId) {
|
||||
if (repository.existsName(tenantId, name, excludedId)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "快递公司名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireStatus(String status) {
|
||||
if (!"ACTIVE".equals(status) && !"INACTIVE".equals(status)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "状态只能设置为 ACTIVE 或 INACTIVE");
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerImportError;
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerImportResult;
|
||||
import com.baiyue.warehouse.entity.PartnerEntity;
|
||||
import com.baiyue.warehouse.repository.PartnerRepository;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** 往来单位标准模板生成与原子批量导入。 */
|
||||
@Service
|
||||
public class PartnerImportService {
|
||||
private static final int MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
private static final int MAX_ROWS = 1000;
|
||||
private static final String TEMPLATE_VERSION = "1";
|
||||
private static final Set<String> ALLOWED_TYPES = Set.of(
|
||||
"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE");
|
||||
private static final Pattern EMAIL = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
|
||||
private static final String[] HEADERS = {
|
||||
"单位编码*", "单位名称*", "单位类型*", "联系人*", "联系电话*", "联系邮箱",
|
||||
"国家二字码*", "省/州", "城市*", "区/县", "详细地址*", "邮政编码", "状态*", "备注"
|
||||
};
|
||||
|
||||
private final PartnerRepository repository;
|
||||
|
||||
public PartnerImportService(PartnerRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public byte[] template() {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
XSSFSheet sheet = workbook.createSheet("往来单位");
|
||||
Row version = sheet.createRow(0);
|
||||
version.createCell(0).setCellValue("模板版本");
|
||||
version.createCell(1).setCellValue(TEMPLATE_VERSION);
|
||||
Row instruction = sheet.createRow(1);
|
||||
instruction.createCell(0).setCellValue("说明");
|
||||
instruction.createCell(1).setCellValue(
|
||||
"仅新增;类型可用中文逗号或英文逗号分隔:CUSTOMER、SUPPLIER、CARRIER、CUSTOMS_BROKER、SHIPPER、CONSIGNEE;状态为 ACTIVE 或 INACTIVE");
|
||||
Row header = sheet.createRow(2);
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
header.createCell(i).setCellValue(HEADERS[i]);
|
||||
sheet.setColumnWidth(i, Math.min(48, Math.max(14, HEADERS[i].length() * 2 + 4)) * 256);
|
||||
}
|
||||
workbook.write(output);
|
||||
return output.toByteArray();
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("生成往来单位导入模板失败", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PARTNER_IMPORT", resourceType = "PARTNER_IMPORT", description = "批量导入往来单位", captureBefore = false)
|
||||
public PartnerImportResult importFile(long tenantId, String fileName, byte[] content) {
|
||||
String safeFileName = safeFileName(fileName);
|
||||
if (content == null || content.length == 0) return failure(safeFileName, 0, 0, "文件", "文件不能为空");
|
||||
if (content.length > MAX_FILE_BYTES) return failure(safeFileName, 0, 0, "文件", "文件不能超过 5MB");
|
||||
if (!safeFileName.toLowerCase(Locale.ROOT).endsWith(".xlsx")) {
|
||||
return failure(safeFileName, 0, 0, "文件", "只支持 .xlsx 文件");
|
||||
}
|
||||
|
||||
List<ImportRow> rows = new ArrayList<>();
|
||||
List<PartnerImportError> errors = new ArrayList<>();
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
if (workbook.getNumberOfSheets() == 0) return failure(safeFileName, 0, 0, "模板", "缺少往来单位工作表");
|
||||
XSSFSheet sheet = workbook.getSheetAt(0);
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
if (!"模板版本".equals(value(sheet.getRow(0), 0, formatter))
|
||||
|| !TEMPLATE_VERSION.equals(value(sheet.getRow(0), 1, formatter))) {
|
||||
errors.add(new PartnerImportError(1, "模板版本", "模板版本不匹配,请下载最新模板"));
|
||||
}
|
||||
for (int i = 0; i < HEADERS.length; i++) {
|
||||
if (!HEADERS[i].equals(value(sheet.getRow(2), i, formatter))) {
|
||||
errors.add(new PartnerImportError(3, "表头", "第 " + (i + 1) + " 列应为“" + HEADERS[i] + "”"));
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) return result(safeFileName, 0, 0, errors);
|
||||
|
||||
for (int index = 3; index <= sheet.getLastRowNum(); index++) {
|
||||
Row row = sheet.getRow(index);
|
||||
if (isBlank(row, formatter)) continue;
|
||||
if (rows.size() >= MAX_ROWS) {
|
||||
errors.add(new PartnerImportError(index + 1, "文件", "单次最多导入 1000 行"));
|
||||
break;
|
||||
}
|
||||
ImportRow item = new ImportRow(index + 1,
|
||||
value(row, 0, formatter), value(row, 1, formatter), parseTypes(value(row, 2, formatter)),
|
||||
value(row, 3, formatter), value(row, 4, formatter), value(row, 5, formatter),
|
||||
value(row, 6, formatter).toUpperCase(Locale.ROOT), value(row, 7, formatter),
|
||||
value(row, 8, formatter), value(row, 9, formatter), value(row, 10, formatter),
|
||||
value(row, 11, formatter), value(row, 12, formatter).toUpperCase(Locale.ROOT), value(row, 13, formatter));
|
||||
validate(tenantId, item, rows, errors);
|
||||
rows.add(item);
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return failure(safeFileName, rows.size(), 0, "文件", "文件格式无法读取,请使用标准 XLSX 模板");
|
||||
}
|
||||
if (rows.isEmpty() && errors.isEmpty()) errors.add(new PartnerImportError(0, "文件", "文件中没有可导入的数据行"));
|
||||
if (!errors.isEmpty()) return result(safeFileName, rows.size(), 0, errors);
|
||||
|
||||
try {
|
||||
for (ImportRow row : rows) repository.insert(toEntity(tenantId, row));
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return failure(safeFileName, rows.size(), 0, "单位编码", "与现有档案重复,整批数据未写入");
|
||||
}
|
||||
return new PartnerImportResult(safeFileName, rows.size(), rows.size(), true, List.of());
|
||||
}
|
||||
|
||||
private void validate(long tenantId, ImportRow row, List<ImportRow> previousRows, List<PartnerImportError> errors) {
|
||||
required(row, errors, "单位编码", row.code, 50);
|
||||
required(row, errors, "单位名称", row.name, 128);
|
||||
if (row.types.isEmpty()) errors.add(error(row, "单位类型", "至少填写一个类型"));
|
||||
row.types.stream().filter(type -> !ALLOWED_TYPES.contains(type))
|
||||
.forEach(type -> errors.add(error(row, "单位类型", "不支持类型 " + type)));
|
||||
required(row, errors, "联系人", row.contactName, 64);
|
||||
required(row, errors, "联系电话", row.contactPhone, 32);
|
||||
if (row.email.length() > 128 || (!row.email.isBlank() && !EMAIL.matcher(row.email).matches())) {
|
||||
errors.add(error(row, "联系邮箱", "请输入有效的邮箱地址,且不超过 128 个字符"));
|
||||
}
|
||||
if (!row.countryCode.matches("^[A-Z]{2}$")) errors.add(error(row, "国家二字码", "必须是两个英文字母"));
|
||||
optionalLength(row, errors, "省/州", row.province, 64);
|
||||
required(row, errors, "城市", row.city, 64);
|
||||
optionalLength(row, errors, "区/县", row.district, 64);
|
||||
required(row, errors, "详细地址", row.address, 255);
|
||||
optionalLength(row, errors, "邮政编码", row.postalCode, 20);
|
||||
if (!"ACTIVE".equals(row.status) && !"INACTIVE".equals(row.status)) errors.add(error(row, "状态", "只能填写 ACTIVE 或 INACTIVE"));
|
||||
optionalLength(row, errors, "备注", row.remark, 500);
|
||||
if (previousRows.stream().anyMatch(previous -> previous.code.equalsIgnoreCase(row.code))) {
|
||||
errors.add(error(row, "单位编码", "与文件内其他行重复"));
|
||||
}
|
||||
if (!row.code.isBlank() && repository.existsCode(tenantId, row.code, null)) {
|
||||
errors.add(error(row, "单位编码", "与现有档案重复"));
|
||||
}
|
||||
}
|
||||
|
||||
private static PartnerEntity toEntity(long tenantId, ImportRow row) {
|
||||
PartnerEntity entity = new PartnerEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setDataScopeId(tenantId);
|
||||
entity.setPartnerCode(row.code);
|
||||
entity.setPartnerName(row.name);
|
||||
entity.setTypes(row.types);
|
||||
entity.setPartnerType(row.types.contains("CUSTOMER") && row.types.contains("SUPPLIER") ? "BOTH" : row.types.get(0));
|
||||
entity.setContactName(row.contactName);
|
||||
entity.setContactPhone(row.contactPhone);
|
||||
entity.setContactEmail(nullable(row.email));
|
||||
entity.setCountryCode(row.countryCode);
|
||||
entity.setProvince(nullable(row.province));
|
||||
entity.setCity(row.city);
|
||||
entity.setDistrict(nullable(row.district));
|
||||
entity.setAddressLine(row.address);
|
||||
entity.setPostalCode(nullable(row.postalCode));
|
||||
entity.setStatus(row.status);
|
||||
entity.setRemark(nullable(row.remark));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static List<String> parseTypes(String value) {
|
||||
LinkedHashSet<String> types = new LinkedHashSet<>();
|
||||
for (String type : value.toUpperCase(Locale.ROOT).split("[,,、]")) if (!type.isBlank()) types.add(type.trim());
|
||||
return List.copyOf(types);
|
||||
}
|
||||
private static void required(ImportRow row, List<PartnerImportError> errors, String field, String value, int max) {
|
||||
if (value.isBlank()) errors.add(error(row, field, "不能为空"));
|
||||
else if (value.length() > max) errors.add(error(row, field, "不能超过 " + max + " 个字符"));
|
||||
}
|
||||
private static void optionalLength(ImportRow row, List<PartnerImportError> errors, String field, String value, int max) {
|
||||
if (value.length() > max) errors.add(error(row, field, "不能超过 " + max + " 个字符"));
|
||||
}
|
||||
private static String value(Row row, int index, DataFormatter formatter) {
|
||||
if (row == null) return "";
|
||||
Cell cell = row.getCell(index);
|
||||
return cell == null ? "" : formatter.formatCellValue(cell).trim();
|
||||
}
|
||||
private static boolean isBlank(Row row, DataFormatter formatter) {
|
||||
if (row == null) return true;
|
||||
for (int i = 0; i < HEADERS.length; i++) if (!value(row, i, formatter).isBlank()) return false;
|
||||
return true;
|
||||
}
|
||||
private static String nullable(String value) { return value.isBlank() ? null : value; }
|
||||
private static String safeFileName(String value) {
|
||||
if (value == null || value.isBlank()) return "未命名文件.xlsx";
|
||||
String normalized = value.replace('\\', '/');
|
||||
return normalized.substring(normalized.lastIndexOf('/') + 1);
|
||||
}
|
||||
private static PartnerImportError error(ImportRow row, String field, String message) {
|
||||
return new PartnerImportError(row.row, field, message);
|
||||
}
|
||||
private static PartnerImportResult failure(String fileName, int total, int imported, String field, String message) {
|
||||
return result(fileName, total, imported, List.of(new PartnerImportError(0, field, message)));
|
||||
}
|
||||
private static PartnerImportResult result(String fileName, int total, int imported, List<PartnerImportError> errors) {
|
||||
return new PartnerImportResult(fileName, total, imported, errors.isEmpty(), errors);
|
||||
}
|
||||
|
||||
private record ImportRow(int row, String code, String name, List<String> types, String contactName,
|
||||
String contactPhone, String email, String countryCode, String province, String city,
|
||||
String district, String address, String postalCode, String status, String remark) { }
|
||||
}
|
||||
@@ -12,6 +12,9 @@ public class PartnerQueryService {
|
||||
private final PartnerRepository repository;
|
||||
public PartnerQueryService(PartnerRepository repository) { this.repository = repository; }
|
||||
public List<PartnerEntity> candidates(long tenantId, String keyword, String type) {
|
||||
if (type != null && !type.isBlank() && !"SHIPPER".equals(type) && !"CONSIGNEE".equals(type)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "候选类型只能是 SHIPPER 或 CONSIGNEE");
|
||||
}
|
||||
return repository.activeCandidates(tenantId, keyword, type);
|
||||
}
|
||||
public PartnerEntity active(long tenantId, long id) {
|
||||
|
||||
+29
-1
@@ -12,6 +12,10 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** 平台往来单位管理服务。 */
|
||||
@Service
|
||||
public class PlatformPartnerQueryService {
|
||||
@@ -35,6 +39,7 @@ public class PlatformPartnerQueryService {
|
||||
ensureCodeUnique(tenantId, request.partnerCode(), null);
|
||||
PartnerEntity entity = new PartnerEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setDataScopeId(tenantId);
|
||||
copy(entity, request);
|
||||
try {
|
||||
repository.insert(entity);
|
||||
@@ -87,9 +92,11 @@ public class PlatformPartnerQueryService {
|
||||
}
|
||||
|
||||
private static void copy(PartnerEntity entity, PartnerSaveRequest request) {
|
||||
List<String> types = normalizeTypes(request.types(), request.partnerType());
|
||||
entity.setPartnerCode(request.partnerCode());
|
||||
entity.setPartnerName(request.partnerName());
|
||||
entity.setPartnerType(request.partnerType());
|
||||
entity.setTypes(types);
|
||||
entity.setPartnerType(legacyType(types));
|
||||
entity.setContactName(request.contactName());
|
||||
entity.setContactPhone(request.contactPhone());
|
||||
entity.setContactEmail(request.contactEmail());
|
||||
@@ -100,5 +107,26 @@ public class PlatformPartnerQueryService {
|
||||
entity.setAddressLine(request.addressLine());
|
||||
entity.setPostalCode(request.postalCode());
|
||||
entity.setStatus(request.status());
|
||||
entity.setRemark(request.remark());
|
||||
}
|
||||
|
||||
static List<String> normalizeTypes(List<String> requested, String legacyType) {
|
||||
Set<String> normalized = new LinkedHashSet<>();
|
||||
if (requested != null) requested.forEach(type -> {
|
||||
if (type != null && !type.isBlank()) normalized.add(type);
|
||||
});
|
||||
if (normalized.isEmpty() && "BOTH".equals(legacyType)) {
|
||||
normalized.add("CUSTOMER");
|
||||
normalized.add("SUPPLIER");
|
||||
} else if (normalized.isEmpty() && legacyType != null && !legacyType.isBlank()) {
|
||||
normalized.add(legacyType);
|
||||
}
|
||||
if (normalized.isEmpty()) throw new BizException(ResultCode.BAD_REQUEST, "请至少选择一个单位类型");
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static String legacyType(List<String> types) {
|
||||
if (types.contains("CUSTOMER") && types.contains("SUPPLIER")) return "BOTH";
|
||||
return types.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -16,7 +16,8 @@ public class WarehouseSchemaReadinessService {
|
||||
"SELECT id, tenant_id, warehouse_code, warehouse_name, warehouse_type, country_code, timezone, status, deleted FROM wh_warehouse LIMIT 0",
|
||||
"SELECT id, tenant_id, warehouse_id, address_type, country_code, city, address_line1, deleted FROM wh_warehouse_address LIMIT 0",
|
||||
"SELECT id, tenant_id, warehouse_id, capability_code, capability_name, enabled, deleted FROM wh_service_capability LIMIT 0",
|
||||
"SELECT id, tenant_id, partner_code, partner_name, partner_type, status, deleted FROM wh_partner LIMIT 0",
|
||||
"SELECT id, tenant_id, data_scope_id, partner_code, partner_name, partner_type, status, remark, deleted FROM wh_partner LIMIT 0",
|
||||
"SELECT partner_id, tenant_id, data_scope_id, partner_type FROM wh_partner_type LIMIT 0",
|
||||
"SELECT id, tenant_id, company_code, company_name, status, deleted FROM wh_express_company LIMIT 0"
|
||||
);
|
||||
|
||||
|
||||
@@ -145,3 +145,6 @@ PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = @schema_name AND table_name = 'wh_express_company' AND index_name = 'uk_wh_express_tenant_code') = 0,
|
||||
'ALTER TABLE wh_express_company ADD UNIQUE KEY uk_wh_express_tenant_code (tenant_id, company_code, deleted)', 'SELECT 1');
|
||||
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = @schema_name AND table_name = 'wh_express_company' AND index_name = 'uk_wh_express_tenant_name') = 0,
|
||||
'ALTER TABLE wh_express_company ADD UNIQUE KEY uk_wh_express_tenant_name (tenant_id, company_name, deleted)', 'SELECT 1');
|
||||
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
@@ -19,5 +19,6 @@ CREATE TABLE IF NOT EXISTS wh_express_company (
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_wh_express_tenant_code (tenant_id, company_code, deleted),
|
||||
UNIQUE KEY uk_wh_express_tenant_name (tenant_id, company_name, deleted),
|
||||
KEY idx_wh_express_tenant_status (tenant_id, status, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户维度快递公司主数据';
|
||||
|
||||
@@ -2,19 +2,30 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_warehouse;
|
||||
CREATE TABLE IF NOT EXISTS wh_partner (
|
||||
id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围',
|
||||
partner_code VARCHAR(50) NOT NULL, partner_name VARCHAR(128) NOT NULL,
|
||||
partner_type VARCHAR(20) NOT NULL COMMENT 'CUSTOMER、SUPPLIER或BOTH',
|
||||
contact_name VARCHAR(64) NOT NULL, contact_phone VARCHAR(32) NOT NULL,
|
||||
contact_email VARCHAR(128) NULL, country_code CHAR(2) NOT NULL,
|
||||
province VARCHAR(64) NULL, city VARCHAR(64) NOT NULL, district VARCHAR(64) NULL,
|
||||
address_line VARCHAR(255) NOT NULL, postal_code VARCHAR(20) NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', created_by BIGINT UNSIGNED NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', remark VARCHAR(500) NULL,
|
||||
created_by BIGINT UNSIGNED NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_by BIGINT UNSIGNED NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted),
|
||||
KEY idx_wh_partner_tenant_status(tenant_id,status,deleted)
|
||||
) ENGINE=InnoDB COMMENT='仓储域内的租户往来单位配置';
|
||||
INSERT INTO wh_partner(id,tenant_id,partner_code,partner_name,partner_type,contact_name,contact_phone,contact_email,country_code,province,city,district,address_line,postal_code,status)
|
||||
VALUES(191000000000000001,10001,'CUS-001','深圳示例贸易有限公司','CUSTOMER','张三','13800138000','contact@example.com','CN','广东省','深圳市','宝安区','航城街道示例路1号','518000','ACTIVE')
|
||||
CREATE TABLE IF NOT EXISTS wh_partner_type (
|
||||
partner_id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
data_scope_id BIGINT UNSIGNED NOT NULL, partner_type VARCHAR(32) NOT NULL,
|
||||
PRIMARY KEY(partner_id,partner_type),
|
||||
KEY idx_wh_partner_type_scope(tenant_id,data_scope_id,partner_type,partner_id)
|
||||
) ENGINE=InnoDB COMMENT='往来单位稳定业务类型';
|
||||
INSERT INTO wh_partner(id,tenant_id,data_scope_id,partner_code,partner_name,partner_type,contact_name,contact_phone,contact_email,country_code,province,city,district,address_line,postal_code,status)
|
||||
VALUES(191000000000000001,10001,10001,'CUS-001','深圳示例贸易有限公司','CUSTOMER','张三','13800138000','contact@example.com','CN','广东省','深圳市','宝安区','航城街道示例路1号','518000','ACTIVE')
|
||||
ON DUPLICATE KEY UPDATE partner_name=VALUES(partner_name),status='ACTIVE';
|
||||
INSERT INTO wh_partner_type(partner_id,tenant_id,data_scope_id,partner_type)
|
||||
VALUES(191000000000000001,10001,10001,'CUSTOMER')
|
||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id),data_scope_id=VALUES(data_scope_id);
|
||||
|
||||
+4
@@ -24,6 +24,8 @@ class PlatformMasterDataControllerContractTest {
|
||||
assertMethod(PlatformPartnerController.class, "create", PostMapping.class, "platform:counterparty:create");
|
||||
assertMethod(PlatformPartnerController.class, "update", PutMapping.class, "platform:counterparty:edit");
|
||||
assertMethod(PlatformPartnerController.class, "changeStatus", PatchMapping.class, "platform:counterparty:status");
|
||||
assertMethod(PlatformPartnerController.class, "importTemplate", GetMapping.class, "platform:counterparty:import");
|
||||
assertMethod(PlatformPartnerController.class, "importPartners", PostMapping.class, "platform:counterparty:import");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -34,6 +36,8 @@ class PlatformMasterDataControllerContractTest {
|
||||
assertMethod(PlatformExpressCompanyController.class, "create", PostMapping.class, "platform:express:create");
|
||||
assertMethod(PlatformExpressCompanyController.class, "update", PutMapping.class, "platform:express:edit");
|
||||
assertMethod(PlatformExpressCompanyController.class, "changeStatus", PatchMapping.class, "platform:express:status");
|
||||
assertMethod(PlatformExpressCompanyController.class, "importTemplate", GetMapping.class, "platform:express:import");
|
||||
assertMethod(PlatformExpressCompanyController.class, "importCompanies", PostMapping.class, "platform:express:import");
|
||||
}
|
||||
|
||||
private static void assertContract(Class<?> controller, String authority, String path) throws Exception {
|
||||
|
||||
@@ -26,10 +26,28 @@ class PartnerRepositoryTest {
|
||||
@DisplayName("候选查询强制租户和启用状态")
|
||||
void shouldEnforceTenantAndActiveStatusForCandidateQuery() {
|
||||
PartnerMapper mapper=mock(PartnerMapper.class); when(mapper.selectList(any())).thenReturn(List.of());
|
||||
new PartnerRepository(mapper).activeCandidates(10001,"示例","CUSTOMER");
|
||||
when(mapper.selectPartnerIdsByType(10001,"SHIPPER")).thenReturn(List.of(1L));
|
||||
new PartnerRepository(mapper).activeCandidates(10001,"示例","SHIPPER");
|
||||
@SuppressWarnings("unchecked") ArgumentCaptor<LambdaQueryWrapper<PartnerEntity>> c=ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(mapper).selectList(c.capture()); c.getValue().getSqlSegment();
|
||||
assertThat(c.getValue().getParamNameValuePairs().values()).contains(10001L,"ACTIVE","CUSTOMER");
|
||||
assertThat(c.getValue().getParamNameValuePairs().values()).contains(10001L,"ACTIVE",1L);
|
||||
verify(mapper).selectPartnerIdsByType(10001L, "SHIPPER");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("默认候选只包含发货单位和收货单位")
|
||||
void shouldLimitDefaultCandidatesToOrderPartyTypes() {
|
||||
PartnerMapper mapper=mock(PartnerMapper.class);
|
||||
when(mapper.selectPartnerIdsByType(10001L,"SHIPPER")).thenReturn(List.of(1L));
|
||||
when(mapper.selectPartnerIdsByType(10001L,"CONSIGNEE")).thenReturn(List.of(2L,1L));
|
||||
when(mapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
new PartnerRepository(mapper).activeCandidates(10001L,null,null);
|
||||
|
||||
@SuppressWarnings("unchecked") ArgumentCaptor<LambdaQueryWrapper<PartnerEntity>> wrapper=ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(mapper).selectList(wrapper.capture());
|
||||
wrapper.getValue().getSqlSegment();
|
||||
assertThat(wrapper.getValue().getParamNameValuePairs().values()).contains(10001L,"ACTIVE",1L,2L);
|
||||
}
|
||||
@Test
|
||||
@DisplayName("详情查询强制租户和启用状态")
|
||||
@@ -45,6 +63,7 @@ class PartnerRepositoryTest {
|
||||
@DisplayName("平台分页强制租户并支持筛选")
|
||||
void shouldEnforceTenantAndSupportFilteringForPlatformPage() {
|
||||
PartnerMapper mapper=mock(PartnerMapper.class);
|
||||
when(mapper.selectPartnerIdsByType(10001L, "CUSTOMER")).thenReturn(List.of(9L));
|
||||
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
new PartnerRepository(mapper).page(10001L,
|
||||
@@ -55,6 +74,6 @@ class PartnerRepositoryTest {
|
||||
verify(mapper).selectPage(any(Page.class), wrapper.capture());
|
||||
wrapper.getValue().getSqlSegment();
|
||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||
.contains(10001L, "CUSTOMER", "ACTIVE", "%示例%");
|
||||
.contains(10001L, 9L, "ACTIVE", "%示例%");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -28,9 +28,11 @@ class WarehouseSchemaContractTest {
|
||||
@DisplayName("往来单位表包含实体全部持久化字段")
|
||||
void shouldContainAllPersistentFieldsInPartnerTable() throws IOException {
|
||||
String sql = read("warehouse-partner.sql");
|
||||
assertColumns(sql, "wh_partner", "partner_code", "partner_name", "partner_type", "contact_name",
|
||||
assertColumns(sql, "wh_partner", "data_scope_id", "partner_code", "partner_name", "partner_type", "contact_name",
|
||||
"contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line",
|
||||
"postal_code", "status");
|
||||
"postal_code", "status", "remark");
|
||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type")
|
||||
.contains("idx_wh_partner_type_scope");
|
||||
assertBaseColumns(sql);
|
||||
}
|
||||
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.warehouse.entity.ExpressCompanyEntity;
|
||||
import com.baiyue.warehouse.repository.ExpressCompanyRepository;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@DisplayName("快递公司批量导入")
|
||||
class ExpressCompanyImportServiceTest {
|
||||
@Test
|
||||
@DisplayName("标准模板通过整批校验后写入全部数据")
|
||||
void shouldImportValidWorkbookAfterFullValidation() throws Exception {
|
||||
ExpressCompanyRepository repository = mock(ExpressCompanyRepository.class);
|
||||
ExpressCompanyImportService service = new ExpressCompanyImportService(repository);
|
||||
byte[] workbook = withRows(service.template(), new String[][] {
|
||||
{"SF", "顺丰速运", "95338", "https://www.sf-express.com", "https://example.com/{trackingNo}", "ACTIVE", "国内件"}
|
||||
});
|
||||
|
||||
var result = service.importFile(10001L, "express.xlsx", workbook);
|
||||
|
||||
assertThat(result.success()).isTrue();
|
||||
assertThat(result.importedRows()).isEqualTo(1);
|
||||
var captor = org.mockito.ArgumentCaptor.forClass(ExpressCompanyEntity.class);
|
||||
verify(repository).insert(captor.capture());
|
||||
assertThat(captor.getValue().getTenantId()).isEqualTo(10001L);
|
||||
assertThat(captor.getValue().getCompanyCode()).isEqualTo("SF");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("文件内重复名称时整批拒绝且不写入")
|
||||
void shouldRejectWholeWorkbookWhenAnyRowIsInvalid() throws Exception {
|
||||
ExpressCompanyRepository repository = mock(ExpressCompanyRepository.class);
|
||||
ExpressCompanyImportService service = new ExpressCompanyImportService(repository);
|
||||
byte[] workbook = withRows(service.template(), new String[][] {
|
||||
{"SF", "顺丰速运", "", "", "", "ACTIVE", ""},
|
||||
{"SF2", "顺丰速运", "", "", "", "ACTIVE", ""}
|
||||
});
|
||||
|
||||
var result = service.importFile(10001L, "express.xlsx", workbook);
|
||||
|
||||
assertThat(result.success()).isFalse();
|
||||
assertThat(result.importedRows()).isZero();
|
||||
assertThat(result.errors()).anyMatch(error -> error.row() == 5 && error.field().contains("名称"));
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
private static byte[] withRows(byte[] template, String[][] rows) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(template));
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
var sheet = workbook.getSheetAt(0);
|
||||
for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
||||
var row = sheet.createRow(rowIndex + 3);
|
||||
for (int column = 0; column < rows[rowIndex].length; column++) {
|
||||
row.createCell(column).setCellValue(rows[rowIndex][column]);
|
||||
}
|
||||
}
|
||||
workbook.write(output);
|
||||
return output.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerImportResult;
|
||||
import com.baiyue.warehouse.entity.PartnerEntity;
|
||||
import com.baiyue.warehouse.repository.PartnerRepository;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@DisplayName("往来单位批量导入")
|
||||
class PartnerImportServiceTest {
|
||||
@Test
|
||||
@DisplayName("标准模板支持多类型并写入服务端数据范围")
|
||||
void shouldImportValidMultiTypeRow() throws Exception {
|
||||
PartnerRepository repository = mock(PartnerRepository.class);
|
||||
PartnerImportService service = new PartnerImportService(repository);
|
||||
byte[] file = withRows(service.template(), new String[][] {
|
||||
{"CUS-100", "测试单位", "CUSTOMER,SHIPPER", "张三", "13800138000", "test@example.com",
|
||||
"CN", "广东省", "深圳市", "南山区", "测试路 1 号", "518000", "ACTIVE", "测试备注"}
|
||||
});
|
||||
|
||||
PartnerImportResult result = service.importFile(10001L, "partners.xlsx", file);
|
||||
|
||||
assertThat(result.success()).isTrue();
|
||||
assertThat(result.importedRows()).isEqualTo(1);
|
||||
var captor = org.mockito.ArgumentCaptor.forClass(PartnerEntity.class);
|
||||
verify(repository).insert(captor.capture());
|
||||
assertThat(captor.getValue().getDataScopeId()).isEqualTo(10001L);
|
||||
assertThat(captor.getValue().getTypes()).containsExactly("CUSTOMER", "SHIPPER");
|
||||
assertThat(captor.getValue().getRemark()).isEqualTo("测试备注");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("文件内任一错误时整批零写入并返回行号")
|
||||
void shouldRejectWholeBatchWhenAnyRowIsInvalid() throws Exception {
|
||||
PartnerRepository repository = mock(PartnerRepository.class);
|
||||
PartnerImportService service = new PartnerImportService(repository);
|
||||
byte[] file = withRows(service.template(), new String[][] {
|
||||
{"CUS-100", "测试单位一", "CUSTOMER", "张三", "13800138000", "", "CN", "", "深圳市", "", "测试路 1 号", "", "ACTIVE", ""},
|
||||
{"CUS-100", "测试单位二", "UNKNOWN", "李四", "13900139000", "bad-email", "C", "", "", "", "", "", "ENABLED", ""}
|
||||
});
|
||||
|
||||
PartnerImportResult result = service.importFile(10001L, "partners.xlsx", file);
|
||||
|
||||
assertThat(result.success()).isFalse();
|
||||
assertThat(result.importedRows()).isZero();
|
||||
assertThat(result.errors()).anyMatch(error -> error.row() == 5 && error.field().equals("单位编码"));
|
||||
assertThat(result.errors()).anyMatch(error -> error.row() == 5 && error.field().equals("单位类型"));
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
private static byte[] withRows(byte[] template, String[][] values) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(template));
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
for (int rowIndex = 0; rowIndex < values.length; rowIndex++) {
|
||||
Row row = workbook.getSheetAt(0).createRow(rowIndex + 3);
|
||||
for (int column = 0; column < values[rowIndex].length; column++) {
|
||||
row.createCell(column).setCellValue(values[rowIndex][column]);
|
||||
}
|
||||
}
|
||||
workbook.write(output);
|
||||
return output.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PartnerQueryServiceTest {
|
||||
@Test void unsupportedCandidateTypeReturnsBadRequest() {
|
||||
PartnerRepository repository = mock(PartnerRepository.class);
|
||||
assertThatThrownBy(() -> new PartnerQueryService(repository).candidates(10001L, null, "CUSTOMER"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("SHIPPER");
|
||||
}
|
||||
@Test void missingOrCrossTenantPartnerReturnsNotFound() {
|
||||
PartnerRepository repository = mock(PartnerRepository.class);
|
||||
|
||||
|
||||
+17
-1
@@ -72,8 +72,11 @@ class PlatformMasterDataManagementServiceTest {
|
||||
org.mockito.ArgumentCaptor<PartnerEntity> entity = org.mockito.ArgumentCaptor.forClass(PartnerEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
assertThat(entity.getValue().getTenantId()).isEqualTo(10001L);
|
||||
assertThat(entity.getValue().getDataScopeId()).isEqualTo(10001L);
|
||||
assertThat(entity.getValue().getPartnerCode()).isEqualTo("CUS-001");
|
||||
assertThat(entity.getValue().getPartnerName()).isEqualTo("示例客户");
|
||||
assertThat(entity.getValue().getTypes()).containsExactly("CUSTOMER", "SHIPPER");
|
||||
assertThat(entity.getValue().getRemark()).isEqualTo("测试备注");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -132,6 +135,18 @@ class PlatformMasterDataManagementServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("快递公司名称在租户内必须唯一")
|
||||
void shouldRequireUniqueExpressCompanyNameWithinTenant() {
|
||||
ExpressCompanyRepository repository = mock(ExpressCompanyRepository.class);
|
||||
when(repository.existsName(10001L, "顺丰速运", null)).thenReturn(true);
|
||||
ExpressCompanyQueryService service = new ExpressCompanyQueryService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, expressRequest()))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("名称");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("快递公司并发重复编码统一返回业务冲突")
|
||||
void shouldReturnBusinessConflictForConcurrentDuplicateExpressCompanyCode() {
|
||||
@@ -200,7 +215,8 @@ class PlatformMasterDataManagementServiceTest {
|
||||
|
||||
private static PartnerSaveRequest partnerRequest() {
|
||||
return new PartnerSaveRequest("CUS-001", "示例客户", "CUSTOMER", "张三", "13800138000",
|
||||
"test@example.com", "CN", "广东省", "深圳市", "南山区", "示例地址", "518000", "ACTIVE");
|
||||
"test@example.com", "CN", "广东省", "深圳市", "南山区", "示例地址", "518000", "ACTIVE",
|
||||
java.util.List.of("CUSTOMER", "SHIPPER"), "测试备注");
|
||||
}
|
||||
|
||||
private static ExpressCompanySaveRequest expressRequest() {
|
||||
|
||||
Reference in New Issue
Block a user