Spring项目-抽奖系统(实操项目)(ONE)
^__^
(oo)\______
(__)\ )\/\
||----w |
|| ||
一:前言:
随着互联网技术的快速发展,线上营销活动已成为企业吸引用户、增强品牌影响力的重要手段。抽奖系统作为一种高效的用户互动工具,广泛应用于电商促销、节日活动、用户拉新等场景。然而,一个高并发、高可用且安全可靠的抽奖系统,不仅需要满足基本的随机抽奖功能,还需解决库存管理、概率控制、防刷机制、分布式事务等一系列技术挑战。
本项目基于Spring Boot框架,结合Spring生态的核心技术(如Spring MVC等),设计并实现了一个轻量级、模块化的抽奖系统。系统以可扩展性和高并发处理能力为核心目标,通过分层架构设计,将用户管理、抽奖规则配置、奖品库存管理、抽奖逻辑执行及结果记录等功能模块解耦,同时引入Redis实现分布式缓存与库存预扣减,利用MySQL的事务特性保障数据一致性,支持灵活的中奖策略配置。
1.1项目中涉及到的技术栈:
1. Spring Framework
Spring Boot:用于快速搭建和配置项目,简化Spring应用的开发。
Spring MVC:处理Web请求和响应,实现RESTful API。
Spring Transaction Management:管理事务,确保数据一致性。
MyBatis:是一个基于Java的持久层框架,主要用于管理数据库操作。
2. 数据库
MySQL:关系型数据库,用于存储用户、奖品、抽奖记录等数据。
Redis:作为缓存,存储抽奖活动的临时数据,如奖品库存、用户抽奖次数等。
3. 消息队列
RabbitMQ:用于异步处理抽奖请求,解耦系统模块,提升系统性能和可靠性。
4. 缓存
Redis:用于缓存热门数据,如奖品信息、用户抽奖记录等,减少数据库压力。
5. 安全
JWT:用于实现无状态的身份验证和授权机制。
6. 日志
Logback/Log4j2:用于记录系统日志,便于排查问题。
7. 部署
Ubantu:以桌面应用为主的 Linux 发行版操作系统。
8. 其他
Lombok:简化Java代码,减少样板代码。
Hutool:用于数据校验,生成随机数。
Spring AOP:用于实现切面编程,如日志记录、事务管理等。
1.2项目结构:
Controller层:处理HTTP请求,调用Service层。
Service层:实现业务逻辑,调用DAO层。
DAO层:与数据库交互,进行数据持久化操作。
DO层:定义数据模型,与数据库表对应。
DTO层:数据传输对象,用于前后端数据交互。
COMMON层:工具类,提供通用功能。
1.3关键功能:
用户管理:用户注册、登录、权限管理。
抽奖活动管理:创建、修改、删除抽奖活动。
抽奖逻辑:实现抽奖算法,确保公平性和随机性。
奖品管理:奖品库存管理、奖品发放。
抽奖记录:记录用户抽奖结果,提供查询功能。
项目gitee链接:lottery-system-plus/lottery-system-plus · Folio/JavaEE-Study - 码云 - 开源中国
二:系统设计:
2.1:系统架构:
前端 :使⽤J avaScript(JS) 管理各界⾯的动态性,使⽤ AJAX技术 从后端API获取数据。后端 :采⽤ Spring Boot3 构建后端应⽤,实现业务逻辑。数据库 :使⽤ MySQL 作为主数据库,存储⽤⼾数据和活动信息。缓存 :使⽤ Redis 作为缓存层,减少数据库访问次数。消息队列 :使⽤ RabbitMQ 处理异步任务,如处理抽奖⾏为。⽇志与安全 :使⽤ JWT 进⾏⽤⼾认证,使⽤ SLF4J+logback 完成⽇志。
项⽬环境编程语⾔:Java(后端),JavaScript(前端)。开发⼯具包:JDK 17 。后端框架:Spring Boot3。数据库:MySQL。缓存:Redis。消息队列:RabbitMQ。⽇志:logback。安全:JWT + 加密。
2.2:业务模块:
⼈员业务模块:管理员 注册、登录,及普通⽤⼾的创建 。活动业务模块:活动管理及活动状态管理。奖品业务模块:奖品管理与奖品的分配。还包括奖品图的上传。通知业务模块: 发送短信、邮件等业务 ,例如验证码发送,中奖通知。抽奖业务模块:完成抽奖动作,以及抽奖后的结果展⽰。
2.3:数据库设计:
⽤⼾表:存储⽤⼾信息,如⽤⼾名、密码、邮箱等。活动表:存储活动信息,如活动名称、描述、活动状态等。奖品表:存储奖品信息,如奖品名称、奖品图等。活动奖品关联表:存储⼀个活动下关联了哪些奖品。活动⽤⼾关联表:存储⼀个活动下设置的参与⼈员。中奖记录表:存储⼀个活动的中奖名单,如活动id,奖品id,中奖者id等

2.3.1:建表:
使⽤ source 命令导⼊ .sql ⽂件
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;DROP DATABASE IF EXISTS `lottery_system`;CREATE DATABASE `lottery_system` CHARACTER SET utf8mb4 COLLATEutf8mb4_general_ci;USE `lottery_system`;-- ------------------------------ Table structure for activity-- ----------------------------DROP TABLE IF EXISTS `activity`;CREATE TABLE `activity` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`activity_name` varchar(255) CHARACTER SET utf8mb3 COLLATEutf8mb3_general_ci NOT NULL COMMENT ' 活动名称 ',`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 活动描述 ',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' 活动状态 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for activity_prize-- ----------------------------DROP TABLE IF EXISTS `activity_prize`;CREATE TABLE `activity_prize` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`activity_id` bigint NOT NULL COMMENT ' 活动 id',`prize_id` bigint NOT NULL COMMENT ' 活动关联的奖品 id',`prize_amount` bigint NOT NULL DEFAULT 1 COMMENT ' 关联奖品的数量 ',`prize_tiers` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 奖品等级 ',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' 活动奖品状态 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE,UNIQUE INDEX `uk_a_p_id`(`activity_id` ASC, `prize_id` ASC) USING BTREE,INDEX `idx_activity_id`(`activity_id` ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for activity_user-- ----------------------------DROP TABLE IF EXISTS `activity_user`;CREATE TABLE `activity_user` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`activity_id` bigint NOT NULL COMMENT ' 活动时间 ',`user_id` bigint NOT NULL COMMENT ' 圈选的⽤⼾ id',`user_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' ⽤⼾名 ',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' ⽤⼾状态 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE,UNIQUE INDEX `uk_a_u_id`(`activity_id` ASC, `user_id` ASC) USING BTREE,INDEX `idx_activity_id`(`activity_id` ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for prize-- ----------------------------DROP TABLE IF EXISTS `prize`;CREATE TABLE `prize` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' 奖品名称 ',`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNULL DEFAULT NULL COMMENT ' 奖品描述 ',`price` decimal(10, 2) NOT NULL COMMENT ' 奖品价值 ',`image_url` varchar(2048) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNULL DEFAULT NULL COMMENT ' 奖品展⽰图 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`;CREATE TABLE `user` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`user_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' ⽤⼾姓名 ',`email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' 邮箱 ',`phone_number` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' ⼿机号 ',`password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNULL DEFAULT NULL COMMENT ' 登录密码 ',`identity` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOTNULL COMMENT ' ⽤⼾⾝份 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE,UNIQUE INDEX `uk_email`(`email`(30) ASC) USING BTREE,UNIQUE INDEX `uk_phone_number`(`phone_number`(11) ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for winning_record-- ----------------------------DROP TABLE IF EXISTS `winning_record`;CREATE TABLE `winning_record` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ' 主键 ',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' 创建时间 ',`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP COMMENT ' 更新时间 ',`activity_id` bigint NOT NULL COMMENT ' 活动 id',`activity_name` varchar(255) CHARACTER SET utf8mb3 COLLATEutf8mb3_general_ci NOT NULL COMMENT ' 活动名称 ',`prize_id` bigint NOT NULL COMMENT ' 奖品 id',`prize_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 奖品名称 ',`prize_tier` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 奖品等级 ',`winner_id` bigint NOT NULL COMMENT ' 中奖⼈ id',`winner_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 中奖⼈姓名 ',`winner_email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ciNOT NULL COMMENT ' 中奖⼈邮箱 ',`winner_phone_number` varchar(255) CHARACTER SET utf8mb3 COLLATEutf8mb3_general_ci NOT NULL COMMENT ' 中奖⼈电话 ',`winning_time` datetime NOT NULL COMMENT ' 中奖时间 ',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `uk_id`(`id` ASC) USING BTREE,UNIQUE INDEX `uk_w_a_p_id`(`winner_id` ASC, `activity_id` ASC, `prize_id`ASC) USING BTREE,INDEX `idx_activity_id`(`activity_id` ASC) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 69 CHARACTER SET = utf8mb3 COLLATE =utf8mb3_general_ci ROW_FORMAT = DYNAMIC;SET FOREIGN_KEY_CHECKS = 1;
数据库表ER图:
确定数据库创建完成:
(这里推荐大家可以使用MySql的可视化工具,可以使得开发更加快捷,例如:Navicat)
2.4:安全设计:
⽤⼾登录⾝份验证 :使⽤ JWT 进⾏⽤⼾⾝份验证。需强制⽤⼾在某些⻚⾯必须进⾏登录操作。加密 :敏感信息数据加密。例如⼿机号、⽤⼾密码等敏感数据落库需要加密。
三:项目启动:
使用IDEA开发项目:
3.1:代码结构设计:
代码结构设计参考《阿⾥巴巴Java开发⼿册》-- 第六章 ⼯程结构:
总结:
本项目大致分为4层(controller、service、dao、common),Manager层暂时不需要,加入一个Common层,代替Manager的一些功能,将所有的错误信息、异常处理放在该层以及一些通用的工具等。
其中阿里巴巴开发手册还有一个分层参考:
3. 【参考】分层领域模型规约:DO ( Data Object ) :与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。DTO ( Data Transfer Object ) :数据传输对象, Service 或 Manager 向外传输的对象。BO ( Business Object ) :业务对象。由 Service 层输出的封装业务逻辑的对象。AO ( Application Object ) :应用对象。在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。VO ( View Object ) :显示层对象,通常是 Web 向模板渲染引擎层传输的对象。Query :数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
3.2: 功能模块设计:
3.2.1:错误码:
为了使得项目中出现的错误更加可控,这里引入错误码和错误信息,当遇到异常或者错误时,能够及时查明错误信息来源,方便解决。
构造错误码格式(骨架):
其中包含错误码和错误码信息。
import lombok.AllArgsConstructor;
import lombok.Data;@Data
//带全参的构造函数
@AllArgsConstructor
public class ErrorCode {/*** 错误码*/private final Integer code;/*** 错误信息*/private final String message;}
(错误码+错误信息,这里使用lombok包,原因在于该包中提供了@Data注解,该注解可以直接帮我们实现set、get方法可以直接使用,
还写了一个全参的构造方法,因为当出现错误时,应该同时返回错误码和错误码信息)
构造全局错误码:
有了错误码的骨架,现在构建全局错误码,当遇到不可预知的错误的时候,全局错误码可以出来兜底,不至于最后将错误无视!!
public interface GlobalErrorCodeConstants{/*** 访问成功*/ErrorCode SUCCESS = new ErrorCode(200,"成功");/*** 系统异常*/ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500,"系统异常");/*** 功能未实现*/ErrorCode NOT_IMPLEMENTED = new ErrorCode(501,"功能未实现");/*** 错误的配置项*/ErrorCode ERROR_CONFIGURATION = new ErrorCode(502,"错误的配置项");/*** 未知错误*/ErrorCode UNKNOW = new ErrorCode(999,"未知错误");
}
定义业务错误码----controller层模板:
public interface ControllerErrorCodeConstants {//---------人员模块错误码----------//---------活动模块错误码----------//---------奖品模块错误码----------//---------抽奖模块错误码----------}
定义业务错误码----service层模板:
public interface ServiceErrorCodeConstatns {//---------人员模块错误码----------//---------活动模块错误码----------//---------奖品模块错误码----------//---------抽奖模块错误码----------
}
后续写具体业务的时候遇到什么错误,就往里面去填充!
3.2.2:自定义异常类:
在实现业务出现异常时,需要我们进行控制,需要及时捕捉异常,尽量不要向上抛给JVM去处理。
同样也定义异常错误码和异常信息。
@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException{/*** 业务错误码* @see com.example.lotterysystemplus.common.errorcode.ControllerErrorCodeConstants*/private Integer code;/*** 错误提示*/private String message;/*** 空构造方法,避免反序列化问题*/public ControllerException() {}public ControllerException(ErrorCode errorCode) {this.code = errorCode.getCode();this.message = errorCode.getMessage();}public ControllerException(Integer code, String message) {this.code = code;this.message = message;}
}
有以下几点需要解释一下:
1.为什么要写两个不同的构造方法呢?
(1).当想要抛出异常时,该异常中肯定要携带错误原因,可以直接使用我们之前构造好的ErroeCode类,该类中包含了错误码和错误码信息。
2.为什么要写一个空的构造方法呢?
(1).当需要进行反序列化操作时,需要无参构造方法作为容器!
详情可浏览:
3.3: 统一返回格式:
3.3.1:创建统一序列化对象
当控制层接收到前端传过来的参数后,处理完成后返回给前端的数据的格式应该需要统一。
这里采用JSON格式返回。
在JSON格式返回之前,需要统一序列化对象
因此需要写一个类,而且可以自动将返回结果转化为JSON格式:
/*** 统一序列化对象* @param <T>*/
@Data
public class CommonResult<T> implements Serializable {//填写返回时数据类型/*** 错误码*/private Integer code;/*** 错误提示*/private String msg;/*** 返回数据*/private T data;/*** /*返回时调用该统一方法*/public static <T> CommonResult<T> error(CommonResult<?> result) {return error(result.getCode(), result.getMsg());}public static <T> CommonResult<T> error(ErrorCode errorCode) {return error(errorCode.getCode(), errorCode.getMessage());}/*** 将失败返回结果转化为CommonResult类型传出*/public static <T> CommonResult<T> error(Integer code, String message) {Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");CommonResult<T> result = new CommonResult<>();result.setCode(code);result.setMsg(message);return result;}/*** 将成功返回结果转化为CommonResult类型传出*/public static <T> CommonResult<T> succcess(T data) {CommonResult<T> commonResult = new CommonResult<>();commonResult.setCode(GlobalErrorCodeConstants.SUCCESS.getCode());commonResult.setMsg("");commonResult.setData(data);return commonResult;}/*** 判断传入的状态码是否是成功状态*/public static Boolean isSuccess(Integer code) {return Objects.equals(code,GlobalErrorCodeConstants.SUCCESS.getCode());}
}
3.3.2:创建序列化与反序列化工具类:
主要用到的时Jackson包中的工具类——ObjectMapper类,ObjectMapper类提供了writeValueAsString方法(序列化方法)与readVaule反序列化方法。
但是在项目集成开发时,尽量不直接使用,进行封装作为公共接口使用。
以下是对ObjectMapper类进行的封装,其中涉及到了单例模式(静态方法单例),对一些异常进行控制捕获!并且用到了lambda表达式进行简写调用!
package com.example.lotterysystemplus.common.utils;import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.json.JsonParseException;
import org.springframework.util.ReflectionUtils;import java.util.List;
import java.util.concurrent.Callable;public class JacksonUtil {private JacksonUtil() {}/*** 单例*/private final static ObjectMapper OBJECT_MAPPER;static{OBJECT_MAPPER = new ObjectMapper();}private static ObjectMapper getObjectMapper() {return OBJECT_MAPPER;}public static <T> T tryParse(Callable<T> parser) {return tryParse(parser, JacksonException.class);}public static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {try {return parser.call();} catch (Exception var4) {if (check.isAssignableFrom(var4.getClass())) {throw new JsonParseException(var4);} else {throw new IllegalStateException(var4);}}}public static String writeValueAsString(Object object) {return JacksonUtil.tryParse(()->{return JacksonUtil.getObjectMapper().writeValueAsString(object);});}/*** 反序列化object方法* @param content* @param valueType* @return* @param <T>*/public static <T> T readValue(String content,Class<T> valueType) {return JacksonUtil.tryParse(()->{return JacksonUtil.getObjectMapper().readValue(content,valueType);});}/*** 反序列化List方法* @param content* @param paramClasses* @return* @param <T>*/public static <T> T readListValue(String content,Class<?> paramClasses) {JavaType javaType = JacksonUtil.getObjectMapper().getTypeFactory().constructParametricType(List.class, paramClasses);return JacksonUtil.tryParse(()->{return JacksonUtil.getObjectMapper().readValue(content,javaType);});}}
3.4:日志处理:
项目中需要有日志监控,当项目在运行过程中出现异常时,我们希望可以及时报警,并且希望报警的信息内容尽可能详细。
这里采用lombok包下面的@Slf4j注解进行日志的相关处理,这比起之前的日志打印方便许多。当需要打日志时,只需要在该类上方添加@Slf4j注解(前提是添加了lombok坐标),用log.方式进行日志说明。
例如:
四:用户模块:
4.1:注册:
用户如果想要注册一个新用户时,需要进行以下内容的填写:
1、用户姓名
2、用户邮箱
3、用户手机号
4、用户密码
UI界面如下:
此时有以下几个问题:
1.密码可见性问题(加密问题):
当用户注册完毕后,相关信息肯定是要放入数据库进行存储,但是用户注册时的密码属于用户隐私,就算是管理员我们也不应该窃取用户密码,所以在存入数据库之前涉及到加密问题。
2.手机号可见性问题:
手机号作为用户的信息之一也是非常重要的,但是重要程度肯定没有密码那么高,但是为了防止有人盗窃数据库,为了防止有人给你打"骚扰电话"!!因此手机号应该也被加密存储起来。
3.校验问题:
当用户把所有信息填写完毕之后,我们需要对所填写的信息信息及时校验,尤其是格式、长度等的校验,防止有的用户"乱填"!
针对以上出现的相关问题,做出提前"防御":
4.1.1:加密:
首先我们要清楚,我们需要对两个信息进行加密,分别是手机号和密码。
但是加密的方式很多,究竟采用哪种加密合适呢?
以下有几种常见的加密方式:
对称加密:⽐如3DES、AES等算法,使⽤这种⽅式加密是可以通过解密来还原出原始密码的,当然前提条件是需要获取到密钥。密钥很可能也会泄露,当然可以将⼀般数据和密钥分开存储、分开管理,但要完全保护好密钥也是⼀件⾮常复杂的事情,所以这种⽅式并不是很好的⽅式。哈希:加密不可逆, 同一个字符加密所得到的密文是相同的 。可能会涉及到彩虹表攻击加盐哈希:加盐哈希是⽬前业界最常⻅的做法。⽤⼾注册时,给他随机⽣成⼀段字符串,这段字符串就是盐(Salt)把⽤⼾注册输⼊的密码和盐拼接在⼀起,叫做加盐密码对加盐密码进⾏哈希,并把结果和盐都储存起来
上述加密方式的不同:
对称加密:加密后的密文是可逆的,通过"密钥"可以还原出初始值!
哈希加密:加密后的密文是不可逆的,拿不到初始值!
如果考虑安全性,哈希加密相较可能更加安全,因为加密之后,就连管理员也很难直到初始值。
但是需要考虑业务场景:
密码加密:我们管理员不需要直到用户的密码,只需要在用户注册完成时,将加密后的密文存放在数据库中,如果进行登陆操作时,只需要将登录密码加密之后与数据库中的密文进行比较即可。
手机号加密:由于此次登录页面涉及到可以用手机验证码登录,因此我们给用户发送登陆短信时,是需要用到用户的手机号的,所以采用对称加密的方式更加稳妥一点。
加密工具:
引入相关依赖:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version>
</dependency>
建议大家可以到Hutool的官网有相关加密案例,还有更多有用的方法可以学习!
加密的相关原理和方法掌握以后,我们后续再实现对手机号和密码的"自动加密操作"!!
4.2:用户注册:
用户注册时序图:
该时序图中清楚反映出用户注册时的业务逻辑。
当然再我们编写代码时候,是需要按照阿里巴巴开发手册进行每一步的完成:
前后端约定:
[ 请求 ] /register POST{"name":"张三 ","mail":"451@qq.com","phoneNumber":"13188888888","password":"123456789","identity":"ADMIN"}[ 响应 ]{"code": 200,"data": {"userId": 22},"msg": ""}
4.2.1:Controller层设计:
Controller层是接收和发送数据的进出口,需要对发送的数据进行一定的封装(序列化为JSON格式),对接收到的信息进行反序列化操作,打印日志时也需要序列化操作,让日志看起来更加简洁明了:
@Slf4j
@RestController
public class UserController {@Autowiredprivate UserService userService;/*** @UserRegisterResult 返回响应的内容进行序列化操作* @param param 以JSON格式进行接收* @Validated 该注解用于检验接收到的数据是否符合规定* @return*/@RequestMapping("/register")public CommonResult<UserRegisterResult> register(@Validated @RequestBody UserRegisterParam param) {log.info("register UserRegisterParam:{}",JacksonUtil.writeValueAsString(param));UserRegisterDTO userRegisterDTO = userService.register();return CommonResult.succcess(covertToRegisterResult(userRegisterDTO));}/*** 对Service层传下来的数据进行最后的处理并返回* @param userRegisterDTO* @return*/private UserRegisterResult covertToRegisterResult(UserRegisterDTO userRegisterDTO) {if(userRegisterDTO == null) {throw new ControllerException(ControllerErrorCodeConstants.REGISTER_ERROR);}UserRegisterResult result = new UserRegisterResult();result.setUserId(userRegisterDTO.getUserId());return result;}
}
(1):UserRegisterResult设计:
该类负责最后要给前端返回的结果控制:
@Data
public class UserRegisterResult implements Serializable {/*** 用户id*/private Integer userId;
}
(2):UserRegisterParam设计:
该类负责接收前端传过来的结果,所以要与前端的变量名保持一致:
@Data
public class UserRegisterParam implements Serializable {/*** 用户姓名*/@NotBlank(message = "姓名不能为空!")private String name;/*** 用户邮箱*/@NotBlank(message = "邮箱不能为空!")private String mail;/*** 用户电话号码*/@NotBlank(message = "电话号码不能为空!")private String phoneNumber;/*** 用户密码* 对于管理员来说密码不能为空* 对于普通用户来书不用设置密码*/private String password;/*** 用户身份* 身份设计为枚举类*/private String identity;
}
其中要使用@NotBlank等对参数进行校验,需要添加如下的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
(3):UserRegisterDTO:
该类负责Service层向Controller层传输的结果接收:
@Data
public class UserRegisterDTO implements Serializable {/*** 用户id*/private Integer userId;
}
(4):UserIdentityEnum枚举类:
该类负责存放identity的全部类型:
@AllArgsConstructor
@Getter
public enum UserIdentityEnum {ADMIN("管理员"),NORMAL("普通用户");private final String name;/*** 自动判断身份信息并返回*/public static UserIdentityEnum forname(String name) {for(UserIdentityEnum userIdentityEnum:UserIdentityEnum.values()) {if(name.equals(userIdentityEnum.name)) {return userIdentityEnum;}}return null;}
}
4.2.2:Service层设计:
Service层涉及到业务的处理,与数据库打交道。
@Service
public interface UserService {/*** 完成具体注册业务的方法* @return*/public UserRegisterDTO register();
}
(1):ServiceImpl层设计:
该层中的Service相关类是实现UserService接口,具体实现业务逻辑的类:
/*** UserService类中方法的具体实现*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;/*** 完成注册具体业务* @param param* @return*/@Overridepublic UserRegisterDTO register(UserRegisterParam param) {//校验参数//Controller进行了简单的校验,Service层需要进行更加具体的校验工作checkRegisterInfo(param);//入库UserDO userDO = new UserDO();userDO.setUserName(param.getName());userDO.setEmail(param.getEmail());userDO.setPhoneNumber(new Encrypt(param.getPhoneNumber()));if(StringUtils.hasText(param.getPassword())) {//采用md5加密userDO.setPassword(DigestUtil.md5Hex(param.getPassword()));}userDO.setIdentity(param.getIdentity());//返回结果UserRegisterDTO userRegisterDTO = new UserRegisterDTO();userRegisterDTO.setUserId(userDO.getUserId());return userRegisterDTO;}/*** 校验人员的身份信息是否满足注册要求* @request request*/private void checkRegisterInfo(UserRegisterParam request) {if(request == null) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_INFO_IS_EMPTY);}/*** 邮箱\手机号校验运用正则表达式(Regex)校验* @see com.example.lotterysystemplus.common.utils.RegexUtil*///邮箱格式校验if(!RegexUtil.checkMail(request.getEmail())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_EMAIL_ERROR);}//手机号格式校验if(!RegexUtil.checkMobile(request.getPhoneNumber())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_PHONENUMBER_ERROR);}//检查身份信息if(null == UserIdentityEnum.forname(request.getIdentity())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_IDENTITY_ERROR);}//管理员必须设置密码if(UserIdentityEnum.ADMIN.getName().equals(request.getIdentity())&& !StringUtils.hasText(request.getPassword())) {throw new ServiceException(ServiceErrorCodeConstatns.ADMIN_PASSWORD_IS_EMPTY);}//密码格式校验,最少6位if(!RegexUtil.checkPassword(request.getPassword())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_PASSWORD_ERROR);}//检查邮箱是否被使用if(checkMailUsed(request.getEmail())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_MAIL_IS_USED);}//检查手机号是否被使用if(checkPhoneNumberUsed(request.getPhoneNumber())) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_PHONENUMBER_IS_USED);}}/*** 检查手机号是否被使用* @param phoneNumber* @return*/private boolean checkPhoneNumberUsed(String phoneNumber) {//检查是否为空if(!StringUtils.hasText(phoneNumber)) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_PHONENUMBER_IS_EMPTY);}//从数据库中查重int countUser = userMapper.selectByPhoneNumber(new Encrypt(phoneNumber));return countUser>0;}/*** 检查邮箱是否被使用* @param email* @return*/private boolean checkMailUsed(String email) {//检查是否为空if(!StringUtils.hasText(email)) {throw new ServiceException(ServiceErrorCodeConstatns.REGISTER_MAIL_IS_USED);}//从数据库中查重int countUser = userMapper.selectByEmail(email);return countUser>0;}
}
(2):dataobject层设计:
该层放的类,都是直接与数据库交互的类,里面的成员变量要与数据库的统一:
BaseDO:
通用类:数据库中常用字段
@Data
public class BaseDO {/*** 用户id*/private Integer userId;/*** 创建时间*/private Date gmtCreate;/*** 更新时间*/private Date gmtModified;
}
UserDO:本次注册时与数据库接触的成员变量
@Data
public class UserDO extends BaseDO{/*** 用户姓名*/private String userName;/*** 用户邮箱*/private String email;/*** 用户电话号码*/private Encrypt phoneNumber;/*** 用户密码* 对于管理员来说密码不能为空* 对于普通用户来书不用设置密码*/private String password;/*** 用户身份* 身份设计为枚举类*/private String identity;
}
此处需要说明两点:
1.数据库中存放的用户手机号和密码都必须是加密过以后的,所以在ServiceImpl类中实现具体业务方法时,一定要将用户的密码和手机号进行加密处理。
2.为了方便起见,手机号时是采用对称加密的方式,放入数据库时需要加密,从数据库取出时需要解密,因此可以写一个自动化加解密的方法:EncryptTypeHandler方法:
原理如下图:
/*** Jdbc的数据类型* 需要进行自动化加解密的类*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Encrypt.class)
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {private static final byte[] KEYS = "123456789ABCDEFG".getBytes(StandardCharsets.UTF_8);@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {if(parameter == null || parameter.getValue() == null) {ps.setString(i,null);return ;}AES aes = SecureUtil.aes(KEYS);String secret = aes.encryptHex(parameter.getValue());ps.setString(i,secret);}/*** 解密* @param rs* @param columnName* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {return decrypt(rs.getString(columnName));}/*** 解密* @param rs* @param columnIndex* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return decrypt(rs.getString(columnIndex));}/*** 解密* @param cs 结果集* @param columnIndex 索引* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {System.out.println("获取到加密内容"+cs.getString(columnIndex));return decrypt(cs.getString(columnIndex));}/*** 解密方法* @param str* @return*/private Encrypt decrypt(String str) {if (!StringUtils.hasText(str)) {return null;}return new Encrypt(SecureUtil.aes(KEYS).decryptStr(str));}}
4.3:dao层设计:
与数据库直接打交道的可以放在dao层处理:
4.3.1:Mapper层:
该层是写sql语句的一层,是直接与数据库对接的一层:
1.注册时插入接口
2.查询重复手机号接口
3.查询重复邮箱号接口
@Mapper
public interface UserMapper {//插入新的注册信息@Insert("insert into user (user_name, email, password, phone_number, `identity`)" +" values (#{userName}, #{email}, #{password}, #{phoneNumber}, #{identity})")Integer insert(UserDO userDO);//查询重复手机号@Select("select count(1) from user where phone_number = #{phoneNumber} ")Integer selectByPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);//查询重复邮箱@Select("select count(1) from user where email = #{email} ")Integer selectByEmail(@Param("email") String email);}
4.4:全局异常处理:
注册后端写完之后,我们还需要注意一点,当发生不可预知异常时,我们需要控制住,不能直接交给JVM去处理!
因此有了设计EncryptTypeHandler的思路之后,我们也可以写一个全局的异常处理,防止异常无法控制!
@RestControllerAdvice //可以捕获全局抛得异常
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(value = ServiceException.class)public CommonResult<?> ServiceException(ServiceException e) {//打印错误日志log.error("ServiceException:{}",e);//构造返回结果return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}@ExceptionHandler(value = ControllerException.class)public CommonResult<?> ControllerException(ServiceException e) {//打印错误日志log.error("ServiceException:{}",e);//构造返回结果return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}@ExceptionHandler(value = Exception.class)public CommonResult<?> Exception(ServiceException e) {//打印错误日志log.error("服务异常",e);//构造返回结果return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}}
注册的相关接口的实现到着也就接近尾声了,当然大伙棵以适当的添加更加有趣功能:例如还可以增加二维码验证等有趣的注册方式!
接下来需要完成登录、抽奖活动设计等相关功能,感兴趣的朋友,可以继续跟我一起探索:
^__^
(oo)\______
(__)\ )\/\
||----w |
|| ||
相关文章:

Spring项目-抽奖系统(实操项目)(ONE)
^__^ (oo)\______ (__)\ )\/\ ||----w | || || 一:前言: 随着互联网技术的快速发展,线上营销活动已成为企业吸引用户、…...

STM32-智能小车项目
项目框图 ST-link接线 实物图: 正面: 反面: 相关内容 使用L9110S电机模块 电机驱动模块L9110S详解 | 良许嵌入式 测速模块 语音模块SU-03T 网站:智能公元/AI产品零代码平台 一、让小车动起来 新建文件夹智能小车项目 在里面…...

Python:字符串常见操作
find(子字符串,开始位置下标,结束位置下标) 注意:开始位置和结束位置下标可以省略,表示在整个字符串中查找 stasdfghjkl print(st.find(a))#输出结果为0,表明a在第一个位置默认从零开始,找不到则返回-1 …...
Redis 哈希(Hash)
Redis 哈希(Hash) 概述 Redis 哈希(Hash)是一种特殊的键值对类型,它允许存储结构化的数据,例如一个对象或记录。每个哈希值可以包含多个字段,每个字段又可以存储一个字符串值。这使得Redis哈希非常适合用于存储对象的…...

Windows对比MacOS
Windows对比MacOS 文章目录 Windows对比MacOS1-环境变量1-Windows添加环境变量示例步骤 1:打开环境变量设置窗口步骤 2:添加系统环境变量 2-Mac 系统添加环境变量示例步骤 1:打开终端步骤 2:编辑环境变量配置文件步骤 3࿱…...
react 路由跳转的几种方式
在 React 中,路由跳转通常通过 react-router-dom(或类似的路由库)来实现。以下是几种常见的路由跳转方式: 1. 使用 <Link> 组件 <Link> 是最简单的路由跳转方式,它会生成一个 <a> 标签,…...
2.你有什么绝活儿?—Java能做什么?
1、Java的绝活儿:要问Java有什么绝活,我觉得它应该算是一位魔法师,会的绝活儿有很多,要说最能拿得出手的当属以下三个。 1.1 平台无关性:Java可以在任何地方施展魔法,无论是Windows、Linux还是Mac…...
2025年2月文章一览
2025年2月编程人总共更新了17篇文章: 1.2025年1月文章一览 2.《Operating System Concepts》阅读笔记:p2-p8 3.《Operating System Concepts》阅读笔记:p9-p12 4.《Operating System Concepts》阅读笔记:p13-p16 5.《Operati…...
C++ | 面向对象 | 类
👻类 👾语法格式 class className{Access specifiers: // 访问权限DataType variable; // 变量returnType functions() { } // 方法 };👾访问权限 class className {public:// 公有成员protected:// 受保护成员private:// 私有成员 }…...

leetcode:2164. 对奇偶下标分别排序(python3解法)
难度:简单 给你一个下标从 0 开始的整数数组 nums 。根据下述规则重排 nums 中的值: 按 非递增 顺序排列 nums 奇数下标 上的所有值。 举个例子,如果排序前 nums [4,1,2,3] ,对奇数下标的值排序后变为 [4,3,2,1] 。奇数下标 1 和…...
Visionpro cogToolBlockEditV2.Refresh()
在 C# 中使用 cogToolBlockEditV2.Refresh() 方法主要用于刷新 CogToolBlockEditV2 控件的显示状态,适用于动态更新界面或重新加载工具块(ToolBlock)的场景。以下是具体说明和典型应用场景。 基本作用 刷新控件显示:当修改了与 C…...
Apache Spark中的依赖关系与任务调度机制解析
Apache Spark中的依赖关系与任务调度机制解析 在Spark的分布式计算框架中,RDD(弹性分布式数据集)的依赖关系是理解任务调度、性能优化及容错机制的关键。宽依赖(Wide Dependency)与窄依赖(Narrow Dependency)作为两种核心依赖类型,直接影响Stage划分、Shuffle操作及容…...

网络基础III
目录 一、网络层 1.1IP协议 1.2网段划分(🔺) 1.3特殊的ip地址 1.4ip地址的数量限制 1.5私有ip和公网ip 1.6路由 二、数据链路层 2.1认识以太网 2.2以太网帧格式 2.3认识mac地址 2.4mac地址和ip地址 2.5认识MTU 2.6MTU对IP协议的…...

【SpringBoot】自动配置原理与自定义启动器
Spring Boot 自动配置原理与自定义启动器 目录标题 Spring Boot 自动配置原理与自定义启动器摘要1. 引言2. Spring Boot自动配置原理分析2.1 自动配置的核心流程2.2 核心注解与配置文件解析2.2.1 EnableAutoConfiguration2.2.2 spring.factories 文件 2.3 自动配置类剖析2.4 配…...
Element实现el-dialog弹框移动、全屏功能
1、在Vue项目中src/utils目录中创建dialog.js,用来定义draggable-dialog; import Vue from vue Vue.directive(draggable-dialog, { // 属性名称draggable-dialog,前面加v- 使用bind(el, binding, vnode) {const dialogHeaderEl el.querySe…...
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_init_cycle 函数 - 详解(11)
详解(11) 初始化配置解析上下文 senv environ;ngx_memzero(&conf, sizeof(ngx_conf_t));/* STUB: init array ? */conf.args ngx_array_create(pool, 10, sizeof(ngx_str_t));if (conf.args NULL) {ngx_destroy_pool(pool);return NULL;}conf.te…...

千峰React:案例一
做这个案例捏 因为需要用到样式,所以创建一个样式文件: //29_实战.module.css .active{text-decoration:line-through } 然后创建jsx文件,修改main文件:导入Todos,写入Todos组件 import { StrictMode } from react …...
部署Joplin私有云服务器postgres版-docker compose
我曾经使用过一段时间 Joplin,官方版本是收费的,而我更倾向于将数据掌握在自己手中。因此,在多次权衡后,我决定自己搭建 Joplin 服务器并进行尝试。 个人搭建的版本与数据库直连,下面是使用 Docker Compose 配置数据库…...

rust学习笔记6-数组练习704. 二分查找
上次说到rust所有权看看它和其他语言比有什么优势,就以python为例 # Python3 def test():a [1, 3, -4, 7, 9]print(a[4])b a # 所有权没有发生转移del b[4]print(a[4]) # 由于b做了删除,导致a再度访问报数组越界if __name__ __main__:test() 运行结…...

Jsmoke-一款强大的js检测工具,浏览器部署即用,使用方便且高效
目录标题 Jsmoke 🚬🚬 by Yn8rt使用方式界面预览功能特性支持的敏感信息类型 Jsmoke 🚬🚬 by Yn8rt 该插件由 Yn8rt师傅 开发,插件可以理解为主动版的hae和apifinder,因为其中的大多数规则我都引用了&a…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...

React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...

【深度学习新浪潮】什么是credit assignment problem?
Credit Assignment Problem(信用分配问题) 是机器学习,尤其是强化学习(RL)中的核心挑战之一,指的是如何将最终的奖励或惩罚准确地分配给导致该结果的各个中间动作或决策。在序列决策任务中,智能体执行一系列动作后获得一个最终奖励,但每个动作对最终结果的贡献程度往往…...
面试高频问题
文章目录 🚀 消息队列核心技术揭秘:从入门到秒杀面试官1️⃣ Kafka为何能"吞云吐雾"?性能背后的秘密1.1 顺序写入与零拷贝:性能的双引擎1.2 分区并行:数据的"八车道高速公路"1.3 页缓存与批量处理…...

针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...