连续周更任务模块的设计与实现

连续周更任务模块的设计与实现

1. 需求背景

某小说平台为了提升作者的创作积极性,计划推出连续周更任务模块,需求大致如下:

  1. 当作者连续更新1、2、3、...、8周时,可以分别领取相应档位的奖励;
  2. 作者在一周内更新字数达到2000才算作有效更新;即作者必须每周更新至少2000字,否则视为断更;
  3. 在作者断更或完成模块的所有任务后,下周一重置该模块,开启新一轮的连续周更任务;
  4. 任务个数、任务对应的奖励、单周内更新的最少字数均可配置。

2. 设计思路

和每周任务模块不同,连续周更任务模块的结束时间是不确定的,因为我们无法预知作者什么时候会断更。

而结束时间不确定,就导致我们不知道什么时候需要重置连续周更模块;这也是本功能的难点所在。

我最初的想法是采用定时任务,思路:

  1. 预留出本周结束前的一段时间(如前一个小时)作为维护期,在该时期执行定时任务;
  2. 定时任务负责统计各个作者本周的更新字数,如果发现作者断更,则将该作者本期的连续周更任务模块结束掉;
  3. 在查询作者当前进行中的连续周更任务模块时,如果发现没有,则创建;这样就实现了重置功能。

我实际采用的方案是观察者模式 + 动态续期,思路:

  1. 在创建本期的连续周更任务模块时,结束时间默认为下周一;
  2. 监听作者的更新事件:当作者更新时,计算作者本周的更新字数,如果达到要求,则给本期任务模块的结束时间延长7天;
  3. 这样一来,只要作者本周完成了有效更新,就能在下周继续查询到本期任务模块,否则会在下周开启新的一期连续周更任务。

3. 表结构设计

数据库表continuous_week_module主要包含的列:

  1. idINT类型,自增主键;
  2. author_idINT类型,作者id;
  3. begin_timeTIMESTAMP类型,本期任务的开始时间(某个周一的零点);
  4. end_timeTIMESTAMP类型,本期任务的结束时间;初始化为begin_time + 7d
  5. continuous_weeksINT类型,已连续更新的周数;
  6. receive_flagsINT类型,是一个Bitmap,用于存放每个任务奖励的领取状态;
  7. configTEXT类型,用于存放本期任务的配置信息(如每周更新字数、任务的个数、每个任务对应的奖励)。

下面是该表数据的一个简单例子:

id author_id begin_time end_time weeks flags config
1 1 2025-08-11 2025-08-18 0 0 {"wordCount": 2000, "missions": [...]}
2 2 2025-08-11 2025-08-25 1 1 {"wordCount": 2000, "missions": [...]}

4. 代码实现

java 复制代码
@Service
public class ContinuousWeekModuleServiceImpl implements ContinuousWeekModuleService {
    private static final Logger log = LogManager.getLogger(ContinuousWeekModuleServiceImpl.class);

    @Autowired
    private ContinuousWeekModuleDao continuousWeekModuleDao;

    /**
     * 查询某个作者当前正在进行中的连续周更任务模块
     *
     * @param authorId  作者id
     * @param now       当前时间
     */
    private ContinuousWeekModule getOngoing(Integer authorId, Timestamp now) {
        // SELECT * FROM continuous_week_module
        // WHERE author_id = #{authorId} AND begin_time <= #{now} AND end_time > #{now}
        return continuousWeekModuleDao.getOngoing(authorId, now);
    }

    /**
     * 查询某个作者当前正在进行中的连续周更任务模块;如果不存在,则创建
     * 本方法主要提供给业务层来调用,以便于向作者展示每个任务的完成情况
     *
     * @param authorId  作者id
     * @param now       当前时间
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContinuousWeekModule getOrCreate(Integer authorId, Timestamp now) {
        ContinuousWeekModule module = getOngoing(authorId, now);
        if (module != null) {
            return module;
        }

        // 获取连续周更任务模块的配置信息
        Config config = getConfig();

        // 获取到本周一和下周一;分别作为模块的开始时间和结束时间
        Timestamp thisMonday = DateUtil.getMonday(now);
        Timestamp nextMonday = DateUtil.addDays(thisMonday, 7);

        // 创建新一期的连续周更任务
        module = new ContinuousWeekModule();
        module.setAuthorId(authorId);
        module.setBeginTime(thisMonday);
        module.setEndTime(nextMonday);
        module.setContinuousWeeks(0);
        module.setReceiveFlags(0);
        module.setConfig(JsonUtil.toJson(config));
        int id = continuousWeekModuleDao.insert(module);

        // 注意,在创建本期任务之前,作者有可能已经在本周更新了足够的字数了
        // 因此这里手动触发一次字数检查,判断是否完成了本周任务;如果是,则更新数据,并重新查出最新数据
        if (finishNextMissionIfNecessary(authorId, now, module)) {
            module = continuousWeekModuleDao.getById(id);
        }
        return module;
    }

    /**
     * 监听作者的更新事件;当作者更新时,需要回调本方法
     *
     * @param authorId  作者id
     * @param now       更新时间
     * @return          是否恰好完成了本周的任务,即continuous_week_module记录有更新
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean onAuthorUpdateEvent(Integer authorId, Timestamp now) {

        // 查询正在进行中的连续周更任务模块;如果有,则触发字数检查,否则直接返回false
        ContinuousWeekModule module = getOngoing(authorId, now);
        if (module != null) {
            return finishNextMissionIfNecessary(authorId, now, module);
        }
        return false;
    }

    /**
     * 触发字数检查
     *
     * @param authorId  作者id
     * @param now       当前时间
     * @param module    当前进行中的连续周更模块
     * @return          连续周更模块是否发生了更新
     */
    private boolean finishNextMissionIfNecessary(Integer authorId, Timestamp now, ContinuousWeekModule module) {

        // 获取到本周一,并计算当前属于第几周(从0开始算)
        Timestamp thisMonday = DateUtil.getMonday(now);
        long curWeekNo = (thisMonday.getTime() - module.getBeginTime().getTime()) / DateUtil.WEEK_MILLIS;

        // 如果本周任务已经完成了,则忽略
        if (curWeekNo < module.getContinuousWeeks()) {
            return false;
        }

        // 这个分支理论上不会被执行,除非代码逻辑有问题
        if (curWeekNo > module.getContinuousWeeks()) {
            log.error("周数不连续!module={}, now={}, curWeekNo={}", module, now, curWeekNo);
            return false;
        }

        // 解析本模块的配置信息,并统计本周的更新字数
        Config config = JsonUtil.parse(module.getConfig(), Config.class);
        int totalWordCount = statisticWordCount(authorId, thisMonday, DateUtil.addDays(thisMonday, 7));
        
        // 如果字数未达到要求,则返回false
        if (totalWordCount < config.getWordCount()) {
            return false;
        }
        
        // 否则说明作者完成了有效更新,此时连续周更数需要加一,并且结束时间需要在原来的基础上延长7天
        // 注意,结束时间不能超过beginTime + 7d * weekCount;也就是说,如果当前已经是最后一个任务了,则不需要延期
        Timestamp newEndTime = DateUtil.addDays(module.getEndTime(), 7);
        Timestamp maxEndTime = DateUtil.addDays(module.getBeginTime(), config.getMissions().size() * 7);
        if (newEndTime.after(maxEndTime)) {
            newEndTime = maxEndTime;
        }
        
        // UPDATE continuous_week_module SET
        // end_time = #{newEndTime},
        // continuous_weeks = continuous_weeks + 1
        // WHERE id = #{id} AND continuous_weeks = #{oldContinuousWeeks}
        return continuousWeekModuleDao.finishNextMission(module.getId(), newEndTime, module.getContinuousWeeks());
    }

    /**
     * 领取任务奖励;需要在业务层上确保作者已完成该任务
     * 
     * @param authorId  作者id
     * @param id        连续周更任务模块id
     * @param missionId 任务id;从0开始算
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doReceive(Integer authorId, Integer id, Integer missionId) {
        int missionIdFlag = 1 << missionId;
        
        // UPDATE continuous_week_module SET
        // receive_flags = receive_flags | #{missionIdFlag}
        // WHERE id = #{id}
        boolean success = continuousWeekModuleDao.updateReceiveFlags(id, missionIdFlag);
        
        if (success) {
            // 执行奖励发放逻辑
        }
    }
}

业务层的代码相对比较简单,因此这里就直接省略了

相关推荐
舒一笑9 分钟前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员
IT果果日记10 分钟前
flink+dolphinscheduler+dinky打造自动化数仓平台
大数据·后端·flink
Java技术小馆22 分钟前
InheritableThreadLoca90%开发者踩过的坑
后端·面试·github
寒士obj31 分钟前
Spring容器Bean的创建流程
java·后端·spring
掉鱼的猫43 分钟前
Spring AOP 与 Solon AOP 有什么区别?
java·spring
不是光头 强1 小时前
axure chrome 浏览器插件的使用
java·chrome
笨蛋不要掉眼泪1 小时前
Spring Boot集成腾讯云人脸识别实现智能小区门禁系统
java·数据库·spring boot
桃源学社(接毕设)1 小时前
云计算下数据隐私保护系统的设计与实现(LW+源码+讲解+部署)
java·云计算·毕业设计·swing·隐私保护
数字人直播1 小时前
视频号数字人直播带货,青否数字人提供全套解决方案!
前端·javascript·后端
shark_chili2 小时前
提升Java开发效率的秘密武器:Jadx反编译工具详解
后端