
签到这种功能看着人畜无害真做起来一堆细节。日结零工系统里我要的是每天来打开 App的习惯光记个sign_date没意义得跟积分挂钩还得让用户有连续签到的动力。落地版本是配置驱动的连续签到基础积分 连续第 7 天翻倍奖励积分规则全放配置表运营改数字不用发版。今天把实现思路和几个坑一次讲透。数据设计一张表记住连续状态签到表不存今天第几天而是存连续签到天数查昨天的记录就能算出今天应该给多少publicclassUmsSign{privateStringid;privateStringuserId;privateStringsignDate;// yyyy-MM-ddprivateIntegerseriesDays;// 连续签到天数privateIntegerintegral;// 本次签到获得积分privateDatecreateTime;// getter/setter 略}积分规则不写死在代码里全部走配置表BaseConfig两个 keyJF_SIGN每日基础积分和JF_SIGN_SEVEN连续七日奖励积分。运营想调数值后台改配置就行。-- 签到记录表精简CREATETABLEums_sign(idVARCHAR(32)NOTNULL,user_idVARCHAR(32)NOTNULLCOMMENT用户ID,sign_dateDATENOTNULLCOMMENT签到日期,series_daysINTDEFAULT1COMMENT连续签到天数,integralINTDEFAULT0COMMENT本次获得积分,create_timeDATETIMEDEFAULTNULL,PRIMARYKEY(id),UNIQUEKEYuk_user_date(user_id,sign_date)COMMENT防重复签到)ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT签到记录表;那个唯一索引是兜底代码里查了今天是否已签到但并发下两个请求同时过检查就是双签到唯一键直接拦住。核心逻辑连续天数怎么算签到的核心就三步查今天是否已签 → 查昨天是否签过 → 按昨天的连续天数推今天的OverridepublicbooleanaddSign(StringuserId){StringdayDateUtils.getCurrentTime(yyyy-MM-dd);// 1. 今天是否已签到if(getSign(userId,day)!null){thrownewRuntimeException(今日已签到,无需重复);}UmsSignsignnewUmsSign();sign.setSignDate(day);sign.setCreateTime(newDate());sign.setUserId(userId);// 2. 基础积分配置BaseConfigconfigconfigService.getConfigByCode(BizConstants.JF_SIGN);intbaseIntegralInteger.parseInt(config.getConfigValue());sign.setIntegral(baseIntegral);// 3. 查昨天是否签过StringlastDayDateUtils.formatDate(DateUtil.offsetDay(newDate(),-1),yyyy-MM-dd);UmsSignlastSigngetSign(userId,lastDay);if(lastSign!null){if(lastSign.getSeriesDays()7){// 昨天已是第7天今天重新开始sign.setSeriesDays(1);}else{// 连续天数 1sign.setSeriesDays(lastSign.getSeriesDays()1);if(lastSign.getSeriesDays()6){// 第7天翻倍奖励BaseConfigmaxconfigService.getConfigByCode(BizConstants.JF_SIGN_SEVEN);sign.setIntegral(Integer.parseInt(max.getConfigValue()));}}}else{// 昨天没签今天从第1天算起sign.setSeriesDays(1);}// 4. 记积分流水if(lastSign!nulllastSign.getSeriesDays()6){integralLogService.addIntegralLog(userId,BizConstants.JF_SIGN_SEVEN,,每日签到(连续七日)获得积分);}else{integralLogService.addIntegralLog(userId,BizConstants.JF_SIGN,,每日签到获得积分);}returnthis.save(sign);}注意几个边界第 7 天翻倍奖励判断的是lastSign.getSeriesDays() 6也就是昨天连续到第 6 天今天签到就是第 7 天如果昨天已经是第 7 天 7今天重新从第 1 天开始。这里最容易写错的是把今天第几天跟昨天第几天搞混。前端要的今天能拿多少分签到页顶部要显示今天签到可得 X 积分这个接口把三档数值一次性给前端前端就不用写死规则了OverridepublicMapgetLeftReSignCount(StringuserId){MapmapnewHashMap();BaseConfigconfigconfigService.getConfigByCode(BizConstants.JF_SIGN);intbaseIntegralInteger.parseInt(config.getConfigValue());BaseConfigsevenconfigService.getConfigByCode(BizConstants.JF_SIGN_SEVEN);intsevenIntegralInteger.parseInt(seven.getConfigValue());map.put(baseIntegral,baseIntegral);map.put(sevenIntegral,sevenIntegral);StringlastDayDateUtils.formatDate(DateUtil.offsetDay(newDate(),-1),yyyy-MM-dd);UmsSignlastSigngetSign(userId,lastDay);inttodaySignbaseIntegral;if(lastSign!nulllastSign.getSeriesDays()6){todaySignsevenIntegral;// 明天就是第7天}map.put(todaySign,todaySign);returnmap;}这里有个和直觉相反的点判断明天签到能拿多少看的是昨天的记录。昨天连续到第 6 天今天签到拿翻倍昨天连续到第 7 天今天签了就是新一轮第 1 天拿基础分。前端如果不理解这个逻辑容易把连续 7 天显示成连续 8 天。踩坑并发双签 补签留的坑现象上线第一周后台查到同一天同一个用户两条签到记录积分还发了双份。排查过程翻日志发现两条记录 create_time 只差 300ms怀疑并发定位思路getSign检查今天是否已签是查完再插入两个请求都查到没签双双落库最终解决给(user_id, sign_date)加唯一索引兜底同时签到时包一层事务先 insert 后检查唯一键冲突冲突就回滚报今日已签到。唯一索引加完后并发场景直接抛DuplicateKeyException再也没出现过双签。顺带说个真实的坑代码里addReSign补签至今是空实现return false——需求砍了但接口留着。这种半成品接口如果前端调了会拿到失败但没有明确提示宁可没有也别留假接口要么实现要么直接删。积分流水签到是入口账要记清楚签到本身不是目的积分后面要能换东西所以每一笔积分都得有流水。签到积分走integral_log加积分和写流水在同一个事务里CREATETABLEintegral_log(idVARCHAR(32)NOTNULL,user_idVARCHAR(32)NOTNULLCOMMENT用户ID,change_typeVARCHAR(32)NOTNULLCOMMENTJF_SIGN/JF_SIGN_SEVEN/...,change_valINTNOTNULLCOMMENT变动值正负,balanceINTNOTNULLCOMMENT变动后余额快照,remarkVARCHAR(255)DEFAULTNULL,create_timeDATETIMEDEFAULTNULL,PRIMARYKEY(id),KEYidx_user_time(user_id,create_time))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT积分流水表;balance那个字段是余额快照对账神器。用户说我积分少了拉流水一算就知道差在哪一笔没有快照就只能猜。签到列表接口顺带分页后台看签到记录、前端看我的签到历史都用它OverridepublicIPagegetSignList(Pagepage,UmsSignparams){returnbaseMapper.getSignList(page,params);}这里也提醒一句签到查询接口里orderByDesc(create_time)和orderByDesc(sign_date)是两个排序口径列表页用创建时间、日历视图用签到日期别混。前端状态今天签了没、连续几天、还剩几次后端把数据给全了前端才能少写死逻辑。签到页需要三个状态今天是否已签、当前连续几天、明天签到能拿多少。后端getLastSign一次返回最近两天记录前端自己推连续天数getSignIntegral返回三档积分数值。前端拿到后渲染日历格子已签的格子打勾、未签的置灰、连续第 7 天的格子加个星星角标。有个交互细节值得说连续天数断了要不要提示我们的处理是今天没签、昨天签过签到页顶部弹一条昨天没签到连续天数已中断今天重新开始比用户自己发现少了几十积分体验好得多。这逻辑放前端判断昨天无记录 前天有记录就提示中断。签到还有个反作弊的点同一账号多端签到。后端按user_id sign_date唯一键兜底后同一用户手机、平板各签一次第二次直接抛今日已签到积分不会重复发。用户视角是多端同步体验没问题。可以直接抄走的清单连续天数存字段series_days随记录落库查昨天一条记录就能推今天。规则进配置表基础分、翻倍分走BaseConfig运营改配置不发版。唯一索引兜底(user_id, sign_date)唯一键防并发双签比代码检查可靠。第 7 天判定用昨天lastSign.seriesDays 6是翻倍条件别用今天的。积分必须留流水加积分的同时写integral_log对账、补发全靠它。项目源码https://gitee.com/gzqkl/xllg