This commit is contained in:
2026-08-05 09:11:26 +08:00
parent d544bbd0c5
commit 3791fee285
45 changed files with 1187 additions and 126 deletions
@@ -13,6 +13,7 @@ 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.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -42,7 +43,7 @@ import java.nio.charset.StandardCharsets;
/** 平台快递公司管理接口。 */
@Validated
@Tag(name = "平台快递公司", description = "按目标租户分页查询快递公司主数据")
@Tag(name = "平台快递公司", description = "跨企业分页查询和按目标租户维护快递公司主数据")
@RestController
@RequestMapping("/api/platform/v1/express-companies")
public class PlatformExpressCompanyController {
@@ -56,7 +57,7 @@ public class PlatformExpressCompanyController {
@GetMapping
@PreAuthorize("hasAuthority('platform:express:list')")
@Operation(summary = "分页查询快递公司", description = "按目标租户、关键字和状态分页查询快递公司")
@Operation(summary = "分页查询快递公司", description = "默认查询全部企业;可按目标租户、关键字和状态筛选")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
@@ -65,7 +66,8 @@ public class PlatformExpressCompanyController {
})
public ApiResponse<PageResult<ExpressCompanyEntity>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @Min(1) long tenantId,
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
@RequestParam(required = false) @Min(1) Long tenantId,
@Valid @ParameterObject ExpressCompanyPageQuery query) {
requirePlatform(user);
return ApiResponse.success(service.page(tenantId, query));
@@ -13,6 +13,7 @@ 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.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -42,7 +43,7 @@ import java.nio.charset.StandardCharsets;
/** 平台往来单位管理接口。 */
@Validated
@Tag(name = "平台往来单位", description = "按目标租户分页查询往来单位主数据")
@Tag(name = "平台往来单位", description = "跨企业分页查询和按目标租户维护往来单位主数据")
@RestController
@RequestMapping("/api/platform/v1/partners")
public class PlatformPartnerController {
@@ -83,7 +84,8 @@ public class PlatformPartnerController {
@GetMapping
@PreAuthorize("hasAuthority('platform:counterparty:list')")
@Operation(summary = "分页查询往来单位", description = "按目标租户、关键字、类型和状态分页查询往来单位")
@Operation(summary = "分页查询往来单位",
description = "默认分页查询全部企业往来单位;可按目标租户、关键字、类型和状态筛选")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
@@ -92,7 +94,8 @@ public class PlatformPartnerController {
})
public ApiResponse<PageResult<PartnerEntity>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @Min(1) long tenantId,
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
@RequestParam(required = false) @Min(1) Long tenantId,
@Valid @ParameterObject PartnerPageQuery query) {
requirePlatform(user);
return ApiResponse.success(service.page(tenantId, query));
@@ -1,5 +1,6 @@
package com.baiyue.warehouse.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.warehouse.entity.PartnerEntity;
import com.baiyue.warehouse.entity.PartnerTypeEntity;
import org.apache.ibatis.annotations.Delete;
@@ -25,6 +26,56 @@ public interface PartnerMapper extends BaseMapper<PartnerEntity> {
@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);
@Select("""
<script>
SELECT p.*
FROM wh_partner p
WHERE p.deleted = 0
<if test="tenantId != null">
AND p.tenant_id = #{tenantId}
</if>
<choose>
<when test="type == 'BOTH'">
AND EXISTS (
SELECT 1 FROM wh_partner_type customer_type
WHERE customer_type.partner_id = p.id
AND customer_type.partner_type = 'CUSTOMER'
AND customer_type.tenant_id = p.tenant_id
)
AND EXISTS (
SELECT 1 FROM wh_partner_type supplier_type
WHERE supplier_type.partner_id = p.id
AND supplier_type.partner_type = 'SUPPLIER'
AND supplier_type.tenant_id = p.tenant_id
)
</when>
<otherwise>
AND EXISTS (
SELECT 1 FROM wh_partner_type pt
WHERE pt.partner_id = p.id
AND pt.partner_type = #{type}
AND pt.tenant_id = p.tenant_id
)
</otherwise>
</choose>
<if test="status != null and status != ''">
AND p.status = #{status}
</if>
<if test="keyword != null and keyword != ''">
AND (p.partner_code LIKE CONCAT('%', #{keyword}, '%')
OR p.partner_name LIKE CONCAT('%', #{keyword}, '%')
OR p.contact_name LIKE CONCAT('%', #{keyword}, '%')
OR p.contact_phone LIKE CONCAT('%', #{keyword}, '%'))
</if>
ORDER BY p.partner_name ASC, p.id ASC
</script>
""")
Page<PartnerEntity> selectPartnerPageByType(Page<PartnerEntity> page,
@Param("tenantId") Long tenantId,
@Param("type") String type,
@Param("status") String status,
@Param("keyword") String keyword);
@Delete("DELETE FROM wh_partner_type WHERE partner_id = #{partnerId}")
void deleteTypes(@Param("partnerId") long partnerId);
@@ -9,7 +9,7 @@ import com.baiyue.warehouse.mapper.ExpressCompanyMapper;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
/** 快递公司主数据仓储。 */
/** 快递公司主数据仓储;平台分页租户条件可选,资源读写仍强制租户边界。 */
@Repository
public class ExpressCompanyRepository {
private final ExpressCompanyMapper mapper;
@@ -18,9 +18,9 @@ public class ExpressCompanyRepository {
this.mapper = mapper;
}
public PageResult<ExpressCompanyEntity> page(long tenantId, ExpressCompanyPageQuery query) {
public PageResult<ExpressCompanyEntity> page(Long tenantId, ExpressCompanyPageQuery query) {
LambdaQueryWrapper<ExpressCompanyEntity> wrapper = new LambdaQueryWrapper<ExpressCompanyEntity>()
.eq(ExpressCompanyEntity::getTenantId, tenantId)
.eq(tenantId != null, ExpressCompanyEntity::getTenantId, tenantId)
.eq(StringUtils.hasText(query.status()), ExpressCompanyEntity::getStatus, query.status())
.and(StringUtils.hasText(query.keyword()), condition -> condition
.like(ExpressCompanyEntity::getCompanyCode, query.keyword())
@@ -41,14 +41,17 @@ public class PartnerRepository {
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);
public PageResult<PartnerEntity> page(Long tenantId, PartnerPageQuery request) {
if (StringUtils.hasText(request.type())) {
Page<PartnerEntity> page = mapper.selectPartnerPageByType(
new Page<>(request.page(), request.size()), tenantId, request.type(),
request.status(), request.keyword());
PageResult<PartnerEntity> result = PageResult.from(page);
hydrateTypes(result.records());
return result;
}
LambdaQueryWrapper<PartnerEntity> wrapper = new LambdaQueryWrapper<PartnerEntity>()
.eq(PartnerEntity::getTenantId, tenantId)
.in(StringUtils.hasText(request.type()), PartnerEntity::getId, typedIds)
.eq(tenantId != null, PartnerEntity::getTenantId, tenantId)
.eq(StringUtils.hasText(request.status()), PartnerEntity::getStatus, request.status())
.and(StringUtils.hasText(request.keyword()), query -> query
.like(PartnerEntity::getPartnerCode, request.keyword())
@@ -21,7 +21,7 @@ public class ExpressCompanyQueryService {
this.repository = repository;
}
public PageResult<ExpressCompanyEntity> page(long tenantId, ExpressCompanyPageQuery query) {
public PageResult<ExpressCompanyEntity> page(Long tenantId, ExpressCompanyPageQuery query) {
return repository.page(tenantId, query);
}
@@ -25,7 +25,7 @@ public class PlatformPartnerQueryService {
this.repository = repository;
}
public PageResult<PartnerEntity> page(long tenantId, PartnerPageQuery query) {
public PageResult<PartnerEntity> page(Long tenantId, PartnerPageQuery query) {
return repository.page(tenantId, query);
}
@@ -0,0 +1,31 @@
package com.baiyue.warehouse.controller;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Method;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class PlatformExpressCompanyAllTenantQueryContractTest {
@Test
void 分页租户参数可选但详情写入启停和导入仍然必填() {
Method page = method("page");
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
for (String scopedMethod : new String[] {"detail", "create", "update", "changeStatus", "importCompanies"}) {
Method method = method(scopedMethod);
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
}
}
private static Method method(String name) {
return Arrays.stream(PlatformExpressCompanyController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst()
.orElseThrow();
}
}
@@ -75,7 +75,7 @@ class PlatformMasterDataControllerContractTest {
Method page = controller.getDeclaredMethod("page",
com.baiyue.common.security.AuthenticatedUser.class,
long.class,
Long.class,
controller == PlatformPartnerController.class
? com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery.class
: com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyPageQuery.class);
@@ -0,0 +1,64 @@
package com.baiyue.warehouse.controller;
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery;
import com.baiyue.warehouse.mapper.PartnerMapper;
import com.baiyue.warehouse.repository.PartnerRepository;
import com.baiyue.warehouse.service.PlatformPartnerQueryService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
class PlatformPartnerAllTenantQueryContractTest {
@Test
void 平台分页租户可选但详情写入和导入仍要求租户() {
Method page = method(PlatformPartnerController.class, "page");
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
for (String scopedMethod : new String[] {"importPartners", "detail", "create", "update", "changeStatus"}) {
Method method = method(PlatformPartnerController.class, scopedMethod);
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
}
}
@Test
void 分页链路使用可选租户而其他仓储方法保持原有强租户边界() throws Exception {
assertThat(PlatformPartnerQueryService.class.getMethod("page", Long.class, PartnerPageQuery.class))
.isNotNull();
assertThat(PartnerRepository.class.getMethod("page", Long.class, PartnerPageQuery.class))
.isNotNull();
assertThat(PartnerRepository.class.getMethod("findOwned", long.class, long.class))
.isNotNull();
assertThat(PartnerRepository.class.getMethod("existsCode", long.class, String.class, Long.class))
.isNotNull();
}
@Test
void 类型筛选在数据库内关联分页且租户条件可选() throws Exception {
Method method = PartnerMapper.class.getMethod("selectPartnerPageByType",
Page.class, Long.class, String.class, String.class, String.class);
String sql = String.join("\n", method.getAnnotation(Select.class).value());
assertThat(sql)
.contains("AND EXISTS (")
.contains("pt.partner_id = p.id")
.contains("pt.partner_type = #{type}")
.contains("pt.tenant_id = p.tenant_id")
.contains("<if test=\"tenantId != null\">")
.contains("AND p.tenant_id = #{tenantId}")
.doesNotContain("partner_id IN");
}
private static Method method(Class<?> type, String name) {
return java.util.Arrays.stream(type.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name))
.findFirst()
.orElseThrow();
}
}
@@ -0,0 +1,24 @@
package com.baiyue.warehouse.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PartnerMapperContractTest {
@Test
void bothTypeRequiresCustomerAndSupplierAssociations() throws Exception {
Select select = PartnerMapper.class.getDeclaredMethod("selectPartnerPageByType",
Page.class, Long.class, String.class, String.class, String.class)
.getAnnotation(Select.class);
String sql = String.join("\n", select.value());
assertThat(sql)
.contains("<when test=\"type == 'BOTH'\">")
.contains("customer_type.partner_type = 'CUSTOMER'")
.contains("supplier_type.partner_type = 'SUPPLIER'")
.contains("<otherwise>")
.contains("pt.partner_type = #{type}");
}
}
@@ -25,7 +25,7 @@ class ExpressCompanyRepositoryTest {
}
@Test
void 平台分页强制租户并支持筛选() {
void 平台分页传入租户时按租户并支持筛选() {
ExpressCompanyMapper mapper = mock(ExpressCompanyMapper.class);
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
@@ -40,4 +40,22 @@ class ExpressCompanyRepositoryTest {
assertThat(wrapper.getValue().getParamNameValuePairs().values())
.contains(10001L, "ACTIVE", "%顺丰%");
}
@Test
void 平台分页不传租户时查询全部企业并保留其他筛选() {
ExpressCompanyMapper mapper = mock(ExpressCompanyMapper.class);
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
new ExpressCompanyRepository(mapper).page(null,
new ExpressCompanyPageQuery(1, 20, "顺丰", "ACTIVE"));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<ExpressCompanyEntity>> wrapper =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(mapper).selectPage(any(Page.class), wrapper.capture());
wrapper.getValue().getSqlSegment();
assertThat(wrapper.getValue().getParamNameValuePairs().values())
.contains("ACTIVE", "%顺丰%")
.doesNotContain(10001L);
}
}
@@ -56,17 +56,27 @@ class PartnerRepositoryTest {
@Test void 平台分页强制租户并支持筛选() {
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));
when(mapper.selectPartnerPageByType(any(Page.class), eq(10001L), eq("CUSTOMER"),
eq("ACTIVE"), eq("示例"))).thenAnswer(invocation -> invocation.getArgument(0));
new PartnerRepository(mapper).page(10001L,
new PartnerPageQuery(1, 20, "示例", "CUSTOMER", "ACTIVE"));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<PartnerEntity>> wrapper=ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(mapper).selectPage(any(Page.class), wrapper.capture());
wrapper.getValue().getSqlSegment();
assertThat(wrapper.getValue().getParamNameValuePairs().values())
.contains(10001L, 9L, "ACTIVE", "%示例%");
verify(mapper).selectPartnerPageByType(any(Page.class), eq(10001L), eq("CUSTOMER"),
eq("ACTIVE"), eq("示例"));
verify(mapper, never()).selectPartnerIdsByType(anyLong(), anyString());
}
@Test void 平台分页不传租户时查询全部企业并支持承运商类型筛选() {
PartnerMapper mapper=mock(PartnerMapper.class);
when(mapper.selectPartnerPageByType(any(Page.class), isNull(), eq("CARRIER"),
eq("ACTIVE"), isNull())).thenAnswer(invocation -> invocation.getArgument(0));
new PartnerRepository(mapper).page(null,
new PartnerPageQuery(1, 20, null, "CARRIER", "ACTIVE"));
verify(mapper).selectPartnerPageByType(any(Page.class), isNull(), eq("CARRIER"),
eq("ACTIVE"), isNull());
verify(mapper, never()).selectPartnerIdsByType(anyLong(), anyString());
}
}