feat: add product catalog and goods orders

This commit is contained in:
王鹏
2026-07-07 15:37:56 +08:00
parent cad4bd3ea1
commit 40ea2cd9ff
20 changed files with 2050 additions and 0 deletions

View File

@@ -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<MerchantResponse>> list() {
CurrentUser user = authService.currentUser();
return ApiResponse.ok(merchantService.list(user.getCommunityId()));
}
@PostMapping
public ApiResponse<MerchantResponse> 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<MerchantResponse> 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<MerchantResponse> enable(@PathVariable Long id) {
return ApiResponse.ok(merchantService.enable(id));
}
@PutMapping("/{id}/disable")
public ApiResponse<MerchantResponse> disable(@PathVariable Long id) {
return ApiResponse.ok(merchantService.disable(id));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Long, MerchantResponse> merchants = new LinkedHashMap<Long, MerchantResponse>();
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<MerchantResponse> list(Long communityId) {
List<MerchantResponse> result = new ArrayList<MerchantResponse>();
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());
}
}

View File

@@ -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;
}
}

View File

@@ -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<CreateGoodsOrderItemRequest> items = new ArrayList<CreateGoodsOrderItemRequest>();
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<CreateGoodsOrderItemRequest> getItems() {
return items;
}
public void setItems(List<CreateGoodsOrderItemRequest> items) {
this.items = items;
}
}

View File

@@ -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<GoodsOrderResponse> 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<List<GoodsOrderResponse>> listMine() {
CurrentUser user = authService.currentUser();
return ApiResponse.ok(goodsOrderService.listMine(user.getUserId()));
}
@GetMapping("/api/mini/goods-orders/{id}")
public ApiResponse<GoodsOrderResponse> miniDetail(@PathVariable Long id) {
return ApiResponse.ok(goodsOrderService.detail(id));
}
@PutMapping("/api/mini/goods-orders/{id}/cancel")
public ApiResponse<GoodsOrderResponse> cancel(@PathVariable Long id) {
return ApiResponse.ok(goodsOrderService.cancel(id));
}
@GetMapping("/api/admin/goods-orders")
@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"})
public ApiResponse<List<GoodsOrderResponse>> 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<GoodsOrderResponse> 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<GoodsOrderResponse> 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<GoodsOrderResponse> 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<GoodsOrderResponse> 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<GoodsOrderResponse> 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<GoodsOrderResponse> markPaid(@PathVariable Long id) {
return ApiResponse.ok(goodsOrderService.markPaid(id));
}
}

View File

@@ -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;
}
}

View File

@@ -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<GoodsOrderItemResponse> items = new ArrayList<GoodsOrderItemResponse>();
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<GoodsOrderItemResponse> getItems() {
return items;
}
public void setItems(List<GoodsOrderItemResponse> 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;
}
}

View File

@@ -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<Long, GoodsOrderResponse> orders = new LinkedHashMap<Long, GoodsOrderResponse>();
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<Long, Integer> skuQuantities = collectSkuQuantities(request);
Map<Long, ProductSkuResponse> skuSnapshots = loadAndCheckStock(skuQuantities);
for (Map.Entry<Long, Integer> 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<GoodsOrderResponse> listMine(Long userId) {
List<GoodsOrderResponse> result = new ArrayList<GoodsOrderResponse>();
for (GoodsOrderResponse order : orders.values()) {
if (userId.equals(order.getUserId())) {
result.add(copyOrder(order));
}
}
return result;
}
public synchronized List<GoodsOrderResponse> listAdmin(Long communityId, GoodsOrderStatus status) {
List<GoodsOrderResponse> result = new ArrayList<GoodsOrderResponse>();
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<Long, Integer> collectSkuQuantities(CreateGoodsOrderRequest request) {
Map<Long, Integer> skuQuantities = new LinkedHashMap<Long, Integer>();
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<Long, ProductSkuResponse> loadAndCheckStock(Map<Long, Integer> skuQuantities) {
Map<Long, ProductSkuResponse> snapshots = new LinkedHashMap<Long, ProductSkuResponse>();
for (Map.Entry<Long, Integer> 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<GoodsOrderItemResponse> buildItems(Long orderId,
CreateGoodsOrderRequest request,
Map<Long, ProductSkuResponse> skuSnapshots) {
List<GoodsOrderItemResponse> items = new ArrayList<GoodsOrderItemResponse>();
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<GoodsOrderItemResponse> 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<GoodsOrderItemResponse> copyItems(List<GoodsOrderItemResponse> sourceItems) {
List<GoodsOrderItemResponse> items = new ArrayList<GoodsOrderItemResponse>();
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<List<ProductCategoryResponse>> 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<ProductCategoryResponse> 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<ProductCategoryResponse> 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<ProductCategoryResponse> 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<ProductCategoryResponse> disableCategory(@PathVariable Long id) {
return ApiResponse.ok(productService.disableCategory(id));
}
@GetMapping("/api/admin/products")
@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"})
public ApiResponse<List<ProductResponse>> 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<ProductResponse> 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<ProductResponse> 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<ProductResponse> 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<ProductResponse> offShelf(@PathVariable Long id) {
return ApiResponse.ok(productService.offShelf(id));
}
@PostMapping("/api/admin/products/{id}/skus")
@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"})
public ApiResponse<ProductSkuResponse> 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<ProductSkuResponse> 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<ProductSkuResponse> 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<ProductSkuResponse> disableSku(@PathVariable Long id, @PathVariable Long skuId) {
return ApiResponse.ok(productService.disableSku(id, skuId));
}
@GetMapping("/api/mini/product-categories")
public ApiResponse<List<ProductCategoryResponse>> listMiniCategories() {
CurrentUser user = authService.currentUser();
return ApiResponse.ok(productService.listCategories(user.getCommunityId(), true));
}
@GetMapping("/api/mini/products")
public ApiResponse<List<ProductResponse>> 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<ProductResponse> miniProductDetail(@PathVariable Long id) {
return ApiResponse.ok(productService.detail(id));
}
}

View File

@@ -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;
}
}

View File

@@ -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<ProductSkuResponse> skus = new ArrayList<ProductSkuResponse>();
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<ProductSkuResponse> getSkus() {
return skus;
}
public void setSkus(List<ProductSkuResponse> skus) {
this.skus = skus;
}
}

View File

@@ -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<Long, ProductCategoryResponse> categories = new LinkedHashMap<Long, ProductCategoryResponse>();
private final Map<Long, ProductResponse> products = new LinkedHashMap<Long, ProductResponse>();
private final Map<Long, ProductSkuResponse> skus = new LinkedHashMap<Long, ProductSkuResponse>();
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<ProductCategoryResponse> listCategories(Long communityId, boolean onlyEnabled) {
List<ProductCategoryResponse> result = new ArrayList<ProductCategoryResponse>();
for (ProductCategoryResponse category : categories.values()) {
if (communityId.equals(category.getCommunityId()) && (!onlyEnabled || category.isEnabled())) {
result.add(copyCategory(category));
}
}
Collections.sort(result, new Comparator<ProductCategoryResponse>() {
@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<ProductResponse> listAdmin(Long communityId, Long merchantId, Long categoryId) {
return listProducts(communityId, merchantId, categoryId, false);
}
public synchronized List<ProductResponse> 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<ProductResponse> listProducts(Long communityId, Long merchantId, Long categoryId, boolean onlyOnShelf) {
List<ProductResponse> result = new ArrayList<ProductResponse>();
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<ProductSkuResponse> listSkusByProduct(Long productId) {
List<ProductSkuResponse> result = new ArrayList<ProductSkuResponse>();
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}