From e7ccadaea0a8cf8d729fb24b3b9c3d800348f5b6 Mon Sep 17 00:00:00 2001 From: irving Date: Fri, 31 Oct 2025 02:48:03 -0400 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=9B=B2=E7=9B=92=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=E5=92=8C=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复和改进: - 修复字段映射:blind_box_gift_id -> blind_box_id - 移除不必要的 @Version 乐观锁字段 - 优化 Mapper 方法:统一使用 listActiveEntries,简化查询逻辑 - 新增客户端接口:盲盒购买、奖励查询和兑现 - 增强权限校验:奖励兑现时验证客户身份 - 完善单元测试:增加客户身份验证测试用例 - 代码格式化:调整 import 顺序,优化代码结构 客户端 API: - GET /wx/blind-box/config/list - 查询可用盲盒列表 - POST /wx/blind-box/order/purchase - 购买盲盒 - GET /wx/blind-box/reward/list - 查询我的盲盒奖励 - POST /wx/blind-box/reward/{id}/dispatch - 兑现盲盒奖励 其他优化: - 增强 SQL 查询安全性,添加 deleted 字段过滤 - 优化店员提成计算逻辑 - 改进参数可选性(levelId 参数改为可选) --- .../controller/BlindBoxConfigController.java | 2 +- .../blindbox/mapper/BlindBoxPoolMapper.java | 14 +- .../blindbox/mapper/BlindBoxRewardMapper.java | 14 ++ .../module/entity/BlindBoxRewardEntity.java | 5 +- .../service/BlindBoxDispatchService.java | 8 +- .../service/BlindBoxPoolAdminService.java | 1 - .../blindbox/service/BlindBoxService.java | 18 +- .../impl/BlindBoxConfigServiceImpl.java | 2 +- .../controller/WxBlindBoxController.java | 89 ++++++++ .../WxClerkCommodityController.java | 2 +- .../entity/blindbox/BlindBoxConfigView.java | 21 ++ .../blindbox/BlindBoxPurchaseRequest.java | 18 ++ .../blindbox/BlindBoxPurchaseResult.java | 42 ++++ .../BlindBoxRewardDispatchRequest.java | 11 + .../entity/blindbox/BlindBoxRewardView.java | 33 +++ .../service/WxBlindBoxOrderService.java | 207 ++++++++++++++++++ .../blindbox/service/BlindBoxServiceTest.java | 23 +- 17 files changed, 480 insertions(+), 30 deletions(-) create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxBlindBoxController.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxConfigView.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseRequest.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseResult.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardDispatchRequest.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardView.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxBlindBoxOrderService.java diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/controller/BlindBoxConfigController.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/controller/BlindBoxConfigController.java index 4127cfa..3460008 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/controller/BlindBoxConfigController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/controller/BlindBoxConfigController.java @@ -4,8 +4,8 @@ import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.starry.admin.common.exception.CustomException; -import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus; +import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; import com.starry.admin.modules.blindbox.service.BlindBoxConfigService; import com.starry.admin.utils.SecurityUtils; import com.starry.common.result.R; diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxPoolMapper.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxPoolMapper.java index e21a2d8..ec3f99a 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxPoolMapper.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxPoolMapper.java @@ -3,7 +3,6 @@ package com.starry.admin.modules.blindbox.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate; import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity; -import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus; import java.time.LocalDateTime; import java.util.List; import org.apache.ibatis.annotations.Mapper; @@ -17,24 +16,24 @@ public interface BlindBoxPoolMapper extends BaseMapper { @Select({ "SELECT id AS poolId,", " tenant_id AS tenantId,", - " blind_box_gift_id AS blindBoxId,", + " blind_box_id AS blindBoxId,", " reward_gift_id AS rewardGiftId,", " reward_price AS rewardPrice,", " weight,", " remaining_stock AS remainingStock", "FROM blind_box_pool", "WHERE tenant_id = #{tenantId}", - " AND blind_box_gift_id = #{blindBoxId}", - " AND status = #{enabledStatus}", + " AND blind_box_id = #{blindBoxId}", + " AND status = 1", + " AND deleted = 0", " AND (valid_from IS NULL OR valid_from <= #{now})", " AND (valid_to IS NULL OR valid_to >= #{now})", " AND (remaining_stock IS NULL OR remaining_stock > 0)" }) - List listEntries( + List listActiveEntries( @Param("tenantId") String tenantId, @Param("blindBoxId") String blindBoxId, - @Param("now") LocalDateTime now, - @Param("enabledStatus") int enabledStatus); + @Param("now") LocalDateTime now); @Update({ "UPDATE blind_box_pool", @@ -45,6 +44,7 @@ public interface BlindBoxPoolMapper extends BaseMapper { "WHERE tenant_id = #{tenantId}", " AND id = #{poolId}", " AND reward_gift_id = #{rewardGiftId}", + " AND deleted = 0", " AND (remaining_stock IS NULL OR remaining_stock > 0)" }) int consumeRewardStock( diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxRewardMapper.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxRewardMapper.java index a4e547b..390f2f5 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxRewardMapper.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/mapper/BlindBoxRewardMapper.java @@ -3,6 +3,7 @@ package com.starry.admin.modules.blindbox.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity; import java.time.LocalDateTime; +import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; @@ -29,4 +30,17 @@ public interface BlindBoxRewardMapper extends BaseMapper { @Param("clerkId") String clerkId, @Param("orderId") String orderId, @Param("usedTime") LocalDateTime usedTime); + + @Select({ + "SELECT * FROM blind_box_reward", + "WHERE tenant_id = #{tenantId}", + " AND customer_id = #{customerId}", + " AND deleted = 0", + " AND (#{status} IS NULL OR status = #{status})", + "ORDER BY created_time DESC" + }) + List listByCustomer( + @Param("tenantId") String tenantId, + @Param("customerId") String customerId, + @Param("status") String status); } diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/module/entity/BlindBoxRewardEntity.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/module/entity/BlindBoxRewardEntity.java index 5496aba..b07e53f 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/module/entity/BlindBoxRewardEntity.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/module/entity/BlindBoxRewardEntity.java @@ -2,7 +2,6 @@ package com.starry.admin.modules.blindbox.module.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.annotation.Version; import com.fasterxml.jackson.annotation.JsonFormat; import com.starry.common.domain.BaseEntity; import java.math.BigDecimal; @@ -20,7 +19,7 @@ public class BlindBoxRewardEntity extends BaseEntity { private String tenantId; private String customerId; - @TableField("blind_box_gift_id") + @TableField("blind_box_id") private String blindBoxId; private String rewardGiftId; private BigDecimal rewardPrice; @@ -42,6 +41,4 @@ public class BlindBoxRewardEntity extends BaseEntity { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime usedTime; - @Version - private Integer version; } diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxDispatchService.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxDispatchService.java index 8797c64..0332e1e 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxDispatchService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxDispatchService.java @@ -11,6 +11,7 @@ import com.starry.admin.modules.order.module.dto.PaymentInfo; import com.starry.admin.modules.order.service.IOrderLifecycleService; import com.starry.admin.modules.order.service.IPlayOrderInfoService; import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity; +import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService; import com.starry.admin.modules.shop.service.IPlayGiftInfoService; import com.starry.common.utils.IdUtils; import java.math.BigDecimal; @@ -26,6 +27,7 @@ public class BlindBoxDispatchService { private final IOrderLifecycleService orderLifecycleService; private final IPlayOrderInfoService orderInfoService; private final IPlayGiftInfoService giftInfoService; + private final IPlayClerkGiftInfoService clerkGiftInfoService; @Transactional(rollbackFor = Exception.class) public OrderPlacementResult dispatchRewardOrder(BlindBoxRewardEntity reward, String clerkId) { @@ -71,9 +73,13 @@ public class BlindBoxDispatchService { .remark("盲盒奖励兑现") .build(); - return orderLifecycleService.placeOrder(OrderPlacementCommand.builder() + OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder() .orderContext(context) .balanceOperationAction("盲盒奖励兑现") .build()); + if (clerkId != null) { + clerkGiftInfoService.incrementGiftCount(clerkId, giftInfo.getId(), reward.getTenantId(), 1); + } + return result; } } diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxPoolAdminService.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxPoolAdminService.java index 9cdf54f..8d043d7 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxPoolAdminService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxPoolAdminService.java @@ -25,7 +25,6 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxService.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxService.java index 866a574..5c3c7ff 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/BlindBoxService.java @@ -3,7 +3,6 @@ package com.starry.admin.modules.blindbox.service; import com.starry.admin.common.exception.CustomException; import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper; import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper; -import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus; import com.starry.admin.modules.blindbox.module.constant.BlindBoxRewardStatus; import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate; import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; @@ -76,11 +75,7 @@ public class BlindBoxService { if (!tenantId.equals(config.getTenantId())) { throw new CustomException("盲盒不存在或已下架"); } - List candidates = poolMapper.listEntries( - tenantId, - blindBoxId, - now, - BlindBoxPoolStatus.ENABLED.getCode()); + List candidates = poolMapper.listActiveEntries(tenantId, blindBoxId, now); if (CollectionUtils.isEmpty(candidates)) { throw new CustomException("盲盒奖池暂无可用奖励"); } @@ -111,15 +106,18 @@ public class BlindBoxService { } @Transactional(rollbackFor = Exception.class) - public OrderPlacementResult dispatchReward(String rewardId, String clerkId) { + public OrderPlacementResult dispatchReward(String rewardId, String clerkId, String customerId) { BlindBoxRewardEntity reward = rewardMapper.lockByIdForUpdate(rewardId); if (reward == null) { throw new CustomException("盲盒奖励不存在"); } + if (customerId != null && !customerId.equals(reward.getCustomerId())) { + throw new CustomException("无权操作该盲盒奖励"); + } + LocalDateTime now = LocalDateTime.now(clock); if (!BlindBoxRewardStatus.UNUSED.getCode().equals(reward.getStatus())) { throw new CustomException("盲盒奖励已使用"); } - LocalDateTime now = LocalDateTime.now(clock); if (reward.getExpiresAt() != null && reward.getExpiresAt().isBefore(now)) { throw new CustomException("盲盒奖励已过期"); } @@ -160,6 +158,10 @@ public class BlindBoxService { : value.setScale(2, RoundingMode.HALF_UP); } + public java.util.List listRewards(String tenantId, String customerId, String status) { + return rewardMapper.listByCustomer(tenantId, customerId, status); + } + public interface RandomAdapter { double nextDouble(); } diff --git a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/impl/BlindBoxConfigServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/impl/BlindBoxConfigServiceImpl.java index 0fa13c9..383ca7a 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/impl/BlindBoxConfigServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/blindbox/service/impl/BlindBoxConfigServiceImpl.java @@ -3,10 +3,10 @@ package com.starry.admin.modules.blindbox.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.starry.admin.common.exception.CustomException; import com.starry.admin.modules.blindbox.mapper.BlindBoxConfigMapper; +import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus; import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; import com.starry.admin.modules.blindbox.service.BlindBoxConfigService; import java.util.List; -import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus; import org.springframework.stereotype.Service; @Service diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxBlindBoxController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxBlindBoxController.java new file mode 100644 index 0000000..e1afb62 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxBlindBoxController.java @@ -0,0 +1,89 @@ +package com.starry.admin.modules.weichat.controller; + +import cn.hutool.core.collection.CollUtil; +import com.starry.admin.common.aspect.CustomUserLogin; +import com.starry.admin.common.conf.ThreadLocalRequestDetail; +import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; +import com.starry.admin.modules.blindbox.service.BlindBoxConfigService; +import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxConfigView; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxPurchaseRequest; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxPurchaseResult; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxRewardDispatchRequest; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxRewardView; +import com.starry.admin.modules.weichat.service.WxBlindBoxOrderService; +import com.starry.common.result.R; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/wx/blind-box") +@Validated +public class WxBlindBoxController { + + @Resource + private BlindBoxConfigService blindBoxConfigService; + + @Resource + private WxBlindBoxOrderService wxBlindBoxOrderService; + + @CustomUserLogin + @GetMapping("/config/list") + public R listConfigs() { + PlayCustomUserInfoEntity user = ThreadLocalRequestDetail.getCustomUserInfo(); + if (user == null) { + throw new CustomException("用户未登录"); + } + List configs = blindBoxConfigService.listActiveByTenant(user.getTenantId()); + List views = CollUtil.emptyIfNull(configs).stream() + .map(this::toConfigView) + .collect(Collectors.toList()); + return R.ok(views); + } + + @CustomUserLogin + @PostMapping("/order/purchase") + public R purchase(@Valid @RequestBody BlindBoxPurchaseRequest request) { + BlindBoxPurchaseResult result = wxBlindBoxOrderService.purchase(request); + return R.ok(result); + } + + @CustomUserLogin + @PostMapping("/reward/{id}/dispatch") + public R dispatch(@PathVariable("id") String rewardId, @Valid @RequestBody BlindBoxRewardDispatchRequest body) { + BlindBoxRewardView view = wxBlindBoxOrderService.dispatchReward(rewardId, body.getClerkId()); + return R.ok(view); + } + + @CustomUserLogin + @GetMapping("/reward/list") + public R listRewards(@RequestParam(value = "status", required = false) String status) { + PlayCustomUserInfoEntity user = ThreadLocalRequestDetail.getCustomUserInfo(); + if (user == null) { + throw new CustomException("用户未登录"); + } + List rewards = wxBlindBoxOrderService.listRewards(user.getTenantId(), user.getId(), status); + return R.ok(rewards); + } + + private BlindBoxConfigView toConfigView(BlindBoxConfigEntity entity) { + BlindBoxConfigView view = new BlindBoxConfigView(); + view.setId(entity.getId()); + view.setName(entity.getName()); + view.setCoverUrl(entity.getCoverUrl()); + view.setDescription(entity.getDescription()); + view.setPrice(entity.getPrice()); + return view; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkCommodityController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkCommodityController.java index 5adbd61..fcdf941 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkCommodityController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkCommodityController.java @@ -57,7 +57,7 @@ public class WxClerkCommodityController { @ApiResponses({ @ApiResponse(code = 200, message = "操作成功", response = PlayCommodityReturnVo.class, responseContainer = "List")}) @GetMapping("/custom/queryClerkAllCommodityByLevel") - public R queryClerkAllCommodityByLevel(@RequestParam("id") String levelId) { + public R queryClerkAllCommodityByLevel(@RequestParam(value = "id", required = false) String levelId) { List levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll(); List tree = playCommodityInfoService.selectTree(); if (levelId == null || levelId.isEmpty()) { diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxConfigView.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxConfigView.java new file mode 100644 index 0000000..a8a2343 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxConfigView.java @@ -0,0 +1,21 @@ +package com.starry.admin.modules.weichat.entity.blindbox; + +import java.math.BigDecimal; +import lombok.Data; + +/** + * 盲盒配置前端视图。 + */ +@Data +public class BlindBoxConfigView { + + private String id; + + private String name; + + private String coverUrl; + + private String description; + + private BigDecimal price; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseRequest.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseRequest.java new file mode 100644 index 0000000..8678ed5 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseRequest.java @@ -0,0 +1,18 @@ +package com.starry.admin.modules.weichat.entity.blindbox; + +import javax.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class BlindBoxPurchaseRequest { + + @NotBlank(message = "盲盒ID不能为空") + private String blindBoxId; + + @NotBlank(message = "店员ID不能为空") + private String clerkId; + + private String weiChatCode; + + private String remark; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseResult.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseResult.java new file mode 100644 index 0000000..2304a23 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxPurchaseResult.java @@ -0,0 +1,42 @@ +package com.starry.admin.modules.weichat.entity.blindbox; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class BlindBoxPurchaseResult { + + private String orderId; + + private BlindBoxRewardInfo reward; + + @Data + @Builder + public static class BlindBoxRewardInfo { + + private String rewardId; + + private String blindBoxId; + + private String blindBoxName; + + private String blindBoxCover; + + private String rewardGiftId; + + private String rewardGiftName; + + private String rewardGiftImage; + + private BigDecimal rewardGiftPrice; + + private BigDecimal boxPrice; + + private LocalDateTime expiresAt; + + private String status; + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardDispatchRequest.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardDispatchRequest.java new file mode 100644 index 0000000..5b3f9cc --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardDispatchRequest.java @@ -0,0 +1,11 @@ +package com.starry.admin.modules.weichat.entity.blindbox; + +import javax.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class BlindBoxRewardDispatchRequest { + + @NotBlank(message = "店员ID不能为空") + private String clerkId; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardView.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardView.java new file mode 100644 index 0000000..26cec26 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/entity/blindbox/BlindBoxRewardView.java @@ -0,0 +1,33 @@ +package com.starry.admin.modules.weichat.entity.blindbox; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class BlindBoxRewardView { + + private String id; + + private String blindBoxId; + + private String blindBoxName; + + private String blindBoxCover; + + private String rewardGiftId; + + private String rewardGiftName; + + private String rewardGiftImage; + + private BigDecimal rewardGiftPrice; + + private BigDecimal boxPrice; + + private LocalDateTime expiresAt; + + private String status; +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxBlindBoxOrderService.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxBlindBoxOrderService.java new file mode 100644 index 0000000..2b456ed --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxBlindBoxOrderService.java @@ -0,0 +1,207 @@ +package com.starry.admin.modules.weichat.service; + +import com.starry.admin.common.conf.ThreadLocalRequestDetail; +import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper; +import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity; +import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity; +import com.starry.admin.modules.blindbox.service.BlindBoxConfigService; +import com.starry.admin.modules.blindbox.service.BlindBoxService; +import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; +import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; +import com.starry.admin.modules.order.module.constant.OrderConstant; +import com.starry.admin.modules.order.module.constant.OrderConstant.OrderActor; +import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType; +import com.starry.admin.modules.order.module.dto.CommodityInfo; +import com.starry.admin.modules.order.module.dto.OrderCreationContext; +import com.starry.admin.modules.order.module.dto.OrderPlacementCommand; +import com.starry.admin.modules.order.module.dto.OrderPlacementResult; +import com.starry.admin.modules.order.module.dto.PaymentInfo; +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.order.service.IOrderLifecycleService; +import com.starry.admin.modules.order.service.IPlayOrderInfoService; +import com.starry.admin.modules.shop.mapper.PlayGiftInfoMapper; +import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxPurchaseRequest; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxPurchaseResult; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxPurchaseResult.BlindBoxRewardInfo; +import com.starry.admin.modules.weichat.entity.blindbox.BlindBoxRewardView; +import com.starry.common.utils.IdUtils; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class WxBlindBoxOrderService { + + @Resource + private BlindBoxConfigService blindBoxConfigService; + + @Resource + private BlindBoxService blindBoxService; + + @Resource + private IOrderLifecycleService orderLifecycleService; + + @Resource + private IPlayOrderInfoService playOrderInfoService; + + @Resource + private IPlayCustomUserInfoService customUserInfoService; + + @Resource + private PlayGiftInfoMapper playGiftInfoMapper; + + @Resource + private BlindBoxRewardMapper blindBoxRewardMapper; + + @Transactional(rollbackFor = Exception.class) + public BlindBoxPurchaseResult purchase(BlindBoxPurchaseRequest request) { + PlayCustomUserInfoEntity sessionUser = ThreadLocalRequestDetail.getCustomUserInfo(); + if (sessionUser == null || sessionUser.getId() == null) { + throw new CustomException("用户未登录"); + } + PlayCustomUserInfoEntity customer = customUserInfoService.selectById(sessionUser.getId()); + if (customer == null) { + throw new CustomException("用户不存在"); + } + + BlindBoxConfigEntity config = blindBoxConfigService.requireById(request.getBlindBoxId()); + if (!Objects.equals(config.getTenantId(), customer.getTenantId())) { + throw new CustomException("盲盒不存在或已下架"); + } + if (!Objects.equals(config.getStatus(), 1)) { + throw new CustomException("盲盒已下架"); + } + + BigDecimal boxPrice = Objects.requireNonNull(config.getPrice(), "盲盒价格未配置"); + if (boxPrice.compareTo(BigDecimal.ZERO) <= 0) { + throw new CustomException("盲盒价格异常"); + } + + String orderId = IdUtils.getUuid(); + OrderCreationContext orderRequest = buildOrderRequest(orderId, request, customer, config, boxPrice); + OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder() + .orderContext(orderRequest) + .balanceOperationAction("购买盲盒") + .build()); + PlayOrderInfoEntity order = result.getOrder(); + + BlindBoxRewardEntity reward = blindBoxService.drawReward( + customer.getTenantId(), + orderId, + customer.getId(), + config.getId(), + UUID.randomUUID().toString()); + + PlayGiftInfoEntity giftInfo = playGiftInfoMapper.selectById(reward.getRewardGiftId()); + + return BlindBoxPurchaseResult.builder() + .orderId(order.getId()) + .reward(buildRewardInfo(reward, config, giftInfo)) + .build(); + } + + public List listRewards(String tenantId, String customerId, String status) { + return blindBoxService.listRewards(tenantId, customerId, status).stream() + .map(this::toRewardView) + .collect(Collectors.toList()); + } + + @Transactional(rollbackFor = Exception.class) + public BlindBoxRewardView dispatchReward(String rewardId, String clerkId) { + PlayCustomUserInfoEntity sessionUser = ThreadLocalRequestDetail.getCustomUserInfo(); + if (sessionUser == null || sessionUser.getId() == null) { + throw new CustomException("用户未登录"); + } + blindBoxService.dispatchReward(rewardId, clerkId, sessionUser.getId()); + BlindBoxRewardEntity updated = blindBoxRewardMapper.selectById(rewardId); + if (updated == null) { + throw new CustomException("盲盒奖励不存在"); + } + return toRewardView(updated); + } + + public BlindBoxRewardView toRewardView(BlindBoxRewardEntity reward) { + BlindBoxConfigEntity config = blindBoxConfigService.getById(reward.getBlindBoxId()); + PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(reward.getRewardGiftId()); + return BlindBoxRewardView.builder() + .id(reward.getId()) + .blindBoxId(reward.getBlindBoxId()) + .blindBoxName(config != null ? config.getName() : null) + .blindBoxCover(config != null ? config.getCoverUrl() : null) + .rewardGiftId(reward.getRewardGiftId()) + .rewardGiftName(gift != null ? gift.getName() : reward.getRewardGiftId()) + .rewardGiftImage(gift != null ? gift.getUrl() : null) + .rewardGiftPrice(reward.getRewardPrice()) + .boxPrice(reward.getBoxPrice()) + .expiresAt(reward.getExpiresAt()) + .status(reward.getStatus()) + .build(); + } + + private BlindBoxRewardInfo buildRewardInfo( + BlindBoxRewardEntity reward, + BlindBoxConfigEntity config, + PlayGiftInfoEntity giftInfo) { + return BlindBoxRewardInfo.builder() + .rewardId(reward.getId()) + .blindBoxId(reward.getBlindBoxId()) + .blindBoxName(config != null ? config.getName() : null) + .blindBoxCover(config != null ? config.getCoverUrl() : null) + .rewardGiftId(reward.getRewardGiftId()) + .rewardGiftName(giftInfo != null ? giftInfo.getName() : reward.getRewardGiftId()) + .rewardGiftImage(giftInfo != null ? giftInfo.getUrl() : null) + .rewardGiftPrice(reward.getRewardPrice()) + .boxPrice(reward.getBoxPrice()) + .expiresAt(reward.getExpiresAt()) + .status(reward.getStatus()) + .build(); + } + + private OrderCreationContext buildOrderRequest( + String orderId, + BlindBoxPurchaseRequest request, + PlayCustomUserInfoEntity customer, + BlindBoxConfigEntity config, + BigDecimal boxPrice) { + + return OrderCreationContext.builder() + .orderId(orderId) + .orderNo(playOrderInfoService.getOrderNo()) + .orderStatus(OrderConstant.OrderStatus.COMPLETED) + .orderType(OrderConstant.OrderType.BLIND_BOX_PURCHASE) + .placeType(OrderConstant.PlaceType.REWARD) + .rewardType(RewardType.GIFT) + .isFirstOrder(false) + .creatorActor(OrderActor.CUSTOMER) + .creatorId(customer.getId()) + .commodityInfo(CommodityInfo.builder() + .commodityId(config.getId()) + .commodityType(OrderConstant.CommodityType.GIFT) + .commodityPrice(config.getPrice()) + .commodityName(config.getName()) + .commodityNumber("1") + .serviceDuration("") + .build()) + .paymentInfo(PaymentInfo.builder() + .orderMoney(boxPrice) + .finalAmount(boxPrice) + .discountAmount(BigDecimal.ZERO) + .couponIds(Collections.emptyList()) + .payMethod(OrderConstant.PayMethod.BALANCE.getCode()) + .paymentSource(OrderConstant.PaymentSource.BALANCE) + .build()) + .purchaserBy(customer.getId()) + .acceptBy(request.getClerkId()) + .weiChatCode(request.getWeiChatCode()) + .remark(request.getRemark()) + .build(); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/modules/blindbox/service/BlindBoxServiceTest.java b/play-admin/src/test/java/com/starry/admin/modules/blindbox/service/BlindBoxServiceTest.java index 82dd7ed..fed5264 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/blindbox/service/BlindBoxServiceTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/blindbox/service/BlindBoxServiceTest.java @@ -87,7 +87,7 @@ class BlindBoxServiceTest { BlindBoxCandidate.of(1L, "tenant-1", "blind-1", "gift-low", BigDecimal.valueOf(10), 10, 5), BlindBoxCandidate.of(2L, "tenant-1", "blind-1", "gift-high", BigDecimal.valueOf(99), 90, 3) ); - when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any())).thenReturn(candidates); + when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class))).thenReturn(candidates); BlindBoxRewardEntity entity = blindBoxService.drawReward("tenant-1", "order-1", "customer-1", "blind-1", "seed-123"); @@ -115,7 +115,7 @@ class BlindBoxServiceTest { List candidates = Collections.singletonList( BlindBoxCandidate.of(7L, "tenant-1", "blind-1", "gift-unlimited", BigDecimal.valueOf(59), 100, null) ); - when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any())).thenReturn(candidates); + when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class))).thenReturn(candidates); blindBoxService.drawReward("tenant-1", "order-2", "customer-9", "blind-1", "seed-unlimited"); @@ -137,11 +137,11 @@ class BlindBoxServiceTest { when(dispatchService.dispatchRewardOrder(eq(reward), eq("clerk-1"))).thenReturn(mock(OrderPlacementResult.class)); when(rewardMapper.markUsed(eq("reward-1"), eq("clerk-1"), any(), any())).thenReturn(1); - blindBoxService.dispatchReward("reward-1", "clerk-1"); + blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1"); reward.setStatus(BlindBoxRewardStatus.USED.getCode()); CustomException ex = assertThrows(CustomException.class, () -> - blindBoxService.dispatchReward("reward-1", "clerk-1")); + blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1")); assertTrue(ex.getMessage().contains("已使用")); verify(rewardMapper, times(1)).markUsed(eq("reward-1"), eq("clerk-1"), any(), any()); @@ -154,11 +154,23 @@ class BlindBoxServiceTest { when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward); CustomException ex = assertThrows(CustomException.class, () -> - blindBoxService.dispatchReward("reward-1", "clerk-1")); + blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1")); assertTrue(ex.getMessage().contains("已过期")); verify(dispatchService, times(0)).dispatchRewardOrder(any(), any()); } + @Test + void shouldRejectDispatchWhenCustomerMismatch() { + BlindBoxRewardEntity reward = buildRewardEntity(); + when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward); + + CustomException ex = assertThrows(CustomException.class, () -> + blindBoxService.dispatchReward("reward-1", "clerk-1", "someone-else")); + assertTrue(ex.getMessage().contains("无权")); + verify(dispatchService, times(0)).dispatchRewardOrder(any(), any()); + verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any()); + } + private BlindBoxRewardEntity buildRewardEntity() { BlindBoxRewardEntity reward = new BlindBoxRewardEntity(); reward.setId("reward-1"); @@ -175,7 +187,6 @@ class BlindBoxServiceTest { reward.setCreatedTime(java.sql.Timestamp.from(clock.instant())); reward.setUpdatedTime(java.sql.Timestamp.from(clock.instant())); reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusHours(1)); - reward.setVersion(0); return reward; }