在线抽奖系统——通用模块

发布于:2025-02-27 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

项目创建

通用功能模块

错误码

自定义异常类

CommonResult

jackson

加密工具


项目创建

使用 Idea 创建 SpringBoot 项目,并引入相关依赖:

 

导入前端页面

前端页面:前端代码/在线抽奖系统 · Echo/project - 码云 - 开源中国

将其导入到 static 目录下:

 

通用功能模块

错误码

错误码主要用于标识和处理程序运行中的各种异常情况,能够精确的指出问题所在

创建 errorcode 包,并定义错误码类型:

@Data
public class ErrorCode {
    /**
     * 错误码
     */
    private final Integer code;
    /**
     * 错误描述
     */
    private final String message;

    public ErrorCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

定义全局错误码:

public interface GlobalErrorCodeConstants {
    // 成功
    ErrorCode SUCCESS = new ErrorCode(200, "成功");

    ErrorCode BAD_REQUEST = new ErrorCode(400, "客户端请求错误");
    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "配置项错误");
    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
}

定义 controller 层业务错误码

public interface ControllerErrorCodeConstants {

}

 其中的错误码信息随着后续业务代码的完成补充

定义 service 层业务错误码

public interface ServiceErrorCodeConstants {

}

其中的错误码信息随着后续业务代码的完成补充

自定义异常类

自定义异常类是为了在程序中处理特定的错误或异常情境,使得异常处理更加清晰和灵活。通过自定义异常类,可以根据业务需求定义特定的异常类型,方便捕获和处理特定的错误

创建 exception 包,自定义异常类:

controller 层异常类:

@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException {
    /**
     * controller 层错误码
     * @see com.example.lotterysystem.common.errorcode.ControllerErrorCodeConstants
     */
    private Integer code;
    /**
     * 错误描述信息
     */
    private String message;

    /**
     * 无参构造方法,为了后续进行序列化
     */
    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.getMessage();
    }
}

service 层异常类:

@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {
    /**
     * controller 层错误码
     * @see com.example.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.getMessage();
    }
}

CommonResult<T>

CommonResult<T> 作为 控制层 方法的 返回类型,封装接口调用结果,包括成功数据、错误数据 和 状态码,可以被 SpringBoot 框架自动转化为 JSON 或其他格式的响应体,发送给客户端

定义业务处理成功和失败时返回的 CommonResult<T>

@Data
public class CommonResult<T> implements Serializable {
    /**
     * 错误码
     */
    private Integer code;
    /**
     * 返回数据
     */
    private T data;
    /**
     * 错误描述信息
     */
    private String errorMessage;
    public CommonResult() {
    }

    /**
     * 运行成功时返回结果
     * @param data
     * @return
     * @param <T>
     */
    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
        result.data = data;
        result.errorMessage = "";
        return result;
    }

    /**
     * 运行失败时返回结果
     * @param code
     * @param errorMessage
     * @return
     * @param <T>
     */
    public static <T> CommonResult<T> fail(Integer code, String errorMessage) {
        Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code),
                "code = 200, 运行成功");
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.errorMessage = errorMessage;
        return result;
    }

    /**
     * 运行失败时返回结果
     * @param errorCode
     * @return
     * @param <T>
     */
    public static <T> CommonResult<T> fail(ErrorCode errorCode) {
        return fail(errorCode.getCode(), errorCode.getMessage());
    }
}

 其中,serializable 接口是 java 提供的一个标记接口(空接口),用于指示一个类的对象可以被序列化,无需实现任何方法,定义在 java.io 包中

此外,若想在 idea 中使用断言,需要先开启断言功能,可参考:

如何开启idea中的断言功能?_idea开启断言-CSDN博客

jackson

在前后端交互的过程中,经常会使用 JSON 格式来传递数据,这也就涉及到 序列化 反序列化,此外,我们在进行日志打印时,也会涉及到序列化

因此,我们可以定义一个工具类,来专门处理 序列化

在 java 中,通常使用 ObjectMapper 来处理 Java 对象与 JSON 数据之间的转换

查看 SpringBoot 框架中是如何实现的:

不同类型的对象序列化是基本相同的,都是使用 writeValueAsString 方法来进行序列化,因此我们主要来看反序列化:

可以看到,反序列化 Map 和 List 都调用了 tryParse 方法,并传递了两个参数:一个 lambda 表达式,一个 Exception

我们继续看 tryParse 方法:

其中,最主要的方法就是 parse.call(),通过 call() 方法,来执行定义的任务

且 tryParse 方法中对异常进行了处理:

check.isAssignableFrom(var4.getClass()) 判断抛出的异常是否是传入的 check 异常,若是,则抛出 JsonParseException 异常;若不是,则抛出 IllegalStateException 异常

可以看到,框架中通过 tryParse() 方法,巧妙地对异常进行了处理

因此,我们可以借鉴上述方法来进行实现

由于只需要使用一个 ObjectMapper 实例,因此可以创建 单例 ObjectMapper

public class JacksonUtil {
    private JacksonUtil() {}

    private final static ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper();
    }

    private static ObjectMapper getObjectMapper() {
        return OBJECT_MAPPER;
    }
}

实现 tryParse 方法:

    private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
        try {
            return parser.call();
        } catch (Exception e) {
            if (check.isAssignableFrom(e.getClass())) {
                throw new JsonParseException(e);
            }
            throw new IllegalStateException(e);
        }
    }

    private static <T> T tryParse(Callable<T> parser) {
        return tryParse(parser, JsonParseException.class);
    }

实现序列化方法:

    /**
     * 序列化
     * @param value
     * @return
     */
    public static String writeValueAsString(Object value) {
        return tryParse(() -> getObjectMapper().writeValueAsString(value));
    }

反序列化: 

    /**
     * 反序列化
     * @param content
     * @param valueType
     * @return
     * @param <T>
     */
    public static <T> T readValue(String content, Class<T> valueType) {
        return tryParse(() -> {
            return getObjectMapper().readValue(content, valueType);
        });
    }

    /**
     * 反序列化 List
     * @param content
     * @param param List 中元素类型
     * @return
     */
    public static <T> T readListValue(String content, Class<?> param) {
        JavaType javaType = getObjectMapper().getTypeFactory()
                .constructParametricType(List.class, param);
        return tryParse(() -> {
            return getObjectMapper().readValue(content, javaType);
        });
    }

在对 List 类型进行反序列化时,不能直接将 List 类型传递给 valueType,而是需要构造一个 JavaType 类型

完整代码:

public class JacksonUtil {
    private JacksonUtil() {}

    private final static ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper();
    }

    private static ObjectMapper getObjectMapper() {
        return OBJECT_MAPPER;
    }

    /**
     * 序列化
     * 对象 -> 字符串
     * @param value
     * @return
     */
    public static String writeValueAsString(Object value) {
        return JacksonUtil.tryParse(() -> JacksonUtil.getObjectMapper().writeValueAsString(value));
    }

    /**
     * 反序列化
     * 字符串 -> 对象
     * @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 param
     * @return
     */
    public static <T> T readListValue(String content, Class<?> param) {
        JavaType javaType = JacksonUtil.getObjectMapper().getTypeFactory()
                .constructParametricType(List.class, param);
        return JacksonUtil.tryParse(() -> {
            return JacksonUtil.getObjectMapper().readValue(content, javaType);
        });
    }

    /**
     * 进行序列化/反序列化
     * @param parser
     * @param check
     * @return
     * @param <T>
     */
    private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
        try {
            return parser.call();
        } catch (Exception e) {
            if (check.isAssignableFrom(e.getClass())) {
                throw new JsonParseException(e);
            }
            throw new IllegalStateException(e);
        }
    }

    private static <T> T tryParse(Callable<T> parser) {
        return tryParse(parser, JsonParseException.class);
    }
}

加密工具

在对敏感信息(如密码、手机号等)进行存储时,需要进行加密,从而保证数据的安全性,若直接明文存储,当黑客入侵数据库时,就可以轻松拿到用户的相关信息,从而造成信息泄露或财产损失

在这里,可以使用 md5 对用户密码进行加密:

采用 判断哈希值是否一致 的方法来判断密码是否正确

除了密码以外,手机号也是重要的隐私数据,但手机号与密码不同:对于后端来说,不知道密码的明文也不会对业务逻辑造成影响;但后端需要明文的手机号,在一些情况下给用户发送短信

因此,对于手机号这样的信息,需要采用相对安全的做法:先对手机号进行对称加密,再将加密结果存储在数据库中;在使用时再使用密钥对其进行解密

在这里,我们使用 Hutool 的加密工具:加密解密工具-SecureUtil

引入依赖:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>

上述引入的是所有模块,但其实绝大部分项目可能都用不上,因此可以只引入需要的模块

使用对应方法进行加解密:

@SpringBootTest
public class SecurityTest {
    /**
     * 使用 aes 对用户手机号进行加密
     */
    @Test
    void test() {
        // 密钥
        String key = "1234567890abcdefghijklmn";
        AES aes = SecureUtil.aes(key.getBytes());
        String encrypt = aes.encryptHex("12345678").toString();
        System.out.println("加密结果:" +encrypt);
        String decrypt = aes.decryptStr(encrypt);
        System.out.println("解密结果:" + decrypt);
    }

    /**
     * 使用 md5 对用户密码进行加密
     */
    @Test
    void md5Test() {
        // 生成随机盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        System.out.println("salt: " + salt);
        String password = "123456";
        // 进行加密
        String encrypt = DigestUtil.md5Hex(password +salt);
        System.out.println("加密结果:" + encrypt);
    }
}

测试结果:

实现 SecurityUtil 工具类:

@Slf4j
public class SecurityUtil {
    // 密钥
    private static final String AES_KEY = "3416b730f0f244128200c59fd07e6249";
    /**
     * 使用 md5 对密码进行加密
     * @param password 输入的密码
     * @return 密码 + 盐值
     */
    public static String encipherPassword(String password) {
        String salt = UUID.randomUUID().toString().replace("-", "");
        String secretPassword = DigestUtil.md5Hex(password + salt);
        return secretPassword + salt;
    }

    /**
     * 验证用户输入的密码是否正确
     * @param inputPassword 用户输入密码
     * @param sqlPassword 数据库中存储密码
     * @return
     */
    public static Boolean verifyPassword(String inputPassword, String sqlPassword) {
        log.info("用户输入密码:{}, 数据库获取密码:{}", inputPassword, sqlPassword);
        if (!StringUtils.hasLength(inputPassword)) {
            return false;
        }
        if (!StringUtils.hasLength(sqlPassword) || sqlPassword.length() != 64) {
            return false;
        }
        String salt = sqlPassword.substring(32, 64);
        String secretPassword = DigestUtil.md5Hex(inputPassword + salt);
        return sqlPassword.substring(0, 32).equals(secretPassword);
    }

    /**
     * 使用 aes 对用户手机号进行加密
     * @param phone
     * @return
     */
    public static String encipherPhone(String phone) {
        return SecureUtil.aes(AES_KEY.getBytes()).encryptHex(phone);
    }

    /**
     * 对加密的手机号进行解密
     * @param encryptPhone
     * @return
     */
    public static String decryptPhone(String encryptPhone) {
        // log.info("解析 encryptPhone:{}", encryptPhone);
         return SecureUtil.aes(AES_KEY.getBytes()).decryptStr(encryptPhone);
    }
}

AES 使用的密钥可以使用 UUID 生成

日志配置

logback-spring.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="com.example.lotterysystem.common.filter.InfoLevelFilter"/>
            <!--滚动策略,按照时间滚动 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 级别和 info 级别的日志,并采用基于时间的日志滚动策略,设置日志保留周期为14天

由于需要过滤 info 级别的日志,因此我们需要新增一个自定义过滤器:

在 com.example.lotterysystem.common.filter 路径下新增 InfoLevelFilter

public class InfoLevelFilter extends Filter<ILoggingEvent> {

    @Override
    public FilterReply decide(ILoggingEvent iLoggingEvent) {
        if (iLoggingEvent.getLevel().toInt() == Level.INFO.toInt()){
            return FilterReply.ACCEPT;
        }
        return FilterReply.DENY;
    }
}

application.yml 中新增配置:

logging:
  config: classpath:logback-spring.xml

网站公告

今日签到

点亮在社区的每一天
去签到