连续周更任务模块的设计与实现
1. 需求背景
某小说平台为了提升作者的创作积极性,计划推出连续周更任务模块,需求大致如下:
- 当作者连续更新1、2、3、...、8周时,可以分别领取相应档位的奖励;
- 作者在一周内更新字数达到
2000
才算作有效更新;即作者必须每周更新至少2000
字,否则视为断更; - 在作者断更或完成模块的所有任务后,下周一重置该模块,开启新一轮的连续周更任务;
- 任务个数、任务对应的奖励、单周内更新的最少字数均可配置。
2. 设计思路
和每周任务模块不同,连续周更任务模块的结束时间是不确定的,因为我们无法预知作者什么时候会断更。
而结束时间不确定,就导致我们不知道什么时候需要重置连续周更模块;这也是本功能的难点所在。
我最初的想法是采用定时任务,思路:
- 预留出本周结束前的一段时间(如前一个小时)作为维护期,在该时期执行定时任务;
- 定时任务负责统计各个作者本周的更新字数,如果发现作者断更,则将该作者本期的连续周更任务模块结束掉;
- 在查询作者当前进行中的连续周更任务模块时,如果发现没有,则创建;这样就实现了重置功能。
我实际采用的方案是观察者模式 + 动态续期,思路:
- 在创建本期的连续周更任务模块时,结束时间默认为下周一;
- 监听作者的更新事件:当作者更新时,计算作者本周的更新字数,如果达到要求,则给本期任务模块的结束时间延长7天;
- 这样一来,只要作者本周完成了有效更新,就能在下周继续查询到本期任务模块,否则会在下周开启新的一期连续周更任务。
3. 表结构设计
数据库表continuous_week_module
主要包含的列:
id
:INT
类型,自增主键;author_id
:INT
类型,作者id;begin_time
:TIMESTAMP
类型,本期任务的开始时间(某个周一的零点);end_time
:TIMESTAMP
类型,本期任务的结束时间;初始化为begin_time + 7d
;continuous_weeks
:INT
类型,已连续更新的周数;receive_flags
:INT
类型,是一个Bitmap
,用于存放每个任务奖励的领取状态;config
:TEXT
类型,用于存放本期任务的配置信息(如每周更新字数、任务的个数、每个任务对应的奖励)。
下面是该表数据的一个简单例子:
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) {
// 执行奖励发放逻辑
}
}
}
业务层的代码相对比较简单,因此这里就直接省略了