فهرست منبع

1、优惠券批量核销功能

yys 1 هفته پیش
والد
کامیت
62f4f1468c

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveCouponUserController.java

@@ -9,6 +9,7 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.live.domain.LiveCouponUser;
+import com.fs.live.param.LiveCouponBatchVerifyParam;
 import com.fs.live.param.LiveCouponVerifyParam;
 import com.fs.live.service.ILiveCouponService;
 import com.fs.live.service.ILiveCouponUserService;
@@ -76,4 +77,16 @@ public class LiveCouponUserController extends BaseController
         param.setVerifyUserId(verifyUserId);
         return liveCouponService.verifyCoupon(param);
     }
+
+    /**
+     * 销售端批量核销优惠券(传入多条核销码)
+     */
+    @Log(title = "优惠券批量核销", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchVerify")
+    public R batchVerify(@RequestBody LiveCouponBatchVerifyParam param)
+    {
+        Long verifyUserId = SecurityUtils.getLoginUser().getUser().getUserId();
+        param.setVerifyUserId(verifyUserId);
+        return liveCouponService.batchVerifyCoupon(param);
+    }
 }

+ 22 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java

@@ -88,6 +88,28 @@ public interface LiveCouponUserMapper
     @Select("select lcu.* from live_coupon_user lcu where lcu.verify_code = #{verifyCode} and lcu.is_del = 0 limit 1")
     LiveCouponUser selectLiveCouponUserByVerifyCode(@Param("verifyCode") String verifyCode);
 
+    /**
+     * 按核销码批量查询优惠券(未删除)
+     */
+    List<LiveCouponUser> selectLiveCouponUserByVerifyCodes(@Param("verifyCodes") List<String> verifyCodes);
+
+    /**
+     * 按发放记录ID批量查询详情
+     */
+    List<LiveCouponUserDetailVo> selectLiveCouponUserDetailByIds(@Param("ids") List<Long> ids);
+
+    /**
+     * 批量将未核销优惠券标记为已过期
+     */
+    int batchExpireCouponUserByIds(@Param("ids") List<Long> ids, @Param("updateTime") java.util.Date updateTime);
+
+    /**
+     * 批量核销未核销优惠券(仅更新 status=0 的记录)
+     */
+    int batchVerifyCouponUserByIds(@Param("ids") List<Long> ids,
+                                   @Param("verifyUserId") Long verifyUserId,
+                                   @Param("now") java.util.Date now);
+
     /**
      * 按用户与状态统计直播优惠券数量(排除已删除)
      */

+ 18 - 0
fs-service/src/main/java/com/fs/live/param/LiveCouponBatchVerifyParam.java

@@ -0,0 +1,18 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 直播优惠券批量核销参数
+ */
+@Data
+public class LiveCouponBatchVerifyParam {
+
+    /** 核销码列表 */
+    private List<String> verifyCodes;
+
+    /** 销售用户ID(服务端填充) */
+    private Long verifyUserId;
+}

+ 6 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCouponService.java

@@ -6,6 +6,7 @@ import java.util.Map;
 import com.fs.common.core.domain.R;
 import com.fs.live.domain.LiveCoupon;
 import com.fs.live.param.CouponPO;
+import com.fs.live.param.LiveCouponBatchVerifyParam;
 import com.fs.live.param.LiveCouponVerifyParam;
 import com.fs.live.vo.LiveCouponListVo;
 import com.fs.live.vo.LiveCouponUserDetailVo;
@@ -104,6 +105,11 @@ public interface ILiveCouponService
      */
     R verifyCoupon(LiveCouponVerifyParam param);
 
+    /**
+     * 销售批量核销优惠券(按核销码列表)
+     */
+    R batchVerifyCoupon(LiveCouponBatchVerifyParam param);
+
     List<LiveCoupon> listOn(Long liveId);
 
     /**

+ 20 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCouponUserService.java

@@ -77,4 +77,24 @@ public interface ILiveCouponUserService
     LiveCouponUserDetailVo selectLiveCouponUserDetailByVerifyCode(String verifyCode);
 
     LiveCouponUser selectLiveCouponUserByVerifyCode(String verifyCode);
+
+    /**
+     * 按核销码批量查询优惠券
+     */
+    List<LiveCouponUser> selectLiveCouponUserByVerifyCodes(List<String> verifyCodes);
+
+    /**
+     * 按发放记录ID批量查询详情
+     */
+    List<LiveCouponUserDetailVo> selectLiveCouponUserDetailByIds(List<Long> ids);
+
+    /**
+     * 批量标记已过期
+     */
+    int batchExpireCouponUserByIds(List<Long> ids, java.util.Date updateTime);
+
+    /**
+     * 批量核销
+     */
+    int batchVerifyCouponUserByIds(List<Long> ids, Long verifyUserId, java.util.Date now);
 }

+ 211 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java

@@ -14,14 +14,18 @@ import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DictUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserService;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveCouponIssueMapper;
 import com.fs.live.mapper.LiveCouponIssueUserMapper;
 import com.fs.live.mapper.LiveMapper;
 import com.fs.live.param.CouponPO;
+import com.fs.live.param.LiveCouponBatchVerifyParam;
 import com.fs.live.param.LiveCouponVerifyParam;
 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.LiveCouponUserDetailVo;
 
@@ -64,6 +68,8 @@ public class LiveCouponServiceImpl implements ILiveCouponService
     private ILiveConsoleOpLogService liveConsoleOpLogService;
     @Autowired
     private IFsUserService fsUserService;
+    @Autowired
+    private FsUserMapper fsUserMapper;
 
     /**
      * 查询优惠券
@@ -475,7 +481,7 @@ public class LiveCouponServiceImpl implements ILiveCouponService
     }
 
     /**
-     * 销售扫码核销优惠券。
+     * 销售扫码核销优惠券(单条,独立实现,不受批量逻辑影响)
      * <p>流程:参数校验 → 查券 → 绑定销售校验 → 状态/过期校验 → 落库核销 → 返回详情</p>
      */
     @Override
@@ -513,6 +519,210 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         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查询。
      *

+ 32 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponUserServiceImpl.java

@@ -131,4 +131,36 @@ public class LiveCouponUserServiceImpl implements ILiveCouponUserService
     public LiveCouponUser selectLiveCouponUserByVerifyCode(String verifyCode) {
         return liveCouponUserMapper.selectLiveCouponUserByVerifyCode(verifyCode);
     }
+
+    @Override
+    public List<LiveCouponUser> selectLiveCouponUserByVerifyCodes(List<String> verifyCodes) {
+        if (verifyCodes == null || verifyCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return liveCouponUserMapper.selectLiveCouponUserByVerifyCodes(verifyCodes);
+    }
+
+    @Override
+    public List<LiveCouponUserDetailVo> selectLiveCouponUserDetailByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return liveCouponUserMapper.selectLiveCouponUserDetailByIds(ids);
+    }
+
+    @Override
+    public int batchExpireCouponUserByIds(List<Long> ids, java.util.Date updateTime) {
+        if (ids == null || ids.isEmpty()) {
+            return 0;
+        }
+        return liveCouponUserMapper.batchExpireCouponUserByIds(ids, updateTime);
+    }
+
+    @Override
+    public int batchVerifyCouponUserByIds(List<Long> ids, Long verifyUserId, java.util.Date now) {
+        if (ids == null || ids.isEmpty()) {
+            return 0;
+        }
+        return liveCouponUserMapper.batchVerifyCouponUserByIds(ids, verifyUserId, now);
+    }
 }

+ 39 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCouponBatchVerifyItemVo.java

@@ -0,0 +1,39 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * 批量核销单条结果
+ */
+@Data
+public class LiveCouponBatchVerifyItemVo {
+
+    /** 核销码 */
+    private String verifyCode;
+
+    /** 是否核销成功 */
+    private boolean success;
+
+    /** 结果说明 */
+    private String msg;
+
+    /** 核销成功时的优惠券详情 */
+    private LiveCouponUserDetailVo data;
+
+    public static LiveCouponBatchVerifyItemVo ok(String verifyCode, LiveCouponUserDetailVo data) {
+        LiveCouponBatchVerifyItemVo item = new LiveCouponBatchVerifyItemVo();
+        item.setVerifyCode(verifyCode);
+        item.setSuccess(true);
+        item.setMsg("核销成功");
+        item.setData(data);
+        return item;
+    }
+
+    public static LiveCouponBatchVerifyItemVo fail(String verifyCode, String msg) {
+        LiveCouponBatchVerifyItemVo item = new LiveCouponBatchVerifyItemVo();
+        item.setVerifyCode(verifyCode);
+        item.setSuccess(false);
+        item.setMsg(msg);
+        return item;
+    }
+}

+ 35 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCouponBatchVerifyResultVo.java

@@ -0,0 +1,35 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 批量核销汇总结果
+ */
+@Data
+public class LiveCouponBatchVerifyResultVo {
+
+    /** 提交总数 */
+    private int total;
+
+    /** 成功数 */
+    private int successCount;
+
+    /** 失败数 */
+    private int failCount;
+
+    /** 逐条结果 */
+    private List<LiveCouponBatchVerifyItemVo> items = new ArrayList<>();
+
+    public void addSuccess(String verifyCode, LiveCouponUserDetailVo data) {
+        items.add(LiveCouponBatchVerifyItemVo.ok(verifyCode, data));
+        successCount++;
+    }
+
+    public void addFail(String verifyCode, String msg) {
+        items.add(LiveCouponBatchVerifyItemVo.fail(verifyCode, msg));
+        failCount++;
+    }
+}

+ 46 - 0
fs-service/src/main/resources/mapper/live/LiveCouponUserMapper.xml

@@ -207,4 +207,50 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </delete>
 
+    <select id="selectLiveCouponUserByVerifyCodes" resultMap="LiveCouponUserResult">
+        <include refid="selectLiveCouponUserVo"/>
+        where is_del = 0
+        and verify_code in
+        <foreach collection="verifyCodes" item="code" open="(" separator="," close=")">
+            #{code}
+        </foreach>
+    </select>
+
+    <select id="selectLiveCouponUserDetailByIds" resultMap="LiveCouponUserDetailVoResult">
+        <include refid="selectLiveCouponUserDetailSql"/>
+        where lcu.is_del = 0
+        and lcu.id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <update id="batchExpireCouponUserByIds">
+        update live_coupon_user
+        set status = 2,
+            is_fail = 0,
+            update_time = #{updateTime}
+        where status = 0
+          and (is_del is null or is_del = 0)
+          and id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <update id="batchVerifyCouponUserByIds">
+        update live_coupon_user
+        set status = 1,
+            use_time = #{now},
+            verify_time = #{now},
+            verify_user_id = #{verifyUserId},
+            update_time = #{now}
+        where status = 0
+          and (is_del is null or is_del = 0)
+          and id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
 </mapper>

+ 16 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCouponController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.live.param.CouponPO;
+import com.fs.live.param.LiveCouponBatchVerifyParam;
 import com.fs.live.param.LiveCouponVerifyParam;
 import com.fs.live.service.ILiveCouponService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -76,6 +77,21 @@ public class LiveCouponController extends AppBaseController {
         return liveCouponService.verifyCoupon(param);
     }
 
+    /**
+     * 销售批量核销优惠券(传入多条核销码)
+     */
+    @Login
+    @PostMapping("/batchVerify")
+    public R batchVerify(@RequestBody LiveCouponBatchVerifyParam param) {
+        Long companyUserId = getCompanyUserId();
+        CompanyUser companyUser = companyUserService.selectCompanyUserById(companyUserId);
+        if (companyUser == null) {
+            return R.error("销售不存在");
+        }
+        param.setVerifyUserId(companyUserId);
+        return liveCouponService.batchVerifyCoupon(param);
+    }
+
     /**
      * 当前商品优惠券
      */