|
@@ -14,14 +14,18 @@ import com.fs.common.utils.DateUtils;
|
|
|
import com.fs.common.utils.DictUtils;
|
|
import com.fs.common.utils.DictUtils;
|
|
|
import com.fs.common.utils.StringUtils;
|
|
import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.his.domain.FsUser;
|
|
import com.fs.his.domain.FsUser;
|
|
|
|
|
+import com.fs.his.mapper.FsUserMapper;
|
|
|
import com.fs.his.service.IFsUserService;
|
|
import com.fs.his.service.IFsUserService;
|
|
|
import com.fs.live.domain.*;
|
|
import com.fs.live.domain.*;
|
|
|
import com.fs.live.mapper.LiveCouponIssueMapper;
|
|
import com.fs.live.mapper.LiveCouponIssueMapper;
|
|
|
import com.fs.live.mapper.LiveCouponIssueUserMapper;
|
|
import com.fs.live.mapper.LiveCouponIssueUserMapper;
|
|
|
import com.fs.live.mapper.LiveMapper;
|
|
import com.fs.live.mapper.LiveMapper;
|
|
|
import com.fs.live.param.CouponPO;
|
|
import com.fs.live.param.CouponPO;
|
|
|
|
|
+import com.fs.live.param.LiveCouponBatchVerifyParam;
|
|
|
import com.fs.live.param.LiveCouponVerifyParam;
|
|
import com.fs.live.param.LiveCouponVerifyParam;
|
|
|
import com.fs.live.service.*;
|
|
import com.fs.live.service.*;
|
|
|
|
|
+import com.fs.live.vo.LiveCouponBatchVerifyItemVo;
|
|
|
|
|
+import com.fs.live.vo.LiveCouponBatchVerifyResultVo;
|
|
|
import com.fs.live.vo.LiveCouponListVo;
|
|
import com.fs.live.vo.LiveCouponListVo;
|
|
|
import com.fs.live.vo.LiveCouponUserDetailVo;
|
|
import com.fs.live.vo.LiveCouponUserDetailVo;
|
|
|
|
|
|
|
@@ -64,6 +68,8 @@ public class LiveCouponServiceImpl implements ILiveCouponService
|
|
|
private ILiveConsoleOpLogService liveConsoleOpLogService;
|
|
private ILiveConsoleOpLogService liveConsoleOpLogService;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private IFsUserService fsUserService;
|
|
private IFsUserService fsUserService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private FsUserMapper fsUserMapper;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 查询优惠券
|
|
* 查询优惠券
|
|
@@ -475,7 +481,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 销售扫码核销优惠券。
|
|
|
|
|
|
|
+ * 销售扫码核销优惠券(单条,独立实现,不受批量逻辑影响)。
|
|
|
* <p>流程:参数校验 → 查券 → 绑定销售校验 → 状态/过期校验 → 落库核销 → 返回详情</p>
|
|
* <p>流程:参数校验 → 查券 → 绑定销售校验 → 状态/过期校验 → 落库核销 → 返回详情</p>
|
|
|
*/
|
|
*/
|
|
|
@Override
|
|
@Override
|
|
@@ -513,6 +519,210 @@ public class LiveCouponServiceImpl implements ILiveCouponService
|
|
|
return R.ok("核销成功").put("data", detail);
|
|
return R.ok("核销成功").put("data", detail);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 销售批量核销优惠券(按核销码列表)。
|
|
|
|
|
+ * <p>流程:参数校验 → 批量预加载 → 内存分类 → 批量落库 → 组装结果</p>
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public R batchVerifyCoupon(LiveCouponBatchVerifyParam param) {
|
|
|
|
|
+ if (param.getVerifyUserId() == null) {
|
|
|
|
|
+ return R.error("销售未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> codes = normalizeBatchVerifyCodes(param.getVerifyCodes());
|
|
|
|
|
+ if (codes == null) {
|
|
|
|
|
+ return R.error("核销码不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (codes.size() > 100) {
|
|
|
|
|
+ return R.error("单次最多核销100张优惠券");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 批量预加载:优惠券 + 持券用户(各1次查询)
|
|
|
|
|
+ Map<String, LiveCouponUser> couponMap = loadCouponMapByVerifyCodes(codes);
|
|
|
|
|
+ Map<Long, FsUser> userMap = loadUserMapByCoupons(couponMap.values());
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 内存分类:待核销 / 待过期 / 失败原因
|
|
|
|
|
+ Date now = DateUtils.getNowDate();
|
|
|
|
|
+ BatchVerifyClassify classify = classifyBatchVerify(codes, couponMap, userMap, param.getVerifyUserId(), now);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 批量落库(过期、核销各最多1次)
|
|
|
|
|
+ persistBatchVerify(classify, param.getVerifyUserId(), now);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 批量回查明细并按入参顺序组装结果
|
|
|
|
|
+ Map<Long, LiveCouponUserDetailVo> detailMap = loadDetailMapByIds(classify.verifyIds);
|
|
|
|
|
+ LiveCouponBatchVerifyResultVo result = buildBatchVerifyResult(codes, classify, detailMap);
|
|
|
|
|
+ String msg = String.format("批量核销完成:成功%d,失败%d", result.getSuccessCount(), result.getFailCount());
|
|
|
|
|
+ return R.ok(msg).put("data", result);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 规范化核销码列表:去空白、保序;无效时返回 null。
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<String> normalizeBatchVerifyCodes(List<String> verifyCodes) {
|
|
|
|
|
+ if (verifyCodes == null || verifyCodes.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> codes = verifyCodes.stream()
|
|
|
|
|
+ .filter(StringUtils::isNotEmpty)
|
|
|
|
|
+ .map(String::trim)
|
|
|
|
|
+ .filter(StringUtils::isNotEmpty)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ return codes.isEmpty() ? null : codes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按核销码批量加载优惠券,构建 code -> 优惠券 映射。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, LiveCouponUser> loadCouponMapByVerifyCodes(List<String> codes) {
|
|
|
|
|
+ List<String> uniqueCodes = codes.stream().distinct().collect(Collectors.toList());
|
|
|
|
|
+ return liveCouponUserService.selectLiveCouponUserByVerifyCodes(uniqueCodes).stream()
|
|
|
|
|
+ .filter(c -> StringUtils.isNotEmpty(c.getVerifyCode()))
|
|
|
|
|
+ .collect(Collectors.toMap(LiveCouponUser::getVerifyCode, c -> c, (a, b) -> a));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按优惠券所属用户批量加载 fs_user,构建 userId -> 用户 映射。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<Long, FsUser> loadUserMapByCoupons(Collection<LiveCouponUser> coupons) {
|
|
|
|
|
+ List<Long> userIds = coupons.stream()
|
|
|
|
|
+ .map(LiveCouponUser::getUserId)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .map(Integer::longValue)
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ if (userIds.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ return fsUserMapper.findUsersByIds(userIds).stream()
|
|
|
|
|
+ .collect(Collectors.toMap(FsUser::getUserId, u -> u, (a, b) -> a));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 对核销码做内存预校验,拆分为:待核销、待过期、失败。
|
|
|
|
|
+ * <p>重复码在组装结果阶段按出现次数记失败,此处仅处理首次出现的码。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+ private BatchVerifyClassify classifyBatchVerify(List<String> codes,
|
|
|
|
|
+ Map<String, LiveCouponUser> couponMap,
|
|
|
|
|
+ Map<Long, FsUser> userMap,
|
|
|
|
|
+ Long verifyUserId,
|
|
|
|
|
+ Date now) {
|
|
|
|
|
+ BatchVerifyClassify classify = new BatchVerifyClassify();
|
|
|
|
|
+ Set<String> seen = new HashSet<>();
|
|
|
|
|
+ for (String code : codes) {
|
|
|
|
|
+ if (!seen.add(code)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ LiveCouponUser couponUser = couponMap.get(code);
|
|
|
|
|
+ if (couponUser == null || Objects.equals(couponUser.getIsDel(), 1)) {
|
|
|
|
|
+ classify.failMsgMap.put(code, "优惠券不存在");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String bindError = checkBindSalesForBatch(couponUser, verifyUserId, userMap);
|
|
|
|
|
+ if (bindError != null) {
|
|
|
|
|
+ classify.failMsgMap.put(code, bindError);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer status = couponUser.getStatus();
|
|
|
|
|
+ if (status != null && status == 1) {
|
|
|
|
|
+ classify.failMsgMap.put(code, "优惠券已核销");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (status != null && status == 2) {
|
|
|
|
|
+ classify.failMsgMap.put(code, "优惠券已过期");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (couponUser.getLimitTime() != null && couponUser.getLimitTime().before(now)) {
|
|
|
|
|
+ classify.expireIds.add(couponUser.getId());
|
|
|
|
|
+ classify.failMsgMap.put(code, "优惠券已过期");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ classify.pendingCodeIdMap.put(code, couponUser.getId());
|
|
|
|
|
+ classify.verifyIds.add(couponUser.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ return classify;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量落库:先同步过期,再批量核销。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void persistBatchVerify(BatchVerifyClassify classify, Long verifyUserId, Date now) {
|
|
|
|
|
+ if (!classify.expireIds.isEmpty()) {
|
|
|
|
|
+ liveCouponUserService.batchExpireCouponUserByIds(classify.expireIds, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!classify.verifyIds.isEmpty()) {
|
|
|
|
|
+ liveCouponUserService.batchVerifyCouponUserByIds(classify.verifyIds, verifyUserId, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按发放记录ID批量加载详情。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<Long, LiveCouponUserDetailVo> loadDetailMapByIds(List<Long> ids) {
|
|
|
|
|
+ if (ids == null || ids.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ return liveCouponUserService.selectLiveCouponUserDetailByIds(ids).stream()
|
|
|
|
|
+ .collect(Collectors.toMap(LiveCouponUserDetailVo::getId, d -> d, (a, b) -> a));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按入参顺序组装批量核销结果;重复核销码每次记失败。
|
|
|
|
|
+ */
|
|
|
|
|
+ private LiveCouponBatchVerifyResultVo buildBatchVerifyResult(List<String> codes,
|
|
|
|
|
+ BatchVerifyClassify classify,
|
|
|
|
|
+ Map<Long, LiveCouponUserDetailVo> detailMap) {
|
|
|
|
|
+ LiveCouponBatchVerifyResultVo result = new LiveCouponBatchVerifyResultVo();
|
|
|
|
|
+ result.setTotal(codes.size());
|
|
|
|
|
+ Set<String> firstSeen = new HashSet<>();
|
|
|
|
|
+ for (String code : codes) {
|
|
|
|
|
+ if (!firstSeen.add(code)) {
|
|
|
|
|
+ result.addFail(code, "重复的核销码");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (classify.failMsgMap.containsKey(code)) {
|
|
|
|
|
+ result.addFail(code, classify.failMsgMap.get(code));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Long id = classify.pendingCodeIdMap.get(code);
|
|
|
|
|
+ LiveCouponUserDetailVo detail = id == null ? null : detailMap.get(id);
|
|
|
|
|
+ if (detail != null && Objects.equals(detail.getStatus(), 1)) {
|
|
|
|
|
+ fillCouponStatusName(detail);
|
|
|
|
|
+ result.addSuccess(code, detail);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.addFail(code, "核销失败,请重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量核销中间分类结果。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class BatchVerifyClassify {
|
|
|
|
|
+ private final List<Long> expireIds = new ArrayList<>();
|
|
|
|
|
+ private final List<Long> verifyIds = new ArrayList<>();
|
|
|
|
|
+ private final Map<String, String> failMsgMap = new HashMap<>();
|
|
|
|
|
+ private final Map<String, Long> pendingCodeIdMap = new LinkedHashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量核销场景的绑定销售校验(使用预加载的用户 Map,避免逐条查库)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 失败原因;通过返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ private String checkBindSalesForBatch(LiveCouponUser couponUser, Long verifyUserId, Map<Long, FsUser> userMap) {
|
|
|
|
|
+ if (couponUser.getUserId() == null) {
|
|
|
|
|
+ return "优惠券所属用户不存在";
|
|
|
|
|
+ }
|
|
|
|
|
+ FsUser fsUser = userMap.get(Long.valueOf(couponUser.getUserId()));
|
|
|
|
|
+ if (fsUser == null) {
|
|
|
|
|
+ return "优惠券所属用户不存在";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Objects.equals(fsUser.getBindCompanyUserId(), verifyUserId)) {
|
|
|
|
|
+ return "该用户未绑定当前销售,无法核销";
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 解析待核销优惠券:优先按核销码查询,否则按发放记录ID查询。
|
|
* 解析待核销优惠券:优先按核销码查询,否则按发放记录ID查询。
|
|
|
*
|
|
*
|