BitMap实现打卡(二)
后端环境搭建
新建项目
设置项目名
引入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>linc.fun</groupId>
<artifactId>clock-in</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.5</spring-boot.version>
<lombok.version>1.18.24</lombok.version>
<fastjson.version>2.0.1</fastjson.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<druid.version>1.2.9</druid.version>
<hutool.version>5.7.22</hutool.version>
<guava.version>31.1-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<finalName>${project.build.finalName}</finalName>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写启动类
@SpringBootApplication
public class ClockApplication {
public static void main(String[] args) {
SpringApplication.run(ClockApplication.class, args);
}
}
配置application.yml
server:
port: 8080
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${remote-ip}:3306/clock_in?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null
redis:
url: redis://123456@${remote-ip}:6379
username: root
jedis:
pool:
max-active: 8
max-wait: -1
min-idle: 0
max-idle: 8
mybatis-plus:
mapper-locations: classpath:mapping/**/*.xml
global-config:
db-config:
id-type: AUTO
banner: false
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
remote-ip: localhost
编写公共返回结果集
public abstract class DTO implements Serializable {
private static final long serialVersionUID = 1L;
}
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@NoArgsConstructor
public class Response extends DTO {
private static final long serialVersionUID = 1L;
private boolean success;
private String errCode;
private String errMessage;
public static Response buildFailure(String errCode, String errMessage) {
Response response = new Response();
response.setSuccess(false);
response.setErrCode(errCode);
response.setErrMessage(errMessage);
return response;
}
public static Response buildSuccess() {
Response response = new Response();
response.setSuccess(true);
return response;
}
@Override
public String toString() {
return "Response [isSuccess=" + this.success + ", errCode=" + this.errCode + ", errMessage=" + this.errMessage + "]";
}
}
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class SingleResponse<T> extends Response {
private T data;
public static <T> SingleResponse<T> of(T data) {
SingleResponse<T> singleResponse = new SingleResponse<>();
singleResponse.setSuccess(true);
singleResponse.setData(data);
return singleResponse;
}
public static SingleResponse<?> buildFailure(String errCode, String errMessage) {
SingleResponse<?> response = new SingleResponse<>();
response.setSuccess(false);
response.setErrCode(errCode);
response.setErrMessage(errMessage);
return response;
}
public static SingleResponse<?> buildSuccess() {
SingleResponse<?> response = new SingleResponse<>();
response.setSuccess(true);
return response;
}
}
编写返回集统一处理
定义相关注解,便于管理
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@RestController
public @interface ResponseResult {
/**
* 判断是否可以忽略
*/
boolean ignore() default false;
}
重写ResponseBodyAdvice
@Slf4j
@ControllerAdvice
@AllArgsConstructor
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
final var method = methodParameter.getMethod();
final var clazz = Objects.requireNonNull(method, "method is null").getDeclaringClass();
// 判断类上面是否加了@ResponseResult
var annotation = clazz.getAnnotation(ResponseResult.class);
if (Objects.isNull(annotation)) {
annotation = method.getAnnotation(ResponseResult.class);
}
// 如果是FileSystemResource 则不拦截
if (method.getAnnotatedReturnType().getType().getTypeName().equals(FileSystemResource.class.getTypeName())) {
return false;
}
return annotation != null && !annotation.ignore();
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object data, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
var singleResponse = SingleResponse.of(data);
if ((data instanceof String) && !MediaType.APPLICATION_XML_VALUE.equals(mediaType.toString())) {
ObjectMapper om = new ObjectMapper();
return om.writeValueAsString(singleResponse);
}
if (Objects.isNull(data) && MediaType.TEXT_HTML_VALUE.equals(mediaType.toString())) {
ObjectMapper om = new ObjectMapper();
return om.writeValueAsString(singleResponse);
}
return singleResponse;
}
}
配置全局配置Bean
配置MyBatis Plus
@Configuration
@MapperScan("linc.fun.*.mapper")
@EnableTransactionManagement
public class ApplicationConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
编写测试接口
@ResponseResult
@RequestMapping("/api")
public class TestController {
@GetMapping("/test")
public Map<String, Object> test() {
Map<String, Object> map = new HashMap<>();
map.put("info", "你懂的");
System.out.println("11111");
return map;
}
}
数据库创建
创建数据库
CREATE DATABASE clock_in charset utf8mb4;
USE clock_in;
CREATE TABLE `user` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(30) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户';
编写相关实体
public class PO implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1534419234554796749L;
}
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "`user`")
public class User extends PO {
/**
* ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 姓名
*/
@TableField(value = "`name`")
private String name;
}
编写MyBatis相关映射
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User>{
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="linc.fun.clock.mapper.UserMapper">
<resultMap id="BaseResultMap" type="linc.fun.clock.po.User">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
</resultMap>
</mapper>
实现后端打卡接口
创建控制层
先约定好要写拿几个接口
@ResponseResult
@RequestMapping("/api")
public class UserController {
@Resource
private UserService service;
/**
* 用户当天打卡
*/
@PostMapping("/todayClockIn/{id}")
public void todayClockIn(@PathVariable Long id) {
service.todayClockIn(id);
}
/**
* 判断当天是否打卡
*/
@GetMapping("/checkTodayClockIn/{id}")
public boolean checkTodayClockIn(@PathVariable Long id) {
return service.checkTodayClockIn(id);
}
/**
* 获取当月用户打卡信息
*/
@GetMapping("/getClockInCurrentMonthInfo/{id}")
public UserClockInfoVO getClockInCurrentMonthInfo(@PathVariable Long id) {
return service.getClockInCurrentMonthInfo(id);
}
/**
* 判断当前用户当月连续签到次数
*/
@GetMapping("/getContinuousClockInTimes/{id}")
public int getContinuousClockInTimes(@PathVariable Long id) {
return service.getContinuousClockInTimes(id);
}
}
@EqualsAndHashCode(callSuper = true)
@Data
public class UserClockInfoVO extends VO {
/**
* 打卡列表
*/
List<ClockIn> clockInfo;
/**
* 打卡
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ClockIn {
/**
* 日期
* 2022-04-06
*/
private String day;
/**
* 是否打卡
*/
private Boolean clock;
}
}
public class VO implements Serializable {
private static final long serialVersionUID = 9106194241611838335L;
}
定义Redis常量
public interface RedisKeyConstants {
/**
* 用户打卡key
*/
String USER_SIGN_IN_KEY = "USER_SIGN_IN_KEY";
/**
* 后缀
*/
String SUFFIX = "_";
/**
* 生成: SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_9_10
*
* @param redisKey SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY
* @param args 9,10
*
* @return SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_9_10
*/
static String generateKeyConstantWithSuffix(String redisKey, String... args) {
if (args.length == 0) {
return redisKey + RedisKeyConstants.SUFFIX;
} else {
StringBuilder sb = new StringBuilder(redisKey);
for (String arg : args) {
sb.append(RedisKeyConstants.SUFFIX).append(arg);
}
return sb.toString();
}
}
/**
* 如 SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_
* 判断是否以这个key为前缀
*
* @param text 需要校验的字符串
* @param keyConstant 常量
*
* @return true|false
*/
static boolean startsWithKeySuffix(String text, String keyConstant) {
return text.startsWith(keyConstant + SUFFIX);
}
/**
* 分割如 SIMBOT_SENSITIVE_ACCOUNT_KEY_1220127046
*
* @param text SIMBOT_SENSITIVE_ACCOUNT_KEY_1220127046
* @param keyConstant SIMBOT_SENSITIVE_ACCOUNT_KEY_
*
* @return [1220127046]
*/
static String[] splitKeyConstantParam(String text, String keyConstant) {
if (startsWithKeySuffix(text, keyConstant)) {
return text.substring((keyConstant + SUFFIX).length()).split(SUFFIX);
}
return null;
}
}
定义打卡Key
对于两个场景
-
用户月签到
-
月签到key:
USER_SIGN_IN_KEY_<id>_2022-4
比如说id为1的用户在2022年4月签到信息
bit: 0 1 0 1 0 1 offset: 0 1 2 3 4 5 偏移量表示这个月的第几天 含义: 签到 签到 签到
-
-
月签到用户数
-
签到用户key:
USER_SIGN_IN_KEY_2022-4
比如说2022年4月有多少人签到了,全部用这个key存放
bit: 0 1 0 1 0 1 offset: 0 1 2 3 4 5 便宜量表示这个月签到用户的id 含义: 签到 签到 签到
-
自定义BitMapUtil
@Component
public class BitMapUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 设置key字段第offset位bit数值
*
* @param key 字段
* @param offset 位置
* @param value 数值
*/
public void setBit(String key, long offset, boolean value) {
stringRedisTemplate.execute((RedisCallback<?>) con -> con.setBit(key.getBytes(), offset, value));
}
/**
* 判断该key字段offset位否为1
*
* @param key 字段
* @param offset 位置
*
* @return 结果
*/
public boolean getBit(String key, long offset) {
return (boolean) stringRedisTemplate.execute((RedisCallback<?>) con -> con.getBit(key.getBytes(), offset));
}
/**
* 统计key字段value为1的总数
*
* @param key 字段
*
* @return 总数
*/
public Long bitCount(String key) {
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
}
/**
* 统计key字段value为1的总数,从start开始到end结束
*
* @param key 字段
* @param start 起始
* @param end 结束
*
* @return 总数
*/
public Long bitCount(String key, Long start, Long end) {
return (Long) stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitCount(key.getBytes(), start, end));
}
/**
* 取多个key并集并计算总数
*
* @param key key
*
* @return 总数
*/
public Long opOrCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.OR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return this.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的交集并计算总数
*
* @param key key
*
* @return 总数
*/
public Long opAndCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.AND, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return this.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的补集并计算总数
*
* @param key key
*
* @return 总数
*/
public Long opXorCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.XOR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return this.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的否集并计算总数
*
* @param key key
*
* @return 总数
*/
public Long opNotCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.NOT, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return this.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 从第start位开始取offset位结果为无符号数的
* 有符号数最多可以获取64位, 无符号数只能获取63位
*
* @param key key
* @param start 从第几位开始
* @param offset 偏移量多少
*
* @return 取出的值
*/
public List<Long> bitfield(String key, int start, int offset) {
return stringRedisTemplate.execute((RedisCallback<List<Long>>) con -> con.bitField(key.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(start)).valueAt(offset)));
}
}
实现用户当天打卡功能
private SimpleDateFormat getYearAndMonthSdf() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
return sdf;
}
public void todayClockIn(Long id) {
// 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
// 签到用户key: USER_SIGN_IN_KEY_2022-4
/**
* 完成月签到
*/
SimpleDateFormat sdf = getYearAndMonthSdf();
// 获取当天日期 2022-04-27
LocalDate nowDate = LocalDate.now();
// 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
// 获取今天是这个月的第几天
int dayOfMonth = nowDate.getDayOfMonth();
// 设置偏移量
bitMapUtil.setBit(monthClockInKey, dayOfMonth, true);
/**
* 完成签到用户
*/
// 签到用户key: USER_SIGN_IN_KEY_2022-4
String clockInUserKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, sdf.format(Date.valueOf(nowDate)));
// 设置偏移量
bitMapUtil.setBit(clockInUserKey, id, true);
}
实现判断当天是否打卡
public boolean checkTodayClockIn(Long id) {
SimpleDateFormat sdf = getYearAndMonthSdf();
// 获取当天日期 2022-04-27
LocalDate nowDate = LocalDate.now();
// 获取今天是这个月的第几天
int dayOfMonth = nowDate.getDayOfMonth();
// 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
return bitMapUtil.getBit(monthClockInKey, dayOfMonth);
}
实现获取当月用户打卡信息
public UserClockInfoVO getClockInCurrentMonthInfo(Long id) {
SimpleDateFormat sdf = getYearAndMonthSdf();
// 获取当天日期 2022-04-27
LocalDate nowDate = LocalDate.now();
// 获取今天是这个月的第几天
int dayOfMonth = nowDate.getDayOfMonth();
// 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
// 设置这个月有多天打卡了
UserClockInfoVO vo = new UserClockInfoVO();
vo.setClockInfo(Lists.newArrayListWithCapacity(dayOfMonth));
// 这个月有多少天
int lengthOfMonth = nowDate.lengthOfMonth();
// 从第一位开始因为我设置的id默认从1开始
List<Long> bitfields = bitMapUtil.bitfield(monthClockInKey, lengthOfMonth, 1);
if (!CollectionUtils.isEmpty(bitfields)) {
// 由低位->高位,为0表示未签到,为1表示已签到
long v = Objects.isNull(bitfields.get(0)) ? 0 : bitfields.get(0);
for (int i = lengthOfMonth; i > 0; i--) {
// 过去的日期
LocalDate pastDate = nowDate.withDayOfMonth(i);
UserClockInfoVO.ClockIn clockIn = new UserClockInfoVO.ClockIn();
// 设置日期 2022-4-27
clockIn.setDay(String.valueOf(pastDate));
// 设置是否打卡
clockIn.setClock(v >> 1 << 1 != v);
// 右移一位
v >>= 1;
// 添加到集合
vo.getClockInfo().add(clockIn);
}
}
return vo;
}
实现判断当前用户当月连续签到次数
public int getContinuousClockInTimes(Long id) {
SimpleDateFormat sdf = getYearAndMonthSdf();
// 获取当天日期 2022-04-27
LocalDate nowDate = LocalDate.now();
// 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
int clockInTimes = 0;
// 从第一位开始因为我设置的id默认从1开始
List<Long> bitfields = bitMapUtil.bitfield(monthClockInKey, nowDate.getDayOfMonth(), 1);
if (!CollectionUtils.isEmpty(bitfields)) {
// 由低位连续不为0的个数即为连续签到次数,需要考虑没有签到的情况
long v = Objects.isNull(bitfields.get(0)) ? 0 : bitfields.get(0);
for (int i = 0; i < nowDate.getDayOfMonth(); i++) {
// 先右移再左移,如果相同就代表低位为0
if (v >> 1 << 1 == v) {
if (i > 0) break;
} else {
clockInTimes++;
}
v >>= 1;
}
}
return clockInTimes;
}
实现前端打卡接口
编写前端接口api/clock.ts
import request from "../util/request";
export const test = () => {
return request({
url: '/test',
method: 'GET'
})
}
/**
* 用户当天打卡
*/
export const todayClockIn = (id: number): any => {
return request({
url: `/todayClockIn/${id}`,
method: 'POST'
})
}
/**
* 判断当天是否打卡
*/
export const checkTodayClockIn = (id: number): any => {
return request({
url: `/checkTodayClockIn/${id}`,
method: 'GET'
})
}
/**
* 获取当月用户打卡信息
*/
export const getClockInCurrentMonthInfo = (id: number): any => {
return request({
url: `/getClockInCurrentMonthInfo/${id}`,
method: 'GET'
})
}
/**
* 判断当前用户当月连续签到次数
*/
export const getContinuousClockInTimes = (id: number): any => {
return request({
url: `/getContinuousClockInTimes/${id}`,
method: 'GET'
})
}
编写页面请求
我们假定用户是id为1的,因为这里我们没有做登录等功能,所以暂定用id位1的用户
<script setup lang="ts">
import {onMounted, ref, unref} from "vue"
import {ElNotification} from "element-plus"
import {todayClockIn, checkTodayClockIn, getClockInCurrentMonthInfo, getContinuousClockInTimes} from '../api/clock'
const userId = 2;
interface ClockData {
day: string,
clock: boolean
}
interface UserClockInfoVO {
clockInfo: Array<any>
}
// 是否已经打卡
let isClocked = ref<boolean>(false)
// 打卡信息
let clockData = ref<Array<ClockData>>([])
// 连续签到次数
let clockTimes = ref<Number>(0)
// 打卡
const clockIn = async () => {
await todayClockIn(userId)
ElNotification({showClose: true, message: '打卡成功', type: 'success', duration: 2000})
isClocked.value = true
// 重新获取
await getClockInfo()
}
// 判断是否打卡
const checkClockIn = async () => {
isClocked.value = await checkTodayClockIn(userId)
}
// 获取打卡信息
const getClockInfo = async () => {
let data: UserClockInfoVO = await getClockInCurrentMonthInfo(userId);
clockData.value = data.clockInfo
}
// 获取连续签到次数
const getContinuousClock = async () => {
clockTimes.value = await getContinuousClockInTimes(userId)
}
onMounted(() => {
checkClockIn()
getClockInfo()
getContinuousClock()
})
</script>
连续签到标签添加
<el-badge :value="clockTimes" type="primary">
<el-button>连续签到</el-button>
</el-badge>
前端最终代码
<script setup lang="ts">
import {onMounted, ref, unref} from "vue"
import {ElNotification} from "element-plus"
import {todayClockIn, checkTodayClockIn, getClockInCurrentMonthInfo, getContinuousClockInTimes} from '../api/clock'
const userId = 2;
interface ClockData {
day: string,
clock: boolean
}
interface UserClockInfoVO {
clockInfo: Array<any>
}
// 是否已经打卡
let isClocked = ref<boolean>(false)
// 打卡信息
let clockData = ref<Array<ClockData>>([])
// 连续签到次数
let clockTimes = ref<Number>(0)
// 打卡
const clockIn = async () => {
await todayClockIn(userId)
ElNotification({showClose: true, message: '打卡成功', type: 'success', duration: 2000})
isClocked.value = true
// 重新获取
await getClockInfo()
}
// 判断是否打卡
const checkClockIn = async () => {
isClocked.value = await checkTodayClockIn(userId)
}
// 获取打卡信息
const getClockInfo = async () => {
let data: UserClockInfoVO = await getClockInCurrentMonthInfo(userId);
clockData.value = data.clockInfo
}
// 获取连续签到次数
const getContinuousClock = async () => {
clockTimes.value = await getContinuousClockInTimes(userId)
}
onMounted(() => {
checkClockIn()
getClockInfo()
getContinuousClock()
})
</script>
<template>
<div class="picker">
<el-card style="height: 760px">
<div style="display: flex;justify-content: space-between">
<span style="font-size: 22px;display: block;margin-bottom: 30px;margin-left: 10px;">打卡</span>
<el-badge :value="clockTimes" type="primary">
<el-button>连续签到</el-button>
</el-badge>
<span v-if="!isClocked"><el-button type="primary" class="el-icon-s-promotion" style="border: none"
@click="clockIn"><span
style="color: white;font-weight: bolder">未打卡</span></el-button></span>
<span v-else><el-button type="success" class="el-icon-s-promotion" style="border: none"><span
style="color: white;font-weight: bolder">已打卡</span></el-button></span>
</div>
<el-calendar :first-day-of-week="7">
<template #dateCell="{ data }">
<p>{{ data.day.split('-').slice(2).join('-') }}<br/></p>
<div v-for="(item, index) in clockData" :key="index">
<div v-if="data.day === item.day">
<span v-if="item.clock">
<i class="el-icon-check" style="color: green;font-weight: bolder;">已打卡</i>
</span>
</div>
</div>
</template>
</el-calendar>
</el-card>
</div>
</template>
<style scoped lang="scss">
.picker {
display: flex;
justify-content: center;
align-items: center;
}
::v-deep(.el-calendar-table .el-calendar-day) {
padding: 0;
}
::v-deep(.el-calendar-table td.is-today ) {
background-color: #fff;
}
::v-deep(.el-calendar-table td.is-selected ) {
background-color: #fff;
}
::v-deep(.el-calendar-table .el-calendar-day) {
height: 60px;
font-size: 12px;
text-align: center;
}
::v-deep(.el-calendar-table:not(.is-range) td.prev) {
.calendarFont {
color: #C0C4CC;
}
pointer-events: none;
}
::v-deep(.el-calendar-table thead th) {
font-size: 12px;
padding-bottom: 6px;
}
.cal ::v-deep(.el-calendar-day .calendar_circle1) {
margin: 0 auto;
padding: 2px;
text-align: center;
}
.cal ::v-deep(.el-calendar-day .calendar_circle2) {
border: 1px solid #DE4747;
border-radius: 50%;
margin: 0 auto;
padding: 2px;
text-align: center;
}
</style>