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

Merge remote-tracking branch 'origin/master'

zyp 5 napja
szülő
commit
6a957b1ee5
24 módosított fájl, 934 hozzáadás és 13 törlés
  1. 2 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  2. 4 0
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  3. 4 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  4. 1 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  5. 108 0
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  6. 29 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  7. 1 1
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  8. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreAfterSalesScrm.java
  9. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  10. 16 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  11. 17 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  12. 58 0
      fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java
  13. 4 0
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  14. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
  15. 53 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  16. 43 0
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java
  17. 328 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  18. 16 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  19. 4 0
      fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  20. 7 0
      fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml
  21. 116 0
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  22. 7 3
      fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml
  23. 92 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java
  24. 16 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

+ 2 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -26,6 +26,7 @@ import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.param.FsStoreOrderParam;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
+import com.github.pagehelper.PageHelper;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -82,7 +83,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
 //    @PreAuthorize("@ss.hasPermi('store:healthStoreOrder:list')")
       @PostMapping("/healthList")
       public TableDataInfo healthStoreList(@RequestBody FsStoreOrderParam param) {
-        startPage();
+          PageHelper.startPage(param.getPageNum(), param.getPageSize());
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCreateTimeList(param.getCreateTimeRange().split("--"));
         }

+ 4 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -93,6 +93,10 @@ public class LiveAfterSalesController extends BaseController
     public TableDataInfo list(LiveAfterSalesVo liveAfterSales)
     {
         startPage();
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));

+ 4 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java

@@ -59,6 +59,10 @@ public class LiveAfterSalesController extends BaseController
         startPage();
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
         liveAfterSales.setCompanyId(user.getCompanyId());
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -376,7 +376,7 @@ public class IpadSendServer {
         if(setting.getVideoId()!= null){
             FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId( setting.getVideoId().longValue());
             if(video != null){
-                if(video.getIsOnPut() == 1){
+                if(video.getIsOnPut()!=null && video.getIsOnPut() == 1){
                     log.warn("SOP_LOG_ID:{}, 视频已下架,不发送", qwSopLogs.getId());
                     qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "视频已下架,不发送");
                     return false;

+ 108 - 0
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -0,0 +1,108 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 直播完课积分定时任务
+ */
+@Slf4j
+@Component
+public class LiveCompletionPointsTask {
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    /**
+     * 定时检查观看时长并创建完课记录
+     * 每分钟执行一次
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionStatus() {
+        try {
+            // 1. 获取所有观看时长的Redis key
+            Collection<String> keys = redisCache.keys("live:watch:duration:*");
+            
+            if (keys == null || keys.isEmpty()) {
+                return;
+            }
+
+            // 2. 遍历处理每个用户的观看时长
+            for (String key : keys) {
+                try {
+                    String[] parts = key.split(":");
+                    if (parts.length != 5) {
+                        continue;
+                    }
+
+                    Long liveId = Long.parseLong(parts[3]);
+                    Long userId = Long.parseLong(parts[4]);
+
+                    // 3. 获取观看时长(秒)
+                    Object durationObj = redisCache.getCacheObject(key);
+                    if (durationObj == null) {
+                        continue;
+                    }
+
+                    Long watchDuration = Long.parseLong(durationObj.toString());
+
+                    // 4. 检查并创建完课记录
+                    completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, watchDuration);
+
+                    // 5. 检查是否有新的完课记录待领取,推送弹窗消息
+                    sendCompletionNotification(liveId, userId);
+
+                } catch (Exception e) {
+                    log.error("处理观看时长失败, key={}", key, e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("检查完课状态定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 发送完课通知(通过WebSocket推送弹窗)
+     */
+    private void sendCompletionNotification(Long liveId, Long userId) {
+        try {
+            // 查询未领取的完课记录
+            List<LiveCompletionPointsRecord> unreceivedRecords = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+            
+            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
+                // 构造弹窗消息
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setCmd("completionPoints");
+                sendMsgVo.setMsg("完成任务!");
+                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+
+                // 通过WebSocket发送给特定用户(调用已有的发送方法)
+                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+                
+                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}", liveId, userId);
+            }
+        } catch (Exception e) {
+            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+}

+ 29 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -315,6 +315,23 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
+                    
+                    // 心跳时同步更新观看时长
+                    long watchUserId = (long) userProperties.get("userId");
+                    String durationKey = "live:watch:duration:" + liveId + ":" + watchUserId;
+                    
+                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+                        try {
+                            Long currentDuration = Long.parseLong(msg.getData());
+                            Object existingDuration = redisCache.getCacheObject(durationKey);
+                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+                                redisCache.setCacheObject(durationKey, currentDuration.toString(), 2, TimeUnit.HOURS);
+                            }
+                        } catch (Exception e) {
+                            log.error("心跳更新观看时长失败, liveId={}, userId={}", liveId, watchUserId, e);
+                        }
+                    }
+                    
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -610,6 +627,18 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 发送完课积分弹窗通知给特定用户
+     */
+    public void sendCompletionPointsMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
     private void sendBlockMessage(Long liveId, Long userId) {
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);

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

@@ -844,7 +844,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         log.info("订单号: {},发货状态: {},是否发货后: {}",fsStoreOrder.getOrderCode(),fsStoreOrder.getStatus(),ObjectUtils.equals(fsStoreOrder.getStatus(),2));
 
         // 发货后退款
-        if(ObjectUtils.equals(param.getOrderStatus(),2)){
+        if(ObjectUtils.equals(param.getOrderStatus(),2) || ObjectUtils.equals(param.getOrderStatus(),3)){
 
             FsJstAftersalePush fsJstAftersalePush = new FsJstAftersalePush();
             fsJstAftersalePush.setOrderId(fsStoreOrder.getOrderCode());

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

@@ -140,4 +140,8 @@ public class FsStoreAfterSalesScrm extends BaseEntity
 
     private String remark;
 
+    /** 产品名称查询参数(用于搜索) */
+    @TableField(exist = false)
+    private String productName;
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -140,6 +140,9 @@ public interface FsStoreAfterSalesScrmMapper
             "<if test = 'maps.consigneePhone != null and  maps.consigneePhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.consigneePhone},'%') " +
             "</if>" +
+            "<if test = 'maps.productName != null and  maps.productName != \"\" '> " +
+            "and EXISTS (SELECT 1 FROM fs_store_order_item_scrm oi WHERE oi.order_id = o.id AND JSON_UNQUOTE(JSON_EXTRACT(oi.json_info, '$.productName')) LIKE CONCAT('%', #{maps.productName}, '%')) " +
+            "</if>" +
             "<if test = 'maps.deptId != null    '> " +
             "  AND (o.dept_id = #{maps.deptId} OR o.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors) )) " +
             "</if>" +

+ 16 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -87,7 +87,17 @@ public interface FsStoreOrderItemScrmMapper
             " left join company_tcm_schedule cts on cts.id = o.schedule_id " +
             " left join fs_store_product_scrm psps on i.product_id=psps.product_id " +
             " left join fs_store_product_category_scrm fspcs on fspcs.cate_id=psps.cate_id " +
+            "            LEFT JOIN (\n" +
+            "            SELECT\n" +
+            "            sp.*,\n" +
+            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
+            "            FROM fs_store_payment_scrm sp\n" +
+            "            WHERE sp.business_code IS NOT NULL\n" +
+            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
             " where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
@@ -162,7 +172,7 @@ public interface FsStoreOrderItemScrmMapper
             "left join company_user cu on cu.user_id=o.company_user_id " +
             "left join company_tcm_schedule cts on cts.id = o.schedule_id " +
             "LEFT JOIN fs_store_order_df df on df.order_id=o.id\n" +
-            "        <if test=\"maps.appId != null and maps.appId != ''\">\n" +
+            "        <if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">\n" +
             "            LEFT JOIN (\n" +
             "            SELECT\n" +
             "            sp.*,\n" +
@@ -170,9 +180,14 @@ public interface FsStoreOrderItemScrmMapper
             "            FROM fs_store_payment_scrm sp\n" +
             "            WHERE sp.business_code IS NOT NULL\n" +
             "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
+                        "<if test=\"maps.appId != null and maps.appId != ''\">" +
             "            LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id\n" +
+                        "</if>" +
             "        </if>" +
             "where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            "and sp_latest.bank_transaction_id = #{maps.bankTransactionId}\n" +
+            "</if>" +
             "<if test=\"maps.appId != null and maps.appId != ''\">\n" +
             "   and csc.appid = #{maps.appId}\n" +
             " </if>\n" +

+ 17 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -648,9 +648,24 @@ public interface FsStoreOrderScrmMapper
 
     @Select({"<script> " +
             "select o.*,cts.name as scheduleName,u.nickname,u.phone,cc.push_code,cc.create_time as customer_create_time,cc.source,cc.customer_code, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber ,p.title as package_title ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload  " +
-            " from fs_store_order_scrm o  left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id left join fs_user u on o.user_id=u.user_id  " +
-            " left join company c on c.company_id=o.company_id left join company_user cu on cu.user_id=o.company_user_id left join crm_customer cc on cc.customer_id=o.customer_id left join company_tcm_schedule cts on cts.id = o.schedule_id " +
+            " from fs_store_order_scrm o  " +
+            " left JOIN fs_store_product_package_scrm p on o.package_id=p.package_id " +
+            " left join fs_user u on o.user_id=u.user_id  " +
+            " left join company c on c.company_id=o.company_id " +
+            " left join company_user cu on cu.user_id=o.company_user_id " +
+            " left join crm_customer cc on cc.customer_id=o.customer_id " +
+            " left join company_tcm_schedule cts on cts.id = o.schedule_id " +
+            "            LEFT JOIN (\n" +
+            "            SELECT\n" +
+            "            sp.*,\n" +
+            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
+            "            FROM fs_store_payment_scrm sp\n" +
+            "            WHERE sp.business_code IS NOT NULL\n" +
+            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
             "where 1=1 " +
+            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
+            " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +

+ 58 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java

@@ -0,0 +1,58 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 直播完课积分领取记录对象 live_completion_points_record
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCompletionPointsRecord extends BaseEntity {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 直播ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 观看时长(秒) */
+    private Long watchDuration;
+
+    /** 视频总时长(秒) */
+    private Long videoDuration;
+
+    /** 完课比例(%) */
+    private BigDecimal completionRate;
+
+    /** 连续完课天数 */
+    private Integer continuousDays;
+
+    /** 获得积分 */
+    private Integer pointsAwarded;
+
+    /** 上次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date lastCompletionDate;
+
+    /** 本次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date currentCompletionDate;
+
+    /** 领取状态 0-未领取 1-已领取 */
+    private Integer receiveStatus;
+
+    /** 领取时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/live/domain/LiveOrder.java

@@ -352,4 +352,8 @@ public class LiveOrder extends BaseEntity {
     @TableField(exist = false)
     private Long attrValueId;
 
+    /** 小程序AppId */
+    @Excel(name = "小程序AppId")
+    private String appId;
+
 }

+ 1 - 1
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -27,7 +27,7 @@ public class LiveWatchConfig extends BaseEntity{
     private Boolean enabled;
 
     /** 参与条件 1达到指定观看时长 */
-    @Excel(name = "参与条件 1达到指定观看时长")
+    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分")
     private Long participateCondition;
 
     /** 观看时长 */

+ 53 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -0,0 +1,53 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Mapper接口
+ */
+public interface LiveCompletionPointsRecordMapper {
+
+    /**
+     * 插入完课积分记录
+     */
+    int insertRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 更新完课积分记录
+     */
+    int updateRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 查询用户某天的完课记录
+     */
+    LiveCompletionPointsRecord selectByUserAndDate(@Param("liveId") Long liveId, 
+                                                     @Param("userId") Long userId, 
+                                                     @Param("currentDate") Date currentDate);
+
+    /**
+     * 查询用户最近一次完课记录
+     */
+    LiveCompletionPointsRecord selectLatestByUser(@Param("liveId") Long liveId, 
+                                                   @Param("userId") Long userId);
+
+    /**
+     * 查询用户未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> selectUnreceivedByUser(@Param("liveId") Long liveId, 
+                                                             @Param("userId") Long userId);
+
+    /**
+     * 查询用户的完课积分领取记录列表
+     */
+    List<LiveCompletionPointsRecord> selectRecordsByUser(@Param("liveId") Long liveId, 
+                                                          @Param("userId") Long userId);
+
+    /**
+     * 根据ID查询
+     */
+    LiveCompletionPointsRecord selectById(@Param("id") Long id);
+}

+ 43 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java

@@ -0,0 +1,43 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service接口
+ */
+public interface ILiveCompletionPointsRecordService {
+
+    /**
+     * 检查并创建完课记录(定时任务调用)
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @param watchDuration 观看时长(秒)
+     */
+    void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 用户领取完课积分
+     * @param recordId 完课记录ID
+     * @param userId 用户ID
+     * @return 领取结果
+     */
+    LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId);
+
+    /**
+     * 获取用户完课状态
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId);
+
+    /**
+     * 查询用户积分领取记录
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId);
+}

+ 328 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -0,0 +1,328 @@
+package com.fs.live.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.base.BaseException;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import lombok.extern.slf4j.Slf4j;
+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;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service业务层处理
+ */
+@Slf4j
+@Service
+public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPointsRecordService {
+
+    @Autowired
+    private LiveCompletionPointsRecordMapper recordMapper;
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
+
+    /**
+     * 检查并创建完课记录(由定时任务调用)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration) {
+        try {
+            // 1. 获取直播信息和配置
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                log.warn("直播间不存在, liveId={}", liveId);
+                return;
+            }
+
+            // 2. 从数据库获取完课积分配置
+            CompletionPointsConfig config = getCompletionPointsConfig(live);
+            
+            // 检查是否开启完课积分功能
+            if (!config.isEnabled()) {
+                log.debug("直播间未开启完课积分功能, liveId={}", liveId);
+                return;
+            }
+            
+            // 检查配置完整性
+            Integer completionRate = config.getCompletionRate();
+            int[] pointsConfig = config.getPointsConfig();
+            
+            if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
+                log.warn("完课积分配置不完整, liveId={}, completionRate={}, pointsConfig={}", 
+                        liveId, completionRate, pointsConfig);
+                return;
+            }
+
+            // 3. 获取视频总时长(秒)
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                log.warn("直播间视频时长无效, liveId={}, duration={}", liveId, videoDuration);
+                return;
+            }
+
+            // 4. 计算完课比例
+            BigDecimal watchRate = BigDecimal.valueOf(watchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+
+            // 5. 判断是否达到完课标准
+            if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
+                log.debug("观看时长未达到完课标准, liveId={}, userId={}, watchRate={}%, required={}%",
+                        liveId, userId, watchRate, completionRate);
+                return;
+            }
+
+            // 6. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+            if (todayRecord != null) {
+                log.debug("今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            // 7. 查询最近一次完课记录,计算连续天数
+            LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(liveId, userId);
+            int continuousDays = 1;
+
+            if (latestRecord != null) {
+                LocalDate lastDate = latestRecord.getCurrentCompletionDate()
+                        .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+                long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
+
+                if (daysBetween == 1) {
+                    // 昨天完课了,连续天数+1
+                    continuousDays = latestRecord.getContinuousDays() + 1;
+                } else if (daysBetween > 1) {
+                    // 中断了,重新开始
+                    continuousDays = 1;
+                } else {
+                    // daysBetween == 0 说明今天已经有记录了(理论上不会进入这里,因为前面已经检查过)
+                    log.warn("异常情况: 今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                    return;
+                }
+            }
+
+            // 8. 计算积分
+            int points = calculatePoints(continuousDays, pointsConfig);
+
+            // 9. 创建完课记录
+            LiveCompletionPointsRecord record = new LiveCompletionPointsRecord();
+            record.setLiveId(liveId);
+            record.setUserId(userId);
+            record.setWatchDuration(watchDuration);
+            record.setVideoDuration(videoDuration);
+            record.setCompletionRate(watchRate);
+            record.setContinuousDays(continuousDays);
+            record.setPointsAwarded(points);
+            record.setCurrentCompletionDate(currentDate);
+            record.setReceiveStatus(0); // 未领取
+
+            if (latestRecord != null) {
+                record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
+            }
+
+            recordMapper.insertRecord(record);
+
+            log.info("创建完课记录成功, liveId={}, userId={}, continuousDays={}, points={}",
+                    liveId, userId, continuousDays, points);
+
+        } catch (Exception e) {
+            log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 用户领取完课积分
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId) {
+        // 1. 查询完课记录
+        LiveCompletionPointsRecord record = recordMapper.selectById(recordId);
+        if (record == null) {
+            throw new BaseException("完课记录不存在");
+        }
+
+        // 2. 校验用户
+        if (!record.getUserId().equals(userId)) {
+            throw new BaseException("无权领取该完课积分");
+        }
+
+        // 3. 校验领取状态
+        if (record.getReceiveStatus() == 1) {
+            throw new BaseException("该完课积分已领取");
+        }
+
+        // 4. 更新用户积分
+        FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+        if (user == null) {
+            throw new BaseException("用户不存在");
+        }
+
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + record.getPointsAwarded();
+
+        FsUser updateUser = new FsUser();
+        updateUser.setUserId(userId);
+        updateUser.setIntegral(newIntegral);
+        fsUserMapper.updateFsUser(updateUser);
+
+        // 5. 记录积分变动日志
+        FsUserIntegralLogs integralLog = new FsUserIntegralLogs();
+        integralLog.setUserId(userId);
+        integralLog.setIntegral(Long.valueOf(record.getPointsAwarded()));
+        integralLog.setBalance(newIntegral);
+        integralLog.setLogType(5); // 5-直播完课积分
+        integralLog.setBusinessId("live_completion_" + recordId); // 业务ID:直播完课记录ID
+        integralLog.setBusinessType(5); // 5-直播完课
+        integralLog.setStatus(1);
+        integralLog.setCreateTime(new Date());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog);
+
+        // 6. 更新完课记录状态
+        LiveCompletionPointsRecord updateRecord = new LiveCompletionPointsRecord();
+        updateRecord.setId(recordId);
+        updateRecord.setReceiveStatus(1);
+        updateRecord.setReceiveTime(new Date());
+        recordMapper.updateRecord(updateRecord);
+
+        // 7. 返回更新后的记录
+        record.setReceiveStatus(1);
+        record.setReceiveTime(new Date());
+
+        log.info("用户领取完课积分成功, userId={}, recordId={}, points={}", userId, recordId, record.getPointsAwarded());
+
+        return record;
+    }
+
+    /**
+     * 获取用户未领取的完课记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId) {
+        return recordMapper.selectUnreceivedByUser(liveId, userId);
+    }
+
+    /**
+     * 查询用户积分领取记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId) {
+        return recordMapper.selectRecordsByUser(liveId, userId);
+    }
+
+    /**
+     * 从直播配置中获取完课积分配置
+     */
+    private CompletionPointsConfig getCompletionPointsConfig(Live live) {
+        CompletionPointsConfig config = new CompletionPointsConfig();
+        config.setEnabled(false);
+        config.setCompletionRate(null);
+        config.setPointsConfig(null);
+        
+        String configJson = live.getConfigJson();
+        if (configJson == null || configJson.isEmpty()) {
+            return config;
+        }
+        
+        try {
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+
+            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+
+            Integer rate = jsonConfig.getInteger("completionRate");
+            if (rate != null && rate > 0 && rate <= 100) {
+                config.setCompletionRate(rate);
+            }
+
+            List<Integer> pointsList = jsonConfig.getObject("pointsConfig", List.class);
+            if (pointsList != null && !pointsList.isEmpty()) {
+                config.setPointsConfig(pointsList.stream().mapToInt(Integer::intValue).toArray());
+            }
+        } catch (Exception e) {
+            log.warn("解析完课积分配置失败, liveId={}, 配置未生效", live.getLiveId(), e);
+        }
+        
+        return config;
+    }
+    
+    /**
+     * 计算积分
+     * 根据连续天数和积分配置计算应得积分
+     * @param continuousDays 连续完课天数
+     * @param pointsConfig 积分配置数组
+     * @return 应得积分
+     */
+    private int calculatePoints(int continuousDays, int[] pointsConfig) {
+        if (continuousDays <= 0) {
+            return pointsConfig[0];
+        }
+        if (continuousDays > pointsConfig.length) {
+            // 超过配置天数,使用最后一天的积分
+            return pointsConfig[pointsConfig.length - 1];
+        }
+        return pointsConfig[continuousDays - 1];
+    }
+    
+    /**
+     * 完课积分配置内部类
+     */
+    private static class CompletionPointsConfig {
+        private boolean enabled;
+        private Integer completionRate;
+        private int[] pointsConfig;
+        
+        public boolean isEnabled() {
+            return enabled;
+        }
+        
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+        
+        public Integer getCompletionRate() {
+            return completionRate;
+        }
+        
+        public void setCompletionRate(Integer completionRate) {
+            this.completionRate = completionRate;
+        }
+        
+        public int[] getPointsConfig() {
+            return pointsConfig;
+        }
+        
+        public void setPointsConfig(int[] pointsConfig) {
+            this.pointsConfig = pointsConfig;
+        }
+    }
+}

+ 16 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -191,6 +191,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private FsStoreProductAttrValueScrmMapper fsStoreProductAttrValueMapper;
 
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
     @Autowired
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
 
@@ -1962,7 +1965,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
-        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf("-" + liveOrder.getTotalNum()),fsStoreProduct.getProductId());
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         goods.setSales(goods.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
@@ -2946,12 +2949,18 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     public R payConfirmReward(LiveOrder liveOrder) {
         Long orderId = liveOrder.getOrderId();
         if(orderId==null) return R.error("订单ID不存在");
+        // 保存传入的appId
+        String appId = liveOrder.getAppId();
         Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
         try {
             liveOrder = baseMapper.selectLiveOrderByOrderId(String.valueOf(orderId));
             if(liveOrder==null || !liveOrder.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())){
                 throw new CustomException("当前订单未找到或者订单状态不为待支付! orderId:" + orderId);
             }
+            // 设置appId
+            if (StringUtils.isNotEmpty(appId)) {
+                liveOrder.setAppId(appId);
+            }
             FsUserScrm user = userMapper.selectFsUserById(Long.valueOf(liveOrder.getUserId()));
             if(user == null) return R.error("用户不存在");
 //            String json = configService.selectConfigByKey("store.pay");
@@ -3103,6 +3112,10 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     order.setPayDelivery(order.getPayPrice().subtract(payMoney) );
 //                    order.setPayMoney(BigDecimal.ZERO);
                 }
+                // 保存appId到订单
+                if (StringUtils.isNotEmpty(param.getAppId())) {
+                    order.setAppId(param.getAppId());
+                }
                 baseMapper.updateLiveOrder(order);
             }
             String payCode = OrderCodeUtils.getOrderSn();
@@ -3564,7 +3577,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProduct.setSales(fsStoreProduct.getSales()+Integer.parseInt(liveOrder.getTotalNum()));
-        fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf("-" + liveOrder.getTotalNum()),fsStoreProduct.getProductId());
 
         // 更新直播间库存
         goods.setStock(goods.getStock()-Integer.parseInt(liveOrder.getTotalNum()));
@@ -3828,7 +3841,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             }
 
             // 更新商品库存
-            fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
+            fsStoreProductScrmMapper.incStockDecSales(Long.valueOf(liveOrder.getTotalNum()),fsStoreProduct.getProductId());
             goods.setStock(goods.getStock()+Long.parseLong(liveOrder.getTotalNum()));
             // 更新商品库存
             liveGoodsMapper.updateLiveGoods(goods);

+ 4 - 0
fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java

@@ -151,6 +151,10 @@ public class LiveAfterSalesVo {
 
     @Excel(name ="产品名称")
     private String productName;
+    
+    /** 产品名称查询参数(用于搜索) */
+    private String productNameQuery;
+    
     @Excel(name ="产品编码")
     private String productBarCode;
     @Excel(name ="规格")

+ 7 - 0
fs-service/src/main/resources/mapper/live/LiveAfterSalesMapper.xml

@@ -74,12 +74,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         left join company_user cu on cu.user_id = las.company_user_id
         left join company c on c.company_id = cu.company_id
         left join live_order_payment lop on lop.business_id = lo.order_id and lop.status in (1,-1)
+        <if test="productName != null and productName != ''">
+        left join live_order_item loi on loi.order_id = lo.order_id
+        </if>
 
         <where>
             <if test="liveId != null and liveId != ''"> and las.live_id = #{liveId}</if>
             <if test="companyUserNickName != null and companyUserNickName != ''"> and cu.nick_name like concat(#{companyUserNickName},'%')</if>
             <if test="storeId != null and storeId != ''"> and las.store_id = #{storeId}</if>
             <if test="orderCode != null and orderCode != ''"> and lo.order_code = #{orderCode}</if>
+            <if test="productNameQuery != null and productNameQuery != ''"> and JSON_UNQUOTE(JSON_EXTRACT(loi.json_info, '$.productName')) like concat('%', #{productNameQuery}, '%')</if>
             <if test="refundAmount != null "> and las.refund_amount = #{refundAmount}</if>
             <if test="refundType != null "> and las.refund_type = #{refundType}</if>
             <if test="status != null "> and las.status = #{status}</if>
@@ -105,6 +109,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="deptId != null "> and cu.dept_id = #{deptId}</if>
             <if test="userPhone != null "> and lo.user_phone like concat(#{userPhone},'%')</if>
         </where>
+        <if test="productName != null and productName != ''">
+        group by las.id
+        </if>
         order by las.create_time desc
     </select>
 

+ 116 - 0
fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml

@@ -0,0 +1,116 @@
+<?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.live.mapper.LiveCompletionPointsRecordMapper">
+
+    <resultMap type="com.fs.live.domain.LiveCompletionPointsRecord" id="LiveCompletionPointsRecordResult">
+        <id     property="id"                      column="id"    />
+        <result property="liveId"                  column="live_id"    />
+        <result property="userId"                  column="user_id"    />
+        <result property="watchDuration"           column="watch_duration"    />
+        <result property="videoDuration"           column="video_duration"    />
+        <result property="completionRate"          column="completion_rate"    />
+        <result property="continuousDays"          column="continuous_days"    />
+        <result property="pointsAwarded"           column="points_awarded"    />
+        <result property="lastCompletionDate"      column="last_completion_date"    />
+        <result property="currentCompletionDate"   column="current_completion_date"    />
+        <result property="receiveStatus"           column="receive_status"    />
+        <result property="receiveTime"             column="receive_time"    />
+        <result property="createTime"              column="create_time"    />
+        <result property="updateTime"              column="update_time"    />
+    </resultMap>
+
+    <!-- 插入完课积分记录 -->
+    <insert id="insertRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO live_completion_points_record (
+            live_id,
+            user_id,
+            watch_duration,
+            video_duration,
+            completion_rate,
+            continuous_days,
+            points_awarded,
+            last_completion_date,
+            current_completion_date,
+            receive_status,
+            receive_time,
+            create_time,
+            update_time
+        ) VALUES (
+            #{liveId},
+            #{userId},
+            #{watchDuration},
+            #{videoDuration},
+            #{completionRate},
+            #{continuousDays},
+            #{pointsAwarded},
+            #{lastCompletionDate},
+            #{currentCompletionDate},
+            #{receiveStatus},
+            #{receiveTime},
+            NOW(),
+            NOW()
+        )
+    </insert>
+
+    <!-- 更新完课积分记录 -->
+    <update id="updateRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord">
+        UPDATE live_completion_points_record
+        <set>
+            <if test="watchDuration != null">watch_duration = #{watchDuration},</if>
+            <if test="videoDuration != null">video_duration = #{videoDuration},</if>
+            <if test="completionRate != null">completion_rate = #{completionRate},</if>
+            <if test="continuousDays != null">continuous_days = #{continuousDays},</if>
+            <if test="pointsAwarded != null">points_awarded = #{pointsAwarded},</if>
+            <if test="lastCompletionDate != null">last_completion_date = #{lastCompletionDate},</if>
+            <if test="currentCompletionDate != null">current_completion_date = #{currentCompletionDate},</if>
+            <if test="receiveStatus != null">receive_status = #{receiveStatus},</if>
+            <if test="receiveTime != null">receive_time = #{receiveTime},</if>
+            update_time = NOW()
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <!-- 查询用户某天的完课记录 -->
+    <select id="selectByUserAndDate" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND current_completion_date = #{currentDate}
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户最近一次完课记录 -->
+    <select id="selectLatestByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户未领取的完课记录列表 -->
+    <select id="selectUnreceivedByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND receive_status = 0
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 查询用户的完课积分领取记录列表 -->
+    <select id="selectRecordsByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 根据ID查询 -->
+    <select id="selectById" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE id = #{id}
+    </select>
+
+</mapper>

+ 7 - 3
fs-service/src/main/resources/mapper/live/LiveOrderMapper.xml

@@ -78,6 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="companyName"    column="company_name"    />
         <result property="customerId"    column="customer_id"    />
         <result property="couponPrice"    column="coupon_price"    />
+        <result property="appId"    column="app_id"    />
     </resultMap>
 
     <sql id="selectLiveOrderVo">
@@ -92,7 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             a.pay_remain,	a.delivery_status,	a.delivery_pay_status,	a.delivery_pay_time,	a.delivery_type,
             a.delivery_pay_money,	a.delivery_import_time,	a.delivery_send_time,	a.is_after_sales,	a.dept_id,
             a.channel,	a.source,	a.bill_price,	a.total_postage,	a.pay_postage,	a.gain_integral,a.coupon_price,
-            a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id
+            a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id,a.app_id
         FROM
             live_order a LEFT JOIN fs_store_product_scrm b ON a.product_id = b.product_id
     </sql>
@@ -109,7 +110,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         a.pay_remain,	a.delivery_status,	a.delivery_pay_status,	a.delivery_pay_time,	a.delivery_type,
         a.delivery_pay_money,	a.delivery_import_time,	a.delivery_send_time,	a.is_after_sales,	a.dept_id,
         a.channel,	a.source,	a.bill_price,	a.total_postage,	a.pay_postage,	a.gain_integral,a.coupon_price,
-        a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id
+        a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id,a.app_id
         FROM
         live_order a LEFT JOIN fs_store_product_scrm b ON a.product_id = b.product_id
         left join company_user cu on a.company_user_id = cu.user_id
@@ -197,7 +198,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             a.pay_remain,	a.delivery_status,	a.delivery_pay_status,	a.delivery_pay_time,	a.delivery_type,
             a.delivery_pay_money,	a.delivery_import_time,	a.delivery_send_time,	a.is_after_sales,	a.dept_id,
             a.channel,	a.source,	a.bill_price,	a.total_postage,	a.pay_postage,	a.gain_integral,a.coupon_price,
-            a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id
+            a.use_integral,	a.pay_integral,	a.back_integral,	a.is_edit_money,	b.product_info as product_introduce,a.customer_id,a.app_id
         FROM
             live_order a LEFT JOIN fs_store_product_scrm b ON a.product_id = b.product_id
                          left join company_user cu on a.company_user_id = cu.user_id
@@ -279,6 +280,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="productId != null">product_id,</if>
             <if test="customerId != null">customer_id,</if>
             <if test="couponPrice != null">coupon_price,</if>
+            <if test="appId != null and appId != ''">app_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="liveId != null">#{liveId},</if>
@@ -351,6 +353,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="productId != null">#{productId},</if>
             <if test="customerId != null">#{customerId},</if>
             <if test="couponPrice != null">#{couponPrice},</if>
+            <if test="appId != null and appId != ''">#{appId},</if>
          </trim>
     </insert>
 
@@ -426,6 +429,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isEditMoney != null">is_edit_money = #{isEditMoney},</if>
             <if test="customerId != null">customer_id = #{customerId},</if>
             <if test="couponPrice != null">coupon_price = #{couponPrice},</if>
+            <if test="appId != null and appId != ''">app_id = #{appId},</if>
         </trim>
         where order_id = #{orderId}
     </update>

+ 92 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -0,0 +1,92 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直播完课积分Controller
+ */
+@RestController
+@RequestMapping("/app/live/completion")
+public class LiveCompletionPointsController extends AppBaseController {
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private IFsUserService fsUserService;
+
+    /**
+     * 领取完课积分
+     */
+    @PostMapping("/receive")
+    public R receive(@RequestParam Long recordId) {
+        Long userId = Long.parseLong(getUserId());
+        LiveCompletionPointsRecord record = completionPointsRecordService.receiveCompletionPoints(recordId, userId);
+        return R.ok().put("data", record);
+    }
+
+    /**
+     * 获取用户未领取的积分列表
+     */
+    @GetMapping("/unreceived")
+    public R getUnreceived(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+        return R.ok().put("data", records);
+    }
+
+    /**
+     * 查询用户积分领取记录
+     */
+    @GetMapping("/records")
+    public R getRecords(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
+        return R.ok().put("data", records);
+    }
+
+    /**
+     * 查询用户积分余额和看直播信息统计
+     */
+    @GetMapping("/info")
+    public R getInfo(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        
+        // 1. 获取用户积分余额
+        FsUser user = fsUserService.selectFsUserByUserId(userId);
+        Long integral = user != null && user.getIntegral() != null ? user.getIntegral() : 0L;
+        
+        // 2. 获取完课记录列表(包含已领取和未领取)
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
+        
+        // 3. 统计信息
+        long totalPoints = records.stream()
+                .filter(r -> r.getReceiveStatus() == 1)
+                .mapToLong(LiveCompletionPointsRecord::getPointsAwarded)
+                .sum();
+        
+        long unreceivedCount = records.stream()
+                .filter(r -> r.getReceiveStatus() == 0)
+                .count();
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("integral", integral);  // 当前积分余额
+        result.put("totalPoints", totalPoints);  // 本直播间累计获得积分
+        result.put("totalDays", records.size());  // 累计看直播天数
+        result.put("unreceivedCount", unreceivedCount);  // 未领取记录数
+        result.put("records", records);  // 完课记录列表
+        
+        return R.ok().put("data", result);
+    }
+}

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

@@ -274,6 +274,10 @@ public class LiveOrderController extends AppBaseController
     @RepeatSubmit
     public R createReward(@RequestBody LiveOrder liveOrder)
     {
+        // 校验appId
+        if (StringUtils.isEmpty(liveOrder.getAppId())) {
+            return R.error("appId不能为空");
+        }
         log.info("新增中奖订单: {}", JSON.toJSONString(liveOrder));
 
         liveOrder.setUserId(getUserId());
@@ -289,6 +293,10 @@ public class LiveOrderController extends AppBaseController
     @RepeatSubmit
     public R payConfirmReward(@RequestBody LiveOrder liveOrder)
     {
+        // 校验appId
+        if (StringUtils.isEmpty(liveOrder.getAppId())) {
+            return R.error("appId不能为空");
+        }
         log.info("新增订单: {}", JSON.toJSONString(liveOrder));
 
         liveOrder.setUserId(getUserId());
@@ -299,6 +307,10 @@ public class LiveOrderController extends AppBaseController
     @ApiOperation("创建订单")
     @PostMapping("/create")
     public R create(@Validated @RequestBody LiveOrder param, HttpServletRequest request){
+        // 校验appId
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
         String userId= getUserId();
         log.info("开始创建订单,登录用户id:{}", userId);
         param.setUserId(userId);
@@ -642,6 +654,10 @@ public class LiveOrderController extends AppBaseController
     @Transactional
     public R pay(HttpServletRequest request, @Validated @RequestBody LiveOrderPayParam param)
     {
+        // 校验appId
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
         Long orderId = param.getOrderId();
         logger.info("开始处理支付请求, 订单号: {}, 支付类型: {}", orderId, param.getPayType());
         try{