Forráskód Böngészése

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 5 napja
szülő
commit
7948ff3f74
18 módosított fájl, 661 hozzáadás és 99 törlés
  1. 12 3
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  2. 10 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java
  3. 20 0
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  4. 12 3
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  5. 2 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  6. 114 26
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  7. 1 1
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  8. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductUserEndCategory.java
  9. 18 0
      fs-service/src/main/java/com/fs/hisStore/dto/FsStoreProductSortItemDTO.java
  10. 17 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductUserEndCategoryMapper.java
  11. 4 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreUserEndCategoryScrmService.java
  12. 27 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  13. 3 2
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  14. 65 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java
  15. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java
  16. 4 4
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  17. 118 55
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml
  18. 229 3
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

+ 12 - 3
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -744,11 +744,20 @@ public class FsStoreOrderScrmController extends BaseController {
     public R getExpress(@PathVariable("id") Long id) {
         FsStoreOrderScrm order = fsStoreOrderService.selectFsStoreOrderById(id);
         ExpressInfoDTO expressInfoDTO = null;
+        String lastFourNumber = "";
+
         if (StringUtils.isNotEmpty(order.getDeliveryId())) {
-            String lastFourNumber = "";
             if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
-                lastFourNumber = order.getUserPhone();
-                if (lastFourNumber.length() == 11) {
+                if("恒春来".equals(cloudHostProper.getCompanyName())
+                        && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
+                    if (lastFourNumber.contains("-")) {
+                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                    }else{
+                        lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                    }
+                }
+                // 原逻辑
+                else if ((lastFourNumber = order.getUserPhone()).length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
             }

+ 10 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.dto.FsStoreProductSortItemDTO;
 import com.fs.hisStore.service.IFsStoreUserEndCategoryScrmService;
 import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -55,6 +56,15 @@ public class FsStoreUserEndCategoryScrmController extends BaseController {
         return getDataTable(list);
     }
 
+    /** 保存关联商品在当前页的排序(写入 fs_store_product_user_end_category.sort) */
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:edit')")
+    @Log(title = "用户分端类关联商品排序", businessType = BusinessType.UPDATE)
+    @PutMapping("/products/sort")
+    public AjaxResult saveCategoryProductsSort(@RequestParam Long id,
+                                               @RequestBody List<FsStoreProductSortItemDTO> items) {
+        return toAjax(userEndCategoryService.saveProductsSort(id, items));
+    }
+
     @PreAuthorize("@ss.hasPermi('store:userEndCategory:query')")
     @GetMapping("/{id}")
     public AjaxResult getInfo(@PathVariable Long id) {

+ 20 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -379,6 +379,11 @@ public class FsStoreOrderController extends BaseController
                         vo.setCateName("");
                         vo.setBankTransactionId("");
                     }
+                    vo.setCost(java.math.BigDecimal.ZERO);
+                    vo.setFPrice(java.math.BigDecimal.ZERO);
+                    vo.setBarCode("");
+                    vo.setCateName("");
+                    vo.setBankTransactionId("");
                 }
             }
             ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -409,6 +414,11 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
+                vo.setCost(java.math.BigDecimal.ZERO);
+                vo.setFPrice(java.math.BigDecimal.ZERO);
+                vo.setBarCode("");
+                vo.setCateName("");
+                vo.setBankTransactionId("");
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
@@ -484,6 +494,11 @@ public class FsStoreOrderController extends BaseController
                             vo.setCateName("");
                             vo.setBankTransactionId("");
                         }
+                        vo.setCost(java.math.BigDecimal.ZERO);
+                        vo.setFPrice(java.math.BigDecimal.ZERO);
+                        vo.setBarCode("");
+                        vo.setCateName("");
+                        vo.setBankTransactionId("");
                     }
                 }
                 ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -509,6 +524,11 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
+                vo.setCost(java.math.BigDecimal.ZERO);
+                vo.setFPrice(java.math.BigDecimal.ZERO);
+                vo.setBarCode("");
+                vo.setCateName("");
+                vo.setBankTransactionId("");
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);

+ 12 - 3
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -2,6 +2,7 @@ package com.fs.hisStore.controller;
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
@@ -414,12 +415,20 @@ public class FsStoreOrderScrmController extends BaseController
     {
         FsStoreOrderScrm order=fsStoreOrderService.selectFsStoreOrderById(id);
         ExpressInfoDTO expressInfoDTO=null;
-        if(StringUtils.isNotEmpty(order.getDeliveryId())){
 
+        if (StringUtils.isNotEmpty(order.getDeliveryId())) {
             String lastFourNumber = "";
             if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
-                lastFourNumber = order.getUserPhone();
-                if (lastFourNumber.length() == 11) {
+                if("恒春来".equals(cloudHostProper.getCompanyName())
+                        && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
+                    if (lastFourNumber.contains("-")) {
+                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                    }else{
+                        lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                    }
+                }
+                // 原逻辑
+                else if ((lastFourNumber = order.getUserPhone()).length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
             }

+ 2 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -52,7 +52,8 @@ public class EndNode extends AbstractWorkflowNode {
             Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
             if(Integer.valueOf(0).equals(i)){
                 CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
-                if(robotic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
                     robotic.setId(roboticBusiness.getRoboticId());
                     robotic.setTaskStatus(3);
                     companyVoiceRoboticMapper.updateById(robotic);

+ 114 - 26
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -33,10 +33,8 @@ import com.fs.his.vo.AppWatchLogReportVO;
 import com.fs.his.vo.WatchLogReportVO;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.dto.FsStoreCartDTO;
-import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
-import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
@@ -185,9 +183,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
 
-    @Autowired
-    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
-
     @Autowired
     private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
@@ -1889,29 +1884,10 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             Long redCount = fsCourseRedPacketLogMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId,companyId,companyUserId);
             vo.setRedPacketUserCount(redCount != null ? redCount : 0L);
 
-            // 单品销量统计:从订单明细汇总
+            // 单品销量统计:使用订单主表 itemJson 明细,按 payPrice 分摊(与 GMV 一致,避免逐单查库)
             Map<Long, CourseProductSalesVO> productSalesMap = new HashMap<>();
             for (FsStoreOrderScrm order : paidOrders) {
-                // todo 数据量大的时候需要优化查询 外面批量查询 里面数据过滤
-                List<FsStoreOrderItemVO> items = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
-                if (items == null || items.isEmpty()) continue;
-                long totalNum = order.getTotalNum() != null && order.getTotalNum() > 0 ? order.getTotalNum() : 1;
-//                BigDecimal orderPayPrice = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
-
-                for (FsStoreOrderItemVO item : items) {
-                    FsStoreCartDTO cartDTO = JSONUtil.toBean(item.getJsonInfo(), FsStoreCartDTO.class);
-                    if (item.getProductId() == null) continue;
-                    long itemNum = item.getNum() != null ? item.getNum() : 0;
-                    BigDecimal itemAmount = totalNum > 0 ? cartDTO.getPrice().multiply(BigDecimal.valueOf(itemNum)) : BigDecimal.ZERO;
-                    CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(item.getProductId(), k -> {
-                        CourseProductSalesVO pvo = new CourseProductSalesVO();
-                        pvo.setProductName(cartDTO.getProductName());
-                        return pvo;
-                    });
-
-                    productSales.setSalesCount(productSales.getSalesCount() + itemNum);
-                    productSales.setSalesAmount(productSales.getSalesAmount().add(itemAmount));
-                }
+                accumulateProductSalesFromPaidOrder(productSalesMap, order);
             }
             List<CourseProductSalesVO> productList = new ArrayList<>(productSalesMap.values());
             productList.sort((a, b) -> b.getSalesAmount().compareTo(a.getSalesAmount()));
@@ -2370,6 +2346,118 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             vo.setVideoTitle("");
         }
     }
+
+    /**
+     * 将单笔已支付订单的 payPrice 按明细「单价×数量」占比分摊到各商品;无有效标价时按行数均分。
+     * 明细来自订单主表 {@link FsStoreOrderScrm#getItemJson()}(与行表结构一致:productId、num、jsonInfo 等)。
+     * 每笔订单分摊之和严格等于该笔 payPrice,故多笔订单汇总后各商品 salesAmount 之和等于 GMV。
+     */
+    private void accumulateProductSalesFromPaidOrder(Map<Long, CourseProductSalesVO> productSalesMap, FsStoreOrderScrm order) {
+        String itemJson = order.getItemJson();
+        if (itemJson == null || itemJson.trim().isEmpty()) {
+            return;
+        }
+        JSONArray items;
+        try {
+            items = JSON.parseArray(itemJson);
+        } catch (Exception e) {
+            return;
+        }
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+        BigDecimal orderPay = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+        List<Long> pids = new ArrayList<>();
+        List<Long> nums = new ArrayList<>();
+        List<BigDecimal> raws = new ArrayList<>();
+        List<String> names = new ArrayList<>();
+        for (int j = 0; j < items.size(); j++) {
+            JSONObject o = items.getJSONObject(j);
+            if (o == null) {
+                continue;
+            }
+            Long productId = o.getLong("productId");
+            if (productId == null) {
+                continue;
+            }
+            long itemNum = 0L;
+            if (o.get("num") != null) {
+                try {
+                    itemNum = o.getLongValue("num");
+                } catch (Exception e) {
+                    itemNum = 0L;
+                }
+            }
+            String jsonInfo = o.getString("jsonInfo");
+            FsStoreCartDTO cartDTO = null;
+            try {
+                if (jsonInfo != null && !jsonInfo.isEmpty()) {
+                    cartDTO = JSONUtil.toBean(jsonInfo, FsStoreCartDTO.class);
+                }
+            } catch (Exception ignored) {
+            }
+            BigDecimal unit = (cartDTO != null && cartDTO.getPrice() != null) ? cartDTO.getPrice() : BigDecimal.ZERO;
+            BigDecimal raw = unit.multiply(BigDecimal.valueOf(itemNum));
+            pids.add(productId);
+            nums.add(itemNum);
+            raws.add(raw);
+            names.add(cartDTO != null ? cartDTO.getProductName() : null);
+        }
+        int n = pids.size();
+        if (n == 0) {
+            return;
+        }
+        BigDecimal rawSum = BigDecimal.ZERO;
+        for (BigDecimal r : raws) {
+            rawSum = rawSum.add(r);
+        }
+        int lastPos = -1;
+        for (int i = n - 1; i >= 0; i--) {
+            if (raws.get(i).compareTo(BigDecimal.ZERO) > 0) {
+                lastPos = i;
+                break;
+            }
+        }
+        BigDecimal[] allocs = new BigDecimal[n];
+        Arrays.fill(allocs, BigDecimal.ZERO);
+        if (rawSum.compareTo(BigDecimal.ZERO) > 0 && lastPos >= 0) {
+            BigDecimal used = BigDecimal.ZERO;
+            for (int i = 0; i < n; i++) {
+                BigDecimal raw = raws.get(i);
+                if (raw.compareTo(BigDecimal.ZERO) <= 0) {
+                    allocs[i] = BigDecimal.ZERO;
+                } else if (i == lastPos) {
+                    allocs[i] = orderPay.subtract(used);
+                } else {
+                    allocs[i] = orderPay.multiply(raw).divide(rawSum, 8, RoundingMode.HALF_UP);
+                    used = used.add(allocs[i]);
+                }
+            }
+        } else {
+            BigDecimal used = BigDecimal.ZERO;
+            BigDecimal share = orderPay.divide(BigDecimal.valueOf(n), 8, RoundingMode.HALF_UP);
+            for (int i = 0; i < n; i++) {
+                if (i == n - 1) {
+                    allocs[i] = orderPay.subtract(used);
+                } else {
+                    allocs[i] = share;
+                    used = used.add(share);
+                }
+            }
+        }
+        for (int i = 0; i < n; i++) {
+            Long pid = pids.get(i);
+            final int idx = i;
+            CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(pid, k -> {
+                CourseProductSalesVO pvo = new CourseProductSalesVO();
+                pvo.setProductName(names.get(idx));
+                return pvo;
+            });
+            productSales.setSalesCount(productSales.getSalesCount() + nums.get(i));
+            productSales.setSalesAmount(productSales.getSalesAmount().add(allocs[i]));
+        }
+    }
+
     /**
      * 从 Map 中安全获取 Long 值,兼容 MyBatis 返回的驼峰/小写键名
      */

+ 1 - 1
fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java

@@ -80,7 +80,7 @@ public class CrmCustomerAiTagUtil {
 
         // 3. 调用AI服务
         R aiResponse = callAiService(requestParam, logId,APP_KEY);
-        System.out.println(aiResponse);
+//        System.out.println(aiResponse);
         // 4. 解析响应并保存
         List<CrmCustomerAiTagVo> results = parseAiResponse(aiResponse, customerId);
 

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductUserEndCategory.java

@@ -17,4 +17,7 @@ public class FsStoreProductUserEndCategory implements Serializable {
 
     /** 用户分端类ID */
     private Long userEndCategoryId;
+
+    /** 在该用户分端类下的排序(仅关联表,非商品表 sort) */
+    private Long sort;
 }

+ 18 - 0
fs-service/src/main/java/com/fs/hisStore/dto/FsStoreProductSortItemDTO.java

@@ -0,0 +1,18 @@
+package com.fs.hisStore.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 批量更新商品排序项(用户端分类关联商品列表用)
+ */
+@Data
+public class FsStoreProductSortItemDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long productId;
+    /** 排序值,0~9999 */
+    private Long sort;
+}

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

@@ -27,4 +27,21 @@ public interface FsStoreProductUserEndCategoryMapper {
 
     /** 按区域位置查询商品ID:1=金刚区 2=瀑布区,配合 keyword 筛选,支持 storeId */
     List<Long> selectDistinctProductIdsByPosition(@Param("id") Long id,@Param("storeId") Long storeId, @Param("position") Integer position, @Param("keyword") String keyword);
+
+    /** 更新指定分类下某商品的关联排序 */
+    int updateRelSortByCategoryAndProduct(@Param("userEndCategoryId") Long userEndCategoryId,
+                                          @Param("productId") Long productId,
+                                          @Param("sort") long sort);
+
+    /** 指定分类下,批量查询商品与关联 sort(管理端/App 指定分类列表用) */
+    List<FsStoreProductUserEndCategory> selectSortByCategoryAndProductIds(@Param("userEndCategoryId") Long userEndCategoryId,
+                                                                          @Param("productIds") List<Long> productIds);
+
+    /** 按金刚区/瀑布区聚合:同一商品多条关联时取最大 sort(App 未指定分类时列表展示用) */
+    List<FsStoreProductUserEndCategory> selectAggSortByPositionAndProductIds(@Param("storeId") Long storeId,
+                                                                              @Param("position") Integer position,
+                                                                              @Param("productIds") List<Long> productIds);
+
+    /** 全部分类关联聚合:同一商品多条关联时取最大 sort(App「全部」列表展示用) */
+    List<FsStoreProductUserEndCategory> selectAggSortGlobalAndProductIds(@Param("productIds") List<Long> productIds);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreUserEndCategoryScrmService.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.service;
 
 import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.dto.FsStoreProductSortItemDTO;
 import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
 
 import java.util.List;
@@ -26,6 +27,9 @@ public interface IFsStoreUserEndCategoryScrmService {
     /** 按用户端分类ID分页查询关联商品(去重商品ID分页,再查商品简表+标签并组装) */
     List<FsStoreUserEndCategoryProductVO> listProductsByCategoryId(Long categoryId, String keyword);
 
+    /** 批量更新关联排序(写入 fs_store_product_user_end_category.sort,需指定用户分端类 id) */
+    int saveProductsSort(Long userEndCategoryId, List<FsStoreProductSortItemDTO> items);
+
     /** 首页商品列表:id 为空查全部(分页商品ID后查简表+标签),id 不为空按用户端分类查;position=1金刚区/2瀑布区 时按区域查全部;返回 list+total */
     java.util.Map<String, Object> listProductsForApp(Long id, String keyword, Integer pageNum, Integer pageSize, Long storeId, Integer position);
 

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

@@ -11,6 +11,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
@@ -91,6 +92,7 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -155,6 +157,9 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private RedisCache redisCache;
+
     @Autowired
     SysConfigMapper sysConfigMapper;
 
@@ -291,7 +296,29 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     @Transactional
     public R applyForAfterSales(long userId, FsStoreAfterSalesParam storeAfterSalesParam) {
         logger.info("申请退款请求信息:"+JSONUtil.toJsonStr(storeAfterSalesParam));
+        if (StringUtils.isEmpty(storeAfterSalesParam.getOrderCode())) {
+            return R.error("订单号不能为空");
+        }
+        String lockKey = "after_sales:apply:" + storeAfterSalesParam.getOrderCode();
+        String lockVal = IdUtil.fastSimpleUUID();
+        if (!redisCache.setIfAbsent(lockKey, lockVal, 10, TimeUnit.SECONDS)) {
+            logger.warn("申请售后未获取到分布式锁,orderCode={}", storeAfterSalesParam.getOrderCode());
+            return R.error("售后申请处理中,请稍后再试");
+        }
+        try {
+            return doApplyForAfterSales(userId, storeAfterSalesParam);
+        } finally {
+            Object cached = redisCache.getCacheObject(lockKey);
+            if (cached != null && lockVal.equals(String.valueOf(cached))) {
+                redisCache.deleteObject(lockKey);
+            }
+        }
+    }
 
+    /**
+     * 售后申请业务(由 {@link #applyForAfterSales} 在 Redis 分布式锁内调用,按订单号互斥)
+     */
+    private R doApplyForAfterSales(long userId, FsStoreAfterSalesParam storeAfterSalesParam) {
         // 查询配置:是否删除历史售后数据
         try {
             String deleteAfterSalesConfig = configService.selectConfigByKey("delete_after_sales");

+ 3 - 2
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -2929,9 +2929,10 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if("恒春来".equals(cloudHostProper.getCompanyName())
                     && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                 if (lastFourNumber.contains("-")) {
-                    lastFourNumber = lastFourNumber.substring(0, lastFourNumber.indexOf("-"));
+                    lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                }else{
+                    lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
-                lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
             }
             // 原逻辑
             else if ((lastFourNumber = order.getUserPhone()).length() == 11) {

+ 65 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java

@@ -3,7 +3,9 @@ package com.fs.hisStore.service.impl;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.domain.FsStoreProductUserEndCategory;
 import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.dto.FsStoreProductSortItemDTO;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductTagRelationScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductUserEndCategoryMapper;
@@ -15,6 +17,7 @@ import com.github.pagehelper.Page;
 import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -69,13 +72,15 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
             tagVoMap.computeIfAbsent(tn.getProductId(), k -> new ArrayList<>()).add(tn);
         }
         // 转换为标签名称列表,已按sort排序(SQL已排序)
-        Map<Long, List<String>> tagMap = new LinkedHashMap<>();
+               Map<Long, List<String>> tagMap = new LinkedHashMap<>();
         for (Map.Entry<Long, List<FsStoreProductTagNameVO>> entry : tagVoMap.entrySet()) {
             List<String> tagNameList = entry.getValue().stream()
                     .map(FsStoreProductTagNameVO::getTagName)
                     .collect(Collectors.toList());
             tagMap.put(entry.getKey(), tagNameList);
         }
+        Map<Long, Long> relSortMap = toRelationSortMap(
+                productUserEndCategoryMapper.selectSortByCategoryAndProductIds(categoryId, productIds));
         List<FsStoreUserEndCategoryProductVO> result = new ArrayList<>();
         for (Long pid : productIds) {
             FsStoreProductScrm p = productMap.get(pid);
@@ -87,6 +92,7 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
             vo.setPrice(p.getPrice());
             vo.setOtPrice(p.getOtPrice());
             vo.setSales(p.getSales());
+            vo.setSort(relSortMap.getOrDefault(pid, 0L));
             vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
             result.add(vo);
         }
@@ -97,6 +103,29 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
         return pageResult;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int saveProductsSort(Long userEndCategoryId, List<FsStoreProductSortItemDTO> items) {
+        if (userEndCategoryId == null || items == null || items.isEmpty()) {
+            return 0;
+        }
+        int n = 0;
+        for (FsStoreProductSortItemDTO item : items) {
+            if (item == null || item.getProductId() == null) {
+                continue;
+            }
+            long s = item.getSort() == null ? 0L : item.getSort();
+            if (s < 0) {
+                s = 0;
+            }
+            if (s > 9999) {
+                s = 9999;
+            }
+            n += productUserEndCategoryMapper.updateRelSortByCategoryAndProduct(userEndCategoryId, item.getProductId(), s);
+        }
+        return n;
+    }
+
     @Override
     public Map<String, Object> listProductsForApp(Long id, String keyword, Integer pageNum, Integer pageSize, Long storeId, Integer position) {
         Map<String, Object> out = new HashMap<>();
@@ -136,6 +165,7 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
                     .collect(Collectors.toList());
             tagMap.put(entry.getKey(), tagNameList);
         }
+        Map<Long, Long> relSortMap = relationSortMapForApp(id, storeId, position, productIds);
         List<FsStoreUserEndCategoryProductVO> result = new ArrayList<>();
         for (Long pid : productIds) {
             FsStoreProductScrm p = productMap.get(pid);
@@ -147,6 +177,7 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
             vo.setPrice(p.getPrice());
             vo.setOtPrice(p.getOtPrice());
             vo.setSales(p.getSales());
+            vo.setSort(relSortMap.getOrDefault(pid, 0L));
             vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
             vo.setPositiveRating(getFixedPositiveRating(p.getProductId()));
             result.add(vo);
@@ -207,6 +238,39 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
         return mapper.deleteByIds(ids);
     }
 
+    private Map<Long, Long> relationSortMapForApp(Long categoryId, Long storeId, Integer position, List<Long> productIds) {
+        if (productIds == null || productIds.isEmpty()) {
+            return new HashMap<>();
+        }
+        if (position != null && (position == 1 || position == 2)) {
+            if (categoryId != null && categoryId != 0L) {
+                return toRelationSortMap(
+                        productUserEndCategoryMapper.selectSortByCategoryAndProductIds(categoryId, productIds));
+            }
+            return toRelationSortMap(
+                    productUserEndCategoryMapper.selectAggSortByPositionAndProductIds(storeId, position, productIds));
+        }
+        if (categoryId != null && categoryId != 0L) {
+            return toRelationSortMap(
+                    productUserEndCategoryMapper.selectSortByCategoryAndProductIds(categoryId, productIds));
+        }
+        return toRelationSortMap(productUserEndCategoryMapper.selectAggSortGlobalAndProductIds(productIds));
+    }
+
+    private static Map<Long, Long> toRelationSortMap(List<FsStoreProductUserEndCategory> rows) {
+        Map<Long, Long> m = new HashMap<>();
+        if (rows == null) {
+            return m;
+        }
+        for (FsStoreProductUserEndCategory row : rows) {
+            if (row.getProductId() == null) {
+                continue;
+            }
+            m.put(row.getProductId(), row.getSort() != null ? row.getSort() : 0L);
+        }
+        return m;
+    }
+
     /**
      * 获取固定的好评率随机值
      * 先从 Redis 获取,如果不存在则生成随机值并保存到 Redis(永久保存)

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java

@@ -26,6 +26,8 @@ public class FsStoreUserEndCategoryProductVO implements Serializable {
     private BigDecimal positiveRating;
     /** 销量 */
     private Long sales;
+    /** 在用户分端类下的展示排序(fs_store_product_user_end_category.sort;App 聚合场景可能为多条关联中的最大 sort) */
+    private Long sort;
     /** 产品标签名称列表 */
     private List<String> tagList;
     

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

@@ -522,7 +522,7 @@
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and p.is_new=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc limit #{count}
+        and p.is_new=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
     </select>
     <select id="selectFsStoreProductNewQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -537,7 +537,7 @@
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and p.is_new=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc
+        and p.is_new=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
     </select>
     <select id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -549,7 +549,7 @@
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc limit #{count}
+        and  p.is_hot=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
     </select>
     <select id="selectFsStoreProductHotQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -564,7 +564,7 @@
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc
+        and  p.is_hot=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
     </select>
     <select id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p

+ 118 - 55
fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml

@@ -1,55 +1,118 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.fs.hisStore.mapper.FsStoreProductUserEndCategoryMapper">
-
-    <insert id="insertBatch">
-        insert into fs_store_product_user_end_category (product_id, user_end_category_id) values
-        <foreach collection="categoryIds" item="cid" separator=",">(#{productId}, #{cid})</foreach>
-    </insert>
-
-    <delete id="deleteByProductId">
-        delete from fs_store_product_user_end_category where product_id = #{productId}
-    </delete>
-
-    <select id="selectCategoryIdsByProductId" resultType="java.lang.Long">
-        select user_end_category_id from fs_store_product_user_end_category where product_id = #{productId}
-    </select>
-
-    <delete id="deleteByCategoryIds">
-        delete from fs_store_product_user_end_category where user_end_category_id in
-        <foreach collection="categoryIds" item="id" open="(" separator="," close=")">#{id}</foreach>
-    </delete>
-
-    <select id="selectDistinctProductIdsByCategoryId" resultType="java.lang.Long">
-        select distinct a.product_id from fs_store_product_user_end_category a left join fs_store_product_scrm c on a.product_id = c.product_id
-        where a.user_end_category_id = #{categoryId} and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1
-        <if test="keyword != null and keyword != ''">
-            and c.product_name like CONCAT('%', #{keyword}, '%')
-        </if>
-        order by c.sort asc, c.create_time desc, a.product_id
-    </select>
-
-    <select id="selectDistinctProductIds" resultType="java.lang.Long">
-        select distinct a.product_id from fs_store_product_user_end_category  a left join fs_store_product_scrm c on a.product_id = c.product_id
-        where c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1
-        <if test="keyword != null and keyword != ''">
-            and c.product_name like CONCAT('%', #{keyword}, '%')
-        </if>
-        order by c.sort asc, c.create_time desc, a.product_id
-    </select>
-
-    <!-- 按区域位置(1金刚区 2瀑布区)查询商品ID,支持 keyword、storeId -->
-    <select id="selectDistinctProductIdsByPosition" resultType="java.lang.Long">
-        select distinct a.product_id from fs_store_product_user_end_category a
-        left join fs_store_product_scrm c on a.product_id = c.product_id
-        left join fs_store_user_end_category_scrm uec on a.user_end_category_id = uec.id
-        where uec.status = 1 and uec.position = #{position}
-        and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1
-        <if test="id != null and id != 0">and a.user_end_category_id = #{id}</if>
-        <if test="storeId != null">and uec.store_id = #{storeId}</if>
-        <if test="keyword != null and keyword != ''">
-            and c.product_name like CONCAT('%', #{keyword}, '%')
-        </if>
-        order by c.sort asc, c.create_time desc, a.product_id
-    </select>
-</mapper>
+<?xml version="1.0" encoding="UTF-8" ?>

+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductUserEndCategoryMapper">

+

+    <insert id="insertBatch">

+        insert into fs_store_product_user_end_category (product_id, user_end_category_id, sort) values

+        <foreach collection="categoryIds" item="cid" separator=",">(#{productId}, #{cid}, 0)</foreach>

+    </insert>

+

+    <delete id="deleteByProductId">

+        delete from fs_store_product_user_end_category where product_id = #{productId}

+    </delete>

+

+    <select id="selectCategoryIdsByProductId" resultType="java.lang.Long">

+        select user_end_category_id from fs_store_product_user_end_category where product_id = #{productId}

+    </select>

+

+    <delete id="deleteByCategoryIds">

+        delete from fs_store_product_user_end_category where user_end_category_id in

+        <foreach collection="categoryIds" item="id" open="(" separator="," close=")">#{id}</foreach>

+    </delete>

+

+    <!-- 排序使用关联表 a.sort;GROUP BY 避免同一商品多分类时 DISTINCT+ORDER BY 不稳定 -->

+    <select id="selectDistinctProductIdsByCategoryId" resultType="java.lang.Long">

+        select t.product_id from (

+        select a.product_id,

+               min(case when a.sort is null or a.sort = 0 then 1 else 0 end) as o1,

+               max(case when a.sort is null or a.sort = 0 then 0 else a.sort end) as o2,

+               max(c.create_time) as ct

+        from fs_store_product_user_end_category a

+        left join fs_store_product_scrm c on a.product_id = c.product_id

+        where a.user_end_category_id = #{categoryId} and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1

+        <if test="keyword != null and keyword != ''">

+            and c.product_name like CONCAT('%', #{keyword}, '%')

+        </if>

+        group by a.product_id

+        ) t

+        order by t.o1 asc, t.o2 desc, t.ct desc, t.product_id desc

+    </select>

+

+    <select id="selectDistinctProductIds" resultType="java.lang.Long">

+        select t.product_id from (

+        select a.product_id,

+               min(case when a.sort is null or a.sort = 0 then 1 else 0 end) as o1,

+               max(case when a.sort is null or a.sort = 0 then 0 else a.sort end) as o2,

+               max(c.create_time) as ct

+        from fs_store_product_user_end_category  a

+        left join fs_store_product_scrm c on a.product_id = c.product_id

+        where c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1

+        <if test="keyword != null and keyword != ''">

+            and c.product_name like CONCAT('%', #{keyword}, '%')

+        </if>

+        group by a.product_id

+        ) t

+        order by t.o1 asc, t.o2 desc, t.ct desc, t.product_id desc

+    </select>

+

+    <!-- 按区域位置(1金刚区 2瀑布区)查询商品ID,支持 keyword、storeId;排序为关联表 a.sort -->

+    <select id="selectDistinctProductIdsByPosition" resultType="java.lang.Long">

+        select t.product_id from (

+        select a.product_id,

+               min(case when a.sort is null or a.sort = 0 then 1 else 0 end) as o1,

+               max(case when a.sort is null or a.sort = 0 then 0 else a.sort end) as o2,

+               max(c.create_time) as ct

+        from fs_store_product_user_end_category a

+        left join fs_store_product_scrm c on a.product_id = c.product_id

+        left join fs_store_user_end_category_scrm uec on a.user_end_category_id = uec.id

+        where uec.status = 1 and uec.position = #{position}

+        and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1

+        <if test="id != null and id != 0">and a.user_end_category_id = #{id}</if>

+        <if test="storeId != null">and uec.store_id = #{storeId}</if>

+        <if test="keyword != null and keyword != ''">

+            and c.product_name like CONCAT('%', #{keyword}, '%')

+        </if>

+        group by a.product_id

+        ) t

+        order by t.o1 asc, t.o2 desc, t.ct desc, t.product_id desc

+    </select>

+

+    <update id="updateRelSortByCategoryAndProduct">

+        update fs_store_product_user_end_category

+        set sort = #{sort}

+        where user_end_category_id = #{userEndCategoryId} and product_id = #{productId}

+    </update>

+

+    <select id="selectSortByCategoryAndProductIds" resultType="com.fs.hisStore.domain.FsStoreProductUserEndCategory">

+        select product_id, user_end_category_id, sort

+        from fs_store_product_user_end_category

+        where user_end_category_id = #{userEndCategoryId}

+        and product_id in

+        <foreach collection="productIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>

+    </select>

+

+    <select id="selectAggSortByPositionAndProductIds" resultType="com.fs.hisStore.domain.FsStoreProductUserEndCategory">

+        select a.product_id,

+               max(case when a.sort is null or a.sort = 0 then 0 else a.sort end) as sort

+        from fs_store_product_user_end_category a

+        left join fs_store_product_scrm c on a.product_id = c.product_id

+        left join fs_store_user_end_category_scrm uec on a.user_end_category_id = uec.id

+        where uec.status = 1 and uec.position = #{position}

+        and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1

+        <if test="storeId != null">and uec.store_id = #{storeId}</if>

+        and a.product_id in

+        <foreach collection="productIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>

+        group by a.product_id

+    </select>

+

+    <select id="selectAggSortGlobalAndProductIds" resultType="com.fs.hisStore.domain.FsStoreProductUserEndCategory">

+        select a.product_id,

+               max(case when a.sort is null or a.sort = 0 then 0 else a.sort end) as sort

+        from fs_store_product_user_end_category a

+        left join fs_store_product_scrm c on a.product_id = c.product_id

+        where c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1

+        and a.product_id in

+        <foreach collection="productIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>

+        group by a.product_id

+    </select>

+</mapper>


+ 229 - 3
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -5,6 +5,7 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
@@ -20,7 +21,11 @@ import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.company.service.impl.call.node.AiQwAddWxTaskNode;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
 import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedisKeyScanner;
+import com.fs.course.domain.FsCourseLink;
+import com.fs.course.domain.FsCourseRealLink;
+import com.fs.course.mapper.FsCourseLinkMapper;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -33,10 +38,14 @@ import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.qwApi.Result.QwAddContactWayResult;
 import com.fs.qwApi.domain.QwLinkCreateResult;
+import com.fs.qwApi.param.QwAddContactWayParam;
 import com.fs.qwApi.param.QwLinkCreateParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
 import com.fs.wxcid.dto.friend.AddContactParam;
 import com.fs.wxcid.service.FriendService;
@@ -60,6 +69,7 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.*;
@@ -119,6 +129,13 @@ public class WxTaskService {
     private final QwExternalContactMapper qwExternalContactMapper;
     private final CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
+    private  final FsCourseLinkMapper fsCourseLinkMapper;
+    private final ISysConfigService configService;
+
+
+    private static final String REAL_CID_LINK_PREFIX = "/pages_cidAddQw/cidAddQw.html?link=";
+    private static final String REAL_CID_H5LINK_PREFIX = "/pages_cidAddQwH5/cidAddQw.html?link=";
+
     public void addWx(List<Long> accountIdList) {
         log.info("==========执行加微信任务开始==========");
         String json = sysConfigService.selectConfigByKey("wx.config");
@@ -884,6 +901,16 @@ public class WxTaskService {
      */
     private static final int QW_ADD_WX_TYPE_SMS_LINK = 2;
 
+    /**
+     * 企微加微方式:短信-小程序渠道活码-链接
+     */
+    private static final int QW_ADD_WX_TYPE_MINI_LINK = 3;
+
+    /**
+     * 企微加微方式:短信获客链接
+     */
+    private static final int QW_ADD_WX_TYPE_H5_LINK = 4;
+
     /**
      * 默认加微方式 ID
      */
@@ -1003,6 +1030,10 @@ public class WxTaskService {
                 return handleApplyAddWx(qwUser, client, config);
             case QW_ADD_WX_TYPE_SMS_LINK:
                 return handleSmsLinkAddWx(qwUser, client, config);
+            case QW_ADD_WX_TYPE_MINI_LINK:
+                return handleMINILinkAddWx(qwUser, client, config,"小程序-渠道活码生成失败");
+            case QW_ADD_WX_TYPE_H5_LINK:
+                return handleMINILinkAddWx(qwUser, client, config,"H5-渠道活码生成失败");
             default:
                 log.warn("未知的加微方式:{}", config.qwWxAddWayId);
                 return null;
@@ -1120,20 +1151,79 @@ public class WxTaskService {
         String linkUrl = getLinkUrl(qwUser);
 
         if (StringUtil.strIsNullOrEmpty(linkUrl)) {
-            return handleLinkGenerationFailure(client, qwUser, config);
+            return handleLinkGenerationFailure(client, qwUser, config,"获客链接生成失败");
         }
 
         return handleSmsSendAndAddWx(qwUser, client, temp, linkUrl, config);
 
 
     }
+
+    /**
+     * 处理短信-小程序-渠道活码加微
+     */
+    private CompanyWxClient handleMINILinkAddWx(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            NodeConfig config,String errMsg) {
+
+        // 查询短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempById((long) config.smsTempId);
+        if (temp == null || !temp.getStatus().equals(1) || !temp.getIsAudit().equals(1)) {
+            log.error("短信模板无效或未审核:{}", temp);
+            throw new RuntimeException("短信模板无效或未审核");
+        }
+
+        // 查询公司短信信息
+        CompanySms sms = companySmsService.selectCompanySmsByCompanyId(qwUser.getCompanyId());
+        if (sms == null) {
+            log.error("公司短信信息不存在:companyId: {}", qwUser.getCompanyId());
+            throw new RuntimeException("公司短信信息不存在");
+        }
+
+        if (sms.getRemainSmsCount() <= 0) {
+            log.error("剩余短信数量不足:companyId: {}", qwUser.getCompanyId());
+            throw new RuntimeException("剩余短信数量不足,请充值");
+        }
+
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig courseConfig = JSON.parseObject(json, CourseConfig.class);
+
+
+        String linkUrl = getQrLinkUrl(qwUser);
+//        String linkUrl = "https://p.qpic.cn/wwhead/duc2TvpEgSdicZ9RrdUtBkv2UiaA/0";
+
+        if (StringUtil.strIsNullOrEmpty(linkUrl)) {
+            return handleLinkGenerationFailure(client, qwUser, config,errMsg);
+        }
+
+        String miniShortLink = createSmsShortLink(qwUser.getCorpId(), String.valueOf(qwUser.getId()),
+                String.valueOf(qwUser.getCompanyUserId()), String.valueOf(qwUser.getCompanyId()), linkUrl, courseConfig,config);
+
+
+
+        // 替换短信内容中的签名和链接
+        String smsContent = temp.getContent()
+                .replaceAll("【(.*?)】", "【" + courseConfig.getSmsDomain() + "】")
+                .replace("${sms.cardUrl}", miniShortLink);
+
+        // 更新模板内容
+        temp.setContent(smsContent);
+
+        return handleMINISendAndAddWx(qwUser, client, temp, miniShortLink, config);
+
+
+    }
+
+
     /**
      * 处理链接生成失败
      */
     private CompanyWxClient handleLinkGenerationFailure(
             CompanyWxClient4WorkFlowVO client,
             QwUser qwUser,
-            NodeConfig config) {
+            NodeConfig config,
+            String msg) {
 
         client.setIsAdd(3);
         client.setAddTime(LocalDateTime.now());
@@ -1142,7 +1232,7 @@ public class WxTaskService {
         BeanUtils.copyProperties(client, addItem);
 
         CompanyVoiceRoboticCallLogAddwx addLogAddWx = CompanyVoiceRoboticCallLogAddwx.initCallLog(
-                "获客链接生成失败",
+                msg,
                 client.getId(),
                 client.getRoboticId(),
                 qwUser.getId(),
@@ -1155,6 +1245,15 @@ public class WxTaskService {
         addLogAddWx.setIsWeCom(2);
 
         asyncSaveCompanyVoiceRoboticCallLog(addLogAddWx);
+
+        //更新工作流的执行日志
+        CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+
+        queryP.setWorkflowInstanceId(client.getWorkflowInstanceId());
+        queryP.setNodeKey(client.getNodeKey());
+        queryP.setStatus(ExecutionStatusEnum.FAILURE.getValue());
+        companyAiWorkflowExecLogMapper.updateCompanyAiWorkflowExecLog(queryP);
+
         return addItem;
     }
 
@@ -1188,6 +1287,94 @@ public class WxTaskService {
         return addItem;
     }
 
+
+    /**
+     * 处理短信发送和加微-小程序的渠道活码
+     */
+    private CompanyWxClient handleMINISendAndAddWx(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            CompanySmsTemp temp,
+            String linkUrl,
+            NodeConfig config) {
+
+
+        SmsSendBatchParam smsSendBatchParam = buildSmsSendParam(qwUser, client, temp, linkUrl);
+        JSONObject runParamSms = (JSONObject) JSON.toJSON(smsSendBatchParam);
+        runParamSms.put("temp", temp);
+
+        //发送短信并记录日志
+        sendSmsWithLog(smsSendBatchParam, runParamSms, client, qwUser, temp);
+
+        //保存加微日志
+        saveAddWxLog(runParamSms, client, qwUser, config);
+
+        client.setIsAdd(2);
+        client.setAddTime(LocalDateTime.now());
+
+
+        CompanyWxClient addItem = new CompanyWxClient();
+        BeanUtils.copyProperties(client, addItem);
+
+        return addItem;
+    }
+
+
+
+    private String createSmsShortLink(String corpId, String qwUserId, String companyUserId, String companyId,
+                                      String url,CourseConfig config,NodeConfig nodeConfig) {
+
+        Date createTime=new Date();
+
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setCorpId(corpId);
+        link.setLinkType(0); //正常链接
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = ShortCodeGeneratorUtils.generate8();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+        link.setCreateTime(createTime);;
+
+
+        LocalDateTime sendDateTime = createTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(2);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        link.setUpdateTime(updateTime);
+
+
+        String prefix = "";
+        switch (nodeConfig.qwWxAddWayId) {
+            case 3:
+                prefix = REAL_CID_LINK_PREFIX;
+                break;
+            case 4:
+                prefix = REAL_CID_H5LINK_PREFIX;
+                break;
+            default:
+            break;
+        }
+
+        String realLinkFull= prefix + JSON.toJSONString(url);
+
+        link.setRealLink(realLinkFull);
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        if(StringUtils.isEmpty(config.getSmsDomainName())){
+            log.error("检测到未配置看课短信链接域名");
+            return null;
+        }
+        return config.getSmsDomainName() + "/" + link.getLink();
+
+    }
+
+
     /**
      * 发送短信并记录日志
      */
@@ -1283,6 +1470,41 @@ public class WxTaskService {
 
     }
 
+    /**
+     * 获取渠道链接
+     */
+    private String getQrLinkUrl(QwUser qwUser){
+
+        String contactWay = "";
+
+        if (qwUser == null) {
+            return null;
+        }
+        if (StringUtils.isNotEmpty(qwUser.getContactWay())) {
+            contactWay = qwUser.getContactWay();
+        } else {
+            String qwUserId = "[\"" + qwUser.getQwUserId() + "\"]";
+            List<String> users = JSON.parseArray(qwUserId, String.class);
+            QwAddContactWayParam qwAddContactWayParam = new QwAddContactWayParam();
+            qwAddContactWayParam.setType(1);
+            qwAddContactWayParam.setScene(2);
+            qwAddContactWayParam.setUser(users);
+            qwAddContactWayParam.setSkip_verify(false);
+            QwAddContactWayResult qwAddContactWayResult = qwApiService.addContactWay(qwAddContactWayParam, qwUser.getCorpId());
+            if (qwAddContactWayResult.getErrcode() == 0) {
+                qwUser.setContactWay(qwAddContactWayResult.getQr_code());
+                qwUser.setConfigId(qwAddContactWayResult.getConfig_id());
+                qwUserMapper.updateQwUser(qwUser);
+                contactWay = qwUser.getContactWay();
+            } else {
+                log.error("生成企业微信渠道活码失败>>>" + qwUser.getId()+"----"+qwAddContactWayResult.getErrmsg());
+                return null;
+            }
+        }
+        return contactWay;
+
+    }
+
     /**
      * 构建加微请求参数
      */
@@ -1753,6 +1975,10 @@ public class WxTaskService {
                 .set(CompanyVoiceRoboticCallLogAddwx::getStatus, 3)
                 .set(CompanyVoiceRoboticCallLogAddwx::getResult, reason)
                 .update();
+
+        //更新工作流的执行日志
+        CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+
     }
 
     /**