这个每天积分的需求应该如何实现?

基本规则

  1. 每天第一次访问网站资源,积分+1
  2. 若有一天未访问网站资源,则积分-1

其它要求

  1. 积分会显示在用户头像上,因此像评论列表这种,一个页面可能出现上百个头像的位置也要获取对应用户的当前积分,也完全可能有重复的用户

我和后端讨论了几个方案:

方案1:

方案逻辑:
记录用户近几次访问的情况,当需要获取积分时根据这几次访问的时间临时计算分数。

方案问题:
因为只记录最近几次(假设3次)的访问情况,若出现用户先积了2分(11),然后隔一天访问一次(110101),按照最近3次有效访问计算,这个用户是1分,但实际应该是2分。若是增加记录的天数,那相应的也可能出现11010101010101010...更长的情况出现。逼到极限就是从功能上线的那一天开始计算,所以不太合适。

方案2:

  1. 新增当前分数参数
  2. 新增上一次分数计算时间参数
  3. 已有用户操作记录

方案逻辑:

  1. 当用户访问网站资源时,生成一条操作记录。若此记录为当天的第一个访问资源记录时,当前积分+1,并更新上一次分数计算时间
  2. 当用户积分被查看时(不论是用户自己还是其他用户),若上一次分数计算时间与昨晚23:59间隔小于一天,则直接返回当前分数
  3. 当用户积分被查看时(不论是用户自己还是其他用户),若上一次分数计算时间与昨晚23:59间隔大于一天,则将当前积分减去间隔的完整天数,并返回分数,并更新上一次分数计算时间

方案问题:
虽然积分逻辑没有问题,但不适用于显示在列表头像上,因为这样会导致有多少头像就进行多少次计算,重复的头像也会单独判断,导致性能下降。

方案3:

方案逻辑:

  1. 每天第一次访问资源,积分+1
  2. 每天晚上跑个脚本,为所有用户计算今天是否应该减分

方案问题:
开发认为这个方案非常丑陋,但在产品眼里“不就是每天跑个脚本”

其它说明

开发基本没有话语权,以需求为主导。
碰到性能与需求相冲突时,基本都是先不考虑性能
但有没有一个能让开发也能接受的方案?

阅读 3.1k
4 个回答

我认为你提出的方案 2 已经很接近最终的答案了,我在你的方案 2 上提出一些改进。

首先我把环境限定在数据库。

改进方案

  1. 每个用户有一张积分表,这张表每个用户只有一条记录,只更新。
  2. 表中有三个字段,用户积分用户注册日期更新日期

根据你的方案2,我默认你有办法判断用户是否是今天第一次访问了。

那么这张表为什么只需要三个字段呢?这是关键点:
如果用户需要加积分了,那么用户积分需要加一,用户注册日期增加一天,更新日期修改为今天

也不一定是用户注册日期,我只是举个例子,根据需求来确定,可以是用户第一次访问资源的日期

计算方式

使用下面的公式,含义是用户积分减去你落后的分数。

如果你每天都访问了,用户注册日期会增加访问的天数,应该都为 0 才对。那么分数就能对的上了。

使用 if(更新日期=今天, 0 , 1) 的原因是会多扣分,所以用更新日期判断一下,是否需要补这一分。

用户积分 - daydiff(当前时间, 用户注册时间) + if(更新日期=今天, 0 , 1)

举例子

在应用环境中,today 变量换为 curdate()

# 新建一张表
create table test
(
    reg_date    date,
    score       int,
    update_date date
);

# 2023-03-01 第一次访问,给一分
insert into test value (str_to_date('2023-03-01', '%Y-%m-%d'), 1, str_to_date('2023-03-01', '%Y-%m-%d'));

# 2023-03-01 查得分情况
set @today=str_to_date('2023-03-01', '%Y-%m-%d');
select score - TIMESTAMPDIFF(DAY, reg_date, @today) + IF(update_date = @today, 0, 1) from test;

# 2023-03-02 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-02', '%Y-%m-%d');

# 2023-03-03 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-03', '%Y-%m-%d');

# 2023-03-04 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-04', '%Y-%m-%d');

# 2023-03-05 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-05', '%Y-%m-%d');

# 2023-03-06 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-06', '%Y-%m-%d');

# 2023-03-07 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-07', '%Y-%m-%d');

# 2023-03-08 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-08', '%Y-%m-%d');

# 2023-03-09 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-09', '%Y-%m-%d');

# 2023-03-10 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-10', '%Y-%m-%d');

# 在 2023-03-11 看得分情况
set @today=str_to_date('2023-03-11', '%Y-%m-%d');
select score - TIMESTAMPDIFF(DAY, reg_date, @today) + IF(update_date = @today, 0, 1) from test;

# 2023-03-11 没访问,在 2023-03-12 看得分情况
set @today=str_to_date('2023-03-12', '%Y-%m-%d');
select score - TIMESTAMPDIFF(DAY, reg_date, @today) + IF(update_date = @today, 0, 1) from test;

# 2023-03-11,2023-03-12 没访问,在 2023-03-13 看得分情况
set @today=str_to_date('2023-03-13', '%Y-%m-%d');
select score - TIMESTAMPDIFF(DAY, reg_date, @today) + IF(update_date = @today, 0, 1) from test;

# 2023-03-13 访问一次
update test
set reg_date=DATE_ADD(reg_date, INTERVAL 1 DAY),
    score   =score + 1,
    update_date=str_to_date('2023-03-13', '%Y-%m-%d');

# 2023-03-13 看得分情况
set @today=str_to_date('2023-03-13', '%Y-%m-%d');
select score - TIMESTAMPDIFF(DAY, reg_date, @today) + IF(update_date = @today, 0, 1) from test;

性能

这种方案的性能是可以保障的,除非你的用户积分查询一次能够达到十几万,否则对于数据库来说都是非常轻松的。

并且只要你能够保证源头上限定每天更新一次正常,那么写入的效率也是非常高的,一条 sql 就能够同时更新积分和时间两个字段,根本不用查询。

最后

如果对这种方式有什么疑问或者补充需求,可以联系我。

更新

重新理解了一下你的需求,发现是按天处理,不需要考虑小时,这样简单很多。

有必要说明一下。
不要将计算视为洪水猛兽,这么点计算量完全没有任何影响,连耗时的波动都影响不了。
你的需求看下来只有两种方法,一种是事后处理,一种是实时计算,没有第二种方式。
事后处理完全接受不了,就要尽最大的可能减少实时计算的影响。

第一次访问积分+1,我理解是1天最多得1分。
一、总天数

1、用户注册日期与当前日期的前1天之间的天数差,即总天数

二、访问天数

1、用户在注册日期至当前日期的前1天之间总访问天数。
2、在**访问表**里,需要根据访问日期去重。

三、未访问天数

总天数减去访问天数即未访问天数。

四、计算得分=1访问天数+(-1未访问天数)+当有访问记录加1分(无访问记录不得分)
五、假设用户9时访问了。那在8时30分统计时,是否需要减分。

1、如果需要减分,上面计算天数时,直接计算注册日期与当前日期之间的天数差。
2、如果不需要减分,则今天的分数需要与截止前1天的分数分开计算。即查询1当用户当天是否有访问,有访问加1分,未访问不减分。

其它:
1、由于减分规则的存在,未访问人员的分数也需要扣减,所以必须定时(每天)或实时计算每位用户的分数。
2、如果只加不减,则可以在用户登录时,去累加一下用户的分数,这样方便很多。

已知你们知道如何计算加分,如何计算扣分,无非就是取舍计算分的性能问题,我提供一个解决方案:

方案逻辑:

  1. 当用户访问资源时,若是当天的第一条访问记录时,生成加分记录,计算出当前应有的分数然后再+1
  2. 当用户积分被查看时,返回上一次的分数与上一次加分的时间,由前端计算当前应该是多少分。

后端:

  1. 用户表新增当前的分数和计算时间
  2. 新增分数记录表(操作表)
  3. 分数计算方法

前端:

  1. 分数计算方法

后端只负责加分前端在显示分数前需要计算

在新增分数记录后,后端计算当前用户应有的分数:
加分前按照规则计算出用户的应有分数。
根据上一次加分时间与当前时间的间隔,得出因扣的分数。 上一次的分数 - 根据间隔应扣的分数 + 1 = 当前分数。 且后端更新分数计算时间

后端在用户信息中返回上次算分结果和时间,前端根据算分时间和分数计算出当前用户应有的分数(计算方式与后端一致)

优点:
后端无需频繁更新表数据, 表中数据只有在用户实际触发加分时再去操作,分数与时间可进用户缓存,性能更优。

缺点:
前后端必须同时对规则进行改写,在列表中前端计算量稍大(根据列表数量)


更新
如果完全不需要前端计算的话,直接让后端在返回列表之前将用户的分数计算好就完事了,前端不做计算也能实现。
后端记录分值的思路不变的情况下,将计算方法抽出来,在列表中根据已知的计算时间和计算分值再次套用这个计算方法即可, 这样前端就不用计算了,而且计算量也仅仅是需要展示的列表中二三十个数值罢了。这种计算量对于编程语言就是毛毛雨级别了

我觉得方案三是比较简单优雅的。每个用户只需要保存两个字段:

  • count (当前积分)
  • update_on (上次更新的 date)

用户访问资源的时候,判断当前日期是否大于 update_on, 是,则count+=1,否,count不变。

每天晚上24点,脚本判断用户的 update_on 是否等于当天的日期,是,则不变,否,count-=1,update_on变为当天的日期。


但是如果让我自己来实现的话,我会用时序数据库里记录用户每次访问资源的时间。因为对于一个生产环境的系统来说,这些数据本就是是必要记录的。

需要获取用户积分的时候再去计算,出现性能问题可以每日缓存。

推荐问题
宣传栏