diff --git a/backend/src/main/java/com/linhelp/feedback/FeedbackController.java b/backend/src/main/java/com/linhelp/feedback/FeedbackController.java new file mode 100644 index 0000000..b3f7355 --- /dev/null +++ b/backend/src/main/java/com/linhelp/feedback/FeedbackController.java @@ -0,0 +1,48 @@ +package com.linhelp.feedback; + +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 FeedbackController { + private final FeedbackService feedbackService; + private final AuthService authService; + + public FeedbackController(FeedbackService feedbackService, AuthService authService) { + this.feedbackService = feedbackService; + this.authService = authService; + } + + @PostMapping("/api/mini/feedback") + public ApiResponse create(@Valid @RequestBody FeedbackRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(feedbackService.create(user.getUserId(), request)); + } + + @GetMapping("/api/admin/feedback") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse> listAdmin(@RequestParam(required = false) String status) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(feedbackService.listAdmin(user.getCommunityId(), status)); + } + + @PutMapping("/api/admin/feedback/{id}/process") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse process(@PathVariable Long id, @RequestBody FeedbackProcessRequest request) { + return ApiResponse.ok(feedbackService.process(id, request.getProcessNote())); + } +} diff --git a/backend/src/main/java/com/linhelp/feedback/FeedbackProcessRequest.java b/backend/src/main/java/com/linhelp/feedback/FeedbackProcessRequest.java new file mode 100644 index 0000000..8e7e3e7 --- /dev/null +++ b/backend/src/main/java/com/linhelp/feedback/FeedbackProcessRequest.java @@ -0,0 +1,13 @@ +package com.linhelp.feedback; + +public class FeedbackProcessRequest { + private String processNote; + + public String getProcessNote() { + return processNote; + } + + public void setProcessNote(String processNote) { + this.processNote = processNote; + } +} diff --git a/backend/src/main/java/com/linhelp/feedback/FeedbackRequest.java b/backend/src/main/java/com/linhelp/feedback/FeedbackRequest.java new file mode 100644 index 0000000..d557897 --- /dev/null +++ b/backend/src/main/java/com/linhelp/feedback/FeedbackRequest.java @@ -0,0 +1,72 @@ +package com.linhelp.feedback; + +import javax.validation.constraints.NotBlank; + +public class FeedbackRequest { + private Long tenantId; + private Long communityId; + private String category; + + @NotBlank + private String content; + + private String contactPhone; + private String bizType; + private Long bizOrderId; + + 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 getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public Long getBizOrderId() { + return bizOrderId; + } + + public void setBizOrderId(Long bizOrderId) { + this.bizOrderId = bizOrderId; + } +} diff --git a/backend/src/main/java/com/linhelp/feedback/FeedbackResponse.java b/backend/src/main/java/com/linhelp/feedback/FeedbackResponse.java new file mode 100644 index 0000000..c3b4d1a --- /dev/null +++ b/backend/src/main/java/com/linhelp/feedback/FeedbackResponse.java @@ -0,0 +1,123 @@ +package com.linhelp.feedback; + +import java.time.LocalDateTime; + +public class FeedbackResponse { + private Long id; + private Long tenantId; + private Long communityId; + private Long userId; + private String category; + private String content; + private String contactPhone; + private String bizType; + private Long bizOrderId; + private String status; + private String processNote; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + 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 getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public Long getBizOrderId() { + return bizOrderId; + } + + public void setBizOrderId(Long bizOrderId) { + this.bizOrderId = bizOrderId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getProcessNote() { + return processNote; + } + + public void setProcessNote(String processNote) { + this.processNote = processNote; + } + + 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/feedback/FeedbackService.java b/backend/src/main/java/com/linhelp/feedback/FeedbackService.java new file mode 100644 index 0000000..e304c0d --- /dev/null +++ b/backend/src/main/java/com/linhelp/feedback/FeedbackService.java @@ -0,0 +1,81 @@ +package com.linhelp.feedback; + +import com.linhelp.common.exception.BizException; +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 FeedbackService { + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_PROCESSED = "PROCESSED"; + + private final AtomicLong ids = new AtomicLong(1L); + private final Map feedbacks = new LinkedHashMap(); + + public synchronized FeedbackResponse create(Long userId, FeedbackRequest request) { + FeedbackResponse feedback = new FeedbackResponse(); + feedback.setId(ids.getAndIncrement()); + feedback.setTenantId(request.getTenantId()); + feedback.setCommunityId(request.getCommunityId()); + feedback.setUserId(userId); + feedback.setCategory(request.getCategory()); + feedback.setContent(request.getContent()); + feedback.setContactPhone(request.getContactPhone()); + feedback.setBizType(request.getBizType()); + feedback.setBizOrderId(request.getBizOrderId()); + feedback.setStatus(STATUS_PENDING); + feedback.setCreatedAt(LocalDateTime.now()); + feedback.setUpdatedAt(feedback.getCreatedAt()); + feedbacks.put(feedback.getId(), feedback); + return copy(feedback); + } + + public synchronized List listAdmin(Long communityId, String status) { + List result = new ArrayList(); + for (FeedbackResponse feedback : feedbacks.values()) { + if (!communityId.equals(feedback.getCommunityId())) { + continue; + } + if (status != null && !status.equals(feedback.getStatus())) { + continue; + } + result.add(copy(feedback)); + } + return result; + } + + public synchronized FeedbackResponse process(Long id, String processNote) { + FeedbackResponse feedback = feedbacks.get(id); + if (feedback == null) { + throw new BizException(404, "反馈不存在"); + } + feedback.setStatus(STATUS_PROCESSED); + feedback.setProcessNote(processNote); + feedback.setUpdatedAt(LocalDateTime.now()); + return copy(feedback); + } + + private FeedbackResponse copy(FeedbackResponse source) { + FeedbackResponse target = new FeedbackResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setUserId(source.getUserId()); + target.setCategory(source.getCategory()); + target.setContent(source.getContent()); + target.setContactPhone(source.getContactPhone()); + target.setBizType(source.getBizType()); + target.setBizOrderId(source.getBizOrderId()); + target.setStatus(source.getStatus()); + target.setProcessNote(source.getProcessNote()); + target.setCreatedAt(source.getCreatedAt()); + target.setUpdatedAt(source.getUpdatedAt()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyController.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyController.java new file mode 100644 index 0000000..8ada03a --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyController.java @@ -0,0 +1,119 @@ +package com.linhelp.groupbuy; + +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.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +public class GroupBuyController { + private final GroupBuyService groupBuyService; + private final GroupBuyOrderService orderService; + private final AuthService authService; + + public GroupBuyController(GroupBuyService groupBuyService, + GroupBuyOrderService orderService, + AuthService authService) { + this.groupBuyService = groupBuyService; + this.orderService = orderService; + this.authService = authService; + } + + @GetMapping("/api/admin/group-buys") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse> listAdmin() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(groupBuyService.listAdmin(user.getCommunityId())); + } + + @PostMapping("/api/admin/group-buys") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse create(@Valid @RequestBody GroupBuyRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(groupBuyService.create(request)); + } + + @PutMapping("/api/admin/group-buys/{id}") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse update(@PathVariable Long id, @Valid @RequestBody GroupBuyRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(groupBuyService.update(id, request)); + } + + @PutMapping("/api/admin/group-buys/{id}/start") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse start(@PathVariable Long id) { + return ApiResponse.ok(groupBuyService.start(id)); + } + + @PutMapping("/api/admin/group-buys/{id}/close") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse close(@PathVariable Long id) { + return ApiResponse.ok(groupBuyService.close(id)); + } + + @PutMapping("/api/admin/group-buys/{id}/complete") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse completeGroupBuy(@PathVariable Long id) { + return ApiResponse.ok(groupBuyService.complete(id)); + } + + @GetMapping("/api/admin/group-buys/{id}/orders") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"}) + public ApiResponse> listOrders(@PathVariable Long id) { + return ApiResponse.ok(orderService.listByGroupBuy(id)); + } + + @GetMapping("/api/mini/group-buys") + public ApiResponse> listMini() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(groupBuyService.listMini(user.getCommunityId())); + } + + @GetMapping("/api/mini/group-buys/{id}") + public ApiResponse detail(@PathVariable Long id) { + return ApiResponse.ok(groupBuyService.detail(id)); + } + + @PostMapping("/api/mini/group-buy-orders") + public ApiResponse createOrder(@Valid @RequestBody GroupBuyOrderRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(orderService.create(user.getUserId(), request)); + } + + @GetMapping("/api/mini/group-buy-orders") + public ApiResponse> listMine() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(orderService.listMine(user.getUserId())); + } + + @GetMapping("/api/mini/group-buy-orders/{id}") + public ApiResponse orderDetail(@PathVariable Long id) { + return ApiResponse.ok(orderService.detail(id)); + } + + @PutMapping("/api/mini/group-buy-orders/{id}/cancel") + public ApiResponse cancel(@PathVariable Long id) { + return ApiResponse.ok(orderService.cancel(id)); + } + + @PutMapping("/api/mini/group-buy-orders/{id}/complete") + public ApiResponse completeOrder(@PathVariable Long id) { + return ApiResponse.ok(orderService.complete(id)); + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderRequest.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderRequest.java new file mode 100644 index 0000000..3cc39ba --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderRequest.java @@ -0,0 +1,80 @@ +package com.linhelp.groupbuy; + +import com.linhelp.common.enums.DeliveryMethod; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class GroupBuyOrderRequest { + private Long tenantId; + private Long communityId; + + @NotNull + private Long groupBuyId; + + private Long addressId; + + @Min(1) + private int quantity; + + @NotNull + private DeliveryMethod deliveryMethod = DeliveryMethod.IMMEDIATE; + + private String remark; + + 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 getGroupBuyId() { + return groupBuyId; + } + + public void setGroupBuyId(Long groupBuyId) { + this.groupBuyId = groupBuyId; + } + + public Long getAddressId() { + return addressId; + } + + public void setAddressId(Long addressId) { + this.addressId = addressId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + public void setDeliveryMethod(DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderResponse.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderResponse.java new file mode 100644 index 0000000..1f227c6 --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderResponse.java @@ -0,0 +1,172 @@ +package com.linhelp.groupbuy; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GroupBuyOrderStatus; +import com.linhelp.common.enums.OfflinePayStatus; + +import java.time.LocalDateTime; + +public class GroupBuyOrderResponse { + private Long id; + private String orderNo; + private Long tenantId; + private Long communityId; + private Long userId; + private Long groupBuyId; + private String groupBuyTitle; + private String coverUrl; + private Long addressId; + private int quantity; + private int priceCent; + private int amountCent; + private DeliveryMethod deliveryMethod; + private GroupBuyOrderStatus status; + private OfflinePayStatus offlinePayStatus; + private String remark; + 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 getGroupBuyId() { + return groupBuyId; + } + + public void setGroupBuyId(Long groupBuyId) { + this.groupBuyId = groupBuyId; + } + + public String getGroupBuyTitle() { + return groupBuyTitle; + } + + public void setGroupBuyTitle(String groupBuyTitle) { + this.groupBuyTitle = groupBuyTitle; + } + + public String getCoverUrl() { + return coverUrl; + } + + public void setCoverUrl(String coverUrl) { + this.coverUrl = coverUrl; + } + + public Long getAddressId() { + return addressId; + } + + public void setAddressId(Long addressId) { + this.addressId = addressId; + } + + 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; + } + + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + public void setDeliveryMethod(DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + } + + public GroupBuyOrderStatus getStatus() { + return status; + } + + public void setStatus(GroupBuyOrderStatus status) { + this.status = status; + } + + public OfflinePayStatus getOfflinePayStatus() { + return offlinePayStatus; + } + + public void setOfflinePayStatus(OfflinePayStatus offlinePayStatus) { + this.offlinePayStatus = offlinePayStatus; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + 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/groupbuy/GroupBuyOrderService.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderService.java new file mode 100644 index 0000000..2444396 --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyOrderService.java @@ -0,0 +1,158 @@ +package com.linhelp.groupbuy; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GroupBuyOrderStatus; +import com.linhelp.common.enums.OfflinePayStatus; +import com.linhelp.common.exception.BizException; +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 GroupBuyOrderService { + private final GroupBuyService groupBuyService; + private final AtomicLong ids = new AtomicLong(1L); + private final Map orders = new LinkedHashMap(); + + public GroupBuyOrderService(GroupBuyService groupBuyService) { + this.groupBuyService = groupBuyService; + } + + public synchronized GroupBuyOrderResponse create(Long userId, GroupBuyOrderRequest request) { + if (request.getDeliveryMethod() != DeliveryMethod.SELF_PICKUP && request.getAddressId() == null) { + throw new BizException("配送地址不能为空"); + } + GroupBuyResponse groupBuy = groupBuyService.reserveForOrder(request.getGroupBuyId(), request.getQuantity()); + Long orderId = ids.getAndIncrement(); + GroupBuyOrderResponse order = new GroupBuyOrderResponse(); + order.setId(orderId); + order.setOrderNo("GB" + String.format("%08d", orderId)); + order.setTenantId(request.getTenantId()); + order.setCommunityId(request.getCommunityId()); + order.setUserId(userId); + order.setGroupBuyId(groupBuy.getId()); + order.setGroupBuyTitle(groupBuy.getTitle()); + order.setCoverUrl(groupBuy.getCoverUrl()); + order.setAddressId(request.getAddressId()); + order.setQuantity(request.getQuantity()); + order.setPriceCent(groupBuy.getPriceCent()); + order.setAmountCent(groupBuy.getPriceCent() * request.getQuantity()); + order.setDeliveryMethod(request.getDeliveryMethod()); + order.setStatus(GroupBuyOrderStatus.PENDING_CONFIRM); + order.setOfflinePayStatus(OfflinePayStatus.UNPAID); + order.setRemark(request.getRemark()); + order.setCreatedAt(LocalDateTime.now()); + order.setUpdatedAt(order.getCreatedAt()); + orders.put(order.getId(), order); + return copy(order); + } + + public synchronized GroupBuyOrderResponse detail(Long id) { + return copy(require(id)); + } + + public synchronized List listMine(Long userId) { + List result = new ArrayList(); + for (GroupBuyOrderResponse order : orders.values()) { + if (userId.equals(order.getUserId())) { + result.add(copy(order)); + } + } + return result; + } + + public synchronized List listByGroupBuy(Long groupBuyId) { + List result = new ArrayList(); + for (GroupBuyOrderResponse order : orders.values()) { + if (groupBuyId.equals(order.getGroupBuyId())) { + result.add(copy(order)); + } + } + return result; + } + + public synchronized GroupBuyOrderResponse confirm(Long id) { + GroupBuyOrderResponse order = require(id); + move(order, GroupBuyOrderStatus.PENDING_CONFIRM, GroupBuyOrderStatus.CONFIRMED); + return copy(order); + } + + public synchronized GroupBuyOrderResponse markReady(Long id) { + GroupBuyOrderResponse order = require(id); + GroupBuyOrderStatus target = DeliveryMethod.SELF_PICKUP.equals(order.getDeliveryMethod()) + ? GroupBuyOrderStatus.PENDING_PICKUP + : GroupBuyOrderStatus.PENDING_DELIVERY; + move(order, GroupBuyOrderStatus.CONFIRMED, target); + return copy(order); + } + + public synchronized GroupBuyOrderResponse markDelivering(Long id) { + GroupBuyOrderResponse order = require(id); + move(order, GroupBuyOrderStatus.PENDING_DELIVERY, GroupBuyOrderStatus.DELIVERING); + return copy(order); + } + + public synchronized GroupBuyOrderResponse complete(Long id) { + GroupBuyOrderResponse order = require(id); + if (order.getStatus() != GroupBuyOrderStatus.PENDING_PICKUP && order.getStatus() != GroupBuyOrderStatus.DELIVERING) { + throw new BizException("订单状态不允许操作"); + } + order.setStatus(GroupBuyOrderStatus.COMPLETED); + order.setUpdatedAt(LocalDateTime.now()); + return copy(order); + } + + public synchronized GroupBuyOrderResponse cancel(Long id) { + GroupBuyOrderResponse order = require(id); + if (order.getStatus() == GroupBuyOrderStatus.COMPLETED) { + throw new BizException("订单状态不允许操作"); + } + order.setStatus(GroupBuyOrderStatus.CANCELED); + order.setUpdatedAt(LocalDateTime.now()); + return copy(order); + } + + private void move(GroupBuyOrderResponse order, GroupBuyOrderStatus expected, GroupBuyOrderStatus target) { + if (order.getStatus() != expected) { + throw new BizException("订单状态不允许操作"); + } + order.setStatus(target); + order.setUpdatedAt(LocalDateTime.now()); + } + + private GroupBuyOrderResponse require(Long id) { + GroupBuyOrderResponse order = orders.get(id); + if (order == null) { + throw new BizException(404, "团购订单不存在"); + } + return order; + } + + private GroupBuyOrderResponse copy(GroupBuyOrderResponse source) { + GroupBuyOrderResponse target = new GroupBuyOrderResponse(); + target.setId(source.getId()); + target.setOrderNo(source.getOrderNo()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setUserId(source.getUserId()); + target.setGroupBuyId(source.getGroupBuyId()); + target.setGroupBuyTitle(source.getGroupBuyTitle()); + target.setCoverUrl(source.getCoverUrl()); + target.setAddressId(source.getAddressId()); + target.setQuantity(source.getQuantity()); + target.setPriceCent(source.getPriceCent()); + target.setAmountCent(source.getAmountCent()); + target.setDeliveryMethod(source.getDeliveryMethod()); + target.setStatus(source.getStatus()); + target.setOfflinePayStatus(source.getOfflinePayStatus()); + target.setRemark(source.getRemark()); + target.setCreatedAt(source.getCreatedAt()); + target.setUpdatedAt(source.getUpdatedAt()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyRequest.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyRequest.java new file mode 100644 index 0000000..7c7074e --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyRequest.java @@ -0,0 +1,121 @@ +package com.linhelp.groupbuy; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +public class GroupBuyRequest { + private Long tenantId; + private Long communityId; + + @NotBlank + private String title; + + private String coverUrl; + private String description; + + @Min(0) + private int priceCent; + + private Integer originPriceCent; + + @Min(0) + private int stock; + + @NotNull + private LocalDateTime startTime; + + @NotNull + private LocalDateTime endTime; + + private String pickupAddress; + + 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 getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + 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 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 LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public String getPickupAddress() { + return pickupAddress; + } + + public void setPickupAddress(String pickupAddress) { + this.pickupAddress = pickupAddress; + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyResponse.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyResponse.java new file mode 100644 index 0000000..de6796d --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyResponse.java @@ -0,0 +1,132 @@ +package com.linhelp.groupbuy; + +import java.time.LocalDateTime; + +public class GroupBuyResponse { + private Long id; + private Long tenantId; + private Long communityId; + private String title; + private String coverUrl; + private String description; + private int priceCent; + private Integer originPriceCent; + private int stock; + private int soldCount; + private String status; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String pickupAddress; + + 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 getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + 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 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 int getSoldCount() { + return soldCount; + } + + public void setSoldCount(int soldCount) { + this.soldCount = soldCount; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public String getPickupAddress() { + return pickupAddress; + } + + public void setPickupAddress(String pickupAddress) { + this.pickupAddress = pickupAddress; + } +} diff --git a/backend/src/main/java/com/linhelp/groupbuy/GroupBuyService.java b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyService.java new file mode 100644 index 0000000..390ad20 --- /dev/null +++ b/backend/src/main/java/com/linhelp/groupbuy/GroupBuyService.java @@ -0,0 +1,141 @@ +package com.linhelp.groupbuy; + +import com.linhelp.common.exception.BizException; +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 GroupBuyService { + public static final String STATUS_DRAFT = "DRAFT"; + public static final String STATUS_ONGOING = "ONGOING"; + public static final String STATUS_CLOSED = "CLOSED"; + public static final String STATUS_COMPLETED = "COMPLETED"; + + private final AtomicLong ids = new AtomicLong(1L); + private final Map groupBuys = new LinkedHashMap(); + + public synchronized GroupBuyResponse create(GroupBuyRequest request) { + GroupBuyResponse groupBuy = new GroupBuyResponse(); + groupBuy.setId(ids.getAndIncrement()); + groupBuy.setTenantId(request.getTenantId()); + groupBuy.setCommunityId(request.getCommunityId()); + copyFields(request, groupBuy); + groupBuy.setStatus(STATUS_DRAFT); + groupBuys.put(groupBuy.getId(), groupBuy); + return copy(groupBuy); + } + + public synchronized GroupBuyResponse update(Long id, GroupBuyRequest request) { + GroupBuyResponse groupBuy = require(id); + if (request.getTenantId() != null) { + groupBuy.setTenantId(request.getTenantId()); + } + if (request.getCommunityId() != null) { + groupBuy.setCommunityId(request.getCommunityId()); + } + copyFields(request, groupBuy); + return copy(groupBuy); + } + + public synchronized GroupBuyResponse start(Long id) { + GroupBuyResponse groupBuy = require(id); + groupBuy.setStatus(STATUS_ONGOING); + return copy(groupBuy); + } + + public synchronized GroupBuyResponse close(Long id) { + GroupBuyResponse groupBuy = require(id); + groupBuy.setStatus(STATUS_CLOSED); + return copy(groupBuy); + } + + public synchronized GroupBuyResponse complete(Long id) { + GroupBuyResponse groupBuy = require(id); + groupBuy.setStatus(STATUS_COMPLETED); + return copy(groupBuy); + } + + public synchronized GroupBuyResponse detail(Long id) { + return copy(require(id)); + } + + public synchronized List listAdmin(Long communityId) { + List result = new ArrayList(); + for (GroupBuyResponse groupBuy : groupBuys.values()) { + if (communityId.equals(groupBuy.getCommunityId())) { + result.add(copy(groupBuy)); + } + } + return result; + } + + public synchronized List listMini(Long communityId) { + List result = new ArrayList(); + for (GroupBuyResponse groupBuy : groupBuys.values()) { + if (communityId.equals(groupBuy.getCommunityId()) && STATUS_ONGOING.equals(groupBuy.getStatus())) { + result.add(copy(groupBuy)); + } + } + return result; + } + + public synchronized GroupBuyResponse reserveForOrder(Long id, int quantity) { + if (quantity <= 0) { + throw new BizException("购买数量必须大于0"); + } + GroupBuyResponse groupBuy = require(id); + if (!STATUS_ONGOING.equals(groupBuy.getStatus()) || LocalDateTime.now().isAfter(groupBuy.getEndTime())) { + throw new BizException("团购已截单"); + } + if (groupBuy.getSoldCount() + quantity > groupBuy.getStock()) { + throw new BizException("团购库存不足"); + } + groupBuy.setSoldCount(groupBuy.getSoldCount() + quantity); + return copy(groupBuy); + } + + private GroupBuyResponse require(Long id) { + GroupBuyResponse groupBuy = groupBuys.get(id); + if (groupBuy == null) { + throw new BizException(404, "团购不存在"); + } + return groupBuy; + } + + private void copyFields(GroupBuyRequest request, GroupBuyResponse groupBuy) { + groupBuy.setTitle(request.getTitle()); + groupBuy.setCoverUrl(request.getCoverUrl()); + groupBuy.setDescription(request.getDescription()); + groupBuy.setPriceCent(request.getPriceCent()); + groupBuy.setOriginPriceCent(request.getOriginPriceCent()); + groupBuy.setStock(request.getStock()); + groupBuy.setStartTime(request.getStartTime()); + groupBuy.setEndTime(request.getEndTime()); + groupBuy.setPickupAddress(request.getPickupAddress()); + } + + private GroupBuyResponse copy(GroupBuyResponse source) { + GroupBuyResponse target = new GroupBuyResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setTitle(source.getTitle()); + target.setCoverUrl(source.getCoverUrl()); + target.setDescription(source.getDescription()); + target.setPriceCent(source.getPriceCent()); + target.setOriginPriceCent(source.getOriginPriceCent()); + target.setStock(source.getStock()); + target.setSoldCount(source.getSoldCount()); + target.setStatus(source.getStatus()); + target.setStartTime(source.getStartTime()); + target.setEndTime(source.getEndTime()); + target.setPickupAddress(source.getPickupAddress()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentRequest.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentRequest.java new file mode 100644 index 0000000..2b2066b --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentRequest.java @@ -0,0 +1,16 @@ +package com.linhelp.secondhand; + +import javax.validation.constraints.NotBlank; + +public class SecondGoodsCommentRequest { + @NotBlank + private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentResponse.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentResponse.java new file mode 100644 index 0000000..8e7695d --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsCommentResponse.java @@ -0,0 +1,51 @@ +package com.linhelp.secondhand; + +import java.time.LocalDateTime; + +public class SecondGoodsCommentResponse { + private Long id; + private Long goodsId; + private Long userId; + private String content; + private LocalDateTime createdAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getGoodsId() { + return goodsId; + } + + public void setGoodsId(Long goodsId) { + this.goodsId = goodsId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsController.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsController.java new file mode 100644 index 0000000..a145f42 --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsController.java @@ -0,0 +1,91 @@ +package com.linhelp.secondhand; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.enums.SecondGoodsStatus; +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 SecondGoodsController { + private final SecondGoodsService secondGoodsService; + private final AuthService authService; + + public SecondGoodsController(SecondGoodsService secondGoodsService, AuthService authService) { + this.secondGoodsService = secondGoodsService; + this.authService = authService; + } + + @GetMapping("/api/admin/second-goods") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse> listAdmin(@RequestParam(required = false) SecondGoodsStatus status) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(secondGoodsService.listAdmin(user.getCommunityId(), status)); + } + + @PutMapping("/api/admin/second-goods/{id}/approve") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse approve(@PathVariable Long id) { + return ApiResponse.ok(secondGoodsService.approve(id)); + } + + @PutMapping("/api/admin/second-goods/{id}/reject") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse reject(@PathVariable Long id, @RequestBody SecondGoodsRejectRequest request) { + return ApiResponse.ok(secondGoodsService.reject(id, request.getReason())); + } + + @PutMapping("/api/admin/second-goods/{id}/off-shelf") + @RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) + public ApiResponse adminOffShelf(@PathVariable Long id) { + return ApiResponse.ok(secondGoodsService.adminOffShelf(id)); + } + + @GetMapping("/api/mini/second-goods") + public ApiResponse> listPublic(@RequestParam(required = false) String category) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(secondGoodsService.listPublic(user.getCommunityId(), category)); + } + + @GetMapping("/api/mini/second-goods/{id}") + public ApiResponse detail(@PathVariable Long id) { + return ApiResponse.ok(secondGoodsService.detail(id)); + } + + @PostMapping("/api/mini/second-goods") + public ApiResponse create(@Valid @RequestBody SecondGoodsRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(secondGoodsService.create(user.getUserId(), request)); + } + + @PutMapping("/api/mini/second-goods/{id}/off-shelf") + public ApiResponse offShelf(@PathVariable Long id) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(secondGoodsService.offShelf(user.getUserId(), id)); + } + + @PutMapping("/api/mini/second-goods/{id}/sold") + public ApiResponse sold(@PathVariable Long id) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(secondGoodsService.sold(user.getUserId(), id)); + } + + @PostMapping("/api/mini/second-goods/{id}/comments") + public ApiResponse comment(@PathVariable Long id, + @Valid @RequestBody SecondGoodsCommentRequest request) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(secondGoodsService.comment(user.getUserId(), id, request)); + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRejectRequest.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRejectRequest.java new file mode 100644 index 0000000..43f4805 --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRejectRequest.java @@ -0,0 +1,13 @@ +package com.linhelp.secondhand; + +public class SecondGoodsRejectRequest { + private String reason; + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRequest.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRequest.java new file mode 100644 index 0000000..713e346 --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsRequest.java @@ -0,0 +1,96 @@ +package com.linhelp.secondhand; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +public class SecondGoodsRequest { + private Long tenantId; + private Long communityId; + + @NotBlank + private String title; + + private List imageUrls = new ArrayList(); + + @Min(0) + private int priceCent; + + private String description; + private String category; + private String contactPhone; + private String tradeMethod; + + 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 getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getImageUrls() { + return imageUrls; + } + + public void setImageUrls(List imageUrls) { + this.imageUrls = imageUrls; + } + + public int getPriceCent() { + return priceCent; + } + + public void setPriceCent(int priceCent) { + this.priceCent = priceCent; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getTradeMethod() { + return tradeMethod; + } + + public void setTradeMethod(String tradeMethod) { + this.tradeMethod = tradeMethod; + } +} diff --git a/backend/src/main/java/com/linhelp/secondhand/SecondGoodsResponse.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsResponse.java new file mode 100644 index 0000000..27e4df5 --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsResponse.java @@ -0,0 +1,154 @@ +package com.linhelp.secondhand; + +import com.linhelp.common.enums.SecondGoodsStatus; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class SecondGoodsResponse { + private Long id; + private Long tenantId; + private Long communityId; + private Long sellerUserId; + private String title; + private List imageUrls = new ArrayList(); + private int priceCent; + private String description; + private String category; + private String contactPhone; + private String tradeMethod; + private SecondGoodsStatus status; + private String reviewReason; + private List comments = new ArrayList(); + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + 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 getSellerUserId() { + return sellerUserId; + } + + public void setSellerUserId(Long sellerUserId) { + this.sellerUserId = sellerUserId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getImageUrls() { + return imageUrls; + } + + public void setImageUrls(List imageUrls) { + this.imageUrls = imageUrls; + } + + public int getPriceCent() { + return priceCent; + } + + public void setPriceCent(int priceCent) { + this.priceCent = priceCent; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getTradeMethod() { + return tradeMethod; + } + + public void setTradeMethod(String tradeMethod) { + this.tradeMethod = tradeMethod; + } + + public SecondGoodsStatus getStatus() { + return status; + } + + public void setStatus(SecondGoodsStatus status) { + this.status = status; + } + + public String getReviewReason() { + return reviewReason; + } + + public void setReviewReason(String reviewReason) { + this.reviewReason = reviewReason; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + 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/secondhand/SecondGoodsService.java b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsService.java new file mode 100644 index 0000000..e64de3b --- /dev/null +++ b/backend/src/main/java/com/linhelp/secondhand/SecondGoodsService.java @@ -0,0 +1,189 @@ +package com.linhelp.secondhand; + +import com.linhelp.common.enums.SecondGoodsStatus; +import com.linhelp.common.exception.BizException; +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 SecondGoodsService { + private final AtomicLong ids = new AtomicLong(1L); + private final AtomicLong commentIds = new AtomicLong(1L); + private final Map goods = new LinkedHashMap(); + + public synchronized SecondGoodsResponse create(Long sellerUserId, SecondGoodsRequest request) { + SecondGoodsResponse response = new SecondGoodsResponse(); + response.setId(ids.getAndIncrement()); + response.setTenantId(request.getTenantId()); + response.setCommunityId(request.getCommunityId()); + response.setSellerUserId(sellerUserId); + copyFields(request, response); + response.setStatus(SecondGoodsStatus.PENDING_REVIEW); + response.setCreatedAt(LocalDateTime.now()); + response.setUpdatedAt(response.getCreatedAt()); + goods.put(response.getId(), response); + return copy(response); + } + + public synchronized List listPublic(Long communityId, String category) { + List result = new ArrayList(); + for (SecondGoodsResponse item : goods.values()) { + if (!communityId.equals(item.getCommunityId())) { + continue; + } + if (category != null && !category.equals(item.getCategory())) { + continue; + } + if (SecondGoodsStatus.PUBLISHED == item.getStatus()) { + result.add(copy(item)); + } + } + return result; + } + + public synchronized List listAdmin(Long communityId, SecondGoodsStatus status) { + List result = new ArrayList(); + for (SecondGoodsResponse item : goods.values()) { + if (!communityId.equals(item.getCommunityId())) { + continue; + } + if (status != null && status != item.getStatus()) { + continue; + } + result.add(copy(item)); + } + return result; + } + + public synchronized SecondGoodsResponse detail(Long id) { + return copy(require(id)); + } + + public synchronized SecondGoodsResponse approve(Long id) { + SecondGoodsResponse item = require(id); + item.setStatus(SecondGoodsStatus.PUBLISHED); + item.setReviewReason(null); + item.setUpdatedAt(LocalDateTime.now()); + return copy(item); + } + + public synchronized SecondGoodsResponse reject(Long id, String reason) { + if (reason == null || reason.trim().isEmpty()) { + throw new BizException("拒绝原因不能为空"); + } + SecondGoodsResponse item = require(id); + item.setStatus(SecondGoodsStatus.REJECTED); + item.setReviewReason(reason); + item.setUpdatedAt(LocalDateTime.now()); + return copy(item); + } + + public synchronized SecondGoodsResponse adminOffShelf(Long id) { + SecondGoodsResponse item = require(id); + item.setStatus(SecondGoodsStatus.OFF_SHELF); + item.setUpdatedAt(LocalDateTime.now()); + return copy(item); + } + + public synchronized SecondGoodsResponse offShelf(Long sellerUserId, Long id) { + SecondGoodsResponse item = requireMine(sellerUserId, id); + item.setStatus(SecondGoodsStatus.OFF_SHELF); + item.setUpdatedAt(LocalDateTime.now()); + return copy(item); + } + + public synchronized SecondGoodsResponse sold(Long sellerUserId, Long id) { + SecondGoodsResponse item = requireMine(sellerUserId, id); + item.setStatus(SecondGoodsStatus.SOLD); + item.setUpdatedAt(LocalDateTime.now()); + return copy(item); + } + + public synchronized SecondGoodsCommentResponse comment(Long userId, Long goodsId, SecondGoodsCommentRequest request) { + SecondGoodsResponse item = require(goodsId); + if (SecondGoodsStatus.PUBLISHED != item.getStatus()) { + throw new BizException("闲置暂不可留言"); + } + SecondGoodsCommentResponse comment = new SecondGoodsCommentResponse(); + comment.setId(commentIds.getAndIncrement()); + comment.setGoodsId(goodsId); + comment.setUserId(userId); + comment.setContent(request.getContent()); + comment.setCreatedAt(LocalDateTime.now()); + item.getComments().add(comment); + return copyComment(comment); + } + + private SecondGoodsResponse requireMine(Long sellerUserId, Long id) { + SecondGoodsResponse item = require(id); + if (!sellerUserId.equals(item.getSellerUserId())) { + throw new BizException(404, "闲置不存在"); + } + return item; + } + + private SecondGoodsResponse require(Long id) { + SecondGoodsResponse item = goods.get(id); + if (item == null) { + throw new BizException(404, "闲置不存在"); + } + return item; + } + + private void copyFields(SecondGoodsRequest request, SecondGoodsResponse response) { + response.setTitle(request.getTitle()); + response.setImageUrls(request.getImageUrls() == null + ? new ArrayList() + : new ArrayList(request.getImageUrls())); + response.setPriceCent(request.getPriceCent()); + response.setDescription(request.getDescription()); + response.setCategory(request.getCategory()); + response.setContactPhone(request.getContactPhone()); + response.setTradeMethod(request.getTradeMethod()); + } + + private SecondGoodsResponse copy(SecondGoodsResponse source) { + SecondGoodsResponse target = new SecondGoodsResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setSellerUserId(source.getSellerUserId()); + target.setTitle(source.getTitle()); + target.setImageUrls(new ArrayList(source.getImageUrls())); + target.setPriceCent(source.getPriceCent()); + target.setDescription(source.getDescription()); + target.setCategory(source.getCategory()); + target.setContactPhone(source.getContactPhone()); + target.setTradeMethod(source.getTradeMethod()); + target.setStatus(source.getStatus()); + target.setReviewReason(source.getReviewReason()); + target.setComments(copyComments(source.getComments())); + target.setCreatedAt(source.getCreatedAt()); + target.setUpdatedAt(source.getUpdatedAt()); + return target; + } + + private List copyComments(List sourceComments) { + List comments = new ArrayList(); + for (SecondGoodsCommentResponse source : sourceComments) { + comments.add(copyComment(source)); + } + return comments; + } + + private SecondGoodsCommentResponse copyComment(SecondGoodsCommentResponse source) { + SecondGoodsCommentResponse target = new SecondGoodsCommentResponse(); + target.setId(source.getId()); + target.setGoodsId(source.getGoodsId()); + target.setUserId(source.getUserId()); + target.setContent(source.getContent()); + target.setCreatedAt(source.getCreatedAt()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/storage/FileStorageController.java b/backend/src/main/java/com/linhelp/storage/FileStorageController.java new file mode 100644 index 0000000..06276a9 --- /dev/null +++ b/backend/src/main/java/com/linhelp/storage/FileStorageController.java @@ -0,0 +1,26 @@ +package com.linhelp.storage; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +public class FileStorageController { + private final FileStorageService fileStorageService; + private final AuthService authService; + + public FileStorageController(FileStorageService fileStorageService, AuthService authService) { + this.fileStorageService = fileStorageService; + this.authService = authService; + } + + @PostMapping("/api/files/upload") + public ApiResponse upload(@RequestParam("file") MultipartFile file) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(fileStorageService.upload(user.getTenantId(), user.getCommunityId(), file)); + } +} diff --git a/backend/src/main/java/com/linhelp/storage/FileStorageService.java b/backend/src/main/java/com/linhelp/storage/FileStorageService.java new file mode 100644 index 0000000..a4c5735 --- /dev/null +++ b/backend/src/main/java/com/linhelp/storage/FileStorageService.java @@ -0,0 +1,51 @@ +package com.linhelp.storage; + +import com.linhelp.common.exception.BizException; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +@Service +public class FileStorageService { + private static final DateTimeFormatter DATE_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + + public FileUploadResponse upload(Long tenantId, Long communityId, MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BizException("文件不能为空"); + } + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new BizException("仅支持图片上传"); + } + + String originalFilename = file.getOriginalFilename(); + String extension = extensionOf(originalFilename); + String objectKey = "tenant/" + tenantId + + "/community/" + communityId + + "/" + DATE_PATH_FORMATTER.format(LocalDate.now()) + + "/" + UUID.randomUUID().toString().replace("-", "") + + extension; + + FileUploadResponse response = new FileUploadResponse(); + response.setOriginalFilename(originalFilename); + response.setContentType(contentType); + response.setSize(file.getSize()); + response.setObjectKey(objectKey); + response.setUrl("/uploads/" + objectKey); + return response; + } + + private String extensionOf(String filename) { + if (filename == null) { + return ""; + } + int dot = filename.lastIndexOf('.'); + if (dot < 0 || dot == filename.length() - 1) { + return ""; + } + return filename.substring(dot).toLowerCase(); + } +} diff --git a/backend/src/main/java/com/linhelp/storage/FileUploadResponse.java b/backend/src/main/java/com/linhelp/storage/FileUploadResponse.java new file mode 100644 index 0000000..83df712 --- /dev/null +++ b/backend/src/main/java/com/linhelp/storage/FileUploadResponse.java @@ -0,0 +1,49 @@ +package com.linhelp.storage; + +public class FileUploadResponse { + private String originalFilename; + private String contentType; + private long size; + private String objectKey; + private String url; + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getObjectKey() { + return objectKey; + } + + public void setObjectKey(String objectKey) { + this.objectKey = objectKey; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/backend/src/test/java/com/linhelp/feedback/FeedbackServiceTests.java b/backend/src/test/java/com/linhelp/feedback/FeedbackServiceTests.java new file mode 100644 index 0000000..81bc4b8 --- /dev/null +++ b/backend/src/test/java/com/linhelp/feedback/FeedbackServiceTests.java @@ -0,0 +1,23 @@ +package com.linhelp.feedback; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FeedbackServiceTests { + @Test + void createFeedbackStartsPending() { + FeedbackService service = new FeedbackService(); + FeedbackRequest request = new FeedbackRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setCategory("DELIVERY"); + request.setContent("配送慢了"); + request.setContactPhone("13800000000"); + + FeedbackResponse response = service.create(100L, request); + + assertThat(response.getUserId()).isEqualTo(100L); + assertThat(response.getStatus()).isEqualTo(FeedbackService.STATUS_PENDING); + } +} diff --git a/backend/src/test/java/com/linhelp/groupbuy/GroupBuyServiceTests.java b/backend/src/test/java/com/linhelp/groupbuy/GroupBuyServiceTests.java new file mode 100644 index 0000000..d5dcd98 --- /dev/null +++ b/backend/src/test/java/com/linhelp/groupbuy/GroupBuyServiceTests.java @@ -0,0 +1,62 @@ +package com.linhelp.groupbuy; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.GroupBuyOrderStatus; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GroupBuyServiceTests { + @Test + void cannotOrderAfterGroupBuyDeadline() { + GroupBuyService groupBuyService = new GroupBuyService(); + GroupBuyOrderService orderService = new GroupBuyOrderService(groupBuyService); + GroupBuyResponse groupBuy = groupBuyService.create(groupBuyRequest(LocalDateTime.now().minusMinutes(1))); + groupBuyService.start(groupBuy.getId()); + + assertThatThrownBy(() -> orderService.create(1L, orderRequest(groupBuy.getId(), DeliveryMethod.IMMEDIATE))) + .hasMessageContaining("团购已截单"); + } + + @Test + void groupBuyOrderCanBePreparedForSelfPickup() { + GroupBuyService groupBuyService = new GroupBuyService(); + GroupBuyOrderService orderService = new GroupBuyOrderService(groupBuyService); + GroupBuyResponse groupBuy = groupBuyService.create(groupBuyRequest(LocalDateTime.now().plusDays(1))); + groupBuyService.start(groupBuy.getId()); + + GroupBuyOrderResponse order = orderService.create(1L, orderRequest(groupBuy.getId(), DeliveryMethod.SELF_PICKUP)); + orderService.confirm(order.getId()); + orderService.markReady(order.getId()); + + assertThat(orderService.detail(order.getId()).getStatus()).isEqualTo(GroupBuyOrderStatus.PENDING_PICKUP); + } + + private GroupBuyRequest groupBuyRequest(LocalDateTime endTime) { + GroupBuyRequest request = new GroupBuyRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setTitle("今日鸡蛋团购"); + request.setCoverUrl("https://img.test/egg.jpg"); + request.setPriceCent(2990); + request.setStock(100); + request.setStartTime(LocalDateTime.now().minusHours(1)); + request.setEndTime(endTime); + request.setPickupAddress("小区北门"); + return request; + } + + private GroupBuyOrderRequest orderRequest(Long groupBuyId, DeliveryMethod deliveryMethod) { + GroupBuyOrderRequest request = new GroupBuyOrderRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setGroupBuyId(groupBuyId); + request.setAddressId(1L); + request.setQuantity(2); + request.setDeliveryMethod(deliveryMethod); + return request; + } +} diff --git a/backend/src/test/java/com/linhelp/secondhand/SecondGoodsReviewTests.java b/backend/src/test/java/com/linhelp/secondhand/SecondGoodsReviewTests.java new file mode 100644 index 0000000..b18a483 --- /dev/null +++ b/backend/src/test/java/com/linhelp/secondhand/SecondGoodsReviewTests.java @@ -0,0 +1,48 @@ +package com.linhelp.secondhand; + +import com.linhelp.common.enums.SecondGoodsStatus; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SecondGoodsReviewTests { + @Test + void newListingStartsPendingAndBecomesVisibleAfterApproval() { + SecondGoodsService service = new SecondGoodsService(); + SecondGoodsResponse listing = service.create(1L, request()); + + assertThat(listing.getStatus()).isEqualTo(SecondGoodsStatus.PENDING_REVIEW); + assertThat(service.listPublic(1L, null)).isEmpty(); + + service.approve(listing.getId()); + + assertThat(service.listPublic(1L, null)).extracting(SecondGoodsResponse::getId) + .containsExactly(listing.getId()); + } + + @Test + void rejectionRequiresReason() { + SecondGoodsService service = new SecondGoodsService(); + SecondGoodsResponse listing = service.create(1L, request()); + + assertThatThrownBy(() -> service.reject(listing.getId(), "")) + .hasMessageContaining("拒绝原因不能为空"); + } + + private SecondGoodsRequest request() { + SecondGoodsRequest request = new SecondGoodsRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setTitle("九成新儿童车"); + request.setPriceCent(9900); + request.setDescription("小区内自提"); + request.setCategory("母婴"); + request.setContactPhone("13800000000"); + request.setTradeMethod("FACE_TO_FACE"); + request.setImageUrls(Arrays.asList("https://img.test/bike.jpg")); + return request; + } +} diff --git a/backend/src/test/java/com/linhelp/storage/FileStorageServiceTests.java b/backend/src/test/java/com/linhelp/storage/FileStorageServiceTests.java new file mode 100644 index 0000000..fd34f1b --- /dev/null +++ b/backend/src/test/java/com/linhelp/storage/FileStorageServiceTests.java @@ -0,0 +1,30 @@ +package com.linhelp.storage; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FileStorageServiceTests { + @Test + void imageUploadUsesTenantCommunityDatePrefix() { + FileStorageService service = new FileStorageService(); + MockMultipartFile file = new MockMultipartFile("file", "avatar.png", "image/png", new byte[]{1, 2, 3}); + + FileUploadResponse response = service.upload(1L, 2L, file); + + assertThat(response.getObjectKey()).startsWith("tenant/1/community/2/"); + assertThat(response.getObjectKey()).endsWith(".png"); + assertThat(response.getUrl()).contains(response.getObjectKey()); + } + + @Test + void nonImageUploadIsRejected() { + FileStorageService service = new FileStorageService(); + MockMultipartFile file = new MockMultipartFile("file", "doc.pdf", "application/pdf", new byte[]{1}); + + assertThatThrownBy(() -> service.upload(1L, 1L, file)) + .hasMessageContaining("仅支持图片上传"); + } +}