优化
This commit is contained in:
@@ -26,7 +26,7 @@ public final class TenantDtos {
|
|||||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||||
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
||||||
@Schema(description = "18-character unified social credit code / 18 位统一社会信用代码", example = "91440300MA5EXAMPLE") @NotBlank @Pattern(regexp = "[0-9A-Z]{18}") String unifiedSocialCreditCode,
|
@Schema(description = "Optional while creating a draft; required before review / 创建草稿时选填,送审前必填", example = "91440300MA5EXAMPLE", nullable = true) @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
|
||||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
||||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ public class ProfileResourceService {
|
|||||||
throw new BizException(ResultCode.CONFLICT,
|
throw new BizException(ResultCode.CONFLICT,
|
||||||
"Unified social credit code already exists / 统一社会信用代码已存在");
|
"Unified social credit code already exists / 统一社会信用代码已存在");
|
||||||
}
|
}
|
||||||
|
if (StringUtils.hasText(tenant.getUnifiedSocialCreditCode())
|
||||||
|
&& !certificateNo.equals(tenant.getUnifiedSocialCreditCode().trim().toUpperCase())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT,
|
||||||
|
"Unified social credit code cannot be changed / 统一社会信用代码已登记,营业执照代码必须与企业主档一致");
|
||||||
|
}
|
||||||
return certificateNo;
|
return certificateNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportRow;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -53,7 +54,8 @@ public class TenantService {
|
|||||||
)
|
)
|
||||||
@Transactional
|
@Transactional
|
||||||
public TenantResponse register(RegisterRequest request, long operatorId) {
|
public TenantResponse register(RegisterRequest request, long operatorId) {
|
||||||
if (repository.existsByCreditCode(request.unifiedSocialCreditCode())) {
|
String creditCode = normalizeCreditCode(request.unifiedSocialCreditCode());
|
||||||
|
if (creditCode != null && repository.existsByCreditCode(creditCode)) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Enterprise already exists / 企业已存在");
|
throw new BizException(ResultCode.CONFLICT, "Enterprise already exists / 企业已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ public class TenantService {
|
|||||||
tenant.setStatus("PROFILE_REQUIRED");
|
tenant.setStatus("PROFILE_REQUIRED");
|
||||||
tenant.setLegalName(request.legalName());
|
tenant.setLegalName(request.legalName());
|
||||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||||
tenant.setUnifiedSocialCreditCode(request.unifiedSocialCreditCode());
|
tenant.setUnifiedSocialCreditCode(creditCode);
|
||||||
tenant.setContactName(request.contactName());
|
tenant.setContactName(request.contactName());
|
||||||
tenant.setContactPhone(request.contactPhone());
|
tenant.setContactPhone(request.contactPhone());
|
||||||
tenant.setContactEmail(request.contactEmail());
|
tenant.setContactEmail(request.contactEmail());
|
||||||
@@ -332,6 +334,10 @@ public class TenantService {
|
|||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeCreditCode(String creditCode) {
|
||||||
|
return StringUtils.hasText(creditCode) ? creditCode.trim().toUpperCase() : null;
|
||||||
|
}
|
||||||
|
|
||||||
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.getStatus(),
|
||||||
|
|||||||
@@ -840,12 +840,12 @@ components:
|
|||||||
reviewComment: {type: string}
|
reviewComment: {type: string}
|
||||||
RegisterRequest:
|
RegisterRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [tenantName, legalName, legalRepresentative, unifiedSocialCreditCode, contactName, contactPhone, servicePackage]
|
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone, servicePackage]
|
||||||
properties:
|
properties:
|
||||||
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
legalRepresentative: {type: string, maxLength: 64, example: 张三}
|
legalRepresentative: {type: string, maxLength: 64, example: 张三}
|
||||||
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Z]{18}$', example: 91440300MA5ABCDE12}
|
unifiedSocialCreditCode: {type: string, nullable: true, pattern: '^[0-9A-Za-z]{18}$', example: 91440300MA5ABCDE12, description: 创建草稿时选填,送审前必填}
|
||||||
contactName: {type: string, example: 李四}
|
contactName: {type: string, example: 李四}
|
||||||
contactPhone: {type: string, example: '13800138000'}
|
contactPhone: {type: string, example: '13800138000'}
|
||||||
contactEmail: {type: string, format: email, example: contact@example.com}
|
contactEmail: {type: string, format: email, example: contact@example.com}
|
||||||
|
|||||||
@@ -72,4 +72,26 @@ class MerchantQualificationUploadTest {
|
|||||||
assertEquals("91310101ABCDE12345", tenant.getUnifiedSocialCreditCode());
|
assertEquals("91310101ABCDE12345", tenant.getUnifiedSocialCreditCode());
|
||||||
verify(tenantRepository).saveTenant(tenant);
|
verify(tenantRepository).saveTenant(tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void businessLicenseCannotOverwriteAnEstablishedCreditCode() {
|
||||||
|
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||||
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
|
ProfileResourceService service = new ProfileResourceService();
|
||||||
|
ReflectionTestUtils.setField(service, "tenantRepository", tenantRepository);
|
||||||
|
ReflectionTestUtils.setField(service, "repository", profileRepository);
|
||||||
|
IamTenant tenant = new IamTenant();
|
||||||
|
tenant.setId(10001L);
|
||||||
|
tenant.setTenantType("MERCHANT");
|
||||||
|
tenant.setStatus("REJECTED");
|
||||||
|
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
||||||
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
|
|
||||||
|
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
|
||||||
|
10001L, "BUSINESS_LICENSE", "91310101ABCDE12345",
|
||||||
|
"/api/merchant/v1/files/qualifications/license.png", null, true)));
|
||||||
|
|
||||||
|
verify(profileRepository, never()).insertQualification(any());
|
||||||
|
verify(tenantRepository, never()).saveTenant(tenant);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,21 @@ class TenantServiceTest {
|
|||||||
verify(repository, never()).insertReview(any());
|
verify(repository, never()).insertReview(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformRegistrationAllowsCreditCodeToBeCompletedWithQualificationsLater() {
|
||||||
|
TenantRepository repository = mock(TenantRepository.class);
|
||||||
|
TenantService service = new TenantService();
|
||||||
|
ReflectionTestUtils.setField(service, "repository", repository);
|
||||||
|
|
||||||
|
var created = service.register(new RegisterRequest(
|
||||||
|
"Example Merchant", "Example Merchant Ltd", "Bob", null, "Alice", "13800138000",
|
||||||
|
"alice@example.com", "基础版", null, null, 30, null,
|
||||||
|
null, null, "CNY", null, null), 9001L);
|
||||||
|
|
||||||
|
assertEquals(null, created.unifiedSocialCreditCode());
|
||||||
|
verify(repository).insertTenant(argThat(tenant -> tenant.getUnifiedSocialCreditCode() == null));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void explicitSubmissionRequiresOnlyBusinessLicenseAndLegalPersonIdAndCreatesReview() {
|
void explicitSubmissionRequiresOnlyBusinessLicenseAndLegalPersonIdAndCreatesReview() {
|
||||||
TenantRepository repository = mock(TenantRepository.class);
|
TenantRepository repository = mock(TenantRepository.class);
|
||||||
|
|||||||
+5
-3
@@ -35,7 +35,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 平台库位管理接口;当前数据模型中企业边界即目标 tenantId。 */
|
/** 平台库位管理接口;分页支持跨企业查询,写操作仍以目标 tenantId 隔离。 */
|
||||||
@Validated
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/v1/locations")
|
@RequestMapping("/api/platform/v1/locations")
|
||||||
@@ -49,14 +49,16 @@ public class PlatformLocationController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:location:list')")
|
@PreAuthorize("hasAuthority('platform:location:list')")
|
||||||
@Operation(summary = "分页查询库位", description = "按目标企业租户、仓库、库区、编码、类型和状态分页查询")
|
@Operation(summary = "分页查询库位",
|
||||||
|
description = "默认分页查询全部企业库位;可按目标企业租户、仓库、库区、编码、类型和状态筛选")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要 platform:location:list 权限,且仓库须属于目标企业租户")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要 platform:location:list 权限,且仓库须属于目标企业租户")
|
||||||
})
|
})
|
||||||
public ApiResponse<PageResult<LocationEntity>> page(
|
public ApiResponse<PageResult<LocationEntity>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam @Min(1) long tenantId,
|
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
|
||||||
|
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||||
@Valid @ParameterObject PageQuery query) {
|
@Valid @ParameterObject PageQuery query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(tenantId, query));
|
return ApiResponse.success(service.page(tenantId, query));
|
||||||
|
|||||||
+4
-2
@@ -8,6 +8,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
|||||||
import com.baiyue.warehouse.dto.PalletRequests;
|
import com.baiyue.warehouse.dto.PalletRequests;
|
||||||
import com.baiyue.warehouse.service.PalletApplicationService;
|
import com.baiyue.warehouse.service.PalletApplicationService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -40,10 +41,11 @@ public class PlatformPalletController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:pallet:list')")
|
@PreAuthorize("hasAuthority('platform:pallet:list')")
|
||||||
@Operation(summary = "分页查询托盘")
|
@Operation(summary = "分页查询托盘", description = "默认查询全部企业;可按企业、仓库、托盘编码和类型筛选")
|
||||||
@ApiResponses(@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要 platform:pallet:list 权限"))
|
@ApiResponses(@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要 platform:pallet:list 权限"))
|
||||||
public ApiResponse<PageResult<PalletRequests.View>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<PageResult<PalletRequests.View>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam @Min(1) long tenantId,
|
@Parameter(description = "目标企业租户ID;不传时查询全部企业")
|
||||||
|
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||||
@Valid @ParameterObject PalletRequests.PageQuery query) {
|
@Valid @ParameterObject PalletRequests.PageQuery query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(tenantId, query));
|
return ApiResponse.success(service.page(tenantId, query));
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ public final class LocationDtos {
|
|||||||
|
|
||||||
@Schema(name = "LocationPageQuery", description = "库位分页查询条件")
|
@Schema(name = "LocationPageQuery", description = "库位分页查询条件")
|
||||||
public record PageQuery(
|
public record PageQuery(
|
||||||
@Min(1) long page,
|
@Min(1) @Schema(description = "页码", defaultValue = "1") long page,
|
||||||
@Min(1) @Max(200) long size,
|
@Min(1) @Max(200) @Schema(description = "每页条数", defaultValue = "20") long size,
|
||||||
@NotNull @Min(1) Long warehouseId,
|
@Min(1) @Schema(description = "可选仓库ID;不传时不限制仓库") Long warehouseId,
|
||||||
@Size(max = 50) String zoneCode,
|
@Size(max = 50) String zoneCode,
|
||||||
@Size(max = 50) String locationCode,
|
@Size(max = 50) String locationCode,
|
||||||
@Pattern(regexp = "^(SHELF|PALLET|FLOOR|FLOW_RACK)$") String locationType,
|
@Pattern(regexp = "^(SHELF|PALLET|FLOOR|FLOW_RACK)$") String locationType,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public final class PalletRequests {
|
|||||||
public record PageQuery(
|
public record PageQuery(
|
||||||
@Min(1) @Schema(description = "页码", defaultValue = "1") long page,
|
@Min(1) @Schema(description = "页码", defaultValue = "1") long page,
|
||||||
@Min(1) @Max(200) @Schema(description = "每页条数", defaultValue = "20") long size,
|
@Min(1) @Max(200) @Schema(description = "每页条数", defaultValue = "20") long size,
|
||||||
@Min(1) @Schema(description = "仓库ID") Long warehouseId,
|
@Min(1) @Schema(description = "仓库ID;不传时不限制仓库") Long warehouseId,
|
||||||
@Size(max = 50) @Schema(description = "托盘编码,支持模糊查询") String palletCode,
|
@Size(max = 50) @Schema(description = "托盘编码,支持模糊查询") String palletCode,
|
||||||
@Size(max = 32) @Schema(description = "托盘类型") String palletType
|
@Size(max = 32) @Schema(description = "托盘类型") String palletType
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ public interface LocationMapper extends BaseMapper<LocationEntity> {
|
|||||||
FROM wh_location l
|
FROM wh_location l
|
||||||
JOIN wh_warehouse w
|
JOIN wh_warehouse w
|
||||||
ON w.id = l.warehouse_id AND w.tenant_id = l.tenant_id AND w.deleted = 0
|
ON w.id = l.warehouse_id AND w.tenant_id = l.tenant_id AND w.deleted = 0
|
||||||
WHERE l.tenant_id = #{tenantId}
|
WHERE l.deleted = 0
|
||||||
|
<if test="tenantId != null">
|
||||||
|
AND l.tenant_id = #{tenantId}
|
||||||
|
</if>
|
||||||
|
<if test="warehouseId != null">
|
||||||
AND l.warehouse_id = #{warehouseId}
|
AND l.warehouse_id = #{warehouseId}
|
||||||
AND l.deleted = 0
|
</if>
|
||||||
<if test="zoneCode != null and zoneCode != ''">
|
<if test="zoneCode != null and zoneCode != ''">
|
||||||
AND l.zone_code = #{zoneCode}
|
AND l.zone_code = #{zoneCode}
|
||||||
</if>
|
</if>
|
||||||
@@ -39,8 +43,8 @@ public interface LocationMapper extends BaseMapper<LocationEntity> {
|
|||||||
</script>
|
</script>
|
||||||
""")
|
""")
|
||||||
Page<LocationEntity> selectLocationPage(Page<LocationEntity> page,
|
Page<LocationEntity> selectLocationPage(Page<LocationEntity> page,
|
||||||
@Param("tenantId") long tenantId,
|
@Param("tenantId") Long tenantId,
|
||||||
@Param("warehouseId") long warehouseId,
|
@Param("warehouseId") Long warehouseId,
|
||||||
@Param("zoneCode") String zoneCode,
|
@Param("zoneCode") String zoneCode,
|
||||||
@Param("locationCode") String locationCode,
|
@Param("locationCode") String locationCode,
|
||||||
@Param("locationType") String locationType,
|
@Param("locationType") String locationType,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Repository;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 强制使用租户、仓库联合条件的库位持久化边界。 */
|
/** 库位持久化边界;平台分页条件可选,资源读写继续强制租户、仓库联合条件。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class LocationRepository {
|
public class LocationRepository {
|
||||||
private final LocationMapper locations;
|
private final LocationMapper locations;
|
||||||
@@ -47,7 +47,7 @@ public class LocationRepository {
|
|||||||
.eq(LocationEntity::getId, id));
|
.eq(LocationEntity::getId, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<LocationEntity> page(long tenantId, PageQuery query) {
|
public PageResult<LocationEntity> page(Long tenantId, PageQuery query) {
|
||||||
Page<LocationEntity> result = locations.selectLocationPage(new Page<>(query.page(), query.size()),
|
Page<LocationEntity> result = locations.selectLocationPage(new Page<>(query.page(), query.size()),
|
||||||
tenantId, query.warehouseId(), query.zoneCode(), query.locationCode(), query.locationType(), query.status());
|
tenantId, query.warehouseId(), query.zoneCode(), query.locationCode(), query.locationType(), query.status());
|
||||||
return PageResult.from(result);
|
return PageResult.from(result);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 托盘持久化仓储,所有业务查询均显式包含租户和仓库边界。 */
|
/** 托盘持久化仓储;平台分页支持跨租户筛选,资源操作始终包含租户和仓库边界。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class PalletRepository {
|
public class PalletRepository {
|
||||||
private final PalletMapper mapper;
|
private final PalletMapper mapper;
|
||||||
@@ -22,9 +22,9 @@ public class PalletRepository {
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<PalletEntity> page(long tenantId, PalletRequests.PageQuery query) {
|
public PageResult<PalletEntity> page(Long tenantId, PalletRequests.PageQuery query) {
|
||||||
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
|
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
|
||||||
.eq(PalletEntity::getTenantId, tenantId)
|
.eq(tenantId != null, PalletEntity::getTenantId, tenantId)
|
||||||
.eq(query.warehouseId() != null, PalletEntity::getWarehouseId, query.warehouseId())
|
.eq(query.warehouseId() != null, PalletEntity::getWarehouseId, query.warehouseId())
|
||||||
.like(StringUtils.hasText(query.palletCode()), PalletEntity::getPalletCode, query.palletCode())
|
.like(StringUtils.hasText(query.palletCode()), PalletEntity::getPalletCode, query.palletCode())
|
||||||
.eq(StringUtils.hasText(query.palletType()), PalletEntity::getPalletType, query.palletType())
|
.eq(StringUtils.hasText(query.palletType()), PalletEntity::getPalletType, query.palletType())
|
||||||
|
|||||||
+2
-3
@@ -16,7 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 库位基础资料应用服务,负责 CRUD、租户仓库隔离和启停/删除约束。 */
|
/** 库位基础资料应用服务,负责平台分页、CRUD、租户仓库隔离和启停/删除约束。 */
|
||||||
@Service
|
@Service
|
||||||
public class LocationApplicationService {
|
public class LocationApplicationService {
|
||||||
private final LocationRepository repository;
|
private final LocationRepository repository;
|
||||||
@@ -25,8 +25,7 @@ public class LocationApplicationService {
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<LocationEntity> page(long tenantId, PageQuery query) {
|
public PageResult<LocationEntity> page(Long tenantId, PageQuery query) {
|
||||||
requireWarehouse(tenantId, query.warehouseId(), false);
|
|
||||||
return repository.page(tenantId, query);
|
return repository.page(tenantId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class PalletApplicationService {
|
|||||||
this.warehouseRepository = warehouseRepository;
|
this.warehouseRepository = warehouseRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<PalletRequests.View> page(long tenantId, PalletRequests.PageQuery query) {
|
public PageResult<PalletRequests.View> page(Long tenantId, PalletRequests.PageQuery query) {
|
||||||
PageResult<PalletEntity> source = repository.page(tenantId, query);
|
PageResult<PalletEntity> source = repository.page(tenantId, query);
|
||||||
return new PageResult<>(source.records().stream().map(PalletApplicationService::view).toList(),
|
return new PageResult<>(source.records().stream().map(PalletApplicationService::view).toList(),
|
||||||
source.page(), source.size(), source.total(), source.pages());
|
source.page(), source.size(), source.total(), source.pages());
|
||||||
|
|||||||
+21
-3
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PatchMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -29,13 +30,30 @@ class PlatformLocationControllerContractTest {
|
|||||||
assertMethod("delete", DeleteMapping.class, "platform:location:delete");
|
assertMethod("delete", DeleteMapping.class, "platform:location:delete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 分页查询的企业租户参数可选但写接口仍然必填() {
|
||||||
|
Method page = method("page");
|
||||||
|
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||||
|
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
|
||||||
|
for (String writeMethod : new String[] {"create", "update", "changeStatus", "delete"}) {
|
||||||
|
Method method = method(writeMethod);
|
||||||
|
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||||
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertMethod(String name, Class<? extends Annotation> mapping, String permission) {
|
private static void assertMethod(String name, Class<? extends Annotation> mapping, String permission) {
|
||||||
Method method = java.util.Arrays.stream(PlatformLocationController.class.getDeclaredMethods())
|
Method method = method(name);
|
||||||
.filter(candidate -> candidate.getName().equals(name))
|
|
||||||
.findFirst().orElseThrow();
|
|
||||||
assertThat(method.getAnnotation(mapping)).isNotNull();
|
assertThat(method.getAnnotation(mapping)).isNotNull();
|
||||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||||
.isEqualTo("hasAuthority('" + permission + "')");
|
.isEqualTo("hasAuthority('" + permission + "')");
|
||||||
assertThat(method.getAnnotation(Operation.class).summary()).isNotBlank();
|
assertThat(method.getAnnotation(Operation.class).summary()).isNotBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method method(String name) {
|
||||||
|
return java.util.Arrays.stream(PlatformLocationController.class.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals(name))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -24,15 +25,28 @@ class PlatformPalletControllerContractTest {
|
|||||||
assertMethod("create", PostMapping.class, "platform:pallet:create");
|
assertMethod("create", PostMapping.class, "platform:pallet:create");
|
||||||
assertMethod("update", PutMapping.class, "platform:pallet:edit");
|
assertMethod("update", PutMapping.class, "platform:pallet:edit");
|
||||||
assertMethod("delete", DeleteMapping.class, "platform:pallet:delete");
|
assertMethod("delete", DeleteMapping.class, "platform:pallet:delete");
|
||||||
|
Method page = method("page");
|
||||||
|
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||||
|
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
|
||||||
|
for (String writeMethod : new String[]{"detail", "create", "update", "delete"}) {
|
||||||
|
Method method = method(writeMethod);
|
||||||
|
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||||
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
|
}
|
||||||
assertThat(Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
assertThat(Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
||||||
.noneMatch(method -> method.getName().equals("status"))).isTrue();
|
.noneMatch(method -> method.getName().equals("status"))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertMethod(String name, Class<? extends Annotation> mapping, String authority) {
|
private static void assertMethod(String name, Class<? extends Annotation> mapping, String authority) {
|
||||||
Method method = Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
Method method = method(name);
|
||||||
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
|
||||||
assertThat(method.getAnnotation(mapping)).isNotNull();
|
assertThat(method.getAnnotation(mapping)).isNotNull();
|
||||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||||
.isEqualTo("hasAuthority('" + authority + "')");
|
.isEqualTo("hasAuthority('" + authority + "')");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method method(String name) {
|
||||||
|
return Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.baiyue.warehouse.mapper;
|
package com.baiyue.warehouse.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import org.apache.ibatis.annotations.Insert;
|
import org.apache.ibatis.annotations.Insert;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -9,6 +10,22 @@ import java.lang.reflect.Method;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class LocationMapperContractTest {
|
class LocationMapperContractTest {
|
||||||
|
@Test
|
||||||
|
void 平台分页的企业和仓库条件均为可选且仍返回仓库快照() throws NoSuchMethodException {
|
||||||
|
Method page = LocationMapper.class.getMethod("selectLocationPage", Page.class,
|
||||||
|
Long.class, Long.class, String.class, String.class, String.class, String.class);
|
||||||
|
String pageSql = sql(page.getAnnotation(Select.class).value());
|
||||||
|
|
||||||
|
assertThat(pageSql)
|
||||||
|
.contains("SELECT l.*, w.warehouse_code, w.warehouse_name")
|
||||||
|
.contains("w.id = l.warehouse_id AND w.tenant_id = l.tenant_id")
|
||||||
|
.contains("<if test=\"tenantId != null\">")
|
||||||
|
.contains("AND l.tenant_id = #{tenantId}")
|
||||||
|
.contains("<if test=\"warehouseId != null\">")
|
||||||
|
.contains("AND l.warehouse_id = #{warehouseId}")
|
||||||
|
.contains("ORDER BY l.created_at DESC, l.id DESC");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 可用库位按库区和库位数字自然顺序临近分配() throws NoSuchMethodException {
|
void 可用库位按库区和库位数字自然顺序临近分配() throws NoSuchMethodException {
|
||||||
String selectSql = sql(LocationMapper.class.getMethod(
|
String selectSql = sql(LocationMapper.class.getMethod(
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.baiyue.warehouse.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baiyue.warehouse.dto.PalletRequests;
|
||||||
|
import com.baiyue.warehouse.entity.PalletEntity;
|
||||||
|
import com.baiyue.warehouse.mapper.PalletMapper;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.apache.ibatis.session.Configuration;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
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.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class PalletRepositoryTest {
|
||||||
|
@BeforeAll
|
||||||
|
static void 初始化实体字段缓存() {
|
||||||
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"), PalletEntity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页默认不限制企业和仓库() {
|
||||||
|
Collection<Object> values = queryValues(null, null);
|
||||||
|
assertThat(values).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页可单独按企业筛选() {
|
||||||
|
Collection<Object> values = queryValues(10001L, null);
|
||||||
|
assertThat(values).containsExactly(10001L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页可同时按企业和仓库筛选() {
|
||||||
|
Collection<Object> values = queryValues(10001L, 20L);
|
||||||
|
assertThat(values).containsExactlyInAnyOrder(10001L, 20L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页可按全局唯一仓库筛选且写查询仍保留联合边界() {
|
||||||
|
Collection<Object> pageValues = queryValues(null, 20L);
|
||||||
|
assertThat(pageValues).containsExactly(20L);
|
||||||
|
|
||||||
|
PalletMapper mapper = mapper();
|
||||||
|
new PalletRepository(mapper).findOwned(10001L, 20L, 30L);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<PalletEntity>> wrapper = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(mapper).selectOne(wrapper.capture());
|
||||||
|
wrapper.getValue().getSqlSegment();
|
||||||
|
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||||
|
.containsExactlyInAnyOrder(10001L, 20L, 30L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection<Object> queryValues(Long tenantId, Long warehouseId) {
|
||||||
|
PalletMapper mapper = mapper();
|
||||||
|
new PalletRepository(mapper).page(tenantId,
|
||||||
|
new PalletRequests.PageQuery(1, 20, warehouseId, null, null));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<PalletEntity>> wrapper = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(mapper).selectPage(any(Page.class), wrapper.capture());
|
||||||
|
wrapper.getValue().getSqlSegment();
|
||||||
|
return wrapper.getValue().getParamNameValuePairs().values();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PalletMapper mapper() {
|
||||||
|
PalletMapper mapper = mock(PalletMapper.class);
|
||||||
|
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.warehouse.service;
|
package com.baiyue.warehouse.service;
|
||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.warehouse.dto.LocationDtos.PageQuery;
|
||||||
import com.baiyue.warehouse.dto.LocationDtos.SaveRequest;
|
import com.baiyue.warehouse.dto.LocationDtos.SaveRequest;
|
||||||
import com.baiyue.warehouse.entity.LocationEntity;
|
import com.baiyue.warehouse.entity.LocationEntity;
|
||||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||||
@@ -14,9 +15,44 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class LocationApplicationServiceTest {
|
class LocationApplicationServiceTest {
|
||||||
|
@Test
|
||||||
|
void 平台分页不传企业和仓库时查询全部库位() {
|
||||||
|
LocationRepository repository = mock(LocationRepository.class);
|
||||||
|
LocationApplicationService service = new LocationApplicationService(repository);
|
||||||
|
PageQuery query = pageQuery(null);
|
||||||
|
|
||||||
|
service.page(null, query);
|
||||||
|
|
||||||
|
verify(repository).page(null, query);
|
||||||
|
verifyNoMoreInteractions(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页可只按企业筛选全部仓库() {
|
||||||
|
LocationRepository repository = mock(LocationRepository.class);
|
||||||
|
LocationApplicationService service = new LocationApplicationService(repository);
|
||||||
|
PageQuery query = pageQuery(null);
|
||||||
|
|
||||||
|
service.page(10001L, query);
|
||||||
|
|
||||||
|
verify(repository).page(10001L, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页可同时按企业和仓库筛选() {
|
||||||
|
LocationRepository repository = mock(LocationRepository.class);
|
||||||
|
LocationApplicationService service = new LocationApplicationService(repository);
|
||||||
|
PageQuery query = pageQuery(88L);
|
||||||
|
|
||||||
|
service.page(10001L, query);
|
||||||
|
|
||||||
|
verify(repository).page(10001L, query);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 新增时仓库必须属于当前企业租户() {
|
void 新增时仓库必须属于当前企业租户() {
|
||||||
LocationRepository repository = mock(LocationRepository.class);
|
LocationRepository repository = mock(LocationRepository.class);
|
||||||
@@ -79,6 +115,10 @@ class LocationApplicationServiceTest {
|
|||||||
"PALLET", new BigDecimal("500"), status, "备注", version);
|
"PALLET", new BigDecimal("500"), status, "备注", version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PageQuery pageQuery(Long warehouseId) {
|
||||||
|
return new PageQuery(1, 20, warehouseId, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
private static WarehouseEntity warehouse(String status) {
|
private static WarehouseEntity warehouse(String status) {
|
||||||
WarehouseEntity warehouse = new WarehouseEntity();
|
WarehouseEntity warehouse = new WarehouseEntity();
|
||||||
warehouse.setId(88L);
|
warehouse.setId(88L);
|
||||||
|
|||||||
+14
@@ -1,6 +1,8 @@
|
|||||||
package com.baiyue.warehouse.service;
|
package com.baiyue.warehouse.service;
|
||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
|
import com.baiyue.warehouse.dto.PalletRequests;
|
||||||
import com.baiyue.warehouse.entity.PalletEntity;
|
import com.baiyue.warehouse.entity.PalletEntity;
|
||||||
import com.baiyue.warehouse.repository.PalletRepository;
|
import com.baiyue.warehouse.repository.PalletRepository;
|
||||||
import com.baiyue.warehouse.repository.WarehouseRepository;
|
import com.baiyue.warehouse.repository.WarehouseRepository;
|
||||||
@@ -8,6 +10,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class PalletApplicationServiceTest {
|
class PalletApplicationServiceTest {
|
||||||
@@ -28,6 +31,17 @@ class PalletApplicationServiceTest {
|
|||||||
assertThatThrownBy(() -> service.detail(10001, 21, 30)).isInstanceOf(BizException.class);
|
assertThatThrownBy(() -> service.detail(10001, 21, 30)).isInstanceOf(BizException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页允许不指定企业并保留分页结果() {
|
||||||
|
PalletRequests.PageQuery query = new PalletRequests.PageQuery(1, 20, null, null, null);
|
||||||
|
when(repository.page(null, query)).thenReturn(new PageResult<>(java.util.List.of(pallet("AVAILABLE")),
|
||||||
|
1, 20, 1, 1));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(service.page(null, query).records())
|
||||||
|
.singleElement().extracting("tenantId", "warehouseId").containsExactly(10001L, 20L);
|
||||||
|
verify(repository).page(null, query);
|
||||||
|
}
|
||||||
|
|
||||||
private static PalletEntity pallet(String status) {
|
private static PalletEntity pallet(String status) {
|
||||||
PalletEntity entity = new PalletEntity();
|
PalletEntity entity = new PalletEntity();
|
||||||
entity.setId(30L);
|
entity.setId(30L);
|
||||||
|
|||||||
Reference in New Issue
Block a user