Compare commits
3 Commits
c9439e1021
...
8f89955405
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f89955405 | ||
|
|
e7ccadaea0 | ||
|
|
422e781c60 |
@@ -0,0 +1,108 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.controller;
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Api(tags = "盲盒配置管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/play/blind-box/config")
|
||||||
|
public class BlindBoxConfigController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
|
||||||
|
@ApiOperation("查询盲盒列表")
|
||||||
|
@GetMapping
|
||||||
|
public R list(@RequestParam(required = false) Integer status) {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
LambdaQueryWrapper<BlindBoxConfigEntity> query = Wrappers.lambdaQuery(BlindBoxConfigEntity.class)
|
||||||
|
.eq(BlindBoxConfigEntity::getTenantId, tenantId)
|
||||||
|
.eq(BlindBoxConfigEntity::getDeleted, Boolean.FALSE)
|
||||||
|
.orderByDesc(BlindBoxConfigEntity::getCreatedTime);
|
||||||
|
if (status != null) {
|
||||||
|
query.eq(BlindBoxConfigEntity::getStatus, status);
|
||||||
|
}
|
||||||
|
List<BlindBoxConfigEntity> configs = blindBoxConfigService.list(query);
|
||||||
|
return R.ok(configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("盲盒详情")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R detail(@PathVariable String id) {
|
||||||
|
BlindBoxConfigEntity entity = blindBoxConfigService.requireById(id);
|
||||||
|
return R.ok(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("新增盲盒")
|
||||||
|
@PostMapping
|
||||||
|
public R create(@RequestBody BlindBoxConfigEntity body) {
|
||||||
|
if (StrUtil.isBlank(body.getName())) {
|
||||||
|
throw new CustomException("盲盒名称不能为空");
|
||||||
|
}
|
||||||
|
validatePrice(body.getPrice());
|
||||||
|
body.setId(IdUtils.getUuid());
|
||||||
|
body.setTenantId(SecurityUtils.getTenantId());
|
||||||
|
body.setDeleted(Boolean.FALSE);
|
||||||
|
if (body.getStatus() == null) {
|
||||||
|
body.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
|
||||||
|
}
|
||||||
|
blindBoxConfigService.save(body);
|
||||||
|
return R.ok(body.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("更新盲盒")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public R update(@PathVariable String id, @RequestBody BlindBoxConfigEntity body) {
|
||||||
|
validatePrice(body.getPrice());
|
||||||
|
BlindBoxConfigEntity existing = blindBoxConfigService.requireById(id);
|
||||||
|
if (!SecurityUtils.getTenantId().equals(existing.getTenantId())) {
|
||||||
|
throw new CustomException("无权操作该盲盒");
|
||||||
|
}
|
||||||
|
existing.setName(body.getName());
|
||||||
|
existing.setCoverUrl(body.getCoverUrl());
|
||||||
|
existing.setDescription(body.getDescription());
|
||||||
|
existing.setPrice(body.getPrice());
|
||||||
|
existing.setStatus(body.getStatus());
|
||||||
|
blindBoxConfigService.updateById(existing);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("删除盲盒")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R delete(@PathVariable String id) {
|
||||||
|
BlindBoxConfigEntity existing = blindBoxConfigService.requireById(id);
|
||||||
|
if (!SecurityUtils.getTenantId().equals(existing.getTenantId())) {
|
||||||
|
throw new CustomException("无权操作该盲盒");
|
||||||
|
}
|
||||||
|
blindBoxConfigService.removeById(id);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePrice(BigDecimal price) {
|
||||||
|
if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new CustomException("盲盒价格必须大于0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.controller;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
|
||||||
|
import com.starry.admin.modules.blindbox.service.BlindBoxPoolAdminService;
|
||||||
|
import com.starry.admin.utils.ExcelUtils;
|
||||||
|
import com.starry.common.result.R;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Api(tags = "盲盒奖池管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/play/blind-box/pool")
|
||||||
|
public class BlindBoxPoolController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private BlindBoxPoolAdminService blindBoxPoolAdminService;
|
||||||
|
|
||||||
|
@ApiOperation("查询盲盒奖池列表")
|
||||||
|
@GetMapping
|
||||||
|
public R list(@RequestParam String blindBoxId) {
|
||||||
|
return R.ok(blindBoxPoolAdminService.list(blindBoxId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("导入盲盒奖池配置")
|
||||||
|
@PostMapping("/{blindBoxId}/import")
|
||||||
|
public R importPool(@PathVariable String blindBoxId, @RequestParam("file") MultipartFile file)
|
||||||
|
throws IOException {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new CustomException("上传文件不能为空");
|
||||||
|
}
|
||||||
|
List<?> rows = ExcelUtils.importEasyExcel(file.getInputStream(), BlindBoxPoolImportRow.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<BlindBoxPoolImportRow> importRows = (List<BlindBoxPoolImportRow>) rows;
|
||||||
|
blindBoxPoolAdminService.replacePool(blindBoxId, importRows);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("删除盲盒奖池项")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R remove(@PathVariable Long id) {
|
||||||
|
blindBoxPoolAdminService.removeById(id);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("新增盲盒奖池项")
|
||||||
|
@PostMapping
|
||||||
|
public R create(@RequestBody BlindBoxPoolUpsertRequest body) {
|
||||||
|
return R.ok(blindBoxPoolAdminService.create(body != null ? body.getBlindBoxId() : null, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("更新盲盒奖池项")
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public R update(@PathVariable Long id, @RequestBody BlindBoxPoolUpsertRequest body) {
|
||||||
|
return R.ok(blindBoxPoolAdminService.update(id, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("查询可用中奖礼物")
|
||||||
|
@GetMapping("/gifts")
|
||||||
|
public R giftOptions(@RequestParam(required = false) String keyword) {
|
||||||
|
return R.ok(blindBoxPoolAdminService.listGiftOptions(keyword));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BlindBoxConfigMapper extends BaseMapper<BlindBoxConfigEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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 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;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BlindBoxPoolMapper extends BaseMapper<BlindBoxPoolEntity> {
|
||||||
|
|
||||||
|
@Select({
|
||||||
|
"SELECT id AS poolId,",
|
||||||
|
" tenant_id AS tenantId,",
|
||||||
|
" 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_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<BlindBoxCandidate> listActiveEntries(
|
||||||
|
@Param("tenantId") String tenantId,
|
||||||
|
@Param("blindBoxId") String blindBoxId,
|
||||||
|
@Param("now") LocalDateTime now);
|
||||||
|
|
||||||
|
@Update({
|
||||||
|
"UPDATE blind_box_pool",
|
||||||
|
"SET remaining_stock = CASE",
|
||||||
|
" WHEN remaining_stock IS NULL THEN NULL",
|
||||||
|
" ELSE remaining_stock - 1",
|
||||||
|
" END",
|
||||||
|
"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(
|
||||||
|
@Param("tenantId") String tenantId,
|
||||||
|
@Param("poolId") Long poolId,
|
||||||
|
@Param("rewardGiftId") String rewardGiftId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BlindBoxRewardMapper extends BaseMapper<BlindBoxRewardEntity> {
|
||||||
|
|
||||||
|
@Select("SELECT * FROM blind_box_reward WHERE id = #{id} FOR UPDATE")
|
||||||
|
BlindBoxRewardEntity lockByIdForUpdate(@Param("id") String id);
|
||||||
|
|
||||||
|
@Update({
|
||||||
|
"UPDATE blind_box_reward",
|
||||||
|
"SET status = 'USED',",
|
||||||
|
" used_order_id = #{orderId},",
|
||||||
|
" used_clerk_id = #{clerkId},",
|
||||||
|
" used_time = #{usedTime},",
|
||||||
|
" version = version + 1",
|
||||||
|
"WHERE id = #{id}",
|
||||||
|
" AND status = 'UNUSED'"
|
||||||
|
})
|
||||||
|
int markUsed(
|
||||||
|
@Param("id") String id,
|
||||||
|
@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<BlindBoxRewardEntity> listByCustomer(
|
||||||
|
@Param("tenantId") String tenantId,
|
||||||
|
@Param("customerId") String customerId,
|
||||||
|
@Param("status") String status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒(配置)上下架状态。
|
||||||
|
*/
|
||||||
|
public enum BlindBoxConfigStatus {
|
||||||
|
ENABLED(1),
|
||||||
|
DISABLED(0);
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
BlindBoxConfigStatus(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖池项启停状态。
|
||||||
|
*/
|
||||||
|
public enum BlindBoxPoolStatus {
|
||||||
|
ENABLED(1),
|
||||||
|
DISABLED(0);
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
BlindBoxPoolStatus(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.constant;
|
||||||
|
|
||||||
|
public enum BlindBoxRewardStatus {
|
||||||
|
UNUSED("UNUSED"),
|
||||||
|
USED("USED"),
|
||||||
|
REFUNDED("REFUNDED");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
BlindBoxRewardStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒抽奖候选项,封装奖池记录必要信息。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class BlindBoxCandidate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖池记录主键。
|
||||||
|
*/
|
||||||
|
private Long poolId;
|
||||||
|
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
private String blindBoxId;
|
||||||
|
|
||||||
|
private String rewardGiftId;
|
||||||
|
|
||||||
|
private BigDecimal rewardPrice;
|
||||||
|
|
||||||
|
private int weight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余库存,null 表示不限量。
|
||||||
|
*/
|
||||||
|
private Integer remainingStock;
|
||||||
|
|
||||||
|
public static BlindBoxCandidate of(
|
||||||
|
Long poolId,
|
||||||
|
String tenantId,
|
||||||
|
String blindBoxId,
|
||||||
|
String rewardGiftId,
|
||||||
|
BigDecimal rewardPrice,
|
||||||
|
int weight,
|
||||||
|
Integer remainingStock) {
|
||||||
|
return new BlindBoxCandidate(poolId, tenantId, blindBoxId, rewardGiftId, rewardPrice, weight, remainingStock);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖池可选礼物选项。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class BlindBoxGiftOption {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
private String imageUrl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.dto;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖池导入模板行。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BlindBoxPoolImportRow {
|
||||||
|
|
||||||
|
@ExcelProperty("中奖礼物名称")
|
||||||
|
private String rewardGiftName;
|
||||||
|
|
||||||
|
@ExcelProperty("权重")
|
||||||
|
private Integer weight;
|
||||||
|
|
||||||
|
@ExcelProperty("初始库存")
|
||||||
|
private Integer remainingStock;
|
||||||
|
|
||||||
|
@ExcelProperty("生效时间")
|
||||||
|
private String validFrom;
|
||||||
|
|
||||||
|
@ExcelProperty("失效时间")
|
||||||
|
private String validTo;
|
||||||
|
|
||||||
|
@ExcelProperty("状态")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@ExcelProperty("奖品售价(可选)")
|
||||||
|
private BigDecimal overrideRewardPrice;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BlindBoxPoolUpsertRequest {
|
||||||
|
|
||||||
|
private String blindBoxId;
|
||||||
|
|
||||||
|
private String rewardGiftId;
|
||||||
|
|
||||||
|
private BigDecimal rewardPrice;
|
||||||
|
|
||||||
|
private Integer weight;
|
||||||
|
|
||||||
|
private Integer remainingStock;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖池前端展示对象。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BlindBoxPoolView {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String blindBoxId;
|
||||||
|
|
||||||
|
private String blindBoxName;
|
||||||
|
|
||||||
|
private String rewardGiftId;
|
||||||
|
|
||||||
|
private String rewardGiftName;
|
||||||
|
|
||||||
|
private BigDecimal rewardPrice;
|
||||||
|
|
||||||
|
private Integer weight;
|
||||||
|
|
||||||
|
private Integer remainingStock;
|
||||||
|
|
||||||
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒配置实体。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("blind_box_config")
|
||||||
|
public class BlindBoxConfigEntity extends BaseEntity<BlindBoxConfigEntity> {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖池配置实体。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("blind_box_pool")
|
||||||
|
public class BlindBoxPoolEntity extends BaseEntity<BlindBoxPoolEntity> {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
@TableField("blind_box_id")
|
||||||
|
private String blindBoxId;
|
||||||
|
|
||||||
|
private String rewardGiftId;
|
||||||
|
|
||||||
|
private BigDecimal rewardPrice;
|
||||||
|
|
||||||
|
private Integer weight;
|
||||||
|
|
||||||
|
private Integer remainingStock;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.module.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("blind_box_reward")
|
||||||
|
public class BlindBoxRewardEntity extends BaseEntity<BlindBoxRewardEntity> {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String tenantId;
|
||||||
|
private String customerId;
|
||||||
|
|
||||||
|
@TableField("blind_box_id")
|
||||||
|
private String blindBoxId;
|
||||||
|
private String rewardGiftId;
|
||||||
|
private BigDecimal rewardPrice;
|
||||||
|
private BigDecimal boxPrice;
|
||||||
|
private BigDecimal subsidyAmount;
|
||||||
|
private Integer rewardStockSnapshot;
|
||||||
|
private String seed;
|
||||||
|
private String status;
|
||||||
|
private String createdByOrder;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
private String usedOrderId;
|
||||||
|
private String usedClerkId;
|
||||||
|
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
|
private LocalDateTime usedTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface BlindBoxConfigService extends IService<BlindBoxConfigEntity> {
|
||||||
|
|
||||||
|
BlindBoxConfigEntity requireById(String id);
|
||||||
|
|
||||||
|
List<BlindBoxConfigEntity> listActiveByTenant(String tenantId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
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.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;
|
||||||
|
import java.util.Collections;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
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) {
|
||||||
|
PlayGiftInfoEntity giftInfo = giftInfoService.selectPlayGiftInfoById(reward.getRewardGiftId());
|
||||||
|
if (giftInfo == null) {
|
||||||
|
throw new CustomException("奖励礼物不存在或已下架");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal rewardPrice = reward.getRewardPrice();
|
||||||
|
PaymentInfo paymentInfo = PaymentInfo.builder()
|
||||||
|
.orderMoney(rewardPrice)
|
||||||
|
.finalAmount(rewardPrice)
|
||||||
|
.discountAmount(BigDecimal.ZERO)
|
||||||
|
.couponIds(Collections.emptyList())
|
||||||
|
.payMethod(OrderConstant.PayMethod.BALANCE.getCode())
|
||||||
|
.paymentSource(OrderConstant.PaymentSource.BLIND_BOX)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CommodityInfo commodityInfo = CommodityInfo.builder()
|
||||||
|
.commodityId(giftInfo.getId())
|
||||||
|
.commodityType(OrderConstant.CommodityType.GIFT)
|
||||||
|
.commodityPrice(giftInfo.getPrice())
|
||||||
|
.commodityName(giftInfo.getName())
|
||||||
|
.commodityNumber("1")
|
||||||
|
.serviceDuration("")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OrderCreationContext context = OrderCreationContext.builder()
|
||||||
|
.orderId(IdUtils.getUuid())
|
||||||
|
.orderNo(orderInfoService.getOrderNo())
|
||||||
|
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
|
||||||
|
.orderType(OrderConstant.OrderType.GIFT)
|
||||||
|
.placeType(OrderConstant.PlaceType.REWARD)
|
||||||
|
.rewardType(OrderConstant.RewardType.GIFT)
|
||||||
|
.paymentSource(OrderConstant.PaymentSource.BLIND_BOX)
|
||||||
|
.sourceRewardId(reward.getId())
|
||||||
|
.paymentInfo(paymentInfo)
|
||||||
|
.commodityInfo(commodityInfo)
|
||||||
|
.purchaserBy(reward.getCustomerId())
|
||||||
|
.acceptBy(clerkId)
|
||||||
|
.creatorActor(OrderConstant.OrderActor.CUSTOMER)
|
||||||
|
.creatorId(reward.getCustomerId())
|
||||||
|
.remark("盲盒奖励兑现")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
|
||||||
|
.orderContext(context)
|
||||||
|
.balanceOperationAction("盲盒奖励兑现")
|
||||||
|
.build());
|
||||||
|
if (clerkId != null) {
|
||||||
|
clerkGiftInfoService.incrementGiftCount(clerkId, giftInfo.getId(), reward.getTenantId(), 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BlindBoxInventoryService {
|
||||||
|
|
||||||
|
private final BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void reserveRewardStock(String tenantId, Long poolId, String rewardGiftId) {
|
||||||
|
int affected = blindBoxPoolMapper.consumeRewardStock(tenantId, poolId, rewardGiftId);
|
||||||
|
if (affected <= 0) {
|
||||||
|
throw new CustomException("盲盒奖池库存不足,请稍后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
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.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxGiftOption;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolView;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.shop.mapper.PlayGiftInfoMapper;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftHistory;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftState;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftType;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BlindBoxPoolAdminService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private final BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
private final BlindBoxConfigService blindBoxConfigService;
|
||||||
|
private final PlayGiftInfoMapper playGiftInfoMapper;
|
||||||
|
|
||||||
|
public List<BlindBoxPoolView> list(String blindBoxId) {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(blindBoxId)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity config = blindBoxConfigService.requireById(blindBoxId);
|
||||||
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<BlindBoxPoolEntity> query = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
|
||||||
|
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
|
||||||
|
.eq(BlindBoxPoolEntity::getBlindBoxId, blindBoxId)
|
||||||
|
.eq(BlindBoxPoolEntity::getDeleted, Boolean.FALSE)
|
||||||
|
.orderByAsc(BlindBoxPoolEntity::getId);
|
||||||
|
List<BlindBoxPoolEntity> entities = blindBoxPoolMapper.selectList(query);
|
||||||
|
if (CollUtil.isEmpty(entities)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Set<String> rewardIds = entities.stream()
|
||||||
|
.map(BlindBoxPoolEntity::getRewardGiftId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
List<PlayGiftInfoEntity> gifts = playGiftInfoMapper.selectBatchIds(rewardIds);
|
||||||
|
Map<String, PlayGiftInfoEntity> giftMap = gifts.stream()
|
||||||
|
.collect(Collectors.toMap(PlayGiftInfoEntity::getId, Function.identity()));
|
||||||
|
return entities.stream()
|
||||||
|
.map(entity -> toView(entity, config, giftMap.get(entity.getRewardGiftId())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlindBoxGiftOption> listGiftOptions(String keyword) {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
if (StrUtil.isBlank(tenantId)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<PlayGiftInfoEntity> query = Wrappers.lambdaQuery(PlayGiftInfoEntity.class)
|
||||||
|
.eq(PlayGiftInfoEntity::getTenantId, tenantId)
|
||||||
|
.eq(PlayGiftInfoEntity::getHistory, GiftHistory.CURRENT.getCode())
|
||||||
|
.eq(PlayGiftInfoEntity::getState, GiftState.ACTIVE.getCode())
|
||||||
|
.eq(PlayGiftInfoEntity::getType, GiftType.NORMAL.getCode())
|
||||||
|
.eq(PlayGiftInfoEntity::getDeleted, Boolean.FALSE);
|
||||||
|
if (StrUtil.isNotBlank(keyword)) {
|
||||||
|
query.like(PlayGiftInfoEntity::getName, keyword.trim());
|
||||||
|
}
|
||||||
|
query.orderByAsc(PlayGiftInfoEntity::getName);
|
||||||
|
List<PlayGiftInfoEntity> gifts = playGiftInfoMapper.selectList(query);
|
||||||
|
if (CollUtil.isEmpty(gifts)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return gifts.stream()
|
||||||
|
.map(gift -> new BlindBoxGiftOption(gift.getId(), gift.getName(), gift.getPrice(), gift.getUrl()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public BlindBoxPoolView create(String blindBoxId, BlindBoxPoolUpsertRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new CustomException("参数不能为空");
|
||||||
|
}
|
||||||
|
String tenantId = requireTenantId();
|
||||||
|
String targetBlindBoxId = StrUtil.isNotBlank(blindBoxId) ? blindBoxId : request.getBlindBoxId();
|
||||||
|
if (StrUtil.isBlank(targetBlindBoxId)) {
|
||||||
|
throw new CustomException("盲盒ID不能为空");
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity config = blindBoxConfigService.requireById(targetBlindBoxId);
|
||||||
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity rewardGift = requireRewardGift(tenantId, request.getRewardGiftId());
|
||||||
|
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
||||||
|
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
||||||
|
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
||||||
|
Integer status = resolveStatus(request.getStatus());
|
||||||
|
BigDecimal rewardPrice = resolveRewardPrice(request.getRewardPrice(), rewardGift.getPrice());
|
||||||
|
String operatorId = currentUserIdSafely();
|
||||||
|
|
||||||
|
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setBlindBoxId(targetBlindBoxId);
|
||||||
|
entity.setRewardGiftId(rewardGift.getId());
|
||||||
|
entity.setRewardPrice(rewardPrice);
|
||||||
|
entity.setWeight(weight);
|
||||||
|
entity.setRemainingStock(remainingStock);
|
||||||
|
entity.setValidFrom(request.getValidFrom());
|
||||||
|
entity.setValidTo(request.getValidTo());
|
||||||
|
entity.setStatus(status);
|
||||||
|
entity.setDeleted(Boolean.FALSE);
|
||||||
|
entity.setCreatedBy(operatorId);
|
||||||
|
entity.setUpdatedBy(operatorId);
|
||||||
|
|
||||||
|
blindBoxPoolMapper.insert(entity);
|
||||||
|
return toView(entity, config, rewardGift);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public BlindBoxPoolView update(Long id, BlindBoxPoolUpsertRequest request) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new CustomException("记录ID不能为空");
|
||||||
|
}
|
||||||
|
if (request == null) {
|
||||||
|
throw new CustomException("参数不能为空");
|
||||||
|
}
|
||||||
|
String tenantId = requireTenantId();
|
||||||
|
BlindBoxPoolEntity existing = blindBoxPoolMapper.selectById(id);
|
||||||
|
if (existing == null || Boolean.TRUE.equals(existing.getDeleted())) {
|
||||||
|
throw new CustomException("记录不存在或已删除");
|
||||||
|
}
|
||||||
|
if (!tenantId.equals(existing.getTenantId())) {
|
||||||
|
throw new CustomException("无权操作该记录");
|
||||||
|
}
|
||||||
|
String targetBlindBoxId = StrUtil.isNotBlank(request.getBlindBoxId())
|
||||||
|
? request.getBlindBoxId()
|
||||||
|
: existing.getBlindBoxId();
|
||||||
|
BlindBoxConfigEntity config = blindBoxConfigService.requireById(targetBlindBoxId);
|
||||||
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity rewardGift = requireRewardGift(tenantId, request.getRewardGiftId());
|
||||||
|
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
||||||
|
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
||||||
|
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
||||||
|
Integer status = resolveStatus(request.getStatus());
|
||||||
|
BigDecimal rewardPrice = resolveRewardPrice(request.getRewardPrice(), rewardGift.getPrice());
|
||||||
|
String operatorId = currentUserIdSafely();
|
||||||
|
|
||||||
|
existing.setBlindBoxId(targetBlindBoxId);
|
||||||
|
existing.setRewardGiftId(rewardGift.getId());
|
||||||
|
existing.setRewardPrice(rewardPrice);
|
||||||
|
existing.setWeight(weight);
|
||||||
|
existing.setRemainingStock(remainingStock);
|
||||||
|
existing.setValidFrom(request.getValidFrom());
|
||||||
|
existing.setValidTo(request.getValidTo());
|
||||||
|
existing.setStatus(status);
|
||||||
|
existing.setUpdatedBy(operatorId);
|
||||||
|
blindBoxPoolMapper.updateById(existing);
|
||||||
|
return toView(existing, config, rewardGift);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void replacePool(String blindBoxId, List<BlindBoxPoolImportRow> rows) {
|
||||||
|
if (StrUtil.isBlank(blindBoxId)) {
|
||||||
|
throw new CustomException("盲盒ID不能为空");
|
||||||
|
}
|
||||||
|
if (CollUtil.isEmpty(rows)) {
|
||||||
|
throw new CustomException("导入数据不能为空");
|
||||||
|
}
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
if (StrUtil.isBlank(tenantId)) {
|
||||||
|
throw new CustomException("租户信息缺失");
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity config = blindBoxConfigService.requireById(blindBoxId);
|
||||||
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
|
}
|
||||||
|
List<String> rewardNames = rows.stream()
|
||||||
|
.map(BlindBoxPoolImportRow::getRewardGiftName)
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (CollUtil.isEmpty(rewardNames)) {
|
||||||
|
throw new CustomException("导入数据缺少中奖礼物名称");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<PlayGiftInfoEntity> rewardQuery = Wrappers.lambdaQuery(PlayGiftInfoEntity.class)
|
||||||
|
.eq(PlayGiftInfoEntity::getTenantId, tenantId)
|
||||||
|
.eq(PlayGiftInfoEntity::getHistory, GiftHistory.CURRENT.getCode())
|
||||||
|
.eq(PlayGiftInfoEntity::getState, GiftState.ACTIVE.getCode())
|
||||||
|
.eq(PlayGiftInfoEntity::getType, GiftType.NORMAL.getCode())
|
||||||
|
.in(PlayGiftInfoEntity::getName, rewardNames);
|
||||||
|
List<PlayGiftInfoEntity> rewardGifts = playGiftInfoMapper.selectList(rewardQuery);
|
||||||
|
Map<String, List<PlayGiftInfoEntity>> rewardsByName = rewardGifts.stream()
|
||||||
|
.collect(Collectors.groupingBy(PlayGiftInfoEntity::getName));
|
||||||
|
List<String> duplicateNames = rewardsByName.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().size() > 1)
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (CollUtil.isNotEmpty(duplicateNames)) {
|
||||||
|
throw new CustomException("存在同名礼物,无法区分:" + String.join("、", duplicateNames));
|
||||||
|
}
|
||||||
|
Map<String, PlayGiftInfoEntity> rewardMap = rewardsByName.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get(0)));
|
||||||
|
String operatorId = currentUserIdSafely();
|
||||||
|
List<BlindBoxPoolEntity> toInsert = new ArrayList<>(rows.size());
|
||||||
|
for (BlindBoxPoolImportRow row : rows) {
|
||||||
|
if (StrUtil.isBlank(row.getRewardGiftName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity rewardGift = rewardMap.get(row.getRewardGiftName());
|
||||||
|
if (rewardGift == null) {
|
||||||
|
throw new CustomException("中奖礼物不存在: " + row.getRewardGiftName());
|
||||||
|
}
|
||||||
|
Integer weight = row.getWeight();
|
||||||
|
if (weight == null || weight <= 0) {
|
||||||
|
throw new CustomException("礼物 " + row.getRewardGiftName() + " 权重必须为正整数");
|
||||||
|
}
|
||||||
|
Integer remainingStock = row.getRemainingStock();
|
||||||
|
if (remainingStock != null && remainingStock < 0) {
|
||||||
|
throw new CustomException("礼物 " + row.getRewardGiftName() + " 库存不能为负数");
|
||||||
|
}
|
||||||
|
Integer status = row.getStatus() == null
|
||||||
|
? BlindBoxPoolStatus.ENABLED.getCode()
|
||||||
|
: row.getStatus();
|
||||||
|
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setBlindBoxId(blindBoxId);
|
||||||
|
entity.setRewardGiftId(rewardGift.getId());
|
||||||
|
entity.setRewardPrice(resolveRewardPrice(row.getOverrideRewardPrice(), rewardGift.getPrice()));
|
||||||
|
entity.setWeight(weight);
|
||||||
|
entity.setRemainingStock(remainingStock);
|
||||||
|
entity.setValidFrom(parseDateTime(row.getValidFrom()));
|
||||||
|
entity.setValidTo(parseDateTime(row.getValidTo()));
|
||||||
|
entity.setStatus(status);
|
||||||
|
entity.setDeleted(Boolean.FALSE);
|
||||||
|
entity.setCreatedBy(operatorId);
|
||||||
|
entity.setUpdatedBy(operatorId);
|
||||||
|
toInsert.add(entity);
|
||||||
|
}
|
||||||
|
if (CollUtil.isEmpty(toInsert)) {
|
||||||
|
throw new CustomException("有效导入数据为空");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<BlindBoxPoolEntity> deleteWrapper = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
|
||||||
|
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
|
||||||
|
.eq(BlindBoxPoolEntity::getBlindBoxId, blindBoxId);
|
||||||
|
blindBoxPoolMapper.delete(deleteWrapper);
|
||||||
|
for (BlindBoxPoolEntity entity : toInsert) {
|
||||||
|
blindBoxPoolMapper.insert(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void removeById(Long id) {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
LambdaQueryWrapper<BlindBoxPoolEntity> wrapper = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
|
||||||
|
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
|
||||||
|
.eq(BlindBoxPoolEntity::getId, id);
|
||||||
|
int deleted = blindBoxPoolMapper.delete(wrapper);
|
||||||
|
if (deleted == 0) {
|
||||||
|
throw new CustomException("记录不存在或已删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlindBoxPoolView toView(BlindBoxPoolEntity entity, BlindBoxConfigEntity config,
|
||||||
|
PlayGiftInfoEntity rewardGift) {
|
||||||
|
BlindBoxPoolView view = new BlindBoxPoolView();
|
||||||
|
view.setId(entity.getId());
|
||||||
|
view.setBlindBoxId(entity.getBlindBoxId());
|
||||||
|
view.setBlindBoxName(config.getName());
|
||||||
|
view.setRewardGiftId(entity.getRewardGiftId());
|
||||||
|
view.setRewardGiftName(rewardGift != null ? rewardGift.getName() : entity.getRewardGiftId());
|
||||||
|
view.setRewardPrice(entity.getRewardPrice());
|
||||||
|
view.setWeight(entity.getWeight());
|
||||||
|
view.setRemainingStock(entity.getRemainingStock());
|
||||||
|
view.setValidFrom(entity.getValidFrom());
|
||||||
|
view.setValidTo(entity.getValidTo());
|
||||||
|
view.setStatus(entity.getStatus());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime parseDateTime(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
|
||||||
|
} catch (DateTimeParseException ex) {
|
||||||
|
throw new CustomException("日期格式应为 yyyy-MM-dd HH:mm:ss: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal resolveRewardPrice(BigDecimal overrideRewardPrice, BigDecimal defaultPrice) {
|
||||||
|
return overrideRewardPrice != null ? overrideRewardPrice : defaultPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireTenantId() {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
if (StrUtil.isBlank(tenantId)) {
|
||||||
|
throw new CustomException("租户信息缺失");
|
||||||
|
}
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId) {
|
||||||
|
if (StrUtil.isBlank(rewardGiftId)) {
|
||||||
|
throw new CustomException("请选择中奖礼物");
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
|
||||||
|
if (gift == null
|
||||||
|
|| !tenantId.equals(gift.getTenantId())
|
||||||
|
|| !GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|
||||||
|
|| !GiftState.ACTIVE.getCode().equals(gift.getState())
|
||||||
|
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|
||||||
|
|| Boolean.TRUE.equals(gift.getDeleted())) {
|
||||||
|
throw new CustomException("中奖礼物不存在或已下架");
|
||||||
|
}
|
||||||
|
return gift;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requirePositiveWeight(Integer weight, String giftName) {
|
||||||
|
if (weight == null || weight <= 0) {
|
||||||
|
String name = StrUtil.isBlank(giftName) ? "" : giftName;
|
||||||
|
throw new CustomException(StrUtil.isBlank(name)
|
||||||
|
? "奖池权重必须为正整数"
|
||||||
|
: "礼物 " + name + " 权重必须为正整数");
|
||||||
|
}
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer normalizeRemainingStock(Integer remainingStock, String giftName) {
|
||||||
|
if (remainingStock == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (remainingStock < 0) {
|
||||||
|
String name = StrUtil.isBlank(giftName) ? "" : giftName;
|
||||||
|
throw new CustomException(StrUtil.isBlank(name)
|
||||||
|
? "库存不能为负数"
|
||||||
|
: "礼物 " + name + " 库存不能为负数");
|
||||||
|
}
|
||||||
|
return remainingStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer resolveStatus(Integer status) {
|
||||||
|
if (status == null) {
|
||||||
|
return BlindBoxPoolStatus.ENABLED.getCode();
|
||||||
|
}
|
||||||
|
if (status.equals(BlindBoxPoolStatus.ENABLED.getCode())
|
||||||
|
|| status.equals(BlindBoxPoolStatus.DISABLED.getCode())) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
throw new CustomException("状态参数非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateTimeRange(LocalDateTime validFrom, LocalDateTime validTo) {
|
||||||
|
if (validFrom != null && validTo != null && validFrom.isAfter(validTo)) {
|
||||||
|
throw new CustomException("生效时间不能晚于失效时间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentUserIdSafely() {
|
||||||
|
try {
|
||||||
|
return SecurityUtils.getUserId();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
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.BlindBoxRewardStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BlindBoxService {
|
||||||
|
|
||||||
|
private final BlindBoxPoolMapper poolMapper;
|
||||||
|
private final BlindBoxRewardMapper rewardMapper;
|
||||||
|
private final BlindBoxInventoryService inventoryService;
|
||||||
|
private final BlindBoxDispatchService dispatchService;
|
||||||
|
private final BlindBoxConfigService configService;
|
||||||
|
private final Clock clock;
|
||||||
|
private final Duration rewardValidity;
|
||||||
|
private final RandomAdapter randomAdapter;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public BlindBoxService(
|
||||||
|
BlindBoxPoolMapper poolMapper,
|
||||||
|
BlindBoxRewardMapper rewardMapper,
|
||||||
|
BlindBoxInventoryService inventoryService,
|
||||||
|
BlindBoxDispatchService dispatchService,
|
||||||
|
BlindBoxConfigService configService) {
|
||||||
|
this(poolMapper, rewardMapper, inventoryService, dispatchService, configService, Clock.systemDefaultZone(), Duration.ofDays(30), new DefaultRandomAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlindBoxService(
|
||||||
|
BlindBoxPoolMapper poolMapper,
|
||||||
|
BlindBoxRewardMapper rewardMapper,
|
||||||
|
BlindBoxInventoryService inventoryService,
|
||||||
|
BlindBoxDispatchService dispatchService,
|
||||||
|
BlindBoxConfigService configService,
|
||||||
|
Clock clock,
|
||||||
|
Duration rewardValidity,
|
||||||
|
RandomAdapter randomAdapter) {
|
||||||
|
this.poolMapper = Objects.requireNonNull(poolMapper, "poolMapper");
|
||||||
|
this.rewardMapper = Objects.requireNonNull(rewardMapper, "rewardMapper");
|
||||||
|
this.inventoryService = Objects.requireNonNull(inventoryService, "inventoryService");
|
||||||
|
this.dispatchService = Objects.requireNonNull(dispatchService, "dispatchService");
|
||||||
|
this.configService = Objects.requireNonNull(configService, "configService");
|
||||||
|
this.clock = Objects.requireNonNull(clock, "clock");
|
||||||
|
this.rewardValidity = Objects.requireNonNull(rewardValidity, "rewardValidity");
|
||||||
|
this.randomAdapter = Objects.requireNonNull(randomAdapter, "randomAdapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public BlindBoxRewardEntity drawReward(
|
||||||
|
String tenantId,
|
||||||
|
String orderId,
|
||||||
|
String customerId,
|
||||||
|
String blindBoxId,
|
||||||
|
String seed) {
|
||||||
|
LocalDateTime now = LocalDateTime.now(clock);
|
||||||
|
BlindBoxConfigEntity config = configService.requireById(blindBoxId);
|
||||||
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
|
throw new CustomException("盲盒不存在或已下架");
|
||||||
|
}
|
||||||
|
List<BlindBoxCandidate> candidates = poolMapper.listActiveEntries(tenantId, blindBoxId, now);
|
||||||
|
if (CollectionUtils.isEmpty(candidates)) {
|
||||||
|
throw new CustomException("盲盒奖池暂无可用奖励");
|
||||||
|
}
|
||||||
|
|
||||||
|
BlindBoxCandidate selected = selectCandidate(candidates);
|
||||||
|
inventoryService.reserveRewardStock(tenantId, selected.getPoolId(), selected.getRewardGiftId());
|
||||||
|
|
||||||
|
BlindBoxRewardEntity entity = new BlindBoxRewardEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setCustomerId(customerId);
|
||||||
|
entity.setBlindBoxId(blindBoxId);
|
||||||
|
entity.setRewardGiftId(selected.getRewardGiftId());
|
||||||
|
entity.setRewardPrice(normalizeMoney(selected.getRewardPrice()));
|
||||||
|
entity.setBoxPrice(normalizeMoney(config.getPrice()));
|
||||||
|
entity.setSubsidyAmount(entity.getRewardPrice().subtract(entity.getBoxPrice()));
|
||||||
|
Integer remainingStock = selected.getRemainingStock();
|
||||||
|
entity.setRewardStockSnapshot(remainingStock == null ? null : Math.max(remainingStock - 1, 0));
|
||||||
|
entity.setSeed(seed);
|
||||||
|
entity.setStatus(BlindBoxRewardStatus.UNUSED.getCode());
|
||||||
|
entity.setCreatedByOrder(orderId);
|
||||||
|
entity.setExpiresAt(now.plus(rewardValidity));
|
||||||
|
entity.setCreatedTime(java.sql.Timestamp.valueOf(now));
|
||||||
|
entity.setUpdatedTime(java.sql.Timestamp.valueOf(now));
|
||||||
|
|
||||||
|
rewardMapper.insert(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
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("盲盒奖励已使用");
|
||||||
|
}
|
||||||
|
if (reward.getExpiresAt() != null && reward.getExpiresAt().isBefore(now)) {
|
||||||
|
throw new CustomException("盲盒奖励已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderPlacementResult result = dispatchService.dispatchRewardOrder(reward, clerkId);
|
||||||
|
String orderId = result != null && result.getOrder() != null ? result.getOrder().getId() : null;
|
||||||
|
int affected = rewardMapper.markUsed(rewardId, clerkId, orderId, now);
|
||||||
|
if (affected <= 0) {
|
||||||
|
throw new CustomException("盲盒奖励已使用");
|
||||||
|
}
|
||||||
|
reward.setStatus(BlindBoxRewardStatus.USED.getCode());
|
||||||
|
reward.setUsedClerkId(clerkId);
|
||||||
|
reward.setUsedOrderId(orderId);
|
||||||
|
reward.setUsedTime(now);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlindBoxCandidate selectCandidate(List<BlindBoxCandidate> candidates) {
|
||||||
|
int totalWeight = candidates.stream()
|
||||||
|
.mapToInt(BlindBoxCandidate::getWeight)
|
||||||
|
.sum();
|
||||||
|
if (totalWeight <= 0) {
|
||||||
|
throw new CustomException("盲盒奖池权重配置异常");
|
||||||
|
}
|
||||||
|
double threshold = randomAdapter.nextDouble() * totalWeight;
|
||||||
|
int cumulative = 0;
|
||||||
|
for (BlindBoxCandidate candidate : candidates) {
|
||||||
|
cumulative += Math.max(candidate.getWeight(), 0);
|
||||||
|
if (threshold < cumulative) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates.get(candidates.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal normalizeMoney(BigDecimal value) {
|
||||||
|
return value == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
|
||||||
|
: value.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<BlindBoxRewardEntity> listRewards(String tenantId, String customerId, String status) {
|
||||||
|
return rewardMapper.listByCustomer(tenantId, customerId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface RandomAdapter {
|
||||||
|
double nextDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DefaultRandomAdapter implements RandomAdapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextDouble() {
|
||||||
|
return ThreadLocalRandom.current().nextDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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 org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BlindBoxConfigServiceImpl
|
||||||
|
extends ServiceImpl<BlindBoxConfigMapper, BlindBoxConfigEntity>
|
||||||
|
implements BlindBoxConfigService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlindBoxConfigEntity requireById(String id) {
|
||||||
|
BlindBoxConfigEntity entity = getById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new CustomException("盲盒不存在");
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BlindBoxConfigEntity> listActiveByTenant(String tenantId) {
|
||||||
|
return lambdaQuery()
|
||||||
|
.eq(BlindBoxConfigEntity::getTenantId, tenantId)
|
||||||
|
.eq(BlindBoxConfigEntity::getStatus, BlindBoxConfigStatus.ENABLED.getCode())
|
||||||
|
.eq(BlindBoxConfigEntity::getDeleted, Boolean.FALSE)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,9 @@ public class OrderConstant {
|
|||||||
REFUND("-1", "退款订单"),
|
REFUND("-1", "退款订单"),
|
||||||
RECHARGE("0", "充值订单"),
|
RECHARGE("0", "充值订单"),
|
||||||
WITHDRAWAL("1", "提现订单"),
|
WITHDRAWAL("1", "提现订单"),
|
||||||
NORMAL("2", "普通订单");
|
NORMAL("2", "普通订单"),
|
||||||
|
GIFT("3", "礼物订单"),
|
||||||
|
BLIND_BOX_PURCHASE("4", "盲盒购买订单");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String description;
|
private final String description;
|
||||||
@@ -67,6 +69,31 @@ public class OrderConstant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum PaymentSource {
|
||||||
|
BALANCE("BALANCE", "余额扣款"),
|
||||||
|
WX_PAY("WX_PAY", "微信支付"),
|
||||||
|
ALI_PAY("ALI_PAY", "支付宝支付"),
|
||||||
|
BLIND_BOX("BLIND_BOX", "盲盒奖励抵扣");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
PaymentSource(String code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PaymentSource fromCode(String code) {
|
||||||
|
for (PaymentSource source : values()) {
|
||||||
|
if (source.code.equals(code)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown payment source code: " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下单类型枚举
|
* 下单类型枚举
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ public class OrderCreationContext {
|
|||||||
|
|
||||||
private boolean isFirstOrder;
|
private boolean isFirstOrder;
|
||||||
|
|
||||||
|
private OrderConstant.PaymentSource paymentSource;
|
||||||
|
|
||||||
|
private String sourceRewardId;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull(message = "商品信息不能为空")
|
@NotNull(message = "商品信息不能为空")
|
||||||
private CommodityInfo commodityInfo;
|
private CommodityInfo commodityInfo;
|
||||||
@@ -75,4 +79,13 @@ public class OrderCreationContext {
|
|||||||
public boolean isSpecifiedOrder() {
|
public boolean isSpecifiedOrder() {
|
||||||
return placeType == OrderConstant.PlaceType.SPECIFIED;
|
return placeType == OrderConstant.PlaceType.SPECIFIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OrderConstant.PaymentSource resolvePaymentSource() {
|
||||||
|
if (paymentSource != null) {
|
||||||
|
return paymentSource;
|
||||||
|
}
|
||||||
|
return paymentInfo != null && paymentInfo.getPaymentSource() != null
|
||||||
|
? paymentInfo.getPaymentSource()
|
||||||
|
: OrderConstant.PaymentSource.BALANCE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.starry.admin.modules.order.module.dto;
|
package com.starry.admin.modules.order.module.dto;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -37,4 +38,7 @@ public class PaymentInfo {
|
|||||||
* 支付方式,0:余额支付,1:微信支付,2:支付宝支付
|
* 支付方式,0:余额支付,1:微信支付,2:支付宝支付
|
||||||
*/
|
*/
|
||||||
private String payMethod;
|
private String payMethod;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private OrderConstant.PaymentSource paymentSource = OrderConstant.PaymentSource.BALANCE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,16 @@ public class PlayOrderInfoEntity extends BaseEntity<PlayOrderInfoEntity> {
|
|||||||
*/
|
*/
|
||||||
private String backendEntry;
|
private String backendEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付来源(区分余额、三方、盲盒奖励等)。
|
||||||
|
*/
|
||||||
|
private String paymentSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盲盒奖励引用ID。
|
||||||
|
*/
|
||||||
|
private String sourceRewardId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付方式,0:余额支付,1:微信支付,2:支付宝支付
|
* 支付方式,0:余额支付,1:微信支付,2:支付宝支付
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ abstract class AbstractOrderPlacementStrategy implements OrderPlacementStrategy
|
|||||||
|
|
||||||
PlayOrderInfoEntity order = service.createOrderRecord(context);
|
PlayOrderInfoEntity order = service.createOrderRecord(context);
|
||||||
|
|
||||||
if (command.isDeductBalance()) {
|
if (command.isDeductBalance() && service.shouldDeductBalance(context)) {
|
||||||
service.deductCustomerBalance(
|
service.deductCustomerBalance(
|
||||||
context.getPurchaserBy(),
|
context.getPurchaserBy(),
|
||||||
service.normalizeMoney(paymentInfo.getFinalAmount()),
|
service.normalizeMoney(paymentInfo.getFinalAmount()),
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
throw new CustomException("下单类型不能为空");
|
throw new CustomException("下单类型不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateCouponUsage(context);
|
||||||
|
|
||||||
OrderConstant.RewardType rewardType = context.getRewardType() != null
|
OrderConstant.RewardType rewardType = context.getRewardType() != null
|
||||||
? context.getRewardType()
|
? context.getRewardType()
|
||||||
: OrderConstant.RewardType.NOT_APPLICABLE;
|
: OrderConstant.RewardType.NOT_APPLICABLE;
|
||||||
@@ -206,6 +208,14 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean shouldDeductBalance(OrderCreationContext context) {
|
||||||
|
if (context == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
OrderConstant.PaymentSource paymentSource = context.resolvePaymentSource();
|
||||||
|
return paymentSource != OrderConstant.PaymentSource.BLIND_BOX;
|
||||||
|
}
|
||||||
|
|
||||||
OrderAmountBreakdown calculateOrderAmounts(OrderPlacementCommand.PricingInput pricingInput) {
|
OrderAmountBreakdown calculateOrderAmounts(OrderPlacementCommand.PricingInput pricingInput) {
|
||||||
int quantity = pricingInput.getQuantity();
|
int quantity = pricingInput.getQuantity();
|
||||||
if (quantity <= 0) {
|
if (quantity <= 0) {
|
||||||
@@ -566,6 +576,25 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateCouponUsage(OrderCreationContext context) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PaymentInfo paymentInfo = context.getPaymentInfo();
|
||||||
|
if (paymentInfo == null || CollectionUtil.isEmpty(paymentInfo.getCouponIds())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean isBlindBoxPurchase = context.getOrderType() == OrderConstant.OrderType.BLIND_BOX_PURCHASE;
|
||||||
|
boolean isBlindBoxDispatch = context.resolvePaymentSource() == OrderConstant.PaymentSource.BLIND_BOX
|
||||||
|
|| StrUtil.isNotBlank(context.getSourceRewardId());
|
||||||
|
boolean isRewardOrder = context.getPlaceType() == OrderConstant.PlaceType.REWARD
|
||||||
|
|| context.getRewardType() == OrderConstant.RewardType.GIFT
|
||||||
|
|| context.getRewardType() == OrderConstant.RewardType.BALANCE;
|
||||||
|
if (isBlindBoxPurchase || isBlindBoxDispatch || isRewardOrder) {
|
||||||
|
throw new CustomException("盲盒/礼物订单暂不支持使用优惠券");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PlayOrderInfoEntity buildOrderEntity(OrderCreationContext context) {
|
private PlayOrderInfoEntity buildOrderEntity(OrderCreationContext context) {
|
||||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||||
entity.setId(context.getOrderId());
|
entity.setId(context.getOrderId());
|
||||||
@@ -597,6 +626,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
? YesNoFlag.YES.getCode()
|
? YesNoFlag.YES.getCode()
|
||||||
: YesNoFlag.NO.getCode());
|
: YesNoFlag.NO.getCode());
|
||||||
entity.setPayMethod(resolvePayMethod(paymentInfo.getPayMethod()));
|
entity.setPayMethod(resolvePayMethod(paymentInfo.getPayMethod()));
|
||||||
|
entity.setPaymentSource(context.resolvePaymentSource().getCode());
|
||||||
|
entity.setSourceRewardId(context.getSourceRewardId());
|
||||||
|
|
||||||
entity.setPurchaserBy(context.getPurchaserBy());
|
entity.setPurchaserBy(context.getPurchaserBy());
|
||||||
entity.setPurchaserTime(LocalDateTime.now());
|
entity.setPurchaserTime(LocalDateTime.now());
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class OtherOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
|
|||||||
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
||||||
.couponIds(info.getCouponIds())
|
.couponIds(info.getCouponIds())
|
||||||
.payMethod(info.getPayMethod())
|
.payMethod(info.getPayMethod())
|
||||||
|
.paymentSource(info.getPaymentSource())
|
||||||
.build());
|
.build());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
|||||||
import com.starry.admin.modules.weichat.entity.order.*;
|
import com.starry.admin.modules.weichat.entity.order.*;
|
||||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.utils.ConvertUtil;
|
import com.starry.common.utils.ConvertUtil;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
@@ -456,8 +457,17 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
public List<PlayOrderInfoEntity> clerkSelectOrderInfoList(String clerkId, String startTime, String endTime) {
|
public List<PlayOrderInfoEntity> clerkSelectOrderInfoList(String clerkId, String startTime, String endTime) {
|
||||||
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getAcceptBy, clerkId);
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getAcceptBy, clerkId);
|
||||||
if (StringUtils.isNotBlank(startTime) && StringUtils.isNotBlank(endTime)) {
|
String normalizedStart = DateRangeUtils.normalizeStartOptional(startTime);
|
||||||
lambdaQueryWrapper.between(PlayOrderInfoEntity::getPurchaserTime, startTime, endTime);
|
String normalizedEnd = DateRangeUtils.normalizeEndOptional(endTime);
|
||||||
|
if (StrUtil.isNotBlank(normalizedStart) && StrUtil.isNotBlank(normalizedEnd)) {
|
||||||
|
lambdaQueryWrapper.between(PlayOrderInfoEntity::getPurchaserTime, normalizedStart, normalizedEnd);
|
||||||
|
} else {
|
||||||
|
if (StrUtil.isNotBlank(normalizedStart)) {
|
||||||
|
lambdaQueryWrapper.ge(PlayOrderInfoEntity::getPurchaserTime, normalizedStart);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(normalizedEnd)) {
|
||||||
|
lambdaQueryWrapper.le(PlayOrderInfoEntity::getPurchaserTime, normalizedEnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.baseMapper.selectList(lambdaQueryWrapper);
|
return this.baseMapper.selectList(lambdaQueryWrapper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class RewardGiftOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
|
|||||||
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
||||||
.couponIds(info.getCouponIds())
|
.couponIds(info.getCouponIds())
|
||||||
.payMethod(info.getPayMethod())
|
.payMethod(info.getPayMethod())
|
||||||
|
.paymentSource(info.getPaymentSource())
|
||||||
.build();
|
.build();
|
||||||
context.setPaymentInfo(normalized);
|
context.setPaymentInfo(normalized);
|
||||||
return normalized;
|
return normalized;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class RewardTipOrderPlacementStrategy extends AbstractOrderPlacementStrategy {
|
|||||||
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
.finalAmount(service.normalizeMoney(info.getFinalAmount()))
|
||||||
.couponIds(info.getCouponIds())
|
.couponIds(info.getCouponIds())
|
||||||
.payMethod(info.getPayMethod())
|
.payMethod(info.getPayMethod())
|
||||||
|
.paymentSource(info.getPaymentSource())
|
||||||
.build();
|
.build();
|
||||||
context.setPaymentInfo(normalized);
|
context.setPaymentInfo(normalized);
|
||||||
return normalized;
|
return normalized;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否历史礼物标记。
|
||||||
|
*/
|
||||||
|
public enum GiftHistory {
|
||||||
|
CURRENT("0"),
|
||||||
|
HISTORY("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
GiftHistory(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 礼物上下架状态。
|
||||||
|
*/
|
||||||
|
public enum GiftState {
|
||||||
|
ACTIVE("0"),
|
||||||
|
OFF_SHELF("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
GiftState(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.starry.admin.modules.shop.module.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 礼物类型(字段已标注废弃,但仍有遗留使用)。
|
||||||
|
*/
|
||||||
|
public enum GiftType {
|
||||||
|
BLIND_BOX("0"),
|
||||||
|
NORMAL("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
GiftType(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,10 @@ public class PlayGiftInfoEntity extends BaseEntity<PlayGiftInfoEntity> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 礼物类型(0:盲盒,1:普通礼物)
|
* 礼物类型(0:盲盒,1:普通礼物)
|
||||||
|
*
|
||||||
|
* @deprecated 盲盒已迁移至 blind_box_config,请勿再依赖该字段判断
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewRes
|
|||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
||||||
import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService;
|
import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import com.starry.common.result.R;
|
import com.starry.common.result.R;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -69,19 +70,27 @@ public class PlayClerkPerformanceController {
|
|||||||
public R listByPage(
|
public R listByPage(
|
||||||
@ApiParam(value = "查询条件", required = true) @Validated @RequestBody PlayClerkPerformanceInfoQueryVo vo) {
|
@ApiParam(value = "查询条件", required = true) @Validated @RequestBody PlayClerkPerformanceInfoQueryVo vo) {
|
||||||
IPage<PlayClerkUserInfoEntity> page = clerkUserInfoService.selectByPage(vo);
|
IPage<PlayClerkUserInfoEntity> page = clerkUserInfoService.selectByPage(vo);
|
||||||
IPage<PlayClerkPerformanceInfoReturnVo> voPage = page.convert(u -> {
|
IPage<PlayClerkPerformanceInfoReturnVo> voPage = page.convert(user -> {
|
||||||
List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity = playClerkLevelInfoService.selectAll();
|
List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity = playClerkLevelInfoService.selectAll();
|
||||||
String startTime = vo.getEndOrderTime() != null ? vo.getEndOrderTime().get(0) : "";
|
String rawStart = vo.getEndOrderTime() != null && vo.getEndOrderTime().size() > 0
|
||||||
String endTime = vo.getEndOrderTime() != null ? vo.getEndOrderTime().get(1) : "";
|
? vo.getEndOrderTime().get(0)
|
||||||
List<PlayOrderInfoEntity> orderInfoEntities = playOrderInfoService.clerkSelectOrderInfoList(u.getId(),
|
: null;
|
||||||
|
String rawEnd = vo.getEndOrderTime() != null && vo.getEndOrderTime().size() > 1
|
||||||
|
? vo.getEndOrderTime().get(1)
|
||||||
|
: null;
|
||||||
|
String startTime = DateRangeUtils.normalizeStartOptional(rawStart);
|
||||||
|
String endTime = DateRangeUtils.normalizeEndOptional(rawEnd);
|
||||||
|
List<PlayOrderInfoEntity> orderInfoEntities = playOrderInfoService.clerkSelectOrderInfoList(user.getId(),
|
||||||
startTime, endTime);
|
startTime, endTime);
|
||||||
List<PlayPersonnelGroupInfoEntity> groupInfoEntities = playPersonnelGroupInfoService.selectAll();
|
List<PlayPersonnelGroupInfoEntity> groupInfoEntities = playPersonnelGroupInfoService.selectAll();
|
||||||
return playClerkPerformanceService.getClerkPerformanceInfo(u, orderInfoEntities, clerkLevelInfoEntity,
|
|
||||||
groupInfoEntities);
|
return playClerkPerformanceService.getClerkPerformanceInfo(user, orderInfoEntities, clerkLevelInfoEntity,
|
||||||
|
groupInfoEntities, startTime, endTime);
|
||||||
});
|
});
|
||||||
voPage.setRecords(voPage.getRecords().stream()
|
voPage.setRecords(voPage.getRecords().stream()
|
||||||
.sorted(Comparator.comparing(PlayClerkPerformanceInfoReturnVo::getOrderNumber).reversed())
|
.sorted(Comparator.comparing(PlayClerkPerformanceInfoReturnVo::getOrderNumber).reversed())
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
return R.ok(voPage);
|
return R.ok(voPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public interface IPlayClerkPerformanceService {
|
|||||||
*/
|
*/
|
||||||
PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo,
|
PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo,
|
||||||
List<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity,
|
List<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity,
|
||||||
List<PlayPersonnelGroupInfoEntity> groupInfoEntities);
|
List<PlayPersonnelGroupInfoEntity> groupInfoEntities, String startTime, String endTime);
|
||||||
|
|
||||||
ClerkPerformanceOverviewResponseVo queryOverview(ClerkPerformanceOverviewQueryVo vo);
|
ClerkPerformanceOverviewResponseVo queryOverview(ClerkPerformanceOverviewQueryVo vo);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
|||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
|
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
|
||||||
@@ -26,12 +27,13 @@ import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
|
|||||||
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceTrendPointVo;
|
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceTrendPointVo;
|
||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
||||||
import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService;
|
import com.starry.admin.modules.statistics.service.IPlayClerkPerformanceService;
|
||||||
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -51,9 +53,6 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceService {
|
public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceService {
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IPlayClerkUserInfoService clerkUserInfoService;
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
@@ -66,10 +65,14 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
|
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private EarningsLineMapper earningsLineMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo,
|
public PlayClerkPerformanceInfoReturnVo getClerkPerformanceInfo(PlayClerkUserInfoEntity userInfo,
|
||||||
List<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntities,
|
List<PlayOrderInfoEntity> orderInfoEntities, List<PlayClerkLevelInfoEntity> clerkLevelInfoEntities,
|
||||||
List<PlayPersonnelGroupInfoEntity> groupInfoEntities) {
|
List<PlayPersonnelGroupInfoEntity> groupInfoEntities, String startTime, String endTime) {
|
||||||
|
|
||||||
Set<String> customIds = new HashSet<>();
|
Set<String> customIds = new HashSet<>();
|
||||||
int orderContinueNumber = 0;
|
int orderContinueNumber = 0;
|
||||||
int orderRefundNumber = 0;
|
int orderRefundNumber = 0;
|
||||||
@@ -79,28 +82,31 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
BigDecimal orderTotalAmount = BigDecimal.ZERO;
|
BigDecimal orderTotalAmount = BigDecimal.ZERO;
|
||||||
BigDecimal orderRewardAmount = BigDecimal.ZERO;
|
BigDecimal orderRewardAmount = BigDecimal.ZERO;
|
||||||
BigDecimal orderRefundAmount = BigDecimal.ZERO;
|
BigDecimal orderRefundAmount = BigDecimal.ZERO;
|
||||||
BigDecimal estimatedRevenue = BigDecimal.ZERO;
|
|
||||||
for (PlayOrderInfoEntity orderInfoEntity : orderInfoEntities) {
|
for (PlayOrderInfoEntity orderInfoEntity : orderInfoEntities) {
|
||||||
customIds.add(orderInfoEntity.getPurchaserBy());
|
customIds.add(orderInfoEntity.getPurchaserBy());
|
||||||
finalAmount = finalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
finalAmount = finalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
||||||
if ("1".equals(orderInfoEntity.getFirstOrder())) {
|
if (OrderConstant.YesNoFlag.YES.getCode().equals(orderInfoEntity.getFirstOrder())) {
|
||||||
orderFirstAmount = orderFirstAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
orderFirstAmount = orderFirstAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
||||||
} else {
|
} else {
|
||||||
orderContinueNumber++;
|
orderContinueNumber++;
|
||||||
orderTotalAmount = orderTotalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
orderTotalAmount = orderTotalAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
||||||
}
|
}
|
||||||
if ("2".equals(orderInfoEntity.getPlaceType())) {
|
if (OrderConstant.PlaceType.REWARD.getCode().equals(orderInfoEntity.getPlaceType())) {
|
||||||
orderRewardAmount = orderRewardAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
orderRewardAmount = orderRewardAmount.add(defaultZero(orderInfoEntity.getFinalAmount()));
|
||||||
}
|
}
|
||||||
if ("1".equals(orderInfoEntity.getRefundType())) {
|
if (OrderConstant.OrderRefundFlag.REFUNDED.getCode().equals(orderInfoEntity.getRefundType())) {
|
||||||
orderRefundNumber++;
|
orderRefundNumber++;
|
||||||
orderRefundAmount = orderRefundAmount.add(defaultZero(orderInfoEntity.getRefundAmount()));
|
orderRefundAmount = orderRefundAmount.add(defaultZero(orderInfoEntity.getRefundAmount()));
|
||||||
}
|
}
|
||||||
if ("1".equals(orderInfoEntity.getOrdersExpiredState())) {
|
if (OrderConstant.OrdersExpiredState.EXPIRED.getCode().equals(orderInfoEntity.getOrdersExpiredState())) {
|
||||||
ordersExpiredNumber++;
|
ordersExpiredNumber++;
|
||||||
}
|
}
|
||||||
estimatedRevenue = estimatedRevenue.add(defaultZero(orderInfoEntity.getEstimatedRevenue()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BigDecimal estimatedRevenue =
|
||||||
|
calculateEarningsAmount(userInfo.getId(), orderInfoEntities, startTime, endTime);
|
||||||
|
|
||||||
PlayClerkPerformanceInfoReturnVo returnVo = new PlayClerkPerformanceInfoReturnVo();
|
PlayClerkPerformanceInfoReturnVo returnVo = new PlayClerkPerformanceInfoReturnVo();
|
||||||
returnVo.setClerkId(userInfo.getId());
|
returnVo.setClerkId(userInfo.getId());
|
||||||
returnVo.setClerkNickname(userInfo.getNickname());
|
returnVo.setClerkNickname(userInfo.getNickname());
|
||||||
@@ -149,7 +155,7 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
for (PlayClerkUserInfoEntity clerk : clerks) {
|
for (PlayClerkUserInfoEntity clerk : clerks) {
|
||||||
List<PlayOrderInfoEntity> orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(),
|
List<PlayOrderInfoEntity> orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(),
|
||||||
range.startTime, range.endTime);
|
range.startTime, range.endTime);
|
||||||
snapshots.add(buildSnapshot(clerk, orders, levelNameMap, groupNameMap));
|
snapshots.add(buildSnapshot(clerk, orders, levelNameMap, groupNameMap, range.startTime, range.endTime));
|
||||||
}
|
}
|
||||||
int total = snapshots.size();
|
int total = snapshots.size();
|
||||||
ClerkPerformanceOverviewSummaryVo summary = aggregateSummary(snapshots);
|
ClerkPerformanceOverviewSummaryVo summary = aggregateSummary(snapshots);
|
||||||
@@ -184,7 +190,8 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
(a, b) -> a));
|
(a, b) -> a));
|
||||||
List<PlayOrderInfoEntity> orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(),
|
List<PlayOrderInfoEntity> orders = playOrderInfoService.clerkSelectOrderInfoList(clerk.getId(),
|
||||||
range.startTime, range.endTime);
|
range.startTime, range.endTime);
|
||||||
ClerkPerformanceSnapshotVo snapshot = buildSnapshot(clerk, orders, levelNameMap, groupNameMap);
|
ClerkPerformanceSnapshotVo snapshot =
|
||||||
|
buildSnapshot(clerk, orders, levelNameMap, groupNameMap, range.startTime, range.endTime);
|
||||||
ClerkPerformanceDetailResponseVo responseVo = new ClerkPerformanceDetailResponseVo();
|
ClerkPerformanceDetailResponseVo responseVo = new ClerkPerformanceDetailResponseVo();
|
||||||
responseVo.setProfile(buildProfile(clerk, levelNameMap, groupNameMap));
|
responseVo.setProfile(buildProfile(clerk, levelNameMap, groupNameMap));
|
||||||
responseVo.setSnapshot(snapshot);
|
responseVo.setSnapshot(snapshot);
|
||||||
@@ -262,7 +269,7 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
for (PlayOrderInfoEntity order : orders) {
|
for (PlayOrderInfoEntity order : orders) {
|
||||||
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
|
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
|
||||||
gmv = gmv.add(finalAmount);
|
gmv = gmv.add(finalAmount);
|
||||||
if ("1".equals(order.getFirstOrder())) {
|
if (OrderConstant.YesNoFlag.YES.getCode().equals(order.getFirstOrder())) {
|
||||||
firstAmount = firstAmount.add(finalAmount);
|
firstAmount = firstAmount.add(finalAmount);
|
||||||
} else {
|
} else {
|
||||||
continuedAmount = continuedAmount.add(finalAmount);
|
continuedAmount = continuedAmount.add(finalAmount);
|
||||||
@@ -408,8 +415,27 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateEarningsAmount(String clerkId, List<PlayOrderInfoEntity> orders, String startTime,
|
||||||
|
String endTime) {
|
||||||
|
if (StrUtil.isBlank(clerkId) || CollectionUtil.isEmpty(orders)) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
List<String> orderIds = orders.stream()
|
||||||
|
.map(PlayOrderInfoEntity::getId)
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (CollectionUtil.isEmpty(orderIds)) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
String normalizedStart = DateRangeUtils.normalizeStartOptional(startTime);
|
||||||
|
String normalizedEnd = DateRangeUtils.normalizeEndOptional(endTime);
|
||||||
|
BigDecimal sum = earningsLineMapper.sumAmountByClerkAndOrderIds(clerkId, orderIds, normalizedStart,
|
||||||
|
normalizedEnd);
|
||||||
|
return defaultZero(sum);
|
||||||
|
}
|
||||||
|
|
||||||
private ClerkPerformanceSnapshotVo buildSnapshot(PlayClerkUserInfoEntity clerk, List<PlayOrderInfoEntity> orders,
|
private ClerkPerformanceSnapshotVo buildSnapshot(PlayClerkUserInfoEntity clerk, List<PlayOrderInfoEntity> orders,
|
||||||
Map<String, String> levelNameMap, Map<String, String> groupNameMap) {
|
Map<String, String> levelNameMap, Map<String, String> groupNameMap, String startTime, String endTime) {
|
||||||
ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo();
|
ClerkPerformanceSnapshotVo snapshot = new ClerkPerformanceSnapshotVo();
|
||||||
snapshot.setClerkId(clerk.getId());
|
snapshot.setClerkId(clerk.getId());
|
||||||
snapshot.setClerkNickname(clerk.getNickname());
|
snapshot.setClerkNickname(clerk.getNickname());
|
||||||
@@ -422,7 +448,6 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
BigDecimal continuedAmount = BigDecimal.ZERO;
|
BigDecimal continuedAmount = BigDecimal.ZERO;
|
||||||
BigDecimal rewardAmount = BigDecimal.ZERO;
|
BigDecimal rewardAmount = BigDecimal.ZERO;
|
||||||
BigDecimal refundAmount = BigDecimal.ZERO;
|
BigDecimal refundAmount = BigDecimal.ZERO;
|
||||||
BigDecimal estimatedRevenue = BigDecimal.ZERO;
|
|
||||||
int firstCount = 0;
|
int firstCount = 0;
|
||||||
int continuedCount = 0;
|
int continuedCount = 0;
|
||||||
int refundCount = 0;
|
int refundCount = 0;
|
||||||
@@ -432,28 +457,28 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
|
BigDecimal finalAmount = defaultZero(order.getFinalAmount());
|
||||||
gmv = gmv.add(finalAmount);
|
gmv = gmv.add(finalAmount);
|
||||||
userOrderMap.merge(order.getPurchaserBy(), 1, Integer::sum);
|
userOrderMap.merge(order.getPurchaserBy(), 1, Integer::sum);
|
||||||
if ("1".equals(order.getFirstOrder())) {
|
if (OrderConstant.YesNoFlag.YES.getCode().equals(order.getFirstOrder())) {
|
||||||
firstCount++;
|
firstCount++;
|
||||||
firstAmount = firstAmount.add(finalAmount);
|
firstAmount = firstAmount.add(finalAmount);
|
||||||
} else {
|
} else {
|
||||||
continuedCount++;
|
continuedCount++;
|
||||||
continuedAmount = continuedAmount.add(finalAmount);
|
continuedAmount = continuedAmount.add(finalAmount);
|
||||||
}
|
}
|
||||||
if ("2".equals(order.getPlaceType())) {
|
if (OrderConstant.PlaceType.REWARD.getCode().equals(order.getPlaceType())) {
|
||||||
rewardAmount = rewardAmount.add(finalAmount);
|
rewardAmount = rewardAmount.add(finalAmount);
|
||||||
}
|
}
|
||||||
if ("1".equals(order.getRefundType())) {
|
if (OrderConstant.OrderRefundFlag.REFUNDED.getCode().equals(order.getRefundType())) {
|
||||||
refundCount++;
|
refundCount++;
|
||||||
refundAmount = refundAmount.add(defaultZero(order.getRefundAmount()));
|
refundAmount = refundAmount.add(defaultZero(order.getRefundAmount()));
|
||||||
}
|
}
|
||||||
if ("1".equals(order.getOrdersExpiredState())) {
|
if (OrderConstant.OrdersExpiredState.EXPIRED.getCode().equals(order.getOrdersExpiredState())) {
|
||||||
expiredCount++;
|
expiredCount++;
|
||||||
}
|
}
|
||||||
estimatedRevenue = estimatedRevenue.add(defaultZero(order.getEstimatedRevenue()));
|
|
||||||
}
|
}
|
||||||
int orderCount = orders.size();
|
int orderCount = orders.size();
|
||||||
int userCount = userOrderMap.size();
|
int userCount = userOrderMap.size();
|
||||||
int continuedUserCount = (int) userOrderMap.values().stream().filter(cnt -> cnt > 1).count();
|
int continuedUserCount = (int) userOrderMap.values().stream().filter(cnt -> cnt > 1).count();
|
||||||
|
BigDecimal estimatedRevenue = calculateEarningsAmount(clerk.getId(), orders, startTime, endTime);
|
||||||
snapshot.setGmv(gmv);
|
snapshot.setGmv(gmv);
|
||||||
snapshot.setFirstOrderAmount(firstAmount);
|
snapshot.setFirstOrderAmount(firstAmount);
|
||||||
snapshot.setContinuedOrderAmount(continuedAmount);
|
snapshot.setContinuedOrderAmount(continuedAmount);
|
||||||
@@ -504,41 +529,19 @@ public class PlayClerkPerformanceServiceImpl implements IPlayClerkPerformanceSer
|
|||||||
String startStr;
|
String startStr;
|
||||||
String endStr;
|
String endStr;
|
||||||
if (CollectionUtil.isNotEmpty(endOrderTime) && endOrderTime.size() >= 2) {
|
if (CollectionUtil.isNotEmpty(endOrderTime) && endOrderTime.size() >= 2) {
|
||||||
startStr = normalizeStart(endOrderTime.get(0));
|
startStr = DateRangeUtils.normalizeStart(endOrderTime.get(0));
|
||||||
endStr = normalizeEnd(endOrderTime.get(1));
|
endStr = DateRangeUtils.normalizeEnd(endOrderTime.get(1));
|
||||||
} else {
|
} else {
|
||||||
LocalDate end = LocalDate.now();
|
LocalDate end = LocalDate.now();
|
||||||
LocalDate start = end.minusDays(6);
|
LocalDate start = end.minusDays(6);
|
||||||
startStr = start.format(DATE_FORMATTER) + " 00:00:00";
|
startStr = start.format(DateRangeUtils.DATE_FORMATTER) + " 00:00:00";
|
||||||
endStr = end.format(DATE_FORMATTER) + " 23:59:59";
|
endStr = end.format(DateRangeUtils.DATE_FORMATTER) + " 23:59:59";
|
||||||
}
|
}
|
||||||
LocalDate startDate = LocalDate.parse(startStr.substring(0, 10), DATE_FORMATTER);
|
LocalDate startDate = LocalDate.parse(startStr.substring(0, 10), DateRangeUtils.DATE_FORMATTER);
|
||||||
LocalDate endDate = LocalDate.parse(endStr.substring(0, 10), DATE_FORMATTER);
|
LocalDate endDate = LocalDate.parse(endStr.substring(0, 10), DateRangeUtils.DATE_FORMATTER);
|
||||||
return new DateRange(startStr, endStr, startDate, endDate);
|
return new DateRange(startStr, endStr, startDate, endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeStart(String raw) {
|
|
||||||
if (StrUtil.isBlank(raw)) {
|
|
||||||
return LocalDate.now().minusDays(6).format(DATE_FORMATTER) + " 00:00:00";
|
|
||||||
}
|
|
||||||
if (raw.length() > 10) {
|
|
||||||
LocalDateTime.parse(raw, DATE_TIME_FORMATTER);
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
return raw + " 00:00:00";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeEnd(String raw) {
|
|
||||||
if (StrUtil.isBlank(raw)) {
|
|
||||||
return LocalDate.now().format(DATE_FORMATTER) + " 23:59:59";
|
|
||||||
}
|
|
||||||
if (raw.length() > 10) {
|
|
||||||
LocalDateTime.parse(raw, DATE_TIME_FORMATTER);
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
return raw + " 23:59:59";
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal calcPercentage(int numerator, int denominator) {
|
private BigDecimal calcPercentage(int numerator, int denominator) {
|
||||||
if (denominator <= 0) {
|
if (denominator <= 0) {
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
|
|||||||
@@ -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<BlindBoxConfigEntity> configs = blindBoxConfigService.listActiveByTenant(user.getTenantId());
|
||||||
|
List<BlindBoxConfigView> 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<BlindBoxRewardView> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ public class WxClerkCommodityController {
|
|||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@ApiResponse(code = 200, message = "操作成功", response = PlayCommodityReturnVo.class, responseContainer = "List")})
|
@ApiResponse(code = 200, message = "操作成功", response = PlayCommodityReturnVo.class, responseContainer = "List")})
|
||||||
@GetMapping("/custom/queryClerkAllCommodityByLevel")
|
@GetMapping("/custom/queryClerkAllCommodityByLevel")
|
||||||
public R queryClerkAllCommodityByLevel(@RequestParam("id") String levelId) {
|
public R queryClerkAllCommodityByLevel(@RequestParam(value = "id", required = false) String levelId) {
|
||||||
List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll();
|
List<PlayCommodityAndLevelInfoEntity> levelInfoEntities = iPlayCommodityAndLevelInfoService.selectAll();
|
||||||
List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree();
|
List<PlayCommodityReturnVo> tree = playCommodityInfoService.selectTree();
|
||||||
if (levelId == null || levelId.isEmpty()) {
|
if (levelId == null || levelId.isEmpty()) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.starry.admin.modules.weichat.entity.order.PlayClerkOrderInfoQueryVo;
|
|||||||
import com.starry.admin.modules.weichat.entity.order.PlayClerkOrderListReturnVo;
|
import com.starry.admin.modules.weichat.entity.order.PlayClerkOrderListReturnVo;
|
||||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
import com.starry.admin.modules.weichat.service.WxCustomUserService;
|
import com.starry.admin.modules.weichat.service.WxCustomUserService;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.admin.utils.SmsUtils;
|
import com.starry.admin.utils.SmsUtils;
|
||||||
import com.starry.common.redis.RedisCache;
|
import com.starry.common.redis.RedisCache;
|
||||||
@@ -126,11 +127,13 @@ public class WxClerkController {
|
|||||||
PlayClerkUserInfoEntity entity = clerkUserInfoService
|
PlayClerkUserInfoEntity entity = clerkUserInfoService
|
||||||
.selectById(ThreadLocalRequestDetail.getClerkUserInfo().getId());
|
.selectById(ThreadLocalRequestDetail.getClerkUserInfo().getId());
|
||||||
List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity = playClerkLevelInfoService.selectAll();
|
List<PlayClerkLevelInfoEntity> clerkLevelInfoEntity = playClerkLevelInfoService.selectAll();
|
||||||
|
String startTime = DateRangeUtils.normalizeStartOptional(vo.getStartTime());
|
||||||
|
String endTime = DateRangeUtils.normalizeEndOptional(vo.getEndTime());
|
||||||
List<PlayOrderInfoEntity> orderInfoEntities = playOrderInfoService.clerkSelectOrderInfoList(entity.getId(),
|
List<PlayOrderInfoEntity> orderInfoEntities = playOrderInfoService.clerkSelectOrderInfoList(entity.getId(),
|
||||||
vo.getStartTime(), vo.getEndTime());
|
startTime, endTime);
|
||||||
List<PlayPersonnelGroupInfoEntity> groupInfoEntities = playPersonnelGroupInfoService.selectAll();
|
List<PlayPersonnelGroupInfoEntity> groupInfoEntities = playPersonnelGroupInfoService.selectAll();
|
||||||
return R.ok(playClerkPerformanceService.getClerkPerformanceInfo(entity, orderInfoEntities, clerkLevelInfoEntity,
|
return R.ok(playClerkPerformanceService.getClerkPerformanceInfo(entity, orderInfoEntities, clerkLevelInfoEntity,
|
||||||
groupInfoEntities));
|
groupInfoEntities, startTime, endTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<BlindBoxRewardView> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,10 +178,16 @@ public class WxCustomMpService {
|
|||||||
data.add(new WxMpTemplateData("thing11", commodityName));
|
data.add(new WxMpTemplateData("thing11", commodityName));
|
||||||
data.add(new WxMpTemplateData("amount8", money));
|
data.add(new WxMpTemplateData("amount8", money));
|
||||||
templateMessage.setData(data);
|
templateMessage.setData(data);
|
||||||
|
PlayClerkUserInfoEntity clerkUserInfo =
|
||||||
|
StringUtils.isBlank(openId) ? null : clerkUserInfoService.selectByOpenid(openId);
|
||||||
|
String clerkId = clerkUserInfo == null ? null : clerkUserInfo.getId();
|
||||||
|
String clerkNickname = clerkUserInfo == null ? null : clerkUserInfo.getNickname();
|
||||||
try {
|
try {
|
||||||
proxyWxMpService(tenantId).getTemplateMsgService().sendTemplateMsg(templateMessage);
|
proxyWxMpService(tenantId).getTemplateMsgService().sendTemplateMsg(templateMessage);
|
||||||
} catch (WxErrorException e) {
|
} catch (WxErrorException e) {
|
||||||
log.error("接单成功发送消息异常", e);
|
log.error(
|
||||||
|
"接单成功发送消息异常, tenantId={}, tenantName={}, orderId={}, orderNo={}, recipientType=clerk, clerkId={}, openId={}, nickname={}",
|
||||||
|
tenantId, tenant.getTenantName(), orderId, orderNo, clerkId, openId, clerkNickname, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
@@ -34,4 +35,28 @@ public interface EarningsLineMapper extends BaseMapper<EarningsLineEntity> {
|
|||||||
" AND (status = 'available' OR (status = 'frozen' AND unlock_time <= #{now})) " +
|
" AND (status = 'available' OR (status = 'frozen' AND unlock_time <= #{now})) " +
|
||||||
"ORDER BY unlock_time ASC")
|
"ORDER BY unlock_time ASC")
|
||||||
List<EarningsLineEntity> selectWithdrawableLines(@Param("clerkId") String clerkId, @Param("now") LocalDateTime now);
|
List<EarningsLineEntity> selectWithdrawableLines(@Param("clerkId") String clerkId, @Param("now") LocalDateTime now);
|
||||||
|
|
||||||
|
@Select("<script>" +
|
||||||
|
"SELECT COALESCE(SUM(el.amount), 0) " +
|
||||||
|
"FROM play_earnings_line el " +
|
||||||
|
"JOIN play_order_info oi ON oi.id = el.order_id " +
|
||||||
|
"WHERE el.deleted = 0 " +
|
||||||
|
" AND oi.deleted = 0 " +
|
||||||
|
" AND el.clerk_id = #{clerkId} " +
|
||||||
|
" AND el.status <> 'reversed' " +
|
||||||
|
"<if test='orderIds != null and orderIds.size > 0'>" +
|
||||||
|
" AND el.order_id IN " +
|
||||||
|
" <foreach item='id' collection='orderIds' open='(' separator=',' close=')'>#{id}</foreach>" +
|
||||||
|
"</if>" +
|
||||||
|
"<if test='startTime != null'>" +
|
||||||
|
" AND oi.purchaser_time >= #{startTime}" +
|
||||||
|
"</if>" +
|
||||||
|
"<if test='endTime != null'>" +
|
||||||
|
" AND oi.purchaser_time <= #{endTime}" +
|
||||||
|
"</if>" +
|
||||||
|
"</script>")
|
||||||
|
BigDecimal sumAmountByClerkAndOrderIds(@Param("clerkId") String clerkId,
|
||||||
|
@Param("orderIds") Collection<String> orderIds,
|
||||||
|
@Param("startTime") String startTime,
|
||||||
|
@Param("endTime") String endTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.starry.admin.utils;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for normalizing date/time range inputs.
|
||||||
|
*/
|
||||||
|
public final class DateRangeUtils {
|
||||||
|
|
||||||
|
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private DateRangeUtils() {}
|
||||||
|
|
||||||
|
public static String normalizeStart(String raw) {
|
||||||
|
if (StrUtil.isBlank(raw)) {
|
||||||
|
return LocalDate.now().minusDays(6).format(DATE_FORMATTER) + " 00:00:00";
|
||||||
|
}
|
||||||
|
if (raw.length() > 10) {
|
||||||
|
LocalDateTime.parse(raw, DATE_TIME_FORMATTER);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return raw + " 00:00:00";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeEnd(String raw) {
|
||||||
|
if (StrUtil.isBlank(raw)) {
|
||||||
|
return LocalDate.now().format(DATE_FORMATTER) + " 23:59:59";
|
||||||
|
}
|
||||||
|
if (raw.length() > 10) {
|
||||||
|
LocalDateTime.parse(raw, DATE_TIME_FORMATTER);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return raw + " 23:59:59";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeStartOptional(String raw) {
|
||||||
|
if (StrUtil.isBlank(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeFlexible(raw, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeEndOptional(String raw) {
|
||||||
|
if (StrUtil.isBlank(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeFlexible(raw, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeFlexible(String raw, boolean isEnd) {
|
||||||
|
String candidate = raw.trim().replace('T', ' ').replace("Z", "");
|
||||||
|
if (candidate.length() <= 10) {
|
||||||
|
candidate = candidate + (isEnd ? " 23:59:59" : " 00:00:00");
|
||||||
|
} else if (candidate.length() > 19) {
|
||||||
|
candidate = candidate.substring(0, 19);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LocalDateTime.parse(candidate, DATE_TIME_FORMATTER);
|
||||||
|
return candidate;
|
||||||
|
} catch (DateTimeParseException ex) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS blind_box_config (
|
||||||
|
id varchar(36) NOT NULL COMMENT '盲盒ID',
|
||||||
|
tenant_id varchar(36) NOT NULL COMMENT '租户ID',
|
||||||
|
name varchar(64) NOT NULL COMMENT '盲盒名称',
|
||||||
|
cover_url varchar(255) DEFAULT NULL COMMENT '封面图',
|
||||||
|
description text DEFAULT NULL COMMENT '盲盒描述',
|
||||||
|
price decimal(10,2) NOT NULL COMMENT '盲盒售价',
|
||||||
|
status tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-上架;0-下架',
|
||||||
|
created_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
created_by varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||||
|
updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
updated_by varchar(32) DEFAULT NULL COMMENT '更新人',
|
||||||
|
deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除 1已删除 0未删除',
|
||||||
|
version int NOT NULL DEFAULT 1 COMMENT '数据版本',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_box_tenant_status (tenant_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='盲盒配置';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS blind_box_pool (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id varchar(36) NOT NULL COMMENT '租户ID',
|
||||||
|
blind_box_id varchar(36) NOT NULL COMMENT '盲盒ID',
|
||||||
|
reward_gift_id varchar(36) NOT NULL COMMENT '中奖礼物ID',
|
||||||
|
reward_price decimal(10,2) NOT NULL COMMENT '盲盒赠送价快照',
|
||||||
|
weight int NOT NULL COMMENT '抽奖权重',
|
||||||
|
remaining_stock int DEFAULT NULL COMMENT '剩余库存(NULL 表示不限量)',
|
||||||
|
valid_from datetime DEFAULT NULL COMMENT '生效时间',
|
||||||
|
valid_to datetime DEFAULT NULL COMMENT '失效时间',
|
||||||
|
status tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-启用;0-停用',
|
||||||
|
created_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
created_by varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||||
|
updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
updated_by varchar(32) DEFAULT NULL COMMENT '更新人',
|
||||||
|
deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除 1已删除 0未删除',
|
||||||
|
version int NOT NULL DEFAULT 1 COMMENT '数据版本',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_pool_tenant_box (tenant_id, blind_box_id, status),
|
||||||
|
KEY idx_pool_reward (tenant_id, reward_gift_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='盲盒奖池配置';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS blind_box_reward (
|
||||||
|
id varchar(36) NOT NULL COMMENT '奖励ID',
|
||||||
|
tenant_id varchar(36) NOT NULL COMMENT '租户ID',
|
||||||
|
customer_id varchar(36) NOT NULL COMMENT '顾客ID',
|
||||||
|
blind_box_id varchar(36) NOT NULL COMMENT '盲盒ID',
|
||||||
|
reward_gift_id varchar(36) NOT NULL COMMENT '中奖礼物ID',
|
||||||
|
reward_price decimal(10,2) NOT NULL COMMENT '中奖礼物价格',
|
||||||
|
box_price decimal(10,2) NOT NULL COMMENT '盲盒价格',
|
||||||
|
subsidy_amount decimal(10,2) NOT NULL DEFAULT 0 COMMENT '补贴金额',
|
||||||
|
reward_stock_snapshot int DEFAULT NULL COMMENT '抽奖时库存快照',
|
||||||
|
seed varchar(128) NOT NULL COMMENT '随机种子',
|
||||||
|
status varchar(16) NOT NULL DEFAULT 'UNUSED' COMMENT 'UNUSED / USED / REFUNDED',
|
||||||
|
created_by_order varchar(36) NOT NULL COMMENT '来源订单ID',
|
||||||
|
expires_at datetime NOT NULL COMMENT '到期时间',
|
||||||
|
used_order_id varchar(36) DEFAULT NULL COMMENT '兑现订单ID',
|
||||||
|
used_clerk_id varchar(36) DEFAULT NULL COMMENT '兑现店员ID',
|
||||||
|
used_time datetime DEFAULT NULL COMMENT '兑现时间',
|
||||||
|
created_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
created_by varchar(32) DEFAULT NULL COMMENT '创建人',
|
||||||
|
updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
updated_by varchar(32) DEFAULT NULL COMMENT '更新人',
|
||||||
|
deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除 1已删除 0未删除',
|
||||||
|
version int NOT NULL DEFAULT 1 COMMENT '数据版本',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_reward_customer_status (customer_id, status),
|
||||||
|
KEY idx_reward_tenant_blind (tenant_id, blind_box_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='盲盒奖励明细';
|
||||||
|
|
||||||
|
ALTER TABLE play_order_info
|
||||||
|
ADD COLUMN payment_source varchar(32) NOT NULL DEFAULT 'BALANCE' COMMENT '支付来源(余额、第三方、盲盒奖励等)' AFTER pay_method,
|
||||||
|
ADD COLUMN source_reward_id varchar(36) DEFAULT NULL COMMENT '盲盒奖励引用ID' AFTER payment_source;
|
||||||
|
|
||||||
|
UPDATE play_order_info SET payment_source = 'BALANCE' WHERE payment_source IS NULL;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BlindBoxInventoryServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private BlindBoxInventoryService blindBoxInventoryService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReserveStockWhenDraw() {
|
||||||
|
when(blindBoxPoolMapper.consumeRewardStock(eq("tenant-1"), eq(5L), eq("gift-1"))).thenReturn(1);
|
||||||
|
|
||||||
|
blindBoxInventoryService.reserveRewardStock("tenant-1", 5L, "gift-1");
|
||||||
|
|
||||||
|
verify(blindBoxPoolMapper).consumeRewardStock("tenant-1", 5L, "gift-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowWhenStockInsufficient() {
|
||||||
|
when(blindBoxPoolMapper.consumeRewardStock(eq("tenant-1"), eq(6L), eq("gift-1"))).thenReturn(0);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxInventoryService.reserveRewardStock("tenant-1", 6L, "gift-1"));
|
||||||
|
assertTrue(ex.getMessage().contains("库存不足"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxGiftOption;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolView;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.shop.mapper.PlayGiftInfoMapper;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BlindBoxPoolAdminServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlayGiftInfoMapper playGiftInfoMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private BlindBoxPoolAdminService blindBoxPoolAdminService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupContext() {
|
||||||
|
SecurityUtils.setTenantId("tenant-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListPoolsWithGiftNames() {
|
||||||
|
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
|
||||||
|
entity.setId(10L);
|
||||||
|
entity.setTenantId("tenant-1");
|
||||||
|
entity.setBlindBoxId("blind-1");
|
||||||
|
entity.setRewardGiftId("gift-2");
|
||||||
|
entity.setRewardPrice(BigDecimal.valueOf(9.9));
|
||||||
|
entity.setWeight(50);
|
||||||
|
entity.setRemainingStock(100);
|
||||||
|
entity.setValidFrom(LocalDateTime.of(2024, 8, 1, 0, 0));
|
||||||
|
entity.setStatus(1);
|
||||||
|
|
||||||
|
when(blindBoxPoolMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(entity));
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
config.setName("幸运盲盒");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||||
|
reward.setId("gift-2");
|
||||||
|
reward.setName("超值娃娃");
|
||||||
|
when(playGiftInfoMapper.selectBatchIds(any(Collection.class)))
|
||||||
|
.thenAnswer(invocation -> Arrays.asList(reward));
|
||||||
|
|
||||||
|
List<BlindBoxPoolView> views = blindBoxPoolAdminService.list("blind-1");
|
||||||
|
|
||||||
|
assertEquals(1, views.size());
|
||||||
|
BlindBoxPoolView view = views.get(0);
|
||||||
|
assertEquals("幸运盲盒", view.getBlindBoxName());
|
||||||
|
assertEquals("超值娃娃", view.getRewardGiftName());
|
||||||
|
assertEquals(50, view.getWeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReplacePoolAndInsertRows() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||||
|
reward.setId("gift-2");
|
||||||
|
reward.setName("超值娃娃");
|
||||||
|
reward.setType("1");
|
||||||
|
reward.setPrice(BigDecimal.valueOf(9.9));
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||||
|
.thenReturn(Collections.singletonList(reward));
|
||||||
|
|
||||||
|
when(blindBoxPoolMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(1);
|
||||||
|
AtomicInteger insertCount = new AtomicInteger();
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
insertCount.incrementAndGet();
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||||
|
row.setRewardGiftName("超值娃娃");
|
||||||
|
row.setWeight(80);
|
||||||
|
row.setRemainingStock(10);
|
||||||
|
row.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row));
|
||||||
|
|
||||||
|
assertEquals(1, insertCount.get());
|
||||||
|
ArgumentCaptor<BlindBoxPoolEntity> captor = ArgumentCaptor.forClass(BlindBoxPoolEntity.class);
|
||||||
|
verify(blindBoxPoolMapper).insert(captor.capture());
|
||||||
|
BlindBoxPoolEntity saved = captor.getValue();
|
||||||
|
assertEquals("gift-2", saved.getRewardGiftId());
|
||||||
|
assertEquals(Integer.valueOf(80), saved.getWeight());
|
||||||
|
assertEquals(BigDecimal.valueOf(9.9), saved.getRewardPrice());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowWhenRewardGiftMissing() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||||
|
row.setRewardGiftName("未知礼物");
|
||||||
|
row.setWeight(10);
|
||||||
|
|
||||||
|
assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowForInvalidWeight() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||||
|
reward.setId("gift-2");
|
||||||
|
reward.setName("超值娃娃");
|
||||||
|
reward.setType("1");
|
||||||
|
reward.setPrice(BigDecimal.ONE);
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||||
|
.thenReturn(Collections.singletonList(reward));
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow row = new BlindBoxPoolImportRow();
|
||||||
|
row.setRewardGiftName("超值娃娃");
|
||||||
|
row.setWeight(0);
|
||||||
|
|
||||||
|
assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(row)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldListGiftOptions() {
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId("gift-1");
|
||||||
|
gift.setTenantId("tenant-1");
|
||||||
|
gift.setHistory("0");
|
||||||
|
gift.setState("0");
|
||||||
|
gift.setType("1");
|
||||||
|
gift.setPrice(BigDecimal.valueOf(19.9));
|
||||||
|
gift.setName("超值娃娃");
|
||||||
|
gift.setUrl("https://image");
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(gift));
|
||||||
|
|
||||||
|
List<BlindBoxGiftOption> options = blindBoxPoolAdminService.listGiftOptions("娃");
|
||||||
|
|
||||||
|
assertEquals(1, options.size());
|
||||||
|
assertEquals("gift-1", options.get(0).getId());
|
||||||
|
assertEquals("超值娃娃", options.get(0).getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreatePoolEntry() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
config.setName("幸运盲盒");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId("gift-2");
|
||||||
|
gift.setTenantId("tenant-1");
|
||||||
|
gift.setHistory("0");
|
||||||
|
gift.setState("0");
|
||||||
|
gift.setType("1");
|
||||||
|
gift.setPrice(BigDecimal.valueOf(9.9));
|
||||||
|
gift.setName("超值娃娃");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-2")).thenReturn(gift);
|
||||||
|
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenAnswer(invocation -> {
|
||||||
|
BlindBoxPoolEntity entity = invocation.getArgument(0);
|
||||||
|
entity.setId(100L);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-2");
|
||||||
|
request.setRewardPrice(BigDecimal.valueOf(12.5));
|
||||||
|
request.setWeight(80);
|
||||||
|
request.setRemainingStock(5);
|
||||||
|
request.setStatus(1);
|
||||||
|
request.setValidFrom(LocalDateTime.of(2024, 8, 1, 0, 0));
|
||||||
|
request.setValidTo(LocalDateTime.of(2024, 8, 31, 23, 59, 59));
|
||||||
|
|
||||||
|
BlindBoxPoolView view = blindBoxPoolAdminService.create("blind-1", request);
|
||||||
|
|
||||||
|
assertEquals("gift-2", view.getRewardGiftId());
|
||||||
|
assertEquals(Integer.valueOf(80), view.getWeight());
|
||||||
|
verify(blindBoxPoolMapper).insert(any(BlindBoxPoolEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdatePoolEntry() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
config.setName("幸运盲盒");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId("gift-3");
|
||||||
|
gift.setTenantId("tenant-1");
|
||||||
|
gift.setHistory("0");
|
||||||
|
gift.setState("0");
|
||||||
|
gift.setType("1");
|
||||||
|
gift.setPrice(BigDecimal.valueOf(19.9));
|
||||||
|
gift.setName("超级公仔");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-3")).thenReturn(gift);
|
||||||
|
|
||||||
|
BlindBoxPoolEntity existing = new BlindBoxPoolEntity();
|
||||||
|
existing.setId(200L);
|
||||||
|
existing.setTenantId("tenant-1");
|
||||||
|
existing.setBlindBoxId("blind-1");
|
||||||
|
existing.setRewardGiftId("gift-1");
|
||||||
|
existing.setStatus(1);
|
||||||
|
when(blindBoxPoolMapper.selectById(200L)).thenReturn(existing);
|
||||||
|
when(blindBoxPoolMapper.updateById(any(BlindBoxPoolEntity.class))).thenReturn(1);
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-3");
|
||||||
|
request.setWeight(60);
|
||||||
|
request.setRemainingStock(null);
|
||||||
|
request.setStatus(0);
|
||||||
|
request.setRewardPrice(null);
|
||||||
|
request.setValidFrom(LocalDateTime.of(2024, 9, 1, 0, 0));
|
||||||
|
request.setValidTo(LocalDateTime.of(2024, 9, 30, 23, 59, 59));
|
||||||
|
|
||||||
|
BlindBoxPoolView view = blindBoxPoolAdminService.update(200L, request);
|
||||||
|
|
||||||
|
assertEquals("gift-3", existing.getRewardGiftId());
|
||||||
|
assertEquals(Integer.valueOf(60), existing.getWeight());
|
||||||
|
assertEquals(Integer.valueOf(0), existing.getStatus());
|
||||||
|
assertEquals("超级公仔", view.getRewardGiftName());
|
||||||
|
verify(blindBoxPoolMapper).updateById(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.BlindBoxRewardStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BlindBoxServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxPoolMapper poolMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxRewardMapper rewardMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxInventoryService inventoryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxDispatchService dispatchService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private BlindBoxConfigService configService;
|
||||||
|
|
||||||
|
private Clock clock;
|
||||||
|
private TestRandomAdapter randomAdapter;
|
||||||
|
private BlindBoxService blindBoxService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
clock = Clock.fixed(Instant.parse("2024-08-01T10:15:30Z"), ZoneOffset.UTC);
|
||||||
|
randomAdapter = new TestRandomAdapter();
|
||||||
|
blindBoxService = new BlindBoxService(poolMapper, rewardMapper, inventoryService, dispatchService, configService,
|
||||||
|
clock, Duration.ofDays(7), randomAdapter);
|
||||||
|
|
||||||
|
lenient().when(rewardMapper.insert(any())).thenAnswer(invocation -> {
|
||||||
|
BlindBoxRewardEntity entity = invocation.getArgument(0);
|
||||||
|
if (entity.getId() == null) {
|
||||||
|
entity.setId(UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateRewardRecordWhenOrderCompleted() {
|
||||||
|
randomAdapter.nextDoubleToReturn = 0.95;
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
config.setPrice(BigDecimal.valueOf(49));
|
||||||
|
when(configService.requireById("blind-1")).thenReturn(config);
|
||||||
|
List<BlindBoxCandidate> candidates = Arrays.asList(
|
||||||
|
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(LocalDateTime.class))).thenReturn(candidates);
|
||||||
|
|
||||||
|
BlindBoxRewardEntity entity = blindBoxService.drawReward("tenant-1", "order-1", "customer-1", "blind-1",
|
||||||
|
"seed-123");
|
||||||
|
|
||||||
|
ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
|
||||||
|
verify(rewardMapper).insert(captor.capture());
|
||||||
|
BlindBoxRewardEntity persisted = captor.getValue();
|
||||||
|
|
||||||
|
assertEquals("gift-high", persisted.getRewardGiftId());
|
||||||
|
assertEquals(BigDecimal.valueOf(99).setScale(2), persisted.getRewardPrice());
|
||||||
|
assertEquals(BigDecimal.valueOf(49).setScale(2), persisted.getBoxPrice());
|
||||||
|
assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
|
||||||
|
assertEquals(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusDays(7), persisted.getExpiresAt());
|
||||||
|
verify(inventoryService).reserveRewardStock("tenant-1", 2L, "gift-high");
|
||||||
|
assertEquals(entity.getRewardGiftId(), persisted.getRewardGiftId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleUnlimitedStock() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
config.setPrice(BigDecimal.valueOf(29));
|
||||||
|
when(configService.requireById("blind-1")).thenReturn(config);
|
||||||
|
List<BlindBoxCandidate> 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(LocalDateTime.class))).thenReturn(candidates);
|
||||||
|
|
||||||
|
blindBoxService.drawReward("tenant-1", "order-2", "customer-9", "blind-1", "seed-unlimited");
|
||||||
|
|
||||||
|
ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
|
||||||
|
verify(rewardMapper).insert(captor.capture());
|
||||||
|
BlindBoxRewardEntity persisted = captor.getValue();
|
||||||
|
assertEquals("gift-unlimited", persisted.getRewardGiftId());
|
||||||
|
assertEquals(BigDecimal.valueOf(29).setScale(2), persisted.getBoxPrice());
|
||||||
|
assertEquals(BigDecimal.valueOf(59).setScale(2), persisted.getRewardPrice());
|
||||||
|
assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
|
||||||
|
assertNull(persisted.getRewardStockSnapshot());
|
||||||
|
verify(inventoryService).reserveRewardStock("tenant-1", 7L, "gift-unlimited");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPreventDoubleDispatch() {
|
||||||
|
BlindBoxRewardEntity reward = buildRewardEntity();
|
||||||
|
when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);
|
||||||
|
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", "customer-1");
|
||||||
|
|
||||||
|
reward.setStatus(BlindBoxRewardStatus.USED.getCode());
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () ->
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectExpiredReward() {
|
||||||
|
BlindBoxRewardEntity reward = buildRewardEntity();
|
||||||
|
reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).minusHours(1));
|
||||||
|
when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () ->
|
||||||
|
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");
|
||||||
|
reward.setTenantId("tenant-1");
|
||||||
|
reward.setCustomerId("customer-1");
|
||||||
|
reward.setBlindBoxId("blind-1");
|
||||||
|
reward.setRewardGiftId("gift-high");
|
||||||
|
reward.setRewardPrice(BigDecimal.valueOf(99));
|
||||||
|
reward.setBoxPrice(BigDecimal.valueOf(49));
|
||||||
|
reward.setRewardStockSnapshot(3);
|
||||||
|
reward.setSeed("seed-123");
|
||||||
|
reward.setStatus(BlindBoxRewardStatus.UNUSED.getCode());
|
||||||
|
reward.setCreatedByOrder("order-1");
|
||||||
|
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));
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestRandomAdapter implements BlindBoxService.RandomAdapter {
|
||||||
|
|
||||||
|
double nextDoubleToReturn = 0.5;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextDouble() {
|
||||||
|
return nextDoubleToReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,15 +22,18 @@ import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewRes
|
|||||||
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo;
|
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo;
|
||||||
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
|
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
|
||||||
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
||||||
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.Month;
|
import java.time.Month;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
@@ -55,6 +58,9 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
|
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EarningsLineMapper earningsLineMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PlayClerkPerformanceServiceImpl service;
|
private PlayClerkPerformanceServiceImpl service;
|
||||||
|
|
||||||
@@ -89,6 +95,10 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
|
|
||||||
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(ordersA);
|
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(ordersA);
|
||||||
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c2"), anyString(), anyString())).thenReturn(ordersB);
|
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c2"), anyString(), anyString())).thenReturn(ordersB);
|
||||||
|
when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c1"), anyCollection(), any(), any()))
|
||||||
|
.thenReturn(new BigDecimal("170.00"));
|
||||||
|
when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c2"), anyCollection(), any(), any()))
|
||||||
|
.thenReturn(new BigDecimal("55.00"));
|
||||||
|
|
||||||
setAuthentication();
|
setAuthentication();
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +119,8 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
assertEquals(2, top.getContinuedOrderCount());
|
assertEquals(2, top.getContinuedOrderCount());
|
||||||
assertEquals(new BigDecimal("66.67"), top.getContinuedRate());
|
assertEquals(new BigDecimal("66.67"), top.getContinuedRate());
|
||||||
assertEquals(new BigDecimal("100.00"), top.getAverageTicketPrice());
|
assertEquals(new BigDecimal("100.00"), top.getAverageTicketPrice());
|
||||||
|
assertEquals(new BigDecimal("170.00"), top.getEstimatedRevenue());
|
||||||
|
assertEquals(new BigDecimal("55.00"), response.getRankings().get(1).getEstimatedRevenue());
|
||||||
|
|
||||||
ClerkPerformanceOverviewSummaryVo summary = response.getSummary();
|
ClerkPerformanceOverviewSummaryVo summary = response.getSummary();
|
||||||
assertEquals(new BigDecimal("380.00"), summary.getTotalGmv());
|
assertEquals(new BigDecimal("380.00"), summary.getTotalGmv());
|
||||||
@@ -117,6 +129,7 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
assertEquals(2, summary.getTotalContinuedOrderCount());
|
assertEquals(2, summary.getTotalContinuedOrderCount());
|
||||||
assertEquals(new BigDecimal("50.00"), summary.getContinuedRate());
|
assertEquals(new BigDecimal("50.00"), summary.getContinuedRate());
|
||||||
assertEquals(new BigDecimal("95.00"), summary.getAverageTicketPrice());
|
assertEquals(new BigDecimal("95.00"), summary.getAverageTicketPrice());
|
||||||
|
assertEquals(new BigDecimal("225.00"), summary.getTotalEstimatedRevenue());
|
||||||
} finally {
|
} finally {
|
||||||
clearAuthentication();
|
clearAuthentication();
|
||||||
}
|
}
|
||||||
@@ -146,6 +159,8 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
withRefund(order("c1", "userB", "0", "2", "1", new BigDecimal("60.00"), new BigDecimal("30.00"),
|
withRefund(order("c1", "userB", "0", "2", "1", new BigDecimal("60.00"), new BigDecimal("30.00"),
|
||||||
LocalDateTime.of(2024, Month.AUGUST, 2, 18, 0)), new BigDecimal("20.00")));
|
LocalDateTime.of(2024, Month.AUGUST, 2, 18, 0)), new BigDecimal("20.00")));
|
||||||
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(orders);
|
when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(orders);
|
||||||
|
when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c1"), anyCollection(), any(), any()))
|
||||||
|
.thenReturn(new BigDecimal("110.00"));
|
||||||
|
|
||||||
setAuthentication();
|
setAuthentication();
|
||||||
try {
|
try {
|
||||||
@@ -158,6 +173,7 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
assertEquals(3, response.getSnapshot().getOrderCount());
|
assertEquals(3, response.getSnapshot().getOrderCount());
|
||||||
assertEquals(new BigDecimal("66.67"), response.getSnapshot().getContinuedRate());
|
assertEquals(new BigDecimal("66.67"), response.getSnapshot().getContinuedRate());
|
||||||
assertEquals(new BigDecimal("86.67"), response.getSnapshot().getAverageTicketPrice());
|
assertEquals(new BigDecimal("86.67"), response.getSnapshot().getAverageTicketPrice());
|
||||||
|
assertEquals(new BigDecimal("110.00"), response.getSnapshot().getEstimatedRevenue());
|
||||||
|
|
||||||
assertNotNull(response.getComposition());
|
assertNotNull(response.getComposition());
|
||||||
assertEquals(4, response.getComposition().getOrderComposition().size());
|
assertEquals(4, response.getComposition().getOrderComposition().size());
|
||||||
@@ -191,6 +207,30 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getClerkPerformanceInfo aligns earnings lookup with purchaser time")
|
||||||
|
void getClerkPerformanceInfoAlignsEarningsLookupWithPurchaserTime() {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk("c3", "Carol", "g3", "l3");
|
||||||
|
List<PlayOrderInfoEntity> orders = Collections.singletonList(
|
||||||
|
order("c3", "userX", "1", "0", "0", new BigDecimal("99.00"), new BigDecimal("50.00"),
|
||||||
|
LocalDateTime.of(2024, Month.AUGUST, 1, 15, 30)));
|
||||||
|
when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c3"), anyCollection(), any(), any()))
|
||||||
|
.thenReturn(new BigDecimal("80.00"));
|
||||||
|
|
||||||
|
service.getClerkPerformanceInfo(clerk, orders, Collections.singletonList(level("l3", "钻石")),
|
||||||
|
Collections.singletonList(group("g3", "三组")), "2024-08-01", "2024-08-02");
|
||||||
|
|
||||||
|
ArgumentCaptor<Collection<String>> orderIdsCaptor = ArgumentCaptor.forClass(Collection.class);
|
||||||
|
ArgumentCaptor<String> startCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
ArgumentCaptor<String> endCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(earningsLineMapper).sumAmountByClerkAndOrderIds(eq("c3"), orderIdsCaptor.capture(),
|
||||||
|
startCaptor.capture(), endCaptor.capture());
|
||||||
|
|
||||||
|
assertTrue(orderIdsCaptor.getValue().contains(orders.get(0).getId()));
|
||||||
|
assertEquals("2024-08-01 00:00:00", startCaptor.getValue());
|
||||||
|
assertEquals("2024-08-02 23:59:59", endCaptor.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
||||||
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
@@ -229,6 +269,7 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
order.setEstimatedRevenue(estimatedRevenue);
|
order.setEstimatedRevenue(estimatedRevenue);
|
||||||
order.setOrdersExpiredState("1".equals(refundType) ? "1" : "0");
|
order.setOrdersExpiredState("1".equals(refundType) ? "1" : "0");
|
||||||
order.setPurchaserTime(purchaserTime);
|
order.setPurchaserTime(purchaserTime);
|
||||||
|
order.setId(clerkId + "-" + purchaser + "-" + purchaserTime.toString());
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user