基于Spring3的抽奖系统

注:项目git仓库地址:demo.lottery · 小五Z/Spring items - 码云 - 开源中国

目录

注:项目git仓库地址:demo.lottery · 小五Z/Spring items - 码云 - 开源中国

项目具体代码可参考仓库源码,本文只讲解重点代码逻辑

一、需求分析

1. 包含管理员的注册与登录。

2. 人员管理: 管理员支持创建普通用户,查看用户列表

3. 管理端支持创建奖品、奖品列表展示功能。

4. 管理端⽀持创建活动、活动列表展⽰功能。

5. 抽奖页面:

6. 通知部分: 抽奖完成需以邮件和短信⽅式通知中奖者。

二、系统设计

1.系统架构

 2.项目环境

3.数据库设计

三、项目启动

四、功能模块设计

通用处理

1.错误码

2.自定义异常类

​编辑

3.统一返回格式

​编辑

4.Jackson实现序列化和反序列化

五、用户模块

 1.注册

 1.1敏感字段加密

1.2时序图

1.3 约定前后端交互接口

1.4 Controller层代码设计

UserRegisterParam

UserIdentityEnum

 UserRegisterResult

  1.5 Service层接口设计

UserRegisterDTO

接口实现

RegexUtil

1.6 Dao层接口设计

UserDO

BaseDO 

 Encrypt

EncryptTypeHandler

1.7 全局异常捕获

2.登录

发送验证码

2.2 时序图

​编辑

2.3 配置application.properties

2.4 SMSUtil短信工具类

2.5 CaptchaUtil验证码工具类

 2.6 约定前后端交互接口

2.7 Controlller层接口设计

 2.8 Service层接口设计

2.9 Redis的配置与使用

登录 

2.10 JWT令牌

2.11 登录时序图

​编辑

2.12 约定前后端交互接口

2.13 Controller层接口设计

2.14 Service层接口设计

 2.15 Dao层接口设计

2.16 添加拦截器

3.人员列表展示

3.1 时序图

3.2 约定前后端交互接口

3.3 Controller层接口设计

3.4 Service接口设计

3.5 Dao层接口设计

六、奖品模块

1. 图片上传

application.properties 配置上传⽂件路径 

图⽚服务 PictureService 接⼝定义

2. 奖品创建

2.1 时序图

​编辑

2.2 约定前后端交互接口 

2.3 Controller层接口设计

2.4 Service层接口设计

2.5 Dao层接口设计

 3. 奖品列表展示(翻页)

 3.1 时序图

3.2 约定前后端交互接口

3.3 Controller层接口设计

 3.4 Service层接口设计

3.5 Dao层接口设计

七、活动模块

1. 活动创建

 1.1 需求回顾

 1.2 时序图

1.3 约定前后端交互接口

1.4 Controller层接口设计

1.5 Service层接口设计

 1.6 Dao层接口设计

 2.活动列表展示(翻页)

 2.1 时序图

2.2 约定前后端交互接口

2.3 Controller层接口设计

2.4 Service层接口设计 

2.5 Dao层接口设计

八、抽奖模块

1.抽奖设计

2.RabbitMQ配置使用

3.抽奖请求处理

3.1 时序图

​编辑

3.2  约定前后端交互接口

3.3 Controller层接口设计

3.4 Service层接口设计

4. MQ异步抽奖逻辑执行

4.1 时序图

​编辑

 4.2 消费MQ消息

4.3 请求验证(核对抽奖信息有效性) 

4.4 状态转换(重点)

 5. 结果记录

  5.1 时序图

5.2 Service层接口设计 

6. 邮件服务


项目具体代码可参考仓库源码,本文只讲解重点代码逻辑

一、需求分析

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. 未登录强制跳转登录页面

二、系统设计

1.系统架构

前端:使⽤JavaScript管理各界⾯的动态性,使⽤AJAX技术从后端API获取数据。
后端:采⽤Spring Boot3构建后端应⽤,实现业务逻辑。
数据库:使⽤MySQL作为主数据库,存储⽤⼾数据和活动信息。
缓存:使⽤Redis作为缓存层,减少数据库访问次数。
消息队列:使⽤RabbitMQ处理异步任务,如处理抽奖⾏为。
⽇志与安全:使⽤JWT进⾏⽤⼾认证,使⽤SLF4J+logback完成⽇志。

 2.项目环境

编程语⾔:Java(后端),JavaScript(前端)。
开发⼯具包:JDK 17 
后端框架:Spring Boot3。
数据库:MySQL。
缓存:Redis。
消息队列:RabbitMQ。 
⽇志:logback。
安全:JWT + 加密。

3.数据库设计

E-R图:

三、项目启动

代码结构设计
代码结构设计参考《阿⾥巴巴Java开发⼿册》-- 第六章 ⼯程结构

四、功能模块设计

通用处理

1.错误码

定义错误码类型:

定义全局错误码:

定义业务错误码---controller层错误:

 

2.自定义异常类

3.统一返回格式

4.Jackson实现序列化和反序列化

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;
    }

    private static <T> T tryParse(Callable<T> parser) {
        return tryParse(parser,JsonParseException.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 tryParse(()->{
           return JacksonUtil.getObjectMapper().writeValueAsString(object);
        });
    }

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

    /**
     * List集合反序列化
     * @param content
     * @param valueType
     * @return
     * @param <T>
     */
    public static <T> T readList (String content,Class<?> valueType) {
        JavaType javaType = JacksonUtil.getObjectMapper()
                .getTypeFactory()
                .constructParametricType(List.class,valueType);
        return tryParse(()->{
            return JacksonUtil.getObjectMapper().readValue(content,javaType);
        });
    }
}

五、用户模块

 1.注册

 1.1敏感字段加密

⼀般来说,用户注册时,需要输⼊其账⼾密码及手机号,服务器应该将其保存起来,⽅便后续登录验证。但仅从道德的⻆度来说,后端不应该以明文形式存储用户密码以及其他敏感信息。

从运维层⾯看,任何操作系统漏洞、基础⼯具漏洞的发⽣,都会导致密码泄露

从开发层⾯看,任何代码逻辑有漏洞、任何依赖库的漏洞都可能导致密码泄露
从管理层⾯看,任何⼀个有权限读取数据库的⼈,都能看到所有用户的密码

1.2时序图

1.3 约定前后端交互接口

[请求] /register POST
{
 "name":"张三",
 "mail":"451@qq.com",
 "phoneNumber":"13188888888",
 "password":"123456789",
 "identity":"ADMIN"
}
 
"code": 200,
 "data": {
 "userId": 22
 },
 "msg": ""
}

1.4 Controller层代码设计

@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    UserService userService;
    @Autowired
    VerificationCodeService verificationCodeService;

    /**
     * 注册
     * @param userRegisterParam
     * @return
     */
    @RequestMapping("/register")
    public CommonResult<UserRegisterResult> userRegister(
            @Validated @RequestBody UserRegisterParam userRegisterParam){
        logger.info("userRegister UserRegisterParam:{}", JacksonUtil.writeValueAsString(userRegisterParam));
        UserRegisterDTO userRegisterDTO = userService.register(userRegisterParam);
        return CommonResult.success(converToUserRegisterResult(userRegisterDTO));
    }
 private UserRegisterResult converToUserRegisterResult(UserRegisterDTO userRegisterDTO) {
        if(null == userRegisterDTO) {
            throw new ControllerException(ControllerErrorCodeConstants.REGISTER_ERROR);
        }
        UserRegisterResult result=new UserRegisterResult();
        result.setUserId(userRegisterDTO.getUserId());
        return result;
    }
}
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;

    /**
     * 身份信息
     */
    @NotBlank(message = "身份信息不能为空!")
    private String identity;
}
UserIdentityEnum
@Getter
@AllArgsConstructor
public enum UserIdentityEnum {
    ADMIN("管理员"),
    NORMAL("普通用户");

    private final String massage;

    public static UserIdentityEnum forName (String name) {
        for(UserIdentityEnum userIdentifyEnum : UserIdentityEnum.values()) {
            if(userIdentifyEnum.name().equalsIgnoreCase(name)) {
                return userIdentifyEnum;
            }
        }
        return null;
    }
}
 UserRegisterResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterResult {
    private Long userId;
}

  1.5 Service层接口设计

进行接口分离设计,这样设计有助于创建更加灵活多变的可维护,可扩展的软件系统。

public interface UserService {
    /**
     * 注册
     * @param userRegisterParam
     * @return
     */
    UserRegisterDTO register(UserRegisterParam userRegisterParam);
}
UserRegisterDTO
@Data
public class UserRegisterDTO implements Serializable {
    private Long userId;
}
接口实现
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    VerificationCodeService verificationCodeService;

    @Override
    public UserRegisterDTO register(UserRegisterParam Param) {
        checkRegisterInfo(Param);
        UserDO userDO=new UserDO();
        userDO.setUserName(Param.getName());
        userDO.setIdentity(Param.getIdentity());
        userDO.setEmail(Param.getMail());
        userDO.setPhoneNumber(new Encrypt(Param.getPhoneNumber()));
        if(StringUtils.hasText(Param.getPassword())) {
            userDO.setPassword(DigestUtil.sha256Hex(Param.getPassword()));
        }
        userMapper.insert(userDO);

        UserRegisterDTO userRegisterDTO = new UserRegisterDTO();
        userRegisterDTO.setUserId(userDO.getId());
        return userRegisterDTO;
    }
    private void checkRegisterInfo(UserRegisterParam userRegisterParam) {
        /**
         * 注册信息为空
         */
        if(null == userRegisterParam) {
            throw new ServiceException(ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);
        }
        //检验邮箱格式
        if(!RegexUtil.checkMail(userRegisterParam.getMail())) {
            throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
        }
        //检验手机号格式
        if(!RegexUtil.checkMobile(userRegisterParam.getPhoneNumber())) {
            throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
        }
        //校验身份信息
        if(null == UserIdentityEnum.forName(userRegisterParam.getIdentity())) {
            throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
        }
        //管理员密码不能为空
        
       if(userRegisterParam.getIdentity().equalsIgnoreCase(UserIdentityEnum.ADMIN.name())
                && !StringUtils.hasText(userRegisterParam.getPassword())) {
            throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);
        }
        //校验密码,至少六位
        if(StringUtils.hasText(userRegisterParam.getPassword())
                &&!RegexUtil.checkPassword(userRegisterParam.getPassword())) {
            throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
        }
        //检验邮箱是否被使用
        if(checkMailUsed(userRegisterParam.getMail())) {
            throw new ServiceException(ServiceErrorCodeConstants.MAIL_USED);
        }
        //校验手机号是否被使用
        if(checkPhoneNumberUsed(userRegisterParam.getPhoneNumber())) {
            throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_USED);
        }
    }

    private boolean checkPhoneNumberUsed(String phoneNumber) {
        int count = userMapper.countByPhoneNumber(new Encrypt(phoneNumber));
        return count > 0;
    }

    private boolean checkMailUsed(String mail) {
        int ret=userMapper.countByMail(mail);
        return ret > 0;
    }
}

其中校验⽤⼾信息,例如邮箱、电话、密码格式的内容,我们封装成了⼀个 util 来完成:

RegexUtil
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);
    }
}

 这里将手机号类型设置为自定义的Encrypt,是为了让我们存取手机号不需要每次都进行手动加密解密的复杂操作,所以我们使用MyBatis的TypeHandler来处理。

TypeHandler:简单理解就是当处理某些特定字段时,我们可以实现一些方法,让Mybatis遇到这些字段可以自动运行处理。

1.6 Dao层接口设计

@Mapper
public interface UserMapper {
    /**
     * 获取邮箱绑定的用户数
     * @param email
     * @return
     */
    @Select("select count(*) from user where email=#{email}")
    int countByMail(@Param("email") String email);

    /**
     * 获取手机号绑定的用户数
     * @param phoneNumber
     * @return
     */
    @Select("select count(*) from user where phone_number=#{phoneNumber}")
    int countByPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);

    /**
     * 注册用户
     * @param userDO
     */
    @Insert("insert into user (user_name,email,phone_number,identity,password)"+
    " values (#{userName},#{email},#{phoneNumber},#{identity},#{password})")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    void insert(UserDO userDO);
}
UserDO
@Data
@EqualsAndHashCode(callSuper = true)
public class UserDO extends BaseDO {
    //用户名
    private String userName;
    //邮箱
    private String email;
    //电话号
    private Encrypt phoneNumber;
    //密码
    private String password;
    //身份信息
    private String identity;
}
BaseDO 
@Data
public class BaseDO implements Serializable {
    //主键
    private Long id;
    //创建时间
    private Date gmtCreate;
    //修改时间
    private Date gmtModified;
}
 Encrypt
@Data
public class Encrypt {
    private String value;

    public Encrypt(){}

    public Encrypt(String value) {
        this.value = value;
    }
}
EncryptTypeHandler
@MappedTypes(Encrypt.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {
    private final byte[] KEY = "123456789abcdefg".getBytes();
    /**
     * 设置参数
     * @param ps   SQL预编译的对象
     * @param i    需要赋值的索引位置
     * @param parameter    原本i位置要赋的值
     * @param jdbcType     jdbc类型
     * @throws SQLException
     */
    @Override
    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());
        ps.setString(i,str);
    }

    /**
     * 获取值
     * @param rs       结果集合
     * @param columnName    索引名
     * @return
     * @throws SQLException
     */
    @Override
    public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {
        System.out.println("获取值得到的加密内容:" + rs.getString(columnName));
        return decrypt(rs.getString(columnName));
    }

    /**
     * 获取值
     * @param rs        结果集合
     * @param columnIndex    索引位置
     * @return
     * @throws SQLException
     */
    @Override
    public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        System.out.println("获取值得到的加密内容:" + rs.getString(columnIndex));
        return decrypt(rs.getString(columnIndex));
    }

    /**
     *
     * @param cs        结果集合
     * @param columnIndex     索引位置
     * @return
     * @throws SQLException
     */
    @Override
    public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        System.out.println("获取值得到的加密内容:" + cs.getString(columnIndex));
        return decrypt(cs.getString(columnIndex));
    }

    /**
     * 解密
     * @return
     */
    private Encrypt decrypt (String str) {
        if(!StringUtils.hasText(str)) {
            return null;
        }
        return new Encrypt(SecureUtil.aes(KEY).decryptStr(str));
    }
}
1.7 全局异常捕获
@RestControllerAdvice
public class GlobalExceptionHandler {
    private final static Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(value = ServiceException.class)
    public CommonResult<?> serviceException (ServiceException e) {
        log.error("ServiceException:",e);

        return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()
                , e.getMassage());
    }

    @ExceptionHandler(value = ControllerException.class)
    public CommonResult<?> controllerException (ControllerException e) {
        log.error("ControllerException:",e);

        return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()
                , e.getMassage());
    }

    @ExceptionHandler(value = Exception.class)
    public CommonResult<?> exception (Exception e) {
        log.error("服务异常:",e);

        return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR);
    }
}

2.登录

发送验证码

在我们的登录设计中,有⼀种登录⽅式为短信验证码登录,那么在登录前,就需要先获取验证码。验证码服务使⽤阿⾥云提供的短信服务来完成。阿里云的短信服务需要自己在阿里云申请以及购买,这边只注重于逻辑实现。

2.2 时序图

2.3 配置application.properties

## 短信 ##
sms.access-key-id=填写⾃⼰申请的
sms.access-key-secret=填写⾃⼰申请的
sms.sign-name=填写⾃⼰申请的

如果不想申请短信服务可以直接砍掉这个功能

2.4 SMSUtil短信工具类

@Component
public class SMSUtil {
    private static final Logger logger = LoggerFactory.getLogger(SMSUtil.class);

    @Value(value = "${sms.sign-name}")
    private String signName;
    @Value(value = "${sms.access-key-id}")
    private String accessKeyId;
    @Value(value = "${sms.access-key-secret}")
    private String accessKeySecret;

    /**
     * 发送短信
     *
     * @param templateCode 模板号
     * @param phoneNumbers 手机号
     * @param templateParam 模板参数 {"key":"value"}
     */
    public void sendMessage(String templateCode, String phoneNumbers, String templateParam) {
        try {
            Client client = createClient();
            com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new SendSmsRequest()
                    .setSignName(signName)
                    .setTemplateCode(templateCode)
                    .setPhoneNumbers(phoneNumbers)
                    .setTemplateParam(templateParam);
            RuntimeOptions runtime = new RuntimeOptions();
            SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
            if (null != response.getBody()
                    && null != response.getBody().getMessage()
                    && "OK".equals(response.getBody().getMessage())) {
                logger.info("向{}发送信息成功,templateCode={}", phoneNumbers, templateCode);
                return;
            }
            logger.error("向{}发送信息失败,templateCode={},失败原因:{}",
                    phoneNumbers, templateCode, response.getBody().getMessage());
        } catch (TeaException error) {
            logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
        } catch (Exception _error) {
            TeaException error = new TeaException(_error.getMessage(), _error);
            logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
        }
    }

    /**
     * 使用AK&SK初始化账号Client
     * @return Client
     */
    private Client createClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。
        Config config = new Config()
                .setAccessKeyId(accessKeyId)
                .setAccessKeySecret(accessKeySecret);
        // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
        config.endpoint = "dysmsapi.aliyuncs.com";
        return new Client(config);
    }

}

2.5 CaptchaUtil验证码工具类

依靠Hutool中的随机验证码功能实现。

public class CaptchaUtil {
    /**
     * 生成随机验证码
     *
     * @param length  几位
     * @return
     */
    public static String getCaptcha(int length) {
        // 自定义纯数字的验证码(随机4位数字,可重复)
        RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
        LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
        lineCaptcha.setGenerator(randomGenerator);
        // 重新生成code
        lineCaptcha.createCode();
        return lineCaptcha.getCode();
    }
}

 2.6 约定前后端交互接口

[请求] /verification-code/send?phoneNumber=13199999999 GET
[响应] 
{
 "code": 200,
 "data": true,
 "msg": ""
}

2.7 Controlller层接口设计

@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    UserService userService;
    @Autowired
    VerificationCodeService verificationCodeService;

  

    /**
     * 发送验证码
     * @param phoneNumber
     * @return
     */
    @RequestMapping("/verification-code/send")
    public CommonResult<Boolean> sendVerificationCode(String phoneNumber) {
        logger.info("userRegister UserRegisterParam:{}", phoneNumber);
        verificationCodeService.sendVerificationCodd(phoneNumber);
        return CommonResult.success(Boolean.TRUE);
    }

    
}

 2.8 Service层接口设计

public interface VerificationCodeService {
    /**
     * 发送验证码
     * @param phoneNumber
     */
    void sendVerificationCodd(String phoneNumber);

    /**
     * 获取验证码
     * @param phoneNumber
     * @return
     */
    String getVerificationCode(String phoneNumber);
}

 接口实现

@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
    @Autowired
    SMSUtil smsUtil;

    @Autowired
    RedisUtil redisUtil;

    private static final String VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE_";

    private static final Long VERIFICATION_CODE_TIMEOUT = 60L;

    private static final String VERIFICATION_CODE_TEMPLATE_CODE = "SMS_476785514";
    @Override
    public void sendVerificationCodd(String phoneNumber) {
        if(!RegexUtil.checkMobile(phoneNumber)) {
            throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
        }
        //生成随机验证码
        String code = CaptchaUtil.getCaptcha(4);

        Map<String,String> map = new HashMap<>();
        map.put("code",code);

        smsUtil.sendMessage(VERIFICATION_CODE_TEMPLATE_CODE,phoneNumber,
                JacksonUtil.writeValueAsString(map));

        redisUtil.set(VERIFICATION_CODE_PREFIX+phoneNumber,code,VERIFICATION_CODE_TIMEOUT);
    }

    @Override
    public String getVerificationCode(String phoneNumber) {
        if (!RegexUtil.checkMobile(phoneNumber)) {
            throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
        }

        return redisUtil.get(VERIFICATION_CODE_PREFIX+phoneNumber);
    }
}

2.9 Redis的配置与使用

配置 application.properties:

## redis ##
spring.data.redis.host=localhost
spring.data.redis.port=8888
spring.data.redis.timeout=60s
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=5s

这里博主的Redis客户端部署在了云服务器上,配置了隧道。 剩下的配置信息含义分别为:

连接的超时时间为60s,Redis连接池中允许的最大活动连接数为 8,同时存在的Redis连接池中最大空闲量为8,最小为0,当连接池耗尽时,新的请求将等待可用连接的最大时间为 5 秒。

redis工具类编写:

@Configuration
public class RedisUtil {
    /**
     * RedisTemplate :  先将被存储的数据转换成 字节数组(不可读),再存储到redis中,读取的时候按照字节数组读取
     * StringRedisTemplate : 直接存放的就是 string (可读)
     * 项目背景:String,String
     */
    private final static Logger logger = LoggerFactory.getLogger(RedisUtil.class);

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    //---------String-------------

    /**
     * 设置值
     * @param key
     * @param value
     * @return
     */
    public boolean set(String key , String value) {
        try {
            stringRedisTemplate.opsForValue().set(key , value);
            return true;
        }catch (Exception e) {
            logger.error("RedisUtil error, set({},{})",key,value,e);
            return false;
        }
     }

    /**
     * 设置带有时限的值
     * @param key
     * @param value
     * @param time
     * @return
     */
    public boolean set(String key, String value ,Long time) {
        try {
            stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            return true;
        }catch (Exception e) {
            logger.error("RedisUtil error, set({},{},{})",key,value,time,e);
            return false;
        }
    }

    /**
     * 获取值
     * @param key
     * @return
     */
    public String get(String key) {
        try {
            return StringUtils.hasText(key) ? stringRedisTemplate.opsForValue().get(key) : null;
        }catch (Exception e) {
            logger.error("RedisUtil error, get({})",key,e);
            return null;
        }
    }

    /**
     * 删除元素
     * @param key
     * @return
     */
    public boolean del(String... key) {
        try {
            if (null!=key&&key.length>0) {
                if(key.length==1) {
                    stringRedisTemplate.delete(key[0]);
                }else {
                    stringRedisTemplate.delete(
                            (Collection<String>)CollectionUtils.arrayToList(key));
                }
            }
            return true;
        }catch (Exception e) {
            logger.error("RedisUtil error, del({})", key, e);
            return false;
        }
    }

    public boolean hasKey(String key) {
        try {
            return StringUtils.hasText(key) ?
                    stringRedisTemplate.hasKey(key) : false;
        }catch (Exception e) {
            logger.error("RedisUtil error, hasKey({})", key, e);
            return false;
        }
    }
}

登录 

2.10 JWT令牌

JWT工具类:

public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
    /**
     * 密钥:Base64编码的密钥
     */
    private static final String SECRET = "SDKltwTl3SiWX62dQiSHblEB6O03FG9/vEaivFu6c6g=";
    /**
     * 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
     */
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
            Decoders.BASE64.decode(SECRET));
    /**
     * 过期时间(单位: 毫秒)
     */
    private static final long EXPIRATION = 60*60*1000;

    /**
     * 生成密钥
     *
     * @param claim  {"id": 12, "name":"张山"}
     * @return
     */
    public static String genJwt(Map<String, Object> claim){
        //签名算法
        String jwt = Jwts.builder()
                .setClaims(claim)             // 自定义内容(载荷)
                .setIssuedAt(new Date())      // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
                .signWith(SECRET_KEY)         // 签名算法
                .compact();
        return jwt;
    }

    /**
     * 验证密钥
     */
    public static Claims parseJWT(String jwt){
        if (!StringUtils.hasLength(jwt)){
            return null;
        }
        // 创建解析器, 设置签名密钥
        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
        Claims claims = null;
        try {
            //解析token
            claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
        }catch (Exception e){
            // 签名验证失败
            logger.error("解析令牌错误,jwt:{}", jwt, e);
        }
        return claims;

    }

    /**
     * 从token中获取用户ID
     */
    public static Integer getUserIdFromToken(String jwtToken) {
        Claims claims = JWTUtil.parseJWT(jwtToken);
        if (claims != null) {
            Map<String, Object> userInfo = new HashMap<>(claims);
            return (Integer) userInfo.get("userId");
        }
        return null;
    }
}

2.11 登录时序图

2.12 约定前后端交互接口

密码登录
[请求] /password/login POST
{
 "loginName":"13199999999",
 "password":"123456",
 "mandatoryIdentity":"ADMIN"
}
[响应] 
{
 "code": 200,
 "data": {
 "token": 
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjI5OCwiZXhwIjoxNzE2MjY0MDk4fQ.QfiZmZcfzd5ls_t8lg7bsTF7kA0daK-psjUt1QRj9d4",
 "identity": "ADMIN"
 },
 "msg": ""
}
验证码登录
[请求] /message/login POST
{
 "loginMobile":"13199999999",
 "verificationCode":"0475",
 "mandatoryIdentity":"ADMIN"
}
[响应] 
{
 "code": 200,
 "data": {
 "token": 
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjUyMywiZXhwIjoxNzE2MjY0MzIzfQ.XEuwO8AvNcqstbOrkI9kWaMhbN-HN2DfnUYGhJthA3I",
 "identity": "ADMIN"
 },
 "msg": ""
}

2.13 Controller层接口设计

@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    UserService userService;
    @Autowired
    VerificationCodeService verificationCodeService;
    @RequestMapping("/password/login")
    public CommonResult<UserLoginResult> userPasswordLogin (
           @Validated @RequestBody UserPasswordLoginParam param) {
        logger.info("UserController userPasswordLogin({})"
                ,JacksonUtil.writeValueAsString(param));
        UserLoginDTO userLoginDTO = userService.login(param);
        return CommonResult.success(converToUserLoginResult(userLoginDTO));
    }

    @RequestMapping("/message/login")
    public CommonResult<UserLoginResult> shortMessageLogin (
            @Validated @RequestBody ShortMessageLoginParam param) {
        logger.info("UserController shortMessageLogin({})"
                ,JacksonUtil.writeValueAsString(param));
        UserLoginDTO userLoginDTO = userService.login(param);
        return CommonResult.success(converToUserLoginResult(userLoginDTO));
    }
    private UserLoginResult converToUserLoginResult(UserLoginDTO userLoginDTO) {
        if(null==userLoginDTO) {
            throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);
        }
        UserLoginResult result = new UserLoginResult();
        result.setToken(userLoginDTO.getToken());
        result.setIdentity(userLoginDTO.getIdentity().name());
        return result;
    }

 UserLoginParam

@Data
public class UserLoginParam implements Serializable {
    /**
     * 强制某身份登录,不填不限制身份
     * @see UserIdentityEnum#name()
     */
     private String mandatoryIdentity;
}

ShortMessageLoginParam: 

@Data
@EqualsAndHashCode(callSuper = true)
public class ShortMessageLoginParam extends UserLoginParam{
    /**
     * 电话
     */
    @NotBlank(message = "电话不能为空!")
    private String loginMobile;

    /**
     * 验证码
     */
    @NotBlank(message = "验证码不能为空!")
    private String verificationCode;

}

UserPasswordLoginParam: 

@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
    /**
     * 手机或邮箱
     */
    @NotBlank(message = "手机或邮箱不能为空!")
    private String loginName;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空!")
    private String password;
}

UserLoginResult :

@Data
public class UserLoginResult implements Serializable {
    /**
     * jwt令牌
     */
    private String token;

    /**
     * 用户身份信息
     */
    private String identity;
}

2.14 Service层接口设计

public interface UserService {
 

    /**
     * 登录
     * @param param
     * @return
     */
    UserLoginDTO login(UserLoginParam param);
}

接口实现: 

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    VerificationCodeService verificationCodeService;
    @Override
    public UserLoginDTO login(UserLoginParam param) {
        UserLoginDTO userLoginDTO = null;
        if(param instanceof UserPasswordLoginParam loginParam) {
            //密码登录
            userLoginDTO = loginByUserPassword(loginParam);
        } else if (param instanceof ShortMessageLoginParam loginParam) {
            //验证码登录
            userLoginDTO = loginByShortMessage(loginParam);
        }else {
            throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXIST);
        }
        return userLoginDTO;
    }
    /**
     * 验证码登录
     * @param loginParam
     * @return
     */
    private UserLoginDTO loginByShortMessage(ShortMessageLoginParam loginParam) {
        if(!RegexUtil.checkMobile(loginParam.getLoginMobile())) {
            throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
        }
        //获取用户数据
        UserDO userDO = userMapper.selectByPhoneNumber(
                new Encrypt(loginParam.getLoginMobile()));
        if(null == userDO) {
            throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
        } else if (StringUtils.hasText(userDO.getIdentity())
        &&!userDO.getIdentity().equalsIgnoreCase(loginParam.getVerificationCode())) {
            throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
        }
        //校验验证码
        String code = verificationCodeService.getVerificationCode(
                loginParam.getLoginMobile());

        if (!loginParam.getVerificationCode().equals(code)) {
            throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);
        }

        // 塞入返回值(JWT)
        Map<String, Object> claim = new HashMap<>();
        claim.put("id", userDO.getId());
        claim.put("identity", userDO.getIdentity());
        String token = JWTUtil.genJwt(claim);

        UserLoginDTO userLoginDTO = new UserLoginDTO();
        userLoginDTO.setToken(token);
        userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
        return userLoginDTO;
    }

    /**
     * 密码登录
     * @param loginParam
     * @return
     */
    private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
        UserDO userDO = null;
        if(RegexUtil.checkMobile(loginParam.getLoginName())) {
            //手机号登录
            userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
        } else if (RegexUtil.checkMail(loginParam.getLoginName())) {
            //邮箱登录
            userDO = userMapper.selectByMail(loginParam.getLoginName());
        }else {
            throw new ServiceException(ServiceErrorCodeConstants.LOGIN_NOT_EXIST);
        }
        //校验登录信息
        if(null==userDO) {
            throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
        } else if (StringUtils.hasText(userDO.getIdentity())
                &&!loginParam.getMandatoryIdentity()
                .equalsIgnoreCase(userDO.getIdentity())) {
            //校验身份信息不通过
            throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
        } else if (!DigestUtil.sha256Hex(loginParam.getPassword())
                .equals(userDO.getPassword())) {
            //校验密码不通过
            throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
        }
        //生成jwt
        Map<String, Object> claim = new HashMap<>();
        claim.put("id", userDO.getId());
        claim.put("identity", userDO.getIdentity());
        String token = JWTUtil.genJwt(claim);

        UserLoginDTO userLoginDTO = new UserLoginDTO();
        userLoginDTO.setToken(token);
        userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
        return userLoginDTO;
    }
}

UserLoginDTO: 

@Data
public class UserLoginDTO implements Serializable {
    /**
     * JWT 令牌
     */
    private String token;

    /**
     * 登录人员身份
     */
    private UserIdentityEnum identity;
}

 2.15 Dao层接口设计

@Mapper
public interface UserMapper {
    @Select("select * from user where phone_number=#{phoneNumber}")
    UserDO selectByPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);

    @Select("select * from user where email=#{email}")
    UserDO selectByMail(@Param("email") String email);
}

2.16 添加拦截器

采⽤拦截器来完成校验 token 的合法性 

@Component
public class LoginInterceptor implements HandlerInterceptor {
    private final static Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);

    /**
     * 预处理,请求之前调用
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request
            , HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("user_token");
        logger.info("请求token:{}",token);
        logger.info("请求路径:{}",request.getPathInfo());
        //解析token
        Claims claims = JWTUtil.parseJWT(token);
        if(null==claims) {
            logger.error("解析失败!");
            return false;
        }
        logger.info("解析成功");
        return true;
    }
}

添加配置类 :

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    LoginInterceptor loginInterceptor;

    private final List<String> excludes = Arrays.asList(
            "/**/*.html",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/*.jpg",
            "/*.png",
            "/favicon.ico",
            "/**/login",
            "/register",
            "/verification-code/send",
            "/winning-records/show"
    );


    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludes);
    }
}

3.人员列表展示

3.1 时序图

3.2 约定前后端交互接口

[请求] /base-user/find-list GET
[响应] 
{
 "code": 200,
 "data": [
 {
 "userId": 15,
 "userName": "郭靖",
 "identity": "NORMAL"
 },
 {
 "userId": 14,
 "userName": "王五",
 "identity": "ADMIN"
 }
 ],
 "msg": ""
}

3.3 Controller层接口设计

@RestController
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    UserService userService;
    @Autowired
    VerificationCodeService verificationCodeService;
    @RequestMapping("/base-user/find-list")
    public CommonResult<List<BaseUserInfoResult>> findBaseUserInfo(String identity) {
        logger.info("UserController findBaseUSerInfo({})",identity);
        List<UserDTO> userDTOList = userService.findUserInfo(
                UserIdentityEnum.forName(identity));
        return CommonResult.success(converToList(userDTOList));
    }

    private List<BaseUserInfoResult> converToList(List<UserDTO> userDTOList) {
        return userDTOList.stream()
                .map(userDTO -> {
                    BaseUserInfoResult result = new BaseUserInfoResult();
                    result.setUserName(userDTO.getUserName());
                    result.setIdentity(userDTO.getIdentity().name());
                    result.setUserId(userDTO.getUserId());
                    return result;
                }).collect(Collectors.toList());
    }
}

UserBaseInfoResult:

@Data
public class BaseUserInfoResult {
    /**
     * 人员id
     */
    private Long userId;

    /**
     * 姓名
     */
    private String userName;

    /**
     * 身份信息
     */
    private String identity;

}

3.4 Service接口设计

public interface UserService {
 /**
     * 查找人员列表
     * @param identity
     * @return
     */
    List<UserDTO> findUserInfo(UserIdentityEnum identity);
}

接口实现:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    VerificationCodeService verificationCodeService;
    @Override
    public List<UserDTO> findUserInfo(UserIdentityEnum identity) {
        String identityString = null == identity ? null : identity.name();
        List<UserDO> userDOList = userMapper.selectUserListByIdentity(identityString);
        List<UserDTO> userDTOList = userDOList.stream()
                .map(userDO ->{
                    UserDTO userDTO = new UserDTO();
                    userDTO.setUserName(userDO.getUserName());
                    userDTO.setUserId(userDO.getId());
                    userDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
                    userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
                    userDTO.setEmail(userDO.getEmail());
                    return userDTO;
                        }).collect(Collectors.toList());
        return userDTOList;
    }
}

3.5 Dao层接口设计

@Mapper
public interface UserMapper {
    @Select("<script>" +
            " select * from user" +
            " <if test=\"identity!=null\">" +
            "    where identity = #{identity}" +
            " </if>" +
            " order by id desc" +
            " </script>")
    List<UserDO> selectUserListByIdentity(@Param("identity") String identity);
}

六、奖品模块

1. 图片上传

application.properties 配置上传⽂件路径 

## 图⽚服务 ##
pic.local-path=D:/PIC
# spring boot3 升级配置名
spring.web.resources.static-locations=classpath:/static/,file:${pic.local-path}

图⽚服务 PictureService 接⼝定义

public interface PictureService {

    /**
     * 保存图片
     * @param multipartFile  :上传文件的工具类
     * @return  索引  上传后的文件名
     */

    String savePicture (MultipartFile multipartFile);
}

接口实现:

@Service
public class PictureServiceImpl implements PictureService {

    @Value("${pic.local-path}")
    private String localPath;

    @Override
    public String savePicture(MultipartFile multipartFile) {
        //创建目录
        File dir = new File(localPath);
        if(!dir.exists()) {
            dir.mkdirs();
        }

        //创建索引
        String filename=multipartFile.getOriginalFilename();
        assert null!=filename;
        String suffix = filename.substring(filename.lastIndexOf("."));
        filename = UUID.randomUUID() +suffix;

        //图片保存

        try {
            multipartFile.transferTo(new File(localPath + "/" + filename));
        } catch (IOException e) {
            throw new ServiceException(ServiceErrorCodeConstants.PIC_UPLOAD_ERROR);
        }
        return filename;
    }
}

二进制文件上传配置类:

@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        // MediaType.APPLICATION_OCTET_STREAM 表示这个转换器用于处理二进制流数据,通常用于文件上传。
        super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        // 转换器不用于写入(即不用于响应的序列化)
        return false;
    }

    @Override
    public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    protected boolean canWrite(MediaType mediaType) {
        return false;
    }
}

2. 奖品创建

2.1 时序图

2.2 约定前后端交互接口 

[请求] /prize/create POST
param: {"prizeName":"吹⻛机","description":"吹⻛机","price":100}
prizePic: Obj-C.jpg (FILE) 
[响应] 
{
 "code": 200,
 "data": 17,
 "msg": ""
}

2.3 Controller层接口设计

@RestController
public class PrizeController {
    private final static Logger logger = LoggerFactory.getLogger(PrizeController.class);

    @Autowired
    private PrizeService prizeService;

    @Autowired
    private PictureService pictureService;

    @RequestMapping("/pic/upload")
    public String uploadPic(MultipartFile multipartFile) {
        return pictureService.savePicture(multipartFile);
    }

    /**
     * 创建奖品
     *  RequestPart:用于接收表单数据的 multipart/form-data
     *
     * @param param
     * @param multipartFile
     * @return
     */
    @RequestMapping("/prize/create")
    public CommonResult<Long> createPrize(@Validated @RequestPart("param") CreatePrizeParam param
            ,@RequestPart("prizePic") MultipartFile multipartFile) {
        logger.info("PrizeController createPrize({})"
                , JacksonUtil.writeValueAsString(param));
        return CommonResult.success(prizeService.createPrize(param,multipartFile));
    }
}

CreatePrizeParam:

@Data
public class CreatePrizeParam implements Serializable {
    /**
     * 奖品名
     */
    @NotBlank(message = "奖品名不能为空!")
    private String prizeName;
    /**
     * 描述
     */
    private String description;
    /**
     * 价格
     */
    @NotNull(message = "奖品价格不能为空!")
    private BigDecimal price;
}

2.4 Service层接口设计

public interface PrizeService {
 /**
     * 创建奖品
     * @param param
     * @param picFile
     * @return
     */
    Long createPrize(CreatePrizeParam param, MultipartFile picFile);
}

接口实现:

@Service
public class PrizeServiceImpl implements PrizeService {
    @Autowired
    PictureService pictureService;

    @Autowired
    PrizeMapper prizeMapper;
    @Override
    public Long createPrize(CreatePrizeParam param, MultipartFile picFile) {
        //上传图片
        String fileName = pictureService.savePicture(picFile);
        //存表
        PrizeDO prizeDO = new PrizeDO();
        prizeDO.setName(param.getPrizeName());
        prizeDO.setDescription(param.getDescription());
        prizeDO.setImageUrl(fileName);
        prizeDO.setPrice(param.getPrice());
        prizeMapper.insert(prizeDO);
        return prizeDO.getId();
    }
}

2.5 Dao层接口设计

@Mapper
public interface PrizeMapper {

    @Insert("insert into prize (name,description,price,image_url) " +
            "values (#{name},#{description},#{price},#{imageUrl})")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    void insert(PrizeDO prizeDO);
}

PrizeDO:

@Data
@EqualsAndHashCode(callSuper = true)
public class PrizeDO extends BaseDO{
    /**
     * 奖品名
     */
    private String name;

    /**
     * 图片索引
     */
    private String imageUrl;

    /**
     * 价格
     */
    private BigDecimal price;

    /**
     * 描述
     */
    private String description;
}

 3. 奖品列表展示(翻页)

 3.1 时序图

3.2 约定前后端交互接口

[请求] /prize/find-list?currentPage=1&pageSize=10 GET
[响应] 
{
 "code": 200,
 "data": {
 "total": 3,
 "records": [
 {
 "prizeId": 17,
 "prizeName": "吹⻛机",
 "description": "吹⻛机",
 "price": 100,
 "imageUrl": "d11fa79c-9cfb-46b9-8fb6-3226ba1ff6d6.jpg"
 },
 {
 "prizeId": 13,
 "prizeName": "华为⼿机",
 "description": "华为⼿机",
 "price": 5000,
 "imageUrl": "5a85034b-91b7-48fe-953d-67aef2bdcc2d.jpg"
 },
 {
 "prizeId": 12,
 "prizeName": "咖啡机",
 "description": "家⽤咖啡机",
 "price": 3000,
 "imageUrl": "https://ts1.cn.mm.bing.net/th/id/RC.59493f741a4d956f354d241ec1034624?
rik=JpdNO%2bfC3NMONw&riu=http%3a%2f%2fcdn02.ehaier.com%2fproduct%2f5600fa6c1a0a
2ebc278b47e8_1200_1200.jpg&ehk=8MptQ5r5ILWiL4v%2f5mn3s0%2f1H05r1yp%2fL6feezFw89
Q%3d&risl=&pid=ImgRaw&r=0"
 }
 ]
 },
 "msg": ""
}

3.3 Controller层接口设计

@RestController
public class PrizeController {
    private final static Logger logger = LoggerFactory.getLogger(PrizeController.class);

    @Autowired
    private PrizeService prizeService;

    @Autowired
    private PictureService pictureService;
 @RequestMapping("/prize/find-list")
    public CommonResult<FindPrizeListResult> findPrizeList(PageParam param) {
        logger.info("PrizeController findPrizeList({})", param);
        PageListDTO<PrizeDTO> pageListDTO = prizeService.findPrizeList(param);
        return CommonResult.success(convertToFindPrizeListResult(pageListDTO));
    }

    private FindPrizeListResult convertToFindPrizeListResult(PageListDTO<PrizeDTO> pageListDTO) {
        if(null == pageListDTO) {
            throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
        }
        FindPrizeListResult result = new FindPrizeListResult();
        result.setTotal(pageListDTO.getTotal());
        result.setRecords(
                pageListDTO.getRecords().stream()
                        .map(prizeDTO -> {
                            FindPrizeListResult.PrizeInfo prizeInfo = new FindPrizeListResult.PrizeInfo();
                            prizeInfo.setPrizeName(prizeDTO.getName());
                            prizeInfo.setPrizeId(prizeDTO.getPrizeId());
                            prizeInfo.setDescription(prizeDTO.getDescription());
                            prizeInfo.setPrice(prizeDTO.getPrice());
                            return prizeInfo;
                        }).collect(Collectors.toList())
        );
        return result;
    }

}

PageParam:

@Data
public class PageParam implements Serializable {
    /**
     * 当前页
     */
    private Integer currentPage = 1;

    /**
     * 需要展示的数据数
     */
    private Integer pageSize = 10;

    public Integer getOffset() {
        return (currentPage-1)*pageSize;
    }
}

 FindPrizeListResult:

@Data
public class FindPrizeListResult implements Serializable {
    /**
     * 总量
     */
    private Integer total;

    /**
     * 当前列表
     */
    private List<PrizeInfo> records;

    @Data
    public static class PrizeInfo implements Serializable {

        /**
         * 奖品id
         */
        private Long prizeId;

        /**
         * 名称
         */
        private String prizeName;

        /**
         * 描述
         */
        private String description;

        /**
         * 价值
         */
        private BigDecimal price;

        /**
         * 奖品图
         */
        private String imageUrl;

    }
}

 3.4 Service层接口设计

public interface PrizeService {


    /**
     * 创建奖品
     * @param param
     * @param picFile
     * @return
     */
    Long createPrize(CreatePrizeParam param, MultipartFile picFile);
}

接口实现:

@Service
public class PrizeServiceImpl implements PrizeService {
    @Autowired
    PictureService pictureService;

    @Autowired
    PrizeMapper prizeMapper;

    @Override
    public PageListDTO<PrizeDTO> findPrizeList(PageParam param) {

        //总量
        int total = prizeMapper.count();
        //查询当前列表
        List<PrizeDTO> prizeDTOList = new ArrayList<>();
        List<PrizeDO> prizeDOList = prizeMapper.selectPrizeList(param.getOffset()
                ,param.getPageSize());
        for (PrizeDO prizeDO : prizeDOList) {
            PrizeDTO prizeDTO = new PrizeDTO();
            prizeDTO.setPrizeId(prizeDO.getId());
            prizeDTO.setName(prizeDO.getName());
            prizeDTO.setDescription(prizeDO.getDescription());
            prizeDTO.setImageUrl(prizeDO.getImageUrl());
            prizeDTO.setPrice(prizeDO.getPrice());
            prizeDTOList.add(prizeDTO);
        }
        return new PageListDTO<>(total, prizeDTOList);
    }
}

PrizeDTO:

@Data
public class PrizeDTO {
    /**
     * 奖品Id
     */
    private Long prizeId;
    /**
     * 奖品名
     */
    private String name;

    /**
     * 图片索引
     */
    private String imageUrl;

    /**
     * 价格
     */
    private BigDecimal price;

    /**
     * 描述
     */
    private String description;

}

PageListDTO <T>:

@Data
@AllArgsConstructor
public class PageListDTO <T>{
    /**
     * 总量
     */
    private Integer total;

    /**
     * 当前页列表
     */
    private List<T> records;
}

3.5 Dao层接口设计

@Mapper
public interface PrizeMapper {


    @Select("select count(*) from prize")
    int count();

    @Select("select * from prize order by id desc limit #{offset},#{pageSize}")
    List<PrizeDO> selectPrizeList(@Param("offset") Integer offset
            ,@Param("pageSize") Integer pageSize);
}

七、活动模块

1. 活动创建

 1.1 需求回顾

   创建的活动信息包含:活动名称 , 活动描述 , 关联的一批奖品同时需要选择奖品等级(一二三等奖)以及奖品库存 , 圈选一批人员参与抽奖。

 1.2 时序图

  

1.3 约定前后端交互接口

[请求] /activity/create POST
{
 "activityName": "抽奖测试",
 "description": "年会抽奖活动",
 "activityPrizeList": [
 {
 "prizeId": 13,
 "prizeAmount": 1,
 "prizeTiers": "FIRST_PRIZE"
 },
 {
"prizeId": 12,
 "prizeAmount": 1,
 "prizeTiers": "SECOND_PRIZE"
 }
 ],
 "activityUserList": [
 {
 "userId": 25,
 "userName": "郭靖"
 },
 {
 "userId": 23,
 "userName": "杨康"
 }
 ]
}
[响应] 
{
 "code": 200,
 "data": {
 "activityId": 23
 },
 "msg": ""
}

1.4 Controller层接口设计

@RestController
public class ActivityController {
    private final static Logger logger = LoggerFactory.getLogger(ActivityController.class);

    @Autowired
    ActivityService activityService;
    /**
     * 创建活动
     *
     * @param param
     * @return
     */
    @RequestMapping("/activity/create")
    public CommonResult<CreateActivityResult> createActivity(
            @Validated @RequestBody CreateActivityParam param) {
        logger.info("ActivityController createActivity({})"
                , JacksonUtil.writeValueAsString(param));
        return CommonResult.success(
                convertToCreateActivityResult(activityService.createActivity(param)));
    }
 private CreateActivityResult convertToCreateActivityResult(CreateActivityDTO activity) {
        if(null == activity) {
            throw new 
   ControllerException(ControllerErrorCodeConstants.CREATE_ACTIVITY_ERROR);
        }
        CreateActivityResult result = new CreateActivityResult();
        result.setActivityId(activity.getActivityId());
        return result;
    }
}
CreateActivityParam:

@Data
public class CreateActivityParam implements Serializable {
    /**
     * 活动名称
     */
    @NotBlank(message = "活动名称不能为空!")
    private String activityName;
    /**
     * 描述
     */
    @NotBlank(message = "活动描述不能为空!")
    private String description;
    /**
     * 活动关联奖品列表
     */
    @NotEmpty(message = "活动关联奖品列表不能为空!")
    @Valid
    private List<CreatePrizeByActivityParam> activityPrizeList;
    /**
     * 活动关联人员列表
     */
    @NotEmpty(message = "活动关联人员列表不能为空!")
    @Valid
    private List<CreateUserByActivityParam> activityUserList;
}

CreateActivityResult:

@Data
public class CreateActivityResult implements Serializable {
    /**
     * 创建的活动id
     */
    private Long activityId;
}

1.5 Service层接口设计

public interface ActivityService {
    /**
     * 创建活动
     * @param param
     * @return
     */
    CreateActivityDTO createActivity(CreateActivityParam param);
}

接口实现:

@Service
public class ActivityServiceImpl implements ActivityService {
    /**
     * 活动缓存前置,为了区分业务
     */
    private final String ACTIVITY_PREFIX = "ACTIVITY_";
    /**
     * 活动缓存过期时间
     */
    private final Long ACTIVITY_TIMEOUT = 60 * 60 * 24 * 3L;

    Logger logger = LoggerFactory.getLogger(ActivityServiceImpl.class);
    @Autowired
    RedisUtil redisUtil;


    @Autowired
    ActivityMapper activityMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public CreateActivityDTO createActivity(CreateActivityParam param) {
        //校验活动信息是否正确
        checkActivityInfo(param);

        //保存活动信息
        ActivityDO activityDO = new ActivityDO();
        activityDO.setActivityName(param.getActivityName());
        activityDO.setDescription(param.getDescription());
        activityDO.setStatus(ActivityStatusEnum.RUNNING.name());
        activityMapper.insert(activityDO);

        //保存相关联的奖品信息
        List<CreatePrizeByActivityParam> prizeByActivityParam = param.getActivityPrizeList();
        List<ActivityPrizeDO> prizeDOList = prizeByActivityParam.stream()
                .map(prizeParam -> {
                    ActivityPrizeDO result = new ActivityPrizeDO();
                    result.setActivityId(activityDO.getId());
                    result.setPrizeId(prizeParam.getPrizeId());
                    result.setPrizeAmount(prizeParam.getPrizeAmount());
                    result.setPrizeTiers(prizeParam.getPrizeTiers());
                    result.setStatus(ActivityPrizeStatusEnum.INIT.name());
                    return result;
                }).collect(Collectors.toList());
        activityPrizeMapper.batchInsert(prizeDOList);

        //保存相关联的人员信息
        List<CreateUserByActivityParam> userByActivityParam = param.getActivityUserList();
        List<ActivityUserDO> userDOList = userByActivityParam.stream()
                .map(userParam -> {
                    ActivityUserDO result = new ActivityUserDO();
                    result.setActivityId(activityDO.getId());
                    result.setUserId(userParam.getUserId());
                    result.setUserName(userParam.getUserName());
                    result.setStatus(ActivityUserStatusEnum.INIT.name());
                    return result;
                }).collect(Collectors.toList());
        activityUserMapper.batchInsert(userDOList);

        //整合相关活动信息,存放redis
        List<Long> prizeIds = param.getActivityPrizeList().stream()
                .map(CreatePrizeByActivityParam::getPrizeId).collect(Collectors.toList());
        List<PrizeDO> prizeDOList1 = prizeMapper.batchSelectByIds(prizeIds);
        ActivityDetailDTO detailDTO = convertToActivityDetailDTO(activityDO, userDOList,
                prizeDOList1, prizeDOList);
        cacheActivity(detailDTO);

        //构造返回
        CreateActivityDTO activityDTO = new CreateActivityDTO();
        activityDTO.setActivityId(activityDO.getId());
        return activityDTO;
    }

private ActivityDetailDTO convertToActivityDetailDTO(
            ActivityDO activityDO, List<ActivityUserDO> activityUserDOList
            , List<PrizeDO> prizeDOList, List<ActivityPrizeDO> activityPrizeDOList) {
        ActivityDetailDTO detailDTO = new ActivityDetailDTO();
        detailDTO.setActivityId(activityDO.getId());
        detailDTO.setActivityName(activityDO.getActivityName());
        detailDTO.setDesc(activityDO.getDescription());
        detailDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));

        List<ActivityDetailDTO.PrizeDTO> prizeDTOList = activityPrizeDOList
                .stream()
                .map(apDO -> {
                    ActivityDetailDTO.PrizeDTO prizeDTO = new ActivityDetailDTO.PrizeDTO();
                    prizeDTO.setPrizeId(apDO.getPrizeId());
                    Optional<PrizeDO> optionalPrizeDO = prizeDOList.stream()
                            .filter(prizeDO -> prizeDO.getId().equals(apDO.getPrizeId()))
                            .findFirst();
                    // 如果PrizeDO为空,不执行当前方法,不为空才执行
                    optionalPrizeDO.ifPresent(prizeDO -> {
                        prizeDTO.setName(prizeDO.getName());
                        prizeDTO.setImageUrl(prizeDO.getImageUrl());
                        prizeDTO.setPrice(prizeDO.getPrice());
                        prizeDTO.setDescription(prizeDO.getDescription());
                    });
                    prizeDTO.setTiers(ActivityPrizeTiersEnum.forName(apDO.getPrizeTiers()));
                    prizeDTO.setPrizeAmount(apDO.getPrizeAmount());
                    prizeDTO.setStatus(ActivityPrizeStatusEnum.forName(apDO.getStatus()));
                    return prizeDTO;
                }).collect(Collectors.toList());
        detailDTO.setPrizeDTOList(prizeDTOList);

        List<ActivityDetailDTO.UserDTO> userDTOList = activityUserDOList.stream()
                .map(auDO -> {
                    ActivityDetailDTO.UserDTO userDTO = new ActivityDetailDTO.UserDTO();
                    userDTO.setUserId(auDO.getUserId());
                    userDTO.setUserName(auDO.getUserName());
                    userDTO.setStatus(ActivityUserStatusEnum.forName(auDO.getStatus()));
                    return userDTO;
                }).collect(Collectors.toList());
        detailDTO.setUserDTOList(userDTOList);
        return detailDTO;
    }
 private void cacheActivity(ActivityDetailDTO detailDTO) {
        // key: ACTIVITY_12
        // value: ActivityDetailDTO(json)
        if (null == detailDTO || null == detailDTO.getActivityId()) {
            logger.warn("要缓存的活动信息不存在!");
            return;
        }

        try {
            redisUtil.set(ACTIVITY_PREFIX + detailDTO.getActivityId(),
                    JacksonUtil.writeValueAsString(detailDTO),
                    ACTIVITY_TIMEOUT);
        } catch (Exception e) {
            logger.error("缓存活动异常,ActivityDetailDTO={}",
                    JacksonUtil.writeValueAsString(detailDTO),
                    e);
        }
    }

 /**
     * 校验活动有效性
     *
     * @param param
     */
    private void checkActivityInfo(CreateActivityParam param) {
        if (null == param) {
            throw new ServiceException(ServiceErrorCodeConstants.CREATE_ACTIVITY_INFO_IS_EMPTY);
        }

        //查看人员相关信息是否存在
        List<Long> userIds = param.getActivityUserList().stream()
                .map(CreateUserByActivityParam::getUserId)
                .distinct().collect(Collectors.toList());
        List<Long> existUserIds = userMapper.selectExistByIds(userIds);
        if (CollectionUtils.isEmpty(existUserIds)) {
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
        }
        userIds.forEach(id -> {
            if (!existUserIds.contains(id)) {
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
            }
        });

        //查看奖品相关信息是否存在
        List<Long> prizeIds = param.getActivityPrizeList().stream()
                .map(CreatePrizeByActivityParam::getPrizeId)
                .collect(Collectors.toList());
        List<Long> existPrizeIds = prizeMapper.selectExistByIds(prizeIds);
        if (CollectionUtils.isEmpty(existPrizeIds)) {
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
        }
        prizeIds.forEach(id -> {
            if(!existPrizeIds.contains(id)) {
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
            }
        });
    }
}

 

优点与难点:利用Spring框架事务的特性,用@Transactional注解保证了整个活动创建过程的原子性,  利用Redis缓存活动信息减轻了数据库负担。

活动/活动奖品/活动⼈员状态 枚举类(Enum)

@AllArgsConstructor
@Getter
public enum ActivityPrizeStatusEnum {
    INIT(1, "初始化"),

    COMPLETED(2, "已被抽取");


    private final Integer code;

    private final String message;

    public static ActivityPrizeStatusEnum forName(String name) {
        for (ActivityPrizeStatusEnum activityPrizeStatusEnum : ActivityPrizeStatusEnum.values()) {
            if (activityPrizeStatusEnum.name().equalsIgnoreCase(name)) {
                return activityPrizeStatusEnum;
            }
        }
        return null;
    }
}


@AllArgsConstructor
@Getter
public enum ActivityPrizeTiersEnum {
    FIRST_PRIZE(1, "一等奖"),

    SECOND_PRIZE(2, "二等奖"),

    THIRD_PRIZE(3, "三等奖");

    private final Integer code;

    private final String message;

    public static ActivityPrizeTiersEnum forName(String name) {
        for (ActivityPrizeTiersEnum activityPrizeTiersEnum : ActivityPrizeTiersEnum.values()) {
            if (activityPrizeTiersEnum.name().equalsIgnoreCase(name)) {
                return activityPrizeTiersEnum;
            }
        }
        return null;
    }
}

@AllArgsConstructor
@Getter
public enum ActivityStatusEnum {
    RUNNING(1, "活动进行中"),

    COMPLETED(2, "活动已完成");


    private final Integer code;

    private final String message;

    public static ActivityStatusEnum forName(String name) {
        for (ActivityStatusEnum activityStatusEnum : ActivityStatusEnum.values()) {
            if (activityStatusEnum.name().equalsIgnoreCase(name)) {
                return activityStatusEnum;
            }
        }
        return null;
    }
}


@Getter
@AllArgsConstructor
public enum ActivityUserStatusEnum {
    INIT(1, "初始化"),

    COMPLETED(2, "已被抽取");


    private final Integer code;

    private final String message;

    public static ActivityUserStatusEnum forName(String name) {
        for (ActivityUserStatusEnum activityUserStatusEnum : ActivityUserStatusEnum.values()) {
            if (activityUserStatusEnum.name().equalsIgnoreCase(name)) {
                return activityUserStatusEnum;
            }
        }
        return null;
    }
}

 1.6 Dao层接口设计

@Mapper
public interface ActivityMapper {

    @Insert("insert into activity(activity_name,description,status)" +
            "values (#{activityName},#{description},#{status})")
    @Options(useGeneratedKeys = true, keyProperty ="id", keyColumn ="id")
    void insert(ActivityDO activityDO);
}


@Mapper
public interface ActivityPrizeMapper {

    @Insert("<script>" +
            " insert into activity_prize (activity_id, prize_id, prize_amount, prize_tiers, status)" +
            " values <foreach collection = 'items' item='item' index='index' separator=','>" +
            " (#{item.activityId}, #{item.prizeId}, #{item.prizeAmount}, #{item.prizeTiers}, #{item.status})" +
            " </foreach>" +
            " </script>")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    void batchInsert(@Param("items") List<ActivityPrizeDO> prizeDOList);
}


@Mapper
public interface ActivityUserMapper {

    @Insert("<script>" +
            " insert into activity_user (activity_id, user_id, user_name, status)" +
            " values <foreach collection = 'items' item='item' index='index' separator=','>" +
            " (#{item.activityId}, #{item.userId}, #{item.userName}, #{item.status})" +
            " </foreach>" +
            " </script>")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    void batchInsert(@Param("items") List<ActivityUserDO> userDOList);
}

@Mapper
public interface PrizeMapper {
 @Select("<script>" +
            " select id from prize" +
            " where id in" +
            " <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
            " #{item}" +
            " </foreach>" +
            " </script>")
    List<Long> selectExistByIds(@Param("items") List<Long> prizeIds);

    @Select("<script>" +
            " select * from prize" +
            " where id in" +
            " <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
            " #{item}" +
            " </foreach>" +
            " </script>")
    List<PrizeDO> batchSelectByIds(@Param("items") List<Long> ids);

    @Select("select * from prize where id=#{prizeId}")
    PrizeDO selectById(@Param("prizeId") Long prizeId);
}


@Mapper
public interface UserMapper {
@Select("<script>" +
            " select id from user" +
            " where id in" +
            " <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
            " #{item}" +
            " </foreach>" +
            " </script>")
    List<Long> selectExistByIds(@Param("items") List<Long> userIds);

    @Select("<script>" +
            " select * from user" +
            " where id in" +
            " <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
            " #{item}" +
            " </foreach>" +
            " </script>")
    List<UserDO> batchSelectByIds(@Param("items") List<Long> userIds);
}

 2.活动列表展示(翻页)

 2.1 时序图

2.2 约定前后端交互接口

[请求] /activity/find-list?currentPage=1&pageSize=10 GET
[响应] 
{
 "code": 200,
 "data": {
 "total": 10,
 "records": [
 {
 "activityId": 23,
 "activityName": "抽奖测试",
 "description": "年会抽奖活动",
 "valid": true
 },
 {
 "activityId": 22,
 "activityName": "抽奖测试",
 "description": "年会抽奖活动",
 "valid": true
 },
 {
 "activityId": 21,
 "activityName": "节⽇抽奖",
 "description": "年会抽奖活动",
 "valid": true
 }
 ]
 },
 "msg": ""
}

2.3 Controller层接口设计

@RestController
public class ActivityController {
    private final static Logger logger = LoggerFactory.getLogger(ActivityController.class);

    @Autowired
    ActivityService activityService;

    @RequestMapping("/activity/find-list")
    public CommonResult<FindActivityListResult> findActivityList(PageParam param) {
        logger.info("ActivityController findActivityList({})",param);
        return CommonResult.success(
                convertToFindActivityResult(activityService.findActivityList(param))
        );
    }

 private FindActivityListResult convertToFindActivityResult(
            PageListDTO<ActivityDTO> activityList) {
        if(null == activityList) {
            throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
        }
        FindActivityListResult result = new FindActivityListResult();
        result.setTotal(activityList.getTotal());
        result.setRecords(activityList.getRecords().stream()
                .map(activityDTO -> {
                    FindActivityListResult.ActivityInfo activityInfo = new FindActivityListResult.ActivityInfo();
                    activityInfo.setActivityId(activityDTO.getActivityId());
                    activityInfo.setActivityName(activityDTO.getActivityName());
                    activityInfo.setDescription(activityDTO.getDescription());
                    activityInfo.setValid(activityDTO.valid());
                    return activityInfo;
                }).collect(Collectors.toList()));
        return result;
    }
}

2.4 Service层接口设计 

public interface ActivityService {
 /**
     * 获取活动列表
     * @param param
     * @return
     */
    PageListDTO<ActivityDTO> findActivityList(PageParam param);
}

接口实现:

@Service
public class ActivityServiceImpl implements ActivityService {
    /**
     * 活动缓存前置,为了区分业务
     */
    private final String ACTIVITY_PREFIX = "ACTIVITY_";
    /**
     * 活动缓存过期时间
     */
    private final Long ACTIVITY_TIMEOUT = 60 * 60 * 24 * 3L;

    Logger logger = LoggerFactory.getLogger(ActivityServiceImpl.class);
    @Autowired
    RedisUtil redisUtil;


    @Autowired
    ActivityMapper activityMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;


    @Override
    public PageListDTO<ActivityDTO> findActivityList(PageParam param) {
        //获取总量
        int total = activityMapper.count();

        //获取当前页列表
        List<ActivityDO> activityDOList = activityMapper.selectActivityList(
                param.getOffset(),param.getPageSize());
        List<ActivityDTO> activityDTOList = activityDOList.stream()
                .map(activityDO -> {
                    ActivityDTO activityDTO = new ActivityDTO();
                    activityDTO.setActivityName(activityDO.getActivityName());
                    activityDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
                    activityDTO.setDescription(activityDO.getDescription());
                    activityDTO.setActivityId(activityDO.getId());
                    return activityDTO;
                }).collect(Collectors.toList());
        return new PageListDTO<>(total, activityDTOList);
    }
}

2.5 Dao层接口设计

@Mapper
public interface ActivityMapper {

    @Select("select count(1) from activity")
    int count();

    @Select("select * from activity order by id desc limit #{offset},#{pageSize}")
    List<ActivityDO> selectActivityList(@Param("offset") Integer offset
            , @Param("pageSize") Integer pageSize);
}

八、抽奖模块

1.抽奖设计

抽奖过程是抽奖系统中最重要的核⼼环节,它需要确保公平、透明且⾼效。以下是详细的抽奖过程设计:
1. 参与者注册与奖品建⽴
参与者注册:管理员通过管理端新增⽤⼾, 填写必要的信息,如姓名、联系⽅式等。
奖品建⽴:奖品需要提前建⽴好
2. 抽奖活动设置
活动创建:管理员在系统中创建抽奖活动,输⼊活动名称、描述、奖品列表等信息。
圈选⼈员: 关联该抽奖活动的参与者。
圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等。
活动发布:活动信息发布后,系统通过管理端界⾯展⽰活动列表。
3. 抽奖请求处理(重要)
随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加
信息。
消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。
请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。
4. 抽奖结果公布
前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。
5. 抽奖逻辑执⾏(重要)
消息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。
6. 中奖结果处理(重要)
请求验证: 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数限制等)等;
幂等性:若消息多发,已抽取的内容不能再次抽取
状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取, ⼈员是否已中奖等。
结果记录:中奖结果被记录在数据库中,并同步更新 Redis 缓存。
7. 中奖者通知
通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。
奖品领取:中奖者根据通知中的指引领取奖品。
8. 抽奖异常处理
回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。
补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补
救措施
技术实现细节
异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂
等性。
活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内
容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。
并发处理:中奖者通知,可能要通知多系统,但相互解耦,可以设计为并发处理,加快抽奖效率
作⽤。
事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要
做好事务处理。
通过以上流程,抽奖系统能够确保抽奖过程的公平性和⾼效性,同时提供良好的⽤⼾体验。⽽且还整 合了 Redis 和 MQ , 进⼀步提⾼系统的性能。

2.RabbitMQ配置使用

pom.xml:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.properties

## mq ##
spring.rabbitmq.host=你的云服务器端口号或者将RabbitMQ下载到本地
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

 DirectRabbitConfig 配置类

@Configuration
public class DirectRabbitConfig {
    public static final String QUEUE_NAME = "DirectQueue";
    public static final String EXCHANGE_NAME = "DirectExchange";
    public static final String ROUTING = "DirectRouting";

    public static final String DLX_QUEUE_NAME = "DlxDirectQueue";
    public static final String DLX_EXCHANGE_NAME = "DlxDirectExchange";
    public static final String DLX_ROUTING = "DlxDirectRouting";

    /**
     * 队列 起名:DirectQueue
     *
     * @return
     */
    @Bean
    public Queue directQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("DirectQueue",true,true,false);

        // 一般设置一下队列的持久化就好,其余两个就是默认false
        // return new Queue(QUEUE_NAME,true);

        // 普通队列绑定死信交换机
        return QueueBuilder.durable(QUEUE_NAME)
                .deadLetterExchange(DLX_EXCHANGE_NAME)
                .deadLetterRoutingKey(DLX_ROUTING).build();
    }

    /**
     * Direct交换机 起名:DirectExchange
     *
     * @return
     */
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(EXCHANGE_NAME,true,false);
    }

    /**
     * 绑定  将队列和交换机绑定, 并设置用于匹配键:DirectRouting
     *
     * @return
     */
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(directQueue())
                .to(directExchange())
                .with(ROUTING);
    }

    /**
     * 死信队列
     *
     * @return
     */
    @Bean
    public Queue dlxQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("DirectQueue",true,true,false);

        // 一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue(DLX_QUEUE_NAME,true);
    }

    /**
     * 死信交换机
     *
     * @return
     */
    @Bean
    DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
    }

    /**
     * 绑定死信队列与交换机
     *
     * @return
     */
    @Bean
    Binding bindingDlx() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(DLX_ROUTING);
    }



    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

3.抽奖请求处理

3.1 时序图

3.2  约定前后端交互接口

[请求] /draw-prize POST
{
 "winnerList":[
 {
 "userId":15,
 "userName":"陈"
 },
 {
 "userId":21,
 "userName":"范闲"
 }
 ],
 "activityId":23,
 "prizeId":13,
 "prizeTiers":"FIRST_PRIZE",
 "winningTime":"2024-05-21T11:55:10.000Z"
}
[响应] 
{
 "code": 200,
 "data": true,
 "msg": ""
}

3.3 Controller层接口设计

@RestController
public class DrawPrizeController {

    private static final Logger logger = LoggerFactory.getLogger(DrawPrizeController.class);

    @Autowired
    private DrawPrizeService drawPrizeService;

    @RequestMapping("/draw-prize")
    public CommonResult<Boolean> drawPrize(
            @Validated @RequestBody DrawPrizeParam param) {
        logger.info("drawPrize DrawPrizeParam:{}", param);
        // service
        drawPrizeService.drawPrize(param);
        return CommonResult.success(true);
    }
}

3.4 Service层接口设计

public interface DrawPrizeService {

    /**
     * 异步抽奖接口
     * @param param
     */
    void drawPrize(DrawPrizeParam param);
}

接口实现:

@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
    private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
    private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;

    private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ActivityMapper activityMapper;

    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Autowired
    private PrizeMapper prizeMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public void drawPrize(DrawPrizeParam param) {
        Map<String,String> map = new HashMap<>();
        map.put("messageId",String.valueOf(UUID.randomUUID()));
        map.put("messageData", JacksonUtil.writeValueAsString(param));
        //发消息,交换机,绑定的key,消息体
        rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,map);
        logger.info("mq 消息发送成功,map:{}",JacksonUtil.writeValueAsString(map));

    }
}

4. MQ异步抽奖逻辑执行

4.1 时序图

 4.2 消费MQ消息

 消费者MqReceiver实现:

@Component
@RabbitListener(queues = QUEUE_NAME)
public class MqReceiver {
    private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class);

    @Autowired
    private DrawPrizeService drawPrizeService;
    @Autowired
    private ActivityStatusManager activityStatusManager;
    @Qualifier("asyncServiceExecutor")
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    private MailUtil mailUtil;
    @Autowired
    private SMSUtil smsUtil;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @RabbitHandler
    public void process(Map<String,String> map) {
        logger.info("mq成功接收到消息,message({})"
                , JacksonUtil.writeValueAsString(map));
        String paramString = map.get("messageData");
        DrawPrizeParam param = JacksonUtil.readValue(paramString, DrawPrizeParam.class);
        try {
            //处理抽奖流程
            drawPrizeService.checkDrawPrizeParam(param);

            //状态扭转处理
            statusConvert(param);

            //保存中奖者名单
            List<WinningRecordDO> winningRecordDOList =
                    drawPrizeService.saveWinnerRecords(param);

            //通知中奖者(短信,邮件)
            //异步(并发)处理
            syncExecute(winningRecordDOList);
        }catch (ServiceException e) {
            logger.error("处理MQ 消息异常,{},{}",e.getCode(),e.getMassage(),e);
            //需要保证事务一致性(回滚)
            rollback(param);
            //抛出异常
            throw e;
        }catch (Exception e) {
            logger.error("处理MQ 消息异常");
            //需要保证事务一致性(回滚)
            rollback(param);
            //抛出异常
            throw e;
        }


    }

    /**
     * 处理抽奖异常的回滚行为:恢复处理请求之前的库表状态
     *
     * @param param
     */
    private void rollback(DrawPrizeParam param) {

        // 1、回滚状态:活动、奖品、人员
        // 状态是否需要回滚
        if (!statusNeedRollback(param)) {
            // 不需要:return
            return;
        }
        // 需要回滚: 回滚
        rollbackStatus(param);

        // 2、回滚中奖者名单
        // 是否需要回滚
        if (!winnerNeedRollback(param)) {
            // 不需要:return
            return;
        }
        // 需要: 回滚
        rollbackWinner(param);

    }

    /**
     * 回滚中奖记录:删除奖品下的中奖者
     *
     * @param param
     */
    private void rollbackWinner(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }

    private boolean winnerNeedRollback(DrawPrizeParam param) {
        // 判断活动中的奖品是否存在中奖者
        int count = winningRecordMapper.countByAPId(param.getActivityId(), param.getPrizeId());
        return count > 0;
    }

    /**
     * 恢复相关状态
     *
     * @param param
     */
    private void rollbackStatus(DrawPrizeParam param) {
        // 涉及状态的恢复,使用 ActivityStatusManager
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
        convertActivityStatusDTO.setUserIds(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
        activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);

    }

    private boolean statusNeedRollback(DrawPrizeParam param) {
        // 判断活动+奖品+人员表相关状态是否已经扭转(正常思路)
        // 扭转状态时,保证了事务一致性,要么都扭转了,要么都没扭转(不包含活动):
        // 因此,只用判断人员/奖品是否扭转过,就能判断出状态是否全部扭转
        // 不能判断活动是否已经扭转
        // 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转
        ActivityPrizeDO activityPrizeDO =
                activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
        // 已经扭转了,需要回滚
        return activityPrizeDO.getStatus()
                .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());

    }

    /**
     * 并发处理抽奖后续流程
     *
     * @param winningRecordDOList
     */
    private void syncExecute(List<WinningRecordDO> winningRecordDOList) {
        // 通过线程池 threadPoolTaskExecutor
        // 扩展:加入策略模式或者其他设计模式来完成后续的异步操作
        // 短信通知
        threadPoolTaskExecutor.execute(()->sendMessage(winningRecordDOList));
        // 邮件通知
        threadPoolTaskExecutor.execute(()->sendMail(winningRecordDOList));
    }

    /**
     * 发邮件
     *
     * @param winningRecordDOList
     */
    private void sendMail(List<WinningRecordDO> winningRecordDOList) {
        if (CollectionUtils.isEmpty(winningRecordDOList)) {
            logger.info("中奖列表为空,不用发邮件!");
            return;
        }
        for (WinningRecordDO winningRecordDO : winningRecordDOList) {
            // Hi,陈。恭喜你在抽奖活动活动中获得二等奖:吹风机。获奖奖时间为18:18:44,请尽快领取您的奖励
            String context = "Hi," + winningRecordDO.getWinnerName() + "。恭喜你在"
                    + winningRecordDO.getActivityName() + "活动中获得"
                    + ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage()
                    + ":" + winningRecordDO.getPrizeName() + "。获奖时间为"
                    + DateUtil.formatTime(winningRecordDO.getWinningTime()) + ",请尽快领 取您的奖励!";
            mailUtil.sendSampleMail(winningRecordDO.getWinnerEmail(),
                    "中奖通知", context);
        }
    }

    /**
     * 发短信
     *
     * @param winningRecordDOList
     */
    private void sendMessage(List<WinningRecordDO> winningRecordDOList) {
        if (CollectionUtils.isEmpty(winningRecordDOList)) {
            logger.info("中奖列表为空,不用发短信!");
            return;
        }
        for (WinningRecordDO winningRecordDO : winningRecordDOList) {
            Map<String, String> map = new HashMap<>();
            map.put("name", winningRecordDO.getWinnerName());
            map.put("activityName", winningRecordDO.getActivityName());
            map.put("prizeTiers", ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage());
            map.put("prizeName", winningRecordDO.getPrizeName());
            map.put("winningTime", DateUtil.formatTime(winningRecordDO.getWinningTime()));
            smsUtil.sendMessage("SMS_476785514",
                    winningRecordDO.getWinnerPhoneNumber().getValue(),
                    JacksonUtil.writeValueAsString(map));
        }
    }

    /**
     * 状态扭转
     * @param param
     */
    private void statusConvert(DrawPrizeParam param) {
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
        convertActivityStatusDTO.setUserIds(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.COMPLETED);
        activityStatusManager.handlerEvent(convertActivityStatusDTO);
    }
}

4.3 请求验证(核对抽奖信息有效性) 

 Service编写:

public interface DrawPrizeService {


    /**
     * 校验抽奖请求
     * @param param
     */
    void checkDrawPrizeParam(DrawPrizeParam param);
}

接口实现:

@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
    private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
    private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;

    private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ActivityMapper activityMapper;

    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Autowired
    private PrizeMapper prizeMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @Autowired
    private RedisUtil redisUtil;


    @Override
    public void checkDrawPrizeParam(DrawPrizeParam param) {
        ActivityDO activityDO = activityMapper.selectById(param.getActivityId());

        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId()
                ,param.getPrizeId());


        //活动是否存在
        if(null == activityDO||null == activityPrizeDO) {
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY);
        }

        //活动是否有效
        if(activityDO.getStatus()
                .equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())){
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED);
        }

        //奖品是否有效
        if(activityPrizeDO.getStatus()
                .equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name())){
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED);
        }

        //中奖者人数和奖品数量是否一致
        if(activityPrizeDO.getPrizeAmount()!=param.getWinnerList().size()) {
            throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR);
        }
    }

4.4 状态转换(重点)

   (1)活动/奖品/参与者状态转换设计

 

(2)问题与解决

 采用一般写法的话存在以下问题:

1. 存在多个处理对象的顺序关系需要维护:奖品+活动状态扭转,活动需要依赖奖品状态改变⽽改
变。可以看出请求依赖于多个决策点,因此处理顺序很重要,不易维护。
2. 需要动态改变算法或⾏为:是否可以扭转状态的条件,若将来会发⽣改变,在这⾥不易维护。
3. 系统的灵活性和可扩展性⽆法体现。
4. 处理请求的复杂性不易维护。因此需要对请求处理进⾏优化:如果请求的处理过程需要根据性能或其他因素进⾏优化,策略模式可以轻松替换或更新具体的处理策略,⽽责任链模式可以帮助管理这些策略的应⽤顺序。

 解决方案:

1. 策略模式(Strategy Pattern):定义 AbstractActivityOperator 策列类,和其策略实现类
PrizeOperator、ActivityOperator 和 UserOperator。每个具体的操作类都实现了
AbstractActivityOperator 定义的接⼝,代表了不同的状态转换策略。
2. 责任链模式(Chain of Responsibility Pattern): 定义 ActivityStatusManager 接⼝类, 在
ActivityStatusManagerImpl 实现中,通过遍历operatorMap 中的所有操作符(策略),并按照
⼀定的顺序执⾏,形成了⼀个责任链,每个操作符判断是否是⾃⼰的责任,如果是,则处理请
求。
责任链模式(Chain of Responsibility Pattern)是⼀种⾏为设计模式,它允许将⼀个请求沿着处理 者对象组成的链进⾏传递。每个处理者对象都有责任去处理请求,或者将它传递给链中的下⼀个处理 者。请求的传递⼀直进⾏,直到有⼀个处理者对象对请求进⾏了处理,或者直到链的末端仍未有处理 者处理该请求。
以下是责任链模式在 ActivityStatusManagerImpl 类中的实现细节:
1. 请求的创建:ActivityStatusConvertDTO statusConvertDTO 是请求对象,包含了状态转换所需 的所有信息。
2. 处理者对象:AbstractActivityOperator 及其⼦类 PrizeOperator 和 ActivityOperator 是处理者 对象,它们实现了 needConvert 和convertStatus ⽅法,⽤以判断是否需要处理请求以及执⾏处 理。
3. 责任链的维护:operatorMap 是⼀个包含所有处理者对象的映射,它按照 sequence() ⽅法返回 的顺序维护了责任链。
4. 请求的传递:在 processStatusConversion ⽅法中,通过迭代器遍历 operatorMap,对每个操作 符实例调⽤needConvert⽅法来判断是否需要由当前操作符处理请求。
5. 处理请求:如果 needConvert 返回true,则调⽤ convertStatus ⽅法来处理请求。
6. 终⽌责任链:⼀旦请求被某个操作符处理,迭代器中的该操作符将被移除(it.remove()),这防 ⽌了请求被重复处理,并且终⽌了对该操作符的责任链。
7. 异常处理:如果在责任链中的任何点上请求处理失败(convertStatus返回false),则抛出异
常,这可以看作是责任链的终⽌。
通过这种⽅式,责任链模式允许系统在运⾏时根据请求的类型动态地选择处理者,⽽不需要修改其他 处理者的代码,从⽽提⾼了系统的灵活性和可维护性。

 代码实现:

public interface ActivityStatusManager {

    /**
     * 处理活动相关状态扭转
     * @param convertActivityStatusDTO
     */
    void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO);

    /**
     * 回滚处理活动相关状态
     *
     * @param convertActivityStatusDTO
     */
    void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
}



//策略模式+责任链模式
@Service
public class ActivityStatusManagerImpl implements ActivityStatusManager {
    private static final Logger logger = LoggerFactory.getLogger(ActivityStatusManagerImpl.class);

    @Autowired
    ActivityService activityService;

    @Autowired
    private final Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
        if(CollectionUtils.isEmpty(operatorMap)) {
            logger.warn("operatorMap为空");
            return;
        }
        Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
        Boolean update = false;
        //先处理人员、奖品
        update = processConvertStatus(convertActivityStatusDTO,currMap,1);

        //在处理活动
        update = processConvertStatus(convertActivityStatusDTO,currMap,2)||update;

        //更新缓存
        if(update) {
            activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
        }
    }

    @Override
    public void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
        // operatorMap:活动、奖品、人员
        // 活动是否需要回滚?? 绝对需要,
        // 原因:奖品都恢复成INIT,那么这个活动下的奖品绝对没抽完
        for (AbstractActivityOperator operator : operatorMap.values()) {
            operator.convert(convertActivityStatusDTO);
        }

        // 缓存更新
        activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
    }

    /**
     * 扭转状态
     * @param convertActivityStatusDTO
     * @param currMap
     * @param sequence
     * @return
     */
    private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,
                                         Map<String, AbstractActivityOperator> currMap,
                                         int sequence) {
        Boolean update = false;
        // 遍历currMap
        Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = currMap.entrySet().iterator();
        while (iterator.hasNext()) {
            AbstractActivityOperator operator = iterator.next().getValue();
            // Operator 是否需要转换
            if (operator.sequence() != sequence
                    || !operator.needConvert(convertActivityStatusDTO)) {
                continue;
            }

            // 需要转换:转换
            if (!operator.convert(convertActivityStatusDTO)) {
                logger.error("{}状态转换失败!", operator.getClass().getName());
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR);
            }

            // currMap 删除当前 Operator
            iterator.remove();
            update = true;
        }

        // 返回
        return update;
    }
}
@Component
public abstract class AbstractActivityOperator {
    /**
     * 控制处理顺序
     * @return
     */
    public abstract Integer sequence();

    /**
     * 是否需要转换
     * @param convertActivityStatusDTO
     * @return
     */
    public abstract Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO);

    /**
     * 转换方法
     *
     * @param convertActivityStatusDTO
     * @return
     */
    public abstract Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO);
}


@Component
public class ActivityOperator extends AbstractActivityOperator {
    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Override
    public Integer sequence() {
        return 2;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        ActivityStatusEnum targetStatus = convertActivityStatusDTO.getTargetActivityStatus();
        if (null == activityId
                || null == targetStatus) {
            return false;
        }

        ActivityDO activityDO = activityMapper.selectById(activityId);
        if (null == activityDO) {
            return false;
        }

        // 当前活动状态与传入的状态一致,不处理
        if (targetStatus.name().equalsIgnoreCase(activityDO.getStatus())) {
            return false;
        }

        // 需要判断奖品是否全部抽完
        // 查询 INIT 状态的奖品个数
        int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
        if (count > 0) {
            return false;
        }

        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        try {
            activityMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
                    convertActivityStatusDTO.getTargetActivityStatus().name());
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}


@Component
public class PrizeOperator extends AbstractActivityOperator {
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Override
    public Integer sequence() {
        return 1;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        Long prizeId = convertActivityStatusDTO.getPrizeId();
        ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStatusDTO.getTargetPrizeStatus();
        if (null == activityId
                || null == prizeId
                || null == targetPrizeStatus) {
            return false;
        }

        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(activityId, prizeId);
        if (null == activityPrizeDO) {
            return false;
        }

        // 判断当前奖品状态和目标状态是否一致
        if (targetPrizeStatus.name().equalsIgnoreCase(activityPrizeDO.getStatus())) {
            return false;
        }

        return true;
    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        Long prizeId = convertActivityStatusDTO.getPrizeId();
        ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStatusDTO.getTargetPrizeStatus();
        try {
            activityPrizeMapper.updateStatus(activityId, prizeId, targetPrizeStatus.name());
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}


@Component
public class UserOperator extends AbstractActivityOperator {

    @Autowired
    private ActivityUserMapper activityUserMapper;


    @Override
    public Integer sequence() {
        return 1;
    }

    @Override
    public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        List<Long> userIds = convertActivityStatusDTO.getUserIds();
        ActivityUserStatusEnum targetUserStatus = convertActivityStatusDTO.getTargetUserStatus();
        if (null == activityId
                || CollectionUtils.isEmpty(userIds)
                || null == targetUserStatus) {
            return false;
        }
        List<ActivityUserDO> activityUserDOList =
                activityUserMapper.batchSelectByAUIds(activityId, userIds);
        if (CollectionUtils.isEmpty(activityUserDOList)) {
            return false;
        }

        for (ActivityUserDO auDO : activityUserDOList) {
            if (auDO.getStatus()
                    .equalsIgnoreCase(targetUserStatus.name())) {
                return false;
            }
        }
        return true;

    }

    @Override
    public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
        Long activityId = convertActivityStatusDTO.getActivityId();
        List<Long> userIds = convertActivityStatusDTO.getUserIds();
        ActivityUserStatusEnum targetUserStatus = convertActivityStatusDTO.getTargetUserStatus();
        try {
            activityUserMapper.batchUpdateStatus(activityId, userIds, targetUserStatus.name());
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

 5. 结果记录

  5.1 时序图

 

5.2 Service层接口设计 

 消息队列消费者的逻辑在上文中已经展示这里只展示Service的代码:

@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
    private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
    private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;

    private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ActivityMapper activityMapper;

    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Autowired
    private PrizeMapper prizeMapper;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @Autowired
    private RedisUtil redisUtil;


    @Override
    public List<WinningRecordDO> saveWinnerRecords(DrawPrizeParam param) {
        // 查询相关信息:活动、人员、奖品、活动关联奖品
        ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
        List<UserDO> userDOList = userMapper.batchSelectByIds(
                param.getWinnerList()
                        .stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        PrizeDO prizeDO = prizeMapper.selectById(param.getPrizeId());
        ActivityPrizeDO activityPrizeDO =
                activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());

        // 构造中奖者记录,保存
        List<WinningRecordDO> winningRecordDOList = userDOList.stream()
                .map(userDO -> {
                    WinningRecordDO winningRecordDO = new WinningRecordDO();
                    winningRecordDO.setActivityId(activityDO.getId());
                    winningRecordDO.setActivityName(activityDO.getActivityName());
                    winningRecordDO.setPrizeId(prizeDO.getId());
                    winningRecordDO.setPrizeName(prizeDO.getName());
                    winningRecordDO.setPrizeTier(activityPrizeDO.getPrizeTiers());
                    winningRecordDO.setWinnerId(userDO.getId());
                    winningRecordDO.setWinnerName(userDO.getUserName());
                    winningRecordDO.setWinnerEmail(userDO.getEmail());
                    winningRecordDO.setWinnerPhoneNumber(userDO.getPhoneNumber());
                    winningRecordDO.setWinningTime(param.getWinningTime());
                    return winningRecordDO;
                }).collect(Collectors.toList());
        winningRecordMapper.batchInsert(winningRecordDOList);

        // 缓存中奖者记录
        // 1、缓存奖品维度中奖记录(WinningRecord_activityId_prizeId, winningRecordDOList(奖品维度的中奖名单))
        cacheWinningRecords(param.getActivityId() + "_" + param.getPrizeId(),
                winningRecordDOList,
                WINNING_RECORDS_TIMEOUT);

        // 2、缓存活动维度中奖记录(WinningRecord_activityId, winningRecordDOList(活动维度的中奖名单))
        // 当活动已完成再去存放活动维度中奖记录
        if (activityDO.getStatus()
                .equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
            // 查询活动维度的全量中奖记录
            List<WinningRecordDO> allList = winningRecordMapper.selectByActivityId(param.getActivityId());
            cacheWinningRecords(String.valueOf(param.getActivityId()),
                    allList,
                    WINNING_RECORDS_TIMEOUT);
        }

        return winningRecordDOList;
    }
}

6. 邮件服务

pom.xml:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

application.properties:

## 邮件 ##
spring.mail.host=smtp.qq.com
spring.mail.username=发送者邮箱
# 你的授权码:邮箱设置-》第三⽅服务-》开启IMAP/SMTP服务-》获取授权码
spring.mail.password=你的授权码
spring.mail.default-encoding=UTF-8

邮件工具类编写:

@Component
public class MailUtil {
    private static final Logger logger = LoggerFactory.getLogger(MailUtil.class);
    @Value(value = "${spring.mail.username}")
    private String from;
    @Autowired
    private JavaMailSender mailSender;

    /**
     * 发邮件
     *
     * @param to:  目标邮箱地址
     * @param subject: 标题
     * @param context: 正文
     * @return
     */
    public Boolean sendSampleMail(String to, String subject, String context) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(context);
        try {
            mailSender.send(message);
        } catch (Exception e) {
            logger.error("向{}发送邮件失败!", to, e);
            return false;
        }
        return true;
    }
}

由于本文过长,剩下的查看中奖者名单功能和查看活动详情的功能都较为简单这边不过多赘述。有疑问的可以去博主git仓库翻看源码进行学习。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue使用jsts,将wkt转为geojson

jsts库相关官方资料&#xff1a; JSTS是一个ECMAScript空间谓词和函数库&#xff0c;用于处理符合开放地理空间联盟发布的SQL简单特征规范的几何图形。JSTS也是成熟的Java库JTS的移植。 npm库的地址&#xff1a;https://www.npmjs.com/package/jsts Github开源项目地址&…

【移动WEB开发】rem适配布局

目录 1. rem基础 2.媒体查询 2.1 语法规范 2.2 媒体查询rem 2.3 引入资源&#xff08;理解&#xff09; 3. less基础 3.1 维护css的弊端 3.2 less介绍 3.3 less变量 3.4 less编译 3.5 less嵌套 3.6 less运算 4. rem适配方案 4.1 rem实际开发 4.2 技术使用 4.3 …

数字电子技术基础(二十六)——TTL门电路的输入特性和扇出系数

1 TTL门电路的输入特性 如下图所示为输入端伏安特性曲线的测试电路&#xff1a; 图1 输入端伏安特性曲线测试电路 以流入输入端的方向作为输入电流的参考方向&#xff0c;调节测试电路当中&#xff0c;电位器滑动端的位置&#xff0c;可以为这个与非门的B输入端提供一个可调的…

DeepSeek-R1本地化部署(Mac)

一、下载 Ollama 本地化部署需要用到 Ollama&#xff0c;它能支持很多大模型。官方网站&#xff1a;https://ollama.com/ 点击 Download 即可&#xff0c;支持macOS,Linux 和 Windows&#xff1b;我下载的是 mac 版本&#xff0c;要求macOS 11 Big Sur or later&#xff0c;Ol…

tp8 + easywechat6.17 token 验证失败

按照文档死活不行&#xff1a; 调整为以下就成功了&#xff08;return也是失败&#xff09;&#xff1a;

C++:入门详解(关于C与C++基本差别)

目录 一.C的第一个程序 二.命名空间&#xff08;namespace&#xff09; 1.命名空间的定义与使用&#xff1a; &#xff08;1&#xff09;命名空间里可以定义变量&#xff0c;函数&#xff0c;结构体等多种类型 &#xff08;2&#xff09;命名空间调用&#xff08;&#xf…

Redis 数据持久化之AOF

AOF&#xff08;Append Only File&#xff09; 以日志的形式来记录每个写操作&#xff0c;将Redis执行过的所有写指令记录下来&#xff08;读操作不记录&#xff09;&#xff0c;只许追加文件但不可以改写文件&#xff0c;redis启动之初会读取该文件重新构建数据&#xff0c;换…

网格图学习(附题单与做题思路)

文章目录 一、DFS 经典题型695. 岛屿的最大面积 二、BFS 经典题型994. 腐烂的橘子**算法选择对照表** 一、DFS 经典题型 岛屿的最大面积 LeetCode 695描述&#xff1a;求网格中最大的陆地连通区域面积解题&#xff1a;DFS 遍历所有相邻陆地&#xff0c;标记已访问关键点&#…

开发者社区测试报告(功能测试+性能测试)

功能测试 测试相关用例 开发者社区功能背景 在当今数字化时代&#xff0c;编程已经成为一项核心技能&#xff0c;越来越多的人开始学习编程&#xff0c;以适应快速变化的科技 环境。基于这一需求&#xff0c;我设计开发了一个类似博客的论坛系统&#xff0c;专注于方便程序员…

pyecharts 中设置 ​Map 图表的宽高

在 pyecharts 中设置 ​Map 图表的宽高&#xff0c;需要通过 InitOpts 初始化参数实现。以下是具体方法&#xff1a; &#x1f3af; 完整代码示例 from pyecharts import options as opts from pyecharts.charts import Map# 创建地图时设置宽高 map_chart (Map(init_optsopt…

FPGA|Verilog-SPI驱动

最近准备蓝桥杯FPGA的竞赛&#xff0c;因为感觉官方出的IIC的驱动代码思路非常好&#xff0c;写的内容非常有逻辑并且规范。也想学习一下SPI的协议&#xff0c;所以准备自己照着写一下。直到我打开他们给出的SPI底层驱动&#xff0c;我整个人傻眼了&#xff0c;我只能说&#x…

python语言总结(持续更新)

本文主要是总结各函数&#xff0c;简单的函数不会给予示例&#xff0c;如果在平日遇到一些新类型将会添加 基础知识 输入与输出 print([要输出的内容])输出函数 input([提示内容]如果输入提示内容会在交互界面显示&#xff0c;用以提示用户)输入函数 注释 # 单行注释符&…

NO.26十六届蓝桥杯备战|字符数组七道练习|islower|isupper|tolower|toupper|strstr(C++)

P5733 【深基6.例1】自动修正 - 洛谷 小写字母 - 32 大写字母 大写字母 32 小写字母 #include <bits/stdc.h> using namespace std;const int N 110; char a[N] { 0 };int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cin >> a;int i 0;while (a…

笔记四:C语言中的文件和文件操作

Faye&#xff1a;只要有正确的伴奏&#xff0c;什么都能变成好旋律。 ---------《寻找天堂》 目录 一、文件介绍 1.1程序文件 1.2 数据文件 1.3 文件名 二、文件的打开和关闭 2.1 文件指针 2.2.文件的打开和关闭 2.3 文件读取结束的判定 三、 文件的顺序读写 3.1 顺序读写…

DeepSeek进阶应用(一):结合Mermaid绘图(流程图、时序图、类图、状态图、甘特图、饼图)

&#x1f31f;前言: 在软件开发、项目管理和系统设计等领域&#xff0c;图表是表达复杂信息的有效工具。随着AI助手如DeepSeek的普及&#xff0c;我们现在可以更轻松地创建各种专业图表。 名人说&#xff1a;博观而约取&#xff0c;厚积而薄发。——苏轼《稼说送张琥》 创作者&…

【OneAPI】网页截图API-V2

API简介 生成指定URL的网页截图或缩略图。 旧版本请参考&#xff1a;网页截图 V2版本新增全屏截图、带壳截图等功能&#xff0c;并修复了一些已知问题。 全屏截图&#xff1a; 支持全屏截图&#xff0c;通过设置fullscreentrue来支持全屏截图。全屏模式下&#xff0c;系统…

基于SpringBoot的餐厅点餐管理系统设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

Windows Server 2022:赋能未来,打造智能高效的企业数字基座---免费下载

免费下载地址 Windows Server 2022&#xff1a;赋能未来&#xff0c;打造智能高效的企业数字基座‌ 在数字化转型的浪潮中&#xff0c;企业需要更安全、更灵活、更智能的基础设施支撑。‌Windows Server 2022‌作为微软新一代服务器操作系统&#xff0c;以革新性的技术架构和行…

支持向量简要理解

决策方程符合感知机区分理论&#xff0c;我们基于线性代数来看这满足子空间理论&#xff0c;可以获取得到超平面。 支持向量机的目标是寻找最与超平面最近的点的最大距离&#xff0c;而距离计算如上&#xff0c;符合数学上计算点到线&#xff08;面&#xff09;的距离公式。 …

USB2.0 学习(1)字段和包

目录 1 字段 1.1 包识别字段PID 1.2 地址字段 1.3帧号字段 1.4 数据字段 1.5 CRC字段 2 包 2.1令牌包 2.2帧起始包 2.3数据包 2.4SPLIT包(分割事务包) 2.5握手包 参考 USB包的构成是一个逐层的过程,首先这些串行数据按照特定的规则构成字段,字段是构成包的基本…