Browse Source

溯源码相关逻辑提交

yjwang 6 days ago
parent
commit
28d6d56d76
19 changed files with 853 additions and 51 deletions
  1. 20 3
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreVerifyCodeScrmController.java
  2. 0 1
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  3. 3 0
      fs-common/src/main/java/com/fs/common/constant/FsConstants.java
  4. 1 1
      fs-company-app/src/main/resources/application.yml
  5. 1 1
      fs-redis/src/main/resources/application.yml
  6. 0 3
      fs-service/src/main/java/com/fs/course/dto/FsStoreVerifyCodeDTO.java
  7. 9 0
      fs-service/src/main/java/com/fs/course/dto/WriteOffDTO.java
  8. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreVerifyCodeScrm.java
  9. 17 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreVerifyCodeScrmMapper.java
  10. 11 1
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreVerifyCodeScrmService.java
  11. 45 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  12. 182 9
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  13. 111 22
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreVerifyCodeScrmServiceImpl.java
  14. 1 1
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  15. 27 8
      fs-service/src/main/resources/mapper/hisStore/FsStoreVerifyCodeScrmMapper.xml
  16. 195 0
      fs-store/src/main/java/com/fs/hisStore/controller/store/FsStoreVerifyCodeScrmController.java
  17. 14 1
      fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java
  18. 24 0
      fs-user-app/src/main/java/com/fs/app/redis/RedisConfiguration.java
  19. 180 0
      fs-user-app/src/main/java/com/fs/app/redis/RedisKeyExpirationListener.java

+ 20 - 3
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreVerifyCodeScrmController.java

@@ -8,6 +8,7 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 
 import com.fs.course.dto.FsStoreVerifyCodeDTO;
+import com.fs.course.dto.WriteOffDTO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
 import com.fs.hisStore.service.IFsStoreVerifyCodeScrmService;
@@ -129,9 +130,10 @@ public class FsStoreVerifyCodeScrmController extends BaseController
      * @param file 导入文件
      * @return R
      * **/
-    @Log(title = "发货同步导入", businessType = BusinessType.IMPORT)
+    @Log(title = "核销码批量导入", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('shop:scrm:importExpress')")
     @PostMapping("/importExpress")
-    public R importExpress(@RequestParam("file") MultipartFile file) {
+    public R importExpress(@RequestParam("file") MultipartFile file,@RequestParam("productId") Long productId) {
         // 1. 检查文件是否为空
         if (file.isEmpty()) {
             return R.error("上传的文件不能为空");
@@ -154,7 +156,7 @@ public class FsStoreVerifyCodeScrmController extends BaseController
                     R.error("操作失败,导入数据不能大于200条!");
                 }
                 LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-                return fsStoreVerifyCodeScrmService.importExpress(dtoList,String.valueOf(loginUser.getUserId()));
+                return fsStoreVerifyCodeScrmService.importExpress(dtoList,String.valueOf(loginUser.getUserId()),productId);
             }else {
                 R.error("操作失败,导入数据不能小于1条!");
             }
@@ -164,6 +166,21 @@ public class FsStoreVerifyCodeScrmController extends BaseController
         return R.ok();
     }
 
+    /**
+     * 核销码核销
+     * @return R
+     * **/
+    @PreAuthorize("@ss.hasPermi('shop:scrm:writeOff')")
+    @Log(title = "核销码核销", businessType = BusinessType.IMPORT)
+    @PostMapping("/writeOff")
+    public R writeOff(@RequestBody WriteOffDTO writeOffDTO){
+        if (writeOffDTO.getIds() == null || writeOffDTO.getIds().length == 0) {
+            return R.error("操作失败,核销ID列表不能为空!");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return fsStoreVerifyCodeScrmService.writeOff(writeOffDTO.getIds(),String.valueOf(loginUser.getUserId()));
+    }
+
 
     // 检查文件是否为有效的Excel文件
     private boolean isValidExcelFile(String fileName) {

+ 0 - 1
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -220,7 +220,6 @@ public class MallStoreTask
                                     redisCache.deleteObject(DELIVERY+":"+order.getExtendOrderId());
                                 }
                             }
-
                         }
                     }
 

+ 3 - 0
fs-common/src/main/java/com/fs/common/constant/FsConstants.java

@@ -18,4 +18,7 @@ public interface FsConstants {
     String COMPANY_MONEY_LOCK = "company_money_lock:";
     // 看客统计  按公司分组 按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;
     String WATCH_COURSE_STATISTICS_GROUP_COMPANY = "watch_course_statistics:group_company:";
+
+    //商城订单过期
+    String REDIS_ORDER_UNPAY = "order:unpay:";
 }

+ 1 - 1
fs-company-app/src/main/resources/application.yml

@@ -6,4 +6,4 @@ server:
 spring:
   profiles:
 #    active: druid-fcky-test
-    active: dev-jnlzjk
+    active: dev-yjb

+ 1 - 1
fs-redis/src/main/resources/application.yml

@@ -4,7 +4,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
+    active: dev-yjb
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz

+ 0 - 3
fs-service/src/main/java/com/fs/course/dto/FsStoreVerifyCodeDTO.java

@@ -11,7 +11,4 @@ public class FsStoreVerifyCodeDTO {
      * **/
     @Excel(name = "溯源码(必填)",width = 20,sort = 1)
     private String verifyCode;
-
-    @Excel(name = "商品ID(必填)",width = 20,sort = 1)
-    private Long productId;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/course/dto/WriteOffDTO.java

@@ -0,0 +1,9 @@
+package com.fs.course.dto;
+
+import lombok.Data;
+
+@Data
+public class WriteOffDTO {
+    //核销IDs
+    private Long[] ids;
+}

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreVerifyCodeScrm.java

@@ -43,5 +43,17 @@ public class FsStoreVerifyCodeScrm extends BaseEntity{
     @Excel(name = "是否删除", readConverterExp = "0=未删除、1已删除")
     private Long isDel;
 
+    //订单id
+    private Long orderId;
+
+    //订单详情id
+    private Long orderItemId;
+
+    //门店id
+    private Long storeId;
+
+    //是否回收(0否1是)
+    private Long isRecycle;
+
 
 }

+ 17 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreVerifyCodeScrmMapper.java

@@ -4,6 +4,7 @@ package com.fs.hisStore.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
 
@@ -67,4 +68,20 @@ public interface FsStoreVerifyCodeScrmMapper extends BaseMapper<FsStoreVerifyCod
      * @param list 数据集合
      * **/
     void batchInsertVerifyCode(@Param("list") List<FsStoreVerifyCodeScrm> list);
+
+    /**
+     * 查询锁定未出库溯源码行锁
+     * @param productId 商品ID
+     * @param count 需要数量
+     * @return 锁定的溯源码列表
+     */
+    @Select("SELECT * FROM fs_store_verify_code_scrm " +
+            "WHERE product_id = #{productId} " +
+            "AND outbound_status = 0 " +
+            "AND is_del = 0 " +
+            "ORDER BY create_time ASC " +
+            "LIMIT #{count} FOR UPDATE SKIP LOCKED")
+    List<FsStoreVerifyCodeScrm> selectUnOutboundVerifyCodesWithLock(
+            @Param("productId") Long productId,
+            @Param("count") Integer count);
 }

+ 11 - 1
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreVerifyCodeScrmService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.course.dto.FsStoreVerifyCodeDTO;
 import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
+import org.springframework.web.bind.annotation.PathVariable;
 
 /**
  * 核销码Service接口
@@ -65,7 +66,16 @@ public interface IFsStoreVerifyCodeScrmService extends IService<FsStoreVerifyCod
      * 批量导入
      * @param verifyCodeDTOS
      * @param userId 用户ID
+     * @param productId 商品ID
      * @return R
      * **/
-    R importExpress(List<FsStoreVerifyCodeDTO> verifyCodeDTOS,String userId);
+    R importExpress(List<FsStoreVerifyCodeDTO> verifyCodeDTOS,String userId,Long productId);
+
+    /**
+     * 核销
+     * @param ids 核销码ID
+     * @param userId 用户id
+     * @return R
+     * **/
+    R writeOff(Long[] ids,String userId);
 }

+ 45 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -7,6 +7,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
@@ -85,6 +86,7 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * 售后记录Service业务层处理
@@ -198,6 +200,12 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     @Autowired
     private CloudHostProper cloudHostProper;
 
+    @Autowired
+    private FsStoreVerifyCodeScrmMapper verifyCodeScrmMapper;
+
+    @Autowired
+    private IFsStoreVerifyCodeScrmService verifyCodeScrmService;
+
     /**
      * 查询售后记录
      *
@@ -344,6 +352,25 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         order.setRefundReasonWapExplain(storeAfterSalesParam.getExplains());
         order.setRefundReasonTime(new Date());
         orderService.updateFsStoreOrder(order);
+
+        //判断发货类型
+        if(orderStatus == 2 || orderStatus == 3) {
+            List<FsStoreOrderItemVO> scrmList = fsStoreOrderItemMapper.selectMyFsStoreOrderItemListByOrderId(order.getId());
+            if (!scrmList.isEmpty()) {
+                List<Long> orderItemIds = scrmList.stream().map(FsStoreOrderItemVO::getItemId).collect(Collectors.toList());
+                //获取溯源码,进行回退
+                List<FsStoreVerifyCodeScrm> verifyCodes = verifyCodeScrmMapper.selectList(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getOrderId, order.getId()).in(FsStoreVerifyCodeScrm::getOrderItemId, orderItemIds).eq(FsStoreVerifyCodeScrm::getIsDel, "0"));
+                if (!verifyCodes.isEmpty()) {
+                    verifyCodes.forEach(v -> {
+                        v.setIsRecycle(0L);
+                    });
+                    //批量更新数据
+                    verifyCodeScrmService.updateBatchById(verifyCodes);
+                }
+
+            }
+        }
+
         //生成售后订单
         FsStoreAfterSalesScrm storeAfterSales = new FsStoreAfterSalesScrm();
         storeAfterSales.setOrderCode(storeAfterSalesParam.getOrderCode());
@@ -812,6 +839,24 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
                             payment.setStatus(-1);
                             payment.setRefundTime(new Date());
                             paymentService.updateFsStorePayment(payment);
+
+                            //溯源码状态更改
+                            //获取订单下的详情信息
+                            List<FsStoreOrderItemVO> scrmList = fsStoreOrderItemMapper.selectMyFsStoreOrderItemListByOrderId(order.getId());
+                            if (!scrmList.isEmpty()) {
+                                List<Long> orderItemIds = scrmList.stream().map(FsStoreOrderItemVO::getItemId).collect(Collectors.toList());
+                                //获取溯源码,进行回退
+                                List<FsStoreVerifyCodeScrm> verifyCodes = verifyCodeScrmMapper.selectList(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getOrderId, order.getId()).in(FsStoreVerifyCodeScrm::getOrderItemId, orderItemIds).eq(FsStoreVerifyCodeScrm::getIsRecycle,1L).eq(FsStoreVerifyCodeScrm::getIsDel, "0"));
+                                if (!verifyCodes.isEmpty()) {
+                                    verifyCodes.forEach(v -> {//回收可回退的
+                                        v.setVerifyStatus(0L);
+                                        v.setOutboundStatus(0L);
+                                    });
+                                    //批量更新数据
+                                    verifyCodeScrmService.updateBatchById(verifyCodes);
+                                }
+                            }
+
                         }else {
                             TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                             return R.error(refund.getResp_desc());

+ 182 - 9
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -371,6 +371,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private FsStoreOrderDfMapper fsStoreOrderDfMapper;
 
+    @Autowired
+    private FsStoreVerifyCodeScrmMapper verifyCodeScrmMapper;
+
+    @Autowired
+    private IFsStoreVerifyCodeScrmService verifyCodeScrmService;
+
     @PostConstruct
     public void initErpServiceMap() {
         erpServiceMap = new HashMap<>();
@@ -384,6 +390,14 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private Hospital580PrescriptionScrmMapper prescriptionScrmMapper;
 
+    //溯源码
+    @Autowired
+    private IFsStoreVerifyCodeScrmService verifyCodeService;
+
+    @Autowired
+    private FsStoreVerifyCodeScrmMapper verifyCodeMapper;
+
+
     /**
      * 查询订单
      *
@@ -434,6 +448,24 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             log.error("修改商城订单地址推送到聚水潭ERP失败,orderId: {}", fsStoreOrder.getId(), e);
         }
 
+        //判断发货类型
+        if(fsStoreOrder.getStatus() == 2 || fsStoreOrder.getStatus() == 3) {//待收货
+            List<FsStoreOrderItemVO> scrmList = fsStoreOrderItemMapper.selectMyFsStoreOrderItemListByOrderId(fsStoreOrder.getId());
+            if (!scrmList.isEmpty()) {
+                List<Long> orderItemIds = scrmList.stream().map(FsStoreOrderItemVO::getItemId).collect(Collectors.toList());
+                //获取溯源码,进行回退
+                List<FsStoreVerifyCodeScrm> verifyCodes = verifyCodeScrmMapper.selectList(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getOrderId, fsStoreOrder.getId()).in(FsStoreVerifyCodeScrm::getOrderItemId, orderItemIds).eq(FsStoreVerifyCodeScrm::getIsDel, "0"));
+                if (!verifyCodes.isEmpty()) {
+                    verifyCodes.forEach(v -> {
+                        v.setIsRecycle(0L);
+                    });
+                    //批量更新数据
+                    verifyCodeScrmService.updateBatchById(verifyCodes);
+                }
+
+            }
+        }
+
         return fsStoreOrderMapper.updateFsStoreOrder(fsStoreOrder);
     }
 
@@ -727,7 +759,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class, timeout = 20)
     public R createOrder(long userId, FsStoreOrderCreateParam param) {
         FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
         BeanUtils.copyProperties(param, computedParam);
@@ -957,6 +989,34 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     item.setIsPrescribe(1);
                 }
                 fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+
+                // ===================== 溯源码核心逻辑 ==================
+                Integer buyCount = vo.getCartNum();
+                Long productId = vo.getProductId();
+                String productName = vo.getProductName();
+
+                if (buyCount <= 0) {
+                    throw new ServiceException("商品[" + productName + "]购买数量不能为0");
+                }
+
+                //查询并锁定未出库溯源码带行锁
+                List<FsStoreVerifyCodeScrm> availableVerifyCodes = verifyCodeMapper.selectUnOutboundVerifyCodesWithLock(productId, buyCount);
+                if (availableVerifyCodes == null || availableVerifyCodes.size() < buyCount) {
+                    String errorMsg = String.format("商品[%s]溯源码库存不足,需%s个,仅剩余%s个",
+                            productName, buyCount, (availableVerifyCodes == null ? 0 : availableVerifyCodes.size()));
+                    throw new ServiceException(errorMsg);
+                }
+
+                for (FsStoreVerifyCodeScrm code : availableVerifyCodes) {
+                    code.setOutboundStatus(1L);
+                    code.setOrderId(storeOrder.getId());
+                    code.setOrderItemId(item.getItemId());
+                    code.setUpdateTime(new Date());
+                }
+                if (!availableVerifyCodes.isEmpty()) {
+                    verifyCodeService.updateBatchById(availableVerifyCodes);
+                }
+                // ===================== 溯源码逻辑END =====================
                 listOrderItem.add(item);
             }
             if (listOrderItem.size() > 0) {
@@ -982,9 +1042,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     StoreConstants.REDIS_ORDER_OUTTIME_UNPAY, storeOrder.getId()));
 
             if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药72小时
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                }
             } else {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药
+                    if(config == null){
+                        config = new StoreConfig();
+                    }
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                }
             }
             //添加支付到期时间
             Calendar calendar = Calendar.getInstance();
@@ -1052,6 +1125,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         Long totalNum = 0l;
         BigDecimal totalPrice = new BigDecimal(0);
         BigDecimal totalCostPrice = new BigDecimal("0");
+        Map<Long,String> productMap = new HashMap<>();
         for (FsPrescribeDrug drug : drugs) {
             totalPrice = totalPrice.add(drug.getDrugPrice().multiply(BigDecimal.valueOf(drug.getDrugNum())));
             totalNum = totalNum + drug.getDrugNum();
@@ -1089,6 +1163,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             item.setIsPrescribe(1);
             item.setJsonInfo(JSON.toJSONString(dto));
             items.add(item);
+            productMap.put(drug.getProductId(),drug.getDrugName());
         }
         //中药总价处理
         if (prescribe.getPrescribeType().equals(2)) {
@@ -1109,6 +1184,34 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             for (FsStoreOrderItemScrm item : items) {
                 item.setOrderId(order.getId());
                 fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+
+                // ===================== 溯源码核心逻辑 ==================
+                Integer buyCount = item.getNum();
+                Long productId = item.getProductId();
+                String productName = productMap.get(productId) == null ? "商品" : productMap.get(productId);
+
+                if (buyCount <= 0) {
+                    throw new ServiceException("商品[" + productName + "]购买数量不能为0");
+                }
+
+                //查询并锁定未出库溯源码带行锁
+                List<FsStoreVerifyCodeScrm> availableVerifyCodes = verifyCodeMapper.selectUnOutboundVerifyCodesWithLock(productId, buyCount);
+                if (availableVerifyCodes == null || availableVerifyCodes.size() < buyCount) {
+                    String errorMsg = String.format("商品[%s]溯源码库存不足",
+                            productName, buyCount, (availableVerifyCodes == null ? 0 : availableVerifyCodes.size()));
+                    throw new ServiceException(errorMsg);
+                }
+
+                for (FsStoreVerifyCodeScrm code : availableVerifyCodes) {
+                    code.setOutboundStatus(1L);
+                    code.setOrderId(order.getId());
+                    code.setOrderItemId(item.getItemId());
+                    code.setUpdateTime(new Date());
+                }
+                if (!availableVerifyCodes.isEmpty()) {
+                    verifyCodeService.updateBatchById(availableVerifyCodes);
+                }
+                // ===================== 溯源码逻辑END =====================
             }
             fsStoreOrderLogsService.create(order.getId(), FsStoreOrderLogEnum.CREATE_ORDER.getValue(),
                     FsStoreOrderLogEnum.CREATE_ORDER.getDesc());
@@ -1277,6 +1380,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             order.setDeliveryId(deliveryId);
             order.setDeliverySendTime(new Date());
 
+            //溯源码
+            List<FsStoreOrderItemVO> scrmList = fsStoreOrderItemMapper.selectMyFsStoreOrderItemListByOrderId(order.getId());
+            if (!scrmList.isEmpty()) {
+                List<Long> orderItemIds = scrmList.stream().map(FsStoreOrderItemVO::getItemId).collect(Collectors.toList());
+                //获取溯源码,进行回退
+                List<FsStoreVerifyCodeScrm> verifyCodes = verifyCodeScrmMapper.selectList(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getOrderId, order.getId()).in(FsStoreVerifyCodeScrm::getOrderItemId, orderItemIds).eq(FsStoreVerifyCodeScrm::getIsDel, "0"));
+                if (!verifyCodes.isEmpty()) {
+                    verifyCodes.forEach(v -> {
+                        v.setIsRecycle(0L);
+                    });
+                    //批量更新数据
+                    verifyCodeScrmService.updateBatchById(verifyCodes);
+                }
+
+            }
+
             fsStoreOrderMapper.updateFsStoreOrder(order);
             orderStatusService.create(order.getId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
                     OrderLogEnum.DELIVERY_GOODS.getDesc());
@@ -1655,9 +1774,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             String json = configService.selectConfigByKey("store.config");
             StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
             if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药72小时
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                }
             } else {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药
+                    if(config == null){
+                        config = new StoreConfig();
+                    }
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                }
             }
             Calendar calendar = Calendar.getInstance();
             calendar.setTime(storeOrder.getCreateTime());
@@ -4809,7 +4941,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class, timeout = 20)
     public R createOrderMultiStore(long userId, FsStoreOrderCreateParam param) {
 
         FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
@@ -4992,6 +5124,34 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     item.setIsPrescribe(1);
                 }
                 fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+
+                // ===================== 溯源码核心逻辑 ==================
+                Integer buyCount = vo.getCartNum();
+                Long productId = vo.getProductId();
+                String productName = vo.getProductName();
+
+                if (buyCount <= 0) {
+                    throw new ServiceException("商品[" + productName + "]购买数量不能为0");
+                }
+
+                //查询并锁定未出库溯源码带行锁
+                List<FsStoreVerifyCodeScrm> availableVerifyCodes = verifyCodeMapper.selectUnOutboundVerifyCodesWithLock(productId, buyCount);
+                if (availableVerifyCodes == null || availableVerifyCodes.size() < buyCount) {
+                    String errorMsg = String.format("商品[%s]溯源码库存不足",
+                            productName, buyCount, (availableVerifyCodes == null ? 0 : availableVerifyCodes.size()));
+                    throw new ServiceException(errorMsg);
+                }
+
+                for (FsStoreVerifyCodeScrm code : availableVerifyCodes) {
+                    code.setOutboundStatus(1L);
+                    code.setOrderId(storeOrder.getId());
+                    code.setOrderItemId(item.getItemId());
+                    code.setUpdateTime(new Date());
+                }
+                if (!availableVerifyCodes.isEmpty()) {
+                    verifyCodeService.updateBatchById(availableVerifyCodes);
+                }
+                // ===================== 溯源码逻辑END =====================
                 listOrderItem.add(item);
             }
             if (listOrderItem.size() > 0) {
@@ -5011,15 +5171,28 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
                     OrderLogEnum.CREATE_ORDER.getDesc());
 
-            //加入redis,24小时自动取消
+            //加入redis
             String redisKey = String.valueOf(StrUtil.format("{}{}",
                     StoreConstants.REDIS_ORDER_OUTTIME_UNPAY, storeOrder.getId()));
             String json = configService.selectConfigByKey("store.config");
             com.fs.store.config.StoreConfig config = JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
             if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药72小时
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                }
             } else {
-                redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                if(storeOrder.getIsPrescribe() != null && storeOrder.getIsPrescribe() == 1){//处方药
+                    if(config == null){
+                        config = new com.fs.store.config.StoreConfig();
+                    }
+                    config.setUnPayTime(4320);
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(),4320, TimeUnit.MINUTES);
+                }else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                }
             }
             //添加支付到期时间
             Calendar calendar = Calendar.getInstance();

+ 111 - 22
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreVerifyCodeScrmServiceImpl.java

@@ -36,12 +36,18 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
     private static final Long NOT_DELETED = 0L;
     private static final Long DELETED = 1L;
     private static final Long STATUS_ENABLE = 1L;
-
+    //出库状态
+    private static final Long OUTBOUND_STATUS_UNDO = 0L;
+    //核销状态
+    private static final Long VERIFY_STATUS_WRITEOFF = 1L;
     private static final int BATCH_SIZE = 500;
 
     @Autowired
     private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
+    @Autowired
+    private IFsStoreVerifyCodeScrmService fsStoreVerifyCodeScrmService;
+
     /**
      * 查询核销码
      *
@@ -77,8 +83,15 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
         if (baseMapper.selectCount(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getVerifyCode, fsStoreVerifyCodeScrm.getVerifyCode())) > 0) {
             throw new ServiceException("操作失败,核销码:" + fsStoreVerifyCodeScrm.getVerifyCode() + "已存在");
         }
+        //获取商品信息
+        FsStoreProductScrm product = fsStoreProductScrmMapper.selectFsStoreProductById(fsStoreVerifyCodeScrm.getProductId());
+        if(product == null){
+            throw new ServiceException("操作失败,商品不存在");
+        }
+
+        fsStoreVerifyCodeScrm.setStoreId(product.getStoreId());//获取店铺id
         fsStoreVerifyCodeScrm.setCreateTime(DateUtils.getNowDate());
-        return baseMapper.insert(fsStoreVerifyCodeScrm);
+        return baseMapper.insertFsStoreVerifyCodeScrm(fsStoreVerifyCodeScrm);
     }
 
     /**
@@ -160,11 +173,12 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
      * 核销码导入实现类
      *
      * @param verifyCodeDTOS 数据
-     * @param userId 用户id
+     * @param userId         用户id
      * @return R
      */
     @Override
-    public R importExpress(List<FsStoreVerifyCodeDTO> verifyCodeDTOS,String userId) {
+    @Transactional(rollbackFor = Exception.class)
+    public R importExpress(List<FsStoreVerifyCodeDTO> verifyCodeDTOS, String userId, Long productId) {
         if (CollectionUtils.isEmpty(verifyCodeDTOS)) {
             return R.ok("导入成功!无待导入数据");
         }
@@ -179,10 +193,10 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
                 .filter(dto -> dto.getVerifyCode() == null || dto.getVerifyCode().trim().isEmpty())
                 .count();
 
-        Long[] productIds = verifyCodeDTOS.stream()
-                .map(FsStoreVerifyCodeDTO::getProductId)
-                .filter(Objects::nonNull)
-                .toArray(Long[]::new);
+//        Long[] productIds = verifyCodeDTOS.stream()
+//                .map(FsStoreVerifyCodeDTO::getProductId)
+//                .filter(Objects::nonNull)
+//                .toArray(Long[]::new);
 
         if (emptyCodeCount > 0) {
             errorMsg.append("发现").append(emptyCodeCount).append("条核销码为空的数据\n");
@@ -205,16 +219,26 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
             }
         }
 
-        //获取商品
-        Map<Long, Long> existProductrMap = new HashMap<>();
-        if (!CollectionUtils.isEmpty(Arrays.asList(productIds))) {
-            List<FsStoreProductScrm> productScrmList = fsStoreProductScrmMapper.selectProductByIds(productIds);
-
-            if (!CollectionUtils.isEmpty(productScrmList)) {
-                existProductrMap = productScrmList.stream()
-                        .collect(Collectors.toMap(FsStoreProductScrm::getProductId, FsStoreProductScrm::getProductId));
+        FsStoreProductScrm productScrm = null;
+        //获取商品信息
+        try {
+            productScrm = fsStoreProductScrmMapper.selectFsStoreProductById(productId);
+            if (productScrm == null) {
+                throw new ServiceException("操作失败,商品信息不存在!");
             }
+        } catch (Exception e) {
+            e.printStackTrace();
         }
+//        //获取商品
+//        Map<Long, Long> existProductrMap = new HashMap<>();
+//        if (!CollectionUtils.isEmpty(Arrays.asList(productIds))) {
+//            List<FsStoreProductScrm> productScrmList = fsStoreProductScrmMapper.selectProductByIds(productIds);
+//
+//            if (!CollectionUtils.isEmpty(productScrmList)) {
+//                existProductrMap = productScrmList.stream()
+//                        .collect(Collectors.toMap(FsStoreProductScrm::getProductId, FsStoreProductScrm::getProductId));
+//            }
+//        }
 
         List<FsStoreVerifyCodeScrm> insertList = new ArrayList<>();
         for (FsStoreVerifyCodeDTO dto : verifyCodeDTOS) {
@@ -227,17 +251,14 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
             // 检查是否重复
             if (existVerifyCodeMap.containsKey(verifyCode)) {
                 errorMsg.append("核销码:").append(verifyCode)
-                        .append("(商品ID:").append(dto.getProductId()).append(")已存在,重复新增\n");
-                continue;
-            } else if (!existProductrMap.containsKey(dto.getProductId())) {
-                errorMsg.append("核销码:").append(verifyCode)
-                        .append("绑定(商品ID:不存在!");
+                        .append("(商品:").append(productScrm.getProductName()).append(")已存在,重复新增\n");
                 continue;
             }
 
             FsStoreVerifyCodeScrm entity = new FsStoreVerifyCodeScrm();
             entity.setVerifyCode(verifyCode);
-            entity.setProductId(dto.getProductId());
+            entity.setProductId(productId);
+            entity.setStoreId(productScrm.getStoreId());
             entity.setOutboundStatus(0L);
             entity.setVerifyStatus(0L);
             entity.setStatus(1L);
@@ -269,4 +290,72 @@ public class FsStoreVerifyCodeScrmServiceImpl extends ServiceImpl<FsStoreVerifyC
                 : "导入成功!共导入" + insertList.size() + "条核销码";
         return R.ok(resultMsg);
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R writeOff(Long[] ids, String userId) {
+        if (userId == null || userId.trim().isEmpty()) {
+            log.warn("溯源码核销失败:操作人ID为空,核销ids={}", Arrays.toString(ids));
+            return R.error("操作失败,操作人ID不能为空!");
+        }
+
+        LambdaQueryWrapper<FsStoreVerifyCodeScrm> queryWrapper = new LambdaQueryWrapper<FsStoreVerifyCodeScrm>()
+                .in(FsStoreVerifyCodeScrm::getId, ids)
+                .eq(FsStoreVerifyCodeScrm::getIsDel, NOT_DELETED);
+        List<FsStoreVerifyCodeScrm> dbList = baseMapper.selectList(queryWrapper);
+
+        if (CollectionUtils.isEmpty(dbList)) {
+            log.warn("溯源码核销失败:指定ID的核销数据不存在,核销ids={},userId={}", Arrays.toString(ids), userId);
+            return R.error("操作失败,核销数据不存在!");
+        }
+
+        Set<Long> inputIdSet = new HashSet<>(Arrays.asList(ids));
+        Set<Long> dbIdSet = dbList.stream().map(FsStoreVerifyCodeScrm::getId).collect(Collectors.toSet());
+        inputIdSet.removeAll(dbIdSet);
+        String notExistIds = inputIdSet.isEmpty() ? "" : inputIdSet.stream().map(String::valueOf).collect(Collectors.joining(","));
+
+        List<FsStoreVerifyCodeScrm> hasWriteOffList = dbList.stream()
+                .filter(l -> VERIFY_STATUS_WRITEOFF.equals(l.getVerifyStatus()))
+                .collect(Collectors.toList());
+        if (!CollectionUtils.isEmpty(hasWriteOffList)) {
+            String hasWriteOffCodes = hasWriteOffList.stream()
+                    .filter(l -> l.getVerifyCode() != null && !l.getVerifyCode().trim().isEmpty())
+                    .map(FsStoreVerifyCodeScrm::getVerifyCode)
+                    .collect(Collectors.joining(","));
+            log.warn("溯源码核销失败:部分数据已核销,已核销核销码={},userId={}", hasWriteOffCodes, userId);
+            return R.error("操作失败,核销码【" + hasWriteOffCodes + "】已核销,禁止重复核销!");
+        }
+
+        List<FsStoreVerifyCodeScrm> unOutboundList = dbList.stream()
+                .filter(l -> OUTBOUND_STATUS_UNDO.equals(l.getOutboundStatus()))
+                .filter(l -> l.getVerifyCode() != null && !l.getVerifyCode().trim().isEmpty())
+                .collect(Collectors.toList());
+        if (!CollectionUtils.isEmpty(unOutboundList)) {
+            String unOutboundCodes = unOutboundList.stream()
+                    .map(FsStoreVerifyCodeScrm::getVerifyCode)
+                    .collect(Collectors.joining(","));
+            log.warn("溯源码核销失败:部分核销码未出库,未出库核销码={},userId={}", unOutboundCodes, userId);
+            return R.error("操作失败,核销码【" + unOutboundCodes + "】未出库,禁止核销!");
+        }
+
+        Date updateTime = new Date();
+        dbList.forEach(l -> {
+            l.setVerifyStatus(VERIFY_STATUS_WRITEOFF);
+            l.setUpdateTime(updateTime);
+            l.setUpdateBy(userId.trim());
+        });
+
+        boolean updateResult = this.updateBatchById(dbList);
+        if (!updateResult) {
+            log.error("溯源码核销失败:批量更新数据库失败,核销ids={},userId={}", Arrays.toString(ids), userId);
+            throw new RuntimeException("核销数据更新失败,请重试!");
+        }
+
+        R successR = R.ok("核销成功,共处理" + dbList.size() + "条数据");
+        if (!notExistIds.isEmpty()) {
+            successR.put("notExistIds", notExistIds);
+            log.warn("溯源码核销提示:部分ID不存在,不存在的ids={},userId={}", notExistIds, userId);
+        }
+        return successR;
+    }
 }

+ 1 - 1
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -872,7 +872,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <sql id="baseSql">
        DISTINCT p.product_id, p.image,p.video, p.slider_image, p.product_name, p.product_info, p.keyword, p.bar_code,
-       p.cate_id, p.price, p.vip_price, p.ot_price, p.postage, p.unit_name, p.sort, p.sales, p.stock,
+       p.cate_id, p.price, p.vip_price, p.ot_price, p.postage, p.unit_name, p.sort, ( SELECT COUNT(*) FROM fs_store_verify_code_scrm vc WHERE vc.product_id = p.product_id AND vc.outbound_status = 0 AND is_del = 0) AS stock,( SELECT COUNT(*) FROM fs_store_verify_code_scrm vc WHERE vc.product_id = p.product_id AND vc.outbound_status = 1 AND is_del = 0) AS sales,
        p.is_hot, p.is_benefit, p.is_best, p.is_new, p.description, p.create_time, p.update_time, p.is_postage,
        p.is_del, p.give_integral, p.cost, p.is_good, p.browse, p.code_path, p.temp_id, p.spec_type, p.is_integral,
        p.integral, p.product_type, p.prescribe_code, p.prescribe_spec, p.prescribe_factory, p.prescribe_name,

+ 27 - 8
fs-service/src/main/resources/mapper/hisStore/FsStoreVerifyCodeScrmMapper.xml

@@ -14,22 +14,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="isDel"    column="is_del"    />
         <result property="createTime"    column="create_time"    />
         <result property="updateTime"    column="update_time"    />
+        <result property="orderId"    column="order_id"    />
+        <result property="orderItemId"    column="order_item_id"    />
+        <result property="storeId"    column="store_id"    />
+        <result property="isRecycle"    column="is_recycle"    />
     </resultMap>
 
     <sql id="selectFsStoreVerifyCodeScrmVo">
-        select id, verify_code, product_id, outbound_status, verify_status, status, is_del, create_time, update_time from fs_store_verify_code_scrm
+        select id, verify_code, product_id, outbound_status, verify_status, status, is_del, create_time, update_time,order_id,order_item_id,store_id,is_recycle from fs_store_verify_code_scrm
     </sql>
 
     <select id="selectFsStoreVerifyCodeScrmList" parameterType="FsStoreVerifyCodeScrm" resultMap="FsStoreVerifyCodeScrmResult">
         <include refid="selectFsStoreVerifyCodeScrmVo"/>
         <where>  
-            <if test="verifyCode != null  and verifyCode != ''"> and verify_code = #{verifyCode}</if>
+            <if test="verifyCode != null  and verifyCode != ''"> and verify_code Like  concat('%',#{verifyCode},'%')</if>
             <if test="productId != null  and productId != ''"> and product_id = #{productId}</if>
             <if test="outboundStatus != null "> and outbound_status = #{outboundStatus}</if>
             <if test="verifyStatus != null "> and verify_status = #{verifyStatus}</if>
             <if test="status != null "> and status = #{status}</if>
             <if test="isDel != null "> and is_del = #{isDel}</if>
+            <if test="storeId != null "> and store_id = #{storeId}</if>
         </where>
+        order by create_time desc
     </select>
     
     <select id="selectFsStoreVerifyCodeScrmById" parameterType="String" resultMap="FsStoreVerifyCodeScrmResult">
@@ -37,7 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where id = #{id}
     </select>
         
-    <insert id="insertFsStoreVerifyCodeScrm" parameterType="FsStoreVerifyCodeScrm" useGeneratedKeys="true" keyProperty="id">
+    <insert id="insertFsStoreVerifyCodeScrm" parameterType="FsStoreVerifyCodeScrm">
         insert into fs_store_verify_code_scrm
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="verifyCode != null and verifyCode != ''">verify_code,</if>
@@ -48,6 +54,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null">is_del,</if>
             <if test="createTime != null">create_time,</if>
             <if test="updateTime != null">update_time,</if>
+            <if test="orderId != null">order_id,</if>
+            <if test="orderItemId != null">order_item_id,</if>
+            <if test="storeId != null">store_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="verifyCode != null and verifyCode != ''">#{verifyCode},</if>
@@ -56,8 +65,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="verifyStatus != null">#{verifyStatus},</if>
             <if test="status != null">#{status},</if>
             <if test="isDel != null">#{isDel},</if>
-            <if test="createTime != null">#{createTime},</if>
-            <if test="updateTime != null">#{updateTime},</if>
+            <if test="createTime != null">#{createTime} ,</if>
+            <if test="updateTime != null">#{updateTime} ,</if>
+            <if test="orderId != null">#{orderId} ,</if>
+            <if test="orderItemId != null">#{orderItemId} ,</if>
+            <if test="storeId != null">#{storeId} ,</if>
          </trim>
     </insert>
 
@@ -72,7 +84,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null">is_del = #{isDel},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="updateTime != null">update_time = #{updateTime},</if>
-        </trim>
+            <if test="orderId != null">order_id = #{orderId} ,</if>
+            <if test="orderItemId != null">order_item_id = #{orderItemId} ,</if>
+            <if test="storeId != null">store_id = #{storeId} ,</if>
+            <if test="isRecycle != null">is_recycle = #{isRecycle} ,
+            </if>
+            </trim>
         where id = #{id}
     </update>
 
@@ -98,7 +115,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         create_time,
         update_time,
         create_by,
-        update_by
+        update_by,
+        store_id
         ) VALUES
         <foreach collection="list" item="item" separator=",">
             (
@@ -111,7 +129,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{item.createTime},
             #{item.updateTime},
             #{item.createBy},
-            #{item.updateBy}
+            #{item.updateBy},
+            #{item.storeId}
             )
         </foreach>
     </insert>

+ 195 - 0
fs-store/src/main/java/com/fs/hisStore/controller/store/FsStoreVerifyCodeScrmController.java

@@ -0,0 +1,195 @@
+package com.fs.hisStore.controller.store;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.dto.FsStoreVerifyCodeDTO;
+import com.fs.course.dto.WriteOffDTO;
+import com.fs.framework.service.TokenServiceScrm;
+import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
+import com.fs.hisStore.domain.StoreLoginUserScrm;
+import com.fs.hisStore.service.IFsStoreVerifyCodeScrmService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 核销码Controller
+ * 
+ * @author fs
+ * @date 2025-11-27
+ */
+@RestController
+@RequestMapping("/shop/scrm")
+public class FsStoreVerifyCodeScrmController extends BaseController
+{
+    @Autowired
+    private IFsStoreVerifyCodeScrmService fsStoreVerifyCodeScrmService;
+
+    @Autowired
+    private TokenServiceScrm tokenService;
+
+
+    // 允许的文件扩展名
+    private static final String[] ALLOWED_EXCEL_EXTENSIONS = {".xlsx", ".xls"};
+
+    // 最大文件大小(5MB)
+    private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+
+
+    /**
+     * 查询核销码列表
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreVerifyCodeScrm fsStoreVerifyCodeScrm)
+    {
+        startPage();
+        StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsStoreVerifyCodeScrm.setCreateBy(loginUser.getStoreId().toString());
+        List<FsStoreVerifyCodeScrm> list = fsStoreVerifyCodeScrmService.selectFsStoreVerifyCodeScrmList(fsStoreVerifyCodeScrm);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出核销码列表
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:export')")
+    @Log(title = "核销码", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreVerifyCodeScrm fsStoreVerifyCodeScrm)
+    {
+        List<FsStoreVerifyCodeScrm> list = fsStoreVerifyCodeScrmService.selectFsStoreVerifyCodeScrmList(fsStoreVerifyCodeScrm);
+        ExcelUtil<FsStoreVerifyCodeScrm> util = new ExcelUtil<FsStoreVerifyCodeScrm>(FsStoreVerifyCodeScrm.class);
+        return util.exportExcel(list, "核销码数据");
+    }
+
+    /**
+     * 获取核销码详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") String id)
+    {
+        return AjaxResult.success(fsStoreVerifyCodeScrmService.selectFsStoreVerifyCodeScrmById(id));
+    }
+
+    /**
+     * 新增核销码
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:add')")
+    @Log(title = "核销码", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreVerifyCodeScrm fsStoreVerifyCodeScrm)
+    {
+        StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsStoreVerifyCodeScrm.setCreateBy(loginUser.getStoreId().toString());
+        return toAjax(fsStoreVerifyCodeScrmService.insertFsStoreVerifyCodeScrm(fsStoreVerifyCodeScrm));
+    }
+
+    /**
+     * 修改核销码
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:edit')")
+    @Log(title = "核销码", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreVerifyCodeScrm fsStoreVerifyCodeScrm)
+    {
+        StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsStoreVerifyCodeScrm.setCreateBy(loginUser.getStoreId().toString());
+        return toAjax(fsStoreVerifyCodeScrmService.updateFsStoreVerifyCodeScrm(fsStoreVerifyCodeScrm));
+    }
+
+    /**
+     * 删除核销码
+     */
+    @PreAuthorize("@ss.hasPermi('shop:scrm:remove')")
+    @Log(title = "核销码", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(fsStoreVerifyCodeScrmService.deleteFsStoreVerifyCodeScrmByIds(ids,loginUser.getStoreId()));
+    }
+
+    @GetMapping("/downloadTemplate")
+    public AjaxResult downloadTemplate() {
+        ExcelUtil<FsStoreVerifyCodeDTO> util = new ExcelUtil<>(FsStoreVerifyCodeDTO.class);
+        return util.importTemplateExcel("溯源码那导入模板");
+    }
+
+    /**
+     * 溯源码导入
+     * @param file 导入文件
+     * @return R
+     * **/
+    @Log(title = "核销码批量导入", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('shop:scrm:importExpress')")
+    @PostMapping("/importExpress")
+    public R importExpress(@RequestParam("file") MultipartFile file,@RequestParam("productId") Long productId) {
+        // 1. 检查文件是否为空
+        if (file.isEmpty()) {
+            return R.error("上传的文件不能为空");
+        }
+        // 2. 检查文件大小
+        if (file.getSize() > MAX_FILE_SIZE) {
+            return R.error("文件大小不能超过5MB");
+        }
+        // 3. 检查文件扩展名
+        String fileName = file.getOriginalFilename();
+        if (fileName == null || !isValidExcelFile(fileName)) {
+            return R.error("请上传Excel文件(.xlsx或.xls格式)");
+        }
+
+        ExcelUtil<FsStoreVerifyCodeDTO> util=new ExcelUtil<>(FsStoreVerifyCodeDTO.class);
+        try {
+            List<FsStoreVerifyCodeDTO> dtoList = util.importExcel(file.getInputStream());
+            if(!dtoList.isEmpty()){
+                if(dtoList.size() > 200){
+                    R.error("操作失败,导入数据不能大于200条!");
+                }
+                StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+                return fsStoreVerifyCodeScrmService.importExpress(dtoList,loginUser.getStoreId().toString(),productId);
+            }else {
+                R.error("操作失败,导入数据不能小于1条!");
+            }
+        }catch (Exception e){
+            e.getStackTrace();
+        }
+        return R.ok();
+    }
+
+    /**
+     * 核销码核销
+     * @return R
+     * **/
+    @PreAuthorize("@ss.hasPermi('shop:scrm:writeOff')")
+    @Log(title = "核销码核销", businessType = BusinessType.IMPORT)
+    @PostMapping("/writeOff")
+    public R writeOff(@RequestBody WriteOffDTO writeOffDTO){
+        if (writeOffDTO.getIds() == null || writeOffDTO.getIds().length == 0) {
+            return R.error("操作失败,核销ID列表不能为空!");
+        }
+        StoreLoginUserScrm loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return fsStoreVerifyCodeScrmService.writeOff(writeOffDTO.getIds(),String.valueOf(loginUser.getStoreId()));
+    }
+
+
+    // 检查文件是否为有效的Excel文件
+    private boolean isValidExcelFile(String fileName) {
+        for (String ext : ALLOWED_EXCEL_EXTENSIONS) {
+            if (fileName.toLowerCase().endsWith(ext)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 14 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -2,6 +2,7 @@ package com.fs.app.controller.store;
 
 
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.common.BeanCopyUtils;
@@ -11,6 +12,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.erp.service.IErpGoodsService;
 import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.hisStore.domain.*;
+import com.fs.hisStore.mapper.FsStoreVerifyCodeScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
@@ -30,6 +32,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletRequest;
 import java.util.Date;
 import java.util.List;
+import java.util.stream.Collectors;
 
 
 @Api("商品中心")
@@ -63,6 +66,9 @@ public class ProductScrmController extends AppBaseController {
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private FsStoreVerifyCodeScrmMapper verifyCodeScrmMapper;
+
     /**
      * 获取用户信息
      * @param storeId
@@ -93,6 +99,9 @@ public class ProductScrmController extends AppBaseController {
             param.setIsDrug(isDrug);
             param.setStoreId(storeId);
             List<FsStoreProductCategoryScrm> list=categoryService.selectFsStoreProductCategoryListQuery(param);
+            if(param.getIsDrug() != null && param.getIsDrug() == 1){
+                list = list.stream().filter(item -> item.getCateName().equals("甲类非处方") || item.getCateName().equals("乙类非处方")).collect(Collectors.toList());
+            }
             return R.ok().put("data",list);
         } catch (Exception e){
             return R.error("操作异常");
@@ -145,6 +154,10 @@ public class ProductScrmController extends AppBaseController {
         List<FsStoreProductAttrScrm> productAttr=attrService.selectFsStoreProductAttrByProductId(product.getProductId());
         List<FsStoreProductAttrValueScrm> productValues=attrValueService.selectFsStoreProductAttrValueByProductId(product.getProductId());
 
+        //获取商品溯源码库存
+        int number = verifyCodeScrmMapper.selectCount(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getProductId,product.getProductId()).eq(FsStoreVerifyCodeScrm::getOutboundStatus,0L).eq(FsStoreVerifyCodeScrm::getIsDel,0L));
+
+
 //        for(FsStoreProductAttrValue value:productValues){
 //            if(StringUtils.isEmpty(value.getGroupBarCode())){
 //                //单品
@@ -238,7 +251,7 @@ public class ProductScrmController extends AppBaseController {
                 productRelationService.insertFsStoreProductRelation(relation);
             }
         }
-        return R.ok().put("product",product).put("productAttr",productAttr).put("productValues",productValues).put("store",fsStoreScrm);
+        return R.ok().put("product",product).put("productAttr",productAttr).put("productValues",productValues).put("store",fsStoreScrm).put("num",number);
     }
 
     @Login

+ 24 - 0
fs-user-app/src/main/java/com/fs/app/redis/RedisConfiguration.java

@@ -0,0 +1,24 @@
+package com.fs.app.redis;
+
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+@Configuration
+public class RedisConfiguration {
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Bean
+    public RedisMessageListenerContainer redisMessageListenerContainer() {
+        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
+        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
+        return redisMessageListenerContainer;
+    }
+
+
+
+}

+ 180 - 0
fs-user-app/src/main/java/com/fs/app/redis/RedisKeyExpirationListener.java

@@ -0,0 +1,180 @@
+package com.fs.app.redis;//package com.fs.app.redis;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.constant.FsConstants;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.domain.FsStoreVerifyCodeScrm;
+import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStoreVerifyCodeScrmMapper;
+import com.fs.hisStore.service.IFsStoreVerifyCodeScrmService;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Component
+public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
+
+    private static final Logger log = LoggerFactory.getLogger(RedisKeyExpirationListener.class);
+
+    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
+        super(listenerContainer);
+    }
+
+//    @Autowired
+//    private IQwUserService qwUserService;
+//    @Autowired
+//    private ConfigUtil configUtil;
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;  // 使用 RedisTemplate 进行分布式锁
+
+    @Autowired
+    private FsStoreOrderScrmMapper orderScrmMapper;
+
+    @Autowired
+    private FsStoreOrderItemScrmMapper orderItemScrmMapper;
+
+    @Autowired
+    private FsStoreVerifyCodeScrmMapper verifyCodeScrmMapper;
+
+    @Autowired
+    private IFsStoreVerifyCodeScrmService verifyCodeScrmService;
+
+//    @Autowired
+//    QwUserMapper qwUserMapper;
+//
+//    @Autowired
+//    QwCompanyMapper qwCompanyMapper;
+//
+//    @Autowired
+//    QwApiService qwApiService;
+
+    /**
+     * 针对redis数据失效事件,进行数据处理
+     *
+     * @param message
+     * @param pattern
+     */
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
+        //过期的key
+        String key = new String(message.getBody(), StandardCharsets.UTF_8);
+
+//        //判断是否是appKey失效
+//        if(key.contains(FsConstants.REDIS_QW_appKey_Active)) {
+//
+//            log.info("监听Qw过期appKey-redis过期:pattern={},channel={},key={}",new String(pattern),channel,key);
+//
+//            String lockKey = "lockAppKey:" + key;  // 分布式锁的 key
+//
+//            // 尝试获取锁,设置 30 秒自动过期,防止死锁
+//            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+//
+//            if (Boolean.TRUE.equals(lockAcquired)) {  // 只有一个实例会成功获取锁
+//                try {
+//                    String[] parts = key.split(":");
+//
+//                    if (parts.length == 3) {
+//                        String corpId = parts[1].trim();
+//                        String qwUserId = parts[2].trim();
+//
+//                        String appKey = qwUserService.selectQwUserByQwUserIdAndCorId(qwUserId, corpId);
+//
+//                        String msg="<font color=\"warning\">您的【云联融智】助手已退出,企业微信【侧边栏】有异常,发送已停止,请注意查看</font>";
+//                        sendQwMsg(appKey,msg);
+//
+//                        log.info("监听Qw过期appKey-redis过期:pattern={},channel={},key={}",new String(pattern),channel,key);
+//                    } else {
+//                        System.out.println("监听appKey失效!");
+//                    }
+//                } finally {
+//                    // 释放锁,避免影响后续任务
+//                    redisTemplate.delete(lockKey);
+//                }
+//            } else {
+//                log.info("另一个实例已经处理了 key={},当前实例跳过", key);
+//            }
+//
+//        }else
+        if (key.contains(FsConstants.REDIS_ORDER_UNPAY)) {//未支付支付订单过期
+            log.info("监听商城过期appKey-redis过期:pattern={},channel={},key={}", new String(pattern), channel, key);
+            String lockKey = "lockStoreAppKey:" + key;  // 分布式锁的 key
+            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+            if (Boolean.TRUE.equals(lockAcquired)) {  // 只有一个实例会成功获取锁
+                try {
+                    String[] parts = key.split(":");
+                    if (parts.length == 3) {
+                        Long orderId = Long.parseLong(parts[parts.length - 1].trim());
+                        if (orderId != null) {
+                            //获取订单信息
+                            FsStoreOrderScrm orderScrm = orderScrmMapper.selectFsStoreOrderById(orderId);
+                            if (orderScrm != null && orderScrm.getStatus() == 0) {
+                                //获取订单下的详情信息
+                                List<FsStoreOrderItemVO> scrmList = orderItemScrmMapper.selectMyFsStoreOrderItemListByOrderId(orderId);
+                                if (!scrmList.isEmpty()) {
+                                    List<Long> orderItemIds = scrmList.stream().map(FsStoreOrderItemVO::getItemId).collect(Collectors.toList());
+                                    //获取溯源码,进行回退
+                                    List<FsStoreVerifyCodeScrm> verifyCodes = verifyCodeScrmMapper.selectList(new LambdaQueryWrapper<FsStoreVerifyCodeScrm>().eq(FsStoreVerifyCodeScrm::getOrderId, orderId).in(FsStoreVerifyCodeScrm::getOrderItemId, orderItemIds).eq(FsStoreVerifyCodeScrm::getIsDel, "0"));
+                                    if(!verifyCodes.isEmpty()){
+                                        verifyCodes.forEach(v->{
+                                            v.setVerifyStatus(0L);
+                                            v.setOutboundStatus(0L);
+                                        });
+                                        //批量更新数据
+                                        verifyCodeScrmService.updateBatchById(verifyCodes);
+                                    }
+                                }
+                                //更新订单状态
+                                FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
+                                updateOrder.setId(orderId);
+                                updateOrder.setStatus(-3);
+                                orderScrmMapper.updateFsStoreOrder(updateOrder);
+                            }
+                        }
+                    } else {
+                        System.out.println("监听appKey失效!");
+                    }
+                } finally {
+                    // 释放锁,避免影响后续任务
+                    redisTemplate.delete(lockKey);
+                }
+            } else {
+                log.info("另一个实例已经处理了 key={},当前实例跳过", key);
+            }
+
+        }
+    }
+
+//    /**
+//     * 给企业微信的应用发送消息
+//     */
+//    private void sendQwMsg(String appKey,String msg){
+//        QwUser qwUserByAppKey = qwUserMapper.selectQwUserByAppKey(appKey);
+//
+//        QwSendMsgParam sendMsgParam = new QwSendMsgParam();
+//        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(qwUserByAppKey.getCorpId());
+//        sendMsgParam.setAgentid(Integer.parseInt(qwCompany.getServerAgentId().trim()));
+//        sendMsgParam.setTouser(qwUserByAppKey.getQwUserId());
+//
+//        QwSendMsgParam.Markdown markdown = new QwSendMsgParam.Markdown();
+//        markdown.setContent(msg);
+//
+//        sendMsgParam.setMarkdown(markdown);
+//        sendMsgParam.setMsgtype("markdown");
+//        qwApiService.sendMsg(sendMsgParam, qwCompany.getCorpId());
+//
+//    }
+}