hu项目的开发流程介绍
1. 项目启动阶段
• 项⽬概述:介绍项⽬的背景、⽬标和预期成果。
• 团队组建:建跨职能团队,包括产品经理、UI/UX 设计师、开发⼈员、测试⼈员等。
• ⻆⾊定义:明确团队中各个⻆⾊的职责和⼯作内容。
2. 需求分析与规划
• 需求收集:如何通过访谈、问卷、竞品分析等⽅法收集⽤⼾需求.(一般都是产品经理)
• 需求整理:讲解需求整理的技巧,如需求分类、优先级排序等。
• 需求⽂档撰写:指导如何编写清晰、完整的需求⽂档。
3. UI/UX设计与原型制作
• UI设计基础:介绍 UI 设计的基本原则和流⾏趋势。
• 原型⼯具使⽤:演⽰如何使⽤Axure、Sketch等⼯具制作交互原型。
• ⽤⼾测试:讲解如何进⾏⽤⼾测试,收集反馈并优化设计。
4. 需求拆分与任务分配
• 需求拆分:如何将⼤的需求拆分为⼩的、可管理的任务(开发开始介入:代码,业务功能的拆分)
• 任务估算:如何估算任务的⼯作量和时间。
5. 代码开发与技术实现
] • 技术选型:讲解如何根据项⽬需求选择合适的技术栈。
• 代码规范:强调代码的规范性、可读性和可维护性。
• 版本控制:介绍版本控制⼯具如 Git 的使⽤和管理。
6. 测试与质量保证
• 测试策略:不同类型的测试,如单元测试、集成测试、性能测试等。
• 缺陷跟踪:如何使⽤JIRA、Bugzilla等⼯具进⾏缺陷跟踪和管理。
• ⾃动化测试:介绍⾃动化测试⼯具和框架,如Selenium、JUnit。
7. 部署与上线
• 部署流程:从开发环境到⽣产环境的部署流程。
• 上线准备:如何准备上线,包括安全检查、性能优化等。
• 持续集成/持续部署(CI/CD):CI/CD的概念和实践。
8. 项目回顾与总结
• 项⽬复盘:回顾整个项⽬过程,总结经验教训。
• 团队协作:讨论团队协作中的挑战和改进措施。
• 持续改进:强调持续改进的重要性,介绍如何建⽴反馈机制。
1. 项目介绍
大致要搞什么东西
模拟公司年会/活动抽奖
1. 管理人员/hr人员圈选,设置抽奖活动,设置奖品.抽奖
2. 用户: 看结果
背景
随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客⼾。抽奖活动作为⼀种有效的营销⼿段,能够显著提升⽤⼾参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项⽬,通过这个项⽬提供⼀个全⾯、可靠、易于维护的抽奖平台,该平台将采⽤以下策略:
• 集成多种技术组件:利⽤MySQL、Redis、RabbitMQ等常⽤组件,构建⼀个稳定、⾼效、可扩展的抽奖系统。
• 活动、奖品与⼈员管理:允许管理员创建配置抽奖活动;管理奖品信息;管理⼈员信息。
• 实现状态机管理:通过精⼼设计的状态机,精确控制活动及奖品状态的转换,提⾼系统的可控性和可预测性。
• 保障数据⼀致性:通过事务管理和数据同步机制,确保数据的⼀致性和完整性。
• 加强安全性:实施安全措施,包括数据加密、⽤⼾认证,保护⽤⼾数据和系统安全。
• 降低维护成本:提供全⾯的⽇志记录和异常处理机制,简化问题诊断和系统维护。
• 提⾼扩展性:采⽤模块化设计与设计模式的使⽤,提⾼系统的灵活性和扩展性。
涉及的功能模块页面展示
管理员的注册和登录
活动列表
点击开始的活动,就可以开始抽奖
点击结束的活动,会出现抽奖名单
点击分享结果,就能够把链接进行复制
新建抽奖活动
圈选奖品可以勾选奖品和数量,以及设置的奖种
点击圈选人员,可以圈选当前参与抽奖的人
奖品管理
翻页功能
创建奖品
人员管理
人员列表展示
注册用户
需求概述
预期成果
提供⼀个功能全⾯、操作简便、安全可靠的抽奖平台。
目标用户
• 管理⼈员:管理⼈员、创建和管理抽奖活动,及发起抽奖
• 普通⽤⼾:可查看中奖名单,若⽤⼾中奖需要收到相关中奖通知。
UI 设计
⻅《UI 界⾯》⽂档
需求描述
1. 包含管理员的注册与登录。
a. 注册包含:姓名、邮箱、⼿机号、密码
b. 登录包含两种⽅式:
i. 电话+密码登录;
ii. 电话+短信登录; 验证码获取
iii. 登录需要校验管理员⾝份。
2. ⼈员管理: 管理员⽀持创建普通用户, 查看用户列表
a. 创建普通⽤⼾:姓名,邮箱,⼿机号
b. ⼈员列表:⼈员id、姓名、⾝份(普通⽤⼾、管理员)
3. 管理端⽀持创建奖品、奖品列表展示功能。
a. 创建的奖品信息包含:奖品名称、描述、价格、奖品图(上传)
b. 奖品列表展⽰(可翻⻚):奖品id、奖品图、奖品名、奖品描述、奖品价值(元)
4. 管理端⽀持创建活动、活动列表展示功能。
a. 创建的活动信息包含:
i. 活动名称
ii. 活动描述
iii. 圈选奖品:勾选对应奖品,并设置奖品等级(⼀⼆三等奖),及奖品数量 iv. 圈选⼈员:勾选参与抽奖⼈员
b. 活动列表展⽰(可翻⻚):
i. 活动名称
ii. 描述
iii. 活动状态:
1. 活动状态为进⾏中:点击 "活动进⾏中, 去抽奖" 按钮跳转抽奖⻚
2. 活动状态为已完成:点击 "活动已完成, 查看中奖名单" 按钮跳转抽奖⻚查看结果
5. 抽奖页面:
a. 对于进⾏中的活动,管理员才可抽奖。
b. 每轮抽奖的中奖⼈数跟随当前奖品数量。
c. 每个⼈只能中⼀次奖
d. 多轮抽奖,每轮抽奖有3个环节:展⽰奖品信息(奖品图、份数),⼈名闪动,停⽌闪动确定中奖名单
i. 当前⻚展⽰奖品信息, 点击‘开始抽奖’按钮, 则跳转⾄⼈名闪动画⾯
ii. ⼈员闪动画⾯,点击’点我确定‘按钮,确认中奖名单。
iii. 当前⻚展⽰中奖名单, 点击‘已抽完,下⼀步’按钮, 若还有奖品未抽取, 则展⽰下⼀个奖品信息, 否则展⽰全部中奖名单
iv. 点击’查看上⼀奖项‘按钮,展⽰上⼀个奖品信息
e. 对于抽奖过程中的异常情况,如抽奖过程中刷新⻚⾯,要保证抽取成功的奖项不能重新抽取。
i. 刷新⻚⾯后, 若当前奖品已抽完, 点击"开始抽奖",则直接展⽰当前奖品中奖名单
f. 如该抽奖活动已完成:
i. 展⽰所有奖项的全部中奖名单
ii. 新增"分享结果"按钮, 点击可复制当前⻚链接, 打开后隐藏其他按钮, 只展⽰活动名称与中奖结果, 保留"分享结果" 按钮
6. 通知部分: 抽奖完成需以邮件和短信方式通知中奖者。
a. “Hi,xxx。恭喜你在xxx抽奖中获得⼀等奖:⼿机。中奖时间为:xx:xx。请尽快领取您的奖品。”
7. 管理端涉及的所有页面, 包括抽奖页,需强制管理员登录后⽅可访问。
a. 未登录强制跳转登录⻚⾯
系统设计
系统架构
• 前端:使⽤JavaScript管理各界⾯的动态性,使⽤AJAX技术从后端API获取数据。
• 后端:采⽤Spring Boot3构建后端应⽤,实现业务逻辑。
• 数据库:使⽤MySQL作为主数据库,存储⽤⼾数据和活动信息。
• 缓存:使⽤Redis作为缓存层,减少数据库访问次数。
• 消息队列:使⽤RabbitMQ处理异步任务,如处理抽奖⾏为。
• 日志与安全:使⽤JWT进⾏⽤⼾认证,使⽤SLF4J+logback完成⽇志。
项目环境
• 编程语⾔:Java(后端),JavaScript(前端)。
• 开发⼯具包:JDK 17
• 后端框架:Spring Boot3。
• 数据库:MySQL。
• 缓存:Redis。(在服务器安装)
• 消息队列:RabbitMQ。(在服务器安装)
• 日志:logback。
• 安全:JWT + 加密。
业务功能模块
• ⼈员业务模块:管理员注册、登录,及普通⽤⼾的创建。
• 活动业务模块:活动管理及活动状态管理。
• 奖品业务模块:奖品管理与奖品的分配。还包括奖品图的上传。
• 通知业务模块:发送短信、邮件等业务,例如验证码发送,中奖通知。
• 抽奖业务模块:完成抽奖动作,以及抽奖后的结果展⽰。
数据库设计
• 用户表:存储⽤⼾信息,如⽤⼾名、密码、邮箱等。
• 活动表:存储活动信息,如活动名称、描述、活动状态等。
• 奖品表:存储奖品信息,如奖品名称、奖品图等。
• 活动奖品关联表:存储⼀个活动下关联了哪些奖品。
• 活动用户关联表:存储⼀个活动下设置的参与⼈员。
• 中奖记录表:存储⼀个活动的中奖名单,如活动id,奖品id,中奖者id等。
数据准备
建表:使⽤ source 命令导⼊ .sql ⽂件
在mysql数据库进行建表
-- 设置客户端与服务器之间的字符集为utf8mb4,这个字符集可以存储任何Unicode字符。
SET NAMES utf8mb4;
-- 关闭外键约束检查,这通常在创建或修改表结构时使用,以避免由于外键约束而导致的创建失败。
SET FOREIGN_KEY_CHECKS = 0;drop database IF EXISTS `lottery_system`;
create DATABASE `lottery_system` CHARACTER SET utf8mb4 COLLATE utf8mb4_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 update CURRENT_TIMESTAMP comment '更新时间',`activity_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '活动名称',`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '活动描述',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL 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;
-- ENGINE = InnoDB:指定表的存储引擎为InnoDB,这是MySQL的默认存储引擎,支持事务、外键等特性。
-- AUTO_INCREMENT = 24:为自动增长的ID字段设置起始值。
-- 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 update CURRENT_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_ci NOT NULL comment '奖品等级',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL 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 update CURRENT_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_ci NOT NULL comment '用户名',`status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL 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 update CURRENT_TIMESTAMP comment '更新时间',`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '奖品名称',`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL comment '奖品描述',`price` decimal(10, 2) NOT NULL comment '奖品价值',`image_url` varchar(2048) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL 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 update CURRENT_TIMESTAMP comment '更新时间',`user_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '用户姓名',`email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '邮箱',`phone_number` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '手机号',`password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL comment '登录密码',`identity` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL 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 update CURRENT_TIMESTAMP comment '更新时间',`activity_id` bigint NOT NULL comment '活动id',`activity_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '活动名称',`prize_id` bigint NOT NULL comment '奖品id',`prize_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '奖品名称',`prize_tier` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '奖品等级',`winner_id` bigint NOT NULL comment '中奖人id',`winner_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '中奖人姓名',`winner_email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL comment '中奖人邮箱',`winner_phone_number` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_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;:在脚本的最后,重新开启外键约束检查。
SET FOREIGN_KEY_CHECKS = 1;
或者我们把它放在记事本,然后命名,之后启动mysql,使用source
然后我们执行这行命令,直接就运行我们的写好的sql脚本(刚刚保存的文件注意:路径不能存在中⽂!!)
数据库表ER图
安全设计
• 用户登录⾝份验证:使⽤ JWT 进⾏⽤⼾⾝份验证。需强制⽤⼾在某些⻚⾯必须进⾏登录操作。
• 加密:敏感信息数据加密。例如⼿机号、⽤⼾密码等敏感数据落库需要加密。
项目启动
我们创建项目要加入的依赖:
我们根据阿里巴巴java开发手册来进行分层
我们项目应用到的层
我们进行创建
2.功能模块设计
通用处理
1. 错误码
400 401 402... 客户端异常
500 501 502... 服务端异常
为什么需要错误码?
1. 明确性:错误码提供了⼀种明确的⽅式来表⽰错误的状态。与模糊的异常消息相⽐,错误码能够精确地指出问题所在。
2. 易检索:错误码通常是数字,便于在⽇志、监控系统或错误跟踪系统中检索和过滤。
3. 客⼾端处理:客⼾端可以根据错误码进⾏特定的错误处理,⽽不是依赖于通⽤的异常处理。
4. 维护性:集中管理错误码使得它们更容易维护和更新。如果业务逻辑发⽣变化,只需要更新错误码的定义,⽽不需要修改每个使⽤它们的地⽅。在接⼝⽂档中,错误码也可以清晰地列出所有可能的错误情况,使开发者更容易理解和使⽤接⼝。
5. 调试和测试:错误码可以⽤于⾃动化测试,确保特定的错误情况被正确处理。
6. 错误分类:错误码可以帮助将错误分类为不同的级别或类型,如客⼾端错误、服务器错误、业务逻辑错误等。(也可能是分层分类)
我们用int code 来代表400,500,String msg 对应的描述; 500 -> 服务端XXX异常
注意: /** 然后按 Enter 是多行文档注释的快捷键
错误码类型定义:
package org.xiaobai.lotterysystem.common.errorcode;import lombok.Data;@Data
public class ErrorCode {//错误码private final Integer code;//错误描述private final String msg;public ErrorCode(Integer code,String msg){this.code = code;this.msg = msg;}
}
定义全局错误码:
package org.xiaobai.lotterysystem.common.errorcode;//全局错误码
public interface GlobalErrorCodeConstants {ErrorCode SUCCESS = new ErrorCode(200,"成功! ");ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500,"系统异常");ErrorCode UNKNOWN = new ErrorCode(999,"未知错误");
}
定义业务错误码--controller(后续完善)
定义业务错误码--service(后续完善)
2. 自定义异常类
ControllerException:controller 层异常类
package org.xiaobai.lotterysystem.common.exception;import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xiaobai.lotterysystem.common.errorcode.ErrorCode;@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException{/*** @see org.xiaobai.lotterysystem.common.errorcode.ControllerErrorCodeConstants*/private Integer code;//异常信息private String message;//写不带参数的构造是为了序列化//序列化的时候必须有不带参数的构造才会序列化成功//序列化(Serialization)是将数据结构或对象转换为可存储或传输的格式的过程,通常是将其转换为字节流。它常用于将对象从内存中转换为文件、数据库、网络传输等介质中,以便在不同的系统或程序间进行交换。public ControllerException(){}public ControllerException(Integer code,String message){this.code = code;this.message = message;}public ControllerException(ErrorCode errorCode){this.code = errorCode.getCode();this.message=errorCode.getMsg();}
}
ServiceException:service 层异常类
package org.xiaobai.lotterysystem.common.exception;import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xiaobai.lotterysystem.common.errorcode.ErrorCode;@Data//这个注解会生成自己的equals 和 hashcode,如果我们要使用父类的熟悉就要用下面的注解
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {/*** 异常码** @see org.xiaobai.lotterysystem.common.errorcode.ServiceErrorCodeConstants*/private Integer code;//异常信息private String message;public ServiceException() {}public ServiceException(Integer code, String message) {this.code = code;this.message = message;}public ServiceException(ErrorCode errorCode) {this.code = errorCode.getCode();this.message = errorCode.getMsg();}}
注意:
我们无参构造方法的作用是为了序列化
序列化(Serialization)是将数据结构或对象转换为可存储或传输的格式的过程,通常是将其转换为字节流。它常用于将对象从内存中转换为文件、数据库、网络传输等介质中,以便在不同的系统或程序间进行交换。
关于@Data的使用
这个注解会生成自己的equals 和 hashcode,如果我们要使用父类的熟悉就要用
@EqualsAndHashCode(callSuper = true)
关于@See注解的使用
可以在注释中实现链接跳转.@See可以指向包,类,方法,属性.用来链接代码,方便我们追溯程序。提高可读性.
3. CommonResult<T>
CommonResult<T> 作为控制器层⽅法的返回类型,封装 一个统一的HTTP 接⼝调⽤的结果,包括成功数据、错误信息和状态码。它可以被 Spring Boot 框架等⾃动转换为 JSON 或其他格式的响应体,发送给客⼾端.(使用泛型运行返回任意数据类型)
为什么要封装?
如果不同接口返回的类型不一样,对前端不友好,要进行判断
其他的原因:
1. 统⼀的返回格式:确保客⼾端收到的响应具有⼀致的结构,⽆论处理的是哪个业务逻辑。
2. 错误码和消息:提供错误码(code)和错误消息(msg),帮助客⼾端快速识别和处理错误。
3. 泛型数据返回:使⽤泛型 <T> 允许返回任何类型的数据,增加了返回对象的灵活性。
4. 静态⽅法:提供了 error() 和 success() 静态⽅法,⽅便快速创建错误或成功的响应对象。
5. 错误码常量集成:通过 ErrorCode 和 GlobalErrorCodeConstants 使⽤预定义的错误码,保持错误码的⼀致性和可维护性。
6. 序列化:实现了 Serializable 接⼝,使得 CommonResult<T> 对象可以被序列化为多种格式,如JSON或XML,⽅便⽹络传输。
7. 业务逻辑解耦:将业务逻辑与API的响应格式分离,使得后端开发者可以专注于业务逻辑的实现,⽽不必关⼼如何构建HTTP响应。
8. 客户端友好:客⼾端开发者可以通过统⼀的接⼝获取数据和错误信息,⽆需针对每个API编写特定的错误处理逻辑。
在实际应⽤中,CommonResult<T> 作为控制器层⽅法的返回类型,可以被 Spring boot 框架等⾃动转换为 JSON 或其他格式的响应体,发送给客⼾端。这种⽅式提⾼了API的可⽤性和可维护性,同时也提升了客⼾端开发者的体验。
具体代码:
package org.xiaobai.lotterysystem.common.pojo;import org.springframework.util.Assert;
import org.xiaobai.lotterysystem.common.errorcode.ErrorCode;
import org.xiaobai.lotterysystem.common.errorcode.GlobalErrorCodeConstants;import java.io.Serializable;public class CommonResult <T> implements Serializable {//为了进行各种协议的序列化我们使用这个接口//表示当前调用成功或者失败//返回的错误码private Integer code;//正常返回的数据private T data;//错误码描述private String msg;//成功public static <T> CommonResult<T> success(T data){CommonResult<T> rs = new CommonResult<>();rs.code = GlobalErrorCodeConstants.SUCCESS.getCode();rs.data = data;rs.msg = "";return rs;}//错误public static <T> CommonResult<T> error(Integer code,String msg){//判断是不是传错了Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code),"code 不是错误的异常");CommonResult<T> rs = new CommonResult<>();rs.code = code;rs.msg = msg;return rs;}public static <T> CommonResult<T> error(ErrorCode errorCode){return error(errorCode.getCode(),errorCode.getMsg());}
}
4. jackson
序列化工具(想观察对象,先转化为string...)
1> 日志打印
2> redis key value rabbitmq
工具: fastjson jackson(使用这个,因为可视化比较好) protobuf(PB)速度很快,但是可视化很差
我们先来写个序列化和反序列化的例子
代码详细解释
ObjectMapper objectMapper = new ObjectMapper();CommonResult<String> result = CommonResult.error(500, "系统错误");String str;// 序列化try {str = objectMapper.writeValueAsString(result);System.out.println(str);} catch (JsonProcessingException e) {throw new RuntimeException(e);}// 反序列化try {CommonResult<String> readResult = objectMapper.readValue(str,CommonResult.class);System.out.println(readResult.getCode() + readResult.getMsg());} catch (JsonProcessingException e) {throw new RuntimeException(e);}
反序列化list,map....需要多一步处理: 因为是泛型,所以为了避免擦除机制导致不晓得反序列化是什么类型,用javaType来解决
// List 序列化List<CommonResult<String>> commonResults = Arrays.asList(CommonResult.success("success1"),CommonResult.success("success2"));try {str = objectMapper.writeValueAsString(commonResults);System.out.println(str);} catch (JsonProcessingException e) {throw new RuntimeException(e);}// List 反序列化JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, CommonResult.class);try {commonResults = objectMapper.readValue(str, javaType);for (CommonResult<String> commonResult : commonResults) {System.out.println(commonResult.getData());}} catch (JsonProcessingException e) {throw new RuntimeException(e);}
但是此刻,我们发现每次序列化和反序列化都要捕获异常,觉得麻烦了,我们就可以借鉴底层的实现:
把要抛出异常的语句写成lambd表达式的形式tryparse对异常进行统一 的处理.
具体应用
package org.xiaobai.lotterysystem.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.ListIterator;
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;}private static <T> T tryParse(Callable<T> parser) {return tryParse(parser, JacksonException.class);}private 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);}throw new IllegalStateException(var4);}}/*** 序列化方法** @param object* @return*/public static String writeValueAsString(Object object) {return JacksonUtil.tryParse(() -> {return JacksonUtil.getObjectMapper().writeValueAsString(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);});}}
解释: 把序列化和反序列化的操作用lambda进行打包作为tryParse()的参数传过去,然后对异常进行统一的处理
5.⽇志处理
我们使用SLF4 + logback来达到程序展现日志的目的
本地(dev): 输出到控制台
服务器(prod/test): 输出到目标目录
spring里面内置了SLF4,我们可以在程序中直接调用它
需要配置的xml和properties
properties:
spring.application.name=lottery-system## logback xml ##
logging.config=classpath:logback-spring.xml
spring.profiles.active=dev
# 部署后需要变成
# spring.profiles.active=test## MySql ##
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#链接数据库url
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/lottery_system?characterEncoding=utf8&useSSL=false
#链接数据库用户名
spring.datasource.username=root
#链接数据库密码
spring.datasource.password=123456.## MyBatis ##
#Mapper.xml 文件路径
#mybatis.mapper-locations=classpath:mapper/*Mapper.xml
#驼峰自动转换 user_id->userId
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-handlers-package=com.example.lotterysystem.dao.handler## 短信 ##
sms.access-key-id=LTAI5tHze5qVnURvNaUvxQN7
sms.access-key-secret=yoY1dN16PWKxG3dYS72qcRCaXyyu0u
sms.sign-name=bitejiuyeke## redis spring boot 3.x ##
spring.data.redis.host=localhost
spring.data.redis.port=8888
# 连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcp-keepalive值⼀致
spring.data.redis.timeout=60s
# 默认使⽤ lettuce 连接池
# 允许最⼤连接数,默认8(负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-active=8
# 最⼤空闲连接数,默认8
spring.data.redis.lettuce.pool.max-idle=8
# 最⼩空闲连接数,默认0
spring.data.redis.lettuce.pool.min-idle=0
# 连接⽤完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常JedisConnectionException,(默认-1,负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-wait=5s## 文件上传 ##
# 目标路径
pic.local-path=D:/PIC
# spring boot3 升级配置名
spring.web.resources.static-locations=classpath:/static/,file:${pic.local-path}## mq ##
spring.rabbitmq.host=124.71.229.73
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#消息确认机制,默认auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#设置失败重试 5次
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5## 邮件 ##
spring.mail.host=smtp.qq.com
spring.mail.username=2689241679@qq.com
# 你的授权码:邮箱设置-》第三⽅服务-》开启IMAP/SMTP服务-》获取授权码
spring.mail.password=kdllbncanswudgch
spring.mail.default-encoding=UTF-8## 线程池 ##
async.executor.thread.core_pool_size=10
async.executor.thread.max_pool_size=20
async.executor.thread.queue_capacity=20
async.executor.thread.name.prefix=async-service-
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false"><springProfile name="dev"><!--输出到控制台--><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n%ex</pattern></encoder></appender><root level="info"><appender-ref ref="console" /></root></springProfile><springProfile name="prod,test"><!--ERROR级别的日志放在logErrorDir目录下,INFO级别的日志放在logInfoDir目录下--><property name="logback.logErrorDir" value="/root/lottery-system/logs/error"/><property name="logback.logInfoDir" value="/root/lottery-system/logs/info"/><property name="logback.appName" value="lotterySystem"/><contextName>${logback.appName}</contextName><!--ERROR级别的日志配置如下--><appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender"><!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。--><File>${logback.logErrorDir}/error.log</File><!-- 日志level过滤器,保证error.***.log中只记录ERROR级别的日志--><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><!--滚动策略,按照时间滚动 TimeBasedRollingPolicy--><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间--><FileNamePattern>${logback.logErrorDir}/error.%d{yyyy-MM-dd}.log</FileNamePattern><!--只保留最近14天的日志--><maxHistory>14</maxHistory><!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志--><!--<totalSizeCap>1GB</totalSizeCap>--></rollingPolicy><!--日志输出编码格式化--><encoder><charset>UTF-8</charset><pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern></encoder></appender><!--INFO级别的日志配置如下--><appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender"><!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天的日志改名为今天的日期。即,<File> 的日志都是当天的。--><File>${logback.logInfoDir}/info.log</File><!--自定义过滤器,保证info.***.log中只打印INFO级别的日志, 填写全限定路径--><filter class="org.xiaobai.lotterysystem.common.filter"/><!--滚动策略,按照时间滚动 TimeBasedRollingPolicy--><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间--><FileNamePattern>${logback.logInfoDir}/info.%d{yyyy-MM-dd}.log</FileNamePattern><!--只保留最近14天的日志--><maxHistory>14</maxHistory><!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志--><totalSizeCap>1GB</totalSizeCap></rollingPolicy><!--日志输出编码格式化--><encoder><charset>UTF-8</charset><pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern></encoder></appender><root level="info"><appender-ref ref="fileErrorLog" /><appender-ref ref="fileInfoLog"/></root></springProfile>
</configuration>
⾃定义过滤器
本地不能体现,当项⽬部署到服务器上之后,根据配置⽂件得知,错误⽇志要放在error.log下,正常的⽇志打印放在 info.log中,要实现将 info.***.log中只打印INFO级别的⽇志,需要添加⼀下代码:
package org.xiaobai.lotterysystem.common.filter;import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;public class InfoLevelFilter extends Filter<ILoggingEvent> {@Overridepublic FilterReply decide(ILoggingEvent iLoggingEvent) {//把Info级别的日志过滤出来if (iLoggingEvent.getLevel().toInt() == Level.INFO.toInt()) {return FilterReply.ACCEPT;}return FilterReply.DENY;}
}
配置的对应关系
用户模块
1. 注册
1.1 敏感字段加密
⼀般来说,⽤⼾注册时,需要输⼊其账⼾密码及⼿机号,服务器应该将其保存起来,⽅便后续登录验证。但仅从道德的⻆度来说,后端不应该以明⽂形式存储⽤⼾密码以及其他敏感信息。(因为如果后面因为bug把数据邂逅出去就不好了)
• 从运维层⾯看,任何操作系统漏洞、基础⼯具漏洞的发⽣,都会导致密码泄露
• 从开发层⾯看,任何代码逻辑有漏洞、任何依赖库的漏洞都可能导致密码泄露
• 从管理层⾯看,任何⼀个有权限读取数据库的⼈,都能看到所有⽤⼾的密码(怕直接泄露表数据)
我们可以把数据进行加密操作
密码如何加密?
• 对称加密?
◦ ⽐如3DES、AES等算法,使⽤这种⽅式加密是可以通过解密来还原出原始密码的,当然前提
条件是需要获取到密钥。密钥很可能也会泄露,当然可以将⼀般数据和密钥分开存储、分开
管理,但要完全保护好密钥也是⼀件⾮常复杂的事情,所以这种⽅式并不是很好的⽅式。
• 哈希?
◦ 加密不可逆
◦ 彩虹表攻击
• 加盐哈希?加盐哈希是⽬前业界最常⻅的做法。
◦ ⽤⼾注册时,给他随机⽣成⼀段字符串,这段字符串就是盐(Salt)(Hash结果固定)
◦ 把⽤⼾注册输⼊的密码和盐拼接在⼀起,叫做加盐密码
◦ 对加盐密码进⾏哈希,并把结果和盐都储存起来
⼿机号如何加密?
除了密码以外,⼿机号等信息也是重要的隐私数据。但⼿机号与密码不同:对于后端来说,永远不知道密码的明⽂也不会对业务逻辑造成影响;⽽后端可能需要明⽂的⼿机号,在⼀些情况下给⽤⼾发送短信。
因此对于⼿机号这种信息,只能⽤相对安全的做法,即先对⼿机号进⾏对称加密,再将加密结果储存在数据库⾥;使⽤时再⽤密钥解开。
加密⼯具
国产 Java⼯具类库 Hutool,对⽂件、流、加密解密、转码、正则、线程、XML 等 JDK ⽅法进⾏了封装,开箱即⽤!
引⼊ jar 包:
Maven 仓库地址:https://mvnrepository.com/artifact/cn.hutool HuTool
官⽹地址:https://hutool.cn/
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
我们对手机号(对称加密),因为它需要解密,我们给用户发送获奖信息)和密码(hash加密,不需要解密,直接进行比对即可)进行加盐hash操作.
哈希加密 sha256 -> 密码
对称加密 aes -> 手机号
代码实现
package org.xiaobai.lotterysystem;import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.symmetric.AES;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.DigestUtils;import java.nio.charset.StandardCharsets;@SpringBootTest
public class EncryptTest {//密码采用 hash加密 sha256@Testvoid sha256Test() {String encrypt = DigestUtil.sha256Hex("123456789");System.out.println("经过sha256的结果: "+ encrypt);//15e2b0d3c33891ebb0f1ef609ec419420c20e320ce94c65fbc8c3312448eb225}//手机号 对称加密 使用 aes@Testvoid aesTest(){//密钥:长度的设置要为16(128位) 24(192) 32(256)byte[] KEY = "123456789abcdefg".getBytes(StandardCharsets.UTF_8);//加密AES aes = SecureUtil.aes(KEY);String encrypt = aes.encryptHex("123456789");System.out.println("aes的加密结果为: "+encrypt);//解密System.out.println("aes的解密结果为: "+aes.decryptStr(encrypt));}
}
1.2 用户注册
注册场景:
1> 登录页面点击注册,就有注册页面
2> 管理员在后台进行注册用户
时序图
约定前后端交互接口
controller: 请求的入口
service: 处理业务逻辑
dao: 与数据层(mysql)进行交互
Controller 层接口设计
UserController
package org.xiaobai.lotterysystem.controller;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.lotterysystem.common.errorcode.ControllerErrorCodeConstants;
import org.xiaobai.lotterysystem.common.exception.ControllerException;
import org.xiaobai.lotterysystem.common.pojo.CommonResult;
import org.xiaobai.lotterysystem.common.utils.JacksonUtil;
import org.xiaobai.lotterysystem.controller.param.UserRegisterParam;
import org.xiaobai.lotterysystem.controller.result.UserRegisterResult;
import org.xiaobai.lotterysystem.service.UserService;
import org.xiaobai.lotterysystem.service.dto.UserRegisterDTO;@RestController
public class UserController {private static final Logger logger = LoggerFactory.getLogger(UserController.class);@Autowiredprivate UserService userService;//注册@RequestMapping("/register")public CommonResult<UserRegisterResult> userRegister(//@Validated表示要对传过来的参数进行验证@Validated @RequestBody UserRegisterParam param){//前端传过来的是一个进行序列化的json字符串,我们需要设置接受的格式是json//打印日志,打印请求参数是否正确logger.info("userController UserRegisterParam:{}", JacksonUtil.writeValueAsString(param));//把参数进行序列化//调用service进行访问UserRegisterDTO userRegisterDTO = userService.register(param);return CommonResult.success(convertToUserRegisterResult(userRegisterDTO));//把它转换位CommonResult<UserRegisterResult>统一信息返回类型,进行http请求的返回}private UserRegisterResult convertToUserRegisterResult(UserRegisterDTO userRegisterDTO) {UserRegisterResult rs = new UserRegisterResult();if(null == userRegisterDTO){throw new ControllerException(ControllerErrorCodeConstants.REGISTER_ERROR);}//没有异常就进行类型的转换rs.setUserId(userRegisterDTO.getUserId());return rs;}
}
参数类
package org.xiaobai.lotterysystem.controller.param;import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.springframework.validation.annotation.Validated;@Data
public class UserRegisterParam {//@Validated//设置必填字段//姓名@NotBlank(message = "姓名不能为空!")private String name;//邮箱@NotBlank(message = "邮箱不能为空!")private String mail;//电话@NotBlank(message = "电话不能为空!")private String phoneNumber;//密码private String password;//身份信息@NotBlank(message = "身份信息不能为空!")private String identity;
}
结果类:
package org.xiaobai.lotterysystem.controller.result;import lombok.Data;import java.io.Serializable;@Data
public class UserRegisterResult implements Serializable {private Long userId;
}
其中注意,我们要对注册的信息填写进行必填和非必填的设置,此时可以用到
Validation
对于 controller 接⼝⼊参字段的验证,可以使⽤ Spring Boot 中集成的 Validation 来完成。例如可以看到我们在接⼝⼊参上加⼊了 @Validated 注解,并且 param 对象中的每个成员都使⽤@NotBlank 注解来检查参数不能为空。使⽤需引⼊依赖
<!-- spring-boot 2.3及以上的版本只需要引⼊下⾯的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Service 层接口设计
下面我们这个使用了接口分离的设计
为什么进⾏接⼝分离设计?接⼝与实现的分离是 Java 编程中推崇的⼀种设计哲学,它有助于创建更加灵活、可维护和可扩展的软件系统。
1. 抽象与具体实现分离:接⼝定义了⼀组操作的契约(接口里面定义了一些协议),⽽实现则提供了这些操作的具体⾏为。这种分离允许改变具体实现⽽不影响使⽤接⼝的客⼾端代码。(后面修改的时候不需要改接口,直接改实现类即可)
2. ⽀持多态性:接⼝允许通过共同的接⼝来引⽤不同的实现,这是多态性的基础,使得代码更加灵活和通⽤。
3. 提⾼代码的可读性和可理解性:接⼝提供了清晰的 API 视图,使得其他开发者能够更容易地理解和使⽤这些 API。(直接看接口里面定义的方法名称就可以大致晓得是干啥,没有方法实现)
4. 安全性:接⼝可以隐藏实现细节,只暴露必要的操作,这有助于保护系统的内部状态和实现不被外部直接访问。
5. 遵循开闭原则:软件实体应当对扩展开放,对修改封闭。接⼝与实现的分离使得在不修改客⼾端代码的情况下扩展系统的功能。
6. 促进⾯向对象的设计:接⼝与实现的分离⿎励开发者进⾏⾯向对象的设计,考虑如何将系统分解为可重⽤和可组合的组件。
我们在判断是否邮箱已经被注册过需要调用数据库里面的数据,要使用mybatis,此时我们要引入依赖(程序与数据库相互交互的框架)
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
userServiceImpl: 对注册的信息进行校验操作
package org.xiaobai.lotterysystem.service.Impl;import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.xiaobai.lotterysystem.common.errorcode.ServiceErrorCodeConstants;
import org.xiaobai.lotterysystem.common.exception.ServiceException;
import org.xiaobai.lotterysystem.common.utils.RegexUtil;
import org.xiaobai.lotterysystem.controller.param.UserRegisterParam;
import org.xiaobai.lotterysystem.dao.dataobject.Encrypt;
import org.xiaobai.lotterysystem.dao.dataobject.UserDO;
import org.xiaobai.lotterysystem.dao.mapper.UserMapper;
import org.xiaobai.lotterysystem.service.UserService;
import org.xiaobai.lotterysystem.service.dto.UserRegisterDTO;
import org.xiaobai.lotterysystem.service.enums.UserIdentityEnum;@Service
//接口的实现类
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserRegisterDTO register(UserRegisterParam param) {//校验注册信息checkRegisterInfo(param);//加密私密数据(构造dao层数据)UserDO userDo = new UserDO();userDo.setUserName(param.getName());userDo.setEmail(param.getMail());userDo.setPhoneNumber( new Encrypt(param.getPhoneNumber()));userDo.setIdentity(param.getIdentity());if(StringUtils.hasText(param.getPassword())){//判断是否有密码//有密码就进行加密userDo.setPassword(DigestUtil.sha256Hex(param.getPassword()));}//保存数据//我们把DO传进去userMapper.insert(userDo);//构造返回UserRegisterDTO userRegisterDTO = new UserRegisterDTO();userRegisterDTO.setUserId(userDo.getId());//获取当前的idreturn userRegisterDTO;}private void checkRegisterInfo(UserRegisterParam param) {//为了避免空指针异常if(null == param){throw new ServiceException(ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);}//校验邮箱格式 xxx@xxx.xxxif(!RegexUtil.checkMail(param.getMail())){throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);}//校验手机号格式if(!RegexUtil.checkMobile(param.getPhoneNumber())){throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}//校验身份信息if(null == UserIdentityEnum.forName(param.getIdentity())){throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);}//校验管理员密码必填//判断是不是管理员身份if(param.getIdentity().equalsIgnoreCase(UserIdentityEnum.ADMIN.name())&& !StringUtils.hasLength(param.getPassword())){//是管理员,但是没有写密码throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);}//密码长度必须至少6位if(StringUtils.hasText(param.getPassword())&& !RegexUtil.checkPassword(param.getPassword())){//是管理员,但是长度不符合throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);}//校验邮箱是否被使用if(checkMailUsed(param.getMail())){throw new ServiceException(ServiceErrorCodeConstants.MAIL_USED);}//校验手机号是否被使用if(checkPhoneNumberUsed(param.getPhoneNumber())){throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_USED);}}//校验手机号是否被使用private boolean checkPhoneNumberUsed(String phoneNumber) {//对于手机号而言,它是用对称加密,所以我们需要进行解密才能获得手机号int count = userMapper.countByPhone(new Encrypt(phoneNumber));return count > 0;}//校验邮箱是否被使用private boolean checkMailUsed(String mail) {//去查数据库表里面,有没有这个数据//判断邮箱绑定人数>0,就说明邮箱被使用过了int count = userMapper.coutByMail(mail);return count > 0;}
}
enums: 枚举我们的用户类型
package org.xiaobai.lotterysystem.service.enums;import lombok.AllArgsConstructor;
import lombok.Getter;@Getter
@AllArgsConstructor
public enum UserIdentityEnum{ADMIN("管理员"),NORMAL("普通用户");//private final String message;public static UserIdentityEnum forName(String name){for(UserIdentityEnum userIdentityEnum : UserIdentityEnum.values()){if(userIdentityEnum.name().equalsIgnoreCase(name)){return userIdentityEnum;}}return null;}}
UserRegisterDTO
package org.xiaobai.lotterysystem.service.dto;import lombok.Data;@Data
public class UserRegisterDTO {//DTO是阿里巴巴的规范,是数据传输对象,是service或者manager向外传输的对象//用户IDprivate Long userId;
}
命名为DTO的原因
其中校验⽤⼾信息,例如邮箱、电话、密码格式的内容,我们封装成了⼀个 util 来完成: RegexUtil: 里面都是用正则表达式来规定格式
package org.xiaobai.lotterysystem.common.utils;import org.springframework.util.StringUtils;import java.util.regex.Pattern;/****/
public class RegexUtil {/*** 邮箱:xxx@xx.xxx(形如:abc@qq.com)** @param content* @return*/public static boolean checkMail(String content) {if (!StringUtils.hasText(content)) {//判断是否邮箱内容为空return false;}/*** ^ 表示匹配字符串的开始。* [a-z0-9]+ 表示匹配一个或多个小写字母或数字。* ([._\\-]*[a-z0-9])* 表示匹配零次或多次下述模式:一个点、下划线、反斜杠或短横线,后面跟着一个或多个小写字母或数字。这部分是可选的,并且可以重复出现。* @ 字符字面量,表示电子邮件地址中必须包含的"@"符号。* ([a-z0-9]+[-a-z0-9]*[a-z0-9]+.) 表示匹配一个或多个小写字母或数字,后面可以跟着零个或多个短横线或小写字母和数字,然后是一个小写字母或数字,最后是一个点。这是匹配域名的一部分。* {1,63} 表示前面的模式重复1到63次,这是对顶级域名长度的限制。* [a-z0-9]+ 表示匹配一个或多个小写字母或数字,这是顶级域名的开始部分。* $ 表示匹配字符串的结束。*/String regex = "^[a-z0-9]+([._\\\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$";return Pattern.matches(regex, content);}/*** 手机号码以1开头的11位数字** @param content* @return*/public static boolean checkMobile(String content) {if (!StringUtils.hasText(content)) {return false;}/*** ^ 表示匹配字符串的开始。* 1 表示手机号码以数字1开头。* [3|4|5|6|7|8|9] 表示接下来的数字是3到9之间的任意一个数字。这是中国大陆手机号码的第二位数字,通常用来区分不同的运营商。* [0-9]{9} 表示后面跟着9个0到9之间的任意数字,这代表手机号码的剩余部分。* $ 表示匹配字符串的结束。*/String regex = "^1[3|4|5|6|7|8|9][0-9]{9}$";return Pattern.matches(regex, content);}/*** 密码强度正则,6到12位** @param content* @return*/public static boolean checkPassword(String content){if (!StringUtils.hasText(content)) {return false;}/*** ^ 表示匹配字符串的开始。* [0-9A-Za-z] 表示匹配的字符可以是:* 0-9:任意一个数字(0到9)。* A-Z:任意一个大写字母(从A到Z)。* a-z:任意一个小写字母(从a到z)。* {6,12} 表示前面的字符集合(数字、大写字母和小写字母)可以重复出现6到12次。* $ 表示匹配字符串的结束。*/String regex= "^[0-9A-Za-z]{6,12}$";return Pattern.matches(regex, content);}
}
Dao 层接口设计
我们用MyBatis 来完成程序和数据库交互的框架
Encrypt
package org.xiaobai.lotterysystem.dao.dataobject;import lombok.Data;@Data
public class Encrypt {private String value;public Encrypt(){}//进行序列化的时候必须有无参的构造public Encrypt(String value){this.value = value;}
}
EncryptTypeHandler: 处理Encryprt对象
package org.xiaobai.lotterysystem.dao.handler;import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.util.StringUtils;
import org.xiaobai.lotterysystem.dao.dataobject.Encrypt;import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedTypes(Encrypt.class)//处理Encrypt为jdbc的哪个对象,里面装的是被处理的类型
@MappedJdbcTypes(JdbcType.VARCHAR)//里面是转换后的jdbc类型
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {//密钥private final byte[] KEY = "123456789abcdefg".getBytes(StandardCharsets.UTF_8);@Override//设置参数,ps: SQL 预编译对象 i:需要赋值的索引位置,parameter:原本位置i需要赋的值,jdbcType:jdbc的类型public void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {if(parameter == null || parameter.getValue() == null){ps.setString(i,null);}System.out.println("加密的内容: "+ parameter.getValue());//加密AES aes = SecureUtil.aes(KEY);String str = aes.encryptHex(parameter.getValue());//进行加密//重新赋值我们要修改的第i位ps.setString(i,str);}@Override//获取值: rs: 结果集, columnName 索引名 直接可以从结果集对应的索引名称获取道数据库里面的拿到的加密数据,然后我们再进行解密就可以了public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {System.out.println("获取值得到的加密的内容: "+rs.getString(columnName));return decrypt( rs.getString(columnName));//解密操作}@Override//获取值 rs 结果集, columIndex: 索引public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {System.out.println("获取值得到的加密的内容: "+rs.getString(columnIndex));return decrypt(rs.getString(columnIndex));}@Override//获取值 cs: 结果集 columnIndex 索引public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {System.out.println("获取值得到的加密的内容: "+cs.getString(columnIndex));return decrypt(cs.getString(columnIndex));}//存储的时候希望返回一些值,返回存储过程中输出的数据//结果集中获取某一列的数据,并将其解密后返回。//解密方法private Encrypt decrypt(String str){if(!StringUtils.hasText(str)){return null;}return new Encrypt(SecureUtil.aes(KEY).decryptStr(str));}
}
UserMapper: 和数据库进行交互
package org.xiaobai.lotterysystem.dao.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.lotterysystem.dao.dataobject.Encrypt;@Mapper
public interface UserMapper {//查询邮箱绑定人数@Select("select count(*) from user where email = #{email}")int coutByMail(@Param("email")String email);@Select("select count(*) from user where phone_number = #{phoneNumber}")int countByPhone(@Param("phoneNumber") Encrypt phoneNumber);
}
注意:
TypeHandler
@MappedTypes:表⽰该处理器处理的 java 类型是什么。
@MappedJdbcTypes:表⽰处理器处理的 Jdbc 类型。
对于上述代码还有⼀个疑惑:⼿机号设置的类型为 Encrypt,Encrypt 是什么?
回归最初的问题,我们对⼿机号进⾏存储时,要先将⼿机号加密,如果要拿出使⽤时,还要进⾏⼀次解密操作。为了不让每次⼿动去加密解密,决定使⽤ Mybatis 的 TypeHandler 来解决。 TypeHandler : 简单理解就是当处理某些特定字段时,我们可以实现⼀些⽅法,让 Mybatis 遇到这些特定字段可以⾃动运⾏处理。
总体的调用流程: 我们先把我们的手机号包成Encrypty,然后我们使用Spring里面自带的TypeHandler,自动对我们的电话号码进行加密和解密操作
公共key不允许获得异常: 在propteries里面设置一下:allowPublickRetrieval=true
完美解决:MySQL8报错:Public Key Retrieval is not allowed_mysql8 public key retrieval is not allowed-CSDN博客
注意:
application.properties 中指定 Typehandler 的包路径,#type-handlers处理路径,替换成⾃⼰的的
mybatis.type-handlers-package=com.example.dao.handler
这里补充一个插件,可以不手动打开数据库,直接对我们的数据进行管理
Database Navigator 插件
IDEA 数据库插件Database Navigator 插件是 IntelliJ IDEA 集成开发环境中的⼀个重要组件,它为开发者提供了⼀种⽅便快捷的数据库管理和开发⼯具。通过 Database Navigator 插件,开发者可以连接到各种类型的数据库,执⾏ SQL 查询和更新数据,以及通过可视化的⽅式设计和维护数据库表结构。
下面进行它的插件安装以及使用
install 成功后,点击ok,然后进⾏restart.
3. 找到并进⼊DB Browser(DB Browser 可能会在项⽬最左栏,可根据习惯设置)
或者在这⾥找
选择 MySQL
然后写上自己要用的数据库的名称
注意,我们在更新表之后想观察更新后的数据,需要断开连接重启,才能进行更新
控制层通用异常处理 @RestControllerAdvice+@ExceptionHandler(拦截器的使用)
我们并没有对controller层进行try-catch,如果我们后续在service或者其他层出现了异常,如果没有抓住的话,就会把错误暴露给前端,对用户的体验很不好,但是我们每个请求都要写try-catch是个冗余工作,可以对异常进行统一的处理,此时我们可以使用拦截器
spring boot中使⽤ @RestControllerAdvice 注解,完成优雅的全局异常处理类,可以针对所有异常类型先进⾏通⽤处理后,再对特定异常类型进⾏不同的处理操作。它可以捕获全局的mvc里面的异常.@ExceptionHandler它可以指定某个异常继续抛出(未知的和已知的都可以进行捕获)
package org.xiaobai.lotterysystem.controller.handler;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.xiaobai.lotterysystem.common.errorcode.GlobalErrorCodeConstants;
import org.xiaobai.lotterysystem.common.exception.ControllerException;
import org.xiaobai.lotterysystem.common.exception.ServiceException;
import org.xiaobai.lotterysystem.common.pojo.CommonResult;@RestControllerAdvice//可以捕获全局抛出来的异常
public class GlobalExceptionHandler {private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);@ExceptionHandler(value = ControllerException.class)public CommonResult<?> controllerException(ControllerException e){//打错误日志logger.error("controllerException",e);//构造错误结果return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}//未知的异常@ExceptionHandler(value = Exception.class)public CommonResult<?> Exception(Exception e){//打错误日志logger.error("服务异常",e);//构造错误结果return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}}
使用后我们发现,我们少些个参数,是返回的我们自定义的统一异常信息
邮箱被使用
前端页面展示
成功
失败