wangzhijun 1 неделя назад
Родитель
Сommit
4f6467e333

+ 4 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductOrderInfoServiceImpl.java

@@ -361,7 +361,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
 
         // 3. 状态校验:必须是 待收货 才能确认收货
         if (!ProductOrderStatusEnum.WAIT_RECEIVE.getCode().equals(productOrderInfo.getOrderStatus())) {
-            throw new RuntimeException("订单状态异常,仅待收货状态可确认收货");
+            throw new ServiceException("订单状态异常,仅待收货状态可确认收货");
         }
 
         // 修改订单信息
@@ -384,7 +384,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
 
         // 3. 状态校验:必须是 退货中/退款中 才能取消退货
         if (!ProductOrderStatusEnum.RETURNING.getCode().equals(productOrderInfo.getOrderStatus())) {
-            throw new RuntimeException("订单状态异常,仅退货中状态可取消退货");
+            throw new ServiceException("订单状态异常,仅退货中状态可取消退货");
         }
         // 修改订单信息
         LocalDateTime now = LocalDateTime.now();
@@ -396,7 +396,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
     private WxLoginUser getCurrentWxLoginUser() {
         WxLoginUser loginUser = SecurityUtils.getWxLoginUser();
         if (ObjectUtil.isNull(loginUser)) {
-            throw new RuntimeException("用户未登录或登录已过期");
+            throw new ServiceException("用户未登录或登录已过期");
         }
         return loginUser;
     }
@@ -409,7 +409,7 @@ public class ProductOrderInfoServiceImpl extends ServiceImpl<ProductOrderInfoMap
         // 订单是单条,直接用 selectOne 更合理
         ProductOrderInfo orderInfo = this.productOrderInfoMapper.selectOne(queryWrapper);
         if (ObjectUtil.isNull(orderInfo)) {
-            throw new RuntimeException("订单不存在或无权限操作");
+            throw new ServiceException("订单不存在或无权限操作");
         }
         return orderInfo;
     }

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/point/domain/PointUserActivityTaskCompletion.java

@@ -2,6 +2,7 @@ package com.ylx.point.domain;
 
 import java.util.Date;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.ylx.common.core.domain.BaseEntity;
 import lombok.Data;
@@ -69,4 +70,10 @@ public class PointUserActivityTaskCompletion extends BaseEntity {
     @Excel(name = "最后完成日期", readConverterExp = "生=成列")
     private Date completedDate;
 
+    @TableField(exist = false)
+    private String createBy;
+
+    @TableField(exist = false)
+    private String updateBy;
+
 }

+ 15 - 0
nightFragrance-massage/src/main/java/com/ylx/point/service/IPointUserActivityTaskCompletionService.java

@@ -19,4 +19,19 @@ public interface IPointUserActivityTaskCompletionService extends IService<PointU
     Integer selectCompletedTaskCount(String openId);
 
     List<PointUserActivityTaskCompletion> selectCompletionsByOpenIdAndTaskIds(String openId, List<Long> taskIds);
+
+    /**
+     * 判断用户今日是否已完成该任务(用于每日一次任务、每日签到)
+     */
+    boolean isTodayCompleted(String openId, Long activityId, Long taskId, Integer taskType);
+
+    /**
+     * 任务完成后调用:次数+1,更新最后完成时间
+     */
+    void completeTask(String openId, Long activityId, Long taskId, Integer taskType);
+
+    /**
+     * 获取用户累计完成次数
+     */
+    int getUserTaskTotalCount(String openId, Long activityId, Long taskId, Integer taskType);
 }

+ 70 - 112
nightFragrance-massage/src/main/java/com/ylx/point/service/impl/PointActivityServiceImpl.java

@@ -116,7 +116,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
 
         PointActivity pointActivity = this.pointActivityMapper.selectPointActivityById(id);
         if (ObjectUtil.isNull(pointActivity)) {
-            throw new BaseException("参数有误,活动不存在");
+            throw new IllegalArgumentException("参数有误,活动不存在");
         }
 
         Integer activityType = pointActivity.getActivityType();
@@ -157,18 +157,18 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         // 校验参数
         Long id = dto.getId();
         if (ObjectUtil.isNull(id)) {
-            throw new BaseException("活动主键id不能为空");
+            throw new IllegalArgumentException("活动主键id不能为空");
         }
 
         // 根据id获取活动详情
         PointActivity entity = this.pointActivityMapper.selectPointActivityById(id);
         if (ObjectUtil.isNull(entity)) {
-            throw new BaseException("参数有误,活动不存在");
+            throw new IllegalArgumentException("参数有误,活动不存在");
         }
 
         // 校验活动状态
         if (PointActivityStatusEnum.IN_PROGRESS.getCode().equals(entity.getStatus())) {
-            throw new BaseException("活动进行中,不可编辑");
+            throw new ServiceException("活动进行中,不可编辑");
         }
 
         updateActivity(entity, dto);
@@ -184,7 +184,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         // 根据id获取活动详情
         PointActivity entity = this.pointActivityMapper.selectPointActivityById(id);
         if (ObjectUtil.isNull(entity)) {
-            throw new BaseException("参数有误,活动不存在");
+            throw new IllegalArgumentException("参数有误,活动不存在");
         }
 
         PointActivityStatVo vo = new PointActivityStatVo();
@@ -220,7 +220,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
             boolean hasOngoingActivity = activityList.stream()
                     .anyMatch(activity -> PointActivityStatusEnum.IN_PROGRESS.getCode().equals(activity.getStatus()));
             if (hasOngoingActivity) {
-                throw new BaseException("存在进行中的活动,无法修改有效期");
+                throw new ServiceException("存在进行中的活动,无法修改有效期");
             }
             // 更新积分活动过期策略表
             this.saveOrUpdatePointActivityExpirePolicy(expirePolicy, dto);
@@ -251,7 +251,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
             return result;
         } catch (Exception e) {
             log.error("查询积分活动有效期详情失败", e);
-            throw new BaseException("查询积分活动有效期详情失败");
+            throw new ServiceException("查询积分活动有效期详情失败");
         }
     }
 
@@ -264,10 +264,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
     public Page<UserPointActivityVo> getUserPointActivityList(Page<PointActivity> page, UserPointActivityPageDTO dto) {
 
         // 当前登录用户信息
-        WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
-        if (ObjectUtil.isNull(wxLoginUser)) {
-            throw new RuntimeException("用户不存在");
-        }
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
         String openId = wxLoginUser.getCOpenid();
         dto.setOpenId(openId);
         return pointActivityMapper.selectTaskWithProgress(page, dto);
@@ -277,10 +274,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
     public List<UserPointActivityVo> activityList() {
 
         // 当前登录用户信息
-        WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
-        if (ObjectUtil.isNull(wxLoginUser)) {
-            throw new RuntimeException("用户不存在");
-        }
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
         String openId = wxLoginUser.getCOpenid();
 
         List<UserPointActivityVo> resultList = new ArrayList<>();
@@ -322,11 +316,9 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
 
     @Override
     public List<SignDayVo> getSignInfo(SignDTO dto) {
-        // ... (前面获取用户、任务、奖励配置的代码保持不变) ...
 
-        // 1. 获取用户信息 & 任务 & 奖励列表
-        WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
-        if (ObjectUtil.isNull(wxLoginUser)) throw new ServiceException("用户不存在");
+        // 当前登录用户信息
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
         String openId = wxLoginUser.getCOpenid();
 
         PointSignTask task = getEnabledTask(dto.getCityCode());
@@ -355,9 +347,8 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
 
         // 定义“过期”判定阈值:例如,超过 7 天未签视为过期
         // 注意:这里的逻辑是,如果是“本周”的视图,通常只有“断签”和“未签”。
-        // 但如果你的业务是“补签卡”界面,或者需要展示历史状态,这个逻辑才有意义。
-        // 这里演示:如果漏签日期距离今天超过 7 天,标记为 4。
-        int expireThresholdDays = 7;
+        // 如果漏签日期距离今天超过 2 天,标记为 4。
+        int expireThresholdDays = 1;
 
         List<SignDayVo> voList = new ArrayList<>();
 
@@ -369,7 +360,6 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
             vo.setDate(date);
 
             boolean isToday = date.equals(today);
-            boolean isPast = date.isBefore(today);
 
             // 查询该天是否已签 (建议优化为批量查询)
             boolean actuallySigned = false;
@@ -415,129 +405,89 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void sign(SignDTO dto) {
-        // 1. 获取用户信息
-        WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
-        if (ObjectUtil.isNull(wxLoginUser)) {
-            throw new RuntimeException("用户未登录");
-        }
+        // 1. 用户登录校验
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
         String openId = wxLoginUser.getCOpenid();
 
-        // 2. 获取当前城市有效的签到任务配置
-        // 建议:这里应该加缓存,避免每次签到都查库
+        // 2. 获取有效签到任务
         PointSignTask task = getEnabledTask(dto.getCityCode());
         if (ObjectUtil.isNull(task)) {
-            throw new RuntimeException("当前城市暂无签到活动");
+            throw new ServiceException("当前城市暂无签到活动");
         }
         Long activityId = task.getActivityId();
         Long taskId = task.getId();
         String activityName = task.getActivityName();
+        Integer taskType = PointActivityTypeEnum.SIGN_TASK.getCode();
 
-        // 3. 防重复签到 (利用数据库唯一索引兜底,这里做快速失败)
-        // 优化:直接查库比 count 快,且逻辑更清晰
-        PointUserSignLog todayLog = pointUserSignLogService.getOne(new LambdaQueryWrapper<PointUserSignLog>()
-                .eq(PointUserSignLog::getOpenId, openId)
-                .eq(PointUserSignLog::getTaskId, taskId)
-                .eq(PointUserSignLog::getSignDate, DateUtils.getNowDate()) // 假设日志表有 sign_date 字段,如果没有用 create_time 转 date
-                .last("LIMIT 1"));
-
-        if (ObjectUtil.isNotNull(todayLog)) {
-            throw new RuntimeException("今日已签到");
+        // ======================== 核心:统一使用任务完成表防重 ========================
+        boolean todaySigned = pointUserActivityTaskCompletionService.isTodayCompleted(openId, activityId, taskId, taskType);
+        if (todaySigned) {
+            throw new ServiceException("今日已签到");
         }
 
-        // 4. 获取并锁定用户签到状态 (悲观锁)
-        // 注意:selectByOpenIdAndActivityIdForUpdate 必须使用 FOR UPDATE 语法
+        // 3. 悲观锁获取用户签到状态(并发安全)
         PointUserSignStatus status = pointUserSignStatusService.selectByOpenIdAndActivityIdForUpdate(openId, activityId);
-
-        // 检查今天是否已经签到
-        // 逻辑:如果是老用户(status不为空) 且 最后签到日期是今天
-        boolean isSignedToday = false;
-
-        if (ObjectUtil.isNotNull(status)) {
-            Date lastSignDate = status.getLastSignDate();
-            // 比较日期是否相同 (忽略时分秒)
-            if (DateUtil.isSameDay(lastSignDate, new Date())) {
-                isSignedToday = true;
-            }
-        }
-
-        // 如果今天已签,直接抛出异常
-        if (isSignedToday) {
-            // 这里可以定义一个自定义异常,或者使用通用的业务异常
-            throw new ServiceException("今日已签到,请勿重复操作");
-        }
-
-        int newContinuousDays = 1;
-        DateTime today = DateUtil.date();
-        DateTime yesterday = DateUtil.yesterday();
+        Date todayDate = DateUtil.date();
+        Date yesterdayDate = DateUtil.yesterday();
+        int newContinuousDays;
 
         if (ObjectUtil.isNull(status)) {
-            // --- 首次签到 ---
+            // 首次签到
+            newContinuousDays = 1;
             status = new PointUserSignStatus();
             status.setOpenId(openId);
             status.setActivityId(activityId);
-            status.setLastSignDate(today);
-            status.setCurrentContinuousDays(1);
+            status.setLastSignDate(todayDate);
+            status.setCurrentContinuousDays(newContinuousDays);
             status.setLastRewardCycleDays(0);
             pointUserSignStatusService.save(status);
         } else {
-            // --- 非首次签到:计算连续天数 ---
-            Date lastSignDate = status.getLastSignDate();
-
-            // 判断是否断签:昨天不是最后签到日期,即为断签
-            boolean isBreak = !DateUtil.isSameDay(lastSignDate, yesterday);
-
-            if (isBreak) {
-                if (task.getBreakRule() == 0) {
-                    // 规则0:断签重置
-                    newContinuousDays = 1;
-                } else {
-                    // 规则1:断签保留进度 (这里逻辑通常是 +1,除非你想做补签逻辑)
-                    newContinuousDays = status.getCurrentContinuousDays() + 1;
-                }
-            } else {
-                // 正常连续
+            // 计算连续天数(修复断签逻辑)
+            boolean isContinue = DateUtil.isSameDay(status.getLastSignDate(), yesterdayDate);
+            if (isContinue) {
                 newContinuousDays = status.getCurrentContinuousDays() + 1;
+            } else {
+                // 断签规则:0重置 1保留
+                newContinuousDays = task.getBreakRule() == 0 ? 1 : status.getCurrentContinuousDays();
             }
 
-            // 更新状态
-            status.setLastSignDate(today);
+            status.setLastSignDate(todayDate);
             status.setCurrentContinuousDays(newContinuousDays);
             pointUserSignStatusService.updateById(status);
         }
 
-        // 5. 计算奖励积分
-        // 优化:匹配 <= 当前天数的最大奖励配置 (例如配置了3天、7天,第5天应该拿3天的奖,或者拿基础奖)
-        int rewardPoints = task.getBasePoints(); // 默认基础积分
+        // 4. 计算奖励积分
+        int rewardPoints = task.getBasePoints();
         PointSignReward reward = pointSignRewardService.getOne(new LambdaQueryWrapper<PointSignReward>()
                 .eq(PointSignReward::getSignTaskId, taskId)
-                .le(PointSignReward::getContinueDays, newContinuousDays) // 小于等于当前天数
+                .le(PointSignReward::getContinueDays, newContinuousDays)
                 .orderByDesc(PointSignReward::getContinueDays)
                 .last("LIMIT 1"));
-
-        if (ObjectUtil.isNotNull(reward)) {
+        if (reward != null) {
             rewardPoints = reward.getRewardPoints();
         }
 
-        // 6. 写入签到流水日志
+        // 5. 写入签到流水
         PointUserSignLog signLog = new PointUserSignLog();
         signLog.setOpenId(openId);
         signLog.setActivityId(activityId);
         signLog.setTaskId(taskId);
-        signLog.setSignDate(today); // 需确保数据库字段支持 Date 类型
+        signLog.setSignDate(todayDate);
         signLog.setContinuousDays(newContinuousDays);
         signLog.setPoints(rewardPoints);
         signLog.setIsMakeUp(0);
         pointUserSignLogService.save(signLog);
 
-        // 7. 增加用户积分账户余额
-        // 注意:这里假设 pointAccountService 内部会写入 point_user_log (积分总流水表)
+        // ======================== 统一记录任务完成(必须放在这里,事务内) ========================
+        pointUserActivityTaskCompletionService.completeTask(openId, activityId, taskId, taskType);
+
+        // 6. 发放积分
         try {
-            pointAccountService.addPoints(openId, rewardPoints, activityName, null, activityId, taskId, PointActivityTypeEnum.SIGN_TASK.getCode());
+            pointAccountService.addPoints(openId, rewardPoints, activityName, null, activityId, taskId, taskType);
         } catch (Exception e) {
-            log.error("签到发放积分失败", e);
-            throw new RuntimeException("签到成功,但积分发放失败,请联系客服");
+            log.error("签到发放积分异常 openid:{}", openId, e);
+            throw new ServiceException("签到成功,但积分发放失败,请联系客服");
         }
-
     }
 
     /**
@@ -593,13 +543,13 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
 
     private void validateActivityDTO(PointActivityDTO dto) {
         if (ObjectUtil.isNull(dto)) {
-            throw new BaseException("活动信息不能为空");
+            throw new ServiceException("活动信息不能为空");
         }
 
         PointActivityDTO.ActivityTimeConfig timeConfig = dto.getTimeConfig();
         if (ObjectUtil.isNull(timeConfig) || (!timeConfig.getIsPermanent() &&
                 (ObjectUtil.isNull(timeConfig.getStartTime()) || ObjectUtil.isNull(timeConfig.getEndTime())))) {
-            throw new BaseException("活动时间配置不能为空");
+            throw new ServiceException("活动时间配置不能为空");
         }
     }
 
@@ -632,7 +582,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         entity.setCreateTime(new Date());
         int affectedRows = pointActivityMapper.insertPointActivity(entity);
         if (affectedRows <= 0) {
-            throw new BaseException("保存积分活动失败");
+            throw new ServiceException("保存积分活动失败");
         }
     }
 
@@ -698,11 +648,11 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
             boolean isSuccess = pointActivityExpirePolicyService.updateById(entity);
             if (!isSuccess) {
                 log.error("保存或更新积分活动过期策略失败, activityType: {}", activityType);
-                throw new BaseException(UPDATE_FAILED_MESSAGE);
+                throw new ServiceException(UPDATE_FAILED_MESSAGE);
             }
         } catch (Exception e) {
             log.error("保存或更新积分活动过期策略时发生异常, activityType: {}", activityType, e);
-            throw new BaseException(UPDATE_FAILED_MESSAGE);
+            throw new ServiceException(UPDATE_FAILED_MESSAGE);
         }
     }
 
@@ -748,14 +698,14 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
 
         int affectedRows = pointActivityMapper.updatePointActivity(entity);
         if (affectedRows <= 0) {
-            throw new BaseException("更新积分活动失败");
+            throw new ServiceException("更新积分活动失败");
         }
     }
 
     private void saveSignTaskDetails(PointActivityDTO dto, Long activityId) {
         PointSignTaskDTO signTaskDTO = dto.getSignTaskDTO();
         if (ObjectUtil.isNull(signTaskDTO)) {
-            throw new BaseException("签到任务配置不能为空");
+            throw new ServiceException("签到任务配置不能为空");
         }
 
         PointSignTask pointSignTask = new PointSignTask();
@@ -764,7 +714,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         pointSignTask.setBreakRule(signTaskDTO.getBreakRule());
         int affectedRows = pointSignTaskService.insertPointSignTask(pointSignTask);
         if (affectedRows <= 0) {
-            throw new BaseException("保存积分签到任务规则失败");
+            throw new ServiceException("保存积分签到任务规则失败");
         }
 
         saveSignRewards(signTaskDTO.getSignRewardList(), pointSignTask.getId());
@@ -859,7 +809,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
     private void updateSignTaskDetails(PointActivityDTO dto, Long activityId) {
         PointSignTaskDTO signTaskDTO = dto.getSignTaskDTO();
         if (ObjectUtil.isNull(signTaskDTO)) {
-            throw new BaseException("签到任务配置不能为空");
+            throw new ServiceException("签到任务配置不能为空");
         }
 
         // 获取现有的签到任务
@@ -874,7 +824,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         // 更新签到任务
         int affectedRows = pointSignTaskService.updatePointSignTask(pointSignTask);
         if (affectedRows <= 0) {
-            throw new BaseException("更新积分签到任务规则失败");
+            throw new ServiceException("更新积分签到任务规则失败");
         }
 
         // 更新签到奖励配置
@@ -975,7 +925,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
             log.info("成功更新 {} 个活动的过期策略为 {}", activitiesToUpdate.size(), expirePolicyId);
         } catch (Exception e) {
             log.error("批量更新活动过期策略失败, expirePolicyId: {}", expirePolicyId, e);
-            throw new BaseException("批量更新活动过期策略失败");
+            throw new ServiceException("批量更新活动过期策略失败");
         }
     }
 
@@ -988,7 +938,7 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
                         .last("LIMIT 1")
         );
         if (ObjectUtil.isNull(activity)) {
-            throw new RuntimeException("当前城市未配置签到活动");
+            throw new ServiceException("当前城市未配置签到活动");
         }
 
         // 2. 根据活动绑定的 taskId 查询签到任务
@@ -1012,4 +962,12 @@ public class PointActivityServiceImpl extends ServiceImpl<PointActivityMapper, P
         return vo;
     }
 
+    private WxLoginUser getCurrentWxLoginUser() {
+        WxLoginUser loginUser = SecurityUtils.getWxLoginUser();
+        if (ObjectUtil.isNull(loginUser)) {
+            throw new ServiceException("用户未登录或登录已过期");
+        }
+        return loginUser;
+    }
+
 }

+ 64 - 2
nightFragrance-massage/src/main/java/com/ylx/point/service/impl/PointUserActivityTaskCompletionServiceImpl.java

@@ -1,15 +1,18 @@
 package com.ylx.point.service.impl;
 
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.common.utils.DateUtils;
 import com.ylx.point.domain.PointUserActivityTaskCompletion;
 import com.ylx.point.domain.vo.PointActivityOverviewVO;
 import com.ylx.point.mapper.PointUserActivityTaskCompletionMapper;
 import com.ylx.point.service.IPointUserActivityTaskCompletionService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.Collections;
+import java.time.LocalDate;
 import java.util.List;
 
 /**
@@ -38,4 +41,63 @@ public class PointUserActivityTaskCompletionServiceImpl extends ServiceImpl<Poin
     public List<PointUserActivityTaskCompletion> selectCompletionsByOpenIdAndTaskIds(String openId, List<Long> taskIds) {
         return pointUserActivityTaskCompletionMapper.selectCompletionsByOpenIdAndTaskIds(openId, taskIds);
     }
+
+    /**
+     * 判断用户今日是否已完成该任务(用于每日一次任务、每日签到)
+     */
+    @Override
+    public boolean isTodayCompleted(String openId, Long activityId, Long taskId, Integer taskType) {
+        return pointUserActivityTaskCompletionMapper.exists(Wrappers.lambdaQuery(PointUserActivityTaskCompletion.class)
+                .eq(PointUserActivityTaskCompletion::getOpenId, openId)
+                .eq(PointUserActivityTaskCompletion::getActivityId, activityId)
+                .eq(PointUserActivityTaskCompletion::getTaskId, taskId)
+                .eq(PointUserActivityTaskCompletion::getTaskType, taskType)
+                .eq(PointUserActivityTaskCompletion::getCompletedDate, LocalDate.now()) // 生成列,超快
+        );
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void completeTask(String openId, Long activityId, Long taskId, Integer taskType) {
+        // 1. 先查询是否存在记录
+        PointUserActivityTaskCompletion completion = pointUserActivityTaskCompletionMapper.selectOne(
+                Wrappers.lambdaQuery(PointUserActivityTaskCompletion.class)
+                        .eq(PointUserActivityTaskCompletion::getOpenId, openId)
+                        .eq(PointUserActivityTaskCompletion::getActivityId, activityId)
+                        .eq(PointUserActivityTaskCompletion::getTaskId, taskId)
+                        .eq(PointUserActivityTaskCompletion::getTaskType, taskType)
+        );
+
+        if (ObjectUtil.isNull(completion)) {
+            // 首次完成:插入
+            PointUserActivityTaskCompletion insert = new PointUserActivityTaskCompletion();
+            insert.setOpenId(openId);
+            insert.setActivityId(activityId);
+            insert.setTaskId(taskId);
+            insert.setTaskType(taskType);
+            insert.setCompletedCount(1);
+            insert.setLastCompletedTime(DateUtils.getNowDate());
+            pointUserActivityTaskCompletionMapper.insert(insert);
+        } else {
+            // 非首次:次数+1(原子更新)
+            pointUserActivityTaskCompletionMapper.update(null,
+                    Wrappers.lambdaUpdate(PointUserActivityTaskCompletion.class)
+                            .eq(PointUserActivityTaskCompletion::getId, completion.getId())
+                            .setSql("completed_count = completed_count + 1")
+                            .set(PointUserActivityTaskCompletion::getLastCompletedTime, DateUtils.getNowDate())
+            );
+        }
+    }
+
+    @Override
+    public int getUserTaskTotalCount(String openId, Long activityId, Long taskId, Integer taskType) {
+        PointUserActivityTaskCompletion completion = pointUserActivityTaskCompletionMapper.selectOne(
+                Wrappers.lambdaQuery(PointUserActivityTaskCompletion.class)
+                        .eq(PointUserActivityTaskCompletion::getOpenId, openId)
+                        .eq(PointUserActivityTaskCompletion::getActivityId, activityId)
+                        .eq(PointUserActivityTaskCompletion::getTaskId, taskId)
+                        .eq(PointUserActivityTaskCompletion::getTaskType, taskType)
+        );
+        return completion == null ? 0 : completion.getCompletedCount();
+    }
 }