1. Check in definition and function
Sign in , It refers to signing or writing a letter on a specified book “ reach ” word , Indicates that I have arrived . stay APP Use this feature in , It can increase user stickiness and activity .

One with check-in function APP, Supplementary signing function is often provided , Relevant rewards will be given for the number of consecutive days of check-in ; In order to further increase user stickiness , It also provides check-in task function , You can also get corresponding rewards after completing the task .

Function use case
This article takes you to implement a check-in function including the above use cases , After reading it, you will find , Sign in , It's not as complicated as you think !

2. Technology selection

redis Write query for master ,mysql Auxiliary query . Traditional check-in is mostly adopted directly mysql For storage DB, In the case of big data, the database is under great pressure . The query rate will also increase as the amount of data increases . Therefore, after the requirements were finalized, I consulted many check-in implementation methods , Discovery use redis There are great advantages in doing check-in meetings .

This function mainly uses redis bitmap [1], I will explain the implementation process in detail later .

3. Realization effect
Throw bricks here to attract jade , Show us app Check in effect

4 Function realization
The function is roughly divided into two modules

Check in process ( Sign in , Supplementary signature , continuity , Check in record )
Check in task ( Daily task , Fixed task )
The check-in flow chart is as follows: :

4.1.1 Table design
Because most functions use redis storage , Use to mysql It is mainly used to store users' total points and points records , It is convenient to query the check-in records and users' total points

CREATE TABLE t_user_integral (
id varchar(50) NOT NULL COMMENT ‘id’,
user_id int(11) NOT NULL COMMENT ‘ user id’,
integral int(16) DEFAULT ‘0’ COMMENT ‘ Current integral ’,
integral_total int(16) DEFAULT ‘0’ COMMENT ‘ Cumulative integral ’,
create_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘ Creation time ’,
update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘ Modification time ’,
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT=‘ User points summary ’

CREATE TABLE t_user_integral_log (
id varchar(50) NOT NULL COMMENT ‘id’,
user_id int(11) NOT NULL COMMENT ‘ user id’,
integral_type int(3) DEFAULT NULL COMMENT ‘ Integral type 1. Sign in 2. Continuous check-in 3. Welfare tasks 4. Daily task
5. Supplementary signature ’,
integral int(16) DEFAULT ‘0’ COMMENT ‘ integral ’,
bak varchar(100) DEFAULT NULL COMMENT ‘ Integral supplementary copy ’,
operation_time date DEFAULT NULL COMMENT ‘ Operation time ( Specific date of signing in and re signing )’,
create_time datetime DEFAULT NULL COMMENT ‘ Creation time ’,
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT=‘ User points flow table ’
4.1.2 redis key Design
// Personnel check-in bitmap key, A bitmap stores the check-in status of a user for one year , with userSign For identification , The last two parameters are the year of this year and the user's id
public final static String USER_SIGN_IN = “userSign:%d:%d”;
// Supplementary signature of personnel key, One Hash The list saves the user's supplementary signature status for one month , with userSign:retroactive For identification , The latter two parameters are the month of the current month and the user's id
public final static String USER_RETROACTIVE_SIGN_IN =
“userSign:retroactive:%d:%d”;
// Total days of personnel signing in key, with userSign:count For identification , The following parameters are user's id
public final static String USER_SIGN_IN_COUNT = “userSign:count:%d”;
4.1.3 Realize check-in
Interface restful Form of , Incoming user in header id

@ApiOperation(“ User sign in ”)
@PostMapping("/signIn")
@LoginValidate
public ResponseResult saveSignIn(@RequestHeader Integer userId) {
return userIntegralLogService.saveSignIn(userId);
}
sevice Implementation layer

public ResponseResult saveSignIn(Integer userId) {
// Here is our company's unified return class
ResponseResult responseResult = ResponseResult.newSingleData();
// use String.format Assemble the bitmap of a single user key
String signKey = String.format(RedisKeyConstant.USER_SIGN_IN,
LocalDate.now().getYear(), userId);
// The offset point of the bitmap is the date of the day , As today , The offset value is 1010
long monthAndDay =
Long.parseLong(LocalDate.now().format(DateTimeFormatter.ofPattern(“MMdd”)));
responseResult.setMessage(“ Signed in today ”);
responseResult.setCode((byte) -1);
// Check whether the user has checked in today , use getBit You can get the sign in status of the user on the specific date ( The bitmap has only two values ,1 perhaps 0, here 1 representative true)
if (!cacheClient.getBit(signKey, monthAndDay)) {
// Bitmap set Method returns the value of the bitmap before it has been changed , If you haven't checked in before, the default is 0, that is false
boolean oldResult = cacheClient.setbit(signKey, monthAndDay);
if (!oldResult) {
// Calculate the continuous check-in days of the user from this month to today , This method refers to the code block below to calculate the number of consecutive check-in days
int signContinuousCount = getContinuousSignCount(userId);
// For this method, please refer to the record of check-in integral type and continuous check-in integral code block below
doSaveUserIntegral(userId, signContinuousCount);
responseResult.setCode((byte) 0);
}
}
return responseResult;
}
Calculate continuous check-in days

/**

* @description: Get continuous check-in days
* @author: chenyunxuan
* @updateTime: 2020/8/25 4:43 afternoon
*/
private int getContinuousSignCount(Integer userId) {
int signCount = 0;
LocalDate date = LocalDate.now();
String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, date.getYear(),
userId);
// What is taken out here is the value of an offset value interval of the bitmap , The starting value of the interval is the first day of the month , The range value is the total number of days in the current month ( Reference command bitfield)
List list = cacheClient.getBit(signKey, date.getMonthValue() * 100 + 1,
date.getDayOfMonth());
if (list != null && list.size() > 0) {
// Maybe the user hasn't checked in this month , We need to judge , If it is empty, give a default value 0
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.getDayOfMonth(); i++) {
// If it is obtained by continuous check-in long The value is not equal to the original value after being shifted one bit to the right and one bit to the left , Consecutive days plus one
if (v >> 1 << 1 == v) return signCount;
signCount += 1;
v >>= 1;
}
}
return signCount;
}
Record the type of check-in points and continuous check-in points
public Boolean doSaveUserIntegral(int userId, int signContinuousCount) {
int count = 0;
// Superimposed check-in times
cacheClient.incrValue(String.format(RedisKeyConstant.USER_SIGN_IN_COUNT,
userId));
List userIntegralLogList = new LinkedList<>();
userIntegralLogList.add(UserIntegralLog.builder()
.createTime(LocalDateTime.now())
.operationTime(LocalDate.now())
.bak(BusinessConstant.Integral.NORMAL_SIGN_COPY)
.integral(BusinessConstant.Integral.SIGN_TYPE_NORMAL_INTEGRAL)
.integralType(BusinessConstant.Integral.SIGN_TYPE_NORMAL)
.userId(userId)
.build());
count += BusinessConstant.Integral.SIGN_TYPE_NORMAL_INTEGRAL;
// Continuous check-in processing , Get continuous check-in reward for cache configuration
// Because the number of days in each month is not fixed , Continuous check-in reward is used redis hash Written . So this place uses 32 Number of consecutive check-in days in place of one month , The specific configuration is shown in the figure below
if (signContinuousCount == LocalDate.now().lengthOfMonth()) {
signContinuousCount = 32;
}
Map<String, String> configurationHashMap =
cacheClient.hgetAll(“userSign:configuration”);
String configuration = configurationHashMap.get(signContinuousCount);
if (null != configuration) {
int giveIntegral = 0;
JSONObject item = JSONObject.parseObject(configuration);
giveIntegral = item.getInteger(“integral”);
if (giveIntegral != 0) {
if (signContinuousCount == 32) {
signContinuousCount = LocalDate.now().lengthOfMonth();
}
userIntegralLogList.add(UserIntegralLog.builder()
.createTime(LocalDateTime.now())
.bak(String.format(BusinessConstant.Integral.CONTINUOUS_SIGN_COPY,
signContinuousCount))
.integral(giveIntegral)
.integralType(BusinessConstant.Integral.SIGN_TYPE_CONTINUOUS)
.userId(userId)
.build());
count += giveIntegral;
}
}
// Change the total score and batch write the score record
return updateUserIntegralCount(userId, count) &&
userIntegralLogService.saveBatch(userIntegralLogList);
}
Score configuration and copywriting configuration obtained by continuous check-in

4.1.4 Realize supplementary signing
The supplementary sign in function is a supplementary sign in function , The main purpose is to facilitate users to achieve the corresponding continuous check-in conditions through the supplementary sign function when they forget to sign in , To get a reward .

Supplementary signature main method

//day Indicates the date on which the signature needs to be supplemented , Because the check-in cycle of our platform is one month, we only need to send the information of the day , enter 7 Number incoming 7
public ResponseResult saveSignInRetroactive(Integer userId, Integer day) {
Boolean result = Boolean.TRUE;
ResponseResult responseResult = ResponseResult.newSingleData();
responseResult.setMessage(“ There is no need to sign again today ”);
responseResult.setCode((byte) -1);
LocalDate timeNow = LocalDate.now();
// Check whether the supplementary signature reaches the upper limit String retroactiveKey =
String.format(RedisKeyConstant.USER_RETROACTIVE_SIGN_IN,
timeNow.getMonthValue(), userId); // from redis Retrieve the user's collection of supplementary signatures in the current month set. The limit of our platform is three supplementary signatures
Set<String> keys = cacheClient.hkeys(retroactiveKey); if
(CollUtil.isNotEmpty(keys) && keys.size() == 3) {
responseResult.setMessage(" The number of supplementary signatures this month has reached the upper limit "); result = Boolean.FALSE; }
// Check whether the additional signing points are sufficient , Here is a simple single table query , Used to query whether the points are enough for this consumption UserIntegral userIntegral =
userIntegralService.getOne(new
LambdaQueryWrapper<UserIntegral>().eq(UserIntegral::getUserId, userId));
// Here is just a simple one map Place the points consumed by three supplementary signings (key: frequency
value: Consumption integral ), You can also refer to the previous continuous check-in configuration redis In the cache, it is convenient for background management, and the system can be configured Integer reduceIntegral =
getReduceIntegral().get(keys.size() + 1); if (reduceIntegral >
userIntegral.getIntegral()) { responseResult.setMessage(" Your orange juice is low "); result =
Boolean.FALSE; } if (result) { LocalDate retroactiveDate =
LocalDate.of(timeNow.getYear(), timeNow.getMonthValue(), day); String signKey =
String.format(RedisKeyConstant.USER_SIGN_IN, timeNow.getYear(), userId); long
monthAndDay =
Long.parseLong(retroactiveDate.format(DateTimeFormatter.ofPattern("MMdd")));
// The backend detects whether the user has checked in today, and the supplementary signing date cannot be later than today's date if (!cacheClient.getBit(signKey, monthAndDay) &&
timeNow.getDayOfMonth() > day) { boolean oldResult =
cacheClient.setbit(signKey, monthAndDay); if (!oldResult) { // Supplementary signature record (: month )
Reset after the month , Expiration time is the difference between the calculated current time , The number of supplementary signatures is refreshed once a month cacheClient.hset(retroactiveKey,
retroactiveDate.getDayOfMonth() + "", "1",
(Math.max(retroactiveDate.lengthOfMonth() - timeNow.getDayOfMonth(), 1)) * 60 *
60 * 24); // Here is the reduction of the total score table . And record the integral record . Refer to the code block below doRemoveUserIntegral(userId,
reduceIntegral, RETROACTIVE_SIGN_COPY); responseResult.setCode((byte) 0);
responseResult.setMessage(" Supplementary signing succeeded "); } } } return responseResult;
}
Reduce the points and write the change record of points

public Boolean doRemoveUserIntegral(int userId, int reduceIntegral, String
bak) {
return updateUserIntegralCount(userId, -reduceIntegral)
&& userIntegralLogService.save(UserIntegralLog.builder()
.createTime(LocalDateTime.now())
.operationTime(LocalDate.now())
.bak(bak)
.integral(-reduceIntegral)
.integralType(BusinessConstant.Integral.RETROACTIVE_SIGN_COPY.equals(bak) ?
BusinessConstant.Integral.SIGN_TYPE_RETROACTIVE :
BusinessConstant.Integral.SIGN_TYPE_WELFARE)
.userId(userId)
.build());
}
thus , The check-in and supplementary signing functions in the use case are completed , Next, let's look at the check-in calendar and how to implement the check-in task .

5 Check in calendar period
Check in period :
The commonly used check-in cycle is one week or one month . ours app A one month plan is adopted ( The check-in calendar interface on the market is similar , Next, I will share with you the implementation scheme of monthly check-in calendar and the associated check-in task )

6 Display effect and interface analysis
6.1 design sketch

6.2 requirement analysis
Through the analysis on the figure , This interface can be roughly divided into four parts

Total integral part of head
The most critical part is the sign in calendar display
Continuous check-in copy configuration
Check in task display
Through analysis, I divided this interface into three interfaces

/signIn GET agreement It is used to query the total points and check-in calendar part of the header .
/signIn/configuration GET agreement Query continuous check-in copy configuration , If the background is not required, you can configure the number and copy of points obtained by continuous check-in , This interface can be omitted , Front end write dead .
/signIn/task GET agreement Used to query check-in tasks , And the completion status of each task .
7 Query total points , Check in calendar interface
public ResponseResult selectSignIn(Integer userId, Integer year, Integer
month) {
boolean signFlag = Boolean.FALSE;
String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, year, userId);
LocalDate date = LocalDate.of(year, month, 1);
// This method has been introduced in the previous article . Is a collection of bitmaps that find an offset value range
List list = cacheClient.getBit(signKey, month * 100 + 1,
date.lengthOfMonth());
// query reids Signed by the current user in hash list (hash Tabular key Is the date of supplementary signing ,value If it exists, it means that this date is countersigned )
String retroactiveKey =
String.format(RedisKeyConstant.USER_RETROACTIVE_SIGN_IN, date.getMonthValue(),
userId);
Set keys = cacheClient.hkeys(retroactiveKey);
TreeMap<Integer, Integer> signMap = new TreeMap<>();
if (list != null && list.size() > 0) {
// From low to high , by 0 Indicates not signed , by 1 Indicates signed
long v = list.get(0) == null ? 0 : list.get(0);
// The number of cycles is the number of days in the current month
for (int i = date.lengthOfMonth(); i > 0; i–) {
LocalDate d = date.withDayOfMonth(i);
int type = 0;
if (v >> 1 << 1 != v) {
// The status is normal check-in
type = 1;
// Here is a comparison with the current date , Convenient front-end special mark whether to sign in today
if (d.compareTo(LocalDate.now()) == 0) {
signFlag = Boolean.TRUE;
}
}
if (keys.contains(d.getDayOfMonth() + “”)) {
// Supplementary sign status
type = 2;
}
// Returns all the dates of the previous month , And sign , Status of supplementary or unsigned
signMap.put(Integer.parseInt(d.format(DateTimeFormatter.ofPattern(“dd”))),
type);
v >>= 1;
}
}
ResponseResult responseResult = ResponseResult.newSingleData();
Map<String, Object> result = new HashMap<>(2);
// As mentioned earlier, this table stores the user's total points
UserIntegral userIntegral = userIntegralService.getOne(new
LambdaQueryWrapper().eq(UserIntegral::getUserId, userId));
// Total user points
result.put(“total”, userIntegral.getIntegral());
// Whether the user signs in today
result.put(“todaySignFlag”, signFlag ? 1 : 0);
// The back-end return date is to prevent problems caused by the mobile terminal directly modifying the system time
result.put(“today”, LocalDate.now().getDayOfMonth());
// Check in of the current month
result.put(“signCalendar”, signMap);
// Back to the front end, what day of the week is the first day of the month , It is convenient to locate when rendering the calendar map at the front end
result.put(“firstDayOfWeek”, date.getDayOfWeek().getValue());
// Current month of the server ( ditto , Prevent the mobile terminal from directly modifying the system time )
result.put(“monthValue”, date.getMonthValue());
// The number of times the user signs in the current month
result.put(“retroactiveCount”, keys.size());
// The calendar section will have data on the last few days of the previous month , So here we need to return to the front end. How many days were there last month
result.put(“lengthOfLastMonth”, date.minusMonths(1).lengthOfMonth());
responseResult.setData(result);
return responseResult;
}
Because it is used as a whole Redis Bitmap query , The check-in data of each user is through key Isolated , The time complexity is O(1). The measured data can be returned within 100 milliseconds

8. Query the check-in task and the completion status of the task
This part adopts redis and mysql Combined query . We have done the background configurable task . It can only be completed once Welfare tasks and can be reset every day Daily task .

8.1 Table structure
When designing this task list , Always pay attention to the type and jump mode . Because different tasks have different functional divisions . use
jump_type To distinguish their respective functional areas .jump_source Can be H5 The address can also be the routing address of the mobile terminal . Flexible regulation can be achieved . The front end calls the interface to complete the task and passes in the interface corresponding to the task
task_tag You can complete the specified task

CREATE TABLE t_user_integral_task (
id bigint(11) NOT NULL AUTO_INCREMENT COMMENT ‘id’,
task_type tinyint(4) DEFAULT ‘1’ COMMENT ‘ Task type 1. Daily task 2 Welfare tasks ’,
task_tag varchar(100) DEFAULT NULL COMMENT ‘ Task front end ID ( Capital letter combination )’,
task_title varchar(100) DEFAULT NULL COMMENT ‘ Task title ’,
icon varchar(255) DEFAULT NULL COMMENT ‘ small icons ’,
task_copy varchar(100) DEFAULT NULL COMMENT ‘ Task copy ’,
integral int(16) DEFAULT ‘0’ COMMENT ‘ Task reward product score ’,
jump_type tinyint(4) DEFAULT NULL COMMENT ‘ Jump mode 1. Jump to specified product 2. Jump link
3. Jump to specified interface ,4: Jump to random goods ’,
jump_source text COMMENT ‘ Jump or share address ’,
sort tinyint(2) DEFAULT ‘0’ COMMENT ‘ Sort number ’,
delete_flag tinyint(2) DEFAULT ‘0’ COMMENT ‘ delete / hide ,0: Not deleted / Not hidden ,1: Deleted / Hidden ’,
create_time datetime DEFAULT NULL COMMENT ‘ Creation time ’,
update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘ Modification time ’,
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
COMMENT=‘ User task table ’
8.2 Task query
Because there are only about ten daily tasks and welfare tasks , therefore mysql Query is very fast . Then the completion status is stored in the redis in , The time complexity is O(1)

public ResponseResult selectSignInTask(Integer userId) {
ResponseResult responseResult = ResponseResult.newSingleData();
// Find out the reason for the check-in task first mysql record .
List userIntegralTaskList = list(new LambdaQueryWrapper()

.orderByDesc(UserIntegralTask::getTaskType).orderByAsc(UserIntegralTask::getSort));
// Create a map,key For task task_tag,value Existence is the completion of the task .
// Daily tasks and welfare tasks are divided into two parts reids hash storage . Daily task key Include the date of the day , The expiration time is one day . The benefit task is saved permanently
Map<String, String> completeFlagMap = new
HashMap<>(userIntegralTaskList.size());
Map<String, String> welfareMap =
cacheClient.hgetAll(String.format(RedisKeyConstant.USER_SIGN_WELFARE_TASK,
userId));
if (CollUtil.isNotEmpty(welfareMap)) completeFlagMap.putAll(welfareMap);
Map<String, String> dailyMap =
cacheClient.hgetAll(String.format(RedisKeyConstant.USER_SIGN_DAILY_TASK,
LocalDate.now().getDayOfMonth(), userId));
// Take two hash merge
if (CollUtil.isNotEmpty(dailyMap)) completeFlagMap.putAll(dailyMap);
// Task list in circular Library , Combined use hash of get Method query completed , Then to the front
userIntegralTaskList.forEach(task -> {
task.setCreateTime(null);
task.setUpdateTime(null);
task.setIntegral(null);
String value = completeFlagMap.get(task.getTaskTag());
if (null == value) {
task.setCompleteFlag(0);
} else {
task.setCompleteFlag(1);
}
});
responseResult.setData(userIntegralTaskList);
return responseResult;
}
8.3 Complete the task
How to complete the task . Set as a public method . Pass in the corresponding task_tag Identify to complete the specified task . It only needs to judge whether he is a daily task or a welfare task . Write different redis
hash in .

// Pseudo code
public ResponseResult saveSignInTask(Integer userId, String tag) {
// Find out mysql Corresponding in tag task , Get key information .(integral)

// Write the score record . Corresponding to current task title Record of

// stay redis Write the task completion status of the current user in ( Pay attention here if it is a daily task hash
The list gives the expiration time of one day , Prevent dirty data from being cleaned up for a long time , occupy redis Memory space )
}

< END >

Technology