diff --git a/backend/src/main/java/com/linhelp/merchant/MerchantController.java b/backend/src/main/java/com/linhelp/merchant/MerchantController.java new file mode 100644 index 0000000..b3154bb --- /dev/null +++ b/backend/src/main/java/com/linhelp/merchant/MerchantController.java @@ -0,0 +1,61 @@ +package com.linhelp.merchant; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import com.linhelp.common.security.RequireRole; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/api/admin/merchants") +@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) +public class MerchantController { + private final MerchantService merchantService; + private final AuthService authService; + + public MerchantController(MerchantService merchantService, AuthService authService) { + this.merchantService = merchantService; + this.authService = authService; + } + + @GetMapping + public ApiResponse> list() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(merchantService.list(user.getCommunityId())); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody MerchantRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(merchantService.create(request)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable Long id, @Valid @RequestBody MerchantRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(merchantService.update(id, request)); + } + + @PutMapping("/{id}/enable") + public ApiResponse enable(@PathVariable Long id) { + return ApiResponse.ok(merchantService.enable(id)); + } + + @PutMapping("/{id}/disable") + public ApiResponse disable(@PathVariable Long id) { + return ApiResponse.ok(merchantService.disable(id)); + } +} diff --git a/backend/src/main/java/com/linhelp/merchant/MerchantRequest.java b/backend/src/main/java/com/linhelp/merchant/MerchantRequest.java new file mode 100644 index 0000000..8208f5a --- /dev/null +++ b/backend/src/main/java/com/linhelp/merchant/MerchantRequest.java @@ -0,0 +1,65 @@ +package com.linhelp.merchant; + +import javax.validation.constraints.NotBlank; + +public class MerchantRequest { + private Long tenantId; + private Long communityId; + + @NotBlank + private String name; + + @NotBlank + private String merchantType; + + private String contactName; + private String contactPhone; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMerchantType() { + return merchantType; + } + + public void setMerchantType(String merchantType) { + this.merchantType = merchantType; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } +} diff --git a/backend/src/main/java/com/linhelp/merchant/MerchantResponse.java b/backend/src/main/java/com/linhelp/merchant/MerchantResponse.java new file mode 100644 index 0000000..4894ac9 --- /dev/null +++ b/backend/src/main/java/com/linhelp/merchant/MerchantResponse.java @@ -0,0 +1,76 @@ +package com.linhelp.merchant; + +public class MerchantResponse { + private Long id; + private Long tenantId; + private Long communityId; + private String name; + private String merchantType; + private String contactName; + private String contactPhone; + private String status; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMerchantType() { + return merchantType; + } + + public void setMerchantType(String merchantType) { + this.merchantType = merchantType; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/backend/src/main/java/com/linhelp/merchant/MerchantService.java b/backend/src/main/java/com/linhelp/merchant/MerchantService.java new file mode 100644 index 0000000..bfa27cb --- /dev/null +++ b/backend/src/main/java/com/linhelp/merchant/MerchantService.java @@ -0,0 +1,68 @@ +package com.linhelp.merchant; + +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class MerchantService { + private final AtomicLong ids = new AtomicLong(1L); + private final Map merchants = new LinkedHashMap(); + + public synchronized MerchantResponse create(MerchantRequest request) { + MerchantResponse merchant = new MerchantResponse(); + merchant.setId(ids.getAndIncrement()); + copy(request, merchant); + merchant.setStatus("ENABLED"); + merchants.put(merchant.getId(), merchant); + return merchant; + } + + public synchronized List list(Long communityId) { + List result = new ArrayList(); + for (MerchantResponse merchant : merchants.values()) { + if (communityId.equals(merchant.getCommunityId())) { + result.add(merchant); + } + } + return result; + } + + public synchronized MerchantResponse update(Long id, MerchantRequest request) { + MerchantResponse merchant = merchants.get(id); + if (merchant == null) { + return null; + } + copy(request, merchant); + return merchant; + } + + public synchronized MerchantResponse enable(Long id) { + MerchantResponse merchant = merchants.get(id); + if (merchant != null) { + merchant.setStatus("ENABLED"); + } + return merchant; + } + + public synchronized MerchantResponse disable(Long id) { + MerchantResponse merchant = merchants.get(id); + if (merchant != null) { + merchant.setStatus("DISABLED"); + } + return merchant; + } + + private void copy(MerchantRequest request, MerchantResponse merchant) { + merchant.setTenantId(request.getTenantId()); + merchant.setCommunityId(request.getCommunityId()); + merchant.setName(request.getName()); + merchant.setMerchantType(request.getMerchantType()); + merchant.setContactName(request.getContactName()); + merchant.setContactPhone(request.getContactPhone()); + } +} diff --git a/backend/src/main/java/com/linhelp/order/CreateGoodsOrderItemRequest.java b/backend/src/main/java/com/linhelp/order/CreateGoodsOrderItemRequest.java new file mode 100644 index 0000000..975a11e --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/CreateGoodsOrderItemRequest.java @@ -0,0 +1,48 @@ +package com.linhelp.order; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class CreateGoodsOrderItemRequest { + @NotNull + private Long productId; + + @NotNull + private Long skuId; + + @Min(1) + private int quantity; + + public CreateGoodsOrderItemRequest() { + } + + public CreateGoodsOrderItemRequest(Long productId, Long skuId, int quantity) { + this.productId = productId; + this.skuId = skuId; + this.quantity = quantity; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSkuId() { + return skuId; + } + + public void setSkuId(Long skuId) { + this.skuId = skuId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } +} diff --git a/backend/src/main/java/com/linhelp/order/CreateGoodsOrderRequest.java b/backend/src/main/java/com/linhelp/order/CreateGoodsOrderRequest.java new file mode 100644 index 0000000..8f681b7 --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/CreateGoodsOrderRequest.java @@ -0,0 +1,94 @@ +package com.linhelp.order; + +import com.linhelp.common.enums.DeliveryMethod; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class CreateGoodsOrderRequest { + private Long tenantId; + private Long communityId; + + @NotNull + private Long merchantId; + + private Long addressId; + + @NotNull + private DeliveryMethod deliveryMethod = DeliveryMethod.IMMEDIATE; + + private LocalDateTime scheduledTime; + private String remark; + + @Valid + @NotEmpty + private List items = new ArrayList(); + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getMerchantId() { + return merchantId; + } + + public void setMerchantId(Long merchantId) { + this.merchantId = merchantId; + } + + public Long getAddressId() { + return addressId; + } + + public void setAddressId(Long addressId) { + this.addressId = addressId; + } + + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + public void setDeliveryMethod(DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + } + + public LocalDateTime getScheduledTime() { + return scheduledTime; + } + + public void setScheduledTime(LocalDateTime scheduledTime) { + this.scheduledTime = scheduledTime; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/backend/src/main/java/com/linhelp/order/GoodsOrderController.java b/backend/src/main/java/com/linhelp/order/GoodsOrderController.java new file mode 100644 index 0000000..8313f88 --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/GoodsOrderController.java @@ -0,0 +1,95 @@ +package com.linhelp.order; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import com.linhelp.common.security.RequireRole; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +public class GoodsOrderController { + private final GoodsOrderService goodsOrderService; + private final AuthService authService; + + public GoodsOrderController(GoodsOrderService goodsOrderService, AuthService authService) { + this.goodsOrderService = goodsOrderService; + this.authService = authService; + } + + @PostMapping("/api/mini/goods-orders") + public ApiResponse create(@Valid @RequestBody CreateGoodsOrderRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(goodsOrderService.create(user.getUserId(), request)); + } + + @GetMapping("/api/mini/goods-orders") + public ApiResponse> listMine() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(goodsOrderService.listMine(user.getUserId())); + } + + @GetMapping("/api/mini/goods-orders/{id}") + public ApiResponse miniDetail(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.detail(id)); + } + + @PutMapping("/api/mini/goods-orders/{id}/cancel") + public ApiResponse cancel(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.cancel(id)); + } + + @GetMapping("/api/admin/goods-orders") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse> listAdmin(@RequestParam(required = false) GoodsOrderStatus status) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(goodsOrderService.listAdmin(user.getCommunityId(), status)); + } + + @GetMapping("/api/admin/goods-orders/{id}") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse adminDetail(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.detail(id)); + } + + @PutMapping("/api/admin/goods-orders/{id}/confirm") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse confirm(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.confirm(id)); + } + + @PutMapping("/api/admin/goods-orders/{id}/prepared") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse markPrepared(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.markPrepared(id)); + } + + @PutMapping("/api/admin/goods-orders/{id}/delivering") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse markDelivering(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.markDelivering(id)); + } + + @PutMapping("/api/admin/goods-orders/{id}/complete") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse complete(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.complete(id)); + } + + @PutMapping("/api/admin/goods-orders/{id}/paid") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse markPaid(@PathVariable Long id) { + return ApiResponse.ok(goodsOrderService.markPaid(id)); + } +} diff --git a/backend/src/main/java/com/linhelp/order/GoodsOrderItemResponse.java b/backend/src/main/java/com/linhelp/order/GoodsOrderItemResponse.java new file mode 100644 index 0000000..8df3e44 --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/GoodsOrderItemResponse.java @@ -0,0 +1,85 @@ +package com.linhelp.order; + +public class GoodsOrderItemResponse { + private Long id; + private Long orderId; + private Long productId; + private Long skuId; + private String productName; + private String skuName; + private int quantity; + private int priceCent; + private int amountCent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSkuId() { + return skuId; + } + + public void setSkuId(Long skuId) { + this.skuId = skuId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getSkuName() { + return skuName; + } + + public void setSkuName(String skuName) { + this.skuName = skuName; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public int getPriceCent() { + return priceCent; + } + + public void setPriceCent(int priceCent) { + this.priceCent = priceCent; + } + + public int getAmountCent() { + return amountCent; + } + + public void setAmountCent(int amountCent) { + this.amountCent = amountCent; + } +} diff --git a/backend/src/main/java/com/linhelp/order/GoodsOrderResponse.java b/backend/src/main/java/com/linhelp/order/GoodsOrderResponse.java new file mode 100644 index 0000000..ddda14e --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/GoodsOrderResponse.java @@ -0,0 +1,174 @@ +package com.linhelp.order; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.common.enums.OfflinePayStatus; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class GoodsOrderResponse { + private Long id; + private String orderNo; + private Long tenantId; + private Long communityId; + private Long userId; + private Long merchantId; + private Long addressId; + private DeliveryMethod deliveryMethod; + private GoodsOrderStatus status; + private OfflinePayStatus offlinePayStatus; + private LocalDateTime scheduledTime; + private String remark; + private int totalAmountCent; + private int deliveryFeeCent; + private int payableAmountCent; + private List items = new ArrayList(); + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getOrderNo() { + return orderNo; + } + + public void setOrderNo(String orderNo) { + this.orderNo = orderNo; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getMerchantId() { + return merchantId; + } + + public void setMerchantId(Long merchantId) { + this.merchantId = merchantId; + } + + public Long getAddressId() { + return addressId; + } + + public void setAddressId(Long addressId) { + this.addressId = addressId; + } + + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + public void setDeliveryMethod(DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + } + + public GoodsOrderStatus getStatus() { + return status; + } + + public void setStatus(GoodsOrderStatus status) { + this.status = status; + } + + public OfflinePayStatus getOfflinePayStatus() { + return offlinePayStatus; + } + + public void setOfflinePayStatus(OfflinePayStatus offlinePayStatus) { + this.offlinePayStatus = offlinePayStatus; + } + + public LocalDateTime getScheduledTime() { + return scheduledTime; + } + + public void setScheduledTime(LocalDateTime scheduledTime) { + this.scheduledTime = scheduledTime; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public int getTotalAmountCent() { + return totalAmountCent; + } + + public void setTotalAmountCent(int totalAmountCent) { + this.totalAmountCent = totalAmountCent; + } + + public int getDeliveryFeeCent() { + return deliveryFeeCent; + } + + public void setDeliveryFeeCent(int deliveryFeeCent) { + this.deliveryFeeCent = deliveryFeeCent; + } + + public int getPayableAmountCent() { + return payableAmountCent; + } + + public void setPayableAmountCent(int payableAmountCent) { + this.payableAmountCent = payableAmountCent; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/linhelp/order/GoodsOrderService.java b/backend/src/main/java/com/linhelp/order/GoodsOrderService.java new file mode 100644 index 0000000..f18aab9 --- /dev/null +++ b/backend/src/main/java/com/linhelp/order/GoodsOrderService.java @@ -0,0 +1,257 @@ +package com.linhelp.order; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.common.enums.OfflinePayStatus; +import com.linhelp.common.exception.BizException; +import com.linhelp.product.ProductService; +import com.linhelp.product.ProductSkuResponse; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class GoodsOrderService { + private final ProductService productService; + private final AtomicLong orderIds = new AtomicLong(1L); + private final AtomicLong itemIds = new AtomicLong(1L); + private final Map orders = new LinkedHashMap(); + + public GoodsOrderService(ProductService productService) { + this.productService = productService; + } + + public synchronized GoodsOrderResponse create(Long userId, CreateGoodsOrderRequest request) { + if (request.getItems() == null || request.getItems().isEmpty()) { + throw new BizException("订单商品不能为空"); + } + if (request.getDeliveryMethod() != DeliveryMethod.SELF_PICKUP && request.getAddressId() == null) { + throw new BizException("配送地址不能为空"); + } + + Map skuQuantities = collectSkuQuantities(request); + Map skuSnapshots = loadAndCheckStock(skuQuantities); + for (Map.Entry entry : skuQuantities.entrySet()) { + productService.deductStock(entry.getKey(), entry.getValue()); + } + + Long orderId = orderIds.getAndIncrement(); + GoodsOrderResponse order = new GoodsOrderResponse(); + order.setId(orderId); + order.setOrderNo("GO" + String.format("%08d", orderId)); + order.setTenantId(request.getTenantId()); + order.setCommunityId(request.getCommunityId()); + order.setUserId(userId); + order.setMerchantId(request.getMerchantId()); + order.setAddressId(request.getAddressId()); + order.setDeliveryMethod(request.getDeliveryMethod()); + order.setStatus(GoodsOrderStatus.PENDING_CONFIRM); + order.setOfflinePayStatus(OfflinePayStatus.UNPAID); + order.setScheduledTime(request.getScheduledTime()); + order.setRemark(request.getRemark()); + order.setItems(buildItems(orderId, request, skuSnapshots)); + order.setTotalAmountCent(sumAmount(order.getItems())); + order.setDeliveryFeeCent(resolveDeliveryFee(request.getDeliveryMethod())); + order.setPayableAmountCent(order.getTotalAmountCent() + order.getDeliveryFeeCent()); + order.setCreatedAt(LocalDateTime.now()); + order.setUpdatedAt(order.getCreatedAt()); + orders.put(order.getId(), order); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse detail(Long id) { + return copyOrder(requireOrder(id)); + } + + public synchronized List listMine(Long userId) { + List result = new ArrayList(); + for (GoodsOrderResponse order : orders.values()) { + if (userId.equals(order.getUserId())) { + result.add(copyOrder(order)); + } + } + return result; + } + + public synchronized List listAdmin(Long communityId, GoodsOrderStatus status) { + List result = new ArrayList(); + for (GoodsOrderResponse order : orders.values()) { + if (!communityId.equals(order.getCommunityId())) { + continue; + } + if (status != null && status != order.getStatus()) { + continue; + } + result.add(copyOrder(order)); + } + return result; + } + + public synchronized GoodsOrderResponse confirm(Long id) { + GoodsOrderResponse order = requireOrder(id); + move(order, GoodsOrderStatus.PREPARING); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse markPrepared(Long id) { + GoodsOrderResponse order = requireOrder(id); + GoodsOrderStatus target = DeliveryMethod.SELF_PICKUP.equals(order.getDeliveryMethod()) + ? GoodsOrderStatus.PENDING_PICKUP + : GoodsOrderStatus.PENDING_DELIVERY; + move(order, target); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse markDelivering(Long id) { + GoodsOrderResponse order = requireOrder(id); + move(order, GoodsOrderStatus.DELIVERING); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse complete(Long id) { + GoodsOrderResponse order = requireOrder(id); + move(order, GoodsOrderStatus.COMPLETED); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse cancel(Long id) { + GoodsOrderResponse order = requireOrder(id); + move(order, GoodsOrderStatus.CANCELED); + return copyOrder(order); + } + + public synchronized GoodsOrderResponse markPaid(Long id) { + GoodsOrderResponse order = requireOrder(id); + order.setOfflinePayStatus(OfflinePayStatus.PAID); + order.setUpdatedAt(LocalDateTime.now()); + return copyOrder(order); + } + + private Map collectSkuQuantities(CreateGoodsOrderRequest request) { + Map skuQuantities = new LinkedHashMap(); + for (CreateGoodsOrderItemRequest item : request.getItems()) { + if (item.getSkuId() == null || item.getProductId() == null) { + throw new BizException("订单商品信息不完整"); + } + if (item.getQuantity() <= 0) { + throw new BizException("购买数量必须大于0"); + } + Integer existing = skuQuantities.get(item.getSkuId()); + skuQuantities.put(item.getSkuId(), (existing == null ? 0 : existing) + item.getQuantity()); + } + return skuQuantities; + } + + private Map loadAndCheckStock(Map skuQuantities) { + Map snapshots = new LinkedHashMap(); + for (Map.Entry entry : skuQuantities.entrySet()) { + ProductSkuResponse sku = productService.getSku(entry.getKey()); + if (!sku.isEnabled()) { + throw new BizException("商品规格已下架"); + } + if (sku.getStock() < entry.getValue()) { + throw new BizException("库存不足"); + } + snapshots.put(entry.getKey(), sku); + } + return snapshots; + } + + private List buildItems(Long orderId, + CreateGoodsOrderRequest request, + Map skuSnapshots) { + List items = new ArrayList(); + for (CreateGoodsOrderItemRequest itemRequest : request.getItems()) { + ProductSkuResponse sku = skuSnapshots.get(itemRequest.getSkuId()); + GoodsOrderItemResponse item = new GoodsOrderItemResponse(); + item.setId(itemIds.getAndIncrement()); + item.setOrderId(orderId); + item.setProductId(itemRequest.getProductId()); + item.setSkuId(itemRequest.getSkuId()); + item.setProductName(sku.getProductName()); + item.setSkuName(sku.getSkuName()); + item.setQuantity(itemRequest.getQuantity()); + item.setPriceCent(sku.getPriceCent()); + item.setAmountCent(sku.getPriceCent() * itemRequest.getQuantity()); + items.add(item); + } + return items; + } + + private int sumAmount(List items) { + int sum = 0; + for (GoodsOrderItemResponse item : items) { + sum += item.getAmountCent(); + } + return sum; + } + + private int resolveDeliveryFee(DeliveryMethod method) { + if (DeliveryMethod.SELF_PICKUP.equals(method)) { + return 0; + } + return 300; + } + + private void move(GoodsOrderResponse order, GoodsOrderStatus target) { + if (!order.getStatus().canMoveTo(target)) { + throw new BizException("订单状态不允许操作"); + } + order.setStatus(target); + order.setUpdatedAt(LocalDateTime.now()); + } + + private GoodsOrderResponse requireOrder(Long id) { + GoodsOrderResponse order = orders.get(id); + if (order == null) { + throw new BizException(404, "订单不存在"); + } + return order; + } + + private GoodsOrderResponse copyOrder(GoodsOrderResponse source) { + GoodsOrderResponse target = new GoodsOrderResponse(); + target.setId(source.getId()); + target.setOrderNo(source.getOrderNo()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setUserId(source.getUserId()); + target.setMerchantId(source.getMerchantId()); + target.setAddressId(source.getAddressId()); + target.setDeliveryMethod(source.getDeliveryMethod()); + target.setStatus(source.getStatus()); + target.setOfflinePayStatus(source.getOfflinePayStatus()); + target.setScheduledTime(source.getScheduledTime()); + target.setRemark(source.getRemark()); + target.setTotalAmountCent(source.getTotalAmountCent()); + target.setDeliveryFeeCent(source.getDeliveryFeeCent()); + target.setPayableAmountCent(source.getPayableAmountCent()); + target.setCreatedAt(source.getCreatedAt()); + target.setUpdatedAt(source.getUpdatedAt()); + target.setItems(copyItems(source.getItems())); + return target; + } + + private List copyItems(List sourceItems) { + List items = new ArrayList(); + for (GoodsOrderItemResponse source : sourceItems) { + GoodsOrderItemResponse target = new GoodsOrderItemResponse(); + target.setId(source.getId()); + target.setOrderId(source.getOrderId()); + target.setProductId(source.getProductId()); + target.setSkuId(source.getSkuId()); + target.setProductName(source.getProductName()); + target.setSkuName(source.getSkuName()); + target.setQuantity(source.getQuantity()); + target.setPriceCent(source.getPriceCent()); + target.setAmountCent(source.getAmountCent()); + items.add(target); + } + return items; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductCategoryRequest.java b/backend/src/main/java/com/linhelp/product/ProductCategoryRequest.java new file mode 100644 index 0000000..be514f9 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductCategoryRequest.java @@ -0,0 +1,54 @@ +package com.linhelp.product; + +import javax.validation.constraints.NotBlank; + +public class ProductCategoryRequest { + private Long tenantId; + private Long communityId; + + @NotBlank + private String name; + + private int sortNo; + private boolean enabled = true; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSortNo() { + return sortNo; + } + + public void setSortNo(int sortNo) { + this.sortNo = sortNo; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductCategoryResponse.java b/backend/src/main/java/com/linhelp/product/ProductCategoryResponse.java new file mode 100644 index 0000000..cf0fa60 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductCategoryResponse.java @@ -0,0 +1,58 @@ +package com.linhelp.product; + +public class ProductCategoryResponse { + private Long id; + private Long tenantId; + private Long communityId; + private String name; + private int sortNo; + private boolean enabled; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSortNo() { + return sortNo; + } + + public void setSortNo(int sortNo) { + this.sortNo = sortNo; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductController.java b/backend/src/main/java/com/linhelp/product/ProductController.java new file mode 100644 index 0000000..c2a8cd4 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductController.java @@ -0,0 +1,151 @@ +package com.linhelp.product; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import com.linhelp.common.security.RequireRole; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +public class ProductController { + private final ProductService productService; + private final AuthService authService; + + public ProductController(ProductService productService, AuthService authService) { + this.productService = productService; + this.authService = authService; + } + + @GetMapping("/api/admin/product-categories") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse> listAdminCategories() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(productService.listCategories(user.getCommunityId(), false)); + } + + @PostMapping("/api/admin/product-categories") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse createCategory(@Valid @RequestBody ProductCategoryRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.createCategory(request)); + } + + @PutMapping("/api/admin/product-categories/{id}") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse updateCategory(@PathVariable Long id, @Valid @RequestBody ProductCategoryRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.updateCategory(id, request)); + } + + @PutMapping("/api/admin/product-categories/{id}/enable") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse enableCategory(@PathVariable Long id) { + return ApiResponse.ok(productService.enableCategory(id)); + } + + @PutMapping("/api/admin/product-categories/{id}/disable") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse disableCategory(@PathVariable Long id) { + return ApiResponse.ok(productService.disableCategory(id)); + } + + @GetMapping("/api/admin/products") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse> listAdminProducts(@RequestParam(required = false) Long merchantId, + @RequestParam(required = false) Long categoryId) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(productService.listAdmin(user.getCommunityId(), merchantId, categoryId)); + } + + @PostMapping("/api/admin/products") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse createProduct(@Valid @RequestBody ProductRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.createProduct(request)); + } + + @PutMapping("/api/admin/products/{id}") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse updateProduct(@PathVariable Long id, @Valid @RequestBody ProductRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.updateProduct(id, request)); + } + + @PutMapping("/api/admin/products/{id}/on-shelf") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse onShelf(@PathVariable Long id) { + return ApiResponse.ok(productService.onShelf(id)); + } + + @PutMapping("/api/admin/products/{id}/off-shelf") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse offShelf(@PathVariable Long id) { + return ApiResponse.ok(productService.offShelf(id)); + } + + @PostMapping("/api/admin/products/{id}/skus") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse addSku(@PathVariable Long id, @Valid @RequestBody ProductSkuRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.addSku(id, request)); + } + + @PutMapping("/api/admin/products/{id}/skus/{skuId}") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse updateSku(@PathVariable Long id, + @PathVariable Long skuId, + @Valid @RequestBody ProductSkuRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(productService.updateSku(id, skuId, request)); + } + + @PutMapping("/api/admin/products/{id}/skus/{skuId}/enable") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse enableSku(@PathVariable Long id, @PathVariable Long skuId) { + return ApiResponse.ok(productService.enableSku(id, skuId)); + } + + @PutMapping("/api/admin/products/{id}/skus/{skuId}/disable") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse disableSku(@PathVariable Long id, @PathVariable Long skuId) { + return ApiResponse.ok(productService.disableSku(id, skuId)); + } + + @GetMapping("/api/mini/product-categories") + public ApiResponse> listMiniCategories() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(productService.listCategories(user.getCommunityId(), true)); + } + + @GetMapping("/api/mini/products") + public ApiResponse> listMiniProducts(@RequestParam(required = false) Long categoryId) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(productService.listMini(user.getCommunityId(), categoryId)); + } + + @GetMapping("/api/mini/products/{id}") + public ApiResponse miniProductDetail(@PathVariable Long id) { + return ApiResponse.ok(productService.detail(id)); + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductRequest.java b/backend/src/main/java/com/linhelp/product/ProductRequest.java new file mode 100644 index 0000000..0aec752 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductRequest.java @@ -0,0 +1,86 @@ +package com.linhelp.product; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class ProductRequest { + private Long tenantId; + private Long communityId; + + @NotNull + private Long merchantId; + + @NotNull + private Long categoryId; + + @NotBlank + private String name; + + private String coverUrl; + private String description; + private String unitName = "件"; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getMerchantId() { + return merchantId; + } + + public void setMerchantId(Long merchantId) { + this.merchantId = merchantId; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUnitName() { + return unitName; + } + + public void setUnitName(String unitName) { + this.unitName = unitName; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductResponse.java b/backend/src/main/java/com/linhelp/product/ProductResponse.java new file mode 100644 index 0000000..66585b1 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductResponse.java @@ -0,0 +1,106 @@ +package com.linhelp.product; + +import java.util.ArrayList; +import java.util.List; + +public class ProductResponse { + private Long id; + private Long tenantId; + private Long communityId; + private Long merchantId; + private Long categoryId; + private String name; + private String coverUrl; + private String description; + private String unitName; + private String status; + private List skus = new ArrayList(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getMerchantId() { + return merchantId; + } + + public void setMerchantId(Long merchantId) { + this.merchantId = merchantId; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUnitName() { + return unitName; + } + + public void setUnitName(String unitName) { + this.unitName = unitName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public List getSkus() { + return skus; + } + + public void setSkus(List skus) { + this.skus = skus; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductService.java b/backend/src/main/java/com/linhelp/product/ProductService.java new file mode 100644 index 0000000..e9c33b9 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductService.java @@ -0,0 +1,329 @@ +package com.linhelp.product; + +import com.linhelp.common.exception.BizException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ProductService { + public static final String STATUS_ON_SHELF = "ON_SHELF"; + public static final String STATUS_OFF_SHELF = "OFF_SHELF"; + + private final AtomicLong categoryIds = new AtomicLong(1L); + private final AtomicLong productIds = new AtomicLong(1L); + private final AtomicLong skuIds = new AtomicLong(1L); + private final Map categories = new LinkedHashMap(); + private final Map products = new LinkedHashMap(); + private final Map skus = new LinkedHashMap(); + + public synchronized ProductCategoryResponse createCategory(ProductCategoryRequest request) { + ProductCategoryResponse category = new ProductCategoryResponse(); + category.setId(categoryIds.getAndIncrement()); + category.setTenantId(request.getTenantId()); + category.setCommunityId(request.getCommunityId()); + category.setName(request.getName()); + category.setSortNo(request.getSortNo()); + category.setEnabled(request.isEnabled()); + categories.put(category.getId(), category); + return copyCategory(category); + } + + public synchronized ProductCategoryResponse updateCategory(Long id, ProductCategoryRequest request) { + ProductCategoryResponse category = requireCategory(id); + category.setName(request.getName()); + category.setSortNo(request.getSortNo()); + category.setEnabled(request.isEnabled()); + if (request.getTenantId() != null) { + category.setTenantId(request.getTenantId()); + } + if (request.getCommunityId() != null) { + category.setCommunityId(request.getCommunityId()); + } + return copyCategory(category); + } + + public synchronized ProductCategoryResponse enableCategory(Long id) { + ProductCategoryResponse category = requireCategory(id); + category.setEnabled(true); + return copyCategory(category); + } + + public synchronized ProductCategoryResponse disableCategory(Long id) { + ProductCategoryResponse category = requireCategory(id); + category.setEnabled(false); + return copyCategory(category); + } + + public synchronized List listCategories(Long communityId, boolean onlyEnabled) { + List result = new ArrayList(); + for (ProductCategoryResponse category : categories.values()) { + if (communityId.equals(category.getCommunityId()) && (!onlyEnabled || category.isEnabled())) { + result.add(copyCategory(category)); + } + } + Collections.sort(result, new Comparator() { + @Override + public int compare(ProductCategoryResponse left, ProductCategoryResponse right) { + return Integer.valueOf(left.getSortNo()).compareTo(right.getSortNo()); + } + }); + return result; + } + + public synchronized ProductResponse createProduct(ProductRequest request) { + ProductResponse product = new ProductResponse(); + product.setId(productIds.getAndIncrement()); + product.setTenantId(request.getTenantId()); + product.setCommunityId(request.getCommunityId()); + copyProductFields(request, product); + product.setStatus(STATUS_OFF_SHELF); + products.put(product.getId(), product); + return copyProduct(product); + } + + public synchronized ProductResponse updateProduct(Long id, ProductRequest request) { + ProductResponse product = requireProduct(id); + copyProductFields(request, product); + if (request.getTenantId() != null) { + product.setTenantId(request.getTenantId()); + } + if (request.getCommunityId() != null) { + product.setCommunityId(request.getCommunityId()); + } + return copyProduct(product); + } + + public synchronized ProductResponse onShelf(Long id) { + ProductResponse product = requireProduct(id); + product.setStatus(STATUS_ON_SHELF); + return copyProduct(product); + } + + public synchronized ProductResponse offShelf(Long id) { + ProductResponse product = requireProduct(id); + product.setStatus(STATUS_OFF_SHELF); + return copyProduct(product); + } + + public synchronized ProductResponse detail(Long id) { + return copyProduct(requireProduct(id)); + } + + public synchronized List listAdmin(Long communityId, Long merchantId, Long categoryId) { + return listProducts(communityId, merchantId, categoryId, false); + } + + public synchronized List listMini(Long communityId, Long categoryId) { + return listProducts(communityId, null, categoryId, true); + } + + public synchronized ProductSkuResponse addSku(Long productId, ProductSkuRequest request) { + ProductResponse product = requireProduct(productId); + ProductSkuResponse sku = new ProductSkuResponse(); + sku.setId(skuIds.getAndIncrement()); + sku.setTenantId(request.getTenantId() == null ? product.getTenantId() : request.getTenantId()); + sku.setCommunityId(request.getCommunityId() == null ? product.getCommunityId() : request.getCommunityId()); + sku.setProductId(productId); + sku.setProductName(product.getName()); + copySkuFields(request, sku); + skus.put(sku.getId(), sku); + return copySku(sku); + } + + public synchronized ProductSkuResponse updateSku(Long productId, Long skuId, ProductSkuRequest request) { + requireProduct(productId); + ProductSkuResponse sku = requireSku(skuId); + if (!productId.equals(sku.getProductId())) { + throw new BizException(404, "商品规格不存在"); + } + copySkuFields(request, sku); + if (request.getTenantId() != null) { + sku.setTenantId(request.getTenantId()); + } + if (request.getCommunityId() != null) { + sku.setCommunityId(request.getCommunityId()); + } + ProductResponse product = products.get(productId); + sku.setProductName(product.getName()); + return copySku(sku); + } + + public synchronized ProductSkuResponse enableSku(Long productId, Long skuId) { + return setSkuEnabled(productId, skuId, true); + } + + public synchronized ProductSkuResponse disableSku(Long productId, Long skuId) { + return setSkuEnabled(productId, skuId, false); + } + + public synchronized ProductSkuResponse createDemoSku(Long tenantId, Long communityId, Long productId, String skuName, int priceCent, int stock) { + ProductSkuResponse sku = new ProductSkuResponse(); + sku.setId(skuIds.getAndIncrement()); + sku.setTenantId(tenantId); + sku.setCommunityId(communityId); + sku.setProductId(productId); + sku.setProductName(resolveProductName(productId)); + sku.setSkuName(skuName); + sku.setPriceCent(priceCent); + sku.setStock(stock); + sku.setEnabled(true); + skus.put(sku.getId(), sku); + return copySku(sku); + } + + public synchronized ProductSkuResponse getSku(Long skuId) { + return copySku(requireSku(skuId)); + } + + public synchronized void deductStock(Long skuId, int quantity) { + if (quantity <= 0) { + throw new BizException("购买数量必须大于0"); + } + ProductSkuResponse sku = requireSku(skuId); + if (!sku.isEnabled()) { + throw new BizException("商品规格已下架"); + } + if (sku.getStock() < quantity) { + throw new BizException("库存不足"); + } + sku.setStock(sku.getStock() - quantity); + } + + private ProductSkuResponse setSkuEnabled(Long productId, Long skuId, boolean enabled) { + requireProduct(productId); + ProductSkuResponse sku = requireSku(skuId); + if (!productId.equals(sku.getProductId())) { + throw new BizException(404, "商品规格不存在"); + } + sku.setEnabled(enabled); + return copySku(sku); + } + + private List listProducts(Long communityId, Long merchantId, Long categoryId, boolean onlyOnShelf) { + List result = new ArrayList(); + for (ProductResponse product : products.values()) { + if (!communityId.equals(product.getCommunityId())) { + continue; + } + if (merchantId != null && !merchantId.equals(product.getMerchantId())) { + continue; + } + if (categoryId != null && !categoryId.equals(product.getCategoryId())) { + continue; + } + if (onlyOnShelf && !STATUS_ON_SHELF.equals(product.getStatus())) { + continue; + } + result.add(copyProduct(product)); + } + return result; + } + + private ProductCategoryResponse requireCategory(Long id) { + ProductCategoryResponse category = categories.get(id); + if (category == null) { + throw new BizException(404, "商品分类不存在"); + } + return category; + } + + private ProductResponse requireProduct(Long id) { + ProductResponse product = products.get(id); + if (product == null) { + throw new BizException(404, "商品不存在"); + } + return product; + } + + private ProductSkuResponse requireSku(Long id) { + ProductSkuResponse sku = skus.get(id); + if (sku == null) { + throw new BizException(404, "商品规格不存在"); + } + return sku; + } + + private void copyProductFields(ProductRequest request, ProductResponse product) { + product.setMerchantId(request.getMerchantId()); + product.setCategoryId(request.getCategoryId()); + product.setName(request.getName()); + product.setCoverUrl(request.getCoverUrl()); + product.setDescription(request.getDescription()); + product.setUnitName(request.getUnitName()); + } + + private void copySkuFields(ProductSkuRequest request, ProductSkuResponse sku) { + sku.setSkuName(request.getSkuName()); + sku.setPriceCent(request.getPriceCent()); + sku.setOriginPriceCent(request.getOriginPriceCent()); + sku.setStock(request.getStock()); + sku.setEnabled(request.isEnabled()); + } + + private String resolveProductName(Long productId) { + ProductResponse product = products.get(productId); + if (product == null) { + return "Demo Product " + productId; + } + return product.getName(); + } + + private ProductCategoryResponse copyCategory(ProductCategoryResponse source) { + ProductCategoryResponse target = new ProductCategoryResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setName(source.getName()); + target.setSortNo(source.getSortNo()); + target.setEnabled(source.isEnabled()); + return target; + } + + private ProductResponse copyProduct(ProductResponse source) { + ProductResponse target = new ProductResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setMerchantId(source.getMerchantId()); + target.setCategoryId(source.getCategoryId()); + target.setName(source.getName()); + target.setCoverUrl(source.getCoverUrl()); + target.setDescription(source.getDescription()); + target.setUnitName(source.getUnitName()); + target.setStatus(source.getStatus()); + target.setSkus(listSkusByProduct(source.getId())); + return target; + } + + private List listSkusByProduct(Long productId) { + List result = new ArrayList(); + for (ProductSkuResponse sku : skus.values()) { + if (productId.equals(sku.getProductId())) { + result.add(copySku(sku)); + } + } + return result; + } + + private ProductSkuResponse copySku(ProductSkuResponse source) { + ProductSkuResponse target = new ProductSkuResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setProductId(source.getProductId()); + target.setProductName(source.getProductName()); + target.setSkuName(source.getSkuName()); + target.setPriceCent(source.getPriceCent()); + target.setOriginPriceCent(source.getOriginPriceCent()); + target.setStock(source.getStock()); + target.setEnabled(source.isEnabled()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductSkuRequest.java b/backend/src/main/java/com/linhelp/product/ProductSkuRequest.java new file mode 100644 index 0000000..05ed5b0 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductSkuRequest.java @@ -0,0 +1,78 @@ +package com.linhelp.product; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; + +public class ProductSkuRequest { + private Long tenantId; + private Long communityId; + + @NotBlank + private String skuName; + + @Min(0) + private int priceCent; + + private Integer originPriceCent; + + @Min(0) + private int stock; + + private boolean enabled = true; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getSkuName() { + return skuName; + } + + public void setSkuName(String skuName) { + this.skuName = skuName; + } + + public int getPriceCent() { + return priceCent; + } + + public void setPriceCent(int priceCent) { + this.priceCent = priceCent; + } + + public Integer getOriginPriceCent() { + return originPriceCent; + } + + public void setOriginPriceCent(Integer originPriceCent) { + this.originPriceCent = originPriceCent; + } + + public int getStock() { + return stock; + } + + public void setStock(int stock) { + this.stock = stock; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/main/java/com/linhelp/product/ProductSkuResponse.java b/backend/src/main/java/com/linhelp/product/ProductSkuResponse.java new file mode 100644 index 0000000..3c21a45 --- /dev/null +++ b/backend/src/main/java/com/linhelp/product/ProductSkuResponse.java @@ -0,0 +1,94 @@ +package com.linhelp.product; + +public class ProductSkuResponse { + private Long id; + private Long tenantId; + private Long communityId; + private Long productId; + private String productName; + private String skuName; + private int priceCent; + private Integer originPriceCent; + private int stock; + private boolean enabled; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getSkuName() { + return skuName; + } + + public void setSkuName(String skuName) { + this.skuName = skuName; + } + + public int getPriceCent() { + return priceCent; + } + + public void setPriceCent(int priceCent) { + this.priceCent = priceCent; + } + + public Integer getOriginPriceCent() { + return originPriceCent; + } + + public void setOriginPriceCent(Integer originPriceCent) { + this.originPriceCent = originPriceCent; + } + + public int getStock() { + return stock; + } + + public void setStock(int stock) { + this.stock = stock; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/test/java/com/linhelp/order/GoodsOrderServiceTests.java b/backend/src/test/java/com/linhelp/order/GoodsOrderServiceTests.java new file mode 100644 index 0000000..c49ea6b --- /dev/null +++ b/backend/src/test/java/com/linhelp/order/GoodsOrderServiceTests.java @@ -0,0 +1,53 @@ +package com.linhelp.order; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.product.ProductService; +import com.linhelp.product.ProductSkuResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GoodsOrderServiceTests { + + @Test + void cannotCreateGoodsOrderWhenSkuStockIsInsufficient() { + ProductService productService = new ProductService(); + ProductSkuResponse sku = productService.createDemoSku(1L, 1L, 1L, "500g", 990, 2); + GoodsOrderService goodsOrderService = new GoodsOrderService(productService); + + CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setMerchantId(1L); + request.setAddressId(1L); + request.setDeliveryMethod(DeliveryMethod.IMMEDIATE); + request.setItems(Collections.singletonList(new CreateGoodsOrderItemRequest(1L, sku.getId(), 99))); + + assertThatThrownBy(() -> goodsOrderService.create(1L, request)) + .hasMessageContaining("库存不足"); + } + + @Test + void selfPickupOrderMovesToPendingPickupAfterPreparing() { + ProductService productService = new ProductService(); + ProductSkuResponse sku = productService.createDemoSku(1L, 1L, 1L, "500g", 990, 5); + GoodsOrderService goodsOrderService = new GoodsOrderService(productService); + + CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setMerchantId(1L); + request.setDeliveryMethod(DeliveryMethod.SELF_PICKUP); + request.setItems(Collections.singletonList(new CreateGoodsOrderItemRequest(1L, sku.getId(), 1))); + GoodsOrderResponse order = goodsOrderService.create(1L, request); + + goodsOrderService.confirm(order.getId()); + goodsOrderService.markPrepared(order.getId()); + + assertThat(goodsOrderService.detail(order.getId()).getStatus()).isEqualTo(GoodsOrderStatus.PENDING_PICKUP); + } +} diff --git a/backend/src/test/java/com/linhelp/product/ProductInventoryTests.java b/backend/src/test/java/com/linhelp/product/ProductInventoryTests.java new file mode 100644 index 0000000..0ffa25c --- /dev/null +++ b/backend/src/test/java/com/linhelp/product/ProductInventoryTests.java @@ -0,0 +1,18 @@ +package com.linhelp.product; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProductInventoryTests { + + @Test + void deductStockReducesSkuStock() { + ProductService productService = new ProductService(); + ProductSkuResponse sku = productService.createDemoSku(1L, 1L, 1L, "500g", 990, 5); + + productService.deductStock(sku.getId(), 2); + + assertThat(productService.getSku(sku.getId()).getStock()).isEqualTo(3); + } +}