first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
@@ -0,0 +1,29 @@
package com.ruoyi.task.MQReceiver;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.task.DTO.pWelfareMQData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigDecimal;
import static com.ruoyi.task.config.DirectExchangeConfig.*;
// 不用
@Slf4j
// @Component
public class DirectReceiver {
}
@@ -0,0 +1,48 @@
package com.ruoyi.task.config;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class DirectExchangeConfig {
@Autowired
private AmqpAdmin amqpAdmin;
public static final String PROMOTION_WELFARE_QUEUE = "pWelfareQueue";
public static final String PROMOTION_WELFARE_EXCHANGE = "pWelfareExchange";
public static final String PROMOTION_WELFARE_KEY1 = "pwKey1";
// public static void main(String[] args) {
// System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
// }
// @Bean
// public Queue directQueue() {
// return new Queue(DIRECT_QUEUE, true);
// }
//
// @Bean
// public DirectExchange directExchange() {
// return new DirectExchange(DIRECT_EXCHANGE, true, false);
// }
//
// @Bean
// public Binding bindingDirectExchange(Queue directQueue, DirectExchange directExchange) {
// return BindingBuilder.bind(directQueue).to(directExchange).with(DIRECT_ROUTING_KEY);
// }
// @Bean
// public Queue directQueue2() {
// return new Queue(DIRECT_QUEUE2, true);
// }
// @Bean
// public Binding bindingDirectExchange2(Queue directQueue2, DirectExchange directExchange) {
// return BindingBuilder.bind(directQueue2).to(directExchange).with(DIRECT_ROUTING_KEY);
// }
}
@@ -0,0 +1,8 @@
package com.ruoyi.task.contract.request;
import lombok.Data;
@Data
public class GetAwardRequest {
private Integer taskDoingid;
}
@@ -0,0 +1,8 @@
package com.ruoyi.task.contract.request;
import lombok.Data;
@Data
public class TaskTestRequest {
private String kind;
}
@@ -0,0 +1,38 @@
package com.ruoyi.task.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.domain.task.DTO.pWelfareMQData;
import com.ruoyi.domain.task.VO.TtTaskDoingVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.List;
import static com.ruoyi.domain.task.constant.mq.MQMoudle.PROMOTION_WELFARE_EXCHANGE;
import static com.ruoyi.domain.task.constant.mq.MQMoudle.PROMOTION_WELFARE_KEY1;
@Slf4j
@RestController
@RequestMapping("/api/mq")
public class MQController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/t1")
@Anonymous
public AjaxResult t1() {
log.debug("mq测试");
return AjaxResult.success("mq测试");
}
}
@@ -0,0 +1,73 @@
package com.ruoyi.task.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.domain.task.VO.TtTaskDoingVO;
import com.ruoyi.task.contract.request.GetAwardRequest;
import com.ruoyi.task.contract.request.TaskTestRequest;
import com.ruoyi.task.scheduled.TaskUpdate;
import com.ruoyi.task.service.TtTaskService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Api(tags = "任务模块")
@RestController
@RequestMapping("/api/ttTask")
public class TtTaskController extends BaseController {
@Autowired
private TtTaskService ttTaskService;
@Autowired
private TaskUpdate taskUpdate;
@ApiOperation("关于我的任务")
@PostMapping("/taskOfme")
@Anonymous
// public List<TtTaskDoing> taskOfme(@PathVariable("page") Integer page,@PathVariable("size") Integer size) {
public List<TtTaskDoingVO> taskOfme() {
Long userId = getUserId();
return ttTaskService.taskOfme(1, 5, Integer.valueOf(String.valueOf(userId))); //暂时不需要分页
// return ttTaskService.taskOfme(1,5,1);
}
// 领取奖励
@PostMapping("/getAward")
@Anonymous
public AjaxResult getAward(@RequestBody GetAwardRequest request) {
Long userId = getUserId();
return ttTaskService.getAward(Integer.valueOf(String.valueOf(userId)), request.getTaskDoingid());
// return ttTaskService.getAward(1,1);
}
// 接首次下载任务
@PostMapping("/firsDownLoadTask")
@Anonymous
public AjaxResult firsDownLoadTask() {
Long userId = getUserId();
return ttTaskService.firsDownLoadTask(Integer.valueOf(String.valueOf(userId)));
}
// 接首次下载任务
@PostMapping("/test")
@Anonymous
public AjaxResult firsDownLoadTask(@RequestBody TaskTestRequest request) {
String kind = request.getKind();
switch (kind) {
// case "rank":
// // 已废弃:综合流水排行榜奖励改由 RechargeRankingRewardTask 统一发放
// taskUpdate.blendErcashRankPrize();
case "zhubo":
taskUpdate.pWelfarePrize();
}
return AjaxResult.success("测试完成");
}
}
@@ -0,0 +1,13 @@
package com.ruoyi.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.task.TtTask;
import com.ruoyi.domain.task.TtTaskDoing;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface TtTaskDoingMapper extends BaseMapper<TtTaskDoing> {
TtTaskDoing isOwnUser(@Param("userId") Integer userId,@Param("tid") Integer tid);
}
@@ -0,0 +1,16 @@
package com.ruoyi.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.task.TtTask;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface TtTaskMapper extends BaseMapper<TtTask> {
TtTask byId(Integer id);
List<TtTask> listByState(@Param("state") Integer state);
}
@@ -0,0 +1,28 @@
package com.ruoyi.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.TtUserBlendErcashArchive;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface TtUserBlendErcashArchiveMapper extends BaseMapper<TtUserBlendErcashArchive> {
/**
* 获取待归档的 id 列表(按 id 排序,每批 batchSize 条)
*/
List<Long> selectArchiveIds(@Param("cutoffTime") String cutoffTime,
@Param("batchSize") int batchSize);
/**
* 将指定 id 列表的记录插入归档表
*/
int insertFromMain(@Param("ids") List<Long> ids);
/**
* 从主表删除已归档的记录
*/
int deleteFromMain(@Param("ids") List<Long> ids);
}
@@ -0,0 +1,42 @@
package com.ruoyi.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.TtUserSourceReport;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface TtUserSourceReportTaskMapper extends BaseMapper<TtUserSourceReport> {
/**
* 增量模式:统计昨日各 user+source 的 delta(仅扫昨日 1 天流水)
*/
List<Map<String, Object>> selectYesterdayDelta(
@Param("yesterdayStart") String yesterdayStart,
@Param("todayStart") String todayStart);
/**
* 全量模式:统计 report_date 之前所有流水的累计值(仅首次运行时使用)
*/
List<Map<String, Object>> selectFullCumulative(
@Param("todayStart") String todayStart);
/**
* 批量 UPSERT 报表记录(INSERT ... ON DUPLICATE KEY UPDATE)
*/
int batchUpsert(@Param("list") List<TtUserSourceReport> list,
@Param("reportDate") String reportDate);
/**
* 检查指定日期是否已有报表数据(幂等检查)
*/
int countByReportDate(@Param("reportDate") String reportDate);
/**
* 查询前一天的报表记录(用于增量递推)
*/
List<TtUserSourceReport> selectByReportDate(@Param("reportDate") String reportDate);
}
@@ -0,0 +1,101 @@
package com.ruoyi.task.scheduled;
import com.ruoyi.system.config.ArchiveDaysConfig;
import com.ruoyi.task.mapper.TtUserBlendErcashArchiveMapper;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 流水归档定时任务 —— 每天凌晨 5 点执行。
* <p>
* 注册到 RuoYi 系统监控-定时任务:
* - 任务名称:流水数据归档
* - 任务组名:ARCHIVE
* - 调用目标:flowArchiveTask.archiveOldData
* - cron:0 0 5 * * ?
* <p>
* 将 tt_user_blend_ercash 中 7 天前的数据迁移到 tt_user_blend_ercash_archive。
* 分批处理(每批 1000 条),先插后删,每批独立事务,确保数据安全。
*/
@Slf4j
@Component("flowArchiveTask")
public class FlowArchiveTask {
private static final int BATCH_SIZE = 1000;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Autowired
private TtUserBlendErcashArchiveMapper archiveMapper;
@Autowired
private PlatformTransactionManager transactionManager;
/** 每批归档的事务模板:把"插入归档表 + 删除主表"包成单事务,保证原子提交/回滚 */
private TransactionTemplate transactionTemplate;
@PostConstruct
private void initTransactionTemplate() {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
/**
* 归档 7 天前的流水数据
*/
public void archiveOldData() {
log.info("FlowArchiveTask start ----------");
try {
LocalDate cutoff = LocalDate.now().minusDays(ArchiveDaysConfig.getRetainDays());
String cutoffTime = cutoff.format(DATE_FMT) + " 00:00:00";
log.info("归档截止时间: {}", cutoffTime);
int totalArchived = 0;
int batchNum = 0;
while (true) {
batchNum++;
// Step 1: 获取待归档的 id 列表
List<Long> ids = archiveMapper.selectArchiveIds(cutoffTime, BATCH_SIZE);
if (ids == null || ids.isEmpty()) {
log.info("无更多待归档数据,退出循环");
break;
}
// Step 2+3: 插入归档表 + 从主表删除,放入同一事务原子执行。
// 要么一起提交、要么整批回滚,杜绝"已插未删"造成的两表并存与查询双算;
// 归档表 ON DUPLICATE KEY UPDATE 对重复主键 no-op,残留行不报错并照常从主表删除,实现自愈。
final List<Long> batchIds = ids;
int[] counts = transactionTemplate.execute(status -> {
int ins = archiveMapper.insertFromMain(batchIds);
int del = archiveMapper.deleteFromMain(batchIds);
return new int[]{ins, del};
});
int inserted = counts == null ? 0 : counts[0];
int deleted = counts == null ? 0 : counts[1];
totalArchived += deleted;
// 迁移条数以本批 id 数为准更直观;括号内 inserted 为归档表实际新增行数,
// 自愈重复行时因 ON DUPLICATE KEY UPDATE id=id 的 no-op 计为 0,属正常非失败。
log.info("第 {} 批:迁移 {} 条(归档表新增 {} 条),累计归档 {} 条",
batchNum, batchIds.size(), inserted, totalArchived);
// 如果本批不足 BATCH_SIZE,说明已处理完
if (ids.size() < BATCH_SIZE) {
break;
}
}
log.info("FlowArchiveTask done: totalArchived={} ----------", totalArchived);
} catch (Exception e) {
log.error("FlowArchiveTask error: {}", e.getMessage(), e);
}
}
}
@@ -0,0 +1,395 @@
package com.ruoyi.task.scheduled;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtPromotionUpdateMapper;
import com.ruoyi.admin.mapper.TtUserAmountRecordsMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserCreditsRecordsMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtRechargeRankingRewardService;
import com.ruoyi.admin.service.TtUserAmountRecordsService;
import com.ruoyi.admin.service.TtUserCreditsRecordsService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.service.TtVipLevelService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.common.constant.sys.MoneyType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtRechargeRankingReward;
import com.ruoyi.domain.other.TtVipLevel;
import com.ruoyi.domain.vo.TeamDetailVO;
import com.ruoyi.domain.vo.UserBERankVO;
import com.ruoyi.task.service.TtTaskDoingService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtAccountRecordSource.P_WELFARE;
@Slf4j
@Configuration // 1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class TaskUpdate {
@Autowired
private TtUserService userService;
@Autowired
private TtTaskDoingService ttTaskDoingService;
@Autowired
private TtUserService ttUserService;
@Autowired
private TtUserCreditsRecordsService ttUserCreditsRecordsService;
@Autowired
private TtUserCreditsRecordsMapper ttUserCreditsRecordsMapper;
@Autowired
private TtUserAmountRecordsService ttUserAmountRecordsService;
@Autowired
private TtUserAmountRecordsMapper ttUserAmountRecordsMapper;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private TtUserMapper userMapper;
@Autowired
private TtPromotionUpdateMapper ttPromotionUpdateMapper;
@Autowired
private TtVipLevelService vipLevelService;
@Autowired
private TtRechargeRankingRewardService ttRechargeRankingRewardService;
// 刷新每日任务
//@Scheduled(cron = "0 0 0 * * ?")
// private void refreshDayTask() {
//
// log.info("刷新{}任务", "每日流水奖励");
//
// Timestamp now = new Timestamp(System.currentTimeMillis());
//
// // {每日流水奖励}任务数据重置
// LambdaUpdateWrapper<TtTaskDoing> ttTaskDoingUpdate = new LambdaUpdateWrapper<>();
// ttTaskDoingUpdate
// .eq(TtTaskDoing::getTaskId, 2)
// .set(TtTaskDoing::getBeginTime, now)
// .set(TtTaskDoing::getCompeteTime, null)
// .set(TtTaskDoing::getCompletionState, TaskCompletionState.DOING.getCode())
// .set(TtTaskDoing::getProgress, 0);
// ttTaskDoingService.update(ttTaskDoingUpdate);
//
// }
// 发放综合流水排行榜前十奖励
// 已废弃:该功能由 RechargeRankingRewardTask.distributeRechargeRankingReward 替代,
// 并通过 ruoyi 定时任务后台统一管理,避免与 source=29 的消费排行榜奖励重复发放。
// @Scheduled(cron = "2 7 0 * * ?", zone = "Asia/Shanghai")
public void blendErcashRankPrize() {
log.info("发放综合流水排行榜前十奖励。");
// 正式时间区间-------------------------------------
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH, -1);
Timestamp begin = new Timestamp(c.getTimeInMillis());
// 测试时间区间---------------------------------
// Calendar c = Calendar.getInstance();
// Timestamp end = new Timestamp(c.getTimeInMillis());
// c.set(Calendar.HOUR_OF_DAY, 0);
// c.set(Calendar.MINUTE, 0);
// c.set(Calendar.SECOND, 0);
// c.set(Calendar.MILLISECOND, 0);
// c.add(Calendar.MINUTE, -2);
// Timestamp begin = new Timestamp(c.getTimeInMillis());
//-------------------------------------------------
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String beginT = dateFormat.format(begin);
String endT = dateFormat.format(end);
if (StringUtils.isBlank(beginT) || StringUtils.isBlank(beginT)) {
beginT = null;
endT = null;
}
var source = TtAccountRecordSource.getGameConsumeCodes();
List<UserBERankVO> rank = ttUserBlendErcashMapper.rank(
new ArrayList<>(source),
beginT,
endT,
0,
10);
var rankingRewards = ttRechargeRankingRewardService.selectTtRechargeRankingRewardList(null);
var rankRewardMap = rankingRewards.stream()
.collect(Collectors.toMap(TtRechargeRankingReward::getId, t -> t));
for (int i = 1; i <= rank.size(); i++) {
var vo = rank.get(i - 1);
vo.setBeRank(i);
try {
var reward = rankRewardMap.get(vo.getBeRank());
if (reward == null) {
log.warn("未找到排名奖励配置,排名:{}", vo.getBeRank());
continue;
}
userService.updateOnlyUserAccount(vo.getUserId(), reward.getReward(), TtAccountRecordSource.RANK_BLEND_ERCASH);
} catch (Exception e) {
log.error("发放综合流水排行榜奖励失败,用户:{},异常:{}", vo.getUserId(), e.getMessage());
}
}
log.info("发放综合流水排行榜前十奖励 完成");
}
// 发放主播推广福利奖励
// 已禁用:不再每天凌晨批量发放推广福利(充值返佣已改为实时到账)
// @Scheduled(cron = "2 4 0 * * ?", zone = "Asia/Shanghai") // 每天0点4分2秒执行
public void pWelfarePrize() {
Long start = System.currentTimeMillis();
log.info("发放推广福利奖励。");
// 正式时间段 ----------------------------------
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH, -1);
Timestamp begin = new Timestamp(c.getTimeInMillis());
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String beginT = dateFormat.format(begin);
String endT = dateFormat.format(end);
if (StringUtils.isBlank(beginT) || StringUtils.isBlank(endT)) {
log.warn("推广福利统计失败!");
return;
}
// 查询今天下级有消费的主播
List<Integer> bossIdList = ttUserAmountRecordsMapper.bossByHasConsumeEmployee(beginT, endT);
// 循环所有boss发放福利
for (Integer bossId : bossIdList) {
try {
boolean r = pWelfarePrizeToBoss(bossId, beginT, endT);
if (!r) {
log.error("推广福利统计失败,用户【{}】发奖失败!", bossId);
}
} catch (Exception e) {
log.error("推广福利统计失败,用户【{}】发奖异常!{}", bossId, e.getMessage());
}
}
Long stop = System.currentTimeMillis();
log.info("每日推广福利发放完成。用时:{}分钟", (stop - start) / 60000);
}
// VIP 流水返利已改为实时发放(见 TtUserServiceImpl.tryVipFlowRebate),本定时任务已禁用
// @Scheduled(cron = "2 1 0 * * ?", zone = "Asia/Shanghai") // 每天0点1分2秒执行
private void vipReward() {
Long start = System.currentTimeMillis();
log.info("发放充值vip流水福利奖励");
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime beginTime = yesterday.atStartOfDay();
LocalDateTime endTime = yesterday.atTime(23, 59, 59);
var records = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper).between(TtUserBlendErcash::getCreateTime, beginTime, endTime)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameConsumeCodes()).list();
if (CollectionUtils.isEmpty(records)) {
log.info("昨日无充值VIP流水记录,无需发放奖励");
return;
}
Map<Integer, List<TtUserBlendErcash>> userRecordsMap = records.stream()
.collect(Collectors.groupingBy(TtUserBlendErcash::getUserId));
// 3. 遍历用户进行发放操作
var ttVipLevelMap = vipLevelService.list().stream().collect(Collectors.toMap(TtVipLevel::getId, t -> t));
for (Map.Entry<Integer, List<TtUserBlendErcash>> entry : userRecordsMap.entrySet()) {
Integer userId = entry.getKey();
try {
List<TtUserBlendErcash> userRecords = entry.getValue();
// 计算该用户昨日VIP流水总额
BigDecimal totalAmount = userRecords.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
totalAmount = totalAmount.abs();
TtUser ttUser = userService.getById(userId);
if (ttUser.getVipLevel() > 0) {
TtVipLevel ttVipLevel = ttVipLevelMap.get(ttUser.getVipLevel());
if (ttVipLevel == null) {
log.warn("用户【{}】VIP等级不存在![{}]", ttUser.getUserId(), ttUser.getVipLevel());
continue;
}
// 优先使用 flowRebatePercent;若为 null 则回退到 commissions
BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null
? ttVipLevel.getFlowRebatePercent()
: (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO);
BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4,
RoundingMode.HALF_UP)
.multiply(totalAmount).setScale(2, RoundingMode.HALF_UP);
userService.updateUserAccount(userId, rebate, TtAccountRecordSource.VIP_CONSUME_AWARD);
}
log.debug("用户ID: {}, 昨日VIP流水总额: {}, 奖励发放处理中...", userId, totalAmount);
} catch (Exception e) {
log.error("发放VIP流水福利失败,用户ID: {},错误: {}", userId, e.getMessage());
}
}
Long stop = System.currentTimeMillis();
log.info("发放充值vip流水福利奖励完成。用时:{}分钟", (stop - start) / 60000);
}
// 根据时间区间内下级的消费发放推广福利
private boolean pWelfarePrizeToBoss(Integer bossId, String beginTimeStr, String endTimeStr) {
var user = userMapper.selectTtUserById(Long.valueOf(bossId));
if (user == null) {
log.warn("推广福利统计失败,用户【{}】不存在!", bossId);
return false;
}
if (!UserType.ANCHOR.getCode().equals(user.getUserType())) {
log.warn("推广福利统计失败,用户【{}】不是主播!", bossId);
return false;
}
// 解析时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date beginTime = null;
try {
beginTime = dateFormat.parse(beginTimeStr);
} catch (ParseException e) {
log.warn("日期解析异常,【{}】发奖失败。", bossId);
return false;
}
// 查询所有下级id
List<Integer> allEmployeesId = userMapper.allEmployeesByParents(Arrays.asList(bossId));
// 查询下级的最近一个绑定时间
List<TeamDetailVO> mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId);
if (mit.size() < allEmployeesId.size()) log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!");
// 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准
List<Integer> empIds1 = new ArrayList<>();
List<TeamDetailVO> empIds2 = new ArrayList<>();
for (TeamDetailVO item : mit) {
Timestamp latelyTime = item.getBeginTime();
if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) < 0) {
empIds1.add(item.getEmployeeId());
} else {
empIds2.add(item);
}
}
// 昨日没有换过绑定的玩家数据
List<TtUserBlendErcash> personBEList1 = new ArrayList<>();
if (!empIds1.isEmpty()) {
personBEList1 = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper)
.in(TtUserBlendErcash::getUserId, empIds1)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes())
.between(TtUserBlendErcash::getCreateTime, beginTimeStr, endTimeStr)
.list();
}
// 当日有 更换绑定的下级消费统计
List<TtUserBlendErcash> personBEList2 = new ArrayList<>();
if (!empIds2.isEmpty()) {
for (TeamDetailVO vo : empIds2) {
String beginT = dateFormat.format(vo.getBeginTime());
var l = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, vo.getEmployeeId())
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes())
.between(TtUserBlendErcash::getCreateTime, beginT, endTimeStr)
.list();
personBEList2.addAll(l);
}
}
personBEList1.addAll(personBEList2);
var totalRecharge = personBEList1.stream().filter(v -> {
return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode());
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
var allConsumeList = personBEList1.stream().filter(v -> {
return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode());
}).toList();
var totalNormalGameConsume = allConsumeList.stream().filter(v -> {
return TtAccountRecordSource.isNormalGameConsume(v.getSource());
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
var totalSpecialGameConsume = allConsumeList.stream().filter(v -> {
return TtAccountRecordSource.isSpecialGameConsume(v.getSource());
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
// 发奖
// 0.02*总充值 + 0.03*普通游戏消费 + 0.01*特殊游戏消费
BigDecimal prize = totalRecharge.multiply(new BigDecimal("0.02")).add(totalNormalGameConsume.multiply(new BigDecimal("0.03")))
.add(totalSpecialGameConsume.multiply(new BigDecimal("0.01")));
// 更新账户
userService.updateUserAccount(bossId, prize, P_WELFARE);
return true;
}
// 奖励
private void rankPrizeF(MoneyType prizeMoneyType,
LambdaUpdateWrapper<TtUser> wrapper,
Integer userId,
String value,
TtAccountRecordSource source) {
userService.updateUserAccount(userId, new BigDecimal(value), source);
}
}
@@ -0,0 +1,162 @@
package com.ruoyi.task.scheduled;
import com.ruoyi.domain.entity.TtUserSourceReport;
import com.ruoyi.task.mapper.TtUserSourceReportTaskMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 用户来源日报表定时任务 —— 每天凌晨 3 点执行。
* <p>
* 注册到 RuoYi 系统监控-定时任务:
* - 任务名称:用户来源日报表生成
* - 任务组名:REPORT
* - 调用目标:userSourceReportTask.generateDailyReport
* - cron:0 0 3 * * ?
* <p>
* 核心优化:增量递推
* - 日常运行:今日 total = 昨日报表 total + 今日 delta(只扫 1 天流水)
* - 首次运行:全量扫描 report_date 之前的全部流水
*/
@Slf4j
@Component("userSourceReportTask")
public class UserSourceReportTask {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Autowired
private TtUserSourceReportTaskMapper reportMapper;
/**
* 生成日报(增量/全量自动切换)
*/
public void generateDailyReport() {
log.info("UserSourceReportTask start ----------");
try {
LocalDate today = LocalDate.now();
LocalDate reportDate = today.minusDays(1); // 报表日期 = 昨天
LocalDate prevDate = reportDate.minusDays(1); // 前天
String reportDateStr = reportDate.format(DATE_FMT);
String prevDateStr = prevDate.format(DATE_FMT);
String yesterdayStart = reportDateStr + " 00:00:00";
String todayStart = today.format(DATE_FMT) + " 00:00:00";
// 幂等检查:如果 reportDate 已有数据则跳过
int existCount = reportMapper.countByReportDate(reportDateStr);
if (existCount > 0) {
log.info("reportDate={} 已有 {} 条记录,跳过(幂等)", reportDateStr, existCount);
return;
}
// 检查是否有前日报表(决定增量还是全量)
List<TtUserSourceReport> prevReports = reportMapper.selectByReportDate(prevDateStr);
boolean hasPrev = prevReports != null && !prevReports.isEmpty();
// 构建昨日报表 key -> 前日 total 的映射(增量递推用)
Map<String, BigDecimal> prevTotalMap = new HashMap<>();
if (hasPrev) {
for (TtUserSourceReport prev : prevReports) {
String key = prev.getUserId() + "_" + prev.getSource();
prevTotalMap.put(key, prev.getTotal() != null ? prev.getTotal() : BigDecimal.ZERO);
}
}
List<TtUserSourceReport> upsertList;
if (hasPrev) {
// ===== 增量模式:昨日 delta + 前日 total =====
log.info("增量模式:reportDate={}, 前日记录数={}", reportDateStr, prevReports.size());
List<Map<String, Object>> deltas = reportMapper.selectYesterdayDelta(yesterdayStart, todayStart);
log.info("昨日 delta 记录数={}", deltas.size());
upsertList = new ArrayList<>();
for (Map<String, Object> delta : deltas) {
Integer userId = ((Number) delta.get("userId")).intValue();
Integer source = ((Number) delta.get("source")).intValue();
BigDecimal deltaTotal = delta.get("delta") != null
? new BigDecimal(delta.get("delta").toString()) : BigDecimal.ZERO;
Integer parentId = delta.get("parentId") != null
? ((Number) delta.get("parentId")).intValue() : null;
String key = userId + "_" + source;
BigDecimal prevTotal = prevTotalMap.getOrDefault(key, BigDecimal.ZERO);
BigDecimal newTotal = prevTotal.add(deltaTotal);
upsertList.add(TtUserSourceReport.builder()
.userId(userId)
.parentId(parentId)
.source(source)
.total(newTotal)
.build());
}
// 处理前日有记录但昨日无 delta 的 user+source(累计值不变,仍需要写入今日报表行)
Set<String> deltaKeys = new HashSet<>();
for (Map<String, Object> delta : deltas) {
Integer uid = ((Number) delta.get("userId")).intValue();
Integer src = ((Number) delta.get("source")).intValue();
deltaKeys.add(uid + "_" + src);
}
for (TtUserSourceReport prev : prevReports) {
String key = prev.getUserId() + "_" + prev.getSource();
if (!deltaKeys.contains(key)) {
// 昨日无 delta,累计值 = 前日 total(原样递推)
upsertList.add(TtUserSourceReport.builder()
.userId(prev.getUserId())
.parentId(prev.getParentId())
.source(prev.getSource())
.total(prev.getTotal() != null ? prev.getTotal() : BigDecimal.ZERO)
.build());
}
}
} else {
// ===== 全量模式:首次运行,扫描全部历史 =====
log.info("全量模式:reportDate={}, 无前日数据,扫描全部历史", reportDateStr);
List<Map<String, Object>> fullData = reportMapper.selectFullCumulative(todayStart);
log.info("全量累计记录数={}", fullData.size());
upsertList = new ArrayList<>();
for (Map<String, Object> row : fullData) {
Integer userId = ((Number) row.get("userId")).intValue();
Integer source = ((Number) row.get("source")).intValue();
BigDecimal total = row.get("total") != null
? new BigDecimal(row.get("total").toString()) : BigDecimal.ZERO;
Integer parentId = row.get("parentId") != null
? ((Number) row.get("parentId")).intValue() : null;
upsertList.add(TtUserSourceReport.builder()
.userId(userId)
.parentId(parentId)
.source(source)
.total(total)
.build());
}
}
// 分批 UPSERT(每批 2000 条,避免单次 SQL 过大)
int batchSize = 2000;
int totalUpserted = 0;
for (int i = 0; i < upsertList.size(); i += batchSize) {
int end = Math.min(i + batchSize, upsertList.size());
List<TtUserSourceReport> batch = upsertList.subList(i, end);
reportMapper.batchUpsert(batch, reportDateStr);
totalUpserted += batch.size();
log.info("UPSERT 批次 {}/{}, 本批 {} 条", (i / batchSize + 1),
(upsertList.size() + batchSize - 1) / batchSize, batch.size());
}
log.info("UserSourceReportTask done: reportDate={}, totalUpserted={} ----------",
reportDateStr, totalUpserted);
} catch (Exception e) {
log.error("UserSourceReportTask error: {}", e.getMessage(), e);
}
}
}
@@ -0,0 +1,182 @@
package com.ruoyi.task.scheduled;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.service.TtVipLevelService;
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtVipLevel;
import com.ruoyi.message.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* VIP 流水返利每日定时任务 —— 每天凌晨执行,发放"昨日"游戏流水的 VIP 返利。
* <p>
* 由实时发放改造为定时发放:用户玩游戏时不再实时到账,改为每天统一按昨日游戏
* 流水总额 × 当前 VIP 等级 flowRebatePercent 计算并一次性发放。
* <p>
* 注册到 RuoYi 系统监控-定时任务:
* - 任务名称:VIP流水返利发放
* - 任务组名:VIP_REBATE
* - 调用目标:vipFlowRebateTask.grantDailyVipRebate()
* - cron:0 10 0 * * ? (每天凌晨 0 点 10 分)
*/
@Slf4j
@Component("vipFlowRebateTask")
public class VipFlowRebateTask {
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private TtUserMapper userMapper;
@Autowired
private TtUserService userService;
@Autowired
private TtVipLevelService vipLevelService;
@Autowired
private MessageService messageService;
/**
* 发放昨日游戏流水的 VIP 返利。
*/
public void grantDailyVipRebate() {
long start = System.currentTimeMillis();
log.info("[VIP流水返利] 每日定时发放任务开始 ----------");
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDateTime beginTime = yesterday.atStartOfDay();
LocalDateTime endTime = today.atStartOfDay().minusNanos(1);
// 幂等:今日若已发放过(存在今日创建的 VIP流水奖励 记录),直接跳过,避免重复发放
long grantedToday = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper)
.eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode())
.ge(TtUserBlendErcash::getCreateTime, today.atStartOfDay())
.count();
if (grantedToday > 0) {
log.info("[VIP流水返利] 今日已发放过 {} 条返利记录,跳过(幂等)", grantedToday);
return;
}
// 查询昨日全部游戏消费流水
List<TtUserBlendErcash> records = new LambdaQueryChainWrapper<>(ttUserBlendErcashMapper)
.between(TtUserBlendErcash::getCreateTime, beginTime, endTime)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes())
.list();
if (CollectionUtils.isEmpty(records)) {
log.info("[VIP流水返利] 昨日无游戏流水记录,无需发放");
return;
}
// 按用户分组
Map<Integer, List<TtUserBlendErcash>> userRecordsMap = records.stream()
.collect(Collectors.groupingBy(TtUserBlendErcash::getUserId));
// 预加载 VIP 等级配置
Map<Integer, TtVipLevel> vipLevelMap = vipLevelService.list().stream()
.filter(v -> v != null && v.getId() != null)
.collect(Collectors.toMap(TtVipLevel::getId, v -> v, (a, b) -> a));
int successCount = 0;
BigDecimal totalRebate = BigDecimal.ZERO;
for (Map.Entry<Integer, List<TtUserBlendErcash>> entry : userRecordsMap.entrySet()) {
Integer userId = entry.getKey();
try {
TtUser user = userService.getById(userId);
if (user == null) continue;
// 白号不发放
if (Integer.valueOf(1).equals(user.getIsBlankAccount())) continue;
if (user.getVipLevel() == null || user.getVipLevel() <= 0) continue;
TtVipLevel vipLevel = vipLevelMap.get(user.getVipLevel());
if (vipLevel == null) {
log.warn("[VIP流水返利] 用户【{}】VIP等级不存在![{}]", userId, user.getVipLevel());
continue;
}
// 优先使用 flowRebatePercent;若为 null 则回退到 commissions
BigDecimal percent = vipLevel.getFlowRebatePercent() != null
? vipLevel.getFlowRebatePercent()
: (vipLevel.getCommissions() != null ? vipLevel.getCommissions() : BigDecimal.ZERO);
if (percent.compareTo(BigDecimal.ZERO) <= 0) continue;
// 昨日游戏流水总额(金额为负,取绝对值)
BigDecimal consumeAmount = entry.getValue().stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.abs();
if (consumeAmount.compareTo(BigDecimal.ZERO) <= 0) continue;
BigDecimal rebate = consumeAmount
.multiply(percent)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
if (rebate.compareTo(BigDecimal.ZERO) <= 0) continue;
// 直接加金币(不动积分,不走 updateUserAccount 避免递归触发佣金/返利)
int count = userMapper.addOnlyAccountAmount(Long.valueOf(userId), rebate);
if (count > 0) {
// 重新读取最新余额,写入流水快照
TtUser freshUser = userService.getById(userId);
BigDecimal actualBalance = (freshUser != null)
? freshUser.getAccountAmount()
: user.getAccountAmount().add(rebate);
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(userId)
.amount(rebate)
.finalAmount(actualBalance)
.total(rebate)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.VIP_CONSUME_AWARD.getCode())
.remark(TtAccountRecordSource.VIP_CONSUME_AWARD.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
ttUserBlendErcashMapper.insert(blendErcash);
successCount++;
totalRebate = totalRebate.add(rebate);
// 发送邮件通知
int displayVipLevel = user.getVipLevel() != null ? user.getVipLevel() - 1 : 0;
String consumeStr = consumeAmount.stripTrailingZeros().toPlainString();
String rebateStr = rebate.stripTrailingZeros().toPlainString();
String content = "尊敬的 VIP" + displayVipLevel + " 用户,您昨日累计流水 ¥"
+ consumeStr + ",获得返利 " + rebateStr + ",已到账。";
messageService.sendNotice(
Long.valueOf(userId),
MessageCategoryEnum.SYS_VIP_REBATE.getTitle(),
content,
MessageCategoryEnum.SYS_VIP_REBATE.getCode());
}
} catch (Exception e) {
log.error("[VIP流水返利] 发放失败,userId={}", userId, e);
}
}
long stop = System.currentTimeMillis();
log.info("[VIP流水返利] 每日定时发放任务完成,发放用户数={},返利总额={},用时={}ms ----------",
successCount, totalRebate, (stop - start));
}
}
@@ -0,0 +1,11 @@
package com.ruoyi.task.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.domain.task.TtTask;
import com.ruoyi.domain.task.TtTaskDoing;
import java.util.List;
public interface TtTaskDoingService extends IService<TtTaskDoing> {
TtTaskDoing isOwnUser(Integer userId, Integer tid);
}
@@ -0,0 +1,17 @@
package com.ruoyi.task.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.domain.task.TtTask;
import com.ruoyi.domain.task.TtTaskDoing;
import com.ruoyi.domain.task.VO.TtTaskDoingVO;
import java.util.List;
public interface TtTaskService extends IService<TtTask> {
List<TtTaskDoingVO> taskOfme(Integer page, Integer size, Integer userId);
AjaxResult getAward(Integer userId, Integer taskDoingid);
AjaxResult firsDownLoadTask(Integer userId);
}
@@ -0,0 +1,21 @@
package com.ruoyi.task.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.task.TtTaskDoing;
import com.ruoyi.task.mapper.TtTaskDoingMapper;
import com.ruoyi.task.service.TtTaskDoingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TtTaskDoingServiceImpl extends ServiceImpl<TtTaskDoingMapper, TtTaskDoing> implements TtTaskDoingService {
@Autowired
private TtUserService userService;
@Override
public TtTaskDoing isOwnUser(Integer userId, Integer tid) {
return baseMapper.isOwnUser(userId,tid);
}
}
@@ -0,0 +1,498 @@
package com.ruoyi.task.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.admin.service.TtUserCreditsRecordsService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.task.TtTask;
import com.ruoyi.domain.task.TtTaskDoing;
import com.ruoyi.domain.task.VO.TtTaskDoingVO;
import com.ruoyi.domain.task.constant.*;
import com.ruoyi.task.mapper.TtTaskMapper;
import com.ruoyi.task.service.TtTaskDoingService;
import com.ruoyi.task.service.TtTaskService;
import com.ruoyi.task.utils.TasKUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class TtTaskServiceImpl extends ServiceImpl<TtTaskMapper, TtTask> implements TtTaskService {
@Autowired
private TtUserService userService;
@Autowired
private TtTaskDoingService ttTaskDoingService;
@Autowired
private TtUserService ttUserService;
@Autowired
private TtUserCreditsRecordsService ttUserCreditsRecordsService;
@Autowired
private TasKUtil tasKUtil;
@Override
public List<TtTaskDoingVO> taskOfme(Integer page, Integer size, Integer userId) {
// 用户信息
TtUser userInfo = userService.getById(userId);
// 尝试新接任务
AjaxResult r1 = tryCreateNewTaskDoing(userInfo);
// 维护任务
AjaxResult r2 = maintainTask(userInfo);
// 查询结果
LambdaQueryWrapper<TtTaskDoing> ttTaskDoingQuery = new LambdaQueryWrapper<>();
ttTaskDoingQuery
.eq(TtTaskDoing::getUserId,userInfo.getUserId())
.eq(TtTaskDoing::getCompletionState,TaskState.UP.getCode());
List<TtTaskDoing> ownTask = ttTaskDoingService.list(ttTaskDoingQuery);
return ownTask.stream().map((item)->{
TtTask task = this.getById(item.getTaskId());
TtTaskDoingVO vo = new TtTaskDoingVO();
BeanUtil.copyProperties(task,vo);
vo.setTaskId(task.getId());
vo.setTaskDoingId(item.getId());
vo.setUserId(item.getUserId());
vo.setBeginTime(item.getBeginTime());
vo.setCompeteTime(item.getCompeteTime());
vo.setCompletionState(item.getCompletionState());
vo.setProgress(item.getProgress());
return vo;
}).collect(Collectors.toList());
}
private AjaxResult maintainTask(TtUser userInfo) {
// 已接任务
LambdaQueryWrapper<TtTaskDoing> ttTaskDoingQuery = new LambdaQueryWrapper<>();
ttTaskDoingQuery
.eq(TtTaskDoing::getUserId,userInfo.getUserId());
List<TtTaskDoing> ownTask = ttTaskDoingService.list(ttTaskDoingQuery);
ownTask.stream().forEach((item)->{
if (!tasKUtil.updateTask(item)){
log.warn("任务{}-类型{} 数据更新失败,检查任务配置信息!",item.getId(),item.getTaskId());
}
});
// LambdaQueryWrapper<TtTask> ttTaskQuery = new LambdaQueryWrapper<>();
// LambdaUpdateWrapper<TtTaskDoing> ttTaskDoingUpdate = new LambdaUpdateWrapper<>();
//
// // 维护普通任务{首次下载奖励}
// ttTaskQuery
// .eq(TtTask::getId,1);
// TtTask task1 = ttTaskService.getOne(ttTaskQuery);
//
// ttTaskDoingUpdate
// .eq(TtTaskDoing::getUserId,userInfo.getUserId())
// .eq(TtTaskDoing::getTaskId,1)
// .set(TtTaskDoing::getProgress,task1.getTargetValue())
// .set(TtTaskDoing::getCompletionState,TaskCompletionState.COMPLETION.getCode())
// .set(TtTaskDoing::getCompeteTime,new Timestamp(System.currentTimeMillis()));
// ttTaskDoingService.update(ttTaskDoingUpdate);
//
//
// // 维护每日任务{每日流水奖励}
// ttTaskQuery.clear();
// ttTaskQuery
// .eq(TtTask::getId,2);
// TtTask task2 = ttTaskService.getOne(ttTaskQuery);
//
// Calendar nowDay = Calendar.getInstance();
// nowDay.set(Calendar.HOUR_OF_DAY, 0);
// nowDay.set(Calendar.MINUTE, 0);
// nowDay.set(Calendar.SECOND, 0);
// nowDay.set(Calendar.MILLISECOND, 0);
// Timestamp toDay = new Timestamp(nowDay.getTimeInMillis());
//
// LambdaQueryWrapper<TtUserCreditsRecords> creditsRecordsQuery = new LambdaQueryWrapper<>();
// creditsRecordsQuery
// .eq(TtUserCreditsRecords::getUserId,userInfo.getUserId())
// .ge(TtUserCreditsRecords::getCreateTime,toDay);
// List<TtUserCreditsRecords> todayRecords = ttUserCreditsRecordsService.list(creditsRecordsQuery);
//
// // 累计今日流水
// BigDecimal credits = new BigDecimal("0");
// todayRecords.stream().forEach((item)->{
// if (item.getCredits().compareTo(BigDecimal.ZERO)>=0){
// credits.add(item.getCredits());
// }
// });
//
// ttTaskDoingUpdate.clear();
// ttTaskDoingUpdate
// .eq(TtTaskDoing::getUserId,userInfo.getUserId())
// .eq(TtTaskDoing::getTaskId,2)
// .set(TtTaskDoing::getProgress,credits.intValue())
// .set(credits.compareTo(new BigDecimal(task2.getTargetValue()))>=0,TtTaskDoing::getCompletionState,TaskCompletionState.COMPLETION.getCode())
// .set(credits.compareTo(new BigDecimal(task2.getTargetValue()))>=0,TtTaskDoing::getCompeteTime,new Timestamp(System.currentTimeMillis()));
// ttTaskDoingService.update(ttTaskDoingUpdate);
return AjaxResult.success();
}
private AjaxResult tryCreateNewTaskDoing(TtUser userInfo) {
// 已接任务
LambdaQueryWrapper<TtTaskDoing> ttTaskDoingQuery = new LambdaQueryWrapper<>();
ttTaskDoingQuery
.eq(TtTaskDoing::getUserId,userInfo.getUserId());
List<TtTaskDoing> ownTask = ttTaskDoingService.list(ttTaskDoingQuery);
// 所有上架的任务
List<TtTask> allTask = baseMapper.listByState(TaskState.UP.getCode());
// 筛选可接的新任务
List<TtTask> filterT = filterTask(allTask,ownTask);
// 自动接手新任务
ArrayList<TtTaskDoing> newTaskDoings = new ArrayList<>();
for (TtTask task:filterT){
TtTaskDoing newTaskDoing = TtTaskDoing.builder()
.taskId(task.getId())
.userId(userInfo.getUserId())
.completionState(TaskCompletionState.DOING.getCode())
.beginTime(new Timestamp(System.currentTimeMillis()))
.progress(0)
.build();
newTaskDoings.add(newTaskDoing);
}
// 保存新接的任务
for (TtTaskDoing taskDoing:newTaskDoings){
ttTaskDoingService.save(taskDoing);
}
return AjaxResult.success();
}
@Override
public AjaxResult getAward(Integer userId, Integer taskDoingid) {
// 任务是否存在
TtTaskDoing taskDoing = ttTaskDoingService.isOwnUser(userId,taskDoingid);
if (ObjectUtil.isEmpty(taskDoing)){
return AjaxResult.error("任务不存在");
}
// 是否可领取
AjaxResult r = taskIsComplete(taskDoing);
if (!r.isSuccess()){
return r;
}
// 领奖
getPrize(taskDoing);
return AjaxResult.success("操作成功");
}
@Override
public AjaxResult firsDownLoadTask(Integer userId) {
// 是否已完成
LambdaQueryWrapper<TtTaskDoing> ttTaskDoingQuery = new LambdaQueryWrapper<>();
ttTaskDoingQuery
.eq(TtTaskDoing::getUserId,userId)
.eq(TtTaskDoing::getTaskId,1);
TtTaskDoing one = ttTaskDoingService.getOne(ttTaskDoingQuery);
if (ObjectUtil.isNotEmpty(one)){
return AjaxResult.success("首次下载任务已经完成,不能重复执行。");
}
// 接手任务并直接完成
TtTask task = baseMapper.byId(1);
TtTaskDoing taskDoing = new TtTaskDoing();
BeanUtil.copyProperties(task,taskDoing);
taskDoing.setUserId(userId);
taskDoing.setTaskId(task.getId());
taskDoing.setProgress(1);
taskDoing.setCompletionState(TaskCompletionState.COMPLETION.getCode());
taskDoing.setCompeteTime(new Timestamp(System.currentTimeMillis()));
ttTaskDoingService.save(taskDoing);
return AjaxResult.success("任务完成。");
}
private AjaxResult getPrize(TtTaskDoing taskDoing) {
TtTask task = baseMapper.byId(taskDoing.getTaskId());
if (task.getAwardType().equals(TaskAwardType.CREDITS.getCode())){
BigDecimal prize = BigDecimal.ZERO;
if (ObjectUtil.isEmpty(task.getAwardValue())){
// 暂时没有动态奖励的任务
// 计算奖金
// prize = computerPrize(task, taskDoing.getUserId());
}
prize = new BigDecimal(task.getAwardValue());
TtUser user = ttUserService.getById(taskDoing.getUserId());
BigDecimal add = user.getAccountCredits().add(prize);
user.setAccountCredits(add);
LambdaUpdateWrapper<TtUser> ttUserUpdate = new LambdaUpdateWrapper<>();
ttUserUpdate
.eq(TtUser::getUserId,user.getUserId())
.set(TtUser::getAccountCredits,add);
ttUserService.update(ttUserUpdate);
userService.insertUserCreditsRecords(user.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.TASK, prize, add,taskDoing.getTaskId());
taskDoing.setCompletionState(TaskCompletionState.COMPLETION_PRIZE.getCode());
ttTaskDoingService.updateById(taskDoing);
return AjaxResult.success("领取奖励:积分"+add.toString());
}else if (task.getAwardType().equals(TaskAwardType.OTHER.getCode())){
System.out.println("其他奖励类型");
return AjaxResult.error("其他奖励类型开发中。");
}else {
System.out.println("非法的奖励类型");
return AjaxResult.error("非法的奖励类型。");
}
}
private BigDecimal computerPrize(TtTask task,Integer userId){
// 流水奖励动态计算奖金
if (task.getId().equals(2)){
Calendar nowDay = Calendar.getInstance();
nowDay.set(Calendar.HOUR_OF_DAY, 0);
nowDay.set(Calendar.MINUTE, 0);
nowDay.set(Calendar.SECOND, 0);
nowDay.set(Calendar.MILLISECOND, 0);
Timestamp toDay = new Timestamp(nowDay.getTimeInMillis());
nowDay.add(Calendar.DAY_OF_MONTH,-1);
Timestamp yesterday = new Timestamp(nowDay.getTimeInMillis());
LambdaQueryWrapper<TtUserCreditsRecords> creditsRecordsQuery = new LambdaQueryWrapper<>();
creditsRecordsQuery
.eq(TtUserCreditsRecords::getUserId,userId)
.ge(TtUserCreditsRecords::getCreateTime,yesterday)
.le(TtUserCreditsRecords::getCreateTime,toDay);
List<TtUserCreditsRecords> list = ttUserCreditsRecordsService.list(creditsRecordsQuery);
BigDecimal prize = new BigDecimal("0");
list.stream().forEach((item)->{
if (item.getCredits().compareTo(BigDecimal.ZERO)>=0){
prize.add(item.getCredits());
}
});
prize.multiply(new BigDecimal("0.01"));
return prize;
}else {
return BigDecimal.ZERO;
}
}
private AjaxResult taskIsComplete(TtTaskDoing taskDoing) {
TtTask task = baseMapper.byId(taskDoing.getTaskId());
if (task.getType().equals(TaskType.COMMON.getCode())){
if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION_PRIZE.getCode())){
return AjaxResult.error("不可重复领取奖励。");
}
if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION.getCode())){
return AjaxResult.success();
}
// if (taskDoing.getProgress()>=task.getTargetValue()){
// return AjaxResult.success();
// }
return AjaxResult.error("任务未完成,不可领奖。");
}else if (task.getType().equals(TaskType.DAY.getCode())){
if (taskDoing.getCompletionState().equals(TaskCompletionState.COMPLETION.getCode())){
return AjaxResult.success();
}
return AjaxResult.error("未完成/已领取");
}else if (task.getType().equals(TaskType.WEEK.getCode())){
System.out.println("WEEK");
return AjaxResult.error("非法taskType");
}else if (task.getType().equals(TaskType.MONTH.getCode())){
System.out.println("MONTH");
return AjaxResult.error("非法taskType");
}else {
System.out.println("非法任务类型");
return AjaxResult.error("非法taskType");
}
}
// 废弃方法
private List<TtTaskDoingVO> getDoingTask(List<TtTask> filterT,List<TtTaskDoing> ownTask, TtUser userInfo) {
ArrayList<TtTaskDoingVO> result = new ArrayList<>();
// 自动接手新任务
ArrayList<TtTaskDoing> newTaskDoings = new ArrayList<>();
for (TtTask task:filterT){
TtTaskDoing newTaskDoing = TtTaskDoing.builder()
.taskId(task.getId())
.userId(userInfo.getUserId())
.completionState(TaskCompletionState.DOING.getCode())
.beginTime(new Timestamp(System.currentTimeMillis()))
.progress(0)
.build();
newTaskDoings.add(newTaskDoing);
TtTaskDoingVO ttTaskDoingVO = new TtTaskDoingVO();
BeanUtil.copyProperties(task,ttTaskDoingVO);
ttTaskDoingVO.setCompletionState(newTaskDoing.getCompletionState());
ttTaskDoingVO.setProgress(newTaskDoing.getProgress());
result.add(ttTaskDoingVO);
}
// 保存新接的任务
for (TtTaskDoing taskDoing:newTaskDoings){
ttTaskDoingService.save(taskDoing);
}
List<TtTaskDoingVO> collect = ownTask.stream().map((item) -> {
TtTaskDoingVO vo = new TtTaskDoingVO();
TtTask byId = baseMapper.byId(item.getTaskId());
BeanUtil.copyProperties(byId, vo);
vo.setTaskId(byId.getId());
vo.setTaskDoingId(item.getId());
vo.setCompletionState(item.getCompletionState());
vo.setProgress(item.getProgress());
vo.setBeginTime(item.getBeginTime());
vo.setCompeteTime(item.getCompeteTime());
return vo;
}).collect(Collectors.toList());
// 维护旧任务
collect.stream().forEach((item)->{
if (item.getType().equals(TaskType.DAY.getCode())){
// 当天零点时间戳
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp currenDay = new Timestamp(c.getTimeInMillis());
// 任务开始时间
Timestamp beginTime = item.getBeginTime();
// 任务完成时间
Timestamp competeTime = item.getCompeteTime();
// 尝试重置当日任务
// 完成时间为空或小于当天零点 1刷新开始时间,2状态更新为完成
if (ObjectUtil.isEmpty(competeTime) || competeTime.compareTo(currenDay)<0){
UpdateWrapper<TtTaskDoing> updateWrapper = new UpdateWrapper<TtTaskDoing>();
updateWrapper
.eq("id",item.getTaskDoingId())
.set("begin_time",new Timestamp(System.currentTimeMillis()))
.set("completion_state",TaskCompletionState.COMPLETION.getCode());
ttTaskDoingService.update();
}
// 开始时间小于当天零点 并 未完成 刷新开始时间,状态更新为完成
// 开始时间大于当天零点 并 进行中 1状态更新为完成,2更新完成时间
// if (beginTime.compareTo(currenDay)>=0 && item.getCompletionState().equals(TaskCompletionState.DOING.getCode())){
// UpdateWrapper<TtTaskDoing> updateWrapper = new UpdateWrapper<TtTaskDoing>();
// updateWrapper
// .eq("id",item.getTaskDoingId())
// .set("completion_state",TaskCompletionState.COMPLETION.getCode())
// .set("compete_time",new Timestamp(System.currentTimeMillis()));
// ttTaskDoingService.update();
// }
// 开始时间大于当天零点 并 已完成 不处理
}else if (item.getType().equals(TaskType.WEEK.getCode())){
}else if (item.getType().equals(TaskType.MONTH.getCode())){
}else {
}
});
// 合并所有正在进行的任务
result.addAll(collect);
return result;
}
//筛选可接的新任务
private List<TtTask> filterTask(List<TtTask> allTask,List<TtTaskDoing> ownTask) {
ArrayList<TtTask> result = new ArrayList<>();
// 条件过滤
for (TtTask task:allTask){
String condition = task.getTaskCondition();
if (StringUtils.isBlank(condition)||condition.equals("{}")){
result.add(task);
continue;
}
JSONObject conditionObj = JSON.parseObject(condition);
if (conditionObj.get("key1").equals("key1")){
System.out.println("key1");
}else if (conditionObj.get("key2").equals("key2")){
System.out.println("key2");
}
}
// 筛选可接的新任务
ArrayList<TtTask> canDoingTasks = new ArrayList<>();
for (TtTask task:result){
TtTaskDoing flag = null;
for (TtTaskDoing taskDoing:ownTask){
if (taskDoing.getTaskId().equals(task.getId())){
flag = taskDoing;
break;
}
}
if (ObjectUtil.isEmpty(flag)){
canDoingTasks.add(task);
}
}
return canDoingTasks;
}
}
@@ -0,0 +1,100 @@
package com.ruoyi.task.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.admin.service.TtUserCreditsRecordsService;
import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords;
import com.ruoyi.domain.task.TtTask;
import com.ruoyi.domain.task.TtTaskDoing;
import com.ruoyi.domain.task.constant.TaskCompletionState;
import com.ruoyi.task.mapper.TtTaskMapper;
import com.ruoyi.task.service.TtTaskDoingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.List;
@Component
public class TasKUtil {
@Autowired
private TtTaskMapper ttTaskMapper;
@Autowired
private TtTaskDoingService ttTaskDoingService;
@Autowired
private TtUserCreditsRecordsService ttUserCreditsRecordsService;
// 任务更新
public boolean updateTask(TtTaskDoing ttTaskDoing) {
Integer taskId = ttTaskDoing.getTaskId();
LambdaQueryWrapper<TtTask> ttTaskQuery = new LambdaQueryWrapper<>();
LambdaUpdateWrapper<TtTaskDoing> ttTaskDoingUpdate = new LambdaUpdateWrapper<>();
if (taskId.equals(1)) {
// {首次下载奖励}
ttTaskQuery
.eq(TtTask::getId, 1);
TtTask task1 = ttTaskMapper.selectOne(ttTaskQuery);
ttTaskDoingUpdate
.eq(TtTaskDoing::getUserId, ttTaskDoing.getUserId())
.eq(TtTaskDoing::getTaskId, 1)
.set(TtTaskDoing::getProgress, task1.getTargetValue())
.set(TtTaskDoing::getCompletionState, TaskCompletionState.COMPLETION.getCode())
.set(TtTaskDoing::getCompeteTime, new Timestamp(System.currentTimeMillis()));
ttTaskDoingService.update(ttTaskDoingUpdate);
return true;
} else if (taskId.equals(2)) {
// {每日流水奖励}
ttTaskQuery.clear();
ttTaskQuery
.eq(TtTask::getId, 2);
TtTask task2 = ttTaskMapper.selectOne(ttTaskQuery);
Calendar nowDay = Calendar.getInstance();
nowDay.set(Calendar.HOUR_OF_DAY, 0);
nowDay.set(Calendar.MINUTE, 0);
nowDay.set(Calendar.SECOND, 0);
nowDay.set(Calendar.MILLISECOND, 0);
Timestamp toDay = new Timestamp(nowDay.getTimeInMillis());
LambdaQueryWrapper<TtUserCreditsRecords> creditsRecordsQuery = new LambdaQueryWrapper<>();
creditsRecordsQuery
.eq(TtUserCreditsRecords::getUserId, ttTaskDoing.getUserId())
.ge(TtUserCreditsRecords::getCreateTime, toDay);
List<TtUserCreditsRecords> todayRecords = ttUserCreditsRecordsService.list(creditsRecordsQuery);
// 累计今日流水
BigDecimal credits = new BigDecimal("0");
todayRecords.stream().forEach((item) -> {
if (item.getCredits().compareTo(BigDecimal.ZERO) >= 0) {
credits.add(item.getCredits());
}
});
ttTaskDoingUpdate.clear();
ttTaskDoingUpdate
.eq(TtTaskDoing::getUserId, ttTaskDoing.getUserId())
.eq(TtTaskDoing::getTaskId, 2)
.set(TtTaskDoing::getProgress, credits.intValue())
.set(credits.compareTo(new BigDecimal(task2.getTargetValue())) >= 0, TtTaskDoing::getCompletionState, TaskCompletionState.COMPLETION.getCode())
.set(credits.compareTo(new BigDecimal(task2.getTargetValue())) >= 0, TtTaskDoing::getCompeteTime, new Timestamp(System.currentTimeMillis()));
ttTaskDoingService.update(ttTaskDoingUpdate);
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,69 @@
package com.ruoyi.task.utils.task;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.task.DTO.pWelfareMQData;
import lombok.AllArgsConstructor;
import java.math.BigDecimal;
import java.sql.Timestamp;
//
@AllArgsConstructor
public class PromotionWelfareTask implements Runnable {
private TtUserService userService;
private TtUser user;
// 消费金额
private BigDecimal price;
@Override
public void run() {
// 统计推广福利
pWelfareMQData msgDate = pWelfareMQData.builder()
.userId(user.getUserId())
.account(price)
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
TtUser user = userService.getById(msgDate.getUserId());
TtUser parent = userService.getById(user.getParentId());
if (ObjectUtil.isNotEmpty(parent)){
BigDecimal credits = null;
if ("01".equals(parent.getUserType())) {
credits = msgDate.getAccount().multiply(new BigDecimal("0.045"));
} else {
credits = msgDate.getAccount().multiply(new BigDecimal("0.01"));
}
LambdaUpdateWrapper<TtUser> wrapper = new LambdaUpdateWrapper<>();
wrapper
.eq(TtUser::getUserId,parent.getUserId())
.set(TtUser::getAccountCredits,parent.getAccountCredits().add(credits));
// .set(TtUser::getAccountAmount,parent.getAccountAmount().add(credits));
userService.update(wrapper);
// userService.insertUserCreditsRecords(parent.getUserId(),
// TtAccountRecordType.INPUT,
// TtAccountRecordSource.P_WELFARE,
// credits,
// parent.getAccountCredits().add(credits),
// user.getUserId(),
// user.getNickName(),
// msgDate.getAccount());
userService.insertUserAmountRecords(parent.getUserId(),
TtAccountRecordType.INPUT,
TtAccountRecordSource.P_WELFARE,
credits,
parent.getAccountCredits().add(credits),
user.getUserId(),
user.getNickName(),
msgDate.getAccount());
}
}
}
@@ -0,0 +1,28 @@
<?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.ruoyi.task.mapper.TtTaskDoingMapper">
<!-- <select id="byId" resultType="com.ruoyi.domain.task.TtTask">-->
<!-- SELECT-->
<!-- id,-->
<!-- `describe` AS des,-->
<!-- `name` AS na,-->
<!-- state,type,target_type,target_value,award_type,award_value-->
<!-- FROM-->
<!-- tt_task-->
<!-- WHERE-->
<!-- id=#{id}-->
<!-- </select>-->
<select id="isOwnUser" resultType="com.ruoyi.domain.task.TtTaskDoing">
SELECT
*
FROM
tt_task_doing
WHERE
user_id = #{userId}
and
task_id = #{tid}
</select>
</mapper>
@@ -0,0 +1,37 @@
<?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.ruoyi.task.mapper.TtTaskMapper">
<select id="byId" resultType="com.ruoyi.domain.task.TtTask">
SELECT
id,
`task_describe`,
`name`,
state,type,target_type,target_value,award_type,award_value
FROM
tt_task
WHERE
id=#{id}
</select>
<select id="listByState" resultType="com.ruoyi.domain.task.TtTask">
SELECT
id,
`task_describe`,
`name`,
state,
type,
target_type,
target_value,
award_type,
award_value
FROM
tt_task
WHERE
state=#{state}
</select>
</mapper>
@@ -0,0 +1,43 @@
<?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.ruoyi.task.mapper.TtUserBlendErcashArchiveMapper">
<!-- 获取待归档的 id 列表。
排除 source=30(VIP等级达标赠送):该流水是 VIP 奖励的领取判重凭证,
归档后主表查不到会导致玩家重复领取,必须永久保留在主表。 -->
<select id="selectArchiveIds" resultType="java.lang.Long">
SELECT id FROM tt_user_blend_ercash
WHERE create_time &lt; #{cutoffTime}
AND (source IS NULL OR source != 30)
ORDER BY id
LIMIT #{batchSize}
</select>
<!-- 从主表批量插入到归档表。
🔥 新增:包含 anchor_id 和 business_id 快照字段
ON DUPLICATE KEY UPDATE id=自身:仅对重复主键做 no-op(自愈上一轮"已插未删"的残留),
而截断/空值等真实错误仍会抛出→整批事务回滚,避免 INSERT IGNORE 吞错导致"删了主表却没归档"的静默丢数据。 -->
<insert id="insertFromMain">
INSERT INTO tt_user_blend_ercash_archive
(id, user_id, anchor_id, business_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time, archive_time)
SELECT id, user_id, anchor_id, business_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time, NOW()
FROM tt_user_blend_ercash
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
ON DUPLICATE KEY UPDATE id = tt_user_blend_ercash_archive.id
</insert>
<!-- 从主表删除已归档记录 -->
<delete id="deleteFromMain">
DELETE FROM tt_user_blend_ercash
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,59 @@
<?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.ruoyi.task.mapper.TtUserSourceReportTaskMapper">
<!-- 增量模式:统计昨日各 user+source 的 delta(ABS(total) 之和) -->
<select id="selectYesterdayDelta" resultType="java.util.HashMap">
SELECT
e.user_id AS userId,
u.parent_id AS parentId,
e.source AS source,
SUM(ABS(e.total)) AS delta
FROM tt_user_blend_ercash e
INNER JOIN tt_user u ON e.user_id = u.user_id
WHERE e.create_time &gt;= #{yesterdayStart}
AND e.create_time &lt; #{todayStart}
GROUP BY e.user_id, e.source
</select>
<!-- 全量模式:统计 todayStart 之前所有流水的累计值 -->
<select id="selectFullCumulative" resultType="java.util.HashMap">
SELECT
e.user_id AS userId,
u.parent_id AS parentId,
e.source AS source,
SUM(ABS(e.total)) AS total
FROM tt_user_blend_ercash e
INNER JOIN tt_user u ON e.user_id = u.user_id
WHERE e.create_time &lt; #{todayStart}
GROUP BY e.user_id, e.source
</select>
<!-- 批量 UPSERT -->
<insert id="batchUpsert">
INSERT INTO tt_user_source_report (user_id, parent_id, source, total, report_date)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.parentId}, #{item.source}, #{item.total}, #{reportDate})
</foreach>
ON DUPLICATE KEY UPDATE
total = VALUES(total),
parent_id = VALUES(parent_id),
update_time = NOW()
</insert>
<!-- 幂等检查 -->
<select id="countByReportDate" resultType="int">
SELECT COUNT(*) FROM tt_user_source_report WHERE report_date = #{reportDate}
</select>
<!-- 查询指定日期报表(用于增量递推读取昨日数据) -->
<select id="selectByReportDate" resultType="com.ruoyi.domain.entity.TtUserSourceReport">
SELECT id, user_id, parent_id, source, total, report_date, create_time, update_time
FROM tt_user_source_report
WHERE report_date = #{reportDate}
</select>
</mapper>