ソースを参照

课程打卡管理

wangxy 2 週間 前
コミット
4a80ead445
31 ファイル変更3671 行追加83 行削除
  1. 121 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseCheckinActivityController.java
  2. 101 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseCheckinPrizeController.java
  3. 106 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinActivity.java
  4. 63 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinDetail.java
  5. 80 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinPrize.java
  6. 121 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinReceive.java
  7. 76 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinUser.java
  8. 77 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinActivityMapper.java
  9. 74 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinDetailMapper.java
  10. 87 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinPrizeMapper.java
  11. 87 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinReceiveMapper.java
  12. 81 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinUserMapper.java
  13. 3 0
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  14. 107 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinActivityService.java
  15. 86 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinPrizeService.java
  16. 120 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinReceiveService.java
  17. 82 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinUserService.java
  18. 415 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinActivityServiceImpl.java
  19. 167 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinPrizeServiceImpl.java
  20. 500 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinReceiveServiceImpl.java
  21. 295 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinUserServiceImpl.java
  22. 42 25
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  23. 52 0
      fs-service/src/main/java/com/fs/course/vo/CourseCheckinResultVO.java
  24. 130 0
      fs-service/src/main/resources/mapper/course/FsCourseCheckinActivityMapper.xml
  25. 93 0
      fs-service/src/main/resources/mapper/course/FsCourseCheckinDetailMapper.xml
  26. 128 0
      fs-service/src/main/resources/mapper/course/FsCourseCheckinPrizeMapper.xml
  27. 170 0
      fs-service/src/main/resources/mapper/course/FsCourseCheckinReceiveMapper.xml
  28. 117 0
      fs-service/src/main/resources/mapper/course/FsCourseCheckinUserMapper.xml
  29. 3 3
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  30. 50 55
      fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  31. 37 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

+ 121 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseCheckinActivityController.java

@@ -0,0 +1,121 @@
+package com.fs.course.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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.FsCourseCheckinActivity;
+import com.fs.course.domain.FsCourseCheckinPrize;
+import com.fs.course.service.IFsCourseCheckinActivityService;
+import com.fs.course.service.IFsCourseCheckinPrizeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 看课打卡活动Controller
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@RestController
+@RequestMapping("/course/checkinActivity")
+public class FsCourseCheckinActivityController extends BaseController {
+
+    @Autowired
+    private IFsCourseCheckinActivityService fsCourseCheckinActivityService;
+
+    @Autowired
+    private IFsCourseCheckinPrizeService fsCourseCheckinPrizeService;
+
+    /**
+     * 查询看课打卡活动列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsCourseCheckinActivity fsCourseCheckinActivity) {
+        startPage();
+        List<FsCourseCheckinActivity> list = fsCourseCheckinActivityService.selectFsCourseCheckinActivityList(fsCourseCheckinActivity);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出看课打卡活动列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:export')")
+    @Log(title = "看课打卡活动", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsCourseCheckinActivity fsCourseCheckinActivity) {
+        List<FsCourseCheckinActivity> list = fsCourseCheckinActivityService.selectFsCourseCheckinActivityList(fsCourseCheckinActivity);
+        ExcelUtil<FsCourseCheckinActivity> util = new ExcelUtil<FsCourseCheckinActivity>(FsCourseCheckinActivity.class);
+        return util.exportExcel(list, "看课打卡活动数据");
+    }
+
+    /**
+     * 获取看课打卡活动详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:query')")
+    @GetMapping(value = "/{activityId}")
+    public AjaxResult getInfo(@PathVariable("activityId") Long activityId) {
+        FsCourseCheckinActivity activity = fsCourseCheckinActivityService.selectFsCourseCheckinActivityByActivityId(activityId);
+        return AjaxResult.success(activity);
+    }
+
+    /**
+     * 新增看课打卡活动
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:add')")
+    @Log(title = "看课打卡活动", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/add")
+    public AjaxResult add(@RequestBody FsCourseCheckinActivity fsCourseCheckinActivity) {
+        // 提取奖品列表
+        List<FsCourseCheckinPrize> prizeList = fsCourseCheckinActivity.getPrizeList();
+        return toAjax(fsCourseCheckinActivityService.insertFsCourseCheckinActivityWithPrizes(fsCourseCheckinActivity, prizeList));
+    }
+
+    /**
+     * 修改看课打卡活动
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:edit')")
+    @Log(title = "看课打卡活动", businessType = BusinessType.UPDATE)
+    @PutMapping(value = "/edit")
+    public AjaxResult edit(@RequestBody FsCourseCheckinActivity fsCourseCheckinActivity) {
+        // 提取奖品列表
+        List<FsCourseCheckinPrize> prizeList = fsCourseCheckinActivity.getPrizeList();
+        return toAjax(fsCourseCheckinActivityService.updateFsCourseCheckinActivityWithPrizes(fsCourseCheckinActivity, prizeList));
+    }
+
+    /**
+     * 删除看课打卡活动
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:remove')")
+    @Log(title = "看课打卡活动", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{activityIds}")
+    public AjaxResult remove(@PathVariable Long[] activityIds) {
+        return toAjax(fsCourseCheckinActivityService.deleteFsCourseCheckinActivityByActivityIds(activityIds));
+    }
+
+    /**
+     * 停用看课打卡活动
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:edit')")
+    @Log(title = "看课打卡活动", businessType = BusinessType.UPDATE)
+    @PutMapping("/stop/{activityId}")
+    public AjaxResult stop(@PathVariable("activityId") Long activityId) {
+        return toAjax(fsCourseCheckinActivityService.stopActivity(activityId));
+    }
+
+    /**
+     * 获取活动奖品列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinActivity:query')")
+    @GetMapping("/prize/list/{activityId}")
+    public AjaxResult prizeList(@PathVariable("activityId") Long activityId) {
+        List<FsCourseCheckinPrize> list = fsCourseCheckinPrizeService.selectFsCourseCheckinPrizeByActivityId(activityId);
+        return AjaxResult.success(list);
+    }
+}

+ 101 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseCheckinPrizeController.java

@@ -0,0 +1,101 @@
+package com.fs.course.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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.FsCourseCheckinPrize;
+import com.fs.course.service.IFsCourseCheckinPrizeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 看课打卡活动奖品Controller
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@RestController
+@RequestMapping("/course/checkinPrize")
+public class FsCourseCheckinPrizeController extends BaseController {
+
+    @Autowired
+    private IFsCourseCheckinPrizeService fsCourseCheckinPrizeService;
+
+    /**
+     * 查询看课打卡活动奖品列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsCourseCheckinPrize fsCourseCheckinPrize) {
+        startPage();
+        List<FsCourseCheckinPrize> list = fsCourseCheckinPrizeService.selectFsCourseCheckinPrizeList(fsCourseCheckinPrize);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出看课打卡活动奖品列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:export')")
+    @Log(title = "看课打卡活动奖品", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsCourseCheckinPrize fsCourseCheckinPrize) {
+        List<FsCourseCheckinPrize> list = fsCourseCheckinPrizeService.selectFsCourseCheckinPrizeList(fsCourseCheckinPrize);
+        ExcelUtil<FsCourseCheckinPrize> util = new ExcelUtil<FsCourseCheckinPrize>(FsCourseCheckinPrize.class);
+        return util.exportExcel(list, "看课打卡活动奖品数据");
+    }
+
+    /**
+     * 获取看课打卡活动奖品详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:query')")
+    @GetMapping(value = "/{prizeId}")
+    public AjaxResult getInfo(@PathVariable("prizeId") Long prizeId) {
+        return AjaxResult.success(fsCourseCheckinPrizeService.selectFsCourseCheckinPrizeByPrizeId(prizeId));
+    }
+
+    /**
+     * 新增看课打卡活动奖品
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:add')")
+    @Log(title = "看课打卡活动奖品", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsCourseCheckinPrize fsCourseCheckinPrize) {
+        return toAjax(fsCourseCheckinPrizeService.insertFsCourseCheckinPrize(fsCourseCheckinPrize));
+    }
+
+    /**
+     * 修改看课打卡活动奖品
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:edit')")
+    @Log(title = "看课打卡活动奖品", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsCourseCheckinPrize fsCourseCheckinPrize) {
+        return toAjax(fsCourseCheckinPrizeService.updateFsCourseCheckinPrize(fsCourseCheckinPrize));
+    }
+
+    /**
+     * 删除看课打卡活动奖品
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:remove')")
+    @Log(title = "看课打卡活动奖品", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{prizeIds}")
+    public AjaxResult remove(@PathVariable Long[] prizeIds) {
+        return toAjax(fsCourseCheckinPrizeService.deleteFsCourseCheckinPrizeByPrizeIds(prizeIds));
+    }
+
+    /**
+     * 根据活动ID查询奖品列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:checkinPrize:list')")
+    @GetMapping("/activity/{activityId}")
+    public AjaxResult listByActivityId(@PathVariable("activityId") Long activityId) {
+        List<FsCourseCheckinPrize> list = fsCourseCheckinPrizeService.selectFsCourseCheckinPrizeByActivityId(activityId);
+        return AjaxResult.success(list);
+    }
+}

+ 106 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinActivity.java

@@ -0,0 +1,106 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 看课打卡活动对象 fs_course_checkin_activity
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Data
+public class FsCourseCheckinActivity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 活动ID */
+    @TableId(type = IdType.AUTO)
+    private Long activityId;
+
+    /** 活动名称 */
+    @Excel(name = "活动名称")
+    private String activityName;
+
+    /** 活动开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "活动开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 活动结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "活动结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 需要打卡天数 */
+    @Excel(name = "需要打卡天数")
+    private Integer checkinDays;
+
+    /** 参与公司ID集合,逗号分隔 */
+    private String companyIds;
+
+    /** 参与项目ID集合,逗号分隔 */
+    private String projectIds;
+
+    /** 通知模板 */
+    private String notifyTemplate;
+
+    /** 状态:0-未开始,1-进行中,2-已结束,3-已停用 */
+    @Excel(name = "状态", readConverterExp = "0=未开始,1=进行中,2=已结束,3=已停用")
+    private Integer status;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 请求参数 */
+    private Map<String, Object> params;
+
+    // ========== 非数据库字段 ==========
+
+    /** 公司名称列表(用于展示) */
+    @Excel(name = "参与公司")
+    private String companyNames;
+
+    /** 项目名称列表(用于展示) */
+    @Excel(name = "参与项目")
+    private String projectNames;
+
+    /** 状态文本 */
+    private String statusText;
+
+    /** 奖品列表(非数据库字段) */
+    private List<FsCourseCheckinPrize> prizeList;
+
+    public Map<String, Object> getParams() {
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params) {
+        this.params = params;
+    }
+}

+ 63 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinDetail.java

@@ -0,0 +1,63 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 看课打卡详情对象 fs_course_checkin_detail
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class FsCourseCheckinDetail implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 打卡详情ID */
+    @TableId(type = IdType.AUTO)
+    private Long detailId;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 项目ID */
+    private Long projectId;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 打卡时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date checkinTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 请求参数 */
+    private Map<String, Object> params;
+
+    public Map<String, Object> getParams() {
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params) {
+        this.params = params;
+    }
+}

+ 80 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinPrize.java

@@ -0,0 +1,80 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 看课打卡活动奖品对象 fs_course_checkin_prize
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Data
+public class FsCourseCheckinPrize implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 奖品ID */
+    @TableId(type = IdType.AUTO)
+    private Long prizeId;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 奖品类型:1-红包,2-积分商品券 */
+    @Excel(name = "奖品类型", readConverterExp = "1=红包,2=积分商品券")
+    private Integer prizeType;
+
+    /** 奖品名称 */
+    @Excel(name = "奖品名称")
+    private String prizeName;
+
+    /** 红包金额(元),prize_type=1时有效 */
+    @Excel(name = "红包金额")
+    private BigDecimal redpacketAmount;
+
+    /** 积分商品ID,关联fs_integral_goods,prize_type=2时有效 */
+    private Long goodsId;
+
+    /** 排序 */
+    private Integer sort;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 请求参数 */
+    private Map<String, Object> params;
+
+    public Map<String, Object> getParams() {
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params) {
+        this.params = params;
+    }
+}

+ 121 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinReceive.java

@@ -0,0 +1,121 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 看课打卡奖品领取记录对象 fs_course_checkin_receive
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Data
+public class FsCourseCheckinReceive implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 领取记录ID */
+    @TableId(type = IdType.AUTO)
+    private Long receiveId;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 奖品ID */
+    private Long prizeId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 项目ID */
+    private Long projectId;
+
+    /** 奖品类型:1-红包,2-积分商品券 */
+    @Excel(name = "奖品类型", readConverterExp = "1=红包,2=积分商品券")
+    private Integer prizeType;
+
+    /** 奖品名称 */
+    @Excel(name = "奖品名称")
+    private String prizeName;
+
+    /** 红包金额 */
+    @Excel(name = "红包金额")
+    private BigDecimal redpacketAmount;
+
+    /** 积分商品ID */
+    private Long goodsId;
+
+    /** 领取状态:0-待发放,1-发放成功,2-发放失败 */
+    @Excel(name = "领取状态", readConverterExp = "0=待发放,1=发放成功,2=发放失败")
+    private Integer receiveStatus;
+
+    /** 领取/发放时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "发放时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+
+    /** 微信红包订单号 */
+    private String redpacketBillNo;
+
+    /** 优惠券码 */
+    private String couponCode;
+
+    /** 通知状态:0-未通知,1-已通知,2-通知失败 */
+    @Excel(name = "通知状态", readConverterExp = "0=未通知,1=已通知,2=通知失败")
+    private Integer notifyStatus;
+
+    /** 通知时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date notifyTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 请求参数 */
+    private Map<String, Object> params;
+
+    // ========== 非数据库字段 ==========
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String nickName;
+
+    /** 用户手机号 */
+    @Excel(name = "用户手机号")
+    private String phone;
+
+    /** 活动名称 */
+    @Excel(name = "活动名称")
+    private String activityName;
+
+    private String outBatchNo;
+
+    private  String batchId;
+
+    public Map<String, Object> getParams() {
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params) {
+        this.params = params;
+    }
+}

+ 76 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseCheckinUser.java

@@ -0,0 +1,76 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 看课打卡用户统计对象 fs_course_checkin_user
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Data
+public class FsCourseCheckinUser implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 项目ID */
+    private Long projectId;
+
+    /** 已打卡天数 */
+    private Integer checkinDays;
+
+    /** 首次打卡时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date firstCheckinTime;
+
+    /** 最后打卡时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastCheckinTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 课程id
+     */
+    private Long courseId;
+
+    /** 请求参数 */
+    private Map<String, Object> params;
+
+    public Map<String, Object> getParams() {
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params) {
+        this.params = params;
+    }
+}

+ 77 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinActivityMapper.java

@@ -0,0 +1,77 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsCourseCheckinActivity;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 看课打卡活动Mapper接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface FsCourseCheckinActivityMapper {
+
+    /**
+     * 查询看课打卡活动
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 看课打卡活动
+     */
+    FsCourseCheckinActivity selectFsCourseCheckinActivityByActivityId(Long activityId);
+
+    /**
+     * 查询看课打卡活动列表
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 看课打卡活动集合
+     */
+    List<FsCourseCheckinActivity> selectFsCourseCheckinActivityList(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 新增看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    int insertFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 修改看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    int updateFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 删除看课打卡活动
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinActivityByActivityId(Long activityId);
+
+    /**
+     * 批量删除看课打卡活动
+     *
+     * @param activityIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinActivityByActivityIds(Long[] activityIds);
+
+    /**
+     * 查询当前时间范围内、指定公司和项目的进行中的活动
+     *
+     * @param companyId   公司ID
+     * @param projectId   项目ID
+     * @param currentDate 当前日期(只精确到年月日)
+     * @return 活动列表
+     */
+    List<FsCourseCheckinActivity> selectActiveActivities(
+                                                          @Param("companyId") Long companyId,
+                                                          @Param("projectId") Long projectId,
+                                                          @Param("currentDate") Date currentDate);
+}

+ 74 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinDetailMapper.java

@@ -0,0 +1,74 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsCourseCheckinDetail;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 看课打卡详情Mapper接口
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+public interface FsCourseCheckinDetailMapper {
+    
+    /**
+     * 查询看课打卡详情
+     *
+     * @param detailId 看课打卡详情主键
+     * @return 看课打卡详情
+     */
+    FsCourseCheckinDetail selectFsCourseCheckinDetailByDetailId(Long detailId);
+
+    /**
+     * 查询看课打卡详情列表
+     *
+     * @param fsCourseCheckinDetail 看课打卡详情
+     * @return 看课打卡详情集合
+     */
+    List<FsCourseCheckinDetail> selectFsCourseCheckinDetailList(FsCourseCheckinDetail fsCourseCheckinDetail);
+
+    /**
+     * 新增看课打卡详情
+     *
+     * @param fsCourseCheckinDetail 看课打卡详情
+     * @return 结果
+     */
+    int insertFsCourseCheckinDetail(FsCourseCheckinDetail fsCourseCheckinDetail);
+
+    /**
+     * 修改看课打卡详情
+     *
+     * @param fsCourseCheckinDetail 看课打卡详情
+     * @return 结果
+     */
+    int updateFsCourseCheckinDetail(FsCourseCheckinDetail fsCourseCheckinDetail);
+
+    /**
+     * 删除看课打卡详情
+     *
+     * @param detailId 看课打卡详情主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinDetailByDetailId(Long detailId);
+
+    /**
+     * 批量删除看课打卡详情
+     *
+     * @param detailIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinDetailByDetailIds(Long[] detailIds);
+
+    /**
+     * 查询用户在某活动某天某课程的打卡记录
+     *
+     * @param activityId 活动ID
+     * @param userId 用户ID
+     * @param courseId 课程ID
+     * @param checkinTime 打卡时间
+     * @return 打卡详情
+     */
+    FsCourseCheckinDetail selectByActivityUserCourseAndTime(Long activityId, Long userId, Long courseId, Date checkinTime);
+}

+ 87 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinPrizeMapper.java

@@ -0,0 +1,87 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsCourseCheckinPrize;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 看课打卡活动奖品Mapper接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface FsCourseCheckinPrizeMapper {
+
+    /**
+     * 查询看课打卡活动奖品
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 看课打卡活动奖品
+     */
+    FsCourseCheckinPrize selectFsCourseCheckinPrizeByPrizeId(Long prizeId);
+
+    /**
+     * 查询看课打卡活动奖品列表
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 看课打卡活动奖品集合
+     */
+    List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeList(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 根据活动ID查询奖品列表
+     *
+     * @param activityId 活动ID
+     * @return 奖品列表
+     */
+    List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeByActivityId(Long activityId);
+
+    /**
+     * 新增看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    int insertFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 修改看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    int updateFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 删除看课打卡活动奖品
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByPrizeId(Long prizeId);
+
+    /**
+     * 批量删除看课打卡活动奖品
+     *
+     * @param prizeIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByPrizeIds(Long[] prizeIds);
+
+    /**
+     * 根据活动ID删除奖品
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByActivityId(Long activityId);
+
+    /**
+     * 批量新增奖品
+     *
+     * @param prizeList 奖品列表
+     * @return 结果
+     */
+    int batchInsertFsCourseCheckinPrize(@Param("list") List<FsCourseCheckinPrize> prizeList);
+}

+ 87 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinReceiveMapper.java

@@ -0,0 +1,87 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsCourseCheckinReceive;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 看课打卡奖品领取记录Mapper接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface FsCourseCheckinReceiveMapper {
+
+    /**
+     * 查询看课打卡奖品领取记录
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 看课打卡奖品领取记录
+     */
+    FsCourseCheckinReceive selectFsCourseCheckinReceiveByReceiveId(Long receiveId);
+
+    /**
+     * 查询看课打卡奖品领取记录列表
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 看课打卡奖品领取记录集合
+     */
+    List<FsCourseCheckinReceive> selectFsCourseCheckinReceiveList(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 查询待发放的领取记录
+     *
+     * @param limit 限制数量
+     * @return 领取记录列表
+     */
+    List<FsCourseCheckinReceive> selectPendingReceiveList(@Param("limit") Integer limit);
+
+    /**
+     * 新增看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    int insertFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 修改看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    int updateFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 删除看课打卡奖品领取记录
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByReceiveId(Long receiveId);
+
+    /**
+     * 批量删除看课打卡奖品领取记录
+     *
+     * @param receiveIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByReceiveIds(Long[] receiveIds);
+
+    /**
+     * 根据活动ID删除领取记录
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByActivityId(Long activityId);
+
+    /**
+     * 批量新增领取记录
+     *
+     * @param receiveList 领取记录列表
+     * @return 结果
+     */
+    int batchInsertFsCourseCheckinReceive(@Param("list") List<FsCourseCheckinReceive> receiveList);
+}

+ 81 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseCheckinUserMapper.java

@@ -0,0 +1,81 @@
+package com.fs.course.mapper;
+
+import com.fs.course.domain.FsCourseCheckinUser;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 看课打卡用户统计Mapper接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface FsCourseCheckinUserMapper {
+
+    /**
+     * 查询看课打卡用户统计
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 看课打卡用户统计
+     */
+    FsCourseCheckinUser selectFsCourseCheckinUserById(Long id);
+
+    /**
+     * 根据活动ID和用户ID查询打卡统计
+     *
+     * @param activityId 活动ID
+     * @param userId     用户ID
+     * @return 打卡统计
+     */
+    FsCourseCheckinUser selectByActivityAndUser(@Param("activityId") Long activityId,
+                                                 @Param("userId") Long userId);
+
+    /**
+     * 查询看课打卡用户统计列表
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 看课打卡用户统计集合
+     */
+    List<FsCourseCheckinUser> selectFsCourseCheckinUserList(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 新增看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    int insertFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 修改看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    int updateFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 删除看课打卡用户统计
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinUserById(Long id);
+
+    /**
+     * 批量删除看课打卡用户统计
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinUserByIds(Long[] ids);
+
+    /**
+     * 根据活动ID删除用户统计
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int deleteFsCourseCheckinUserByActivityId(Long activityId);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java

@@ -39,4 +39,7 @@ public class FsCourseSendRewardUParam implements Serializable
 
     private String code;
 
+    private Long  activityId;
+
+
 }

+ 107 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinActivityService.java

@@ -0,0 +1,107 @@
+package com.fs.course.service;
+
+import com.fs.course.domain.FsCourseCheckinActivity;
+import com.fs.course.domain.FsCourseCheckinPrize;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 看课打卡活动Service接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface IFsCourseCheckinActivityService {
+
+    /**
+     * 查询看课打卡活动
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 看课打卡活动
+     */
+    FsCourseCheckinActivity selectFsCourseCheckinActivityByActivityId(Long activityId);
+
+    /**
+     * 查询看课打卡活动列表
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 看课打卡活动集合
+     */
+    List<FsCourseCheckinActivity> selectFsCourseCheckinActivityList(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 新增看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    int insertFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 新增看课打卡活动(带奖品)
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @param prizeList               奖品列表
+     * @return 结果
+     */
+    int insertFsCourseCheckinActivityWithPrizes(FsCourseCheckinActivity fsCourseCheckinActivity,
+                                                 List<FsCourseCheckinPrize> prizeList);
+
+    /**
+     * 修改看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    int updateFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity);
+
+    /**
+     * 修改看课打卡活动(带奖品)
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @param prizeList               奖品列表
+     * @return 结果
+     */
+    int updateFsCourseCheckinActivityWithPrizes(FsCourseCheckinActivity fsCourseCheckinActivity,
+                                                 List<FsCourseCheckinPrize> prizeList);
+
+    /**
+     * 批量删除看课打卡活动
+     *
+     * @param activityIds 需要删除的看课打卡活动主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinActivityByActivityIds(Long[] activityIds);
+
+    /**
+     * 删除看课打卡活动信息
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinActivityByActivityId(Long activityId);
+
+    /**
+     * 查询当前时间范围内、指定公司和项目的进行中的活动
+     *
+     * @param currentTime 当前时间
+     * @param companyId   公司ID
+     * @param projectId   项目ID
+     * @return 活动列表
+     */
+    List<FsCourseCheckinActivity> selectActiveActivities(Date currentTime, Long companyId, Long projectId);
+
+    /**
+     * 更新活动状态(根据时间自动更新)
+     */
+    void updateActivityStatusByTime();
+
+    /**
+     * 停用活动
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int stopActivity(Long activityId);
+}

+ 86 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinPrizeService.java

@@ -0,0 +1,86 @@
+package com.fs.course.service;
+
+import com.fs.course.domain.FsCourseCheckinPrize;
+
+import java.util.List;
+
+/**
+ * 看课打卡活动奖品Service接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface IFsCourseCheckinPrizeService {
+
+    /**
+     * 查询看课打卡活动奖品
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 看课打卡活动奖品
+     */
+    FsCourseCheckinPrize selectFsCourseCheckinPrizeByPrizeId(Long prizeId);
+
+    /**
+     * 查询看课打卡活动奖品列表
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 看课打卡活动奖品集合
+     */
+    List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeList(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 根据活动ID查询奖品列表
+     *
+     * @param activityId 活动ID
+     * @return 奖品列表
+     */
+    List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeByActivityId(Long activityId);
+
+    /**
+     * 新增看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    int insertFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 批量新增奖品
+     *
+     * @param prizeList 奖品列表
+     * @return 结果
+     */
+    int batchInsertFsCourseCheckinPrize(List<FsCourseCheckinPrize> prizeList);
+
+    /**
+     * 修改看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    int updateFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize);
+
+    /**
+     * 批量删除看课打卡活动奖品
+     *
+     * @param prizeIds 需要删除的看课打卡活动奖品主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByPrizeIds(Long[] prizeIds);
+
+    /**
+     * 删除看课打卡活动奖品信息
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByPrizeId(Long prizeId);
+
+    /**
+     * 根据活动ID删除奖品
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int deleteFsCourseCheckinPrizeByActivityId(Long activityId);
+}

+ 120 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinReceiveService.java

@@ -0,0 +1,120 @@
+package com.fs.course.service;
+
+import com.fs.course.domain.FsCourseCheckinReceive;
+import com.fs.course.param.FsCourseSendRewardUParam;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 看课打卡奖品领取记录Service接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface IFsCourseCheckinReceiveService {
+
+    /**
+     * 查询看课打卡奖品领取记录
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 看课打卡奖品领取记录
+     */
+    FsCourseCheckinReceive selectFsCourseCheckinReceiveByReceiveId(Long receiveId);
+
+    /**
+     * 查询看课打卡奖品领取记录列表
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 看课打卡奖品领取记录集合
+     */
+    List<FsCourseCheckinReceive> selectFsCourseCheckinReceiveList(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 查询待发放的领取记录
+     *
+     * @param limit 限制数量
+     * @return 领取记录列表
+     */
+    List<FsCourseCheckinReceive> selectPendingReceiveList(Integer limit);
+
+    /**
+     * 新增看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    int insertFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 批量新增领取记录
+     *
+     * @param receiveList 领取记录列表
+     * @return 结果
+     */
+    int batchInsertFsCourseCheckinReceive(List<FsCourseCheckinReceive> receiveList);
+
+    /**
+     * 修改看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    int updateFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive);
+
+    /**
+     * 批量删除看课打卡奖品领取记录
+     *
+     * @param receiveIds 需要删除的看课打卡奖品领取记录主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByReceiveIds(Long[] receiveIds);
+
+    /**
+     * 删除看课打卡奖品领取记录信息
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByReceiveId(Long receiveId);
+
+    /**
+     * 根据活动ID删除领取记录
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    int deleteFsCourseCheckinReceiveByActivityId(Long activityId);
+
+    /**
+     * 检查并创建奖品领取记录
+     * 当用户打卡天数达到要求时,自动创建领取记录
+     *
+     */
+    Map<String, Object> checkAndCreateReceiveRecord(FsCourseSendRewardUParam fsCourseSendRewardUParam);
+
+    /**
+     * 统计用户在某活动下的领取记录数量
+     * @param activityId 活动ID
+     * @param userId 用户ID
+     * @return 领取记录数量
+     */
+    int countReceiveByActivityAndUser(Long activityId, Long userId);
+
+
+    /**
+     * 批量发放待发放的奖品
+     *
+     * @param limit 限制数量
+     * @return 发放成功数量
+     */
+    int batchSendPendingPrizes(Integer limit);
+
+    /**
+     * 发送通知给用户
+     *
+     * @param receiveId 领取记录ID
+     * @return 结果
+     */
+    boolean sendNotify(Long receiveId);
+}

+ 82 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseCheckinUserService.java

@@ -0,0 +1,82 @@
+package com.fs.course.service;
+
+import com.fs.course.domain.FsCourseCheckinUser;
+import com.fs.course.vo.CourseCheckinResultVO;
+
+import java.util.List;
+
+/**
+ * 看课打卡用户统计Service接口
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+public interface IFsCourseCheckinUserService {
+
+    /**
+     * 查询看课打卡用户统计
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 看课打卡用户统计
+     */
+    FsCourseCheckinUser selectFsCourseCheckinUserById(Long id);
+
+    /**
+     * 根据活动ID和用户ID查询打卡统计
+     *
+     * @param activityId 活动ID
+     * @param userId     用户ID
+     * @return 打卡统计
+     */
+    FsCourseCheckinUser selectByActivityAndUser(Long activityId, Long userId);
+
+    /**
+     * 查询看课打卡用户统计列表
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 看课打卡用户统计集合
+     */
+    List<FsCourseCheckinUser> selectFsCourseCheckinUserList(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 新增看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    int insertFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 修改看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    int updateFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser);
+
+    /**
+     * 批量删除看课打卡用户统计
+     *
+     * @param ids 需要删除的看课打卡用户统计主键集合
+     * @return 结果
+     */
+    int deleteFsCourseCheckinUserByIds(Long[] ids);
+
+    /**
+     * 删除看课打卡用户统计信息
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 结果
+     */
+    int deleteFsCourseCheckinUserById(Long id);
+
+    /**
+     * 用户看课打卡处理
+     *
+     * @param userId    用户ID
+     * @param companyId 公司ID
+     * @param projectId 项目ID
+     * @return 打卡结果,包含提醒信息
+     */
+    CourseCheckinResultVO handleCourseCheckin(Long userId, Long companyId, Long projectId,Long courseId);
+}

+ 415 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinActivityServiceImpl.java

@@ -0,0 +1,415 @@
+package com.fs.course.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.DictUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.course.domain.FsCourseCheckinActivity;
+import com.fs.course.domain.FsCourseCheckinPrize;
+import com.fs.course.mapper.FsCourseCheckinActivityMapper;
+import com.fs.course.mapper.FsCourseCheckinPrizeMapper;
+import com.fs.course.mapper.FsCourseCheckinReceiveMapper;
+import com.fs.course.mapper.FsCourseCheckinUserMapper;
+import com.fs.course.service.IFsCourseCheckinActivityService;
+import com.fs.his.domain.FsIntegralGoods;
+import com.fs.his.mapper.FsIntegralGoodsMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 看课打卡活动Service业务层处理
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Service
+public class FsCourseCheckinActivityServiceImpl implements IFsCourseCheckinActivityService {
+
+    @Autowired
+    private FsCourseCheckinActivityMapper fsCourseCheckinActivityMapper;
+
+    @Autowired
+    private FsCourseCheckinPrizeMapper fsCourseCheckinPrizeMapper;
+
+    @Autowired
+    private FsCourseCheckinUserMapper fsCourseCheckinUserMapper;
+
+    @Autowired
+    private FsCourseCheckinReceiveMapper fsCourseCheckinReceiveMapper;
+
+    @Autowired
+    private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+
+    /**
+     * 查询看课打卡活动
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 看课打卡活动
+     */
+    @Override
+    public FsCourseCheckinActivity selectFsCourseCheckinActivityByActivityId(Long activityId) {
+        FsCourseCheckinActivity activity = fsCourseCheckinActivityMapper.selectFsCourseCheckinActivityByActivityId(activityId);
+        if (activity != null) {
+            // 查询奖品列表
+            List<FsCourseCheckinPrize> prizeList = fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeByActivityId(activityId);
+            activity.setPrizeList(prizeList);
+            // 设置状态文本
+            activity.setStatusText(getStatusText(activity.getStatus()));
+            // 设置公司名称和项目名称
+            activity.setCompanyNames(getCompanyNames(activity.getCompanyIds()));
+            activity.setProjectNames(getProjectNames(activity.getProjectIds()));
+        }
+        return activity;
+    }
+
+    /**
+     * 查询看课打卡活动列表
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 看课打卡活动集合
+     */
+    @Override
+    public List<FsCourseCheckinActivity> selectFsCourseCheckinActivityList(FsCourseCheckinActivity fsCourseCheckinActivity) {
+        List<FsCourseCheckinActivity> list = fsCourseCheckinActivityMapper.selectFsCourseCheckinActivityList(fsCourseCheckinActivity);
+        // 处理状态文本和公司、项目名称
+        for (FsCourseCheckinActivity activity : list) {
+            activity.setStatusText(getStatusText(activity.getStatus()));
+            // 设置公司名称
+            activity.setCompanyNames(getCompanyNames(activity.getCompanyIds()));
+            // 设置项目名称
+            activity.setProjectNames(getProjectNames(activity.getProjectIds()));
+        }
+        return list;
+    }
+
+    /**
+     * 新增看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity) {
+        fsCourseCheckinActivity.setCreateTime(DateUtils.getNowDate());
+        // 根据时间范围计算打卡天数
+        if (fsCourseCheckinActivity.getStartTime() != null && fsCourseCheckinActivity.getEndTime() != null) {
+            int checkinDays = calculateCheckinDays(fsCourseCheckinActivity.getStartTime(), fsCourseCheckinActivity.getEndTime());
+            fsCourseCheckinActivity.setCheckinDays(checkinDays);
+        }
+//        // 根据时间自动设置状态
+//        Date now = new Date();
+//        if (fsCourseCheckinActivity.getStartTime().after(now)) {
+//            fsCourseCheckinActivity.setStatus(0); // 未开始
+//        } else if (fsCourseCheckinActivity.getEndTime().before(now)) {
+//            fsCourseCheckinActivity.setStatus(2); // 已结束
+//        } else {
+//            fsCourseCheckinActivity.setStatus(1); // 进行中
+//        }
+        return fsCourseCheckinActivityMapper.insertFsCourseCheckinActivity(fsCourseCheckinActivity);
+    }
+
+    /**
+     * 计算打卡天数(根据时间范围)
+     * 从开始日期到结束日期的天数(包含开始和结束日期)
+     */
+    private int calculateCheckinDays(Date startTime, Date endTime) {
+        Calendar startCal = Calendar.getInstance();
+        startCal.setTime(startTime);
+        // 设置为当天的开始时间(00:00:00)
+        startCal.set(Calendar.HOUR_OF_DAY, 0);
+        startCal.set(Calendar.MINUTE, 0);
+        startCal.set(Calendar.SECOND, 0);
+        startCal.set(Calendar.MILLISECOND, 0);
+
+        Calendar endCal = Calendar.getInstance();
+        endCal.setTime(endTime);
+        // 设置为当天的开始时间(00:00:00)
+        endCal.set(Calendar.HOUR_OF_DAY, 0);
+        endCal.set(Calendar.MINUTE, 0);
+        endCal.set(Calendar.SECOND, 0);
+        endCal.set(Calendar.MILLISECOND, 0);
+
+        // 计算天数差
+        long diffMillis = endCal.getTimeInMillis() - startCal.getTimeInMillis();
+        int days = (int) (diffMillis / (1000 * 60 * 60 * 24)) + 1; // +1 包含开始和结束日期
+
+        return Math.max(days, 1); // 至少1天
+    }
+
+    /**
+     * 自动设置奖品名称
+     * 红包类型:红包
+     * 积分商品类型:查询积分商品名称
+     */
+    private void setPrizeName(FsCourseCheckinPrize prize) {
+        if (prize.getPrizeType() == null) {
+            return;
+        }
+        if (prize.getPrizeType() == 1) {
+            // 红包类型
+            prize.setPrizeName("红包");
+        } else if (prize.getPrizeType() == 2) {
+            // 积分商品类型,查询商品名称
+            if (prize.getGoodsId() != null) {
+                FsIntegralGoods goods = fsIntegralGoodsMapper.selectFsIntegralGoodsByGoodsId(prize.getGoodsId());
+                if (goods != null && goods.getGoodsName() != null) {
+                    prize.setPrizeName(goods.getGoodsName());
+                } else {
+                    prize.setPrizeName("积分商品");
+                }
+            } else {
+                prize.setPrizeName("积分商品");
+            }
+        }
+    }
+
+    /**
+     * 获取公司名称(根据公司ID字符串)
+     */
+    private String getCompanyNames(String companyIds) {
+        if (companyIds == null || companyIds.trim().isEmpty()) {
+            return "";
+        }
+        try {
+            String[] ids = companyIds.split(",");
+            List<String> names = new ArrayList<>();
+            for (String id : ids) {
+                if (id.trim().isEmpty()) continue;
+                Company company = companyMapper.selectCompanyById(Long.valueOf(id.trim()));
+                if (company != null && company.getCompanyName() != null) {
+                    names.add(company.getCompanyName());
+                }
+            }
+            return String.join(",", names);
+        } catch (Exception e) {
+            return companyIds;
+        }
+    }
+
+    /**
+     * 获取项目名称(根据项目ID字符串)
+     */
+    private String getProjectNames(String projectIds) {
+        if (projectIds == null || projectIds.trim().isEmpty()) {
+            return "";
+        }
+        try {
+            String[] ids = projectIds.split(",");
+            List<String> names = new ArrayList<>();
+            for (String id : ids) {
+                if (id.trim().isEmpty()) continue;
+                // 使用字典表查询项目名称
+                String projectName = DictUtils.getDictLabel("sys_course_project", id.trim());
+                if (projectName != null && !projectName.isEmpty()) {
+                    names.add(projectName);
+                } else {
+                    names.add("项目" + id.trim()); // 如果字典中找不到,显示项目ID
+                }
+            }
+            return String.join(",", names);
+        } catch (Exception e) {
+            return projectIds;
+        }
+    }
+
+
+    /**
+     * 新增看课打卡活动(带奖品)
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @param prizeList               奖品列表
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertFsCourseCheckinActivityWithPrizes(FsCourseCheckinActivity fsCourseCheckinActivity,
+                                                        List<FsCourseCheckinPrize> prizeList) {
+        // 先插入活动
+        int result = insertFsCourseCheckinActivity(fsCourseCheckinActivity);
+        if (result > 0 && prizeList != null && !prizeList.isEmpty()) {
+            // 设置活动ID和奖品名称
+            for (FsCourseCheckinPrize prize : prizeList) {
+                prize.setActivityId(fsCourseCheckinActivity.getActivityId());
+                prize.setCreateTime(DateUtils.getNowDate());
+                // 自动设置奖品名称
+                setPrizeName(prize);
+            }
+            // 批量插入奖品
+            fsCourseCheckinPrizeMapper.batchInsertFsCourseCheckinPrize(prizeList);
+        }
+        return result;
+    }
+
+    /**
+     * 修改看课打卡活动
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateFsCourseCheckinActivity(FsCourseCheckinActivity fsCourseCheckinActivity) {
+        fsCourseCheckinActivity.setUpdateTime(DateUtils.getNowDate());
+        // 根据时间范围重新计算打卡天数
+        if (fsCourseCheckinActivity.getStartTime() != null && fsCourseCheckinActivity.getEndTime() != null) {
+            int checkinDays = calculateCheckinDays(fsCourseCheckinActivity.getStartTime(), fsCourseCheckinActivity.getEndTime());
+            fsCourseCheckinActivity.setCheckinDays(checkinDays);
+        }
+        return fsCourseCheckinActivityMapper.updateFsCourseCheckinActivity(fsCourseCheckinActivity);
+    }
+
+    /**
+     * 修改看课打卡活动(带奖品)
+     *
+     * @param fsCourseCheckinActivity 看课打卡活动
+     * @param prizeList               奖品列表
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateFsCourseCheckinActivityWithPrizes(FsCourseCheckinActivity fsCourseCheckinActivity,
+                                                        List<FsCourseCheckinPrize> prizeList) {
+        // 更新活动
+        int result = updateFsCourseCheckinActivity(fsCourseCheckinActivity);
+        if (result > 0 && prizeList != null) {
+            // 删除原有奖品
+            fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByActivityId(fsCourseCheckinActivity.getActivityId());
+            // 插入新奖品
+            if (!prizeList.isEmpty()) {
+                for (FsCourseCheckinPrize prize : prizeList) {
+                    prize.setActivityId(fsCourseCheckinActivity.getActivityId());
+                    prize.setCreateTime(DateUtils.getNowDate());
+                    // 自动设置奖品名称
+                    setPrizeName(prize);
+                }
+                fsCourseCheckinPrizeMapper.batchInsertFsCourseCheckinPrize(prizeList);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 批量删除看课打卡活动
+     *
+     * @param activityIds 需要删除的看课打卡活动主键集合
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteFsCourseCheckinActivityByActivityIds(Long[] activityIds) {
+        for (Long activityId : activityIds) {
+            // 删除关联数据
+            fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByActivityId(activityId);
+            fsCourseCheckinUserMapper.deleteFsCourseCheckinUserByActivityId(activityId);
+            fsCourseCheckinReceiveMapper.deleteFsCourseCheckinReceiveByActivityId(activityId);
+        }
+        return fsCourseCheckinActivityMapper.deleteFsCourseCheckinActivityByActivityIds(activityIds);
+    }
+
+    /**
+     * 删除看课打卡活动信息
+     *
+     * @param activityId 看课打卡活动主键
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteFsCourseCheckinActivityByActivityId(Long activityId) {
+        // 删除关联数据
+        fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByActivityId(activityId);
+        fsCourseCheckinUserMapper.deleteFsCourseCheckinUserByActivityId(activityId);
+        fsCourseCheckinReceiveMapper.deleteFsCourseCheckinReceiveByActivityId(activityId);
+        return fsCourseCheckinActivityMapper.deleteFsCourseCheckinActivityByActivityId(activityId);
+    }
+
+    /**
+     * 查询当前时间范围内、指定公司和项目的进行中的活动
+     *
+     * @param currentTime 当前时间
+     * @param companyId   公司ID
+     * @param projectId   项目ID
+     * @return 活动列表
+     */
+    @Override
+    public List<FsCourseCheckinActivity> selectActiveActivities(Date currentTime, Long companyId, Long projectId) {
+        return fsCourseCheckinActivityMapper.selectActiveActivities( companyId, projectId,null);
+    }
+
+    /**
+     * 更新活动状态(根据时间自动更新)
+     */
+    @Override
+    public void updateActivityStatusByTime() {
+        // 查询所有非停用状态的活动
+        FsCourseCheckinActivity query = new FsCourseCheckinActivity();
+        query.setStatus(1); // 进行中
+        List<FsCourseCheckinActivity> list = fsCourseCheckinActivityMapper.selectFsCourseCheckinActivityList(query);
+
+        Date now = new Date();
+        for (FsCourseCheckinActivity activity : list) {
+            if (activity.getEndTime().before(now)) {
+                // 活动已结束
+                activity.setStatus(2);
+                activity.setUpdateTime(now);
+                fsCourseCheckinActivityMapper.updateFsCourseCheckinActivity(activity);
+            }
+        }
+
+        // 查询未开始的活动,检查是否变为进行中
+        query.setStatus(0);
+        List<FsCourseCheckinActivity> notStartedList = fsCourseCheckinActivityMapper.selectFsCourseCheckinActivityList(query);
+        for (FsCourseCheckinActivity activity : notStartedList) {
+            if (activity.getStartTime().before(now) && activity.getEndTime().after(now)) {
+                // 活动变为进行中
+                activity.setStatus(1);
+                activity.setUpdateTime(now);
+                fsCourseCheckinActivityMapper.updateFsCourseCheckinActivity(activity);
+            }
+        }
+    }
+
+    /**
+     * 停用活动
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    @Override
+    public int stopActivity(Long activityId) {
+        FsCourseCheckinActivity activity = new FsCourseCheckinActivity();
+        activity.setActivityId(activityId);
+        activity.setStatus(3); // 已停用
+        activity.setUpdateTime(DateUtils.getNowDate());
+        return fsCourseCheckinActivityMapper.updateFsCourseCheckinActivity(activity);
+    }
+
+    /**
+     * 获取状态文本
+     */
+    private String getStatusText(Integer status) {
+        if (status == null) return "";
+        switch (status) {
+            case 0:
+                return "未开始";
+            case 1:
+                return "进行中";
+            case 2:
+                return "已结束";
+            case 3:
+                return "已停用";
+            default:
+                return "";
+        }
+    }
+}

+ 167 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinPrizeServiceImpl.java

@@ -0,0 +1,167 @@
+package com.fs.course.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FsCourseCheckinPrize;
+import com.fs.course.mapper.FsCourseCheckinPrizeMapper;
+import com.fs.course.service.IFsCourseCheckinPrizeService;
+import com.fs.his.domain.FsIntegralGoods;
+import com.fs.his.mapper.FsIntegralGoodsMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 看课打卡活动奖品Service业务层处理
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Service
+public class FsCourseCheckinPrizeServiceImpl implements IFsCourseCheckinPrizeService {
+
+    @Autowired
+    private FsCourseCheckinPrizeMapper fsCourseCheckinPrizeMapper;
+
+    @Autowired
+    private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+
+    /**
+     * 查询看课打卡活动奖品
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 看课打卡活动奖品
+     */
+    @Override
+    public FsCourseCheckinPrize selectFsCourseCheckinPrizeByPrizeId(Long prizeId) {
+        return fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeByPrizeId(prizeId);
+    }
+
+    /**
+     * 查询看课打卡活动奖品列表
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 看课打卡活动奖品集合
+     */
+    @Override
+    public List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeList(FsCourseCheckinPrize fsCourseCheckinPrize) {
+        return fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeList(fsCourseCheckinPrize);
+    }
+
+    /**
+     * 根据活动ID查询奖品列表
+     *
+     * @param activityId 活动ID
+     * @return 奖品列表
+     */
+    @Override
+    public List<FsCourseCheckinPrize> selectFsCourseCheckinPrizeByActivityId(Long activityId) {
+        return fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeByActivityId(activityId);
+    }
+
+    /**
+     * 新增看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize) {
+        fsCourseCheckinPrize.setCreateTime(DateUtils.getNowDate());
+        // 自动设置奖品名称
+        setPrizeName(fsCourseCheckinPrize);
+        return fsCourseCheckinPrizeMapper.insertFsCourseCheckinPrize(fsCourseCheckinPrize);
+    }
+
+    /**
+     * 自动设置奖品名称
+     * 红包类型:红包
+     * 积分商品类型:查询积分商品名称
+     */
+    private void setPrizeName(FsCourseCheckinPrize prize) {
+        if (prize.getPrizeType() == null) {
+            return;
+        }
+        if (prize.getPrizeType() == 1) {
+            // 红包类型
+            prize.setPrizeName("红包");
+        } else if (prize.getPrizeType() == 2) {
+            // 积分商品类型,查询商品名称
+            if (prize.getGoodsId() != null) {
+                FsIntegralGoods goods = fsIntegralGoodsMapper.selectFsIntegralGoodsByGoodsId(prize.getGoodsId());
+                if (goods != null && goods.getGoodsName() != null) {
+                    prize.setPrizeName(goods.getGoodsName());
+                } else {
+                    prize.setPrizeName("积分商品");
+                }
+            } else {
+                prize.setPrizeName("积分商品");
+            }
+        }
+    }
+
+    /**
+     * 批量新增奖品
+     *
+     * @param prizeList 奖品列表
+     * @return 结果
+     */
+    @Override
+    public int batchInsertFsCourseCheckinPrize(List<FsCourseCheckinPrize> prizeList) {
+        if (prizeList == null || prizeList.isEmpty()) {
+            return 0;
+        }
+        // 自动设置奖品名称
+        for (FsCourseCheckinPrize prize : prizeList) {
+            setPrizeName(prize);
+        }
+        return fsCourseCheckinPrizeMapper.batchInsertFsCourseCheckinPrize(prizeList);
+    }
+
+    /**
+     * 修改看课打卡活动奖品
+     *
+     * @param fsCourseCheckinPrize 看课打卡活动奖品
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseCheckinPrize(FsCourseCheckinPrize fsCourseCheckinPrize) {
+        fsCourseCheckinPrize.setUpdateTime(DateUtils.getNowDate());
+        // 自动设置奖品名称
+        setPrizeName(fsCourseCheckinPrize);
+        return fsCourseCheckinPrizeMapper.updateFsCourseCheckinPrize(fsCourseCheckinPrize);
+    }
+
+    /**
+     * 批量删除看课打卡活动奖品
+     *
+     * @param prizeIds 需要删除的看课打卡活动奖品主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinPrizeByPrizeIds(Long[] prizeIds) {
+        return fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByPrizeIds(prizeIds);
+    }
+
+    /**
+     * 删除看课打卡活动奖品信息
+     *
+     * @param prizeId 看课打卡活动奖品主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinPrizeByPrizeId(Long prizeId) {
+        return fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByPrizeId(prizeId);
+    }
+
+    /**
+     * 根据活动ID删除奖品
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinPrizeByActivityId(Long activityId) {
+        return fsCourseCheckinPrizeMapper.deleteFsCourseCheckinPrizeByActivityId(activityId);
+    }
+}

+ 500 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinReceiveServiceImpl.java

@@ -0,0 +1,500 @@
+package com.fs.course.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.mapper.*;
+import com.fs.course.param.FsCourseSendRewardUParam;
+import com.fs.course.service.IFsCourseCheckinReceiveService;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserWx;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.param.WxSendRedPacketParam;
+import com.fs.his.service.IFsStorePaymentService;
+import com.fs.his.service.IFsUserWxService;
+import com.fs.system.service.ISysConfigService;
+import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hc.core5.http2.hpack.HPackException;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 看课打卡奖品领取记录Service业务层处理
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Slf4j
+@Service
+public class FsCourseCheckinReceiveServiceImpl implements IFsCourseCheckinReceiveService {
+
+    private static final Logger logger = LoggerFactory.getLogger(FsCourseCheckinReceiveServiceImpl.class);
+
+    private static final String CHECKIN_PRIZE_LOCK = "course:checkin:prize:lock:%d";
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Autowired
+    private FsCourseCheckinReceiveMapper fsCourseCheckinReceiveMapper;
+
+    @Autowired
+    private FsCourseCheckinUserMapper fsCourseCheckinUserMapper;
+
+    @Autowired
+    private FsCourseCheckinActivityMapper fsCourseCheckinActivityMapper;
+
+    @Autowired
+    private FsCourseCheckinPrizeMapper fsCourseCheckinPrizeMapper;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private FsCourseWatchLogMapper courseWatchLogMapper;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+
+    @Autowired
+    private IFsStorePaymentService paymentService;
+
+    @Autowired
+    private IFsUserWxService fsUserWxService;
+
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
+
+    /**
+     * 查询看课打卡奖品领取记录
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 看课打卡奖品领取记录
+     */
+    @Override
+    public FsCourseCheckinReceive selectFsCourseCheckinReceiveByReceiveId(Long receiveId) {
+        return fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveByReceiveId(receiveId);
+    }
+
+    /**
+     * 查询看课打卡奖品领取记录列表
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 看课打卡奖品领取记录集合
+     */
+    @Override
+    public List<FsCourseCheckinReceive> selectFsCourseCheckinReceiveList(FsCourseCheckinReceive fsCourseCheckinReceive) {
+        return fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveList(fsCourseCheckinReceive);
+    }
+
+    /**
+     * 查询待发放的领取记录
+     *
+     * @param limit 限制数量
+     * @return 领取记录列表
+     */
+    @Override
+    public List<FsCourseCheckinReceive> selectPendingReceiveList(Integer limit) {
+        return fsCourseCheckinReceiveMapper.selectPendingReceiveList(limit);
+    }
+
+    /**
+     * 新增看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive) {
+        fsCourseCheckinReceive.setCreateTime(DateUtils.getNowDate());
+        return fsCourseCheckinReceiveMapper.insertFsCourseCheckinReceive(fsCourseCheckinReceive);
+    }
+
+    /**
+     * 批量新增领取记录
+     *
+     * @param receiveList 领取记录列表
+     * @return 结果
+     */
+    @Override
+    public int batchInsertFsCourseCheckinReceive(List<FsCourseCheckinReceive> receiveList) {
+        if (receiveList == null || receiveList.isEmpty()) {
+            return 0;
+        }
+        return fsCourseCheckinReceiveMapper.batchInsertFsCourseCheckinReceive(receiveList);
+    }
+
+    /**
+     * 修改看课打卡奖品领取记录
+     *
+     * @param fsCourseCheckinReceive 看课打卡奖品领取记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseCheckinReceive(FsCourseCheckinReceive fsCourseCheckinReceive) {
+        fsCourseCheckinReceive.setUpdateTime(DateUtils.getNowDate());
+        return fsCourseCheckinReceiveMapper.updateFsCourseCheckinReceive(fsCourseCheckinReceive);
+    }
+
+    /**
+     * 批量删除看课打卡奖品领取记录
+     *
+     * @param receiveIds 需要删除的看课打卡奖品领取记录主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinReceiveByReceiveIds(Long[] receiveIds) {
+        return fsCourseCheckinReceiveMapper.deleteFsCourseCheckinReceiveByReceiveIds(receiveIds);
+    }
+
+    /**
+     * 删除看课打卡奖品领取记录信息
+     *
+     * @param receiveId 看课打卡奖品领取记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinReceiveByReceiveId(Long receiveId) {
+        return fsCourseCheckinReceiveMapper.deleteFsCourseCheckinReceiveByReceiveId(receiveId);
+    }
+
+    /**
+     * 根据活动ID删除领取记录
+     *
+     * @param activityId 活动ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinReceiveByActivityId(Long activityId) {
+        return fsCourseCheckinReceiveMapper.deleteFsCourseCheckinReceiveByActivityId(activityId);
+    }
+
+    /**
+     * 检查并创建奖品领取记录
+     * 当用户打卡天数达到要求时,自动创建领取记录
+     *
+     */
+    @Override
+    @Transactional
+    public Map<String, Object> checkAndCreateReceiveRecord(FsCourseSendRewardUParam fsCourseSendRewardUParam) {
+        Map<String, Object> result = new HashMap<>();
+
+        // 1. 查询活动信息
+        FsCourseCheckinActivity activity = fsCourseCheckinActivityMapper.selectFsCourseCheckinActivityByActivityId(fsCourseSendRewardUParam.getActivityId());
+        if (activity == null) {
+            result.put("success", false);
+            result.put("msg", "活动不存在");
+            return result;
+        }
+
+        // 2. 查询用户打卡统计
+        FsCourseCheckinUser checkinUser = fsCourseCheckinUserMapper.selectByActivityAndUser(fsCourseSendRewardUParam.getActivityId(), fsCourseSendRewardUParam.getUserId());
+        if (checkinUser == null) {
+            result.put("success", false);
+            result.put("msg", "用户打卡记录不存在");
+            return result;
+        }
+
+        // 3. 检查是否达到打卡天数要求
+        if (checkinUser.getCheckinDays() < activity.getCheckinDays()) {
+            result.put("success", false);
+            result.put("msg", "打卡天数不足");
+            return result;
+        }
+
+        // 4. 检查是否已创建领取记录
+        FsCourseCheckinReceive query = new FsCourseCheckinReceive();
+        query.setActivityId(activity.getActivityId());
+        query.setUserId(fsCourseSendRewardUParam.getUserId());
+        List<FsCourseCheckinReceive> existList = fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveList(query);
+        if (!existList.isEmpty()) {
+            result.put("success", false);
+            result.put("msg", "已创建领取记录");
+            return result; // 已创建领取记录
+        }
+
+        // 5. 查询奖品(一个活动只配一个奖品)
+        List<FsCourseCheckinPrize> prizeList = fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeByActivityId(activity.getActivityId());
+        if (prizeList.isEmpty()) {
+            result.put("success", false);
+            result.put("msg", "活动未配置奖品");
+            return result;
+        }
+
+        // 6. 创建单个领取记录
+        Date now = new Date();
+        FsCourseCheckinPrize prize = prizeList.get(0); // 只取第一个奖品
+        FsCourseCheckinReceive receive = new FsCourseCheckinReceive();
+        receive.setActivityId(activity.getActivityId());
+        receive.setPrizeId(prize.getPrizeId());
+        receive.setUserId(fsCourseSendRewardUParam.getUserId());
+        receive.setCompanyId(checkinUser.getCompanyId());
+        receive.setProjectId(checkinUser.getProjectId());
+        receive.setPrizeType(prize.getPrizeType());
+        receive.setPrizeName(prize.getPrizeName());
+        receive.setRedpacketAmount(prize.getRedpacketAmount());
+        receive.setGoodsId(prize.getGoodsId());
+        receive.setReceiveStatus(0); // 待发放
+        receive.setCreateTime(now);
+        //创建批次号
+        String code =  OrderCodeUtils.getOrderSn();
+        if (StringUtils.isEmpty(code)) {
+            return R.error("红包单号生成失败,请重试");
+        }
+        receive.setOutBatchNo("fsCourse"+cloudHostProper.getProjectCode() + code);
+
+        fsCourseCheckinReceiveMapper.insertFsCourseCheckinReceive(receive);
+
+        // 7. 发放奖励
+        result = sendPrize(receive.getReceiveId(), fsCourseSendRewardUParam);
+
+        return result;
+    }
+
+    /**
+     * 统计用户在某活动下的领取记录数量
+     * @param activityId 活动ID
+     * @param userId 用户ID
+     * @return 领取记录数量
+     */
+    @Override
+    public int countReceiveByActivityAndUser(Long activityId, Long userId) {
+        FsCourseCheckinReceive query = new FsCourseCheckinReceive();
+        query.setActivityId(activityId);
+        query.setUserId(userId);
+        List<FsCourseCheckinReceive> list = fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveList(query);
+        return list != null ? list.size() : 0;
+    }
+
+    /**
+     * 发放奖品(红包或积分商品券)
+     *
+     * @param receiveId 领取记录ID
+     * @return 结果
+     */
+    @Transactional
+    public Map<String, Object> sendPrize(Long receiveId, FsCourseSendRewardUParam param) {
+        FsCourseCheckinReceive receive = fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveByReceiveId(receiveId);
+        if (receive == null || receive.getReceiveStatus() != 0) {
+            return new HashMap<>();
+        }
+        FsUser user = fsUserMapper.selectFsUserById(receive.getUserId());
+
+        String lockKey = String.format(CHECKIN_PRIZE_LOCK, receiveId);
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            boolean locked = lock.tryLock(5, 300, TimeUnit.SECONDS);
+            if (!locked) {
+                logger.error("获取锁失败 - 领取记录ID: {}", receiveId);
+                Map<String, Object> result = new HashMap<>();
+                result.put("success", false);
+                result.put("msg", "系统繁忙,请稍后重试");
+                return result;
+            }
+
+            Date now = new Date();
+            Map<String, Object> result = new HashMap<>();
+            boolean success = false;
+            if (receive.getPrizeType() == 1) {
+                // 红包类型 - 调用红包发放接口
+                success = sendRedpacket(receive, param, user);
+            } else if (receive.getPrizeType() == 2) {
+                // 积分商品类型 - 直接返回标识,不实际发放
+                success = true; // 积分商品券标记为成功,由前端处理
+                result.put("prizeType", 2);
+                result.put("goodsId", receive.getGoodsId());
+            }
+
+            if (success) {
+                receive.setReceiveStatus(1); // 发放成功
+                receive.setReceiveTime(now);
+                result.put("success", true);
+                result.put("msg", "发放成功");
+            } else {
+                receive.setReceiveStatus(2); // 发放失败
+                receive.setReceiveTime(now);
+                result.put("success", false);
+                result.put("msg", "发放失败");
+            }
+
+            receive.setUpdateTime(now);
+            fsCourseCheckinReceiveMapper.updateFsCourseCheckinReceive(receive);
+
+            return result;
+        } catch (InterruptedException e) {
+            logger.error("获取锁被中断 - 领取记录ID: {}", receiveId, e);
+            Thread.currentThread().interrupt();
+            Map<String, Object> result = new HashMap<>();
+            result.put("success", false);
+            result.put("msg", "系统异常,请稍后重试");
+            return result;
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 发放红包
+     * 根据 sendRedPacketRewardFsUser 方法
+     */
+    private boolean sendRedpacket(FsCourseCheckinReceive receive, FsCourseSendRewardUParam param, FsUser user) {
+        try {
+            // 获取配置信息
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+
+            // 3. 确定红包金额(从奖品配置中获取,sendRedPacketRewardFsUser方法)
+            BigDecimal amount = receive.getRedpacketAmount() != null ? receive.getRedpacketAmount() : BigDecimal.ZERO;
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                logger.error("红包金额无效 - 用户ID: {}, 金额: {}", user.getUserId(), amount);
+                return false;
+            }
+
+            // 4. 准备发送红包参数(从sendRedPacketRewardFsUser方法)
+            WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+            packetParam.setOpenId(user.getMpOpenId());
+
+            // 来源是小程序切换openId(从sendRedPacketRewardFsUser方法)
+            if (param.getSource() == 2) {
+                if (user.getMpOpenId() != null) {
+                    packetParam.setOpenId(user.getMpOpenId());
+                } else {
+                    //查询是否绑定小程序
+                    FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(), user.getUserId(), 1);
+                    if (fsUserWx == null) {
+                        logger.error("openId参数错误,请清理缓存重新授权");
+                        return false;
+                    } else {
+                        packetParam.setOpenId(fsUserWx.getOpenId());
+                    }
+                }
+            }
+            //判断服务号配置是否存在
+            if (StringUtils.isNotEmpty(config.getMpAppId())) {
+                packetParam.setMpAppId(config.getMpAppId());
+            }
+            // 5. 设置基本参数(从sendRedPacketRewardFsUser方法)
+            packetParam.setAmount(amount);
+            packetParam.setSource(param.getSource());
+            packetParam.setRedPacketMode(config.getRedPacketMode());
+            packetParam.setCompanyId(receive.getCompanyId());
+            packetParam.setAppId(param.getAppId());
+            packetParam.setUser(user);
+//
+            // 7. 检查公司余额(从sendRedPacketRewardFsUser方法)
+            Company company = companyMapper.selectCompanyById(receive.getCompanyId());
+            if (company == null || company.getMoney().compareTo(amount) < 0) {
+                logger.error("公司余额不足 - 公司ID: {}, 需要金额: {}, 可用余额: {}",
+                        receive.getCompanyId(), amount, company != null ? company.getMoney() : 0);
+                return false;
+            }
+            packetParam.setOutBatchNo(receive.getOutBatchNo());
+            R sendRedPacket = paymentService.sendRedPacket(packetParam);
+            if (sendRedPacket.get("code").equals(200)) {
+                TransferBillsResult transferBillsResult;
+                OffsetDateTime offsetDateTime;
+                if (sendRedPacket.get("isNew").equals(1)) {
+                    transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+
+                    receive.setOutBatchNo(transferBillsResult.getOutBillNo());
+                    receive.setBatchId(transferBillsResult.getTransferBillNo());
+                    offsetDateTime = OffsetDateTime.parse(transferBillsResult.getCreateTime());
+                } else {
+                    receive.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                    receive.setBatchId(sendRedPacket.get("batchId").toString());
+                    offsetDateTime = OffsetDateTime.parse(sendRedPacket.get("createTime").toString());
+                }
+                receive.setCreateTime(Date.from(offsetDateTime.toInstant()));
+
+                logger.info("红包发送成功 - 用户ID: {}, 金额: {}, 批次号: {}",
+                        user.getUserId(), amount, receive.getReceiveId());
+                return true;
+            }
+        } catch (Exception e) {
+            logger.error("发放红包失败 - 用户ID: {}, 活动ID: {}", user.getUserId(), receive.getActivityId(), e);
+            return false;
+        }
+        return false;
+    }
+
+    /**
+     * 发放积分商品券
+     * 积分商品券直接返回标识,不实际发放,由前端处理
+     */
+    private boolean sendGoodsCoupon(FsCourseCheckinReceive receive) {
+        // 积分商品券直接返回成功标识,实际发放由前端或后续流程处理
+        // 这里不生成实际的优惠券码,只是标记为发放成功
+        return true;
+    }
+
+    /**
+     * 批量发放待发放的奖品
+     *
+     * @param limit 限制数量
+     * @return 发放成功数量
+     */
+    @Override
+    public int batchSendPendingPrizes(Integer limit) {
+        List<FsCourseCheckinReceive> pendingList = fsCourseCheckinReceiveMapper.selectPendingReceiveList(limit);
+        int successCount = 0;
+//        for (FsCourseCheckinReceive receive : pendingList) {
+//            if (sendPrize(receive.getReceiveId())) {
+//                successCount++;
+//            }
+//        }
+        return successCount;
+    }
+
+    /**
+     * 发送通知给用户
+     *
+     * @param receiveId 领取记录ID
+     * @return 结果
+     */
+    @Override
+    public boolean sendNotify(Long receiveId) {
+        FsCourseCheckinReceive receive = fsCourseCheckinReceiveMapper.selectFsCourseCheckinReceiveByReceiveId(receiveId);
+        if (receive == null || receive.getReceiveStatus() != 1) {
+            return false;
+        }
+
+        // TODO: 根据活动通知模板发送消息给用户
+        // 可以通过微信模板消息、短信等方式通知
+
+        Date now = new Date();
+        receive.setNotifyStatus(1); // 已通知
+        receive.setNotifyTime(now);
+        receive.setUpdateTime(now);
+        fsCourseCheckinReceiveMapper.updateFsCourseCheckinReceive(receive);
+
+        return true;
+    }
+}

+ 295 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseCheckinUserServiceImpl.java

@@ -0,0 +1,295 @@
+package com.fs.course.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FsCourseCheckinActivity;
+import com.fs.course.domain.FsCourseCheckinPrize;
+import com.fs.course.domain.FsCourseCheckinUser;
+import com.fs.course.domain.FsUserCourse;
+import com.fs.course.mapper.FsCourseCheckinActivityMapper;
+import com.fs.course.mapper.FsCourseCheckinPrizeMapper;
+import com.fs.course.mapper.FsCourseCheckinUserMapper;
+import com.fs.course.mapper.FsUserCourseMapper;
+import com.fs.course.service.IFsCourseCheckinReceiveService;
+import com.fs.course.service.IFsCourseCheckinUserService;
+import com.fs.course.vo.CourseCheckinResultVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 看课打卡用户统计Service业务层处理
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Service
+public class FsCourseCheckinUserServiceImpl implements IFsCourseCheckinUserService {
+
+    @Autowired
+    private FsCourseCheckinUserMapper fsCourseCheckinUserMapper;
+
+    @Autowired
+    private FsCourseCheckinActivityMapper fsCourseCheckinActivityMapper;
+
+    @Autowired
+    private FsCourseCheckinPrizeMapper fsCourseCheckinPrizeMapper;
+
+    @Autowired
+    private IFsCourseCheckinReceiveService fsCourseCheckinReceiveService;
+
+    @Autowired
+    private FsUserCourseMapper  fsUserCourseMapper;
+
+    /**
+     * 查询看课打卡用户统计
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 看课打卡用户统计
+     */
+    @Override
+    public FsCourseCheckinUser selectFsCourseCheckinUserById(Long id) {
+        return fsCourseCheckinUserMapper.selectFsCourseCheckinUserById(id);
+    }
+
+    /**
+     * 根据活动ID和用户ID查询打卡统计
+     *
+     * @param activityId 活动ID
+     * @param userId     用户ID
+     * @return 打卡统计
+     */
+    @Override
+    public FsCourseCheckinUser selectByActivityAndUser(Long activityId, Long userId) {
+        return null;
+    }
+
+    /**
+     * 查询看课打卡用户统计列表
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 看课打卡用户统计集合
+     */
+    @Override
+    public List<FsCourseCheckinUser> selectFsCourseCheckinUserList(FsCourseCheckinUser fsCourseCheckinUser) {
+        return fsCourseCheckinUserMapper.selectFsCourseCheckinUserList(fsCourseCheckinUser);
+    }
+
+    /**
+     * 新增看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser) {
+        fsCourseCheckinUser.setCreateTime(DateUtils.getNowDate());
+        return fsCourseCheckinUserMapper.insertFsCourseCheckinUser(fsCourseCheckinUser);
+    }
+
+    /**
+     * 修改看课打卡用户统计
+     *
+     * @param fsCourseCheckinUser 看课打卡用户统计
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseCheckinUser(FsCourseCheckinUser fsCourseCheckinUser) {
+        fsCourseCheckinUser.setUpdateTime(DateUtils.getNowDate());
+        return fsCourseCheckinUserMapper.updateFsCourseCheckinUser(fsCourseCheckinUser);
+    }
+
+    /**
+     * 批量删除看课打卡用户统计
+     *
+     * @param ids 需要删除的看课打卡用户统计主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinUserByIds(Long[] ids) {
+        return fsCourseCheckinUserMapper.deleteFsCourseCheckinUserByIds(ids);
+    }
+
+    /**
+     * 删除看课打卡用户统计信息
+     *
+     * @param id 看课打卡用户统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseCheckinUserById(Long id) {
+        return fsCourseCheckinUserMapper.deleteFsCourseCheckinUserById(id);
+    }
+
+    /**
+     * 用户看课打卡处理
+     *
+     * @param userId    用户ID
+     * @param companyId 公司ID
+     * @param projectId 项目ID
+     * @return 打卡结果,包含提醒信息
+     */
+    @Override
+    @Transactional
+    public CourseCheckinResultVO handleCourseCheckin(Long userId, Long companyId, Long projectId, Long courseId) {
+        CourseCheckinResultVO result = new CourseCheckinResultVO();
+        //根据课程获取项目
+        FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(courseId);
+        if(fsUserCourse != null){
+            projectId=fsUserCourse.getProject();
+        }
+        Date now = new Date();
+        // 1. 查询当前时间范围内、用户所在公司和项目的进行中的活动
+        List<FsCourseCheckinActivity> activities = fsCourseCheckinActivityMapper.selectActiveActivities(
+                 companyId, projectId, now
+        );
+        if (activities.isEmpty()) {
+            result.setSuccess(false);
+            return result;
+        }
+
+        for (FsCourseCheckinActivity activity : activities) {
+            // 2. 查询用户在该活动的打卡统计
+            FsCourseCheckinUser checkinUser = fsCourseCheckinUserMapper.selectByActivityAndUser(
+                    activity.getActivityId(), userId
+            );
+
+            // 3. 检查今天是否已打卡
+            boolean alreadyCheckedToday = false;
+            if (checkinUser != null && checkinUser.getLastCheckinTime() != null) {
+                if (isSameDay(checkinUser.getLastCheckinTime(), now)) {
+                    alreadyCheckedToday = true;
+                }
+            }
+
+            // 4. 更新或插入打卡统计
+            int checkedDays;
+            if (checkinUser == null) {
+                // 首次打卡
+                checkinUser = new FsCourseCheckinUser();
+                checkinUser.setActivityId(activity.getActivityId());
+                checkinUser.setUserId(userId);
+                checkinUser.setCompanyId(companyId);
+                checkinUser.setProjectId(projectId);
+                checkinUser.setCourseId(courseId);
+                checkinUser.setCheckinDays(1);
+                checkinUser.setFirstCheckinTime(now);
+                checkinUser.setLastCheckinTime(now);
+                checkinUser.setCreateTime(now);
+                fsCourseCheckinUserMapper.insertFsCourseCheckinUser(checkinUser);
+                checkedDays = 1;
+                result.setSuccess(true);
+            } else if (!alreadyCheckedToday) {
+                // 新的一天,天数+1
+                checkedDays = checkinUser.getCheckinDays() + 1;
+                checkinUser.setCheckinDays(checkedDays);
+                checkinUser.setCourseId(courseId);
+                checkinUser.setLastCheckinTime(now);
+                checkinUser.setUpdateTime(now);
+                fsCourseCheckinUserMapper.updateFsCourseCheckinUser(checkinUser);
+                result.setSuccess(true);
+            } else {
+                // 今天已打卡过,不累加天数
+                checkedDays = checkinUser.getCheckinDays();
+                result.setSuccess(true);
+            }
+
+            result.setCheckedDays(checkedDays);
+            result.setActivityId(activity.getActivityId());
+            result.setActivityName(activity.getActivityName());
+
+            // 5. 检查是否已打卡但未领取奖励(无论是否重复打卡都需要判断)
+            boolean checkedButNotReceived = checkCheckedButNotReceived(activity.getActivityId(), userId, checkedDays, activity.getCheckinDays());
+            result.setCheckedButNotReceived(checkedButNotReceived);
+
+            // 6. 生成提醒消息
+            generateReminderMessage(activity, checkedDays, result);
+            
+            // 只处理第一个符合条件的活动
+            break;
+        }
+        
+        return result;
+    }
+
+    /**
+     * 检查是否已打卡但未领取奖励
+     * @param activityId 活动ID
+     * @param userId 用户ID
+     * @param checkedDays 已打卡天数
+     * @param requiredDays 要求打卡天数
+     * @return true-已达标但未领取,false-未达标或已领取
+     */
+    private boolean checkCheckedButNotReceived(Long activityId, Long userId, int checkedDays, int requiredDays) {
+        // 未达到要求天数,不需要检查
+        if (checkedDays < requiredDays) {
+            return false;
+        }
+        
+        // 查询是否已有领取记录
+        // 通过查询领取记录表,检查该用户在该活动下是否有领取记录
+        // 这里假设 FsCourseCheckinReceiveService 有相应的方法
+        // 如果没有,可以通过查询 fs_course_checkin_receive 表来判断
+        
+        // 简化实现:查询该用户在该活动下是否有待发放或发放成功的记录
+        // 如果没有记录,说明已打卡但未领取
+        int receiveCount = fsCourseCheckinReceiveService.countReceiveByActivityAndUser(activityId, userId);
+        return receiveCount == 0;
+    }
+
+    /**
+     * 生成提醒消息
+     */
+    private void generateReminderMessage(FsCourseCheckinActivity activity, int checkedDays, CourseCheckinResultVO result) {
+        int requiredDays = activity.getCheckinDays();
+        int remainingDays = requiredDays - checkedDays;
+        
+        // 查询奖品信息(一个活动只配一个奖品)
+        List<FsCourseCheckinPrize> prizes = fsCourseCheckinPrizeMapper.selectFsCourseCheckinPrizeByActivityId(activity.getActivityId());
+        if (!prizes.isEmpty()) {
+            FsCourseCheckinPrize firstPrize = prizes.get(0);
+            result.setPrizeType(firstPrize.getPrizeType());
+            result.setPrizeName(firstPrize.getPrizeName());
+            
+            String prizeDesc = getPrizeDescription(firstPrize);
+            
+            if (remainingDays <= 0) {
+                // 已达到要求,即将发放奖品
+                result.setRemainingDays(0);
+                result.setReminderMessage(String.format("恭喜您!您已打卡%d天,即将为您发放%s!", checkedDays, prizeDesc));
+            } else {
+                // 还需继续打卡
+                result.setRemainingDays(remainingDays);
+                result.setReminderMessage(String.format("您已打卡%d天,还需打卡%d天,就能领取%s了!", 
+                        checkedDays, remainingDays, prizeDesc));
+            }
+        }
+    }
+
+    /**
+     * 获取奖品描述
+     */
+    private String getPrizeDescription(FsCourseCheckinPrize prize) {
+        if (prize.getPrizeType() == 1) {
+            return "红包";
+        } else if (prize.getPrizeType() == 2) {
+            return prize.getPrizeName() != null ? prize.getPrizeName() : "积分商品券";
+        }
+        return "奖品";
+    }
+
+    /**
+     * 判断两个时间是否是同一天
+     */
+    private boolean isSameDay(Date date1, Date date2) {
+        Calendar cal1 = Calendar.getInstance();
+        cal1.setTime(date1);
+        Calendar cal2 = Calendar.getInstance();
+        cal2.setTime(date2);
+        return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+                && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
+    }
+}

+ 42 - 25
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -1375,6 +1375,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         List<AppUserCompanyDTO> appUserList = userMapper.selectAppUserListForActiveCount(param);
         
         // 3. 如果有APP会员数据,则统计活跃用户数
+        Map<Long, int[]> companyStatsMap = null;
         if (!CollectionUtils.isEmpty(appUserList)) {
             // 提取所有唯一的 userId 并查询活跃用户
             Set<Long> allUserIds = appUserList.stream()
@@ -1386,7 +1387,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             );
 
             // 按公司分组统计 APP 会员数据 [总数, 活跃数]
-            Map<Long, int[]> companyStatsMap = new HashMap<>();
+            companyStatsMap = new HashMap<>();
             for (AppUserCompanyDTO dto : appUserList) {
                 Long cid = dto.getCompanyId();
                 int[] stats = companyStatsMap.computeIfAbsent(cid, k -> new int[]{0, 0});
@@ -1395,16 +1396,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     stats[1]++;
                 }
             }
-
-            // 组装 APP 会员统计数据
-            for (AppCourseReportVO vo : allCompanies) {
-                int[] stats = companyStatsMap.getOrDefault(vo.getCompanyId(), new int[]{0, 0});
-                vo.setAppUserCount(stats[0]);
-                vo.setActiveAppUserCount(stats[1]);
-            }
         }
 
-        // 6. 批量查询看课、答题、红包统计数据
+        // 4. 批量查询看课、答题、红包统计数据
         List<Long> companyIds = allCompanies.stream()
                 .map(AppCourseReportVO::getCompanyId)
                 .collect(Collectors.toList());
@@ -1415,7 +1409,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         List<AppCourseReportVO> answerList = fsCourseWatchLogMapper.selectAppAnswerStatistics(param);
         List<AppCourseReportVO> redpackList = fsCourseWatchLogMapper.selectAppRedPacketStatistics(param);
 
-        // 7. 转换为 Map 便于查找
+        // 5. 转换为 Map 便于查找
         Map<Long, AppCourseReportVO> watchStatsMap = watchStatsList.stream()
                 .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity()));
         Map<Long, AppCourseReportVO> answerStatsMap = answerList.stream()
@@ -1423,30 +1417,48 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Map<Long, AppCourseReportVO> redPacketStatsMap = redpackList.stream()
                 .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity(), (e, r) -> e));
 
-        // 8. 组装最终数据
+        // 6. 一次性组装所有数据,减少遍历次数
         for (AppCourseReportVO vo : allCompanies) {
             Long companyId = vo.getCompanyId();
 
-            // 看课统计
+            // APP会员统计
+            if (companyStatsMap != null) {
+                int[] stats = companyStatsMap.getOrDefault(companyId, new int[]{0, 0});
+                vo.setAppUserCount(stats[0]);
+                vo.setActiveAppUserCount(stats[1]);
+            } else {
+                vo.setAppUserCount(0);
+                vo.setActiveAppUserCount(0);
+            }
+
+            // 看课统计 - 如果查不到数据就用0填充
             AppCourseReportVO watchStats = watchStatsMap.get(companyId);
             if (watchStats != null) {
-                vo.setPendingCount(watchStats.getPendingCount());
-                vo.setWatchingCount(watchStats.getWatchingCount());
-                vo.setFinishedCount(watchStats.getFinishedCount());
+                vo.setPendingCount(watchStats.getPendingCount() != null ? watchStats.getPendingCount() : 0);
+                vo.setWatchingCount(watchStats.getWatchingCount() != null ? watchStats.getWatchingCount() : 0);
+                vo.setFinishedCount(watchStats.getFinishedCount() != null ? watchStats.getFinishedCount() : 0);
+                vo.setAccessCount(watchStats.getAccessCount() != null ? watchStats.getAccessCount() : 0);
                 vo.setWatchRate(calculateWatchingRate(
-                        watchStats.getWatchingCount(),
-                        watchStats.getFinishedCount(),
-                        watchStats.getAccessCount()));
+                        vo.getWatchingCount(),
+                        vo.getFinishedCount(),
+                        vo.getAccessCount()));
+            } else {
+                // 查不到看课数据时设置默认值0
+                vo.setPendingCount(0);
+                vo.setWatchingCount(0);
+                vo.setFinishedCount(0);
+                vo.setAccessCount(0);
+                vo.setWatchRate(BigDecimal.ZERO);
             }
 
-            // 答题统计
+            // 答题统计 - 如果查不到数据就用0填充
             AppCourseReportVO answerStats = answerStatsMap.getOrDefault(companyId, new AppCourseReportVO());
-            vo.setAnswerUserCount(answerStats.getAnswerUserCount());
+            vo.setAnswerUserCount(answerStats.getAnswerUserCount() != null ? answerStats.getAnswerUserCount() : 0);
 
-            // 红包统计
+            // 红包统计 - 如果查不到数据就用0填充
             AppCourseReportVO redPacketStats = redPacketStatsMap.getOrDefault(companyId, new AppCourseReportVO());
-            vo.setPacketUserCount(redPacketStats.getPacketUserCount());
-            vo.setPacketAmount(redPacketStats.getPacketAmount());
+            vo.setPacketUserCount(redPacketStats.getPacketUserCount() != null ? redPacketStats.getPacketUserCount() : 0);
+            vo.setPacketAmount(redPacketStats.getPacketAmount() != null ? redPacketStats.getPacketAmount() : BigDecimal.ZERO);
         }
 
         return allCompanies;
@@ -1896,8 +1908,10 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             if (userStats != null) {
                 vo.setAppUserCount(userStats.getAppUserCount());
                 vo.setNewAppUserCount(userStats.getNewAppUserCount());
+            }else {
+                vo.setAppUserCount(0);
+                vo.setNewAppUserCount(0);
             }
-
             // 计算完课率 = 完课数 / (完课数 + 未完课数 + 未看数) * 100%
             int total = vo.getFinishedCount() + vo.getUnfinishedCount() + vo.getNotWatchedCount();
             if (total > 0) {
@@ -1986,8 +2000,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 vo.setAppUserCount(userStats.getAppUserCount());
                 vo.setNewAppUserCount(userStats.getNewAppUserCount());
                 vo.setSalesCount(userStats.getSalesCount()); // 销售数(部门维度特有)
+            }else {
+                vo.setAppUserCount(0);
+                vo.setNewAppUserCount(0);
+                vo.setSalesCount(0);
             }
-
             // 计算完课率 = 完课数 / (完课数 + 未完课数 + 未看数) * 100%
             int total = vo.getFinishedCount() + vo.getUnfinishedCount() + vo.getNotWatchedCount();
             if (total > 0) {

+ 52 - 0
fs-service/src/main/java/com/fs/course/vo/CourseCheckinResultVO.java

@@ -0,0 +1,52 @@
+package com.fs.course.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 看课打卡结果VO
+ *
+ * @author fs
+ * @date 2025-03-11
+ */
+@Data
+@ApiModel("看课打卡结果")
+public class CourseCheckinResultVO {
+
+    @ApiModelProperty("是否打卡成功")
+    private boolean success;
+
+    @ApiModelProperty("已打卡天数")
+    private Integer checkedDays;
+
+    @ApiModelProperty("还需打卡天数")
+    private Integer remainingDays;
+
+    @ApiModelProperty("提醒消息(为空则不显示弹框)")
+    private String reminderMessage;
+
+    @ApiModelProperty("奖品名称")
+    private String prizeName;
+
+    @ApiModelProperty("奖品类型:1-红包,2-积分商品券")
+    private Integer prizeType;
+
+    @ApiModelProperty("活动ID")
+    private Long activityId;
+
+    @ApiModelProperty("活动名称")
+    private String activityName;
+
+    /**
+     * 是否领取奖励
+     */
+    @ApiModelProperty("是否领取奖励")
+    private Boolean isReceived;
+
+    /**
+     * 是否已打卡但未领取奖励
+     */
+    @ApiModelProperty("是否已打卡但未领取奖励")
+    private Boolean checkedButNotReceived;
+}

+ 130 - 0
fs-service/src/main/resources/mapper/course/FsCourseCheckinActivityMapper.xml

@@ -0,0 +1,130 @@
+<?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.course.mapper.FsCourseCheckinActivityMapper">
+
+    <resultMap type="com.fs.course.domain.FsCourseCheckinActivity" id="FsCourseCheckinActivityResult">
+        <result property="activityId" column="activity_id"/>
+        <result property="activityName" column="activity_name"/>
+        <result property="startTime" column="start_time"/>
+        <result property="endTime" column="end_time"/>
+        <result property="checkinDays" column="checkin_days"/>
+        <result property="companyIds" column="company_ids"/>
+        <result property="projectIds" column="project_ids"/>
+        <result property="notifyTemplate" column="notify_template"/>
+        <result property="status" column="status"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <sql id="selectFsCourseCheckinActivityVo">
+        select activity_id, activity_name, start_time, end_time, checkin_days, company_ids, project_ids,
+               notify_template, status, create_by, create_time, update_by, update_time, remark
+        from fs_course_checkin_activity
+    </sql>
+
+    <select id="selectFsCourseCheckinActivityByActivityId" parameterType="Long"
+            resultMap="FsCourseCheckinActivityResult">
+        <include refid="selectFsCourseCheckinActivityVo"/>
+        where activity_id = #{activityId}
+    </select>
+
+    <select id="selectFsCourseCheckinActivityList" parameterType="com.fs.course.domain.FsCourseCheckinActivity"
+            resultMap="FsCourseCheckinActivityResult">
+        <include refid="selectFsCourseCheckinActivityVo"/>
+        <where>
+            <if test="activityName != null and activityName != ''">
+                and activity_name like concat('%', #{activityName}, '%')
+            </if>
+            <if test="status != null">
+                and status = #{status}
+            </if>
+            <if test="params.beginStartTime != null and params.beginStartTime != ''">
+                and start_time &gt;= #{params.beginStartTime}
+            </if>
+            <if test="params.endStartTime != null and params.endStartTime != ''">
+                and start_time &lt;= #{params.endStartTime}
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <insert id="insertFsCourseCheckinActivity" parameterType="com.fs.course.domain.FsCourseCheckinActivity"
+            useGeneratedKeys="true" keyProperty="activityId">
+        insert into fs_course_checkin_activity
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="activityName != null and activityName != ''">activity_name,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="endTime != null">end_time,</if>
+            <if test="checkinDays != null">checkin_days,</if>
+            <if test="companyIds != null and companyIds != ''">company_ids,</if>
+            <if test="projectIds != null and projectIds != ''">project_ids,</if>
+            <if test="notifyTemplate != null and notifyTemplate != ''">notify_template,</if>
+            <if test="status != null">status,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null and updateBy != ''">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="activityName != null and activityName != ''">#{activityName},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="checkinDays != null">#{checkinDays},</if>
+            <if test="companyIds != null and companyIds != ''">#{companyIds},</if>
+            <if test="projectIds != null and projectIds != ''">#{projectIds},</if>
+            <if test="notifyTemplate != null and notifyTemplate != ''">#{notifyTemplate},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null and updateBy != ''">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsCourseCheckinActivity" parameterType="com.fs.course.domain.FsCourseCheckinActivity">
+        update fs_course_checkin_activity
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activityName != null and activityName != ''">activity_name = #{activityName},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="endTime != null">end_time = #{endTime},</if>
+            <if test="checkinDays != null">checkin_days = #{checkinDays},</if>
+            <if test="companyIds != null">company_ids = #{companyIds},</if>
+            <if test="projectIds != null">project_ids = #{projectIds},</if>
+            <if test="notifyTemplate != null">notify_template = #{notifyTemplate},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where activity_id = #{activityId}
+    </update>
+
+    <delete id="deleteFsCourseCheckinActivityByActivityId" parameterType="Long">
+        delete from fs_course_checkin_activity where activity_id = #{activityId}
+    </delete>
+
+    <delete id="deleteFsCourseCheckinActivityByActivityIds">
+        delete from fs_course_checkin_activity where activity_id in
+        <foreach collection="array" item="activityId" open="(" separator="," close=")">
+            #{activityId}
+        </foreach>
+    </delete>
+
+    <!-- 查询当前时间范围内、指定公司和项目的进行中的活动 -->
+    <select id="selectActiveActivities" resultMap="FsCourseCheckinActivityResult">
+        <include refid="selectFsCourseCheckinActivityVo"/>
+        where status = 1
+        and (company_ids is null or company_ids = '' or find_in_set(#{companyId}, company_ids))
+        and (project_ids is null or project_ids = '' or find_in_set(#{projectId}, project_ids))
+        and date(start_time) &lt;= date(#{currentDate})
+        and date(end_time) &gt;= date(#{currentDate})
+    </select>
+
+</mapper>

+ 93 - 0
fs-service/src/main/resources/mapper/course/FsCourseCheckinDetailMapper.xml

@@ -0,0 +1,93 @@
+<?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.course.mapper.FsCourseCheckinDetailMapper">
+    
+    <resultMap type="FsCourseCheckinDetail" id="FsCourseCheckinDetailResult">
+        <result property="detailId"    column="detail_id"    />
+        <result property="activityId"    column="activity_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="projectId"    column="project_id"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="checkinTime"    column="checkin_time"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectFsCourseCheckinDetailVo">
+        select detail_id, activity_id, user_id, company_id, project_id, course_id, checkin_time, create_time from fs_course_checkin_detail
+    </sql>
+
+    <select id="selectFsCourseCheckinDetailList" parameterType="FsCourseCheckinDetail" resultMap="FsCourseCheckinDetailResult">
+        <include refid="selectFsCourseCheckinDetailVo"/>
+        <where>  
+            <if test="activityId != null "> and activity_id = #{activityId}</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="projectId != null "> and project_id = #{projectId}</if>
+            <if test="courseId != null "> and course_id = #{courseId}</if>
+        </where>
+    </select>
+    
+    <select id="selectFsCourseCheckinDetailByDetailId" parameterType="Long" resultMap="FsCourseCheckinDetailResult">
+        <include refid="selectFsCourseCheckinDetailVo"/>
+        where detail_id = #{detailId}
+    </select>
+
+    <select id="selectByActivityUserCourseAndTime" resultMap="FsCourseCheckinDetailResult">
+        <include refid="selectFsCourseCheckinDetailVo"/>
+        where activity_id = #{activityId}
+        and user_id = #{userId}
+        and course_id = #{courseId}
+        and date(checkin_time) = date(#{checkinTime})
+        limit 1
+    </select>
+        
+    <insert id="insertFsCourseCheckinDetail" parameterType="FsCourseCheckinDetail" useGeneratedKeys="true" keyProperty="detailId">
+        insert into fs_course_checkin_detail
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">activity_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="projectId != null">project_id,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="checkinTime != null">checkin_time,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">#{activityId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="projectId != null">#{projectId},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="checkinTime != null">#{checkinTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsCourseCheckinDetail" parameterType="FsCourseCheckinDetail">
+        update fs_course_checkin_detail
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activityId != null">activity_id = #{activityId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="projectId != null">project_id = #{projectId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="checkinTime != null">checkin_time = #{checkinTime},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </trim>
+        where detail_id = #{detailId}
+    </update>
+
+    <delete id="deleteFsCourseCheckinDetailByDetailId" parameterType="Long">
+        delete from fs_course_checkin_detail where detail_id = #{detailId}
+    </delete>
+
+    <delete id="deleteFsCourseCheckinDetailByDetailIds" parameterType="String">
+        delete from fs_course_checkin_detail where detail_id in 
+        <foreach item="detailId" collection="array" open="(" separator="," close=")">
+            #{detailId}
+        </foreach>
+    </delete>
+</mapper>

+ 128 - 0
fs-service/src/main/resources/mapper/course/FsCourseCheckinPrizeMapper.xml

@@ -0,0 +1,128 @@
+<?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.course.mapper.FsCourseCheckinPrizeMapper">
+
+    <resultMap type="com.fs.course.domain.FsCourseCheckinPrize" id="FsCourseCheckinPrizeResult">
+        <result property="prizeId" column="prize_id"/>
+        <result property="activityId" column="activity_id"/>
+        <result property="prizeType" column="prize_type"/>
+        <result property="prizeName" column="prize_name"/>
+        <result property="redpacketAmount" column="redpacket_amount"/>
+        <result property="goodsId" column="goods_id"/>
+        <result property="sort" column="sort"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <sql id="selectFsCourseCheckinPrizeVo">
+        select prize_id, activity_id, prize_type, prize_name, redpacket_amount, goods_id,
+               sort, create_by, create_time, update_by, update_time, remark
+        from fs_course_checkin_prize
+    </sql>
+
+    <select id="selectFsCourseCheckinPrizeByPrizeId" parameterType="Long" resultMap="FsCourseCheckinPrizeResult">
+        <include refid="selectFsCourseCheckinPrizeVo"/>
+        where prize_id = #{prizeId}
+    </select>
+
+    <select id="selectFsCourseCheckinPrizeList" parameterType="com.fs.course.domain.FsCourseCheckinPrize"
+            resultMap="FsCourseCheckinPrizeResult">
+        <include refid="selectFsCourseCheckinPrizeVo"/>
+        <where>
+            <if test="activityId != null">
+                and activity_id = #{activityId}
+            </if>
+            <if test="prizeType != null">
+                and prize_type = #{prizeType}
+            </if>
+            <if test="prizeName != null and prizeName != ''">
+                and prize_name like concat('%', #{prizeName}, '%')
+            </if>
+        </where>
+        order by sort asc, create_time desc
+    </select>
+
+    <select id="selectFsCourseCheckinPrizeByActivityId" parameterType="Long" resultMap="FsCourseCheckinPrizeResult">
+        <include refid="selectFsCourseCheckinPrizeVo"/>
+        where activity_id = #{activityId}
+        order by sort asc, create_time desc
+    </select>
+
+    <insert id="insertFsCourseCheckinPrize" parameterType="com.fs.course.domain.FsCourseCheckinPrize"
+            useGeneratedKeys="true" keyProperty="prizeId">
+        insert into fs_course_checkin_prize
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">activity_id,</if>
+            <if test="prizeType != null">prize_type,</if>
+            <if test="prizeName != null and prizeName != ''">prize_name,</if>
+            <if test="redpacketAmount != null">redpacket_amount,</if>
+            <if test="goodsId != null">goods_id,</if>
+            <if test="sort != null">sort,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null and updateBy != ''">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">#{activityId},</if>
+            <if test="prizeType != null">#{prizeType},</if>
+            <if test="prizeName != null and prizeName != ''">#{prizeName},</if>
+            <if test="redpacketAmount != null">#{redpacketAmount},</if>
+            <if test="goodsId != null">#{goodsId},</if>
+            <if test="sort != null">#{sort},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null and updateBy != ''">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsCourseCheckinPrize" parameterType="com.fs.course.domain.FsCourseCheckinPrize">
+        update fs_course_checkin_prize
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activityId != null">activity_id = #{activityId},</if>
+            <if test="prizeType != null">prize_type = #{prizeType},</if>
+            <if test="prizeName != null and prizeName != ''">prize_name = #{prizeName},</if>
+            <if test="redpacketAmount != null">redpacket_amount = #{redpacketAmount},</if>
+            <if test="goodsId != null">goods_id = #{goodsId},</if>
+            <if test="sort != null">sort = #{sort},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where prize_id = #{prizeId}
+    </update>
+
+    <delete id="deleteFsCourseCheckinPrizeByPrizeId" parameterType="Long">
+        delete from fs_course_checkin_prize where prize_id = #{prizeId}
+    </delete>
+
+    <delete id="deleteFsCourseCheckinPrizeByPrizeIds">
+        delete from fs_course_checkin_prize where prize_id in
+        <foreach collection="array" item="prizeId" open="(" separator="," close=")">
+            #{prizeId}
+        </foreach>
+    </delete>
+
+    <delete id="deleteFsCourseCheckinPrizeByActivityId" parameterType="Long">
+        delete from fs_course_checkin_prize where activity_id = #{activityId}
+    </delete>
+
+    <insert id="batchInsertFsCourseCheckinPrize">
+        insert into fs_course_checkin_prize
+        (activity_id, prize_type, prize_name, redpacket_amount, goods_id, sort, create_by, create_time, remark)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.activityId}, #{item.prizeType}, #{item.prizeName}, #{item.redpacketAmount}, #{item.goodsId},
+             #{item.sort}, #{item.createBy}, #{item.createTime}, #{item.remark})
+        </foreach>
+    </insert>
+
+</mapper>

+ 170 - 0
fs-service/src/main/resources/mapper/course/FsCourseCheckinReceiveMapper.xml

@@ -0,0 +1,170 @@
+<?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.course.mapper.FsCourseCheckinReceiveMapper">
+
+    <resultMap type="com.fs.course.domain.FsCourseCheckinReceive" id="FsCourseCheckinReceiveResult">
+        <result property="receiveId" column="receive_id"/>
+        <result property="activityId" column="activity_id"/>
+        <result property="prizeId" column="prize_id"/>
+        <result property="userId" column="user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="projectId" column="project_id"/>
+        <result property="prizeType" column="prize_type"/>
+        <result property="prizeName" column="prize_name"/>
+        <result property="redpacketAmount" column="redpacket_amount"/>
+        <result property="goodsId" column="goods_id"/>
+        <result property="receiveStatus" column="receive_status"/>
+        <result property="receiveTime" column="receive_time"/>
+        <result property="notifyStatus" column="notify_status"/>
+        <result property="notifyTime" column="notify_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="outBatchNo" column="out_batch_no"/>
+        <result property="batchId" column="batch_id"/>
+
+    </resultMap>
+
+    <sql id="selectFsCourseCheckinReceiveVo">
+        select receive_id, r.activity_id, r.prize_id, r.user_id, r.company_id, r.project_id,
+               r.prize_type, r.prize_name, r.redpacket_amount, r.goods_id,
+               r.receive_status, r.receive_time,
+               r.notify_status, r.notify_time, r.create_time, r.update_time,r.out_batch_no,r.batch_id
+        from fs_course_checkin_receive r
+    </sql>
+
+    <select id="selectFsCourseCheckinReceiveByReceiveId" parameterType="Long"
+            resultMap="FsCourseCheckinReceiveResult">
+        <include refid="selectFsCourseCheckinReceiveVo"/>
+        where r.receive_id = #{receiveId}
+    </select>
+
+    <select id="selectFsCourseCheckinReceiveList" parameterType="com.fs.course.domain.FsCourseCheckinReceive"
+            resultMap="FsCourseCheckinReceiveResult">
+        <include refid="selectFsCourseCheckinReceiveVo"/>
+        left join fs_user u on r.user_id = u.user_id
+        left join fs_course_checkin_activity a on r.activity_id = a.activity_id
+        <where>
+            <if test="activityId != null">
+                and r.activity_id = #{activityId}
+            </if>
+            <if test="userId != null">
+                and r.user_id = #{userId}
+            </if>
+            <if test="prizeId != null">
+                and r.prize_id = #{prizeId}
+            </if>
+            <if test="receiveStatus != null">
+                and r.receive_status = #{receiveStatus}
+            </if>
+            <if test="notifyStatus != null">
+                and r.notify_status = #{notifyStatus}
+            </if>
+        </where>
+        order by r.create_time desc
+    </select>
+
+    <select id="selectPendingReceiveList" resultMap="FsCourseCheckinReceiveResult">
+        <include refid="selectFsCourseCheckinReceiveVo"/>
+        where r.receive_status = 0
+        order by r.create_time asc
+        <if test="limit != null">
+            limit #{limit}
+        </if>
+    </select>
+
+    <insert id="insertFsCourseCheckinReceive" parameterType="com.fs.course.domain.FsCourseCheckinReceive"
+            useGeneratedKeys="true" keyProperty="receiveId">
+        insert into fs_course_checkin_receive
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">activity_id,</if>
+            <if test="prizeId != null">prize_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="projectId != null">project_id,</if>
+            <if test="prizeType != null">prize_type,</if>
+            <if test="prizeName != null and prizeName != ''">prize_name,</if>
+            <if test="redpacketAmount != null">redpacket_amount,</if>
+            <if test="goodsId != null">goods_id,</if>
+            <if test="receiveStatus != null">receive_status,</if>
+            <if test="receiveTime != null">receive_time,</if>
+            <if test="notifyStatus != null">notify_status,</if>
+            <if test="notifyTime != null">notify_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="outBatchNo != null">out_batch_no,</if>
+            <if test="batchId != null">batch_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">#{activityId},</if>
+            <if test="prizeId != null">#{prizeId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="projectId != null">#{projectId},</if>
+            <if test="prizeType != null">#{prizeType},</if>
+            <if test="prizeName != null and prizeName != ''">#{prizeName},</if>
+            <if test="redpacketAmount != null">#{redpacketAmount},</if>
+            <if test="goodsId != null">#{goodsId},</if>
+            <if test="receiveStatus != null">#{receiveStatus},</if>
+            <if test="receiveTime != null">#{receiveTime},</if>
+            <if test="notifyStatus != null">#{notifyStatus},</if>
+            <if test="notifyTime != null">#{notifyTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="outBatchNo != null">#{outBatchNo},</if>
+            <if test="batchId != null">#{batchId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsCourseCheckinReceive" parameterType="com.fs.course.domain.FsCourseCheckinReceive">
+        update fs_course_checkin_receive
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activityId != null">activity_id = #{activityId},</if>
+            <if test="prizeId != null">prize_id = #{prizeId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="projectId != null">project_id = #{projectId},</if>
+            <if test="prizeType != null">prize_type = #{prizeType},</if>
+            <if test="prizeName != null">prize_name = #{prizeName},</if>
+            <if test="redpacketAmount != null">redpacket_amount = #{redpacketAmount},</if>
+            <if test="goodsId != null">goods_id = #{goodsId},</if>
+            <if test="receiveStatus != null">receive_status = #{receiveStatus},</if>
+            <if test="receiveTime != null">receive_time = #{receiveTime},</if>
+            <if test="notifyStatus != null">notify_status = #{notifyStatus},</if>
+            <if test="notifyTime != null">notify_time = #{notifyTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="outBatchNo != null">out_batch_no = #{outBatchNo},</if>
+            <if test="batchId != null">batch_id = #{batchId},</if>
+        </trim>
+        where receive_id = #{receiveId}
+    </update>
+
+    <delete id="deleteFsCourseCheckinReceiveByReceiveId" parameterType="Long">
+        delete from fs_course_checkin_receive where receive_id = #{receiveId}
+    </delete>
+
+    <delete id="deleteFsCourseCheckinReceiveByReceiveIds">
+        delete from fs_course_checkin_receive where receive_id in
+        <foreach collection="array" item="receiveId" open="(" separator="," close=")">
+            #{receiveId}
+        </foreach>
+    </delete>
+
+    <delete id="deleteFsCourseCheckinReceiveByActivityId" parameterType="Long">
+        delete from fs_course_checkin_receive where activity_id = #{activityId}
+    </delete>
+
+    <insert id="batchInsertFsCourseCheckinReceive">
+        insert into fs_course_checkin_receive
+        (activity_id, prize_id, user_id, company_id, project_id, prize_type, prize_name, redpacket_amount, goods_id,
+         receive_status, create_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.activityId}, #{item.prizeId}, #{item.userId}, #{item.companyId}, #{item.projectId},
+             #{item.prizeType}, #{item.prizeName}, #{item.redpacketAmount}, #{item.goodsId},
+             #{item.receiveStatus}, #{item.createTime})
+        </foreach>
+    </insert>
+
+</mapper>

+ 117 - 0
fs-service/src/main/resources/mapper/course/FsCourseCheckinUserMapper.xml

@@ -0,0 +1,117 @@
+<?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.course.mapper.FsCourseCheckinUserMapper">
+
+    <resultMap type="com.fs.course.domain.FsCourseCheckinUser" id="FsCourseCheckinUserResult">
+        <result property="id" column="id"/>
+        <result property="activityId" column="activity_id"/>
+        <result property="userId" column="user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="projectId" column="project_id"/>
+        <result property="checkinDays" column="checkin_days"/>
+        <result property="firstCheckinTime" column="first_checkin_time"/>
+        <result property="lastCheckinTime" column="last_checkin_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="courseId" column="course_id"/>
+    </resultMap>
+
+    <sql id="selectFsCourseCheckinUserVo">
+        select id, activity_id, user_id, company_id, project_id, checkin_days, first_checkin_time,
+               last_checkin_time, create_time, update_time,course_id
+        from fs_course_checkin_user
+    </sql>
+
+    <select id="selectFsCourseCheckinUserById" parameterType="Long" resultMap="FsCourseCheckinUserResult">
+        <include refid="selectFsCourseCheckinUserVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectByActivityAndUser" resultMap="FsCourseCheckinUserResult">
+        <include refid="selectFsCourseCheckinUserVo"/>
+        where activity_id = #{activityId} and user_id = #{userId}
+    </select>
+
+    <select id="selectFsCourseCheckinUserList" parameterType="com.fs.course.domain.FsCourseCheckinUser"
+            resultMap="FsCourseCheckinUserResult">
+        <include refid="selectFsCourseCheckinUserVo"/>
+        <where>
+            <if test="activityId != null">
+                and activity_id = #{activityId}
+            </if>
+            <if test="userId != null">
+                and user_id = #{userId}
+            </if>
+            <if test="companyId != null">
+                and company_id = #{companyId}
+            </if>
+            <if test="projectId != null">
+                and project_id = #{projectId}
+            </if>
+        </where>
+        order by checkin_days desc, last_checkin_time desc
+    </select>
+
+    <insert id="insertFsCourseCheckinUser" parameterType="com.fs.course.domain.FsCourseCheckinUser"
+            useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_checkin_user
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">activity_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="projectId != null">project_id,</if>
+            <if test="checkinDays != null">checkin_days,</if>
+            <if test="firstCheckinTime != null">first_checkin_time,</if>
+            <if test="lastCheckinTime != null">last_checkin_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="courseId != null">course_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="activityId != null">#{activityId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="projectId != null">#{projectId},</if>
+            <if test="checkinDays != null">#{checkinDays},</if>
+            <if test="firstCheckinTime != null">#{firstCheckinTime},</if>
+            <if test="lastCheckinTime != null">#{lastCheckinTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="courseId != null">#{courseId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsCourseCheckinUser" parameterType="com.fs.course.domain.FsCourseCheckinUser">
+        update fs_course_checkin_user
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="activityId != null">activity_id = #{activityId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="projectId != null">project_id = #{projectId},</if>
+            <if test="checkinDays != null">checkin_days = #{checkinDays},</if>
+            <if test="firstCheckinTime != null">first_checkin_time = #{firstCheckinTime},</if>
+            <if test="lastCheckinTime != null">last_checkin_time = #{lastCheckinTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseCheckinUserById" parameterType="Long">
+        delete from fs_course_checkin_user where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseCheckinUserByIds">
+        delete from fs_course_checkin_user where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteFsCourseCheckinUserByActivityId" parameterType="Long">
+        delete from fs_course_checkin_user where activity_id = #{activityId}
+    </delete>
+
+</mapper>

+ 3 - 3
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -728,9 +728,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where user_id = #{userId}
     </update>
 
-    <delete id="deleteFsUserByUserId" parameterType="Long">
-        delete from fs_user where user_id = #{userId}
-    </delete>
+    <update id="deleteFsUserByUserId" parameterType="Long">
+        update fs_user set is_del = 1 where user_id = #{userId}
+    </update>
 
     <delete id="deleteFsUserByUserIds" parameterType="Long">
         delete from fs_user where user_id in

+ 50 - 55
fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -162,61 +162,56 @@ public class CompanyUserController extends AppBaseController {
                     }
                 }
                 // 有多个相同手机号的用户,需要合并用户
-//                if (usersByPhone.size() > 1) {
-//                    // 合并规则:优先保留有 union_id 的用户
-//                    FsUser keepUser = null;
-//                    FsUser deleteUser = null;
-//                    FsUser userByUnionId = usersByPhone.stream()
-//                            .filter(u -> StringUtils.isNotEmpty(u.getUnionId()))
-//                            .findFirst()
-//                            .orElse(null);
-//
-//                    if (userByUnionId != null) {
-//                        // 有 union_id 的用户保留
-//                        keepUser = userByUnionId;
-//                        // 找出需要删除的其他用户(第一个非保留用户)
-//                        FsUser finalKeepUser = keepUser;
-//                        deleteUser = usersByPhone.stream()
-//                                .filter(u -> !u.getUserId().equals(finalKeepUser.getUserId()))
-//                                .findFirst()
-//                                .orElse(null);
-//                    } else {
-//                        // 都没有 union_id,按创建时间保留较早的用户
-//                        keepUser = usersByPhone.get(0);
-//                        for (FsUser u : usersByPhone) {
-//                            if (u.getCreateTime().before(keepUser.getCreateTime())) {
-//                                keepUser = u;
-//                            }
-//                        }
-//                        FsUser finalKeepUser1 = keepUser;
-//                        deleteUser = usersByPhone.stream()
-//                                .filter(u -> !u.getUserId().equals(finalKeepUser1.getUserId()))
-//                                .findFirst()
-//                                .orElse(null);
-//                    }
-//
-//                    if (deleteUser != null && keepUser != null) {
-//                        // 更新保留用户的信息
-//                        keepUser.setPhone(param.getPhone());
-//                        // 如果保留用户没有 source,而被删除用户有 source,则转移 source
-//                        if (StringUtils.isEmpty(keepUser.getSource()) && StringUtils.isNotEmpty(deleteUser.getSource())) {
-//                            keepUser.setSource(deleteUser.getSource());
-//                        }
-//                        // 如果保留用户没有 loginDevice,而被删除用户有 loginDevice,则转移 loginDevice
-//                        if (StringUtils.isEmpty(keepUser.getLoginDevice()) && StringUtils.isNotEmpty(deleteUser.getLoginDevice())) {
-//                            keepUser.setLoginDevice(deleteUser.getLoginDevice());
-//                        }
-//                        // 删除被合并的用户
-//                        fsUserService.realDeleteFsUserByUserId(deleteUser.getUserId());
-//
-//                        // 如果当前用户不是保留用户,需要更新当前用户
-//                        if (!user.getUserId().equals(keepUser.getUserId())) {
-//                            currentUserId = keepUser.getUserId();
-//                            map.setUserId(currentUserId);
-//                            user = keepUser;
-//                        }
-//                    }
-//                }
+                if (usersByPhone.size() > 1) {
+                    // 合并规则:优先保留有 union_id 的用户
+                    FsUser keepUser = null;
+                    FsUser userByUnionId = usersByPhone.stream()
+                            .filter(u -> StringUtils.isNotEmpty(u.getUnionId()))
+                            .findFirst()
+                            .orElse(null);
+
+                    if (userByUnionId != null) {
+                        // 有 union_id 的用户保留
+                        keepUser = userByUnionId;
+                    } else {
+                        // 都没有 union_id,按创建时间保留较早的用户
+                        keepUser = usersByPhone.get(0);
+                        for (FsUser u : usersByPhone) {
+                            if (u.getCreateTime().before(keepUser.getCreateTime())) {
+                                keepUser = u;
+                            }
+                        }
+                    }
+
+                    if (keepUser != null) {
+                        // 找出所有需要删除的用户(除了保留用户)
+                        FsUser finalKeepUser = keepUser;
+                        List<FsUser> deleteUsers = usersByPhone.stream()
+                                .filter(u -> !u.getUserId().equals(finalKeepUser.getUserId()))
+                                .collect(Collectors.toList());
+
+                        // 合并所有需要删除用户的信息到保留用户
+                        for (FsUser deleteUser : deleteUsers) {
+                            // 如果保留用户没有 source,而被删除用户有 source,则转移 source
+                            if (StringUtils.isEmpty(keepUser.getSource()) && StringUtils.isNotEmpty(deleteUser.getSource())) {
+                                keepUser.setSource(deleteUser.getSource());
+                            }
+                            // 如果保留用户没有 loginDevice,而被删除用户有 loginDevice,则转移 loginDevice
+                            if (StringUtils.isEmpty(keepUser.getLoginDevice()) && StringUtils.isNotEmpty(deleteUser.getLoginDevice())) {
+                                keepUser.setLoginDevice(deleteUser.getLoginDevice());
+                            }
+                            // 删除被合并的用户
+                            fsUserService.realDeleteFsUserByUserId(deleteUser.getUserId());
+                        }
+
+                        // 如果当前用户不是保留用户,需要更新当前用户
+                        if (!user.getUserId().equals(keepUser.getUserId())) {
+                            currentUserId = keepUser.getUserId();
+                            map.setUserId(currentUserId);
+                            user = keepUser;
+                        }
+                    }
+                }
             }
             user.setPhone(param.getPhone());
             fsUserService.updateFsUser(user);

+ 37 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -19,6 +19,7 @@ import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
 import com.fs.course.param.newfs.FsUserCourseVideoUParam;
 import com.fs.course.service.*;
+import com.fs.course.vo.CourseCheckinResultVO;
 import com.fs.course.vo.FsUserCourseVideoH5VO;
 import com.fs.course.vo.newfs.FsUserCourseVideoLinkDetailsVO;
 import com.fs.his.domain.FsUser;
@@ -36,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
+import java.util.Map;
 
 @Api("会员-看课接口")
 @RestController
@@ -64,6 +66,12 @@ public class CourseFsUserController extends AppBaseController {
     @Autowired
     private ICompanyUserService companyUserService;
 
+    @Autowired
+    private IFsCourseCheckinUserService fsCourseCheckinUserService;
+
+    @Autowired
+    private IFsCourseCheckinReceiveService fsCourseCheckinReceiveService;
+
 
     @Login
     @ApiOperation("判断是否添加客服(是否关联销售)")
@@ -197,4 +205,33 @@ public class CourseFsUserController extends AppBaseController {
         return R.error();
     }
 
+    /**
+     * 用户看课打卡
+     */
+    @ApiOperation("用户看课打卡")
+    @Login
+    @PostMapping("/doCheckin")
+    public R doCheckin(@RequestBody FsUserCourseAddCompanyUserParam param) {
+            Long userId = Long.parseLong(getUserId());
+            CourseCheckinResultVO result = fsCourseCheckinUserService.handleCourseCheckin(userId, param.getCompanyId(),param.getProjectId(),param.getCourseId());
+            if (result.isSuccess()) {
+                return R.ok().put("data", result);
+            } else {
+                return R.error("活动已经结束");
+            }
+    }
+
+    /**
+     * 领取打卡奖品
+     */
+    @ApiOperation("领取打卡奖品")
+    @Login
+    @PostMapping("/receivePrize")
+    public R receivePrize(@RequestBody FsCourseSendRewardUParam param) {
+        Long userId = Long.parseLong(getUserId());
+        param.setUserId(userId);
+        Map<String, Object> result = fsCourseCheckinReceiveService.checkAndCreateReceiveRecord(param);
+        return R.ok().put("data", result);
+    }
+
 }