暂存提交
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.procurement;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.baiyue.lastleg.procurement.mapper")
|
||||
public class ProcurementMapperConfiguration {
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.lastleg.procurement.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
|
||||
import com.baiyue.lastleg.procurement.service.OverseasProcurementService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/overseas-procurements")
|
||||
public class MerchantOverseasProcurementController {
|
||||
@Resource private OverseasProcurementService service;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> create(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody OverseasProcurementRequests.Create request) {
|
||||
return ApiResponse.success(service.create(customer(user), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<OverseasProcurementResponses.Summary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid OverseasProcurementRequests.Query query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String orderNo) {
|
||||
return ApiResponse.success(service.detail(customer(user), orderNo));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.baiyue.lastleg.procurement.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
|
||||
import com.baiyue.lastleg.procurement.service.OverseasProcurementService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/overseas-procurements")
|
||||
public class PlatformOverseasProcurementController {
|
||||
@Resource private OverseasProcurementService service;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<OverseasProcurementResponses.Summary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid OverseasProcurementRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(trim(customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@PathVariable String orderNo) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.detail(requiredCustomer(customerId), orderNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderNo}/review")
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> review(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.Review request) {
|
||||
requirePermission(user, OverseasProcurementService.REVIEW_PERMISSION);
|
||||
return ApiResponse.success(service.review(requiredCustomer(customerId), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PatchMapping("/{orderNo}")
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> update(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.Update request) {
|
||||
requirePermission(user, OverseasProcurementService.MANAGE_PERMISSION);
|
||||
return ApiResponse.success(service.update(requiredCustomer(customerId), orderNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderNo}/outbounds")
|
||||
public ApiResponse<OverseasProcurementResponses.Detail> addOutbound(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String customerId,
|
||||
@PathVariable String orderNo, @Valid @RequestBody OverseasProcurementRequests.AddOutbound request) {
|
||||
requirePermission(user, OverseasProcurementService.MANAGE_PERMISSION);
|
||||
return ApiResponse.success(service.addOutbound(requiredCustomer(customerId), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePermission(AuthenticatedUser user, String permission) {
|
||||
requirePlatform(user);
|
||||
if (user.permissions() == null || !user.permissions().contains(permission)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, permission + " permission is required");
|
||||
}
|
||||
}
|
||||
|
||||
private String requiredCustomer(String customerId) {
|
||||
String value = trim(customerId);
|
||||
if (value == null || "0".equals(value)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target customerId is required");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
|
||||
private String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.lastleg.procurement.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class OverseasProcurementRequests {
|
||||
private OverseasProcurementRequests() {}
|
||||
|
||||
public record Create(
|
||||
@Size(max = 255) String customerName,
|
||||
@NotBlank @Size(max = 128) String purchaseCategory,
|
||||
@NotBlank @Size(max = 255) String productName,
|
||||
@Size(max = 500) String productSpecification,
|
||||
@Size(max = 2000) String productUrl,
|
||||
@NotNull @Positive Long purchaseQuantity,
|
||||
@DecimalMin(value = "0.00") BigDecimal budgetUnitPrice,
|
||||
@Size(min = 3, max = 3) String currency,
|
||||
@Size(max = 1000) String receivingAddress,
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 64) String contactPhone,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record Query(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String orderNo, String customerKeyword, String purchaseCategory,
|
||||
String productName, String status, String purchaseWarehouseId,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {}
|
||||
|
||||
public record Review(@NotBlank String decision, @Size(max = 1000) String remark) {}
|
||||
|
||||
public record Update(
|
||||
@NotNull @Min(0) Long version,
|
||||
@Size(max = 128) String purchaseCategory,
|
||||
@Size(max = 255) String productName,
|
||||
@Size(max = 500) String productSpecification,
|
||||
@Size(max = 2000) String productUrl,
|
||||
@Min(0) Long inboundQuantity,
|
||||
@Size(max = 128) String purchaseWarehouseId,
|
||||
@Size(max = 255) String purchaseWarehouseName,
|
||||
@Size(max = 128) String domesticCarrier,
|
||||
@Size(max = 128) String domesticTrackingNo,
|
||||
@Size(max = 128) String overseasCarrier,
|
||||
@Size(max = 128) String overseasTrackingNo,
|
||||
LocalDateTime estimatedArrivalAt,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record AddOutbound(
|
||||
@NotNull @Positive Long quantity,
|
||||
@NotBlank @Size(max = 128) String outboundNo,
|
||||
@Size(max = 128) String carrier,
|
||||
@Size(max = 128) String trackingNo,
|
||||
LocalDateTime outboundAt,
|
||||
@Size(max = 1000) String remark) {}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.procurement.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class OverseasProcurementResponses {
|
||||
private OverseasProcurementResponses() {}
|
||||
|
||||
public record Summary(
|
||||
Long id, String orderNo, String customerId, String customerName,
|
||||
String purchaseCategory, String productName, String productSpecification,
|
||||
long purchaseQuantity, BigDecimal budgetUnitPrice, String currency,
|
||||
String purchaseWarehouseId, String purchaseWarehouseName,
|
||||
long inboundQuantity, long outboundQuantity, long inventoryQuantity,
|
||||
String status, long version, LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record Outbound(
|
||||
Long id, long quantity, String outboundNo, String carrier, String trackingNo,
|
||||
LocalDateTime outboundAt, String remark, String createdBy, LocalDateTime createdAt) {}
|
||||
|
||||
public record History(
|
||||
String fromStatus, String toStatus, String operatorId, String remark,
|
||||
LocalDateTime createdAt) {}
|
||||
|
||||
public record Detail(
|
||||
Summary summary, String productUrl, String receivingAddress,
|
||||
String contactName, String contactPhone,
|
||||
String domesticCarrier, String domesticTrackingNo,
|
||||
String overseasCarrier, String overseasTrackingNo,
|
||||
LocalDateTime estimatedArrivalAt, String remark,
|
||||
String createdBy, String reviewedBy, String reviewRemark, LocalDateTime reviewedAt,
|
||||
List<Outbound> outbounds, List<History> histories) {}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.lastleg.procurement.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_overseas_procurement_order")
|
||||
public class OverseasProcurementOrder {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String customerName;
|
||||
private String orderNo;
|
||||
private String purchaseCategory;
|
||||
private String productName;
|
||||
private String productSpecification;
|
||||
private String productUrl;
|
||||
private Long purchaseQuantity;
|
||||
private BigDecimal budgetUnitPrice;
|
||||
private String currency;
|
||||
private String receivingAddress;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String purchaseWarehouseId;
|
||||
private String purchaseWarehouseName;
|
||||
private Long inboundQuantity;
|
||||
private Long outboundQuantity;
|
||||
private String domesticCarrier;
|
||||
private String domesticTrackingNo;
|
||||
private String overseasCarrier;
|
||||
private String overseasTrackingNo;
|
||||
private LocalDateTime estimatedArrivalAt;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
private String reviewedBy;
|
||||
private String reviewRemark;
|
||||
private LocalDateTime reviewedAt;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.procurement.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_overseas_procurement_outbound")
|
||||
public class OverseasProcurementOutbound {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long orderId;
|
||||
private String customerId;
|
||||
private Long quantity;
|
||||
private String outboundNo;
|
||||
private String carrier;
|
||||
private String trackingNo;
|
||||
private LocalDateTime outboundAt;
|
||||
private String remark;
|
||||
private String createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.procurement.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_overseas_procurement_status_history")
|
||||
public class OverseasProcurementStatusHistory {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long orderId;
|
||||
private String customerId;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
private String operatorId;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.procurement.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface OverseasProcurementOrderMapper extends BaseMapper<OverseasProcurementOrder> {
|
||||
@Select("SELECT * FROM ll_overseas_procurement_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1")
|
||||
OverseasProcurementOrder findNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
|
||||
|
||||
@Select("SELECT * FROM ll_overseas_procurement_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
OverseasProcurementOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_overseas_procurement_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1 FOR UPDATE")
|
||||
OverseasProcurementOrder lockNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
|
||||
|
||||
@Update("UPDATE ll_overseas_procurement_order SET outbound_quantity=outbound_quantity+#{quantity},version=version+1," +
|
||||
"updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND customer_id=#{customerId} " +
|
||||
"AND inbound_quantity-outbound_quantity>=#{quantity}")
|
||||
int addOutbound(@Param("id") Long id, @Param("customerId") String customerId, @Param("quantity") long quantity);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.procurement.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOutbound;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface OverseasProcurementOutboundMapper extends BaseMapper<OverseasProcurementOutbound> {
|
||||
@Select("SELECT * FROM ll_overseas_procurement_outbound WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY outbound_at DESC,id DESC")
|
||||
List<OverseasProcurementOutbound> listOrder(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.procurement.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface OverseasProcurementStatusHistoryMapper extends BaseMapper<OverseasProcurementStatusHistory> {
|
||||
@Select("SELECT * FROM ll_overseas_procurement_status_history WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY created_at,id")
|
||||
List<OverseasProcurementStatusHistory> listOrder(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
package com.baiyue.lastleg.procurement.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementRequests;
|
||||
import com.baiyue.lastleg.procurement.dto.OverseasProcurementResponses;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOrder;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementOutbound;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOrderMapper;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementOutboundMapper;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementStatusHistoryMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class OverseasProcurementService {
|
||||
public static final String REVIEW_PERMISSION = "lastleg:overseas-procurement:review";
|
||||
public static final String MANAGE_PERMISSION = "lastleg:overseas-procurement:manage";
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
public static final String APPROVED = "APPROVED";
|
||||
public static final String REJECTED = "REJECTED";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED);
|
||||
|
||||
@Resource private OverseasProcurementOrderMapper orders;
|
||||
@Resource private OverseasProcurementOutboundMapper outbounds;
|
||||
@Resource private OverseasProcurementStatusHistoryMapper histories;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public OverseasProcurementResponses.Detail create(String customerId, String operatorId,
|
||||
OverseasProcurementRequests.Create request) {
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
OverseasProcurementOrder replay = orders.findKey(customerId, key);
|
||||
if (replay != null) return replay(customerId, replay, requestHash);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OverseasProcurementOrder order = new OverseasProcurementOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setCustomerName(trim(request.customerName()));
|
||||
order.setOrderNo("HC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId());
|
||||
order.setPurchaseCategory(request.purchaseCategory().trim());
|
||||
order.setProductName(request.productName().trim());
|
||||
order.setProductSpecification(trim(request.productSpecification()));
|
||||
order.setProductUrl(trim(request.productUrl()));
|
||||
order.setPurchaseQuantity(request.purchaseQuantity());
|
||||
order.setBudgetUnitPrice(request.budgetUnitPrice());
|
||||
order.setCurrency(defaultCurrency(request.currency()));
|
||||
order.setReceivingAddress(trim(request.receivingAddress()));
|
||||
order.setContactName(request.contactName().trim());
|
||||
order.setContactPhone(request.contactPhone().trim());
|
||||
order.setInboundQuantity(0L);
|
||||
order.setOutboundQuantity(0L);
|
||||
order.setStatus(PENDING_REVIEW);
|
||||
order.setRemark(trim(request.remark()));
|
||||
order.setIdempotencyKey(key);
|
||||
order.setRequestHash(requestHash);
|
||||
order.setCreatedBy(operatorId);
|
||||
order.setVersion(0L);
|
||||
order.setCreatedAt(now);
|
||||
order.setUpdatedAt(now);
|
||||
try {
|
||||
orders.insert(order);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
OverseasProcurementOrder concurrent = orders.findKey(customerId, key);
|
||||
if (concurrent != null) return replay(customerId, concurrent, requestHash);
|
||||
throw exception;
|
||||
}
|
||||
history(order, null, PENDING_REVIEW, operatorId, request.remark(), now);
|
||||
return detail(customerId, order.getOrderNo());
|
||||
}
|
||||
|
||||
public PageResult<OverseasProcurementResponses.Summary> page(
|
||||
String customerId, OverseasProcurementRequests.Query query) {
|
||||
validateQuery(query);
|
||||
LambdaQueryWrapper<OverseasProcurementOrder> wrapper = new LambdaQueryWrapper<OverseasProcurementOrder>()
|
||||
.eq(StringUtils.hasText(customerId), OverseasProcurementOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), OverseasProcurementOrder::getOrderNo, query.orderNo())
|
||||
.and(StringUtils.hasText(query.customerKeyword()), value -> value
|
||||
.like(OverseasProcurementOrder::getCustomerId, query.customerKeyword())
|
||||
.or().like(OverseasProcurementOrder::getCustomerName, query.customerKeyword()))
|
||||
.like(StringUtils.hasText(query.purchaseCategory()), OverseasProcurementOrder::getPurchaseCategory,
|
||||
query.purchaseCategory())
|
||||
.like(StringUtils.hasText(query.productName()), OverseasProcurementOrder::getProductName,
|
||||
query.productName())
|
||||
.eq(StringUtils.hasText(query.status()), OverseasProcurementOrder::getStatus,
|
||||
StringUtils.hasText(query.status()) ? query.status().trim().toUpperCase() : null)
|
||||
.eq(StringUtils.hasText(query.purchaseWarehouseId()), OverseasProcurementOrder::getPurchaseWarehouseId,
|
||||
query.purchaseWarehouseId())
|
||||
.ge(query.createdFrom() != null, OverseasProcurementOrder::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, OverseasProcurementOrder::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(OverseasProcurementOrder::getCreatedAt)
|
||||
.orderByDesc(OverseasProcurementOrder::getId);
|
||||
Page<OverseasProcurementOrder> result = orders.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
return new PageResult<>(result.getRecords().stream().map(this::summary).toList(), query.page(), query.size(),
|
||||
result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
public OverseasProcurementResponses.Detail detail(String customerId, String orderNo) {
|
||||
OverseasProcurementOrder order = orders.findNo(customerId, orderNo);
|
||||
if (order == null) throw notFound("Overseas procurement order not found");
|
||||
return new OverseasProcurementResponses.Detail(summary(order), order.getProductUrl(),
|
||||
order.getReceivingAddress(), order.getContactName(), order.getContactPhone(),
|
||||
order.getDomesticCarrier(), order.getDomesticTrackingNo(), order.getOverseasCarrier(),
|
||||
order.getOverseasTrackingNo(), order.getEstimatedArrivalAt(), order.getRemark(),
|
||||
order.getCreatedBy(), order.getReviewedBy(), order.getReviewRemark(), order.getReviewedAt(),
|
||||
outbounds.listOrder(customerId, order.getId()).stream().map(this::outbound).toList(),
|
||||
histories.listOrder(customerId, order.getId()).stream().map(value ->
|
||||
new OverseasProcurementResponses.History(value.getFromStatus(), value.getToStatus(),
|
||||
value.getOperatorId(), value.getRemark(), value.getCreatedAt())).toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OverseasProcurementResponses.Detail review(String customerId, String orderNo, String operatorId,
|
||||
OverseasProcurementRequests.Review request) {
|
||||
String next = switch (request.decision().trim().toUpperCase()) {
|
||||
case "APPROVE", APPROVED -> APPROVED;
|
||||
case "REJECT", REJECTED -> REJECTED;
|
||||
default -> throw badRequest("Review decision must be APPROVE or REJECT");
|
||||
};
|
||||
OverseasProcurementOrder order = lock(customerId, orderNo);
|
||||
if (next.equals(order.getStatus())) return detail(customerId, orderNo);
|
||||
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("Overseas procurement order was already reviewed");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
order.setStatus(next);
|
||||
order.setReviewedBy(operatorId);
|
||||
order.setReviewRemark(trim(request.remark()));
|
||||
order.setReviewedAt(now);
|
||||
touch(order, now);
|
||||
orders.updateById(order);
|
||||
history(order, PENDING_REVIEW, next, operatorId, request.remark(), now);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OverseasProcurementResponses.Detail update(String customerId, String orderNo,
|
||||
OverseasProcurementRequests.Update request) {
|
||||
OverseasProcurementOrder order = lock(customerId, orderNo);
|
||||
if (!request.version().equals(order.getVersion())) throw conflict("Order was changed; refresh and retry");
|
||||
if (request.inboundQuantity() != null) {
|
||||
if (!APPROVED.equals(order.getStatus())) throw conflict("Only an approved procurement order can receive stock");
|
||||
if (request.inboundQuantity() < value(order.getOutboundQuantity())) {
|
||||
throw conflict("Inbound quantity cannot be lower than the recorded outbound quantity");
|
||||
}
|
||||
order.setInboundQuantity(request.inboundQuantity());
|
||||
}
|
||||
setIfText(request.purchaseCategory(), order::setPurchaseCategory);
|
||||
setIfText(request.productName(), order::setProductName);
|
||||
setIfPresent(request.productSpecification(), order::setProductSpecification);
|
||||
setIfPresent(request.productUrl(), order::setProductUrl);
|
||||
setIfPresent(request.purchaseWarehouseId(), order::setPurchaseWarehouseId);
|
||||
setIfPresent(request.purchaseWarehouseName(), order::setPurchaseWarehouseName);
|
||||
setIfPresent(request.domesticCarrier(), order::setDomesticCarrier);
|
||||
setIfPresent(request.domesticTrackingNo(), order::setDomesticTrackingNo);
|
||||
setIfPresent(request.overseasCarrier(), order::setOverseasCarrier);
|
||||
setIfPresent(request.overseasTrackingNo(), order::setOverseasTrackingNo);
|
||||
if (request.estimatedArrivalAt() != null) order.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
setIfPresent(request.remark(), order::setRemark);
|
||||
touch(order, LocalDateTime.now());
|
||||
orders.updateById(order);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OverseasProcurementResponses.Detail addOutbound(String customerId, String orderNo, String operatorId,
|
||||
OverseasProcurementRequests.AddOutbound request) {
|
||||
OverseasProcurementOrder order = lock(customerId, orderNo);
|
||||
if (!APPROVED.equals(order.getStatus())) throw conflict("Only an approved procurement order can ship stock");
|
||||
if (orders.addOutbound(order.getId(), customerId, request.quantity()) != 1) {
|
||||
throw conflict("Outbound quantity exceeds current procurement inventory");
|
||||
}
|
||||
OverseasProcurementOutbound outbound = new OverseasProcurementOutbound();
|
||||
outbound.setId(IdWorker.getId());
|
||||
outbound.setOrderId(order.getId());
|
||||
outbound.setCustomerId(customerId);
|
||||
outbound.setQuantity(request.quantity());
|
||||
outbound.setOutboundNo(request.outboundNo().trim());
|
||||
outbound.setCarrier(trim(request.carrier()));
|
||||
outbound.setTrackingNo(trim(request.trackingNo()));
|
||||
outbound.setOutboundAt(request.outboundAt() == null ? LocalDateTime.now() : request.outboundAt());
|
||||
outbound.setRemark(trim(request.remark()));
|
||||
outbound.setCreatedBy(operatorId);
|
||||
outbound.setCreatedAt(LocalDateTime.now());
|
||||
try {
|
||||
outbounds.insert(outbound);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw conflict("Outbound number already exists for this procurement order");
|
||||
}
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
private OverseasProcurementResponses.Detail replay(String customerId, OverseasProcurementOrder order, String hash) {
|
||||
if (!hash.equals(order.getRequestHash())) {
|
||||
throw conflict("Idempotency key was reused with different procurement data");
|
||||
}
|
||||
return detail(customerId, order.getOrderNo());
|
||||
}
|
||||
|
||||
private OverseasProcurementOrder lock(String customerId, String orderNo) {
|
||||
OverseasProcurementOrder order = orders.lockNo(customerId, orderNo);
|
||||
if (order == null) throw notFound("Overseas procurement order not found");
|
||||
return order;
|
||||
}
|
||||
|
||||
private OverseasProcurementResponses.Summary summary(OverseasProcurementOrder order) {
|
||||
long inbound = value(order.getInboundQuantity());
|
||||
long outbound = value(order.getOutboundQuantity());
|
||||
return new OverseasProcurementResponses.Summary(order.getId(), order.getOrderNo(), order.getCustomerId(),
|
||||
order.getCustomerName(), order.getPurchaseCategory(), order.getProductName(),
|
||||
order.getProductSpecification(), value(order.getPurchaseQuantity()), order.getBudgetUnitPrice(),
|
||||
order.getCurrency(), order.getPurchaseWarehouseId(), order.getPurchaseWarehouseName(), inbound, outbound,
|
||||
inbound - outbound, order.getStatus(), value(order.getVersion()), order.getCreatedAt(), order.getUpdatedAt());
|
||||
}
|
||||
|
||||
private OverseasProcurementResponses.Outbound outbound(OverseasProcurementOutbound value) {
|
||||
return new OverseasProcurementResponses.Outbound(value.getId(), value(value.getQuantity()), value.getOutboundNo(),
|
||||
value.getCarrier(), value.getTrackingNo(), value.getOutboundAt(), value.getRemark(), value.getCreatedBy(),
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
private void history(OverseasProcurementOrder order, String from, String to, String operator,
|
||||
String remark, LocalDateTime now) {
|
||||
OverseasProcurementStatusHistory history = new OverseasProcurementStatusHistory();
|
||||
history.setId(IdWorker.getId());
|
||||
history.setOrderId(order.getId());
|
||||
history.setCustomerId(order.getCustomerId());
|
||||
history.setFromStatus(from);
|
||||
history.setToStatus(to);
|
||||
history.setOperatorId(operator);
|
||||
history.setRemark(trim(remark));
|
||||
history.setCreatedAt(now);
|
||||
histories.insert(history);
|
||||
}
|
||||
|
||||
private void validateQuery(OverseasProcurementRequests.Query query) {
|
||||
if (StringUtils.hasText(query.status()) && !STATUSES.contains(query.status().trim().toUpperCase())) {
|
||||
throw badRequest("Unsupported procurement status");
|
||||
}
|
||||
if (query.createdFrom() != null && query.createdTo() != null
|
||||
&& query.createdFrom().isAfter(query.createdTo())) throw badRequest("createdFrom must not be after createdTo");
|
||||
}
|
||||
|
||||
private String hash(Object value) {
|
||||
try {
|
||||
byte[] json = objectMapper.writeValueAsBytes(value);
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json));
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("Cannot hash procurement request", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void touch(OverseasProcurementOrder order, LocalDateTime now) {
|
||||
order.setVersion(value(order.getVersion()) + 1);
|
||||
order.setUpdatedAt(now);
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0 : value; }
|
||||
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
private static String defaultCurrency(String value) { return trim(value) == null ? "USD" : value.trim().toUpperCase(); }
|
||||
private static void setIfText(String value, java.util.function.Consumer<String> setter) {
|
||||
if (StringUtils.hasText(value)) setter.accept(value.trim());
|
||||
}
|
||||
private static void setIfPresent(String value, java.util.function.Consumer<String> setter) {
|
||||
if (value != null) setter.accept(trim(value));
|
||||
}
|
||||
private static ResponseStatusException badRequest(String message) {
|
||||
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
private static ResponseStatusException notFound(String message) {
|
||||
return new ResponseStatusException(HttpStatus.NOT_FOUND, message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user