利用Redis bitmap 实现签到案例

news2024/11/20 7:00:10
数据库实现

设计签到功能对应的数据库表

 CREATE TABLE `sign_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `year` year NOT NULL COMMENT '签到年份',
  `month` tinyint NOT NULL COMMENT '签到月份',
  `date` date NOT NULL COMMENT '签到日期',
  `is_backup` bit(1) NOT NULL COMMENT '是否补签',
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签到记录表';

这张表中的一条记录是一个用户一次的签到记录。假如一个用户1年签到100次,而网站有100万用户,就会产生1亿条记录。随着用户量增多、时间的推移,这张表中的数据只会越来越多,占用的空间也会越来越大。

redis bitmap 实现

一个用户签到的情况无非就两种,要么签了,要么没。 可以用 0 或者1如果我们按月来统计用户签到信息,签到记录为1,未签到则记录为0,就可以用一个长度为31位的二级制数来表示一个用户一个月的签到情况。最终效果如下
image.png

java代码
引入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.orchids</groupId>
  <artifactId>signinbybitmap</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>signinbybitmap</name>
  <description>signinbybitmap</description>
  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.6.13</spring-boot.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </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>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <configuration>
          <mainClass>com.orchids.signinbybitmap.SignByBitmapApplication</mainClass>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

# 应用服务 WEB 访问端口
server:
  port: 8080

spring:
  redis:
    host: localhost
    port: 6379
    password: 6379
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

knife4j配置类

package com.orchids.signinbybitmap.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
//import springfox.documentation.swagger2.annotations.EnableSwagger2;


/**
 * @ Author qwh
 * @ Date 2024/7/5 13:08
 */
@Configuration
//@EnableSwagger2
public class knife4jConfiguration {
    @Bean
    public Docket webApiConfig(){
        // 创建Docket实例
        Docket webApi = new Docket(DocumentationType.SWAGGER_2)
        .groupName("StudentApi")
        .apiInfo(webApiInfo())
        .select()
        // 选择需要文档化的API,只显示指定包下的页面
        .apis(RequestHandlerSelectors.basePackage("com.orchids.signinbybitmap"))
        // 指定路径匹配规则,只对/student开头的路径进行文档化
        .paths(PathSelectors.regex("/User/.*"))
        .build();
        return webApi;
    }

    /**
     * 构建API信息
     * 本函数用于创建并返回一个ApiInfo对象,该对象包含了API文档的标题、描述、版本以及联系方式等信息。
     * @return 返回构建好的ApiInfo对象
     */
    private ApiInfo webApiInfo(){
        // 使用ApiInfoBuilder构建API信息
        return new ApiInfoBuilder()
        .title("Student message API文档") // 设置文档标题
        .description("本文档描述了Swagger2测试接口定义") // 设置文档描述
        .version("1.0") // 设置文档版本号
        .contact(new Contact("nullpointer", "http://blog.nullpointer.love", "nullpointer2024@gmail.com")) // 设置联系人信息
        .build(); // 构建并返回ApiInfo对象
    }
}

controller
package com.orchids.signinbybitmap.web.controller;

import com.orchids.signinbybitmap.web.domain.result.Result;
import com.orchids.signinbybitmap.web.domain.vo.SignResultVO;
import com.orchids.signinbybitmap.web.service.SignService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 * @ Author qwh
 * @ Date 2024/7/5 13:01
 */
@Api(tags = "签到相关接口")
@RestController
@RequestMapping("/User")
@RequiredArgsConstructor
public class SignController {
    private final SignService signService;
    @ApiOperation("签到")
    @GetMapping("Sign")
    public Result<SignResultVO> AddSignRecords() {
        return signService.AddSignRecords();
    }
}

service
package com.orchids.signinbybitmap.web.service;

import com.orchids.signinbybitmap.web.domain.result.Result;
import com.orchids.signinbybitmap.web.domain.vo.SignResultVO;

/**
 * @ Author qwh
 * @ Date 2024/7/5 13:35
 */
public interface SignService {
    Result<SignResultVO> AddSignRecords();
}

可以扩展其他功能

package com.orchids.signinbybitmap.web.service.impl;

import com.orchids.signinbybitmap.web.domain.result.Result;
import com.orchids.signinbybitmap.web.domain.vo.SignResultVO;
import com.orchids.signinbybitmap.web.exception.SignException;
import com.orchids.signinbybitmap.web.service.SignService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;
import java.util.List;

/**
 * @ Author qwh
 * @ Date 2024/7/5 13:35
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SignServiceImpl implements SignService {
    private final String SIGN_UID= "sign:uid:";

    private final StringRedisTemplate redisTemplate;
    @Override
    public Result<SignResultVO> AddSignRecords() {
        SignResultVO vo = new SignResultVO();
        //获取签到用户
        Long userId = 1388888L;
        //获取签到日期
        LocalDateTime now = LocalDateTime.now();
        String format = now.format(DateTimeFormatter.ofPattern(":yyyy-MM-dd"));
        //设置redisKey   sign:uid:1388888:2024-07-05 5 1
        String key = SIGN_UID + userId.toString() + format;
        //计算签到偏移量
        int offset = now.getDayOfMonth() - 1;
        //添加签到记录到redis
        Boolean sign = redisTemplate.opsForValue().setBit(key, offset, true);
        if (sign){
            throw new SignException("亲!您今天已经登录过哟 (❁´◡`❁)",520);
        }
        //计算连续签到天数
        int day = now.getDayOfMonth();
        int continueDays = countSignDays(key,day);
        int rewardPoints = 0;
        switch (continueDays){
            case 2:
                rewardPoints = 10;
                break;
            case 4:
                rewardPoints=20;
                break;
            case 6:
                rewardPoints = 40;
                break;
        }
        //获取签到详情信息
        List<Integer> signDayRecord = SignRecords(userId,key,day);
        vo.setUserId(userId.intValue());
        vo.setSignDays(continueDays);
        vo.setRewardPoints(rewardPoints);
        vo.setSignRecords(signDayRecord);
        return Result.ok(vo);
    }

    /**
     * 获取连续签到天数
     * @param key
     * @param days
     * @return
     */
    private int countSignDays(String key, int days) {

        //从redis读取签到记录
        List<Long> nums = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(days)).valueAt(0));
        //计算签到次数
        int num = nums.get(0).intValue();
        //num与1进行与计算得到二进制的末尾 当末尾为1 说明签到 为0 说明没有签到
        int result = 0;
        while ((num & 1) == 1) {
            result++;
            num = num >>>1;
        }
        //返回签到结果
        return result;
    }

    /**
     * 获取签到详情
     * @param userId
     * @param key
     * @param day
     * @return
     */
    private List<Integer> SignRecords(Long userId, String key, int day) {
        //获取从redis中获取登录信息

        List<Long> sign = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(day)).valueAt(0));

        int num = sign.get(0).intValue();
        LinkedList<Integer> result = new LinkedList<>();
        while (day > 0) {
            result.addFirst(num & 1);
            num = num >>> 1;
            day--;
        }
        return result;
    }
}

其他类
package com.orchids.signinbybitmap.web.domain.result;

import lombok.Data;

/**
 * @ Author qwh
 * @ Date 2024/7/5 16:52
 */
@Data
public class Result<T> {
    //返回码
    private Integer code;

    //返回消息
    private String message;

    //返回数据
    private T data;

    public Result() {
    }

    private static <T> Result<T> build(T data) {
        Result<T> result = new Result<>();
        if (data != null)
            result.setData(data);
        return result;
    }

    public static <T> Result<T> build(T body, ResultCode resultCode) {
        Result<T> result = build(body);
        result.setCode(resultCode.getCode());
        result.setMessage(resultCode.getMessage());
        return result;
    }


    public static <T> Result<T> ok(T data) {
        return build(data, ResultCode.SUCCESS);
    }

    public static <T> Result<T> ok() {
        return Result.ok(null);
    }
    public static <T> Result<T> fail(Integer code, String message) {
        Result<T> result = build(null);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static <T> Result<T> fail() {
        return build(null, ResultCode.FAIL);
    }
}

package com.orchids.signinbybitmap.web.domain.result;

import lombok.Getter;

/**
 * @ Author qwh
 * @ Date 2024/7/5 16:54
 */
@Getter
public enum ResultCode {

    SUCCESS(200, "成功"),
    FAIL(201, "失败"),
    PARAM_ERROR(202, "参数不正确"),
    SERVICE_ERROR(203, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    ILLEGAL_REQUEST(205, "非法请求"),
    REPEAT_SUBMIT(206, "重复提交"),
    DELETE_ERROR(207, "请先删除子集"),

    ADMIN_ACCOUNT_EXIST_ERROR(301, "账号已存在"),
    ADMIN_CAPTCHA_CODE_ERROR(302, "验证码错误"),
    ADMIN_CAPTCHA_CODE_EXPIRED(303, "验证码已过期"),
    ADMIN_CAPTCHA_CODE_NOT_FOUND(304, "未输入验证码"),
    ADMIN_ACCOUNT_NOT_EXIST(330,"用户不存在"),


    ADMIN_LOGIN_AUTH(305, "未登陆"),
    ADMIN_ACCOUNT_NOT_EXIST_ERROR(306, "账号不存在"),
    ADMIN_ACCOUNT_ERROR(307, "用户名或密码错误"),
    ADMIN_ACCOUNT_DISABLED_ERROR(308, "该用户已被禁用"),
    ADMIN_ACCESS_FORBIDDEN(309, "无访问权限"),
    APP_LOGIN_AUTH(501, "未登陆"),
    APP_LOGIN_PHONE_EMPTY(502, "手机号码为空"),
    APP_LOGIN_CODE_EMPTY(503, "验证码为空"),
    APP_SEND_SMS_TOO_OFTEN(504, "验证法发送过于频繁"),
    APP_LOGIN_CODE_EXPIRED(505, "验证码已过期"),
    APP_LOGIN_CODE_ERROR(506, "验证码错误"),
    APP_ACCOUNT_DISABLED_ERROR(507, "该用户已被禁用"),


    TOKEN_EXPIRED(601, "token过期"),
    TOKEN_INVALID(602, "token非法");


    private final Integer code;

    private final String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
package com.orchids.signinbybitmap.web.domain.vo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.models.auth.In;
import lombok.Data;

import java.util.List;

/**
 * @ Author qwh
 * @ Date 2024/7/5 13:36
 */
@Data
@ApiModel(description = "签到结果")
public class SignResultVO {

    @ApiModelProperty("签到人")
    private Integer UserId;

    @ApiModelProperty("签到得分")
    private Integer signPoints = 1;

    @ApiModelProperty("连续签到天数")
    private Integer signDays;

    @ApiModelProperty("连续签到奖励积分,连续签到超过7天以上才有奖励")
    private Integer rewardPoints;

    @ApiModelProperty("签到详细信息")
    private List<Integer> signRecords;


}

package com.orchids.signinbybitmap.web.exception;

import com.orchids.signinbybitmap.web.domain.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @ Author qwh
 * @ Date 2024/7/5 16:51
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result error(Exception e){
        e.printStackTrace();
        return Result.fail();
    }
    @ExceptionHandler(SignException.class)
    @ResponseBody
    public Result error(SignException e){
        e.printStackTrace();
        return Result.fail(e.getCode(), e.getMessage());
    }
}

package com.orchids.signinbybitmap.web.exception;

import lombok.Data;

/**
 * @ Author qwh
 * @ Date 2024/7/5 16:47
 */
@Data
public class SignException extends RuntimeException{
    //异常状态码
    private Integer code;
    /**
     * 通过状态码和错误消息创建异常对象
     * @param message
     * @param code
     */
    public SignException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    @Override
    public String toString() {
        return "SignException{" +
                "code=" + code +
                ", message=" + this.getMessage() +
                '}';
    }
}

package com.orchids.signinbybitmap;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class SignByBitmapApplication {

    public static void main(String[] args) {
        SpringApplication.run(SignByBitmapApplication.class, args);
    }

}

测试结果

image.png

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1899598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目部署_持续集成_Jenkins

1 今日内容介绍 1.1 什么是持续集成 持续集成&#xff08; Continuous integration &#xff0c; 简称 CI &#xff09;指的是&#xff0c;频繁地&#xff08;一天多次&#xff09;将代码集成到主干 持续集成的组成要素 一个自动构建过程&#xff0c; 从检出代码、 编译构建…

HTML+CSS+JavaScript入门学习

目录 1. 前言2. HTML2.1 HTML简介2.2 HTML标签 3. CSS3.1 CSS知识整理及总结3.2 CSS之flex布局 4. JavaScript4.1 JavaScript知识整理及总结1-基础篇4.2 JavaScript知识整理及总结2-进阶篇 1. 前言 本文主要采用转载的形式&#xff0c;偶尔发现了一个比较不错的博客站点&#…

Java需要英语基础吗?

Java编程语言本身并不要求必须有很强的英语基础&#xff0c;因为Java的语法和逻辑是独立于任何特定语言的。我收集归类了一份嵌入式学习包&#xff0c;对于新手而言简直不要太棒&#xff0c;里面包括了新手各个时期的学习方向编程教学、问题视频讲解、毕设800套和语言类教学&am…

嵌入式linux面试1

1. linux 1.1. Window系统和Linux系统的区别 linux区分大小写windows在dos&#xff08;磁盘操作系统&#xff09;界面命令下不区分大小写&#xff1b; 1.2. 文件格式区分 windows用扩展名区分文件&#xff1b;如.exe代表执行文件&#xff0c;.txt代表文本文件&#xff0c;.…

短视频父亲:成都柏煜文化传媒有限公司

短视频父亲&#xff1a;镜头背后的温情与力量 在这个信息爆炸的时代&#xff0c;短视频以其短小精悍、直观生动的特点&#xff0c;迅速占据了人们碎片化的时间&#xff0c;成为情感交流与文化传播的重要平台。而在这些纷繁复杂的短视频中&#xff0c;有一类内容尤为触动人心—…

前缀和数组 差分数组

前缀和 一维&#xff1a;通过空间换时间适用于需要频繁查询某一段区间和的场景。 一维数组&#xff1a; 一维前缀和中的每一项&#xff1a; &#xff0c;该前缀和中的每一项也就是数组中对应的前 i 项和。 一维前缀和数组的构造&#xff1a; 在输入原数组时同步构造前缀和…

linux之管道重定向

管道与重定向 一、重定向 将原输出结果存储到其他位置的过程 标准输入、标准正确输出、标准错误输出 ​ 进程在运行的过程中根据需要会打开多个文件&#xff0c;每打开一个文件会有一个数字标识。这个标识叫文件描述符。 进程使用文件描述符来管理打开的文件&#xff08;FD--…

【proteus可调直流稳压电源LM317】2022-4-11

缘由直流稳压电源的设计。-编程语言-CSDN问答 缘由proteus电路仿真设计-嵌入式-CSDN问答 仿真是什么?就是可以恣意妄为,从错误中学习,电感量1安培1伏特1亨利,220亨利220伏特1安培.

数据库原理之并发控制的基本概念

我们今天继续来看数据库原理&#xff0c;我们简单讲讲数据库的并发控制。 并发控制的定义 并发控制是为了保证事务的隔离性和一致性&#xff0c;数据库管理系统需要对并发操作进行正确调度。并发控制的主要技术有&#xff1a;、时间戳、乐观控制法、多版本并发控制等。 并发操…

加入运动健康数据开放平台,共赢鸿蒙未来

HarmonyOS SDK运动健康服务&#xff08;Health Service Kit&#xff09;是为华为生态应用打造的基于华为帐号和用户授权的运动健康数据开放平台。在获取用户授权后&#xff0c;开发者可以使用运动健康服务提供的开放能力获取运动健康数据&#xff0c;基于多种类型数据构建运动健…

读人工智能全传04NP完全问题

1. 问题解决与搜索 1.1. 解决问题的能力无疑是区分人类和其他动物的关键能力之一 1.1.1. 解决问题是需要智慧的 1.2. 汉诺塔 1.2.1. 对于三个金环而言 1.2.1.1. 你不可能找到少于7次的解决方案了 1.2.2. 最初&#xff0c;我们只能选择移动最小的金环&#xff0c;只有将它…

【MySQL系列】VARCHAR 类型详解及其使用策略

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

IT高手修炼手册(2)cmd命令

一、前言 CMD&#xff08;命令提示符&#xff09;是Windows操作系统中的一个重要工具&#xff0c;用于执行命令行操作&#xff0c;旨在提高用户在CMD中的操作效率和便利性。 二、常用cmd命令及其简要说明 1. 快捷键F1&#xff1a;按F1一次&#xff0c;命令提示符向后切换到已经…

计算机应用数学--第二次作业

第二次作业计算题编程题 第二次作业 计算题 给定图 G G G&#xff08;如图 1&#xff0c;图中数值为边权值&#xff09;&#xff0c;图切割将其分割成多个互不连通的⼦图。请使⽤谱聚类算法将图 G G G 聚类成 k 2 k 2 k2 类&#xff0c;使得&#xff1a; (a) RatioCut 最…

golang与以太坊交互

文章目录 golang与以太坊交互什么是go-ethereum与节点交互前的准备使用golang与以太坊区块链交互查询账户的余额使用golang生成以太坊账户使用golang生成以太坊钱包使用golang在账户之间转移eth安装使用solc和abigen生成bin和abi文件生成go文件使用golang在测试网上部署智能合约…

第一百四十七节 Java数据类型教程 - Java字符串字符

Java数据类型教程 - Java字符串字符 索引字符 您可以使用charAt()方法从String对象中获取特定索引处的字符。索引从零开始。 下面的代码打印索引值和字符在“W3CSCHOOL.CN"字符串中的每个索引处: public class Main {public static void main(String[] args) {String s…

SS8812T替代DRV8812的国产双通道H桥电机驱动芯片

由工采网代理的SS8812T是一款国产双通道H桥电机驱动芯片&#xff1b;该芯片为打印机和其它电机一体化应用提供一种双通道集成电机驱动方案&#xff1b;可Pin-to-Pin兼容替代DRV8812&#xff0c;可广泛应用于POS、打印机、安防相机、办公自动化设备、游戏机、机器人等。 产品描述…

免费鼠标连点器有吗?需要付费吗?鼠标连点器电脑版免费推荐6款!

在数字化时代&#xff0c;鼠标连点器成为了许多用户提高工作效率、优化游戏体验的得力助手。然而&#xff0c;面对市场上琳琅满目的鼠标连点器软件&#xff0c;很多用户都会产生疑问&#xff1a;是否有免费的鼠标连点器&#xff1f;它们真的需要付费吗&#xff1f;今天&#xf…

【IT领域新生必看】Java中的对象创建魔法:小白也能掌握的五种方法

文章目录 引言为什么需要创建对象&#xff1f;创建对象的五种常见方式1. 使用 new 关键字示例&#xff1a; 2. 使用反射示例&#xff1a; 3. 使用克隆示例&#xff1a; 4. 使用序列化和反序列化示例&#xff1a; 5. 使用工厂方法示例&#xff1a; 选择合适的对象创建方式总结 引…

解决obsidian加粗中文字体显示不突出的问题

加粗字体显示不突出的原因&#xff1a;默认字体的加粗版本本来就不突出 解决方法&#xff1a;改成显示突出的类型Microsoft YaHei UI 【效果】 修改前&#xff1a;修改后&#xff1a; 其他方法&#xff1a; 修改css&#xff08;很麻烦&#xff0c;改半天也不一定奏效&#…