This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
+1
View File
@@ -21,6 +21,7 @@
## 数据与安全规则
- 平台和商户接口必须分别执行访问域与租户范围校验;客户端传入的 `tenantId` 不能作为唯一授权依据。
- 平台维护的海外库位与实物托盘是公共资源,固定使用 `customer_id=0`;平台资源 API 不接收目标客户参数。资源锁记录仍使用真实业务客户 ID 隔离,但其托盘、库位查找和状态更新必须落到公共资源作用域。
- 资源锁定以业务幂等键和 reservation 标识防重;释放/报废操作必须验证当前状态。
- 内部资源接口必须校验服务令牌;普通用户不得直接调用。
@@ -9,6 +9,7 @@ import com.baiyue.warehouse.dto.WarehouseDetailResponse;
import com.baiyue.warehouse.dto.WarehouseRequests;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.service.WarehouseApplicationService;
import com.baiyue.warehouse.security.OverseasWarehousePermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -17,6 +18,7 @@ import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -44,7 +46,7 @@ public class PlatformWarehouseController {
@Operation(
summary = "Create warehouse / 新建仓库",
description = "Creates a warehouse, address and service capabilities for the target tenant. / 为目标租户创建仓库、地址和服务能力。"
description = "Required authority: " + OverseasWarehousePermissions.WAREHOUSE_CREATE + ". Creates a warehouse, address and service capabilities for the target customer."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Created successfully / 创建成功"),
@@ -53,6 +55,7 @@ public class PlatformWarehouseController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Warehouse code already exists / 仓库编码已存在")
})
@PostMapping
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_CREATE + "')")
public ApiResponse<Long> create(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody WarehouseRequests.SaveRequest request) {
requirePlatform(user);
@@ -62,7 +65,7 @@ public class PlatformWarehouseController {
@Operation(
summary = "Update warehouse / 更新仓库",
description = "Updates the warehouse and replaces its address and capabilities. / 更新仓库,并整体替换地址和服务能力。"
description = "Required authority: " + OverseasWarehousePermissions.WAREHOUSE_EDIT + ". Updates the warehouse and replaces its address and capabilities."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Updated successfully / 更新成功"),
@@ -72,6 +75,7 @@ public class PlatformWarehouseController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Warehouse code already exists / 仓库编码已存在")
})
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_EDIT + "')")
public ApiResponse<Void> update(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true)
@@ -87,7 +91,7 @@ public class PlatformWarehouseController {
@Operation(
summary = "List tenant warehouses / 查询租户仓库",
description = "Returns a tenant-scoped warehouse page with optional country, capability and status filters. / 按租户分页查询仓库,可按国家、能力和状态筛选。"
description = "Required authority: " + OverseasWarehousePermissions.WAREHOUSE_LIST + ". Returns a customer-scoped warehouse page with optional filters."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
@@ -95,6 +99,7 @@ public class PlatformWarehouseController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform access required / 需要平台端权限")
})
@GetMapping
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_LIST + "')")
public ApiResponse<PageResult<WarehouseEntity>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true)
@@ -107,7 +112,7 @@ public class PlatformWarehouseController {
@Operation(
summary = "Get tenant warehouse details / 查询租户仓库详情",
description = "Returns the warehouse, addresses and service capabilities in the target tenant. / 返回目标租户下的仓库、地址及服务能力。"
description = "Required authority: " + OverseasWarehousePermissions.WAREHOUSE_LIST + ". Returns the warehouse, addresses and service capabilities for the target customer."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
@@ -115,6 +120,7 @@ public class PlatformWarehouseController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Warehouse not found / 仓库不存在")
})
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_LIST + "')")
public ApiResponse<WarehouseDetailResponse> detail(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true)
@@ -128,7 +134,7 @@ public class PlatformWarehouseController {
@Operation(
summary = "Enable or disable warehouse / 启用或停用仓库",
description = "Changes warehouse status to ACTIVE or INACTIVE. / 将仓库状态切换为 ACTIVE 或 INACTIVE。"
description = "Required authority: " + OverseasWarehousePermissions.WAREHOUSE_STATUS + ". Changes warehouse status to ACTIVE or INACTIVE."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Status changed / 状态变更成功"),
@@ -137,6 +143,7 @@ public class PlatformWarehouseController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Warehouse not found / 仓库不存在")
})
@PatchMapping("/{id}/status")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_STATUS + "')")
public ApiResponse<Void> status(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true)
@@ -152,7 +159,7 @@ public class PlatformWarehouseController {
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
if (user == null || !"PLATFORM".equals(user.appCode()) || user.tenantId() == null || user.tenantId() != 0L) {
throw new BizException(ResultCode.FORBIDDEN, "Platform application required / 仅允许平台端访问");
}
}
@@ -163,4 +170,3 @@ public class PlatformWarehouseController {
}
}
}
@@ -10,10 +10,13 @@ import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.entity.WarehousePalletEntity;
import com.baiyue.warehouse.service.WarehouseResourceService;
import com.baiyue.warehouse.security.OverseasWarehousePermissions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -28,104 +31,114 @@ import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/platform/v1/warehouse-admin/{warehouseId}")
@Tag(name = "Platform overseas warehouse resources")
public class PlatformWarehouseResourceController {
@Resource private WarehouseResourceService service;
@PostMapping("/locations")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.LOCATION_CREATE + "')")
@Operation(summary = "Create a warehouse location", description = "Required authority: " + OverseasWarehousePermissions.LOCATION_CREATE)
public ApiResponse<Long> createLocation(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId,
@Valid @RequestBody LocationRequests.SaveRequest request) {
requirePlatform(user, customerId);
return ApiResponse.success(service.createLocation(customerId, warehouseId, request));
requirePlatform(user);
return ApiResponse.success(service.createLocation(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, request));
}
@PutMapping("/locations/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.LOCATION_EDIT + "')")
@Operation(summary = "Update a warehouse location", description = "Required authority: " + OverseasWarehousePermissions.LOCATION_EDIT)
public ApiResponse<Void> updateLocation(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId, @PathVariable long id,
@Valid @RequestBody LocationRequests.SaveRequest request) {
requirePlatform(user, customerId);
service.updateLocation(customerId, warehouseId, id, request);
requirePlatform(user);
service.updateLocation(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id, request);
return ApiResponse.success(null);
}
@GetMapping("/locations")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.LOCATION_LIST + "')")
@Operation(summary = "Page warehouse locations", description = "Required authority: " + OverseasWarehousePermissions.LOCATION_LIST)
public ApiResponse<PageResult<WarehouseLocationEntity>> locations(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam @NotNull Long customerId,
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long warehouseId, @Valid LocationRequests.QueryRequest query) {
requirePlatform(user, customerId);
return ApiResponse.success(service.locations(customerId, warehouseId, query));
requirePlatform(user);
return ApiResponse.success(service.locations(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, query));
}
@GetMapping("/locations/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.LOCATION_LIST + "')")
@Operation(summary = "Get a warehouse location", description = "Required authority: " + OverseasWarehousePermissions.LOCATION_LIST)
public ApiResponse<WarehouseLocationEntity> location(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId,
@PathVariable long id) {
requirePlatform(user, customerId);
return ApiResponse.success(service.location(customerId, warehouseId, id));
requirePlatform(user);
return ApiResponse.success(service.location(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id));
}
@DeleteMapping("/locations/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.LOCATION_DELETE + "')")
@Operation(summary = "Delete a warehouse location", description = "Required authority: " + OverseasWarehousePermissions.LOCATION_DELETE)
public ApiResponse<Void> deleteLocation(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId, @PathVariable long id) {
requirePlatform(user, customerId);
service.deleteLocation(customerId, warehouseId, id);
requirePlatform(user);
service.deleteLocation(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id);
return ApiResponse.success(null);
}
@PostMapping("/pallets")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_CREATE + "')")
@Operation(summary = "Create a warehouse pallet", description = "Required authority: " + OverseasWarehousePermissions.PALLET_CREATE)
public ApiResponse<Long> createPallet(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId,
@Valid @RequestBody PalletRequests.SaveRequest request) {
requirePlatform(user, customerId);
return ApiResponse.success(service.createPallet(customerId, warehouseId, request));
requirePlatform(user);
return ApiResponse.success(service.createPallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, request));
}
@PutMapping("/pallets/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_EDIT + "')")
@Operation(summary = "Update a warehouse pallet", description = "Required authority: " + OverseasWarehousePermissions.PALLET_EDIT)
public ApiResponse<Void> updatePallet(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId, @PathVariable long id,
@Valid @RequestBody PalletRequests.SaveRequest request) {
requirePlatform(user, customerId);
service.updatePallet(customerId, warehouseId, id, request);
requirePlatform(user);
service.updatePallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id, request);
return ApiResponse.success(null);
}
@GetMapping("/pallets")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_LIST + "')")
@Operation(summary = "Page warehouse pallets", description = "Required authority: " + OverseasWarehousePermissions.PALLET_LIST)
public ApiResponse<PageResult<WarehousePalletEntity>> pallets(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam @NotNull Long customerId,
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long warehouseId, @Valid PalletRequests.QueryRequest query) {
requirePlatform(user, customerId);
return ApiResponse.success(service.pallets(customerId, warehouseId, query));
requirePlatform(user);
return ApiResponse.success(service.pallets(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, query));
}
@GetMapping("/pallets/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_LIST + "')")
@Operation(summary = "Get a warehouse pallet", description = "Required authority: " + OverseasWarehousePermissions.PALLET_LIST)
public ApiResponse<WarehousePalletEntity> pallet(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId, @PathVariable long id) {
requirePlatform(user, customerId);
return ApiResponse.success(service.pallet(customerId, warehouseId, id));
requirePlatform(user);
return ApiResponse.success(service.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id));
}
@DeleteMapping("/pallets/{id}")
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_DELETE + "')")
@Operation(summary = "Delete a warehouse pallet", description = "Required authority: " + OverseasWarehousePermissions.PALLET_DELETE)
public ApiResponse<Void> deletePallet(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam @NotNull Long customerId,
@PathVariable long warehouseId, @PathVariable long id) {
requirePlatform(user, customerId);
service.deletePallet(customerId, warehouseId, id);
requirePlatform(user);
service.deletePallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, id);
return ApiResponse.success(null);
}
private static void requirePlatform(AuthenticatedUser user, Long customerId) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode()) || user.tenantId() == null || user.tenantId() != 0L) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
if (customerId == null || customerId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
}
}
}
@@ -7,11 +7,13 @@ import com.baiyue.warehouse.entity.WarehouseResourceLockEntity;
import com.baiyue.warehouse.mapper.WarehouseLocationMapper;
import com.baiyue.warehouse.mapper.WarehousePalletMapper;
import com.baiyue.warehouse.mapper.WarehouseResourceLockMapper;
import com.baiyue.warehouse.service.WarehouseResourceService;
import org.springframework.stereotype.Repository;
import jakarta.annotation.Resource;
@Repository
public class WarehouseResourceLockRepository {
private static final long PUBLIC_RESOURCE_SCOPE = WarehouseResourceService.PUBLIC_RESOURCE_SCOPE;
@Resource private WarehouseResourceLockMapper locks;
@Resource private WarehousePalletMapper pallets;
@Resource private WarehouseLocationMapper locations;
@@ -32,58 +34,58 @@ public class WarehouseResourceLockRepository {
public WarehousePalletEntity findPallet(long customerId, long palletId) {
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
.eq(WarehousePalletEntity::getCustomerId, customerId)
.eq(WarehousePalletEntity::getCustomerId, PUBLIC_RESOURCE_SCOPE)
.eq(WarehousePalletEntity::getId, palletId));
}
public WarehousePalletEntity findPallet(long customerId, long warehouseId, long palletId) {
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
.eq(WarehousePalletEntity::getCustomerId, customerId)
.eq(WarehousePalletEntity::getCustomerId, PUBLIC_RESOURCE_SCOPE)
.eq(WarehousePalletEntity::getWarehouseId, warehouseId)
.eq(WarehousePalletEntity::getId, palletId));
}
public WarehouseLocationEntity findLocation(long customerId, long warehouseId, long locationId) {
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
.eq(WarehouseLocationEntity::getCustomerId, customerId)
.eq(WarehouseLocationEntity::getCustomerId, PUBLIC_RESOURCE_SCOPE)
.eq(WarehouseLocationEntity::getWarehouseId, warehouseId)
.eq(WarehouseLocationEntity::getId, locationId));
}
public int occupyPallet(long customerId, long warehouseId, long palletId, long locationId) {
return locks.occupyPallet(customerId, warehouseId, palletId, locationId);
return locks.occupyPallet(PUBLIC_RESOURCE_SCOPE, warehouseId, palletId, locationId);
}
public int occupyLocation(long customerId, long warehouseId, long locationId) {
return locks.occupyLocation(customerId, warehouseId, locationId);
return locks.occupyLocation(PUBLIC_RESOURCE_SCOPE, warehouseId, locationId);
}
public void insert(WarehouseResourceLockEntity lock) { locks.insert(lock); }
public int releasePallet(WarehouseResourceLockEntity lock) {
return locks.releasePallet(lock.getCustomerId(), lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
return locks.releasePallet(PUBLIC_RESOURCE_SCOPE, lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
}
public int releasePalletAtVirtualLocation(WarehouseResourceLockEntity lock) {
return locks.releasePalletAtVirtualLocation(lock.getCustomerId(), lock.getWarehouseId(),
return locks.releasePalletAtVirtualLocation(PUBLIC_RESOURCE_SCOPE, lock.getWarehouseId(),
lock.getPalletId(), lock.getLocationId());
}
public int scrapPallet(WarehouseResourceLockEntity lock) {
return locks.scrapPallet(lock.getCustomerId(), lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
return locks.scrapPallet(PUBLIC_RESOURCE_SCOPE, lock.getWarehouseId(), lock.getPalletId(), lock.getLocationId());
}
public int scrapPalletWithoutLock(long customerId, long palletId) {
return locks.scrapPalletWithoutLock(customerId, palletId);
return locks.scrapPalletWithoutLock(PUBLIC_RESOURCE_SCOPE, palletId);
}
public int scrapPalletAtVirtualLocation(WarehouseResourceLockEntity lock) {
return locks.scrapPalletAtVirtualLocation(lock.getCustomerId(), lock.getWarehouseId(),
return locks.scrapPalletAtVirtualLocation(PUBLIC_RESOURCE_SCOPE, lock.getWarehouseId(),
lock.getPalletId(), lock.getLocationId());
}
public int releaseLocation(WarehouseResourceLockEntity lock) {
return locks.releaseLocation(lock.getCustomerId(), lock.getWarehouseId(), lock.getLocationId());
return locks.releaseLocation(PUBLIC_RESOURCE_SCOPE, lock.getWarehouseId(), lock.getLocationId());
}
public int close(WarehouseResourceLockEntity lock, String status) { return locks.closeLock(lock.getId(), status); }
@@ -45,6 +45,10 @@ public class WarehouseResourceRepository {
.eq(WarehouseEntity::getId, warehouseId));
}
public WarehouseEntity findWarehouseById(long warehouseId) {
return warehouses.selectById(warehouseId);
}
public WarehouseLocationEntity findDefaultVirtualLocation(long customerId, long warehouseId) {
return locations.selectOne(new LambdaQueryWrapper<WarehouseLocationEntity>()
.eq(WarehouseLocationEntity::getCustomerId, customerId)
@@ -0,0 +1,19 @@
package com.baiyue.warehouse.security;
/** Canonical Admin authorities for overseas warehouse resources. */
public final class OverseasWarehousePermissions {
public static final String WAREHOUSE_LIST = "platform:overseas-warehouse:warehouse:list";
public static final String WAREHOUSE_CREATE = "platform:overseas-warehouse:warehouse:create";
public static final String WAREHOUSE_EDIT = "platform:overseas-warehouse:warehouse:edit";
public static final String WAREHOUSE_STATUS = "platform:overseas-warehouse:warehouse:status";
public static final String LOCATION_LIST = "platform:overseas-warehouse:location:list";
public static final String LOCATION_CREATE = "platform:overseas-warehouse:location:create";
public static final String LOCATION_EDIT = "platform:overseas-warehouse:location:edit";
public static final String LOCATION_DELETE = "platform:overseas-warehouse:location:delete";
public static final String PALLET_LIST = "platform:overseas-warehouse:pallet:list";
public static final String PALLET_CREATE = "platform:overseas-warehouse:pallet:create";
public static final String PALLET_EDIT = "platform:overseas-warehouse:pallet:edit";
public static final String PALLET_DELETE = "platform:overseas-warehouse:pallet:delete";
private OverseasWarehousePermissions() { }
}
@@ -22,6 +22,7 @@ import org.springframework.util.StringUtils;
@Service
public class WarehouseResourceService {
public static final long PUBLIC_RESOURCE_SCOPE = 0L;
private static final Set<String> LOCATION_STATUSES = Set.of("ACTIVE", "IDLE", "OCCUPIED", "INACTIVE");
private static final Set<String> PALLET_STATUSES = Set.of("IDLE", "OCCUPIED", "SCRAPPED");
@@ -187,13 +188,20 @@ public class WarehouseResourceService {
}
private void requireWarehouse(long customerId, long warehouseId) {
if (customerId == PUBLIC_RESOURCE_SCOPE) {
if (repository.findWarehouseById(warehouseId) == null) {
throw new BizException(ResultCode.NOT_FOUND, "Warehouse not found");
}
return;
}
if (!repository.warehouseOwned(customerId, warehouseId)) {
throw new BizException(ResultCode.NOT_FOUND, "当前租户下仓库不存在");
}
}
private WarehouseEntity requireWarehouseEntity(long customerId, long warehouseId) {
WarehouseEntity warehouse = repository.findWarehouse(customerId, warehouseId);
WarehouseEntity warehouse = customerId == PUBLIC_RESOURCE_SCOPE
? repository.findWarehouseById(warehouseId) : repository.findWarehouse(customerId, warehouseId);
if (warehouse == null) {
throw new BizException(ResultCode.NOT_FOUND, "Tenant warehouse not found");
}
@@ -18,17 +18,17 @@ spring:
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: ${NACOS_DISCOVERY_ENABLED:true}
register-enabled: ${NACOS_REGISTER_ENABLED:true}
group: ${NACOS_GROUP:BAIYUE}
namespace: ${NACOS_NAMESPACE:public}
config:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
namespace: ${NACOS_NAMESPACE:public}
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
@@ -34,20 +34,29 @@ class WarehouseResourceControllerTest {
PlatformWarehouseResourceController controller = platformController();
assertThatThrownBy(() -> controller.location(user(10001L, "MERCHANT"),
10002L, 20001L, 30001L))
20001L, 30001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("平台端");
}
@Test
void platformApiUsesExplicitTargetTenant() {
void platformApiUsesPublicResourceScope() {
PlatformWarehouseResourceController controller = platformController();
WarehouseLocationEntity expected = new WarehouseLocationEntity();
when(service.location(10002L, 20001L, 30001L)).thenReturn(expected);
when(service.location(0L, 20001L, 30001L)).thenReturn(expected);
controller.location(user(1L, "PLATFORM"), 10002L, 20001L, 30001L);
controller.location(user(0L, "PLATFORM"), 20001L, 30001L);
verify(service).location(10002L, 20001L, 30001L);
verify(service).location(0L, 20001L, 30001L);
}
@Test
void platformApiRejectsPlatformAppWithCustomerScope() {
PlatformWarehouseResourceController controller = platformController();
assertThatThrownBy(() -> controller.location(user(10001L, "PLATFORM"), 20001L, 30001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("平台端");
}
private static AuthenticatedUser user(long customerId, String appCode) {
@@ -0,0 +1,41 @@
package com.baiyue.warehouse.controller;
import io.swagger.v3.oas.annotations.Operation;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WarehouseResourcePermissionContractTest {
private static final String PREFIX = "platform:overseas-warehouse:";
@Test
void locationAndPalletEndpointsUseOnlyCanonicalAuthorities() {
Map<String, Method> methods = Arrays.stream(PlatformWarehouseResourceController.class.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(PreAuthorize.class))
.collect(Collectors.toMap(Method::getName, method -> method));
assertPermission(methods.get("locations"), PREFIX + "location:list");
assertPermission(methods.get("location"), PREFIX + "location:list");
assertPermission(methods.get("createLocation"), PREFIX + "location:create");
assertPermission(methods.get("updateLocation"), PREFIX + "location:edit");
assertPermission(methods.get("deleteLocation"), PREFIX + "location:delete");
assertPermission(methods.get("pallets"), PREFIX + "pallet:list");
assertPermission(methods.get("pallet"), PREFIX + "pallet:list");
assertPermission(methods.get("createPallet"), PREFIX + "pallet:create");
assertPermission(methods.get("updatePallet"), PREFIX + "pallet:edit");
assertPermission(methods.get("deletePallet"), PREFIX + "pallet:delete");
}
private static void assertPermission(Method method, String authority) {
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertEquals("hasAuthority('" + authority + "')", preAuthorize.value());
assertEquals("Required authority: " + authority, operation.description());
}
}
@@ -67,6 +67,23 @@ class WarehouseResourceServiceTest {
assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE);
}
@Test
void platformPalletUsesPublicScopeAgainstAnyExistingWarehouse() {
WarehouseEntity warehouse = warehouse();
when(repository.findWarehouseById(WAREHOUSE)).thenReturn(warehouse);
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(30001L);
when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location);
service.createPallet(0L, WAREHOUSE, new PalletRequests.SaveRequest(
"PLT-PUBLIC", "STANDARD", null, BigDecimal.ONE, "IDLE", 30001L, null));
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
verify(repository).insertPallet(captor.capture());
assertThat(captor.getValue().getCustomerId()).isZero();
assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE);
}
@Test
void rejectsResourceAccessWhenWarehouseDoesNotBelongToTenant() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(false);