Просмотр исходного кода

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

caoliqin 1 неделя назад
Родитель
Сommit
c9e37f88c8
29 измененных файлов с 534 добавлено и 50 удалено
  1. 14 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  2. 28 0
      fs-admin/src/main/java/com/fs/live/controller/LiveMsgController.java
  3. 15 5
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  4. 16 0
      fs-common/src/main/java/com/fs/common/constant/RedisConstant.java
  5. 132 0
      fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java
  6. 19 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java
  7. 1 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  8. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java
  9. 0 31
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  10. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  11. 17 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  12. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  13. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java
  14. 3 0
      fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java
  15. 9 0
      fs-service/src/main/java/com/fs/live/service/ILiveMsgService.java
  16. 77 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java
  17. 7 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  18. 30 0
      fs-service/src/main/java/com/fs/live/vo/LiveMsgExportVO.java
  19. 7 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  20. 1 1
      fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java
  21. 1 0
      fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java
  22. 1 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  23. 9 2
      fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml
  24. 12 0
      fs-service/src/main/resources/mapper/live/LiveMsgMapper.xml
  25. 5 0
      fs-user-app/pom.xml
  26. 0 4
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  27. 1 1
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java
  28. 7 1
      fs-user-app/src/main/java/com/fs/framework/config/DataSourceConfig.java
  29. 105 0
      fs-user-app/src/test/java/com/fs/test/StockDeductTest.java

+ 14 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -1137,6 +1137,20 @@ public class FsStoreOrderScrmController extends BaseController {
         return R.ok("成功审核 " + count + " 条订单");
     }
 
+    @ApiOperation("订单备注")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:remark')")
+    @PostMapping("/remark")
+    public R remark(@Validated @RequestBody FsStoreOrderScrm param) {
+        if (param.getId() == null || param.getId() == 0) {
+            return R.error("订单ID错误");
+        }
+        if (StringUtils.isEmpty(param.getOrderRemark())) {
+            return R.error("订单备注不能为空");
+        }
+        return fsStoreOrderService.orderRemark(param);
+    }
+
     private FsStoreOrderDf getDFInfo(String loginAccount) {
         //查询订单账户 判断是否存在该订单账户
         List<FsDfAccount> erpAccounts = fsDfAccountService.selectFsDfAccountList(null);

+ 28 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveMsgController.java

@@ -3,11 +3,16 @@ package com.fs.live.controller;
 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.domain.model.LoginUser;
 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.framework.web.service.TokenService;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.service.ILiveMsgService;
+import com.fs.live.vo.LiveMsgExportVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -26,6 +31,9 @@ public class LiveMsgController extends BaseController
 {
     @Autowired
     private ILiveMsgService liveMsgService;
+    
+    @Autowired
+    private TokenService tokenService;
 
     /**
      * 查询直播讨论列表
@@ -102,4 +110,24 @@ public class LiveMsgController extends BaseController
     {
         return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
     }
+
+    /**
+     * 导出直播评论
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveMsg:export')")
+    @Log(title = "直播评论导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportComments/{liveId}")
+    public AjaxResult exportComments(@PathVariable("liveId") Long liveId)
+    {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            Long userId = loginUser.getUser().getUserId();
+            
+            List<LiveMsgExportVO> list = liveMsgService.exportLiveMsgComments(liveId, userId);
+            ExcelUtil<LiveMsgExportVO> util = new ExcelUtil<LiveMsgExportVO>(LiveMsgExportVO.class);
+            return util.exportExcel(list, "直播评论数据");
+        } catch (Exception e) {
+            return AjaxResult.error("导出失败:" + e.getMessage());
+        }
+    }
 }

+ 15 - 5
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -121,7 +121,9 @@ public class OrderController extends BaseController
     public AjaxResult export(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -160,7 +162,9 @@ public class OrderController extends BaseController
     public AjaxResult exportDetails(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -200,7 +204,9 @@ public class OrderController extends BaseController
     public AjaxResult exportItems(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
 
         // 如果查询结果超过20000条,返回错误提示
@@ -225,7 +231,9 @@ public class OrderController extends BaseController
     public AjaxResult exportItemsDetails(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
 
         // 如果查询结果超过20000条,返回错误提示
@@ -246,7 +254,9 @@ public class OrderController extends BaseController
     public AjaxResult exportShipping(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {

+ 16 - 0
fs-common/src/main/java/com/fs/common/constant/RedisConstant.java

@@ -0,0 +1,16 @@
+package com.fs.common.constant;
+/**
+ * 库存与锁相关常量(Java 8 静态常量优化)
+ */
+public class RedisConstant {
+    // 库存Key前缀
+    public static final String STOCK_KEY_PREFIX = "product:stock:";
+    // 分布式锁Key前缀
+    public static final String LOCK_KEY_PREFIX = "product:lock:";
+    // 锁过期时间(30秒,避免死锁,大于业务执行时间)
+    public static final long LOCK_EXPIRE_SECONDS = 3L;
+    // 锁重试间隔(50毫秒,非阻塞重试,避免线程阻塞)
+    public static final long LOCK_RETRY_INTERVAL = 100L;
+    // 锁最大重试次数(3次,避免无限重试)
+    public static final int LOCK_MAX_RETRY = 20;
+}

+ 132 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java

@@ -0,0 +1,132 @@
+package com.fs.common.core.redis.service;
+
+import com.fs.common.constant.RedisConstant;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+/**
+ * 高并发库存扣减服务(Java 8 + Redis分布式锁)
+ */
+@Service
+public class StockDeductService {
+
+    // 注入RedisTemplate
+    public final RedisTemplate<String, Object> redisTemplate;
+
+    // 构造器注入(Spring 推荐,Java 8 支持)
+    public StockDeductService(RedisTemplate<String, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    // 库存扣减Lua脚本(预编译,提升高并发性能)
+    private static final DefaultRedisScript<Long> STOCK_DEDUCT_SCRIPT;
+    // 锁释放Lua脚本(预编译)
+    private static final DefaultRedisScript<Long> LOCK_RELEASE_SCRIPT;
+
+    // 库存扣减Lua脚本(优化后,增强健壮性)
+    static {
+        // 初始化库存扣减脚本
+        STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        STOCK_DEDUCT_SCRIPT.setScriptText("if redis.call('exists', KEYS[1]) ~= 1 then " + "return -2; " + "end " + "local stock_str = redis.call('get', KEYS[1]); " + "local stock = tonumber(stock_str); " + "if stock == nil then " + "return -3; " + "end " + "local deductNum_str = ARGV[1]; " + "local deductNum = tonumber(deductNum_str); " + "if deductNum == nil or deductNum <= 0 then " + "return -4; " + "end " + "if stock >= deductNum then " + "return redis.call('decrby', KEYS[1], deductNum); " + "else " + "return -1; " + "end");
+        STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
+
+        // 锁释放脚本保持不变
+        LOCK_RELEASE_SCRIPT = new DefaultRedisScript<>();
+        LOCK_RELEASE_SCRIPT.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 end");
+        LOCK_RELEASE_SCRIPT.setResultType(Long.class);
+    }
+
+    /**
+     * 初始化商品库存(Redis)
+     *
+     * @param productId 商品ID
+     * @param initStock 初始库存
+     */
+    public void initStock(Long productId, Integer initStock) {
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + productId;
+        redisTemplate.opsForValue().set(stockKey, initStock, 24 * 60 * 60, TimeUnit.SECONDS);
+        System.out.println("商品" + productId + "库存初始化完成,初始库存:" + initStock);
+    }
+
+    /**
+     * 高并发库存扣减(核心方法,落地Java 8特性)
+     *
+     * @param productId 商品ID
+     * @param deductNum 扣减数量(默认1)
+     * @return 扣减结果:true=成功,false=失败
+     */
+    public CompletableFuture<Boolean> deductStockAsync(Long productId, Integer deductNum) {
+        // Java 8 CompletableFuture 异步处理,提升高并发吞吐量
+        return CompletableFuture.supplyAsync(() -> {
+            // 1. 参数校验(Java 8 Optional 空值处理)
+            Integer num = Optional.ofNullable(deductNum).orElse(1);
+            String stockKey = RedisConstant.STOCK_KEY_PREFIX + productId;
+            String lockKey = RedisConstant.LOCK_KEY_PREFIX + productId;
+
+            // 2. 生成锁持有者唯一标识(UUID + 线程ID,避免误释放)
+            String lockOwner = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
+
+            // 3. 尝试获取分布式锁(非阻塞重试,Java 8 Stream API 实现重试)
+// 3. 尝试获取分布式锁(优化:加入随机延迟,避免惊群效应)
+            boolean isLockAcquired = IntStream.range(0, RedisConstant.LOCK_MAX_RETRY).anyMatch(retryCount -> {
+                Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockOwner, RedisConstant.LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
+                if (Boolean.TRUE.equals(result)) {
+                    return true;
+                }
+                try {
+                    // 随机延迟:50ms~150ms,避免所有请求同时重试
+                    long randomDelay = RedisConstant.LOCK_RETRY_INTERVAL + new Random().nextInt(100);
+                    TimeUnit.MILLISECONDS.sleep(randomDelay);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    return false;
+                }
+                return false;
+            });
+            // 4. 未获取到锁,直接返回失败
+            if (!isLockAcquired) {
+                System.err.println("商品" + productId + "获取锁失败,高并发限流中");
+                return false;
+            }
+
+            try {
+                // 5. 执行库存扣减Lua脚本(原子操作,防超卖)
+                // 新增日志:打印当前库存值和扣减数量
+                Integer currentStockStr = (Integer) redisTemplate.opsForValue().get(stockKey);
+                System.out.println("拿到锁成功 → 库存Key:" + stockKey + ",当前库存值:" + currentStockStr + ",扣减数量:" + num);
+
+                // 执行库存扣减Lua脚本
+                Long remainingStock = redisTemplate.execute(
+                        STOCK_DEDUCT_SCRIPT,
+                        Collections.singletonList(stockKey),
+                        1
+                );
+
+                // 新增日志:打印Lua返回结果
+                System.out.println("Lua脚本返回值:" + remainingStock);
+
+                // 6. 判断扣减结果
+                if (remainingStock != null && remainingStock >= 0) {
+                    System.out.println("商品" + productId + "库存扣减成功,剩余库存:" + remainingStock);
+                    return true;
+                } else {
+                    System.err.println("商品" + productId + "库存不足,扣减失败");
+                    return false;
+                }
+            } finally {
+                // 7. 释放分布式锁(Lua脚本保证原子性,仅释放自己持有的锁)
+                redisTemplate.execute(LOCK_RELEASE_SCRIPT, Collections.singletonList(lockKey), lockOwner);
+                System.out.println("商品" + productId + "锁释放成功,持有者:" + lockOwner);
+            }
+        });
+    }
+}

+ 19 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java

@@ -10,6 +10,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.service.ILiveMsgService;
+import com.fs.live.vo.LiveMsgExportVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -105,4 +106,22 @@ public class LiveMsgController extends BaseController
         return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
     }
 
+    /**
+     * 导出直播评论
+     */
+    @Log(title = "直播评论导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportComments/{liveId}")
+    public AjaxResult exportComments(@PathVariable("liveId") Long liveId)
+    {
+        try {
+            CompanyUser user = SecurityUtils.getLoginUser().getUser();
+            Long userId = user.getUserId();
+            
+            List<LiveMsgExportVO> list = liveMsgService.exportLiveMsgComments(liveId, userId);
+            ExcelUtil<LiveMsgExportVO> util = new ExcelUtil<LiveMsgExportVO>(LiveMsgExportVO.class);
+            return util.exportExcel(list, "直播评论数据");
+        } catch (Exception e) {
+            return AjaxResult.error("导出失败:" + e.getMessage());
+        }
+    }
 }

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -722,7 +722,7 @@ public class QwExternalContactController extends BaseController
 
         return  qwExternalContactService.setCustomerCourseSopList(param);
     }
-    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit')")
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit') or @ss.hasPermi('qw:externalContact:deptEdit') or @ss.hasPermi('qw:externalContact:myEdit')")
     @Log(title = "批量修改备注", businessType = BusinessType.UPDATE)
     @PostMapping("/batchUpdateExternalContactNotes")
     public R batchUpdateExternalContactNotes(@RequestBody QwExternalContactUpdateNoteParam param) throws JSONException {

+ 1 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java

@@ -254,7 +254,7 @@ public interface FsUserVideoMapper
     @Select({"<script> " +
             "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
             "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.fail_reason,v.status from fs_user_video v " +
+            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.status from fs_user_video v " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
             "where v.is_del = 0 and (" +

+ 0 - 31
fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -524,25 +524,8 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
         requestDTO.setOIds(Collections.singletonList(Long.valueOf(param.getCode())));
 
-        // 限流检查:每分钟最多100次请求,超过95次返回429
-        String rateLimitKey = RATE_LIMIT_KEY_PREFIX + System.currentTimeMillis() / 60000; // 每分钟一个key
-
-        // 使用原子操作增加计数,并获取增加后的值
-        Long currentCount = redisCache.incr(rateLimitKey, 1L);
-
-        // 如果是第一次请求,设置过期时间为1分钟
-        if (currentCount == 1) {
-            redisCache.expire(rateLimitKey, 1, TimeUnit.MINUTES);
-        }
         // 3. 构建响应对象
         ErpOrderQueryResponse response = new ErpOrderQueryResponse();
-        // 如果当前分钟内请求次数超过95次,直接返回429错误
-        if (currentCount >= RATE_LIMIT_THRESHOLD) {
-            response.setCode("429");
-            response.setSuccess(false);
-            return response;
-        }
-
 
         // 2. 调用ERP服务查询订单
         OrderQueryResponseDTO query = jstErpHttpService.query(requestDTO);
@@ -565,24 +548,10 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
 
     @Override
     public ErpOrderQueryResponse getLiveOrder(ErpOrderQueryRequert param) {
-        // 限流检查:每分钟最多100次请求,超过95次返回429
-        String rateLimitKey = RATE_LIMIT_KEY_PREFIX + System.currentTimeMillis() / 60000; // 每分钟一个key
 
-        // 使用原子操作增加计数,并获取增加后的值
-        Long currentCount = redisCache.incr(rateLimitKey, 1L);
 
-        // 如果是第一次请求,设置过期时间为1分钟
-        if (currentCount == 1) {
-            redisCache.expire(rateLimitKey, 1, TimeUnit.MINUTES);
-        }
         // 3. 构建响应对象
         ErpOrderQueryResponse response = new ErpOrderQueryResponse();
-        // 如果当前分钟内请求次数超过95次,直接返回429错误
-        if (currentCount >= RATE_LIMIT_THRESHOLD) {
-            response.setCode("429");
-            response.setSuccess(false);
-            return response;
-        }
 
         // 1. 构建查询请求DTO
         OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -357,4 +357,9 @@ public interface IFsStoreOrderScrmService
      * @return 更新条数
      */
     int batchAuditOrder(FsStoreOrderBatchAuditParam param);
+
+    /**
+     * 订单备注
+     */
+    R orderRemark(FsStoreOrderScrm orderScrm);
 }

+ 17 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -5586,6 +5586,23 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return fsStoreOrderMapper.batchUpdateAuditStatus(param.getOrderIds(), param.getIsAudit());
     }
 
+    @Override
+    public R orderRemark(FsStoreOrderScrm orderScrm) {
+        FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(orderScrm.getId());
+        if (order != null) {
+            FsStoreOrderScrm map = new FsStoreOrderScrm();
+            map.setId(orderScrm.getId());
+            map.setOrderRemark(orderScrm.getOrderRemark());
+
+            if (fsStoreOrderMapper.updateFsStoreOrder(map) > 0) {
+                return R.ok();
+            } else {
+                return R.error("备注失败");
+            }
+        }
+        return R.error("未找到订单");
+    }
+
     private static final DateTimeFormatter CST_FORMATTER = DateTimeFormatter
             .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US)
             .withZone(ZoneId.of("Asia/Shanghai"));

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.common.BeanCopyUtils;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.param.BaseQueryParam;
@@ -145,6 +146,8 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
 
     @Autowired
     private RedisCache redisCache;
+    @Autowired
+    private RedisCacheT<Integer> redisCacheT;
 
     /**
      * 清除商品详情缓存

+ 8 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java

@@ -84,4 +84,12 @@ public interface LiveMsgMapper
     Map<String, BigDecimal> selectDashboardCount(@Param("liveId") Long liveId);
 
     List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg);
+
+    /**
+     * 查询直播评论用于导出
+     *
+     * @param liveId 直播ID
+     * @return 评论列表
+     */
+    List<com.fs.live.vo.LiveMsgExportVO> selectLiveMsgForExport(@Param("liveId") Long liveId);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/live/param/MergedOrderQueryParam.java

@@ -123,5 +123,8 @@ public class MergedOrderQueryParam extends BaseQueryParam implements Serializabl
     
     /** 分页偏移量(在外部计算后传入,不在SQL中计算) */
     private Integer offset;
+
+    /** 分页偏移量(在外部计算后传入,不在SQL中计算) */
+    private Integer exportFlag;
 }
 

+ 9 - 0
fs-service/src/main/java/com/fs/live/service/ILiveMsgService.java

@@ -69,4 +69,13 @@ public interface ILiveMsgService
     List<LiveMsg> listRecentMsg(Long id);
 
     List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg);
+
+    /**
+     * 导出直播评论
+     *
+     * @param liveId 直播ID
+     * @param userId 用户ID(用于Redis加锁)
+     * @return 评论列表
+     */
+    List<com.fs.live.vo.LiveMsgExportVO> exportLiveMsgComments(Long liveId, Long userId);
 }

+ 77 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveMsgServiceImpl.java

@@ -1,15 +1,23 @@
 package com.fs.live.service.impl;
 
 
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.mapper.LiveMsgMapper;
 import com.fs.live.service.ILiveMsgService;
+import com.fs.live.vo.LiveMsgExportVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 直播讨论Service业务层处理
@@ -20,10 +28,20 @@ import java.util.List;
 @Service
 public class LiveMsgServiceImpl implements ILiveMsgService
 {
+    private static final Logger log = LoggerFactory.getLogger(LiveMsgServiceImpl.class);
+    
     @Autowired
     private LiveMsgMapper liveMsgMapper;
     @Autowired
     private LiveDataServiceImpl liveDataService;
+    
+    @Autowired(required = false)
+    private RedisTemplate<String, Object> redisTemplate;
+    
+    /** Redis锁前缀 */
+    private static final String LOCK_PREFIX = "live:msg:export:lock:";
+    /** 锁过期时间(秒) */
+    private static final long LOCK_EXPIRE_TIME = 300; // 5分钟
 
     /**
      * 查询直播讨论
@@ -108,4 +126,63 @@ public class LiveMsgServiceImpl implements ILiveMsgService
     public List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg) {
         return liveMsgMapper.selectLiveMsgSingleList(liveMsg);
     }
+
+    @Override
+    public List<LiveMsgExportVO> exportLiveMsgComments(Long liveId, Long userId) {
+        if (liveId == null) {
+            throw new CustomException("直播ID不能为空");
+        }
+        if (userId == null) {
+            throw new CustomException("用户ID不能为空");
+        }
+
+        // Redis锁的key:用户ID + 直播间ID
+        String lockKey = LOCK_PREFIX + userId + ":" + liveId;
+        String lockValue = String.valueOf(System.currentTimeMillis());
+
+        try {
+            // 尝试获取锁
+            Boolean lockAcquired = false;
+            if (redisTemplate != null) {
+                lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
+            }
+
+            if (redisTemplate != null && !lockAcquired) {
+                log.warn("用户{}正在导出直播间{}的评论,请勿重复操作", userId, liveId);
+                throw new CustomException("正在导出中,请勿重复操作");
+            }
+
+            try {
+                // 查询评论数据
+                List<LiveMsgExportVO> list = liveMsgMapper.selectLiveMsgForExport(liveId);
+                log.info("用户{}导出直播间{}的评论,共{}条", userId, liveId, list != null ? list.size() : 0);
+                return list != null ? list : Collections.emptyList();
+            } finally {
+                // 释放锁
+                if (redisTemplate != null && lockAcquired) {
+                    // 使用Lua脚本确保只删除自己的锁
+                    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                            "return redis.call('del', KEYS[1]) " +
+                            "else return 0 end";
+                    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
+                    script.setScriptText(luaScript);
+                    script.setResultType(Long.class);
+                    redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);
+                }
+            }
+        } catch (CustomException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("导出直播评论失败,liveId: {}, userId: {}", liveId, userId, e);
+            // 确保异常时也释放锁
+            if (redisTemplate != null) {
+                try {
+                    redisTemplate.delete(lockKey);
+                } catch (Exception ex) {
+                    log.error("释放Redis锁失败", ex);
+                }
+            }
+            throw new CustomException("导出失败:" + e.getMessage());
+        }
+    }
 }

+ 7 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -1654,6 +1654,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 userService.subLiveTuiMoney(liveOrder);
             }
         }
+        //优惠券返回
+        if(order.getUserCouponId()!=null){
+            // 退券
+            order.setCouponUserId(Long.parseLong(order.getUserId()));
+            this.refundCoupon(order);
+        }
         // 删除限购记录
         deletePurchaseLimitRecordsForLiveOrder(order);
 
@@ -4025,7 +4031,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 使用 productService.decProductStock 方法,该方法已包含库存检查逻辑
         fsStoreProductService.decProductStock(productId, attrValueId, num);
     }
-    
+
     private void checkPurchaseLimitForLiveOrder(Long userId, Long productId, Integer num) {
         // 查询商品信息
         FsStoreProductScrm product = fsStoreProductService.selectFsStoreProductById(productId);

+ 30 - 0
fs-service/src/main/java/com/fs/live/vo/LiveMsgExportVO.java

@@ -0,0 +1,30 @@
+package com.fs.live.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 直播评论导出VO
+ *
+ * @author fs
+ * @date 2026-01-13
+ */
+@Data
+public class LiveMsgExportVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String nickName;
+
+    /** 评论内容 */
+    @Excel(name = "评论内容")
+    private String msg;
+
+    /** 发送时间 */
+    @Excel(name = "发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 7 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -14,6 +14,7 @@ import com.fs.ad.enums.AdUploadType;
 import com.fs.ad.service.IAdHtmlClickLogService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyConfigService;
@@ -3806,7 +3807,9 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
             if ("link".equals(att.getMsgtype())
                     && !StringUtil.strIsNullOrEmpty(att.getLink().getCourseId())
                     && !StringUtil.strIsNullOrEmpty(att.getLink().getVideoId())) {
-
+                if(CloudHostUtils.hasCloudHostName("木易华康")){
+                    return;
+                }
                 try {
                     FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
                     param.setVideoId(Long.valueOf(att.getLink().getVideoId()));
@@ -3841,7 +3844,9 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
             if("miniprogram".equals(att.getMsgtype())
                     && !StringUtil.strIsNullOrEmpty(att.getMiniprogram().getCourseId())
                     && !StringUtil.strIsNullOrEmpty(att.getMiniprogram().getVideoId())){
-
+                if(CloudHostUtils.hasCloudHostName("木易华康")){
+                    return;
+                }
                 try {
 
                     //小程序

+ 1 - 1
fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java

@@ -16,7 +16,7 @@ public interface FsWxExpressTaskMapper {
      * @param id 任务ID
      * @return FsWxExpressTask 任务实体
      */
-    @Select("SELECT * FROM f s_wx_express_task WHERE id = #{id}")
+    @Select("SELECT * FROM fs_wx_express_task WHERE id = #{id}")
     FsWxExpressTask selectById(@Param("id") Long id);
 
     /**

+ 1 - 0
fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java

@@ -83,6 +83,7 @@ public class ShippingService {
                 if (!weChatApiResponse.isSuccess()) {
                     log.warn("微信接口返回业务错误: code={}, message={}", weChatApiResponse.getErrcode(), weChatApiResponse.getErrmsg());
                     if(ObjectUtil.equal(weChatApiResponse.getErrcode(),40001)) {
+                        accessToken = weChatAuthService.getAccessToken(true);
                         log.info("token缓存失效,清除token,等待下次执行...");
                     }
                 }

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

@@ -434,6 +434,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="followTime != null">follow_time = #{followTime},</if>
             <if test="followDoctorId != null">follow_doctor_id = #{followDoctorId},</if>
             <if test="cycle != null">cycle = #{cycle},</if>
+            <if test="orderRemark != null">order_remark = #{orderRemark},</if>
         </trim>
         where id = #{id}
     </update>

+ 9 - 2
fs-service/src/main/resources/mapper/hisStore/MergedOrderMapper.xml

@@ -119,7 +119,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND DATE(delivery_import_time) BETWEEN SUBSTRING_INDEX(#{maps.deliveryImportTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliveryImportTimeRange}, '--', -1)
           </if>
         ORDER BY create_time DESC
+      <if test="maps.exportFlag == null or maps.exportFlag == ''">
         limit 1000
+      </if>
+
       ) o
       left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.order_id ORDER BY fsois.item_id ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id and item_latest.rn = 1
       LEFT JOIN fs_user u ON o.user_id = u.user_id
@@ -273,7 +276,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND DATE(delivery_import_time) BETWEEN SUBSTRING_INDEX(#{maps.deliveryImportTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliveryImportTimeRange}, '--', -1)
           </if>
         ORDER BY create_time DESC
-      limit 1000
+      <if test="maps.exportFlag == null or maps.exportFlag == ''">
+        limit 1000
+      </if>
       ) o
         left join ( SELECT fsois.*, ROW_NUMBER() OVER ( PARTITION BY fsois.order_id ORDER BY fsois.item_id ) AS rn FROM fs_store_order_item_scrm fsois ) item_latest ON item_latest.order_id = o.id and item_latest.rn = 1
       LEFT JOIN fs_user u ON o.user_id = u.user_id
@@ -424,7 +429,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND DATE(delivery_send_time) BETWEEN SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', 1) AND SUBSTRING_INDEX(#{maps.deliverySendTimeRange}, '--', -1)
           </if>
         ORDER BY create_time DESC
-      limit 1000
+      <if test="maps.exportFlag == null or maps.exportFlag == ''">
+        limit 1000
+      </if>
       ) o
       left join live_order_item loi on loi.order_id = o.order_id
       LEFT JOIN fs_user u ON o.user_id = u.user_id

+ 12 - 0
fs-service/src/main/resources/mapper/live/LiveMsgMapper.xml

@@ -109,4 +109,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{msgId}
         </foreach>
     </delete>
+
+    <!-- 导出直播评论数据 -->
+    <select id="selectLiveMsgForExport" resultType="com.fs.live.vo.LiveMsgExportVO">
+        SELECT 
+            nick_name as nickName,
+            msg,
+            create_time as createTime
+        FROM live_msg
+        WHERE live_id = #{liveId}
+        ORDER BY create_time ASC
+        LIMIT 30000
+    </select>
 </mapper>

+ 5 - 0
fs-user-app/pom.xml

@@ -108,6 +108,11 @@
             <artifactId>rocketmq-spring-boot-starter</artifactId>
             <version>2.2.3</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 0 - 4
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -349,10 +349,6 @@ public class LiveOrderController extends AppBaseController
     {
         log.info("获取订单详细信息 参数: {}",orderId);
         LiveOrder liveOrder = orderService.selectLiveOrderByOrderId(orderId);
-        //订单总价 临时处理 为 商品支付金额(商品支付金额=订单总价-快递费)
-        if(ObjectUtil.isNotEmpty(liveOrder)) {
-            liveOrder.setTotalPrice(liveOrder.getTotalPrice().subtract(liveOrder.getPayPostage()));
-        }
         return AjaxResult.success(liveOrder);
     }
 

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -175,7 +175,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
             liveVo.setTodayRewardReceived(false);
         }
         
-        return R.ok().put("data", liveVo);
+        return R.ok().put("serviceTime", System.currentTimeMillis()).put("data", liveVo);
     }
     
     /**

+ 7 - 1
fs-user-app/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -33,14 +33,20 @@ public class DataSourceConfig {
     public DataSource masterDataSource() {
         return new DruidDataSource();
     }
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
 
 
 
     @Bean
     @Primary
-    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }

+ 105 - 0
fs-user-app/src/test/java/com/fs/test/StockDeductTest.java

@@ -0,0 +1,105 @@
+package com.fs.test;
+
+import com.fs.FsUserAppApplication;
+import com.fs.common.constant.RedisConstant;
+import com.fs.common.core.redis.service.StockDeductService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * 50万高并发库存扣减测试
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = FsUserAppApplication.class)
+@RequiredArgsConstructor
+@Slf4j
+public class StockDeductTest {
+
+    @Autowired
+    private StockDeductService stockDeductService;
+
+    // 商品ID
+    private static final Long PRODUCT_ID = 1001L;
+    // 初始库存(模拟5万库存,应对50万并发扣减)
+    private static final Integer INIT_STOCK = 5000;
+    // 总请求数(50万)
+    private static final int TOTAL_REQUESTS = 50000;
+
+    /**
+     * 模拟50万高并发库存扣减
+     */
+    @Test
+    public void testHighConcurrencyDeduct() throws InterruptedException, ExecutionException {
+        stockDeductService.initStock(PRODUCT_ID, INIT_STOCK);
+        // Java 8 ExecutorService 线程池(固定线程池,适配高并发)
+        ExecutorService executorService = createHighConcurrencyPool();
+
+        // 存储所有异步任务结果
+        List<CompletableFuture<Boolean>> futureList = new ArrayList<>();
+
+        // 提交50万请求
+        for (int i = 0; i < TOTAL_REQUESTS; i++) {
+            futureList.add(stockDeductService.deductStockAsync(PRODUCT_ID, 1));
+        }
+
+        // 等待所有任务完成(Java 8 CompletableFuture 批量处理)
+        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
+                futureList.toArray(new CompletableFuture[0])
+        );
+        allFutures.get();
+
+        // 统计结果
+        long successCount = futureList.stream()
+                .map(future -> {
+                    try {
+                        return future.get();
+                    } catch (Exception e) {
+                        return false;
+                    }
+                })
+                .filter(Boolean::booleanValue)
+                .count();
+
+        // 打印结果
+        System.out.println("======================================");
+        System.out.println("50万高并发库存扣减测试完成");
+        System.out.println("成功扣减次数:" + successCount);
+        System.out.println("失败扣减次数:" + (TOTAL_REQUESTS - successCount));
+        System.out.println("最终剩余库存:" + stockDeductService.redisTemplate.opsForValue().get(RedisConstant.STOCK_KEY_PREFIX + PRODUCT_ID));
+        System.out.println("======================================");
+
+        // 关闭线程池
+        executorService.shutdown();
+        executorService.awaitTermination(1, TimeUnit.MINUTES);
+    }
+
+
+
+    private static ExecutorService createHighConcurrencyPool() {
+        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; // CPU核心数*2
+        int maximumPoolSize = 200; // 最大线程数,根据服务器配置调整
+        long keepAliveTime = 60L;
+        // 用SynchronousQueue,直接提交任务,避免队列积压
+        BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
+        // 拒绝策略:丢弃最老的任务,避免OOM
+        RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
+
+        return new ThreadPoolExecutor(
+                corePoolSize,
+                maximumPoolSize,
+                keepAliveTime,
+                TimeUnit.SECONDS,
+                workQueue,
+                new ThreadPoolExecutor.CallerRunsPolicy() // 兜底:主线程执行,避免任务丢失
+        );
+    }
+}