This commit is contained in:
2026-08-05 16:26:10 +08:00
parent 06f7f9d2f2
commit 5ba9716e12
155 changed files with 2883 additions and 551 deletions
@@ -18,6 +18,6 @@ public class InboundProperties {
private boolean allowRejectedMeasurement = false;
private BigDecimal defaultPalletCapacityCbm = new BigDecimal("1.200000");
private Duration reservationTtl = Duration.ofMinutes(30);
/** 共享收货仓资源所属租户;0 表示与入库业务租户一致。 */
/** 历史配置兼容字段;平台公共仓资源当前固定使用技术作用域 0。 */
private long warehouseResourceTenantId = 0L;
}
@@ -3,10 +3,9 @@ package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.MerchantInboundBillingResponse;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.service.MerchantInboundBillingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -33,10 +32,7 @@ public class MerchantInboundBillingController {
description = "客户只读查看客服审核通过后发布的费用,不需要确认费用。")
public ApiResponse<MerchantInboundBillingResponse> find(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
if (user == null || !"MERCHANT".equals(user.appCode()) || user.tenantId() == null) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
}
return ApiResponse.success(service.find(user.tenantId(), orderId));
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
}
@GetMapping("/{orderId}/inbound-billing/images/{fileId}")
@@ -46,10 +42,7 @@ public class MerchantInboundBillingController {
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long orderId,
@PathVariable long fileId) {
if (user == null || !"MERCHANT".equals(user.appCode()) || user.tenantId() == null) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
}
var image = service.downloadImage(user.tenantId(), orderId, fileId);
var image = service.downloadImage(FirstlegScopeGuard.merchant(user), orderId, fileId);
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.contentType(MediaType.parseMediaType(image.contentType()))
.contentLength(image.content().length).body(image.content());
@@ -4,8 +4,6 @@ import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.FbaPricingDtos.CarrierRateVersionRequest;
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
@@ -114,9 +112,7 @@ public class PlatformFbaMasterDataController {
}
private static Long optionalPlatformTenant(AuthenticatedUser user, Long tenantId) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
FirstlegScopeGuard.platform(user);
return tenantId == null ? null : FirstlegScopeGuard.platform(user, tenantId);
}
}
@@ -3,14 +3,13 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.AssignRequest;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.service.FulfillmentTaskService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -65,8 +64,7 @@ public class PlatformFulfillmentAssignmentController {
@Parameter(description = "First-leg fulfillment task ID / 头程履约任务 ID", example = "190000000000000101", required = true)
@PathVariable long taskId,
@Valid @RequestBody AssignRequest request) {
requirePlatform(user);
requireTenant(tenantId);
FirstlegScopeGuard.platform(user, tenantId);
service.assign(
tenantId,
user.userId(),
@@ -89,8 +87,7 @@ public class PlatformFulfillmentAssignmentController {
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@Valid FulfillmentTaskQuery query) {
requirePlatform(user);
requireTenant(tenantId);
FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.page(tenantId, query));
}
@@ -105,8 +102,7 @@ public class PlatformFulfillmentAssignmentController {
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long taskId) {
requirePlatform(user);
requireTenant(tenantId);
FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.detail(tenantId, taskId));
}
@@ -121,8 +117,7 @@ public class PlatformFulfillmentAssignmentController {
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long orderId) {
requirePlatform(user);
requireTenant(tenantId);
FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.byOrder(tenantId, orderId));
}
@@ -139,20 +134,8 @@ public class PlatformFulfillmentAssignmentController {
@PathVariable long taskId,
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) {
requirePlatform(user);
requireTenant(tenantId);
FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.assignmentHistory(tenantId, taskId, page, size));
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
}
private static void requireTenant(long tenantId) {
if (tenantId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
}
}
}
@@ -9,6 +9,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.service.InboundMeasurementService;
import com.baiyue.firstleg.service.MeasurementSubmissionService;
import io.swagger.v3.oas.annotations.Operation;
@@ -120,10 +121,8 @@ public class PlatformInboundMeasurementController {
}
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
if (tenantId <= 0 || user.userId() == null || user.userId() <= 0) {
FirstlegScopeGuard.platform(user, tenantId);
if (user.userId() == null || user.userId() <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户和操作人");
}
}
@@ -5,14 +5,13 @@ import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
import com.baiyue.firstleg.service.MeasurementReviewService;
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -108,11 +107,6 @@ public class PlatformInboundMeasurementReviewController {
}
private static void validateScope(AuthenticatedUser user, long tenantId) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
if (tenantId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
}
FirstlegScopeGuard.platform(user, tenantId);
}
}
@@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.InboundOrderDetail;
import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.dto.InboundOrderSummary;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.service.InboundOrderQueryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -61,15 +62,10 @@ public class PlatformInboundOrderController {
}
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
requirePlatform(user);
if (tenantId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
}
FirstlegScopeGuard.platform(user, tenantId);
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
FirstlegScopeGuard.platform(user);
}
}
@@ -11,7 +11,8 @@ public interface InboundIdempotencyMapper extends BaseMapper<InboundIdempotencyE
@Update("""
UPDATE fl_inbound_idempotency
SET status = 'SUCCESS', result_json = #{resultJson}, completed_at = CURRENT_TIMESTAMP(3)
WHERE id = #{id} AND status = 'PROCESSING'
WHERE tenant_id = #{tenantId} AND id = #{id} AND status = 'PROCESSING'
""")
int complete(@Param("id") long id, @Param("resultJson") String resultJson);
int complete(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("resultJson") String resultJson);
}
@@ -250,6 +250,7 @@ public class FbaPricingRepository {
public int completeIdempotency(InboundPricingIdempotencyEntity value, long resourceId, LocalDateTime now) {
return idempotencies.update(null, new LambdaUpdateWrapper<InboundPricingIdempotencyEntity>()
.eq(InboundPricingIdempotencyEntity::getTenantId, value.getTenantId())
.eq(InboundPricingIdempotencyEntity::getId, value.getId())
.eq(InboundPricingIdempotencyEntity::getStatus, "PROCESSING")
.set(InboundPricingIdempotencyEntity::getStatus, "COMPLETED")
@@ -60,11 +60,15 @@ public class FeeItemRepository {
}
public int updateItem(FeeItemEntity item) {
return feeItems.updateById(item);
return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, item.getTenantId())
.eq(FeeItemEntity::getId, item.getId()));
}
public int deleteItem(long id) {
return feeItems.deleteById(id);
public int deleteItem(long tenantId, long id) {
return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getId, id));
}
public long countBindings(long tenantId, long feeItemId) {
@@ -99,11 +103,15 @@ public class FeeItemRepository {
}
public int updateDetail(InboundFeeDetailEntity detail) {
return feeDetails.updateById(detail);
return feeDetails.update(detail, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, detail.getTenantId())
.eq(InboundFeeDetailEntity::getId, detail.getId()));
}
public int deleteDetail(long detailId) {
return feeDetails.deleteById(detailId);
public int deleteDetail(long tenantId, long detailId) {
return feeDetails.delete(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getId, detailId));
}
public int review(long tenantId, long inboundId, long detailId, String targetStatus,
@@ -5,6 +5,7 @@ import jakarta.annotation.Resource;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
@@ -53,7 +54,9 @@ public class FulfillmentTaskRepository {
}
public boolean update(FulfillmentTaskEntity task) {
return tasks.updateById(task) == 1;
return tasks.update(task, new LambdaUpdateWrapper<FulfillmentTaskEntity>()
.eq(FulfillmentTaskEntity::getTenantId, task.getTenantId())
.eq(FulfillmentTaskEntity::getId, task.getId())) == 1;
}
public void insertHistory(AssignmentHistoryEntity history) {
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
@@ -42,7 +43,9 @@ public class InboundMeasurementRepository {
}
public boolean updateInbound(InboundOrderEntity inbound) {
return inboundOrders.updateById(inbound) == 1;
return inboundOrders.update(inbound, new LambdaUpdateWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, inbound.getTenantId())
.eq(InboundOrderEntity::getId, inbound.getId())) == 1;
}
public InboundMeasurementEntity findMeasurement(long tenantId, long inboundId, long measurementId) {
@@ -72,7 +75,9 @@ public class InboundMeasurementRepository {
}
public boolean updateMeasurement(InboundMeasurementEntity measurement) {
return measurements.updateById(measurement) == 1;
return measurements.update(measurement, new LambdaUpdateWrapper<InboundMeasurementEntity>()
.eq(InboundMeasurementEntity::getTenantId, measurement.getTenantId())
.eq(InboundMeasurementEntity::getId, measurement.getId())) == 1;
}
public void replaceItems(long tenantId, long measurementId,
@@ -109,7 +114,9 @@ public class InboundMeasurementRepository {
}
public boolean updateWeight(InboundWeightResultEntity weight) {
return weights.updateById(weight) == 1;
return weights.update(weight, new LambdaUpdateWrapper<InboundWeightResultEntity>()
.eq(InboundWeightResultEntity::getTenantId, weight.getTenantId())
.eq(InboundWeightResultEntity::getId, weight.getId())) == 1;
}
@@ -108,8 +108,8 @@ public class InboundWorkflowRepository {
.eq(InboundIdempotencyEntity::getIdempotencyKey, key));
}
public int completeIdempotency(long id, String resultJson) {
return idempotencies.complete(id, resultJson);
public int completeIdempotency(long tenantId, long id, String resultJson) {
return idempotencies.complete(tenantId, id, resultJson);
}
public InboundFeeEntity findConfirmedFee(long tenantId, long inboundId, long feeId) {
@@ -67,7 +67,7 @@ public class FeeItemService {
if (repository.countBindings(tenantId, id) > 0) {
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
}
if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
if (repository.deleteItem(tenantId, id) != 1) throw conflict("费用项目删除冲突");
}
private FeeItemEntity required(long tenantId, long id) {
@@ -4,17 +4,30 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
/** 头程平台接口租户边界校验。 */
/** 头程平台与客户接口的主体边界校验。 */
public final class FirstlegScopeGuard {
private FirstlegScopeGuard() {}
public static long platform(AuthenticatedUser user, long targetTenantId) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
platform(user);
if (targetTenantId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
}
return targetTenantId;
}
public static void platform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode())
|| user.tenantId() == null || user.tenantId() != 0L) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
}
public static long merchant(AuthenticatedUser user) {
if (user == null || !"MERCHANT".equals(user.appCode())
|| user.tenantId() == null || user.tenantId() <= 0L) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
}
return user.tenantId();
}
}
@@ -59,7 +59,7 @@ public class InboundFeeDetailService {
@Transactional
public void delete(long tenantId, long inboundId, long detailId) {
requiredEditable(tenantId, inboundId, detailId);
if (repository.deleteDetail(detailId) != 1) throw conflict("费用明细删除冲突");
if (repository.deleteDetail(tenantId, detailId) != 1) throw conflict("费用明细删除冲突");
}
@Transactional
@@ -44,7 +44,7 @@ public class InboundIdempotencyService {
record.setUpdatedAt(record.getCreatedAt());
try {
repository.insertIdempotency(record);
return new Context(record.getId(), false);
return new Context(tenantId, record.getId(), false);
} catch (DuplicateKeyException duplicate) {
InboundIdempotencyEntity existing = repository.findIdempotency(
tenantId, operation, operatorId, key);
@@ -53,7 +53,7 @@ public class InboundIdempotencyService {
"Idempotency-Key 已用于不同请求");
}
if ("SUCCESS".equals(existing.getStatus())) {
return new Context(existing.getId(), true);
return new Context(tenantId, existing.getId(), true);
}
throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中,请稍后重试");
}
@@ -61,7 +61,8 @@ public class InboundIdempotencyService {
public void complete(Context context, long inboundId) {
if (!context.replayed()
&& repository.completeIdempotency(context.id(), "{\"inboundId\":\"" + inboundId + "\"}") != 1) {
&& repository.completeIdempotency(context.tenantId(), context.id(),
"{\"inboundId\":\"" + inboundId + "\"}") != 1) {
throw new BizException(ResultCode.CONFLICT, "幂等记录已被并发修改");
}
}
@@ -75,6 +76,6 @@ public class InboundIdempotencyService {
}
}
public record Context(long id, boolean replayed) {
public record Context(long tenantId, long id, boolean replayed) {
}
}
@@ -20,6 +20,7 @@ import java.time.LocalDateTime;
@Service
public class MeasurementLocationOccupationService {
private static final String SOURCE_SERVICE = "firstleg-service";
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
@Resource
private WarehousePalletLocationClient warehouse;
@Resource
@@ -35,8 +36,7 @@ public class MeasurementLocationOccupationService {
int measurementVersion) {
InboundOrderEntity order = requireOrder(tenantId, inboundId);
long warehouseId = requireWarehouse(order);
long resourceTenantId = properties.getWarehouseResourceTenantId() > 0
? properties.getWarehouseResourceTenantId() : tenantId;
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
BigDecimal volume = requireVolume(order);
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion;
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
import com.baiyue.firstleg.entity.ThirdPartyCallLogEntity;
import com.baiyue.firstleg.mapper.ThirdPartyCallLogMapper;
@@ -55,6 +56,10 @@ public class ThirdPartyCallLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void complete(long id, String status, String code, String message, String resultJson) {
Long tenantId = tenantId(id);
if (tenantId == null) {
return;
}
ThirdPartyCallLogEntity update = new ThirdPartyCallLogEntity();
update.setId(id);
update.setProcessingStatus(status);
@@ -62,18 +67,25 @@ public class ThirdPartyCallLogService {
update.setResultMessage(limit(message, 1000));
update.setResultJson(resultJson);
update.setProcessedAt(LocalDateTime.now());
mapper.updateById(update);
mapper.update(update, new LambdaUpdateWrapper<ThirdPartyCallLogEntity>()
.eq(ThirdPartyCallLogEntity::getTenantId, tenantId)
.eq(ThirdPartyCallLogEntity::getId, id));
}
/** 签名通过后原子认领 Idempotency-Key;非法签名不会污染幂等空间。 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean claimIdempotency(long logId) {
Long tenantId = tenantId(logId);
if (tenantId == null) {
return false;
}
ThirdPartyCallLogEntity update = new ThirdPartyCallLogEntity();
update.setId(logId);
update.setIdempotencyAnchor(1);
try {
mapper.updateById(update);
return true;
return mapper.update(update, new LambdaUpdateWrapper<ThirdPartyCallLogEntity>()
.eq(ThirdPartyCallLogEntity::getTenantId, tenantId)
.eq(ThirdPartyCallLogEntity::getId, logId)) == 1;
} catch (DuplicateKeyException exception) {
return false;
}
@@ -87,6 +99,11 @@ public class ThirdPartyCallLogService {
.eq(ThirdPartyCallLogEntity::getIdempotencyAnchor, 1));
}
private Long tenantId(long id) {
ThirdPartyCallLogEntity log = mapper.selectById(id);
return log == null ? null : log.getTenantId();
}
private String limit(String value, int max) {
if (value == null || value.length() <= max) {
return value;