# train **Repository Path**: luomoxingchen/train ## Basic Information - **Project Name**: train - **Description**: 铁路购票系统,采用Java+SpringBoot3+MySQL+Redis+Spring Alibaba Cloud+RabbitMQ - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 1 - **Created**: 2024-09-10 - **Last Updated**: 2025-10-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringBoot, MySQL, Java, Redis, nacos ## README # JDK新特性 **Java 9新特性 - 模块开放** 以前的java结构是包 包着类和接口等,JDK9之后就在包的外围添加了模块的类型,可以选择模块下的那些包对外开放(对于其他的模块)使用 ![](D:\Learning\Java\笔记图片\12306\java9进特性.png) ***在需要隐藏的包添加.java类,其内容为*** ``` module test(默认为包名, 其实可以随便取){ exports 包名; (意思是将该包暴露给其他模板) } // 需要使用其他模板开放的包的也需要创建.java对象 module test(){ require 包名; } ``` **Java 10新特性 - var局部变量推导** 使用var来指代变量类型,优点是更方便无脑,但缺点也明显,不能明显看出该变量指代的类型 ```java List list = new List(); // 可以写为 var list = new ArrayList(); ``` **java 11新特性 - 单文件程序** 以前java文件,需要javac先编译后再java运行该程序,但11后可直接运行java 文件名,来直接编译运行 **java 14新特性 - 文本块** 之前java 书写json、html代码块很复杂,在引入14之后,可以使用三双引号来书写原内容的代码块 ```java String json = """ { name: "test" } """; 之前版本 String json = "{\n" + "\tname: \"test\"\n" + "}"; ``` instanceof 之前有这个方法,a instanceof String含义是判断a是否为String类型,但在14之后,可以在使用时定义变量 ```java if(a instanceof String b){ b = a; } 之前为 Object a = "hello"; if(a instanceof String){ String b = (String) a; } ``` **java 16新特性 - record类** 在新建类时创建创建(就是类、接口有个record),在创建时,无法在里面声明任何变量,只能将所需定义该类的参数放在定义式 ```java public record TestRecord(String name, String password){ } // 在new对象时直接参数赋值 TestRecord testRecord = new TestRecord("nihao", "12345"); // 并且定义之后的属性值无法改变 ``` **java 17新特性 - sealed** switch增强版,lambda书写写法 Springboot3新特性 - AOT和JIT # 一、train-member 会员模块 ## (一)基础配置 ### 1. 分模块开发 因为是第一个微服务项目,是将不同服务模块拆开,虽然在同一个项目文件中,但每个版块pom基本独立,但交由最外层的pom管理 操作: 将新建项目的pom文件的boot,web,cloud和中的内容复制到新member模块中,然后在里面开设mvc架构模式 ![](D:\Learning\Java\笔记图片\12306\分模块.png) ### 2. 日志配置 调整日志信息,并保存配置信息保存在项目中,方便运维,但注意分模块放日志文件 日志配置文件xml,放在resources下: ```xml %d{mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n ${PATH}/trace.log ${PATH}/trace.%d{yyyy-MM-dd}.%i.log 10MB %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n ${PATH}/error.log ${PATH}/error.%d{yyyy-MM-dd}.%i.log 10MB %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n ERROR ACCEPT DENY ``` 启动项中使用 ```java @SpringBootApplication @ComponentScan("com.Java") public class MemberApplication { private static final Logger LOG = LoggerFactory.getLogger(MemberApplication.class); public static void main(String[] args) { SpringApplication app = new SpringApplication(MemberApplication.class); Environment env = app.run(args).getEnvironment(); LOG.info("启动成功!!"); LOG.info("测试地址: \thttp://127.0.0.1:{}{}/hello", env.getProperty("server.port"), env.getProperty("server.servlet.context-path")); } } ``` 配置LogAspect - 在Common模块 通过AOP在每次controller前,也就是每次请求服务前作为切点,然后写入log相关信息(生成单一的请求码(目前是随机数)) 切点为隔离所有Java包下的以Controller为结尾的所有方法参数可有可无的方法请求, @Pointcut("execution(public * com.Java..*Controller.*(..))") ```java package com.Java.train.common.aspect; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.support.spring.PropertyPreFilters; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; @Aspect @Component public class LogAspect { public LogAspect() { System.out.println("Common LogAspect"); } private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class); /** * 定义一个切点 * 隔离所有Java包下的以Controller为结尾的所有方法参数可有可无的方法请求 */ @Pointcut("execution(public * com.Java..*Controller.*(..))") public void controllerPointcut() { } @Before("controllerPointcut()") public void doBefore(JoinPoint joinPoint) { // 开始打印请求日志 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Signature signature = joinPoint.getSignature(); String name = signature.getName(); // 打印请求信息 LOG.info("------------- 开始 -------------"); LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name); LOG.info("远程地址: {}", request.getRemoteAddr()); // 打印请求参数 Object[] args = joinPoint.getArgs(); // LOG.info("请求参数: {}", JSONObject.toJSONString(args)); // 排除特殊类型的参数,如文件类型 Object[] arguments = new Object[args.length]; for (int i = 0; i < args.length; i++) { if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) { continue; } arguments[i] = args[i]; } // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters(); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter)); } @Around("controllerPointcut()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters(); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter)); LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); return result; } } ``` ### 3 . Common模块 将所有模块的公共模块,用于放置公共类,如工具类、拦截器、AOP、常量、枚举、公共配置等 pom为所有模块共有的(记得在最外层pom中配置外部依赖) 配置公共pom ```xml org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter org.springframework.boot spring-boot-starter-aop cn.hutool hutool-all com.alibaba fastjson ``` 其他类直接引入该依赖即可 ```xml *** 其他依赖引入方式为 *** com.Java common ``` 设置LogAspect类 ```java package com.Java.train.common.aspect; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.support.spring.PropertyPreFilters; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; @Aspect @Component public class LogAspect { public LogAspect() { System.out.println("Common LogAspect"); } private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class); /** * 定义一个切点 */ @Pointcut("execution(public * com.Java..*Controller.*(..))") public void controllerPointcut() { } @Before("controllerPointcut()") public void doBefore(JoinPoint joinPoint) { // 开始打印请求日志 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Signature signature = joinPoint.getSignature(); String name = signature.getName(); // 打印请求信息 LOG.info("------------- 开始 -------------"); LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name); LOG.info("远程地址: {}", request.getRemoteAddr()); // 打印请求参数 Object[] args = joinPoint.getArgs(); // LOG.info("请求参数: {}", JSONObject.toJSONString(args)); // 排除特殊类型的参数,如文件类型 Object[] arguments = new Object[args.length]; for (int i = 0; i < args.length; i++) { if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) { continue; } arguments[i] = args[i]; } // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters(); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter)); } @Around("controllerPointcut()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters(); PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter(); excludefilter.addExcludes(excludeProperties); LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter)); LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime); return result; } } ``` **配置公共返回值类CommonResq**: ```java package com.Java.train.common.response; public class CommonResp { /** * 业务上的成功或失败 */ private boolean success = true; /** * 返回信息 */ private String message; /** * 返回泛型数据,自定义类型 */ private T content; public CommonResp ok(T content){ return new CommonResp<>(true, null, content); } public CommonResp fail(String errorMsg){ return new CommonResp<>(false, errorMsg, null); } public CommonResp() { } public CommonResp(boolean success, String message, T content) { this.success = success; this.message = message; this.content = content; } public CommonResp(T content) { this.content = content; } public boolean getSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getContent() { return content; } public void setContent(T content) { this.content = content; } @Override public String toString() { final StringBuffer sb = new StringBuffer("CommonResp{"); sb.append("success=").append(success); sb.append(", message='").append(message).append('\''); sb.append(", content=").append(content); sb.append('}'); return sb.toString(); } } ``` **Tips**: 小知识点: 在resources文件下,spring不止会自动扫描直接在resources下的文件,还会优先扫描resources下的config目录下的配置文件 ### 4. Gateway模块 方便项目管理且为保护内网程序,设立网关模块,只在外部开放gateway的服务,有gateway来进行其他服务的跳转 配置其pom文件(只保留cloud提供的gateway服务)具体由netty实现 ```xml train com.Java 0.0.1-SNAPSHOT 4.0.0 gateway 19 19 org.springframework.cloud spring-cloud-starter-gateway ``` 复制其他类的启动类 ```java package com.Java.train.gateway.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.core.env.Environment; @SpringBootApplication @ComponentScan("com.Java") public class GatewayApplication { private static final Logger LOG = LoggerFactory.getLogger(GatewayApplication.class); public static void main(String[] args) { SpringApplication app = new SpringApplication(GatewayApplication.class); Environment env = app.run(args).getEnvironment(); LOG.info("启动成功!!"); LOG.info("网关地址: \thttp://127.0.0.1:{}", env.getProperty("server.port")); } } ``` 在resources下配置日志文件(在日志目录下开gateway目录) 在Spring启动项设置中配置VM(启动netty日志功能(好像)) ``` -Dreactor.netty.http.server.accessLogEnabled=true ``` ### 5.1 统一异常处理 在Common模块中对不同的异常需要统一集中处理,转化为简短语句,且需要定义业务常见错误信息,整合为枚举类型 首先定义处理所有异常前的拦截器,出现异常后跳转至拦截器进行内容简化,其内容分为不同异常类型,如统一异常、业务异常处理、注解异常等等 ```java @ControllerAdvice public class ControllerExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class); /** * 所有异常统一处理 * @param e * @return */ @ExceptionHandler(value = Exception.class) // 触发exception类都要进来 @ResponseBody public CommonResp exceptionHandler(Exception e) { // LOG.info("seata全局事务ID: {}", RootContext.getXID()); // // 如果是在一次全局事务里出异常了,就不要包装返回值,将异常抛给调用方,让调用方回滚事务 // if (StrUtil.isNotBlank(RootContext.getXID())) { // throw e; // } CommonResp commonResp = new CommonResp(); LOG.error("系统异常:", e); commonResp.setSuccess(false); // commonResp.setMessage("系统出现异常,请联系管理员"); commonResp.setMessage("请联系管理员处理异常"); return commonResp; } /** * 所有业务统一处理 * @param e * @return */ @ExceptionHandler(value = BusniessException.class) @ResponseBody public CommonResp exceptionHandler(BusniessException e) { CommonResp commonResp = new CommonResp(); LOG.error("业务异常:{}", e.getE().getDesc()); commonResp.setSuccess(false); // commonResp.setMessage("系统出现异常,请联系管理员"); commonResp.setMessage(e.getE().getDesc()); return commonResp; } } ``` #### 5.2 设立业务异常 BusniessException和枚举类 ```java package com.Java.train.common.exception; // 处理业务异常 public class BusniessException extends RuntimeException{ private BusniessExceptionEnum e; public BusniessException(BusniessExceptionEnum e) { this.e = e; } public BusniessExceptionEnum getE() { return e; } public void setE(BusniessExceptionEnum e) { this.e = e; } @Override public Throwable fillInStackTrace() { return this; } } ``` 枚举类型: ```java package com.Java.train.common.exception; // 枚举所有业务异常 public enum BusniessExceptionEnum { MEMBER_MOBILE_EXITS("手机号已注册"); private String desc; BusniessExceptionEnum(String desc) { this.desc = desc; } public String getDesc() { return desc; } } ``` ### 6、雪花算法生成数据库全局Id 生成雪花算法需要两个参数,一个是机器工作id,另一个是数据中心机器的id,此处统一规定为1,以下是实现 ```java package com.Java.train.common.utils; import cn.hutool.core.util.IdUtil; public class SnowFlakeUtil { private static long workId = 1; private static long dataCenterId = 1; public static long getSnowFlakeNextId() { return IdUtil.getSnowflake(workId, dataCenterId).nextId(); } public static String getStrSnowFlakeNextId() { return String.valueOf(IdUtil.getSnowflake(workId, dataCenterId).nextId()); } } ``` ### 7.1、设置登录拦截器(前端url) 写在gateway板块下(token是否存在、被修改以及是否能解析出用户信息(JWT)) 设置处理前端URL是否已登录(token是否合法(合法指是否过期、无效和是否被修改过)),书写filter,继承GlobalFilter, Ordered(获取优先级,返回值越小,优先级越高) ```java package com.Java.train.gateway.filters; import cn.hutool.json.JSONObject; import com.Java.train.gateway.utils.JwtUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class LoginMemberFilter implements GlobalFilter, Ordered { private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取path String path = exchange.getRequest().getURI().getPath(); // 2.排除不需要的路径 if(path.contains("/admin") || path.equals("/member/send-code") || path.equals("/member/login") || path.equals("/member/hello")){ LOG.info("不需要登录验证:{}" + path); return chain.filter(exchange); // 继续下一个拦截器 } else { LOG.info("需要登录验证:{}", path); } // 3.获取前端登录验证 String token = exchange.getRequest().getHeaders().getFirst("token"); LOG.info("会员认证开始token为:{}", token); if(token == null || token.isEmpty()){ LOG.info("token为空,请求被拦截"); // 设置返回报错参数 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); // 设置完成请求(有问题返回) } else { LOG.info("token:{}", token); } // 检查token是否正确,是否被修改过,是否过期 boolean validate = JwtUtil.validate(token); if(validate){ LOG.info("token有效,通过验证"); return chain.filter(exchange); } else { LOG.warn("token无效,请求被拦截"); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } /** * @Description: 设置优先级 * @param * @return int数越小优先级越高 */ @Override public int getOrder() { return 0; } } ``` #### 7.2、本地缓存存当前用户信息 通过开辟本地线程来专门缓存当前用户的信息(id和mobile),LocalThread - 都为静态方法,即想即用 - 设置set和get方法 - 封装获取用户id的方法 - 封装用户类为MemberLoginResp(登录用的DTO) ```java package com.Java.train.common.context; import com.Java.train.common.response.MemberLoginResp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LoginMemberContext { private static final Logger LOG = LoggerFactory.getLogger(LoginMemberContext.class); private static ThreadLocal member = new ThreadLocal<>(); public static void setMember(MemberLoginResp memberLoginResp){ LoginMemberContext.member.set(memberLoginResp); } public static MemberLoginResp getMember(){ return member.get(); } public static Long getId(){ try{ return member.get().getId(); } catch (Exception e){ LOG.error("获取会员登录异常:{}", e); throw e; } } } ``` #### 7.3、设置用户token拦截器 写在common模块中,启用在member模块中 主要作用是向本地缓存(线程)中写入当前用户信息 ```java package com.Java.train.common.interceptor; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.Java.train.common.context.LoginMemberContext; import com.Java.train.common.response.MemberLoginResp; import com.Java.train.common.utils.JwtUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component public class MemberInterceptor implements HandlerInterceptor { private static final Logger LOG = LoggerFactory.getLogger(MemberInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token LOG.info("正在获取token"); String token = request.getHeader("token"); // 2.1 判断token是否存在 if(StrUtil.isNotBlank(token)){ LOG.info("获取会员登录token:{}", token); JSONObject jsonObject = JwtUtil.getJSONObject(token); LOG.info("当前会员用户:{}", jsonObject); MemberLoginResp member = JSONUtil.toBean(jsonObject, MemberLoginResp.class); LoginMemberContext.setMember(member); } return true; } } ``` **member中的启用类** 在登录拦截器之前先统一输出以下日志信息,即日志拦截器 ```java package com.Java.train.member.config; import com.Java.train.common.interceptor.LogInterceptor; import com.Java.train.common.interceptor.MemberInterceptor; import jakarta.annotation.Resource; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Resource LogInterceptor logInterceptor; @Resource MemberInterceptor memberInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor); registry.addInterceptor(memberInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/member/login", "/member/send-code", "/member/register", "/member/hello" ); } } ``` ### 8、分页查询 在总项目中添加分页依赖,在需要使用分页的项目中添加依赖 ```xml com.github.pagehelper pagehelper-spring-boot-starter 1.4.6 ``` #### 8.1 获取分页信息 PageHelper.startPage(1(页数),1(数量)); 在其紧接着下一条sql中添加分页查询(在sql后添加limit ?,?) 通过PageInfo获取分页信息 ```java // 3.1 获取分页总条数和页数 PageInfo pageInfo = new PageInfo<>(passengers); LOG.info("总行数:{}", pageInfo.getTotal()); LOG.info("总页数:{}", pageInfo.getPages()); ``` #### 8.2 封装分页返回类 —— PageResp ```java package com.Java.train.common.resp; import java.io.Serializable; import java.util.List; public class PageResp implements Serializable { /** * 总条数 */ private Long total; /** * 当前页的列表 */ private List list; public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } public List getList() { return list; } public void setList(List list) { this.list = list; } @Override public String toString() { return "PageResp{" + "total=" + total + ", list=" + list + '}'; } } ``` 具体应用: ```java // 3.2 转为封装后的返回值并封装到分页返回值中 List list = BeanUtil.copyToList(passengers, PassengerDTO.class); PageResp pageResp = new PageResp<>(); pageResp.setTotal(pageInfo.getTotal()); pageResp.setList(list); ``` 整体分页代码 ```java package com.Java.train.member.service.Impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import com.Java.train.common.exception.BusniessException; import com.Java.train.common.exception.BusniessExceptionEnum; import com.Java.train.common.resp.PageResp; import com.Java.train.common.response.CommonResp; import com.Java.train.member.domain.DTO.PassengerDTO; import com.Java.train.member.domain.Passenger; import com.Java.train.member.mapper.PassengerMapper; import com.Java.train.member.req.PassengerQueryReq; import com.Java.train.member.service.PassengerService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @Service public class PassengerServiceImpl extends ServiceImpl implements PassengerService { private static final Logger LOG = LoggerFactory.getLogger(PassengerService.class); @Override public CommonResp> queryList(PassengerQueryReq passengerQueryReq) { // 1.获取当前用户id信息 Long memberId = passengerQueryReq.getMemberId(); // Long memberId = LoginMemberContext.getId(); // 2.查询数据库中是否存在当前乘车人信息(memberId) LOG.info("查询页码:{}", passengerQueryReq.getPage()); LOG.info("每页条数:{}", passengerQueryReq.getSize()); PageHelper.startPage(passengerQueryReq.getPage(),passengerQueryReq.getSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); List passengers = list(wrapper.eq(Passenger::getMemberId, memberId)); if(passengers == null || CollUtil.isEmpty(passengers)){ throw new BusniessException(BusniessExceptionEnum.MEMBER_PASSENGER_NOT_EXITS); } // 3 分析分页信息 // 3.1 获取分页总条数和页数 PageInfo pageInfo = new PageInfo<>(passengers); LOG.info("总行数:{}", pageInfo.getTotal()); LOG.info("总页数:{}", pageInfo.getPages()); // 3.2 转为封装后的返回值并封装到分页返回值中 List list = BeanUtil.copyToList(passengers, PassengerDTO.class); PageResp pageResp = new PageResp<>(); pageResp.setTotal(pageInfo.getTotal()); pageResp.setList(list); // 4.返回查询到的乘车人信息 return new CommonResp<>(pageResp); } } ``` # 二、business模块开发 ## (一)代码生成器 由于后续板块都需要最基本的增删改查代码,因此为方便开发,构造一个代码生成器,使用mybatis-plus的官方推荐的构造器CodeGenerator生成基本增删改查代码 首先开创generator模块 ![image-20240922155216779](D:\Learning\Java\笔记图片\12306\代码生成器目录.png) 1. 引入生成器所需的依赖pom.xml ```xml org.freemarker freemarker com.baomidou mybatis-plus-boot-starter 3.5.5 com.baomidou mybatis-plus-generator 3.5.5 org.springframework.boot spring-boot-starter-freemarker org.apache.velocity velocity-engine-core 2.3 com.alibaba druid 1.1.10 com.mysql mysql-connector-j 8.0.33 cn.hutool hutool-all ``` 2. 配置官方拦截器 ```java package com.Java.train.generator.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement //开启事务 @Configuration //配置类注解 @MapperScan("com.Java.train.generator.mapper") public class MybatisPlusConfig { //配置拦截器 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); //创建乐观锁拦截器 OptimisticLockerInnerInterceptor mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //插件分页拦截器,我的是mysql // 分页插件放到最后面 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return mybatisPlusInterceptor; } } ``` 3. 复制生成器启动代码 ```java package com.Java.train.generator; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import com.baomidou.mybatisplus.generator.fill.Column; import java.sql.Types; import java.util.Collections; import java.util.HashMap; import java.util.Map; //自定义模板生成 public class CodeGeneratorPlus { public static void main(String[] args) { String projectPath = System.getProperty("user.dir"); //获取当前项目路径 // System.out.println(projectPath); FastAutoGenerator.create("jdbc:mysql://rm-8vb36yjot6i39u421so.rwlb.zhangbei.rds.aliyuncs.com/train_business?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai", "train_business", "Rzh063915") // 全局配置 .globalConfig(builder -> { builder .enableSwagger() // 是否启用swagger注解 .author("luomoxingchen") // 作者名称 .dateType(DateType.ONLY_DATE) // 时间策略 .commentDate("yyyy-MM-dd") // 注释日期 // .outputDir(projectPath + "[module]/src/main/java/com/Java/train/[module]/") // 输出目录 .outputDir(projectPath + "/train-business/src/main/java/") // 输出目录 .disableOpenDir(); // 生成后禁止打开所生成的系统目录 }) //java和数据库字段的类型转换 .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { int typeCode = metaInfo.getJdbcType().TYPE_CODE; if (typeCode == Types.SMALLINT || typeCode == Types.TINYINT) { // 自定义类型转换 return DbColumnType.INTEGER; } return typeRegistry.getColumnType(metaInfo); })) // 包配置 .packageConfig(builder -> { builder .parent("com.Java.train") // 父包名 // .moduleName("[module]") // 模块包名 .moduleName("business") // 模块包名 .controller("controller") .entity("entity") // 实体类包名 .service("service") // service包名 .serviceImpl("service.Impl") // serviceImpl包名 .mapper("mapper") // mapper包名 .xml("mapper.xml") // .pathInfo(Collections.singletonMap(OutputFile.xml, projectPath + "/[module]/src/main/resources/mapper/xml")).build(); .pathInfo(Collections.singletonMap(OutputFile.xml, projectPath + "/business/src/main/resources/mapper/xml")).build(); }) // 策略配置 .strategyConfig(builder -> { builder.enableCapitalMode()//驼峰 .enableSkipView()//跳过视图 .disableSqlFilter() // .addTablePrefix("t_") // 增加过滤表前缀 // .addTableSuffix("_db") // 增加过滤表后缀 // .addFieldPrefix("t_") // 增加过滤字段前缀 // .addFieldSuffix("_field") // 增加过滤字段后缀 // .addInclude("test") // 表匹配 // Entity 策略配置 .entityBuilder() .enableFileOverride() .enableLombok() // 开启lombok .enableChainModel() // 链式 .enableRemoveIsPrefix() // 开启boolean类型字段移除is前缀 .enableTableFieldAnnotation() //开启生成实体时生成的字段注解 .versionColumnName("version") // 乐观锁数据库字段 .versionPropertyName("version") // 乐观锁实体类名称 .logicDeleteColumnName("delflag") // 逻辑删除数据库中字段名 .logicDeletePropertyName("delFlag") // 逻辑删除实体类中的字段名 .naming(NamingStrategy.underline_to_camel) // 表名 下划线 -》 驼峰命名 .columnNaming(NamingStrategy.underline_to_camel) // 字段名 下划线 -》 驼峰命名 .idType(IdType.ASSIGN_ID) // 主键生成策略 雪花算法生成id .formatFileName("%s") // Entity 文件名称 .addTableFills(new Column("create_time", FieldFill.INSERT)) // 表字段填充 .addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE)) // 表字段填充 //.enableColumnConstant() //.enableActiveRecord()//MPlus中启用ActiveRecord模式,生成的实体类会继承activerecord.Model类,直接进行数据库操作 // Controller 策略配置 .controllerBuilder() .enableFileOverride() .enableHyphenStyle() .enableRestStyle() // 开启@RestController .formatFileName("%sController") // Controller 文件名称 // Service 策略配置 .serviceBuilder() .enableFileOverride() .formatServiceFileName("%sService") // Service 文件名称 .formatServiceImplFileName("%sServiceImpl") // ServiceImpl 文件名称 // Mapper 策略配置 .mapperBuilder() .enableFileOverride() .enableMapperAnnotation() // 开启@Mapper .enableBaseColumnList() // 启用 columnList (通用查询结果列) .enableBaseResultMap() // 启动resultMap .formatMapperFileName("%sMapper") // Mapper 文件名称 .formatXmlFileName("%sMapper"); // Xml 文件名称 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .templateConfig(builder -> { builder.controller("/templates/controller.java") .service("/templates/service.java") .serviceImpl("/templates/serviceImpl.java") // .entity("/templates/queryReq.java") // .entity("/templates/saveReq.java") // .entity("/templates/queryResp.java") //.mapper() .build(); }) .execute(); // 执行 } } ``` 4. 设置所需生成的模板ftl文件 controller.java.ftl ```ftl package ${package.Controller}; import com.Java.train.common.response.CommonResp; import com.Java.train.business.entity.DTO.${table.entityName!}DTO; import com.Java.train.business.req.${table.entityName!}QueryReq; import com.Java.train.common.resp.PageResp; import ${package.Entity}.${entity}; import ${package.Service}.${table.serviceName}; import io.swagger.annotations.ApiOperation; <#--import org.apache.shiro.authz.annotation.Logical;--> <#--import org.apache.shiro.authz.annotation.RequiresPermissions;--> import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.List; <#--import com.common.res.DataResult;--> <#if restControllerStyle> <#else> import org.springframework.stereotype.Controller; <#if superControllerClassPackage??> import ${superControllerClassPackage}; /** * @author ${author} * @since ${date} */ <#if restControllerStyle> @RestController <#else> @Controller @RequestMapping("/<#if controllerMappingHyphenStyle>${controllerMappingHyphen}<#else>${table.entityPath}") <#if kotlin> class ${table.controllerName}<#if superControllerClass??> : ${superControllerClass}() <#else> <#if superControllerClass??> public class ${table.controllerName} extends ${superControllerClass} { <#else> public class ${table.controllerName} { @Autowired private ${table.serviceName} ${table.serviceName?uncap_first}; @GetMapping("/query-list") <#-- @RequiresPermissions("sys:${table.entityName?uncap_first}:list")--> @ApiOperation("${table.entityName}查询全部") public CommonResp< PageResp<${table.entityName}DTO>> queryList(@Valid ${table.entityName}QueryReq ${table.entityName?uncap_first}QueryReq){ PageResp<${table.entityName}DTO> list = ${table.entityName?uncap_first}Service.queryList(${table.entityName?uncap_first}QueryReq); return new CommonResp<>(list); } @PostMapping("/save") <#-- @RequiresPermissions("sys:${table.entityName?uncap_first}:update")--> @ApiOperation("${table.entityName}修改") public CommonResp update(@Valid @RequestBody ${table.entityName} ${table.entityName?uncap_first}) { return ${table.entityName?uncap_first}Service.modify(${table.entityName?uncap_first}); } @DeleteMapping(value = "/delete/{ids}") <#-- @RequiresPermissions("sys:${table.entityName?uncap_first}:delete")--> @ApiOperation("${table.entityName}删除(单个条目)") public CommonResp remove(@NotBlank(message = "{required}") @PathVariable String ids) { return ${table.entityName?uncap_first}Service.remove(ids); } } ``` service.java.ftl ```ftl package ${package.Service}; import com.Java.train.common.response.CommonResp; import ${package.Entity}.${entity}; import ${superServiceClassPackage}; import com.Java.train.common.resp.PageResp; import com.Java.train.business.entity.DTO.${table.entityName!}DTO; import com.Java.train.business.req.${table.entityName!}QueryReq; import java.util.List; /** * @author ${author} * @since ${date} */ <#if kotlin> interface ${table.serviceName} : ${superServiceClass}<${entity}> <#else> public interface ${table.serviceName} extends ${superServiceClass}<${entity}> { /** * ${table.entityName!}详情 * @param * @return */ PageResp<${table.entityName!}DTO> queryList(${table.entityName!}QueryReq ${table.entityName?uncap_first}QueryReq); /** * ${table.entityName!}保存和修改 * @param ${table.entityName?uncap_first} 根据需要进行传值 * @return */ CommonResp modify(${entity} ${table.entityName?uncap_first}); /** * ${table.entityName!}删除 * @param ids * @return */ CommonResp remove(String ids); } ``` service.Impl.java.ftl ```ftl package ${package.ServiceImpl}; import com.Java.train.common.response.CommonResp; import com.Java.train.business.entity.DTO.${table.entityName!}DTO; import com.Java.train.business.req.${table.entityName!}QueryReq; import com.Java.train.common.exception.BusniessException; import com.Java.train.common.exception.BusniessExceptionEnum; import com.Java.train.common.resp.PageResp; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import ${package.Entity}.${entity}; import ${package.Mapper}.${table.mapperName}; import ${package.Service}.${table.serviceName}; import ${superServiceImplClassPackage}; import org.springframework.stereotype.Service; import com.Java.train.common.context.LoginMemberContext; import com.Java.train.common.utils.SnowFlakeUtil; import org.springframework.beans.factory.annotation.Autowired; import cn.hutool.core.util.StrUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import org.springframework.util.CollectionUtils; import cn.hutool.core.date.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Arrays; import java.util.Collections; /** * ${table.comment!} 服务实现类 * * @author ${author} * @since ${date} */ @Service <#if kotlin> open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} { } <#else> public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} { private static final Logger LOG = LoggerFactory.getLogger(${table.entityName}Service.class); @Autowired ${table.entityName}Mapper ${table.entityName?uncap_first}Mapper; @Override public PageResp<${table.entityName}DTO> queryList(${table.entityName}QueryReq ${table.entityName?uncap_first}QueryReq){ // 1.获取当前用户id信息 // Long id = LoginMemberContext.getId(); // 2.查询数据库中是否存在当前信息(id) LOG.info("查询页码:{}", ${table.entityName?uncap_first}QueryReq.getPage()); LOG.info("每页条数:{}", ${table.entityName?uncap_first}QueryReq.getSize()); PageHelper.startPage(${table.entityName?uncap_first}QueryReq.getPage(),${table.entityName?uncap_first}QueryReq.getSize()); List<${table.entityName}> ${table.entityName?uncap_first}s = list(); if(${table.entityName?uncap_first}s == null || CollUtil.isEmpty(${table.entityName?uncap_first}s)){ throw new BusniessException(BusniessExceptionEnum.MEMBER_PASSENGER_NOT_EXITS); } // 3 分析分页信息 // 3.1 获取分页总条数和页数 PageInfo<${table.entityName}> pageInfo = new PageInfo<>(${table.entityName?uncap_first}s); LOG.info("总行数:{}", pageInfo.getTotal()); LOG.info("总页数:{}", pageInfo.getPages()); // 3.2 转为封装后的返回值并封装到分页返回值中 List<${table.entityName}DTO> list = BeanUtil.copyToList(${table.entityName?uncap_first}s, ${table.entityName}DTO.class); PageResp<${table.entityName}DTO> pageResp = new PageResp<>(); pageResp.setTotal(pageInfo.getTotal()); pageResp.setList(list); // 4.返回查询到的信息 return pageResp; } @Override public CommonResp modify(${table.entityName} ${table.entityName?uncap_first}) { Long id = ${table.entityName?uncap_first}.getId(); DateTime dateTime = DateTime.now(); if(ObjectUtil.isNull(id)){ ${table.entityName?uncap_first}.setId(SnowFlakeUtil.getSnowFlakeNextId()); ${table.entityName?uncap_first}.setCreateTime(dateTime); ${table.entityName?uncap_first}.setUpdateTime(dateTime); boolean save = save(${table.entityName?uncap_first}); return new CommonResp<>(save); } else { ${table.entityName?uncap_first}.setUpdateTime(dateTime); boolean update = updateById(${table.entityName?uncap_first}); return new CommonResp<>(update); } } @Override public CommonResp remove(String ids) { if(StrUtil.isNotEmpty(ids)){ String[] array = ids.split(","); if(!CollectionUtils.isEmpty(Arrays.asList(array))) { return new CommonResp<>(${table.entityName?uncap_first}Mapper.deleteBatchIds(Arrays.asList(array))); } } return new CommonResp<>(Collections.EMPTY_LIST); } } ``` ## (二)业务逻辑,表逻辑 本项目对火车和车站的设计表有:**车站Station**(基本信息:名称、拼音缩写等)、**火车信息Train**(车次编号)、**火车车站**(车次编号和站序(idx))、**火车车厢Train-carriage**(车次编号、厢号、座位类型(通过座位类型确定列数)和行数)和**火车车座**train-seat(车次编号、厢号、列号(座次等级划分好的枚举类(ABCDF..) )座位类型) **按火车的车次连接其所有的车站、车厢和车座位信息,因此生成或删除时都需要通过当前的车次来进行** 其他增删改查不解释,注意添加前记得查重,(目前没解决如何删除列车时,删除该车的其他信息) 生成座位接口: 调用url写在train的controller中,调用的是TrainSeatService的gen-seat方法,传入一键生成的火车车次id(trainCode) 流程: 1. 通过车次id查看是否生成seat,生成则删除所有座位信息 2. 获取当前车次下的所有厢号list,然后遍历生成每个厢号下的座位 3. 对于每个厢号,获取行数遍历,然后再根据座位等级遍历列数枚举数组,生成座位编号('0' + 行号),厢里座位id(seatIdx累加) 4. 生成新座位,根据所有信息set进去,然后保存 因为无论火车、车站、车厢还是车座亦或是每日的信息,其代码很通用,就是增删改查,因此此处指展示通汇其他类的火车类 Train: entity ```java package com.Java.train.business.entity; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.sql.Time; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; /** *

* 车次 *

* * @author luomoxingchen * @since 2024-09-22 */ @Getter @Setter @Accessors(chain = true) @TableName("train") @ApiModel(value = "Train对象", description = "车次") public class Train implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("id") @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; @ApiModelProperty("车次编号") @TableField("code") private String code; @ApiModelProperty("车次类型|枚举[TrainTypeEnum]") @TableField("type") private String type; @ApiModelProperty("始发站") @TableField("start") private String start; @ApiModelProperty("始发站拼音") @TableField("start_pinyin") private String startPinyin; @ApiModelProperty("出发时间") @TableField("start_time") @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8") private Time startTime; @ApiModelProperty("终点站") @TableField("end") private String end; @ApiModelProperty("终点站拼音") @TableField("end_pinyin") private String endPinyin; @ApiModelProperty("到站时间") @TableField("end_time") @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8") private Time endTime; @ApiModelProperty("新增时间") @TableField(value = "create_time", fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @ApiModelProperty("修改时间") @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; } ``` TrainControler ```java package com.Java.train.business.controller; import com.Java.train.business.entity.DTO.TrainDTO; import com.Java.train.business.req.TrainQueryReq; import com.Java.train.business.service.TrainSeatService; import com.Java.train.business.service.TrainService; import com.Java.train.common.response.CommonResp; import com.Java.train.common.response.PageResp; import com.Java.train.business.entity.Train; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.List; /** * @author luomoxingchen * @since 2024-09-21 */ @RestController @RequestMapping("/admin/train") public class TrainAdminController { @Autowired private TrainService trainService; @Autowired private TrainSeatService trainSeatService; @GetMapping("/query-list") @ApiOperation("Train查询全部") public CommonResp> queryList(@Valid TrainQueryReq trainQueryReq){ PageResp list = trainService.queryList(trainQueryReq); return new CommonResp<>(list); } @PostMapping("/save") @ApiOperation("Train修改") public CommonResp update(@Valid @RequestBody Train train) { return trainService.modify(train); } @DeleteMapping(value = "/delete/{ids}") @ApiOperation("Train删除(单个条目)") public CommonResp remove(@NotBlank(message = "{required}") @PathVariable String ids) { return trainService.remove(ids); } @GetMapping("/query-all") @ApiOperation("Train查询全部") public CommonResp> queryAll(){ List list = trainService.queryAll(); return new CommonResp<>(list); } @GetMapping(value = "/gen-seat/{id}") @ApiOperation("TrainSeat生成座位数") public CommonResp genSeat(@NotBlank(message = "{required}") @PathVariable(value = "id") String trainCode) { return trainSeatService.genSeat(trainCode); } } ``` DTO(实际改为VO,即返还前端信息的类) 该项目基本都直接将entity类直接复制来的,注意在此项目中的DTO类基本都如此 ```java package com.Java.train.business.entity.DTO; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import java.io.Serializable; import java.sql.Time; import java.util.Date; @Getter @Setter @Accessors(chain = true) public class TrainDTO implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("id") private Long id; @ApiModelProperty("车次编号") private String code; @ApiModelProperty("车次类型|枚举[TrainTypeEnum]") private String type; @ApiModelProperty("始发站") private String start; @ApiModelProperty("始发站拼音") private String startPinyin; @ApiModelProperty("出发时间") @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8") private Time startTime; @ApiModelProperty("终点站") private String end; @ApiModelProperty("终点站拼音") private String endPinyin; @ApiModelProperty("到站时间") @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8") private Time endTime; @ApiModelProperty("新增时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @ApiModelProperty("修改时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; } ``` Req(应为DTO,前端查询所需的参数) ```java package com.Java.train.business.req; import com.Java.train.common.req.PageReq; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @EqualsAndHashCode public class TrainQueryReq extends PageReq { } ``` Service ```java package com.Java.train.business.service; import com.Java.train.common.response.CommonResp; import com.Java.train.business.entity.Train; import com.baomidou.mybatisplus.extension.service.IService; import com.Java.train.common.response.PageResp; import com.Java.train.business.entity.DTO.TrainDTO; import com.Java.train.business.req.TrainQueryReq; import java.util.List; /** * @author luomoxingchen * @since 2024-09-22 */ public interface TrainService extends IService { /** * Train详情 * @param * @return */ PageResp queryList(TrainQueryReq trainQueryReq); /** * Train保存和修改 * @param train 根据需要进行传值 * @return */ CommonResp modify(Train train); /** * Train删除 * @param ids * @return */ CommonResp remove(String ids); List queryAll(); } ``` ServiceImpl ```java package com.Java.train.business.service.Impl; import com.Java.train.business.entity.*; import com.Java.train.business.service.TrainCarriageService; import com.Java.train.business.service.TrainSeatService; import com.Java.train.business.service.TrainStationService; import com.Java.train.common.response.CommonResp; import com.Java.train.business.entity.DTO.TrainDTO; import com.Java.train.business.req.TrainQueryReq; import com.Java.train.common.exception.BusniessException; import com.Java.train.common.exception.BusniessExceptionEnum; import com.Java.train.common.response.PageResp; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.Java.train.business.mapper.TrainMapper; import com.Java.train.business.service.TrainService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import com.Java.train.common.utils.SnowFlakeUtil; import org.springframework.beans.factory.annotation.Autowired; import cn.hutool.core.util.StrUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import cn.hutool.core.date.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Arrays; import java.util.Collections; /** * 车次 服务实现类 * * @author luomoxingchen * @since 2024-09-22 */ @Service public class TrainServiceImpl extends ServiceImpl implements TrainService { private static final Logger LOG = LoggerFactory.getLogger(TrainService.class); @Autowired TrainMapper trainMapper; @Resource TrainStationService trainStationService; @Resource TrainCarriageService trainCarriageService; @Resource TrainSeatService trainSeatService; @Override public PageResp queryList(TrainQueryReq trainQueryReq){ // 1.获取当前用户id信息 // Long id = LoginMemberContext.getId(); // 2.查询数据库中是否存在当前信息(id) LOG.info("查询页码:{}", trainQueryReq.getPage()); LOG.info("每页条数:{}", trainQueryReq.getSize()); PageHelper.startPage(trainQueryReq.getPage(),trainQueryReq.getSize()); List trains = list(); if(trains == null || CollUtil.isEmpty(trains)){ // 返回空列表 return new PageResp(); // throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_NOT_EXITS); } // 3 分析分页信息 // 3.1 获取分页总条数和页数 PageInfo pageInfo = new PageInfo<>(trains); LOG.info("总行数:{}", pageInfo.getTotal()); LOG.info("总页数:{}", pageInfo.getPages()); // 3.2 转为封装后的返回值并封装到分页返回值中 List list = BeanUtil.copyToList(trains, TrainDTO.class); PageResp pageResp = new PageResp<>(); pageResp.setTotal(pageInfo.getTotal()); pageResp.setList(list); // 4.返回查询到的信息 return pageResp; } @Override public CommonResp modify(Train train) { Long id = train.getId(); DateTime dateTime = DateTime.now(); if(ObjectUtil.isNull(id)){ // 查重 Train TrainDB = getTrainByUnique(train.getCode()); if(ObjectUtil.isNotEmpty(TrainDB)){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_ALREADY_EXITS); } train.setId(SnowFlakeUtil.getSnowFlakeNextId()); train.setCreateTime(dateTime); train.setUpdateTime(dateTime); boolean save = save(train); return new CommonResp<>(save); } else { train.setUpdateTime(dateTime); boolean update = updateById(train); return new CommonResp<>(update); } } @Override @Transactional // 因为涉及删除多表,因此开了个事务 public CommonResp remove(String ids) { if (StrUtil.isNotEmpty(ids)) { String[] array = ids.split(","); if (!CollectionUtils.isEmpty(Arrays.asList(array))) { // 删除ids所有有关的信息 Train train = getById(ids); String trainCode = train.getCode(); trainStationService.remove(new LambdaQueryWrapper().eq(TrainStation::getTrainCode, trainCode)); trainCarriageService.remove(new LambdaQueryWrapper().eq(TrainCarriage::getTrainCode, trainCode)); trainSeatService.remove(new LambdaQueryWrapper().eq(TrainSeat::getTrainCode, trainCode)); return new CommonResp<>(trainMapper.deleteBatchIds(Arrays.asList(array))); } } return new CommonResp<>(Collections.EMPTY_LIST); } @Override public List queryAll() { List trains = list(); if(trains == null || CollUtil.isEmpty(trains)){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_NOT_EXITS); } // 3.2 转为封装后的返回值并封装到分页返回值中 List list = BeanUtil.copyToList(trains, TrainDTO.class); // 4.返回查询到的信息 return list; } // 通过唯一键校验查重 private Train getTrainByUnique(String code) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); Train one = getOne(wrapper.eq(Train::getCode, code)); return one; } } ``` ## (三)每日任务生成 daily类基本都是通过其基本类复制类,只是多了日期(注意日期格式(后端接收前端的日期是需要转化为@DateTimeFormat(pattern = "yyyy-MM-dd"),后端转前端(@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")) 存在每日火车、每日车站、每日车厢和每日车座,生成每日信息的方法写在DailyTrain中,在其中调度其他每日的方法 ```java package com.Java.train.business.service.daily.Impl; import cn.hutool.core.date.DateUtil; import com.Java.train.business.entity.*; import com.Java.train.business.entity.DTO.TrainDTO; import com.Java.train.business.service.TrainService; import com.Java.train.business.service.daily.DailyTrainCarriageService; import com.Java.train.business.service.daily.DailyTrainSeatService; import com.Java.train.business.service.daily.DailyTrainStationService; import com.Java.train.common.response.CommonResp; import com.Java.train.business.entity.DTO.DailyTrainDTO; import com.Java.train.business.req.daily.DailyTrainQueryReq; import com.Java.train.common.exception.BusniessException; import com.Java.train.common.exception.BusniessExceptionEnum; import com.Java.train.common.response.PageResp; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.Java.train.business.mapper.DailyTrainMapper; import com.Java.train.business.service.daily.DailyTrainService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import com.Java.train.common.utils.SnowFlakeUtil; import org.springframework.beans.factory.annotation.Autowired; import cn.hutool.core.util.StrUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import org.springframework.util.CollectionUtils; import cn.hutool.core.date.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * 每日车次 服务实现类 * * @author luomoxingchen * @since 2024-09-24 */ @Service public class DailyTrainServiceImpl extends ServiceImpl implements DailyTrainService { private static final Logger LOG = LoggerFactory.getLogger(DailyTrainService.class); @Autowired DailyTrainMapper dailyTrainMapper; @Resource TrainService trainService; @Resource DailyTrainCarriageService dailyTrainCarriageService; @Resource DailyTrainStationService dailyTrainStationService; @Resource DailyTrainSeatService dailyTrainSeatService; /** * @Description: 查询所有每日车次(含条件筛选) * @param * @return */ @Override public PageResp queryList(DailyTrainQueryReq dailyTrainQueryReq){ // 1.获取当前用户id信息 // Long id = LoginMemberContext.getId(); Date date = dailyTrainQueryReq.getDate(); String trainCode = dailyTrainQueryReq.getCode(); // 2.查询数据库中是否存在当前信息(id) LOG.info("查询页码:{}", dailyTrainQueryReq.getPage()); LOG.info("每页条数:{}", dailyTrainQueryReq.getSize()); PageHelper.startPage(dailyTrainQueryReq.getPage(),dailyTrainQueryReq.getSize()); // 判重 List dailyTrains = null; if(StrUtil.isNotBlank(trainCode) || ObjectUtil.isNotEmpty(date)){ dailyTrains = getDailyTrainByCondition(trainCode, date); }else { dailyTrains = list(); } if(dailyTrains == null || CollUtil.isEmpty(dailyTrains)){ // 返回空列表 return new PageResp(); // throw new BusniessException(BusniessExceptionEnum.MEMBER_PASSENGER_NOT_EXITS); } // 3 分析分页信息 // 3.1 获取分页总条数和页数 PageInfo pageInfo = new PageInfo<>(dailyTrains); LOG.info("总行数:{}", pageInfo.getTotal()); LOG.info("总页数:{}", pageInfo.getPages()); // 3.2 转为封装后的返回值并封装到分页返回值中 List list = BeanUtil.copyToList(dailyTrains, DailyTrainDTO.class); PageResp pageResp = new PageResp<>(); pageResp.setTotal(pageInfo.getTotal()); pageResp.setList(list); // 4.返回查询到的信息 return pageResp; } /** * @Description: 保存或修改每日车次数据 * @param * @return */ @Override public CommonResp modify(DailyTrain dailyTrain) { Long id = dailyTrain.getId(); DateTime dateTime = DateTime.now(); if(ObjectUtil.isNull(id)){ // 查重 DailyTrain TrainDB = getTrainByUnique(dailyTrain.getCode(), dailyTrain.getDate()); if(ObjectUtil.isNotEmpty(TrainDB)){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_ALREADY_EXITS); } dailyTrain.setId(SnowFlakeUtil.getSnowFlakeNextId()); dailyTrain.setCreateTime(dateTime); dailyTrain.setUpdateTime(dateTime); boolean save = save(dailyTrain); return new CommonResp<>(save); } else { dailyTrain.setUpdateTime(dateTime); boolean update = updateById(dailyTrain); return new CommonResp<>(update); } } /** * @Description: 删除每日数据 * @param * @return */ @Override public CommonResp remove(String ids) { if(StrUtil.isNotEmpty(ids)){ String[] array = ids.split(","); if(!CollectionUtils.isEmpty(Arrays.asList(array))) { // 删除ids所有有关的信息 DailyTrain train = getById(ids); String trainCode = train.getCode(); dailyTrainStationService.remove(new LambdaQueryWrapper().eq(DailyTrainStation::getTrainCode, trainCode).eq(DailyTrainStation::getDate, train.getDate())); dailyTrainCarriageService.remove(new LambdaQueryWrapper().eq(DailyTrainCarriage::getTrainCode, trainCode).eq(DailyTrainCarriage::getDate, train.getDate())); dailyTrainSeatService.remove(new LambdaQueryWrapper().eq(DailyTrainSeat::getTrainCode, trainCode).eq(DailyTrainSeat::getDate, train.getDate())); return new CommonResp<>(dailyTrainMapper.deleteBatchIds(Arrays.asList(array))); } } return new CommonResp<>(Collections.EMPTY_LIST); } /** * @param * @Description: 根据日期生成每日车次数据(跑批任务) */ @Override public void genDaily(Date date) { // 1.查询是否存在火车 List trainDTOS = trainService.queryAll(); List trains = BeanUtil.copyToList(trainDTOS, Train.class); if(CollUtil.isEmpty(trains)){ LOG.info("没有火车信息"); throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_NOT_EXITS); } // 2.复制生成dailyTrain for (Train train : trains) { // 2.1 删除之前生成的火车 // 转换时间格式(方法默认是yyyy-MM-dd) LOG.info("生成日期为[{}]车次编号为[{}]的火车数据开始", DateUtil.formatDate(date), train.getCode()); genDailyTrain(date, train); LOG.info("生成日期为[{}]车次编号为[{}]的火车数据结束", DateUtil.formatDate(date), train.getCode()); } } /** * @Description: 根据日期删除之前并生成每日车次数据 * @param * @return */ private void genDailyTrain(Date date, Train train) { // 1.删除已有的火车信息 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); remove(wrapper.eq(DailyTrain::getDate, date).eq(DailyTrain::getCode, train.getCode())); // 2.生成当前日期的每日火车信息 DateTime dateTime = DateTime.now(); DailyTrain dailyTrain = BeanUtil.copyProperties(train, DailyTrain.class); dailyTrain.setId(SnowFlakeUtil.getSnowFlakeNextId()); dailyTrain.setCreateTime(dateTime); dailyTrain.setUpdateTime(dateTime); dailyTrain.setDate(date); save(dailyTrain); // 保存每日车厢信息 dailyTrainCarriageService.genDaily(date, train.getCode()); // 生成每日车站信息 dailyTrainStationService.genDaily(date, train.getCode()); // 生成每日车座信息 dailyTrainSeatService.getDaily(date, train.getCode()); } /** * @Description: 条件查询 * @param * @return */ // 通过唯一键校验查重 private List getDailyTrainByCondition(String trainCode, Date date) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); List list = null; if(StrUtil.isNotBlank(trainCode) && ObjectUtil.isEmpty(date)){ list = list(wrapper.eq(DailyTrain::getCode, trainCode).orderBy(true, true, DailyTrain::getCode)); } else if(ObjectUtil.isNotEmpty(date) && StrUtil.isBlank(trainCode)){ list = list(wrapper.eq(DailyTrain::getDate, date).orderBy(true, true, DailyTrain::getDate)); } else{ // 先按车号,在按车号降序 list = list(wrapper.eq(DailyTrain::getCode, trainCode).eq(DailyTrain::getDate, date).orderBy(true, true, DailyTrain::getDate, DailyTrain::getCode) .orderBy(true, false, DailyTrain::getDate)); } return list; } /** * @Description: 唯一键判重 * @param * @return */ // 通过唯一键校验查重 private DailyTrain getTrainByUnique(String code, Date date) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); DailyTrain one = getOne(wrapper.eq(DailyTrain::getCode, code).eq(DailyTrain::getDate, date)); return one; } } ``` 开设自己模块batch,其结构图如下: ![image-20241001210508652](D:\Learning\Java\笔记图片\12306\batch板块架构.png) 因为涉及在此自生成其他业务的信息,因此肯定需要实现跨服务(调用其他接口下的方法),就此引入cloud的**openFeign**实现跨服务调用 ### OpenFeign [SpringCloud OpenFeign 全功能配置详解(一文吃透OpenFeign)-CSDN博客](https://blog.csdn.net/weixin_44606481/article/details/132499972#:~:text=1、使用配置文件配置#:~:text=1、使用配置文件配置) #### 1、概述: Open[Feign客户端](https://so.csdn.net/so/search?q=Feign客户端&spm=1001.2101.3001.7020)是一个web声明式http远程调用工具,直接可以根据服务名称去注册中心拿到指定的服务IP集合,提供了接口和注解方式进行调用,内嵌集成了Ribbon本地负载均衡器。 #### 2、使用步骤: 1. 引入依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-loadbalancer ``` 2. 添加启动项注解 ```java @SpringBootApplication @ComponentScan("com.Java") @MapperScan("com.Java.train.*.mapper") @EnableFeignClients("com.Java.train.batch.feign") public class BatchApplication { } ``` 3. 创建Feign接口 ```java package com.Java.train.batch.feign; import com.Java.train.common.response.CommonResp; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import java.util.Date; @FeignClient(name = "train-business", url = "http://127.0.0.1:8002/business") // public interface BusinessFeign { @GetMapping("/hello") String hello(); @GetMapping("/admin/daily-train/gen-daily/{date}") CommonResp genDaily(@PathVariable(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date); } ``` ### Quartz [Quartz 快速入门案例,看这一篇就够了-CSDN博客](https://blog.csdn.net/qq_34383510/article/details/129021655?ops_request_misc=%7B%22request%5Fid%22%3A%22DD71B13B-0496-45A1-BF8D-350894E5A77C%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=DD71B13B-0496-45A1-BF8D-350894E5A77C&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-129021655-null-null.142^v100^pc_search_result_base6&utm_term=Quartz&spm=1018.2226.3001.4187) #### 1、概述 Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB 作业预构建,JavaMail 及其它,支持 cron-like 表达式等等。 #### 2、使用步骤 1. 引入依赖 ```xml org.springframework.boot spring-boot-starter-quartz ``` 2. 配置调度器(Scheduler) ```java package com.Java.train.batch.config; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import javax.sql.DataSource; import java.io.IOException; @Configuration public class SchedulerConfig { @Resource private MyJobFactory myJobFactory; @Bean public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("dataSource") DataSource dataSource) throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setDataSource(dataSource); factory.setJobFactory(myJobFactory); factory.setStartupDelay(2); return factory; } } ``` 3. 配置调度中心Controller ```java package com.Java.train.batch.controller; import com.Java.train.batch.req.CronJobReq; import com.Java.train.batch.resp.CronJobResp; import com.Java.train.common.response.CommonResp; import com.Java.train.batch.req.CronJobReq; import com.Java.train.batch.resp.CronJobResp; import com.Java.train.common.response.CommonResp; import org.quartz.*; import org.quartz.impl.matchers.GroupMatcher; import org.quartz.impl.triggers.CronTriggerImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.Date; import java.util.List; @RestController @RequestMapping(value = "/admin/job") public class JobController { private static Logger LOG = LoggerFactory.getLogger(JobController.class); @Autowired private SchedulerFactoryBean schedulerFactoryBean; @RequestMapping(value = "/run") public CommonResp run(@RequestBody CronJobReq cronJobReq) throws SchedulerException { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); LOG.info("手动执行任务开始:{}, {}", jobClassName, jobGroupName); schedulerFactoryBean.getScheduler().triggerJob(JobKey.jobKey(jobClassName, jobGroupName)); return new CommonResp<>(); } @RequestMapping(value = "/add") public CommonResp add(@RequestBody CronJobReq cronJobReq) { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); String cronExpression = cronJobReq.getCronExpression(); String description = cronJobReq.getDescription(); LOG.info("创建定时任务开始:{},{},{},{}", jobClassName, jobGroupName, cronExpression, description); CommonResp commonResp = new CommonResp(); try { // 通过SchedulerFactory获取一个调度器实例 Scheduler sched = schedulerFactoryBean.getScheduler(); // 启动调度器 sched.start(); //构建job信息 JobDetail jobDetail = JobBuilder.newJob((Class) Class.forName(jobClassName)).withIdentity(jobClassName, jobGroupName).build(); //表达式调度构建器(即任务执行的时间) CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName).withDescription(description).withSchedule(scheduleBuilder).build(); sched.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { LOG.error("创建定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("创建定时任务失败:调度异常"); } catch (ClassNotFoundException e) { LOG.error("创建定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("创建定时任务失败:任务类不存在"); } LOG.info("创建定时任务结束:{}", commonResp); return commonResp; } @RequestMapping(value = "/pause") public CommonResp pause(@RequestBody CronJobReq cronJobReq) { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); LOG.info("暂停定时任务开始:{},{}", jobClassName, jobGroupName); CommonResp commonResp = new CommonResp(); try { Scheduler sched = schedulerFactoryBean.getScheduler(); sched.pauseJob(JobKey.jobKey(jobClassName, jobGroupName)); } catch (SchedulerException e) { LOG.error("暂停定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("暂停定时任务失败:调度异常"); } LOG.info("暂停定时任务结束:{}", commonResp); return commonResp; } @RequestMapping(value = "/resume") public CommonResp resume(@RequestBody CronJobReq cronJobReq) { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); LOG.info("重启定时任务开始:{},{}", jobClassName, jobGroupName); CommonResp commonResp = new CommonResp(); try { Scheduler sched = schedulerFactoryBean.getScheduler(); sched.resumeJob(JobKey.jobKey(jobClassName, jobGroupName)); } catch (SchedulerException e) { LOG.error("重启定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("重启定时任务失败:调度异常"); } LOG.info("重启定时任务结束:{}", commonResp); return commonResp; } @RequestMapping(value = "/reschedule") public CommonResp reschedule(@RequestBody CronJobReq cronJobReq) { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); String cronExpression = cronJobReq.getCronExpression(); String description = cronJobReq.getDescription(); LOG.info("更新定时任务开始:{},{},{},{}", jobClassName, jobGroupName, cronExpression, description); CommonResp commonResp = new CommonResp(); try { Scheduler scheduler = schedulerFactoryBean.getScheduler(); TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName); // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); CronTriggerImpl trigger1 = (CronTriggerImpl) scheduler.getTrigger(triggerKey); trigger1.setStartTime(new Date()); // 重新设置开始时间 CronTrigger trigger = trigger1; // 按新的cronExpression表达式重新构建trigger trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withDescription(description).withSchedule(scheduleBuilder).build(); // 按新的trigger重新设置job执行 scheduler.rescheduleJob(triggerKey, trigger); } catch (Exception e) { LOG.error("更新定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("更新定时任务失败:调度异常"); } LOG.info("更新定时任务结束:{}", commonResp); return commonResp; } @RequestMapping(value = "/delete") public CommonResp delete(@RequestBody CronJobReq cronJobReq) { String jobClassName = cronJobReq.getName(); String jobGroupName = cronJobReq.getGroup(); LOG.info("删除定时任务开始:{},{}", jobClassName, jobGroupName); CommonResp commonResp = new CommonResp(); try { Scheduler scheduler = schedulerFactoryBean.getScheduler(); scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName)); scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName)); scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName)); } catch (SchedulerException e) { LOG.error("删除定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("删除定时任务失败:调度异常"); } LOG.info("删除定时任务结束:{}", commonResp); return commonResp; } @RequestMapping(value="/query") public CommonResp query() { LOG.info("查看所有定时任务开始"); CommonResp commonResp = new CommonResp(); List cronJobDtoList = new ArrayList(); try { Scheduler scheduler = schedulerFactoryBean.getScheduler(); for (String groupName : scheduler.getJobGroupNames()) { for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) { CronJobResp cronJobResp = new CronJobResp(); cronJobResp.setName(jobKey.getName()); cronJobResp.setGroup(jobKey.getGroup()); //get job's trigger List triggers = (List) scheduler.getTriggersOfJob(jobKey); CronTrigger cronTrigger = (CronTrigger) triggers.get(0); cronJobResp.setNextFireTime(cronTrigger.getNextFireTime()); cronJobResp.setPreFireTime(cronTrigger.getPreviousFireTime()); cronJobResp.setCronExpression(cronTrigger.getCronExpression()); cronJobResp.setDescription(cronTrigger.getDescription()); Trigger.TriggerState triggerState = scheduler.getTriggerState(cronTrigger.getKey()); cronJobResp.setState(triggerState.name()); cronJobDtoList.add(cronJobResp); } } } catch (SchedulerException e) { LOG.error("查看定时任务失败:" + e); commonResp.setSuccess(false); commonResp.setMessage("查看定时任务失败:调度异常"); } commonResp.setContent(cronJobDtoList); LOG.info("查看定时任务结束:{}", commonResp); return commonResp; } } ``` 4. 导入JobFactory,配置CronJobReq JobFactory ```java package com.Java.train.batch.config; import jakarta.annotation.Resource; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.scheduling.quartz.SpringBeanJobFactory; import org.springframework.stereotype.Component; @Component public class MyJobFactory extends SpringBeanJobFactory { @Resource private AutowireCapableBeanFactory beanFactory; /** * 这里覆盖了super的createJobInstance方法,对其创建出来的类再进行autowire。 */ @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobInstance = super.createJobInstance(bundle); beanFactory.autowireBean(jobInstance); return jobInstance; } } ``` CronJobReq ```java package com.Java.train.batch.req; public class CronJobReq { private String group; private String name; private String description; private String cronExpression; @Override public String toString() { final StringBuffer sb = new StringBuffer("CronJobDto{"); sb.append("cronExpression='").append(cronExpression).append('\''); sb.append(", group='").append(group).append('\''); sb.append(", name='").append(name).append('\''); sb.append(", description='").append(description).append('\''); sb.append('}'); return sb.toString(); } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } public String getCronExpression() { return cronExpression; } public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` 5. 实现Job接口重写方法 ```java package com.Java.train.batch.job; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.RandomUtil; import com.Java.train.batch.feign.BusinessFeign; import com.Java.train.common.response.CommonResp; import jakarta.annotation.Resource; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactoryFriend; import org.slf4j.MDC; import java.util.Date; import java.util.Random; public class DailyTrainJob implements Job { private static final Logger LOG = LoggerFactory.getLogger(DailyTrainJob.class); @Resource BusinessFeign businessFeign; @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { // 增加日志流水号 MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(3)); LOG.info("生成15天后的火车数据操作开始"); Date date = new Date(); DateTime offsetDay = DateUtil.offsetDay(date, 15); Date jdkDate = offsetDay.toJdkDate(); CommonResp commonResp = businessFeign.genDaily(jdkDate); LOG.info("生成15天后的火车数据操作结束"); } } ``` ## (四)订单模块(核心业务代码) 其中完成最核心的代码(为会员挑选座位、生成订单信息、完成选中并扣减车座的Sell(售卖情况)、削减用户车票车程影响其他车程的余票存量、生成用户车票信息、并最终确定订单信息) ### 1、类介绍: 控制类: ConfirmOrderAdminController —— 用于后台订单信息的保存 ConfirmOrderController —— 核心业务doConfirm(初始化会员订单信息,挑选车票、选座(genSeat方法)、事务处理购票后的数据,修改sell(calSell方法)、扣减库存) AfterDoConfirm —— 更改车票本身数据、更改该车票影响其他站点车票的数量、修改座位sell信息,扣减余票信息中的数量、为用户增添订单信息、更改订单状态 请求类: ConfirmOrderQueryReq —— 订单信息的查询(查询当前会员的订单信息) ConfirmOrderDoReq —— 会员发起核心业务doConfirm所需的参数(前端请求) MemberTicketReq —— 生成的选中车票信息(保存在Common模块下) ### 2、具体流程: doConfirm(①校验车次、余票存在或日期 -> ②初始化会员订单信息、查询余票数量,模拟扣减,判断是否充足 -> ③选座:3.1 获取所有车票,并查看是否选座(看车票第一个值就行) 3.2 按座位类型创建选座列表 3.3 计算每个座位相对第一个选座位置的偏移量 3.4 genSeat方法筛出每日对应车厢 -> ④ 事务处理购票后的数据(afterDoConfirm类的方法) doConfirm ```java @Override public void doConfim(ConfirmOrderDoReq confirmOrderDoReq) { // 1.1 校验该车次是否存在 String trainCode = confirmOrderDoReq.getTrainCode(); Train train = trainService.getOne(new LambdaQueryWrapper().eq(Train::getCode, trainCode)); if(ObjectUtil.isEmpty(train)){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_NOT_EXITS); } // 1.2 校验该余票是否存在 Date date = confirmOrderDoReq.getDate(); LOG.info("日期为:{}", date); String start = confirmOrderDoReq.getStart(); String end = confirmOrderDoReq.getEnd(); // 通过唯一键查找票 DailyTrainTicket ticket = dailyTrainTicketService.getOne(new LambdaQueryWrapper().eq(DailyTrainTicket::getTrainCode, trainCode) .eq(DailyTrainTicket::getDate, date).eq(DailyTrainTicket::getStart, start).eq(DailyTrainTicket::getEnd, end)); LOG.info("查出的唯一车票为:{}", ticket); if(ObjectUtil.isEmpty(ticket)){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TICKET_NOT_EXITS); } // 1.3 校验该票是否已经过期 DateTime now = DateTime.now(); // parse1小于parse2返回-1,parse1大于parse2返回1,相等返回0 int is_passOver = date.compareTo(now); if(is_passOver == -1){ throw new BusniessException(BusniessExceptionEnum.BUSINESS_TICKET_TIME_PASS); } // 2.1 初始化会员订单信息 ConfirmOrder confirmOrder = new ConfirmOrder(); confirmOrder.setId(SnowFlakeUtil.getSnowFlakeNextId()); confirmOrder.setMemberId(LoginMemberContext.getId()); confirmOrder.setDate(date); confirmOrder.setTrainCode(trainCode); confirmOrder.setStart(start); confirmOrder.setEnd(end); confirmOrder.setDailyTrainTicketId(confirmOrderDoReq.getDailyTrainTicketId()); confirmOrder.setTickets(JSON.toJSONString(confirmOrderDoReq.getTickets())); confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode()); confirmOrder.setCreateTime(now); confirmOrder.setUpdateTime(now); LOG.info("订单信息如下:{}", confirmOrder); save(confirmOrder); // 2.2 查询余票数量,模拟扣减,判断是否充足 int virYdz = ticket.getYdz(), virEdz = ticket.getEdz(), virRw = ticket.getRw(), virYw = ticket.getYw(); List tickets = confirmOrderDoReq.getTickets(); for (ConfirmOrderTicketDTO ticketDTO : tickets) { String seatTypeCode = ticketDTO.getSeatTypeCode(); SeatTypeEnum typeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode); // 消减虚拟票数(没有存到数据库) ticketCountDecrease(ticket, typeEnum); } // 3.选座 // 3.1 获取所有车票,并查看是否选座(看车票第一个值就行) ConfirmOrderTicketDTO confirmOrderTicketDTO0 = tickets.get(0); String seatTypeCode = confirmOrderTicketDTO0.getSeatTypeCode(); // 获取起始站点和终止站点 Integer startIndex = ticket.getStartIndex(); Integer endIndex = ticket.getEndIndex(); List finalSeatList = new ArrayList<>(); if(StrUtil.isBlank(confirmOrderTicketDTO0.getSeat())){ LOG.info("会员没有选座"); for (ConfirmOrderTicketDTO orderTicketDTO : tickets) { genSeat(finalSeatList, date, trainCode, seatTypeCode, null, null, startIndex, endIndex); } } else { LOG.info("会员开始选座"); // 3.2 按座位类型创建选座列表 // 构建选座列表(计算最终偏移量) List referSeatList = new ArrayList<>(); List seatColEnums = SeatColEnum.getColsByType(seatTypeCode); LOG.info("本次选座所包含的列数为:{}", seatColEnums); for (int i = 1; i <= 2; i++) { for (SeatColEnum seatColEnum : seatColEnums) { referSeatList.add(seatColEnum.getDesc() + i); } } LOG.info("用于作参照的两排座位:{}", referSeatList); // 3.3 计算每个座位相对第一个选座位置的偏移量 // 3.3.1 获取所有座位的绝对偏移值 List aboluteOffsetList = new ArrayList<>(); for (ConfirmOrderTicketDTO ticketDTO : tickets) { aboluteOffsetList.add(referSeatList.indexOf(ticketDTO.getSeat())); } // 3.3.2 获取所有座位相对第一个座位的偏移 List offsetList = new ArrayList<>(); for (Integer index : aboluteOffsetList) { offsetList.add(index - aboluteOffsetList.get(0)); } LOG.info("相对偏移座位数组为:{}", offsetList); // 3.4 筛选出每日对应所有车厢 genSeat(finalSeatList, date, trainCode, seatTypeCode, confirmOrderTicketDTO0.getSeat().split("")[0], offsetList, startIndex, endIndex); // 4. 事务处理购票后的数据 // 4.1 修改座位sell信息,扣减余票信息中的数量 afterDoConfirm.afterDofirm(ticket, finalSeatList, tickets, confirmOrder); // 4.3 为用户增添订单信息 // 3.3 更改订单状态 // confirmOrder.setUpdateTime(now); // confirmOrder.setStatus(ConfirmOrderStatusEnum.SUCCESS.getCode()); // 4.4 修改订单信息为已完成 } } ``` calSell ```java /** * @Description: 查找合适的座位,并给Sell赋值 * @param * @return */ private boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex) { String sell = dailyTrainSeat.getSell(); String sellPart = sell.substring(startIndex, endIndex); LOG.info("本车座当前的起始与终止座位为:{},{}", startIndex, endIndex - 1); LOG.info("本车座位的售卖情况为:{}", sell); if(Integer.parseInt(sellPart) > 0){ LOG.info("车座{}在本次车站区间{}~{}已经售出", dailyTrainSeat, startIndex, endIndex); return false; } else { LOG.info("车座{}在本次车站区间{}~{}未售出", dailyTrainSeat, startIndex, endIndex); String curSell = sellPart.replace('0', '1'); // 填充子串之前‘0’ curSell = StrUtil.fillBefore(curSell, '0', endIndex); // 填充子串之后‘0’ curSell = StrUtil.fillAfter(curSell, '0', sell.length()); LOG.info("填充之后的收票情况为:{}", curSell); // 将卖出去票的二进制串与数据库中sell进行与操作 int newSellInt = NumberUtil.binaryToInt(sell) | NumberUtil.binaryToInt(curSell); String newSell = NumberUtil.getBinaryStr(newSellInt); // 与后的字符串可能会出现前位0丢失,因此需要补齐 newSell = StrUtil.fillBefore(newSell, '0', sell.length()); dailyTrainSeat.setSell(newSell); LOG.info("与操作之后的真是sell信息为:{}", newSell); return true; } } ``` genSeat ```java /** * @Description: 生成座位 * @param * @return */ private void genSeat(List finalSeatList, Date date, String trainCode, String seatType, String colmn, List offsetList, Integer startIndex, Integer endIndex){ List genSeatList = new ArrayList<>(); // 以第一个座位为起始的临时最终数组,若偏移获取完就存储到最终数组中 // 1. 筛出符合座位等次的车厢 List trainCarriages = dailyTrainCarriageService .selectCarriageBySeatType(date, trainCode, seatType); LOG.info("一共查出{}个符合条件的车厢", trainCarriages.size()); // 2.根据车厢筛出对应座位 for (DailyTrainCarriage trainCarriage : trainCarriages) { genSeatList = new ArrayList<>(); // 每次车厢需要初始化最终车座数组 Integer trainCarriageIdx = trainCarriage.getIdx(); LOG.info("开始筛选车厢号为{}的车座", trainCarriageIdx); // 2.1 实际筛每日座位 List dailyTrainSeats = dailyTrainSeatService .selectByCarriageIdx(date, trainCode, trainCarriageIdx); LOG.info("获取车厢{},实际车座数量为{}", trainCarriage.getIdx(), dailyTrainSeats.size()); // 2.2 遍历座位获取符合要求的座位 for (DailyTrainSeat dailyTrainSeat : dailyTrainSeats) { // 2.2.1 获取当前第一个所选座位并判断 String col = dailyTrainSeat.getCol(); Integer firstSeatIdx = dailyTrainSeat.getCarriageSeatIndex(); // 判断当前座位不能被选中过 boolean alreadyChooseFlag = false; for (DailyTrainSeat finalSeat : finalSeatList){ if (finalSeat.getId().equals(dailyTrainSeat.getId())) { alreadyChooseFlag = true; break; } } if (alreadyChooseFlag) { LOG.info("座位{}被选中过,不能重复选中,继续判断下一个座位", firstSeatIdx); continue; } if(StrUtil.isBlank(colmn)){ LOG.info("无座位"); } else{ LOG.info("选中第一个座位为:{}", firstSeatIdx); if(!colmn.equals(col)){ LOG.info("本座位与当前座位不匹配,本座位为:{},需求座位为:{}", col, colmn); continue; } } // 2.2.2 判断第一个座位sell是否合法 boolean isChosen = calSell(dailyTrainSeat, startIndex, endIndex); boolean isAllChosen = true; if(isChosen){ genSeatList.add(dailyTrainSeat); // 第一座位合法加入数组 LOG.info("选中座位"); // 2.2.3 根据偏移量判断是否合规 if(CollUtil.isNotEmpty(offsetList)) { for (int i = 1; i < offsetList.size(); i++) { // 获取偏移后的索引值 // 座位索引值从1开始,偏移量数组从0开始 int offsetSeatIdx = firstSeatIdx + offsetList.get(i); // 按偏移长度与座位表长比较 if (offsetSeatIdx > dailyTrainSeats.size()) { LOG.info("座位不可选,所选座位已经超过该车厢座位,错误座位号为:{}", offsetSeatIdx); isAllChosen = false; break; } // 查询offset偏移座位 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); DailyTrainSeat offsetSeat = dailyTrainSeatService.getOne(wrapper.eq(DailyTrainSeat::getDate, date).eq(DailyTrainSeat::getTrainCode, trainCode) .eq(DailyTrainSeat::getCarriageIndex, trainCarriageIdx).eq(DailyTrainSeat::getCarriageSeatIndex, offsetSeatIdx)); // 判断是否合法 if (!calSell(offsetSeat, startIndex, endIndex)) { LOG.info("本座位已售,无法选中:座位号为{}", offsetSeat.getCarriageSeatIndex()); isAllChosen = false; break; } genSeatList.add(offsetSeat); LOG.info("选中本座为:{}", offsetSeatIdx); } } else { LOG.info("偏移量数组为空,即不存在选座"); } if(!isAllChosen){ genSeatList = new ArrayList<>(); // 不是所有车座都合法,即需要初始化 continue; } else { finalSeatList.addAll(genSeatList); LOG.info("最终的座位数据为:{}", finalSeatList); LOG.info("选票成功捏"); return; } } else { // LOG.info("未选中座位"); continue; } } } } ``` AfterDoConfirm ```java private static final Logger LOG = LoggerFactory.getLogger(AfterDoConfirm.class); @Resource private DailyTrainSeatService dailyTrainSeatService; @Resource private DailyTrainTicketService dailyTrainTicketService; @Resource DailyTrainTicketMapperCust dailyTrainTicketMapperCust; @Resource MemberTicketFeign memberTicketFeign; @Resource ConfirmOrderMapper confirmOrderMapper; @Transactional public void afterDofirm(DailyTrainTicket dailyTrainTicket, List finalSeatList, List tickets, ConfirmOrder confirmOrder) { // 1.更改车票本身数据 DateTime now = DateTime.now(); for (int t = 0; t < tickets.size(); t++) { DailyTrainSeat dailyTrainSeat = finalSeatList.get(t); // LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); dailyTrainSeat.setUpdateTime(now); dailyTrainSeatService.updateById(dailyTrainSeat); // 2.更改该车票影响其他站点车票的数量(该车程经过站点区间从起始与终点站向两侧,只要有部分或全部包含该车票车程的区间车票都应减少,直到有其他车票影响过位置) Integer startIndex = dailyTrainTicket.getStartIndex(); Integer endIndex = dailyTrainTicket.getEndIndex(); char[] aChars = dailyTrainSeat.getSell().toCharArray(); Integer minEndIndex = startIndex + 1; Integer maxStartIndex = endIndex - 1; Integer minStartIndex = 0; // 2.1获取最小起始站点(遇到1,即其他买过的车票为止) for(int i = startIndex - 1; i >= 0; i--){ if(aChars[i] == '1'){ minStartIndex = i + 1; break; } } LOG.info("影响出发站区间:" + minStartIndex + "-" + maxStartIndex); // 2.2获取最大终止点 Integer maxEndIndex = dailyTrainSeat.getSell().length(); for(int j = endIndex; j < dailyTrainSeat.getSell().length(); j++){ if(aChars[j] == '1'){ maxEndIndex = j; break; } } LOG.info("影响到达站区间:" + minEndIndex + "-" + maxEndIndex); // 3.1 修改座位sell信息,扣减余票信息中的数量 动态sql,将在这区间内的所有的车站余票总数进行削减 dailyTrainTicketMapperCust.updateCountBySell(dailyTrainSeat.getDate(), dailyTrainSeat.getTrainCode(), dailyTrainSeat.getSeatType(), minStartIndex, maxStartIndex, minEndIndex, maxEndIndex); // 3.2为用户增添订单信息 MemberTicketReq memberTicketReq = new MemberTicketReq(); ConfirmOrderTicketDTO ticketDTO = tickets.get(t); memberTicketReq.setMemberId(LoginMemberContext.getId()); memberTicketReq.setPassengerId(ticketDTO.getPassengerId()); memberTicketReq.setPassengerName(ticketDTO.getPassengerName()); memberTicketReq.setTrainDate(dailyTrainSeat.getDate()); memberTicketReq.setTrainCode(dailyTrainSeat.getTrainCode()); memberTicketReq.setCarriageIndex(dailyTrainSeat.getCarriageIndex()); memberTicketReq.setSeatRow(dailyTrainSeat.getLine()); memberTicketReq.setSeatCol(dailyTrainSeat.getCol()); memberTicketReq.setStartStation(dailyTrainTicket.getStart()); memberTicketReq.setEndStation(dailyTrainTicket.getEnd()); memberTicketReq.setStartTime(dailyTrainTicket.getStartTime()); memberTicketReq.setEndTime(dailyTrainTicket.getEndTime()); memberTicketReq.setSeatType(dailyTrainSeat.getSeatType()); memberTicketFeign.modify(memberTicketReq); // 3.3 更改订单状态 confirmOrder.setUpdateTime(now); confirmOrder.setStatus(ConfirmOrderStatusEnum.SUCCESS.getCode()); confirmOrderMapper.updateById(confirmOrder); } } ``` ### 3、优化 ### (1)sql插入优化 在依照车座信息生成每日车座时可能会有大量车座数据待生成,在执行存储时耗时在for中一个个插入的**耗时为6552ms**(近7s),体验很差,也导致后面很多事务,高并发的问题 **初步优化**:将所有车座信息存储到临时数组中,生成完成后统一生成,执行mp的saveBatch方法,**耗时被优化到3482ms**,还是不佳 **最终优化**:在mysql配置url后添加配置rewriteBatchedStatements=true,将**耗时优化到163ms** 原因讲解:mp的saveBatch方法,默认其实也是for循环插入,只不过少了连接数据库的时间,但无法实现整体插入,在该注解的意思,是在一次性拼接1000条的插入sql进行整体插入,1000以下只需执行一次即可,因此大大提升速度 ### (2)分布式锁限流 通过redis全局锁进行下单锁需求,短时间只有一人能进行下单操作 ```java // 尝试获取锁,分布式锁,限制同一时间只有一个人买票 String lockKey = CONFIRM_ORDER + confirmOrderDoReq.getDate().toString() + '-' + confirmOrderDoReq.getTrainCode(); Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 600, TimeUnit.SECONDS); if(ifAbsent){ LOG.info("恭喜抢到锁哩"); } else{ LOG.info("没抢到锁"); throw new BusniessException(BusniessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL); } ``` ### (3)令牌大闸 为防止机器人刷票(同一用户短时间内大量请求),设置令牌,按每日车列的座位号和车站数的乘积设置为令牌数,用户进行下单操作时,需先尝试获取令牌,获取到令牌才有进行后续下单操作 点单获取分布式锁之前需尝试获取令牌,ConfirmOrderServiceImpl部分代码 ```java boolean vaildSkToken = skTokenService.vaildSkToken(date, trainCode); if(vaildSkToken == false){ // 没令牌了 throw new BusniessException(BusniessExceptionEnum.TICKET_TOKEN_ALREADY_EXITS); } ``` 获取令牌查看是否有redis中存有-> 存有则进行缓存令牌数量的扣减,若达到一定数量则去更新数据库,若不存有则扣减缓存后将令牌数量缓存在redis中,每次扣减redis中库存都会刷新有效期60s,防止缓存令牌失效,大量请求打到数据库,导致缓存击穿 ```java public boolean vaildSkToken(Date date, String trainCode){ // 1.分布式获取令牌,只有有令牌才能进行购票,并给出限制购票运行令牌的时间,防止同一人短时间大量抢票,手动限流(限个人) Long memberId = LoginMemberContext.getId(); String tokenKey = SK_TOKEN + date.toString() + '-' +trainCode + memberId; Boolean tokenIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(tokenKey, tokenKey, 5, TimeUnit.SECONDS); if(tokenIfAbsent){ LOG.info("恭喜,获取到令牌购票权限"); } else { LOG.info("抱歉,没获取到令牌购票权限"); return false; } // 2.从缓存中获取令牌的个数,设置每五(可以自定义)个人获取令牌就更新一次数据库 String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + "-" + DateUtil.formatDate(date) + "-" + trainCode; Object skTokenCount = stringRedisTemplate.opsForValue().get(skTokenCountKey); if(skTokenCount != null){ // 2.1 缓存中有令牌剩余 LOG.info("缓存中有该车次的令牌大闸为:{}", skTokenCount); Long count = stringRedisTemplate.opsForValue().decrement(skTokenCountKey, 1); if(count < 0L){ LOG.info("缓存令牌没剩的了"); return false; } else { // 2.1.2 重置一分钟缓存时间,代表当前可能还有人来买票 stringRedisTemplate.expire(skTokenCountKey, 60, TimeUnit.SECONDS); // 2.1.3 每五个令牌消耗存储一下数据库 if(count % 5 == 0){ int decreaseCount = count.intValue(); skTokenCountCust.decrease(date, trainCode, decreaseCount); } return true; } } else { // 2.2 缓存中没有令牌 // 查数据库中的锁数量 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); SkToken skToken = getOne(wrapper.eq(SkToken::getDate, date).eq(SkToken::getTrainCode, trainCode)); if(ObjectUtil.isEmpty(skToken)){ LOG.info("该车次令牌不存在"); return false; } Integer count = skToken.getCount(); if(count <= 0){ LOG.info("该日期为【{}】车次【{}】的令牌数为空", date, trainCode); return false; } // 令牌有余 count--; skToken.setCount(count); // 存入缓存 stringRedisTemplate.opsForValue().set(skTokenCountKey, String.valueOf(count), 60, TimeUnit.SECONDS); // updateById(skToken); return true; } } ``` ### (4)两重验证码 在前端用户下订单时,会弹出第一层数字验证码和第二层的图形验证码进行验证操作,目的是防止大量刷单并可以进行短时间内的分流,因为第二层的验证码需要和后端进行校验,因此第一层的数字验证码可以减轻第二层图形验证码后端交互的压力 第一层写在前端,不过多介绍,介绍第二层的使用过程: 1. 引入kaptcha依赖 ```xml com.github.penggle kaptcha 2.3.2 javax.servlet javax-servlet-api com.github.penggle kaptcha ``` 2. 书写config类、controller类 config类: ```java package com.Java.train.business.config; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; @Configuration public class KaptchaConfig { @Bean public DefaultKaptcha getDefaultKaptcha() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty("kaptcha.border", "no"); // properties.setProperty("kaptcha.border.color", "105,179,90"); properties.setProperty("kaptcha.textproducer.font.color", "blue"); properties.setProperty("kaptcha.image.width", "90"); properties.setProperty("kaptcha.image.height", "28"); properties.setProperty("kaptcha.textproducer.font.size", "20"); properties.setProperty("kaptcha.session.key", "code"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.font.names", "Arial"); properties.setProperty("kaptcha.noise.color", "255,96,0"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); // 水纹验证码,加密防止黑客破解验证码然后刷单 properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple"); // properties.setProperty("kaptcha.obscurificator.impl", KaptchaWaterRipple.class.getName()); // properties.setProperty("kaptcha.background.impl", KaptchaNoBackhround.class.getName()); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } // 没用到 @Bean public DefaultKaptcha getWebKaptcha() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty("kaptcha.border", "no"); // properties.setProperty("kaptcha.border.color", "105,179,90"); properties.setProperty("kaptcha.textproducer.font.color", "blue"); properties.setProperty("kaptcha.image.width", "90"); properties.setProperty("kaptcha.image.height", "45"); properties.setProperty("kaptcha.textproducer.font.size", "30"); properties.setProperty("kaptcha.session.key", "code"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.font.names", "Arial"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); properties.setProperty("kaptcha.obscurificator.impl", KaptchaWaterRipple.class.getName()); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } } ``` controller类 ```java package com.Java.train.business.controller; import com.google.code.kaptcha.impl.DefaultKaptcha; import jakarta.annotation.Resource; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/kaptcha") public class KaptchaController { @Qualifier("getDefaultKaptcha") @Autowired DefaultKaptcha defaultKaptcha; @Resource public StringRedisTemplate stringRedisTemplate; @GetMapping("/image-code/{imageCodeToken}") public void imageCode(@PathVariable(value = "imageCodeToken") String imageCodeToken, HttpServletResponse httpServletResponse) throws Exception{ ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream(); try { // 生成验证码字符串 String createText = defaultKaptcha.createText(); // 将生成的验证码放入redis缓存中,后续验证的时候用到 stringRedisTemplate.opsForValue().set(imageCodeToken, createText, 300, TimeUnit.SECONDS); // 使用验证码字符串生成验证码图片 BufferedImage challenge = defaultKaptcha.createImage(createText); ImageIO.write(challenge, "jpg", jpegOutputStream); } catch (IllegalArgumentException e) { httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组 byte[] captchaChallengeAsJpeg = jpegOutputStream.toByteArray(); httpServletResponse.setHeader("Cache-Control", "no-store"); httpServletResponse.setHeader("Pragma", "no-cache"); httpServletResponse.setDateHeader("Expires", 0); httpServletResponse.setContentType("image/jpeg"); ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream(); responseOutputStream.write(captchaChallengeAsJpeg); responseOutputStream.flush(); responseOutputStream.close(); } } ``` 3. 自定义设置验证码格式配置 无背景版 ```java package com.Java.train.business.config; import com.google.code.kaptcha.BackgroundProducer; import com.google.code.kaptcha.util.Configurable; import java.awt.*; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; public class KaptchaNoBackhround extends Configurable implements BackgroundProducer { public KaptchaNoBackhround(){ } @Override public BufferedImage addBackground(BufferedImage baseImage) { int width = baseImage.getWidth(); int height = baseImage.getHeight(); BufferedImage imageWithBackground = new BufferedImage(width, height, 1); Graphics2D graph = (Graphics2D)imageWithBackground.getGraphics(); graph.fill(new Rectangle2D.Double(0.0D, 0.0D, (double)width, (double)height)); graph.drawImage(baseImage, 0, 0, null); return imageWithBackground; } } ``` 水波纹干扰版 ```java package com.Java.train.business.config; import com.google.code.kaptcha.GimpyEngine; import com.google.code.kaptcha.NoiseProducer; import com.google.code.kaptcha.util.Configurable; import com.jhlabs.image.RippleFilter; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.util.Random; public class KaptchaWaterRipple extends Configurable implements GimpyEngine { public KaptchaWaterRipple(){} @Override public BufferedImage getDistortedImage(BufferedImage baseImage) { NoiseProducer noiseProducer = this.getConfig().getNoiseImpl(); BufferedImage distortedImage = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), 2); Graphics2D graph = (Graphics2D)distortedImage.getGraphics(); Random rand = new Random(); RippleFilter rippleFilter = new RippleFilter(); rippleFilter.setXAmplitude(7.6F); rippleFilter.setYAmplitude(rand.nextFloat() + 1.0F); rippleFilter.setEdgeAction(1); BufferedImage effectImage = rippleFilter.filter(baseImage, (BufferedImage)null); graph.drawImage(effectImage, 0, 0, (Color)null, (ImageObserver)null); graph.dispose(); noiseProducer.makeNoise(distortedImage, 0.1F, 0.1F, 0.25F, 0.25F); noiseProducer.makeNoise(distortedImage, 0.1F, 0.25F, 0.5F, 0.9F); return distortedImage; } } ``` ### (5)异步化处理订单 将DoConfirm方法拆开分为:①用来为获取令牌和生成初始订单的BeforeConfirmOreder(producer),②从队列中获取的简易订单信息、尝试获取分布式锁并不断循环处理待处理订单的ConfirmOrderServiceImple,③ 消耗①放入队列中的消息的RabbitQueueConsumer,④不变的处理成功购买之后的该票的连锁反应并修改订单状态以及为用户出票的AfterDoConfirm **BeforeConfirmOreder** ```java @Service public class BeforeConfirmOrderService { private static final Logger LOG = LoggerFactory.getLogger(BeforeConfirmOrderService.class); @Resource StringRedisTemplate stringRedisTemplate; @Resource RabbitTemplate rabbitTemplate; @Resource SkTokenServiceImpl skTokenService; @Resource ConfirmOrderMapper confirmOrderMapper; @Resource TrainService trainService; @Resource DailyTrainTicketService dailyTrainTicketService; @Value("${spring.profiles.active}") private String env; public Long beforeDoConfirm(ConfirmOrderDoReq confirmOrderDoReq){ // 1. 验证图形验证码 if(!env.equals("dev")) { String userImageCode = confirmOrderDoReq.getImageCode(); String imageCodeToken = confirmOrderDoReq.getImageCodeToken(); String realImageCode = stringRedisTemplate.opsForValue().get(imageCodeToken); if (StrUtil.isBlank(realImageCode)) { throw new BusniessException(BusniessExceptionEnum.IMAGE_CODE_PASS_AWAY); } if (!realImageCode.equalsIgnoreCase(userImageCode)) { // 验证码不对 LOG.info("验证码错误了老弟"); throw new BusniessException(BusniessExceptionEnum.IMAGE_CODE_WRONG); } else { stringRedisTemplate.delete(imageCodeToken); } } // 校验是否存在该列车等信息 // 2.1 校验该车次是否存在 Date date = confirmOrderDoReq.getDate(); String trainCode = confirmOrderDoReq.getTrainCode(); Train train = trainService.getOne(new LambdaQueryWrapper().eq(Train::getCode, trainCode)); // if(ObjectUtil.isEmpty(train)){ // throw new BusniessException(BusniessExceptionEnum.BUSINESS_TRAIN_NOT_EXITS); // } // // 2.3 校验该余票是否存在 // LOG.info("日期为:{}", date); // String start = confirmOrderDoReq.getStart(); // String end = confirmOrderDoReq.getEnd(); // // 通过唯一键查找票 // DailyTrainTicket ticket = dailyTrainTicketService.getOne(new LambdaQueryWrapper().eq(DailyTrainTicket::getTrainCode, trainCode) // .eq(DailyTrainTicket::getDate, date).eq(DailyTrainTicket::getStart, start).eq(DailyTrainTicket::getEnd, end)); // LOG.info("查出的唯一车票为:{}", ticket); // if(ObjectUtil.isEmpty(ticket)){ // throw new BusniessException(BusniessExceptionEnum.BUSINESS_TICKET_NOT_EXITS); // } // 3.获取令牌 // 分布式获取令牌,只有有令牌才能进行购票,并给出限制购票运行令牌的时间,防止同一人短时间大量抢票,手动限流(限个人) boolean vaildSkToken = skTokenService.vaildSkToken(date, trainCode); if(vaildSkToken == false){ // 没令牌了 throw new BusniessException(BusniessExceptionEnum.TICKET_TOKEN_ALREADY_EXITS); } // 4.初始化订单信息,及时给予用户信息反馈 DateTime now = DateTime.now(); ConfirmOrder confirmOrder = new ConfirmOrder(); confirmOrder.setId(SnowFlakeUtil.getSnowFlakeNextId()); confirmOrder.setMemberId(LoginMemberContext.getId()); confirmOrder.setDate(date); confirmOrder.setTrainCode(trainCode); confirmOrder.setStart(confirmOrderDoReq.getStart()); confirmOrder.setEnd(confirmOrderDoReq.getEnd()); confirmOrder.setDailyTrainTicketId(confirmOrderDoReq.getDailyTrainTicketId()); confirmOrder.setTickets(JSON.toJSONString(confirmOrderDoReq.getTickets())); confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode()); confirmOrder.setCreateTime(now); confirmOrder.setUpdateTime(now); LOG.info("订单信息如下:{}", confirmOrder); confirmOrderMapper.insert(confirmOrder); // 5.将当前订单所需信息封装为DTO,并存入消息队列中 ConfirmOrderReqDTO confirmOrderReqDTO = BeanUtil.copyProperties(confirmOrderDoReq, ConfirmOrderReqDTO.class); confirmOrderReqDTO.setLogId(MDC.get("LOG_ID")); confirmOrderReqDTO.setMemberId(LoginMemberContext.getId()); LOG.info("向消息队列中传入信息"); String message = JSON.toJSONString(confirmOrderReqDTO); LOG.info("传入消息为:{}", message); rabbitTemplate.convertAndSend("ConfirmOrder.queue", message); LOG.info("已向队列中添加订单消息"); return confirmOrder.getId(); } } ``` **ConfirmOrderServiceImple** ```java //@Async // @GlobalTransactional @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock") public void doConfim(ConfirmOrderReqDTO confirmOrderReqDTO) { Date date = confirmOrderReqDTO.getDate(); String trainCode = confirmOrderReqDTO.getTrainCode(); Long id = confirmOrderReqDTO.getMemberId(); // 1.尝试获取锁,分布式锁,限制同一时间只有一个人买票 String lockKey = CONFIRM_ORDER + date.toString() + '-' + trainCode; Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 600, TimeUnit.SECONDS); if(ifAbsent){ LOG.info("恭喜抢到锁哩"); } else{ LOG.info("没抢到锁,有其它消费线程正在出票,不做任何处理"); return; } try{ // 2.循环的方式处理所有订单信息 while(true){ // 2.1 获取该待处理订单 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); // 一次查五条 PageHelper.startPage(1, 5); List list = list(wrapper.eq(ConfirmOrder::getDate, date).eq(ConfirmOrder::getTrainCode, trainCode) .eq(ConfirmOrder::getStatus, ConfirmOrderStatusEnum.INIT.getCode()).orderByAsc(ConfirmOrder::getId)); // 判断是否为空 if(CollUtil.isEmpty(list)){ LOG.info("订单列表为空"); break; } else { LOG.info("本次处理{}条订单", list.size()); } LOG.info("开始按消息进入顺序处理订单"); list.forEach(confirmOrder -> { try{ sell(confirmOrder); } catch (BusniessException e){ if(e.getE().equals(BusniessExceptionEnum.BUSINESS_TICKET_NOT_ENOUGH)){ LOG.info("本订单余票不足,继续售卖下一个订单"); confirmOrder.setStatus(ConfirmOrderStatusEnum.EMPTY.getCode()); updateById(confirmOrder); } else { throw e; } } }); } } finally { // 订单完成删除key stringRedisTemplate.delete(lockKey); } } ``` **RabbitQueueConsumer** ```java @Component public class RabbitOrderConsumer { // 利用RabbitListener来声明要监听的队列信息 // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。 // 可以看到方法体中接收的就是消息体的内容 @Resource private ConfirmOrderService confirmOrderService; @RabbitListener(queues = "ConfirmOrder.queue") public void listenSimpleQueueMessage(String msg) throws InterruptedException { ConfirmOrderReqDTO confirmOrderReqDTO = JSONUtil.toBean(msg, ConfirmOrderReqDTO.class); String logId = confirmOrderReqDTO.getLogId(); MDC.put("LOG_ID", logId); System.out.println("spring 消费者接收到消息:【" + confirmOrderReqDTO + "】"); confirmOrderService.doConfim(confirmOrderReqDTO); } } ``` **AfterDoConfirm** ```java public void afterDofirm(DailyTrainTicket dailyTrainTicket, List finalSeatList, List tickets, ConfirmOrder confirmOrder) throws Exception { // LOG.info("seata全局事务ID为:{}", RootContext.getXID()); // 1.更改车票本身数据 DateTime now = DateTime.now(); for (int t = 0; t < tickets.size(); t++) { DailyTrainSeat dailyTrainSeat = finalSeatList.get(t); // LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); dailyTrainSeat.setUpdateTime(now); dailyTrainSeatService.updateById(dailyTrainSeat); // 2.更改该车票影响其他站点车票的数量(该车程经过站点区间从起始与终点站向两侧,只要有部分或全部包含该车票车程的区间车票都应减少,直到有其他车票影响过位置) Integer startIndex = dailyTrainTicket.getStartIndex(); Integer endIndex = dailyTrainTicket.getEndIndex(); char[] aChars = dailyTrainSeat.getSell().toCharArray(); Integer minEndIndex = startIndex + 1; Integer maxStartIndex = endIndex - 1; Integer minStartIndex = 0; // 2.1获取最小起始站点(遇到1,即其他买过的车票为止) for(int i = startIndex - 1; i >= 0; i--){ if(aChars[i] == '1'){ minStartIndex = i + 1; break; } } LOG.info("影响出发站区间:" + minStartIndex + "-" + maxStartIndex); // 2.2获取最大终止点 Integer maxEndIndex = dailyTrainSeat.getSell().length(); for(int j = endIndex; j < dailyTrainSeat.getSell().length(); j++){ if(aChars[j] == '1'){ maxEndIndex = j; break; } } LOG.info("影响到达站区间:" + minEndIndex + "-" + maxEndIndex); long start1 = System.currentTimeMillis(); // 3.1 修改座位sell信息,扣减余票信息中的数量 动态sql,将在这区间内的所有的车站余票总数进行削减 dailyTrainTicketMapperCust.updateCountBySell(dailyTrainSeat.getDate(), dailyTrainSeat.getTrainCode(), dailyTrainSeat.getSeatType(), minStartIndex, maxStartIndex, minEndIndex, maxEndIndex); long end1 = System.currentTimeMillis(); LOG.info("动态扣减区间内sell的时间为:{}", end1 - start1); // 3.2为用户增添订单信息 long start2 = System.currentTimeMillis(); MemberTicketReq memberTicketReq = new MemberTicketReq(); ConfirmOrderTicketDTO ticketDTO = tickets.get(t); memberTicketReq.setMemberId(confirmOrder.getMemberId()); memberTicketReq.setPassengerId(ticketDTO.getPassengerId()); memberTicketReq.setPassengerName(ticketDTO.getPassengerName()); memberTicketReq.setTrainDate(dailyTrainSeat.getDate()); memberTicketReq.setTrainCode(dailyTrainSeat.getTrainCode()); memberTicketReq.setCarriageIndex(dailyTrainSeat.getCarriageIndex()); memberTicketReq.setSeatRow(dailyTrainSeat.getLine()); memberTicketReq.setSeatCol(dailyTrainSeat.getCol()); memberTicketReq.setStartStation(dailyTrainTicket.getStart()); memberTicketReq.setEndStation(dailyTrainTicket.getEnd()); memberTicketReq.setStartTime(dailyTrainTicket.getStartTime()); memberTicketReq.setEndTime(dailyTrainTicket.getEndTime()); memberTicketReq.setSeatType(dailyTrainSeat.getSeatType()); memberTicketFeign.modify(memberTicketReq); long end2 = System.currentTimeMillis(); LOG.info("跨服务为用户增添订单信息的时间为:{}", end2 - start2); // 3.3 更改订单状态 confirmOrder.setUpdateTime(now); confirmOrder.setStatus(ConfirmOrderStatusEnum.SUCCESS.getCode()); confirmOrderMapper.updateById(confirmOrder); } ``` **ConfirmOrderServiceImpl的其他重要方法** ```java /** * @Description: 统计订单之前排队人数 * @param * @return */ @Override public Integer queryOrderCount(Long id) { // 查询比id靠前状态为初始化的订单数 ConfirmOrder confirmOrder = getById(id); ConfirmOrderStatusEnum orderStatusEnum = EnumUtil.getBy(ConfirmOrderStatusEnum::getCode, confirmOrder.getStatus()); Integer result = switch (orderStatusEnum){ case PENDING -> 0; // 处理中 case SUCCESS -> -1; // 订单成功 case FAILURE -> -2; // 失败 case EMPTY -> -3; // 没有票 case CANCEL -> -4;// 取消 case INIT -> 999;// 初始化 }; Date date = confirmOrder.getDate(); String trainCode = confirmOrder.getTrainCode(); Date createTime = confirmOrder.getCreateTime(); if(result == 999){ //查询同一日期、车次、比当前订单早并且状态为INIT或PENDING的订单数量 LambdaQueryWrapper wrapper1 = new LambdaQueryWrapper<>(); Long count = count(wrapper1.eq(ConfirmOrder::getDate, date).eq(ConfirmOrder::getTrainCode, trainCode) .lt(ConfirmOrder::getCreateTime, createTime).eq(ConfirmOrder::getStatus, ConfirmOrderStatusEnum.INIT.getCode())); LambdaQueryWrapper wrapper2 = new LambdaQueryWrapper<>(); count += count(wrapper2.eq(ConfirmOrder::getDate, date).eq(ConfirmOrder::getTrainCode, trainCode) .lt(ConfirmOrder::getCreateTime, createTime).eq(ConfirmOrder::getStatus, ConfirmOrderStatusEnum.PENDING.getCode())); return count.intValue(); } else { return result; } } /** * @Description: 根据车票处理座位问题 * @param * @return */ private void sell(ConfirmOrder confirmOrder) { // try { // Thread.sleep(200); // } catch (Exception e){ // throw new RuntimeException(e); // } // 修改订单状态为处理中 confirmOrder.setStatus(ConfirmOrderStatusEnum.PENDING.getCode()); updateById(confirmOrder); // 2.1 初始化会员订单信息 Date date = confirmOrder.getDate(); String trainCode = confirmOrder.getTrainCode(); String start = confirmOrder.getStart(); String end = confirmOrder.getEnd(); // ConfirmOrder confirmOrder = new ConfirmOrder(); // confirmOrder.setId(SnowFlakeUtil.getSnowFlakeNextId()); // confirmOrder.setMemberId(LoginMemberContext.getId()); // confirmOrder.setDate(date); // confirmOrder.setTrainCode(trainCode); // confirmOrder.setStart(start); // confirmOrder.setEnd(end); // confirmOrder.setDailyTrainTicketId(confirmOrderDoReq.getDailyTrainTicketId()); // confirmOrder.setTickets(JSON.toJSONString(confirmOrderDoReq.getTickets())); // confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode()); // confirmOrder.setCreateTime(now); // confirmOrder.setUpdateTime(now); // LOG.info("订单信息如下:{}", confirmOrder); // save(confirmOrder); LambdaQueryWrapper ticketWrapper = new LambdaQueryWrapper<>(); DailyTrainTicket ticket = dailyTrainTicketService.getOne(ticketWrapper.eq(DailyTrainTicket::getDate, date).eq(DailyTrainTicket::getTrainCode, trainCode) .eq(DailyTrainTicket::getStart, start).eq(DailyTrainTicket::getEnd, end)); // 2.2 查询余票数量,模拟扣减,判断是否充足 int virYdz = ticket.getYdz(), virEdz = ticket.getEdz(), virRw = ticket.getRw(), virYw = ticket.getYw(); // 类型改变(将entity的String类JSON字符串转为List数组 List tickets = JSONUtil.toList(confirmOrder.getTickets(), ConfirmOrderTicketDTO.class); for (ConfirmOrderTicketDTO ticketDTO : tickets) { String seatTypeCode = ticketDTO.getSeatTypeCode(); SeatTypeEnum typeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode); // 消减虚拟票数(没有存到数据库) ticketCountDecrease(ticket, typeEnum); } // 3.选座 // 3.1 获取所有车票,并查看是否选座(看车票第一个值就行) ConfirmOrderTicketDTO confirmOrderTicketDTO0 = tickets.get(0); String seatTypeCode = confirmOrderTicketDTO0.getSeatTypeCode(); // 获取起始站点和终止站点 Integer startIndex = ticket.getStartIndex(); Integer endIndex = ticket.getEndIndex(); List finalSeatList = new ArrayList<>(); if (StrUtil.isBlank(confirmOrderTicketDTO0.getSeat())) { LOG.info("会员没有选座"); for (ConfirmOrderTicketDTO orderTicketDTO : tickets) { genSeat(finalSeatList, date, trainCode, seatTypeCode, null, null, startIndex, endIndex); } } else { LOG.info("会员开始选座"); // 3.2 按座位类型创建选座列表 // 构建选座列表(计算最终偏移量) List referSeatList = new ArrayList<>(); List seatColEnums = SeatColEnum.getColsByType(seatTypeCode); LOG.info("本次选座所包含的列数为:{}", seatColEnums); for (int i = 1; i <= 2; i++) { for (SeatColEnum seatColEnum : seatColEnums) { referSeatList.add(seatColEnum.getDesc() + i); } } LOG.info("用于作参照的两排座位:{}", referSeatList); // 3.3 计算每个座位相对第一个选座位置的偏移量 // 3.3.1 获取所有座位的绝对偏移值 List aboluteOffsetList = new ArrayList<>(); for (ConfirmOrderTicketDTO ticketDTO : tickets) { aboluteOffsetList.add(referSeatList.indexOf(ticketDTO.getSeat())); } // 3.3.2 获取所有座位相对第一个座位的偏移 List offsetList = new ArrayList<>(); for (Integer index : aboluteOffsetList) { offsetList.add(index - aboluteOffsetList.get(0)); } LOG.info("相对偏移座位数组为:{}", offsetList); // 3.4 筛选出每日对应所有车厢 genSeat(finalSeatList, date, trainCode, seatTypeCode, confirmOrderTicketDTO0.getSeat().split("")[0], offsetList, startIndex, endIndex); } // 4. 事务处理购票后的数据 // 4.1 修改座位sell信息,扣减余票信息中的数量 try { afterDoConfirm.afterDofirm(ticket, finalSeatList, tickets, confirmOrder); } catch (Exception e) { LOG.info("保存购票信息失效", e); throw new BusniessException(BusniessExceptionEnum.BUSINESS_CONFIRM_ORDER_EXCEPTION); } } /** * @Description: 生成座位 * @param * @return */ private void genSeat(List finalSeatList, Date date, String trainCode, String seatType, String colmn, List offsetList, Integer startIndex, Integer endIndex){ List genSeatList = new ArrayList<>(); // 以第一个座位为起始的临时最终数组,若偏移获取完就存储到最终数组中 // 1. 筛出符合座位等次的车厢 List trainCarriages = dailyTrainCarriageService .selectCarriageBySeatType(date, trainCode, seatType); LOG.info("一共查出{}个符合条件的车厢", trainCarriages.size()); // 2.根据车厢筛出对应座位 for (DailyTrainCarriage trainCarriage : trainCarriages) { genSeatList = new ArrayList<>(); // 每次车厢需要初始化最终车座数组 Integer trainCarriageIdx = trainCarriage.getIdx(); LOG.info("开始筛选车厢号为{}的车座", trainCarriageIdx); // 2.1 实际筛每日座位 List dailyTrainSeats = dailyTrainSeatService .selectByCarriageIdx(date, trainCode, trainCarriageIdx); LOG.info("获取车厢{},实际车座数量为{}", trainCarriage.getIdx(), dailyTrainSeats.size()); // 2.2 遍历座位获取符合要求的座位 for (DailyTrainSeat dailyTrainSeat : dailyTrainSeats) { // 2.2.1 获取当前第一个所选座位并判断 String col = dailyTrainSeat.getCol(); Integer firstSeatIdx = dailyTrainSeat.getCarriageSeatIndex(); // 判断当前座位不能被选中过 boolean alreadyChooseFlag = false; for (DailyTrainSeat finalSeat : finalSeatList){ if (finalSeat.getId().equals(dailyTrainSeat.getId())) { alreadyChooseFlag = true; break; } } if (alreadyChooseFlag) { LOG.info("座位{}被选中过,不能重复选中,继续判断下一个座位", firstSeatIdx); continue; } if(StrUtil.isBlank(colmn)){ LOG.info("无座位"); } else{ LOG.info("选中第一个座位为:{}", firstSeatIdx); if(!colmn.equals(col)){ LOG.info("本座位与当前座位不匹配,本座位为:{},需求座位为:{}", col, colmn); continue; } } // 2.2.2 判断第一个座位sell是否合法 boolean isChosen = calSell(dailyTrainSeat, startIndex, endIndex); boolean isAllChosen = true; if(isChosen){ genSeatList.add(dailyTrainSeat); // 第一座位合法加入数组 LOG.info("选中座位"); // 2.2.3 根据偏移量判断是否合规 if(CollUtil.isNotEmpty(offsetList)) { for (int i = 1; i < offsetList.size(); i++) { // 获取偏移后的索引值 // 座位索引值从1开始,偏移量数组从0开始 int offsetSeatIdx = firstSeatIdx + offsetList.get(i); // 按偏移长度与座位表长比较 if (offsetSeatIdx > dailyTrainSeats.size()) { LOG.info("座位不可选,所选座位已经超过该车厢座位,错误座位号为:{}", offsetSeatIdx); isAllChosen = false; break; } // 查询offset偏移座位 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); DailyTrainSeat offsetSeat = dailyTrainSeatService.getOne(wrapper.eq(DailyTrainSeat::getDate, date).eq(DailyTrainSeat::getTrainCode, trainCode) .eq(DailyTrainSeat::getCarriageIndex, trainCarriageIdx).eq(DailyTrainSeat::getCarriageSeatIndex, offsetSeatIdx)); // 判断是否合法 if (!calSell(offsetSeat, startIndex, endIndex)) { LOG.info("本座位已售,无法选中:座位号为{}", offsetSeat.getCarriageSeatIndex()); isAllChosen = false; break; } genSeatList.add(offsetSeat); LOG.info("选中本座为:{}", offsetSeatIdx); } } else { LOG.info("偏移量数组为空,即不存在选座"); } if(!isAllChosen){ genSeatList = new ArrayList<>(); // 不是所有车座都合法,即需要初始化 continue; } else { finalSeatList.addAll(genSeatList); LOG.info("最终的座位数据为:{}", finalSeatList); LOG.info("选票成功捏"); return; } } else { // LOG.info("未选中座位"); continue; } } } } /** * @Description: 查找合适的座位,并给Sell赋值 * @param * @return */ private boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex) { String sell = dailyTrainSeat.getSell(); String sellPart = sell.substring(startIndex, endIndex); LOG.info("本车座当前的起始与终止车站编号为:{},{}", startIndex, endIndex - 1); LOG.info("本车座位的售卖情况为:{}", sell); if(Integer.parseInt(sellPart) > 0){ LOG.info("车座{}在本次车站区间{}~{}已经售出", dailyTrainSeat, startIndex, endIndex); return false; } else { LOG.info("车座{}在本次车站区间{}~{}未售出", dailyTrainSeat, startIndex, endIndex); String curSell = sellPart.replace('0', '1'); // 填充子串之前‘0’ curSell = StrUtil.fillBefore(curSell, '0', endIndex); // 填充子串之后‘0’ curSell = StrUtil.fillAfter(curSell, '0', sell.length()); LOG.info("填充之后的收票情况为:{}", curSell); // 将卖出去票的二进制串与数据库中sell进行与操作 int newSellInt = NumberUtil.binaryToInt(sell) | NumberUtil.binaryToInt(curSell); String newSell = NumberUtil.getBinaryStr(newSellInt); // 与后的字符串可能会出现前位0丢失,因此需要补齐 newSell = StrUtil.fillBefore(newSell, '0', sell.length()); dailyTrainSeat.setSell(newSell); LOG.info("与操作之后的真是sell信息为:{}", newSell); return true; } } /** * @Description: 删除对应选票数量的车票并判断余票是否充足 * @param * @return */ private void ticketCountDecrease(DailyTrainTicket ticket, SeatTypeEnum typeEnum) { switch (typeEnum){ case YDZ -> { Integer ydz = ticket.getYdz() - 1; if(ydz < 0){ throw new BusniessException(BUSINESS_TICKET_NOT_ENOUGH); } ticket.setYdz(ydz); } case EDZ -> { Integer edz = ticket.getEdz() - 1; if(edz < 0){ throw new BusniessException(BUSINESS_TICKET_NOT_ENOUGH); } ticket.setEdz(edz); } case RW -> { Integer rw = ticket.getYdz() - 1; if(rw < 0){ throw new BusniessException(BUSINESS_TICKET_NOT_ENOUGH); } ticket.setRw(rw); } case YW -> { Integer yw = ticket.getYdz() - 1; if(yw < 0){ throw new BusniessException(BUSINESS_TICKET_NOT_ENOUGH); } ticket.setYdz(yw); } } } /** * @Description: 限流之后的处理 * @param * @return */ private void doConfirmBlock(ConfirmOrderDoReq req, BlockException e){ LOG.info("购票被限流:{}", req); throw new BusniessException(BusniessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION); } ``` # 三、Nacos ## (一)介绍 Nacos是阿里巴巴开源的一个动态服务发现、配置管理和服务管理平台,特别设计用于简化云原生应用的构建与管理。Nacos的核心特性包括: 1. **服务发现与健康检查**:Nacos支持DNS与RPC方式的服务发现机制,能够自动发现、路由及负载均衡微服务。它通过健康检查机制监控服务实例状态,确保只将请求转发给健康的实例。 2. **动态配置服务**:提供了一个集中式的外部化配置存储,允许你在Nacos中集中管理应用的配置,应用可以在运行时动态获取或更新这些配置。配置变更时,Nacos能实时推送到相关应用。 3. **动态DNS服务**:具备动态DNS服务功能,支持权重路由,为流量管理如蓝绿部署、灰度发布、流量镜像等场景提供灵活性。 4. **服务与元数据管理**:强大的服务和元数据管理能力,方便进行微服务及其相关信息的维护。 Nacos旨在提升微服务平台的构建、部署与运维效率,不仅适用于云端环境,也适合传统数据中心。关于Nacos的部署,它默认开放四个端口以支持其功能,其中最重要的是客户端、控制台及OpenAPI使用的HTTP端口8848,以及客户端gRPC请求服务端端口9848。在集群部署时,通常只需对外暴露这两个端口即可。 ## (二)核心功能 Nacos作为一款动态服务发现、配置管理和服务管理平台,其核心功能主要集中在以下几个方面: 1. **服务发现与注册**:Nacos允许服务实例在启动时向Nacos Server注册自己的元数据信息(如IP地址、端口等),并维持心跳以保持服务的健康状态。客户端可通过Nacos查询到这些注册的服务列表,实现服务间的自动发现与路由,这是微服务架构中不可或缺的基础能力。 2. **配置管理**:Nacos提供了一个集中式的配置中心,允许开发者在Nacos Server上统一管理所有环境的应用配置。应用在启动时可以从Nacos拉取配置,配置变更时能实时推送到各个客户端,实现了配置的集中管理和动态更新,极大地提高了运维效率和灵活性。 3. **动态配置服务**:与静态配置不同,动态配置服务能够让应用在运行时动态调整其行为,无需重启。这包括但不限于数据库连接字符串、线程池大小等运行时参数的调整。 4. **服务健康监测**:Nacos能够持续监控注册在其上的服务实例的状态,当检测到服务不可用时,会自动从服务列表中剔除,确保流量不会被导向不健康的服务实例,增强了系统的稳定性和可靠性。 5. **命名空间与分组管理**:通过命名空间和分组,Nacos支持多环境或多租户的隔离,使得不同环境或团队的服务与配置可以独立管理,互不影响。 6. **权限控制与安全**:Nacos提供了用户认证与授权机制,确保只有合法用户才能访问和修改服务及配置信息,提升了系统的安全性。 ## (三)配置过程 1. 首先配上依赖,注意spring-cloud和spring-cloud-alibaba的版本对应(我这都是2023.),nacos的依赖在alibaba里(不用配版本号) ```xml 19 2023.0.3 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies 2023.0.1.0 pom import org.springframework.cloud spring-cloud-starter-bootstrap com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 2. 书写每个版块的配置中心文件bootstrap.yaml文件 ```yaml spring: # 服务名称 application: name: member # 运行环境 profiles: active: dev cloud: nacos: username: nacos password: nacos discovery: server-addr: 127.0.0.1:8848 namespace: train config: server-addr: 127.0.0.1:8848 # 配置后缀 file-extension: yaml prefix: ${spring.application.name} namespace: train ``` 3. nacos + openFeign 用服务名称代替其URL(只有端口没后续) ```java package com.Java.train.business.feign; import com.Java.train.common.req.MemberTicketReq; import com.Java.train.common.response.CommonResp; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; //@FeignClient(name = "train-member", url = "http://127.0.0.1:8001/member") @FeignClient("member") public interface MemberTicketFeign { @PostMapping("/member/feign/ticket/save") CommonResp modify(@RequestBody MemberTicketReq req); } ``` 4. nacos + gateway 将gateway注册到nacos中,在nacos上的每个服务都会有其自己的备忘录(将nacos同一命名空间中的服务填入其中,可以通过服务名进行调用,因此代表**即使nacos挂了,其之间也能项目调用服务**) application文件: ```yaml spring: application: name: gateway #spring.cloud.gateway.routes[0].id=member #spring.cloud.gateway.routes[0].uri=http://127.0.0.1:8001 ##spring.cloud.gateway.routes[0].uri=lb://member #spring.cloud.gateway.routes[0].predicates[0]=Path=/member/** #配置member模块网关 cloud: gateway: routes: - id: member predicates: - Path=/member/** uri: lb://member - id: business predicates: - Path=/business/** uri: lb://business - id: batch predicates: - Path=/batch/** uri: lb://batch # 配置跨域信息 globalcors: cors-configurations: '[/**]': allowCredentials: true allowedHeaders: '*' allowedMethods: '*' allowedOriginPatterns: '*' maxAge: 3600 server: port: 8000 ``` # 四、缓存 ## (一)Mybatis中的缓存 mabatis中有两级缓存,一级缓存和二级缓存 ### **一级缓存** 是在默认在会话中的缓存,即在一次事务中,相同的方法(要求同一方法并且参数一致),只执行一次,其后不会走查询,而是直接调用,但会产生及时性问题,因此可以在配置文件中,调整关闭会话缓存,**默认是开启的** ```yaml mybatis: configuration: local-cache-scope: statement ``` ### **二级缓存** Mybatis会将查询的结果放在缓存中,在其他线程查询时,直接返还缓存中的结果,只有在对涉及的表进行增删改操作后会清除其缓存,但在面对集群时,多个服务器查询后,在本机产生缓存后,如果删除该数据库数据,但因为只能删除一台服务器上的缓存,导致其他机子还能查询到原来的数据,**默认是不开启的,一般不推荐使用**,Mybatis开启需要在需要开启二级缓存的表中添加,并将其实体类实现序列化接口(implements Serializable)。 mp开启方法 1. 在MybatisPlus的配置类中添加`@EnableGlobalCache`注解,启用全局缓存。 2. 在需要使用二级缓存的Mapper接口或Mapper XML文件中添加`@CacheNamespace`或`@Cache`注解。 3. 在需要使用二级缓存的实体类上添加`@CacheEvict`注解,指定缓存的策略。 ## (二)SpringBoot缓存 在springboot中的缓存,适用本地的小功能的实现,不适合大的集群缓存 [SpringBoot项目中使用缓存的正确姿势,太优雅了!-CSDN博客](https://blog.csdn.net/github_38592071/article/details/131198930) ### 1、使用步骤: 1. 添加缓存依赖(spring boot start cache) ```xml org.springframework.boot spring-boot-starter-cache 3.1.1 ``` 2. 在boot启动类中添加@EnableCaching注解,表示开启注解 **生成存储缓存bean对象**:既然要能使用缓存,就需要有一个缓存管理器Bean,默认情况下,`@EnableCaching` 将注册一个`ConcurrentMapCacheManager`的Bean,不需要单独的 bean 声明。`ConcurrentMapCacheManage`r将值存储在`ConcurrentHashMap`的实例中,这是缓存机制的最简单的线程安全实现。 3. 在需要的方法上添加@Cacheable(value = "") 该注解可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。可能会因为传入参数不同而导致缓存不同的值,而导致的缓存表面失效,此时可以通过对传参类的重写hash和equals方法来判断是否查询过(注意IDEA自动生成的不能重写父类的参数(如Page)) 4. 强制刷新缓存@CachePut springboot内置的缓存,当部分缓存失去其时效性,可以使用@CachePut来强制刷新其内容,该注解的方法不会走缓存,而是直接执行方法并更新缓存 ## (三)Redis 可以将springboot方法返回值存到redis中,可实现多节点唯一性缓存,只需要在需要的模块下绑定即可 ```yaml spring: cache: type: redis redis: use-key-prefix: true key-prefix: train-business_(具体模块) cache-null-values: false time-to-live: 60s ``` **redis红锁**: 高并发锁,集群情况下多个redis配置的redission多有一台锁,一共设置奇数台锁,只有用户拿到半数以上的锁的才算真正拿到锁 A B C D E: 只有获取三个及以上才算正在获取到锁 缺陷:也有缺陷,可能获取不到三个及以上的情况导致死锁,因此需要添加TTL,可能出现: 1:A B 2:C D 3:E ## (四)高并发场景问题 ​ 每个会员每天只会请求几次,但百万级别的用户量,并且每个会员的信息量很大,而且会员信息会在一个请求中大量使用;原解决方法使用**本地缓存**存储用户信息,并设置TTL为1分钟,因为不是频繁的发起请求,临时存本地一下就行, ​ 问题:因为会员信息量很大,还有很多的会员,本地内存容易爆 ​ 最终解决:使用本地线程变量存储,同为临时性,内存转移,将缓存的值转移到本地线程变量(堆) # 五、Seata ## 1、概念 在构建复杂的分布式系统时,确保事务的完整性和一致性至关重要。分布式事务,尤其是跨多个数据库和应用的事务,带来了挑战,因为需要在多个点上协调执行和提交或回滚操作。Seata(分布式的事务一致性解决方案)提供了一种框架,帮助开发者简化分布式事务的管理,确保应用在面对复杂分布式场景时仍然能够提供一致的用户体验。 Seata的出现旨在解决分布式事务中的关键问题,比如故障恢复、事务隔离性、跨数据库操作的统一管理和分布式锁等。通过Seata,你可以轻松地在多种数据库环境下实现ACID(原子性、一致性、隔离性、持久性)事务的分布式一致性。 ## 2、分布式事务的基本原理 分布式事务的基本原理是确保在多个数据库或服务中执行的事务操作能够在任何情况下达到正确的状态。这通常涉及协调器(如Seata的TCC或SAGA模式)来调度和监控事务的执行,以及在发生故障时进行回滚或补偿操作。 Seata相当于代理人,来统汇微服务多模块之间的事务(范围自定),当事务报错即恢复原来状态,方法多样,看你选择什么代理模式 ## 3. Seata的三大角色 在 Seata 的架构中,一共有三个角色: **TC** (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。 **TM** (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。 **RM **(Resource Manager) - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 ## 4. Seata模式选择 本项目使用AT模式,书写undo_log,用来记录和编写反向sql,重点讲解AT,其他笔记都是直接粘的,我感觉说的不清楚,用的时候再查吧(坑) ### ①TX(事务模式) 定义与应用场景 TX模式是Seata的基本场景,用于处理标准的分布式事务。在这种模式下,一个全局的事务ID在整个分布式环境中作为唯一标识,用于协调所有参与者的执行和提交/回滚操作。TX模式适用于简单且直接的分布式事务场景,例如电商系统中的购物车加减操作。 示例代码 ```java // 创建Seata TX模式的全局事务 SeataTransactionManager txManager = new DefaultTransactionManager(); txManager.start("globalTx"); // 执行本地事务 Connection conn = dataSource.getConnection(); try (Statement stmt = conn.createStatement()) { stmt.executeUpdate("UPDATE users SET balance = balance - 10 WHERE id = 1"); stmt.executeUpdate("UPDATE orders SET status = 'Paid' WHERE id = 1"); } catch (SQLException e) { txManager.rollback("globalTx"); throw e; } finally { txManager.commit("globalTx"); } ``` ### ②SAGA(补偿模式) 定义与应用场景 SAGA模式(SAGA transaction,补偿型事务)用于处理业务链路中可能出现的多个操作,其中某些操作可能失败或部分成功。在这种模式下,事务被分解成多个补偿动作,确保业务链路的最终一致性。 示例代码 ```java public void transfer(String fromId, String toId, int amount) { try { // 执行转账操作 txManager.start("sagaTx"); transferTo(fromId, toId, amount); transferFrom(fromId, toId, amount); txManager.commit("sagaTx"); } catch (Exception e) { // 回滚补偿操作 txManager.rollback("sagaTx"); } } private void transferTo(String fromId, String toId, int amount) { // 扣除用户余额 updateBalance(fromId, -amount); } private void transferFrom(String fromId, String toId, int amount) { // 添加用户余额 updateBalance(toId, amount); } private void updateBalance(String id, int amount) { // 执行更新操作 } ``` ### ③补偿模式(分支):AT(读未提交) 定义与应用场景 AT模式(Also Known As,读已提交)允许在事务日志中记录每个操作的结果,一旦事务完成,其结果作为最终状态。这种模式适用于需要在事务执行期间获取最新数据的场景。 AT 模式是一种无侵入的分布式事务解决方案。阿里 Seata 框架,实现了该模式。 在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。 3.1 AT模式实现说明 一阶段: ![img](https://i-blog.csdnimg.cn/blog_migrate/fa068c4299353028d368177073ab5638.png) Seata 会拦截" 业务SQL",解析 SQL 语义 查询 “业务SQL” 要更新的业务数据,在业务数据被更新前,将其保存成 “before image” 执行 “业务SQL” ,更新业务数据 查询更新后的数据,将其保存成 “after image” 将 before image 和 after image 保存至 Undo Log 表中 生成行锁 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。 二阶段(提交): 因为 “业务SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。 ![img](https://i-blog.csdnimg.cn/blog_migrate/1483b5a692aa84f3484f7ff6cceb75b5.png) 二阶段(回滚): 首先要校验脏写,对比“数据库当前业务数据”和 “after image” 如果两份数据完全一致就说明没有脏写,可以还原业务数据。 如果不一致就说明有脏写,出现脏写就需要转人工处理。 用“before image”还原业务数据 删除快照数据和行锁 ![img](https://i-blog.csdnimg.cn/blog_migrate/2ac1bb35c6c4d8208ec9d9b91d4937fb.png) 3.2 实例说明 下单操作中包含 对订单服务与库存服务的操作,分别修改订单状态,与库存数量。示例图如下: 示例代码 ```java public void purchase(String userId, String productId, int quantity) { try { // 开始AT模式的全局事务 SeataTransactionManager txManager = new DefaultTransactionManager(); txManager.start("atTx"); // 获取最新库存数量 int currentStock = getProductStock(productId); // 扣库存并更新订单 if (currentStock >= quantity) { updateStock(productId, -quantity); saveOrder(userId, productId, quantity); } else { // 如果库存不足,回滚事务 txManager.rollback("atTx"); throw new InsufficientStockException(); } txManager.commit("atTx"); } catch (Exception e) { txManager.rollback("atTx"); } } private int getProductStock(String productId) { // 查询库存 // ... } private void updateStock(String productId, int quantity) { // 扣库存 // ... } private void saveOrder(String userId, String productId, int quantity) { // 保存订单信息 // ... } ``` ### ④声明式事务(链路模式) 定义与应用场景 链路模式(Declarative Transaction Management)允许开发者通过注解或配置来声明事务边界,简化了事务管理的复杂性,尤其适合微服务架构的大型系统。 示例代码 ```java @Transactional public void customerService(Customer customer) { // 执行数据操作,自动识别并管理事务边界 // ... } // 定义事务隔离级别 @Isolation(value = Isolation.READ_COMMITTED) public void customerServiceWithIsolationLevel(Customer customer) { // 自定义事务隔离级别 // ... } ``` ## 5. 配置过程 1. 下载Seata-server,下载服务端(最新版不会用,下的2.0,刚好cloud版本自动引入也为2.0) 2. 配置Seata依赖 ```xml io.seata seata-spring-boot-starter com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-spring-boot-starter ``` 3. 修改Seata的conf文件,修改的是application.yml ```yaml seata: config: nacos: group: SEATA_GROUP namespace: train password: nacos server-addr: 127.0.0.1:8848 username: nacos data-id: seataServer.properties type: nacos registry: nacos: application: seata-server cluster: default group: SEATA_GROUP namespace: train password: nacos server-addr: 127.0.0.1:8848 username: nacos type: nacos ``` 4. 在涉及全局事务的类中bootstarp中配置Seata设置 ​ 注解的地方是后来放在nacos中 ```yaml seata: tx-service-group: train-group #service: # vgroup-mapping: # train-group: default # grouplist: # default: 127.0.0.1:8091 config: nacos: group: SEATA_GROUP namespace: train password: nacos server-addr: 127.0.0.1:8848 username: nacos data-id: seataServer.properties type: nacos registry: nacos: application: seata-server # cluster: default group: SEATA_GROUP namespace: train password: nacos server-addr: 127.0.0.1:8848 username: nacos type: nacos ``` 5. 配置nacos的seataServer.properties文件 ```properties store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://rm-8vb36yjot6i39u421so.rwlb.zhangbei.rds.aliyuncs.com/seata?useUnicode=true&rewriteBatchedStatements=true store.db.user=seata store.db.password=Rzh063915 # 和微服务模块的seata.tx-service-group保持一致 service.vgroupMapping.train-group=default service.default.grouplist=127.0.0.1:8091 server.enableParallelRequestHandle=false ``` ## 6. 报错问题解决 本项目使用Seata2.0,Spring-cloud 2023.0.3,Spring-cloud-alibaba 2023.0.1,nacos2.3.2 在配置Seata遇到以下报错 ```java server端报错:以及数组越界 // 莫名的数组越界 java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0 // Seata某个类初始化问题 java.util.concurrent.ExecutionException: java.lang.NoClassDefFoundError: Could not initialize class io.seata.server.cluster.raft.RaftServerFactory$SingletonHandler at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396) ~[na:na] at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073) ~[na:na] at io.seata.core.rpc.processor.server.ServerOnRequestProcessor.onRequestMessage(ServerOnRequestProcessor.java:188) ~[seata-core-2.0.0.jar:2.0.0] ``` **解决措施**:在nacos中seataServer.properties的配置中,加入以下代码即可 ```properties server.enableParallelRequestHandle=false ``` # Jmeter的参数传递 每次给线程组添加token,当线程组很多之后就不好一个个添加了,因此,直接执行参数传递,不放文字了,直接放图片 Json提取器 ![image-20241017165054603](D:\Learning\Java\笔记图片\12306\jmeter参数传递1.png) 调试后置处理器,其中参数的$这个形式,可以工具下的函数助手生成 ![image-20241017165154704](D:\Learning\Java\笔记图片\12306\jmeter参数传递2.png) BeanShell后置处理程序,其中参数的$这个形式,可以工具下的函数助手生成 ![image-20241017165308207](D:\Learning\Java\笔记图片\12306\jmeter参数传递3.png) 在所需token参数的Header添加即可,其中参数的$这个形式,可以工具下的函数助手生成 ![image-20241017165622242](D:\Learning\Java\笔记图片\12306\jmeter参数传递4.png) # 六、Sentinel ## 1、概念 分布式系统的流量防卫兵,随着微服务的流行,服务和服务之间的稳定性变得越来越重要。[Sentinel](https://sentinelguard.io/) 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。 Sentinel 具有以下特征: - **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 - **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 - **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。 - **完善的 SPI 扩展机制**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 - ![Sentinel-features-overview](D:\Learning\Java\笔记图片\12306\sentienl特性.png) **1.1、流量控制** 想象你是一家餐馆的老板,餐馆座位有限,突然来了很多客人,如果不加控制,餐馆很容易超负荷运转,服务质量下降。Sentinel 就像一个门卫,当客人太多时,会限制进入的人数,保证每个进来的人都能享受到优质的服务。 例子:你设置餐馆的座位数为100,当店内人数达到100时,Sentinel 会暂时阻止新的客人进入,直到有空位出现。 **1.2、熔断降级** 如果餐馆的厨师突然生病,菜品的质量和出餐速度都会受到影响。Sentinel 就像一个智能系统,当发现菜品质量下降时,会主动通知客人暂停点菜,等厨师恢复状态再继续接单。 例子:当你的系统某个服务响应时间变得很慢或不可用时,Sentinel 会暂时停止对该服务的调用,防止因为这个问题影响到整个系统。 **1.3、热点参数限流** 有时候某些菜品特别受欢迎,比如限量的招牌菜,可能会被一抢而空。Sentinel 可以对这些热门菜品进行限流,确保更多的客人都有机会尝到。 例子:你设置每个客人只能点一份招牌菜,当有客人重复点这道菜时,Sentinel 会进行限制。 **1.4、系统负载保护** 假如你的餐馆有一定的厨房容量,一次性接收太多订单可能会导致厨房忙不过来。Sentinel 会监控整个系统的负载情况,保证不会超出承受范围。 例子:当系统的 CPU 使用率超过设定阈值时,Sentinel 会自动降低请求的通过率,确保系统在高负载下仍能稳定运行。 ## 2、使用 1. 引入依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` 2. 在启动类配置限流规则 不用特地在启动类加,在连接sentienl控台与服务端的时候可以在控制台进行限流规则的添加 ![image-20241017221457407](D:\Learning\Java\笔记图片\12306\sentinel控制台配置簇点链路.png) ```java package com.Java.train.business.config; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; import org.springframework.core.env.Environment; import java.util.ArrayList; import java.util.List; @SpringBootApplication @ComponentScan("com.Java") @MapperScan("com.Java.train.*.mapper") @EnableFeignClients("com.Java.train.business.feign") public class BusinessApplication { private static final Logger LOG = LoggerFactory.getLogger(BusinessApplication.class); public static void main(String[] args) { SpringApplication app = new SpringApplication(BusinessApplication.class); Environment env = app.run(args).getEnvironment(); LOG.info("启动成功!!"); LOG.info("测试地址: \thttp://127.0.0.1:{}{}", env.getProperty("server.port"), env.getProperty("server.servlet.context-path")); initFlowRules(); LOG.info("已定义限流设置"); } private static void initFlowRules(){ List rules = new ArrayList<>(); FlowRule rule = new FlowRule(); rule.setResource("doConfirm"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // Set limit QPS to 20. rule.setCount(1); rules.add(rule); FlowRuleManager.loadRules(rules); } } ``` 3. 配置注解@SentinelResource 无需添加该注解也能在控台端添加限流控制,除非当需要对限流的请求进行相应的依然需要添加该注解,要求添加的Handler必须参数一致,返回类型一致 ```java @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock") public void doConfim(ConfirmOrderDoReq confirmOrderDoReq) {} ``` 4. 连接控制台 在所需的业务中的配置文件中配置以下内容即可 ```yaml spring: cloud: sentinel: transport: port: 8719 dashboard: localhost:8080 ``` ![](D:\Learning\Java\笔记图片\12306\sentinel控制台配置簇点链路.png) ## 3、限流操作 当请求被限流时,若无特殊配置,直接报错,表示被拦截; 当然可以通过设置拦截后的处理方法来进行被限流请求的后续操作,只需在@SentinelRecourse注解添加(blockHandler) @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock"), 当请求被拦截时自动执行blockHandler方法,其中注意blockHandler的方法参数必须与@SentinelResource注解下的方法参数一致,并且附带BlockException 参数 ```java private void doConfirmBlock(ConfirmOrderDoReq req, BlockException e){ LOG.info("购票被限流:{}", req); throw new BusniessException(BusniessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION); } ``` ## 4、sentinel+nacos配置 sentinel中的配置如果不配置在nacos里,每次重启都会清空,因此为持久性的存储限流策略,通过在nacos中配置sentinel的相关限流策略进行持久化存储 配置步骤: 1. 在业务sentinel中配置nacos相关配置 ```yaml spring: cloud: sentinel: datasource: nacos: # 自定义的服务名称(叫啥都行 nacos: # 使用的服务,若使用nacos则必须写nacos (注意ruleType有自动补齐表示没写错,要不连不上nacos) username: nacos password: nacos dataId: sentinel groupId: TRAIN_GROUP namespace: train ruleType: flow serverAddr: 127.0.0.1:8848 # data-type: json transport: dashboard: localhost:8080 port: 8719 ``` 2. 在nacos配置持久化sentinel配置(以流控模式为例 ```yaml [ { "resource": "/", # 服务名称 "limitApp": "default", # 来源应用 "grade": 1, # 阈值类型,①0为并发线程,当调用该api的线程数达到阈值的时候,进行限流;②1为QPS-每秒查询率(Query Per Second) "count": 100, # 阈值 "strategy": 0, # 流控模式:①直连: api达到限流条件时,直接限流;# ②关联: 当关联的资源达到阈值的时候,就限流自己# ③链路 "controlBehavior": 0, #流控效果:①快速失败;②Warm Up;③匀速排队 "clusterMode": false #是否集群 }, { "resource": "/do", "limitApp": "default", "grade": 1, "count": 50, "strategy": 0, "controlBehavior": 0, "clusterMode": false } ] ``` ## 5、流控效果 1. 快速失败: 当请求被限制之后立马停止,报错或若有Handler进入Handler ![image-20241018184145701](D:\Learning\Java\笔记图片\12306\sentinel快速失败.png) 2. Warm up 预热,适合流量很大的接口,不会将所有当前线程的一下打过去,而是分流,可以限制预热时间,在所规定的预热时间内,由少到多的将请求慢慢推过去,时间之后正常的推送所有请求 ![image-20241018184213651](D:\Learning\Java\笔记图片\12306\sentinel预热.png) 3. 排队等待 当有请求达到阈值进行处理时,其他线程进入排队等待,规定排队时间,如若超过则拒绝掉 ![image-20241018184236614](D:\Learning\Java\笔记图片\12306\sentinel排队等待.png) ## 6、流控模式 1. 直连模式 默认的模式,只对自己当前的接口进行限流 2. 关联 关联一个服务名称,只有当关联的服务开始限流时,当前服务才开始限流,单独的当前服务不触发限流机制 ![image-20241018204017782](D:\Learning\Java\笔记图片\12306\sentinel关联模式.png) 3. 链路模式 对于调用一个方法的不同资源接口,可以选择式的对某个入口资源开启限流 使用要求:关闭web-context-unify=false 默认是接口都集合在-web-context中,因此即使配置链路也不会生效 ```yaml spring: cloud: sentinel: web-context-unify: false ``` 在nacos中配置需指定refResource(限流的资源接口) ![image-20241018204544546](D:\Learning\Java\笔记图片\12306\sentinel中链路配置.png) ## 7、熔断降级模式 当服务响应时间过长、服务出现问题,达到一定指标(自定义)会进行熔断,断开该出现问题的分布式服务,同时通过降级提供备用服务,保证系统完整性 1. 熔断慢调用比例 慢调用比例指在统计时长内达到最小请求数,线程处理时长超过最大RT的比例超过比例阈值,则进入熔断时长的熔断时间,断开当前服务连接,所有请求来都报错(可以降级进行备用处理) ![image-20241019170511679](D:\Learning\Java\笔记图片\12306\熔断配置.png) 2. 异常比例 线程出现异常达到一定比例即进入熔断 ![image-20241019174101194](D:\Learning\Java\笔记图片\12306\熔断异常比.png) 3. 异常数 线程异常达到一定数量即发生熔断 ![image-20241019174121985](D:\Learning\Java\笔记图片\12306\熔断异常数.png) # 七、RabbitMQ [RabbitMQ超详细学习笔记(章节清晰+通俗易懂)_robit mq-CSDN博客](https://blog.csdn.net/qq_45173404/article/details/121687489) 个人理解:MQ就是实现异步处理的中间件,让生产与消费异步进行,通过消息队列沟通 ## 1、安装 安装可以通过docker一键部署在服务器内,本项目时将服务下到在windows中,具体下载步骤可以看一下博客,大体就是现在对应版本的Erlang和RabbitMQ,因为RabbitMQ是用Erlang写的,然后cmd启动mq即可,然后登录默认端口控制台[RabbitMQ Management](http://127.0.0.1:15672/#/) 博客:[【Windows安装RabbitMQ详细教程】_rabbitmq windows-CSDN博客](https://blog.csdn.net/tirster/article/details/121938987) ## 2、使用 首先下载rabbitMQ的server端(默认端口为5672,然后启动,他配套有一个控制台端口号为15672 以下为链接java的步骤 1. 引入依赖 ```xml org.springframework.boot spring-boot-starter-amqp com.fasterxml.jackson.core jackson-databind 2.16.0 ``` 2. Win在RabbitMq的sbin目录下启动cmd输入启动命令 ``` rabbitmq-plugins enable rabbitmq_management ``` 3. 输入网址进入其前端管理界面 http://127.0.0.1:15672/ 4. 可以选择为项目配置虚拟主机和账号,然后在后面配置在项目中使用 5. 配置文件 - 连接rabbitMQ ```yaml spring: rabbitmq: host: 192.168.148.186 # 你的虚拟机IP port: 5672 # 端口 virtual-host: /train # 虚拟主机 username: train # 用户名 password: train # 密码 ``` 6. 连接MQ具体队列 ```java // 向ConfirmOrder.queue发送 rabbitTemplate.convertAndSend("ConfirmOrder.queue", message); ``` 写Consumer来消费(就是RabbitOrderConsumer中消耗) ```java package com.Java.train.business.consumer; import cn.hutool.json.JSON; import cn.hutool.json.JSONUtil; import com.Java.train.business.entity.DTO.ConfirmOrderReqDTO; import com.Java.train.business.enums.RabbitQueueEnums; import com.Java.train.business.service.ConfirmOrderService; import jakarta.annotation.Resource; import org.slf4j.MDC; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class RabbitOrderConsumer { // 利用RabbitListener来声明要监听的队列信息 // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。 // 可以看到方法体中接收的就是消息体的内容 @Resource private ConfirmOrderService confirmOrderService; @RabbitListener(queues = "ConfirmOrder.queue") public void listenSimpleQueueMessage(String msg) throws InterruptedException { ConfirmOrderReqDTO confirmOrderReqDTO = JSONUtil.toBean(msg, ConfirmOrderReqDTO.class); String logId = confirmOrderReqDTO.getLogId(); MDC.put("LOG_ID", logId); System.out.println("spring 消费者接收到消息:【" + confirmOrderReqDTO + "】"); confirmOrderService.doConfim(confirmOrderReqDTO); } } ``` 但配置队列是在控制台添加的,但实际应该在Config中进行注解配置 Tips: 进行消息处理时是不会走日志过滤器,因此需要通过参数Req来传LogId来添加日志号 # 八、支付模块(支付宝) ## 1、概念介绍 支付宝提供的一套供开发者模拟支付的开发接口,支付宝提供的支付场景涉及多种(大致八种)包含当面付、APP支付、手机网站支付、电脑网站支付、刷脸付、预授权支付、商家扣款、订单码支付;本项目应用**当面付中的扫码支付** ## 2、支付流程 官方支付流程的教程——[扫码支付快速接入 - 支付宝文档中心](https://opendocs.alipay.com/open/194/106078?pathHash=193f2039) ![img](D:\Learning\Java\笔记图片\12306\支付模块开发流程.png) 扫码支付流程图 ![img](D:\Learning\Java\笔记图片\12306\扫码支付流程.jpeg) 2.1 交互流程 用户交互可能出现多种情况,如余额不足,网络延迟等,以下为交互各情况图 ![img](D:\Learning\Java\笔记图片\12306\扫码支付流程图.png) 3、安全性(公钥与秘钥) 为防止在用户付款时,发送的信息被破译和修改,导致的问题,因此为保证安全性,使用公钥加密,私钥破译等手段 ![image-20241024164830646](D:\Learning\Java\笔记图片\12306\秘钥加密.png) ## 3、支付宝配置类 本机调用支付宝的SDK接口来完成实现支付流程 1. 首先引入支付宝依赖 ```xml com.alipay.sdk alipay-sdk-java 4.34.0.ALL com.google.zxing core 3.3.3 com.alipay.sdk alipay-easysdk 2.2.0 ``` 2. 配置连接支付宝沙盒的配置名称 ```yaml alipay: easy: protocol: https gatewayHost: openchannel-sandbox.dl.alipaydev.com signType: RSA2 appId: 9021000141651837 merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJeUhrKhvPeS+HSsmgeCZYFz8GZqHZoFuCTSm+hyiQSU7i6sydcOfxsuui6ba7DsLjutlGzEHK7DdwHqAArx87zhYhzL1xMV1dSN3NogMwrjbvH9OH1Qo3VzJrsx6aAcs/MLm/bDTEhIfE5U4qjtQx6H4dalTPhOqHTooeWHo6jdEmu9cyyIx3ocYozXyT4iOKEklTbRoXX+22UMOluelSqwRobmlyhUjGQ9xKRWYEVLnGbEwNhXv3g3Sp6QMgcqocSfoqpfVb2ZoQGWUAOX/mVMglQHin5Vh0TrF2B3WB0iKt1+dyhkgSuB7sJTmDGy70yCvy2ZVABDQGlnQVjNEzAgMBAAECggEBAILn0MwAgi1eGimXyEYBizh1cjfge40BijOxm0v2Wb7WQvFhii1J7Qq4LC6gGGCWCL8a6o37rUwCC0tIOsH3Bx4+m1a+xuBDKNyxuZwghl4c9AiO70w0efzXJSWhLmL2g269ZBMiZW7ChczDCPTR9MGwxDCspApp3FCqEUgCSPi9f+VHA6UTvSD667QsSY94t6OjpgdF8/+vPUy8T2xnNt+UaHwxRM+iEqlWZjviVuaQzEgjBTgQnA5/44ZGA9HC0Qm+G4Uxs95brwtSwanutyYHGuGff9kYssgNFUznAMYVJ0J0CBkhqd3wnw8v6jTNYczwayPJwNajA1YiK/lBK3ECgYEA+zXtnA2FN1KvkHAvitWA9w0+v4M9QMwWL97PV6+uosgZiwEyIHk1gIoD9jC4Hy4Z5LlSUAyvZU4J2654FGfZ2BoTM1gemtnVh9HLoUa0IxYQe2aybGSkRlUgUZQlW/CQcQ5RXSUw84tG6nYAoVtTybAoE4qTpZFaNZWJSgSQea0CgYEAzVCbMjw+i72HU12MXfW5RyeFdRcwGw4E4ABy+uiS0BbnLg7iua4b7vrSR6XOhoWTWMdqV/3Fb0uUvDw0VOnIYcZKzQHAR7bW46IBvmfv0LgV1YkSCXg3S87KWhlV3hdDUi4b1lJrvJYACtkbAMI3P/rq9JZFZ0gBVdw2BVAXkl8CgYBKuAadpPPbua9YkdXCQLDkSr9ALWP5svpicK5RUYLrDgNy8f8b45GDUwnMnz8Unxy6iFiwUKWWxzhtbB5xS1ZjTZYqbmpj+qjlhgRASwxnZzetzKUDUof+F3HOcfcOuqXryqtuvhIqTmHgeQrE86ofUlMJRO2XkH05dOnp5yOl3QKBgDJhv1O6eNatsZGBeTptXAPb7OHoyMM603NsaMqtW6l4lU42FOjfkrc4EXXHeECGcrvsY2ooOdSvxVXTqDvWCngDwsM/1MFEWsMNNERXGQAszB7UPfrY5yjRG9K0OOAA8WIvMmGLZCio3mXLFNdA0XH/zrwqS+mmP72kBhWTq9UdAoGACVZ7IHzyzbYG53jILz5nCxOEB02ZXCZ9wM9N9K3Y4AsIQMakY17NGFMrhC4J3cjl2r+n6xkAtGubrkiPNhpLz+PKXoVY8/ycOtiAMlHs0kywVAuvxuNElwCkmtzhVDAHQeDhfLM8SanVEjiGVt5zQw2udSPgodcFTy+i+oBmGYM= alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiar54ci1InXYI96uuS/h2tkRaQsAFZfjKJhOejGUGqNzL8umCJfkXGDfi0uWHsNz7zQNZxtjOvH1vIFwFzR5b752XwgsoxxhmmufE89/IvrgnpVgIk4G7IMx2jDc4ZS7ybwfaMGRLzubGL11tqTNjVpWYLF6KgLPeBUo9xySO0NkVtuiiuMz3tjCigN/1dfCBMcp4Pc4a9uAqQAQZdzVv7tyAqWS7ghO7XVxxqTWIzycfDVx9fMnWkxOifwmGcagswECk2XgwprpUg9fIHVvbIFYxOM00Q1eY7K2S/tSwIxNeaZWzUv3Zy8wpXJLhqFh3u/Nw3D5ubBB5SvvjwqU9wIDAQAB notifyUrl: http://127.0.0.1.com/ charset: utf-8 format: json ``` 其上内容都在沙盒中 3. 配置config来获取这些配置类,并注入spring中,以便方便直接配置Factory的环境 ```java package com.Java.train.pay.config; import com.alipay.easysdk.factory.Factory; import com.alipay.easysdk.kernel.Config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @Configuration @Data public class AlipayConfig extends Config { @Bean public Config config(AliProperties aliProperties){ Config config = new Config(); // 协议 config.protocol = aliProperties.getProtocol(); // 请求网关 config.gatewayHost = aliProperties.getGatewayHost(); // 签名类型 config.signType = aliProperties.getSignType(); // 应用id config.appId = aliProperties.getAppId(); // 应用公钥 config.merchantPrivateKey = aliProperties.getMerchantPrivateKey(); // 支付宝公钥 config.alipayPublicKey = aliProperties.getAlipayPublicKey(); // 异步通知网关 config.notifyUrl = aliProperties.getNotifyUrl(); // config.encryptKey = aliProperties.getEncryptKey(); return config; } } ``` 根据内容生成支付宝二维码的代码和异步接收消息的接口 ```java package com.Java.train.pay.controller; import cn.hutool.extra.qrcode.QrCodeUtil; import cn.hutool.extra.qrcode.QrConfig; import cn.hutool.json.JSONUtil; import com.Java.train.pay.config.AlipayConfig; import com.alibaba.fastjson.JSONObject; import com.alipay.easysdk.factory.Factory; import com.alipay.easysdk.kernel.Config; import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.lang.module.Configuration; import java.nio.file.LinkOption; @RestController @RequestMapping @AllArgsConstructor public class PaymentController { private static final Logger LOG = LoggerFactory.getLogger(PaymentController.class); // @Resource // AlipayConfig alipayConfig; private final Config config; @GetMapping public String pay() throws Exception { // 注入config Factory.setOptions(config); // 调用支付宝接口 AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace().preCreate("iphone 15pro max", "1231233", "1000"); // 获取支付宝返还的交易连接 String httpBody = response.getHttpBody(); JSONObject jsonObject = JSONObject.parseObject(httpBody); String qrCode = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString(); // 根据支付连接生成二维码 QrCodeUtil.generate(qrCode, 500, 500, new File("D://nihao.jpg")); return httpBody; } @PostMapping("/notify") public String notify(HttpServletRequest request){ LOG.info("收到支付宝反馈信息"); String out_trade_no = request.getParameter("out_trade_no"); LOG.info("流水号为:{}", out_trade_no); return out_trade_no; } } ``` ## 4、cpolar [利用cpolar-内网穿透工具,将内网服务器暴露给公网访问_tunnel status-CSDN博客](https://blog.csdn.net/probezy/article/details/83707433) 内网穿透,外部人员无法访问内网的接口(目前场景是支付宝需要调用咱的接口给咱返还响应内容,但咱内网没开放,因此需要cpolar来公开外网可访问的url)cpolar可以把内网的站点变成公网可以访问的站点 使用步骤: 1. 首先先去官网下载对应版本 [cpolar - secure introspectable tunnels to localhost](https://dashboard.cpolar.com/get-started) 2. 安装之后在命令行窗口执行以下代码,将你的cpolar账号的authtoken添加到其配置文件中去,其位置在C:/用户/.cpolar/cpolar.yaml文件下 ```shell cpolar authtoken YWQ5ZTkyOTEtOTc1NC00OTc3LWEyMzgtNDIyNjdhYzgyZWQw ``` 3. 添加你的站点authtoken,此处使用natapp生成其隧道,附带authtoken,然后使用cpolar或natapp也行,此处用的cpolar来对内网生成外网站点(相当于公开在外网上) ![image-20241025231940129](D:\Learning\Java\笔记图片\12306\natapp.png) 在命令行中执行cpolar http 8004 (你要开放的端口) 运行接口如下 ![image-20241025232047948](D:\Learning\Java\笔记图片\12306\内网穿透.png) # 九、异常与解决 ## 1、前端vue项目无法启动 1. 需要先配置nodejs和npm环境变量(最好自定义,更统一配置环境) 2. 下载vue项目用到的依赖 执行 npm install 即可 3. 启动vue运行脚本 npm run web-dev ## 2、解决前端和后端独立服务器的跨域问题 在所涉及跨域的模块yaml下配置一下内容即可 ```yaml spring: cloud: gateway: # 配置跨域信息 globalcors: cors-configurations: '[/**]': allowCredentials: true allowedHeaders: '*' allowedMethods: '*' allowedOriginPatterns: '*' maxAge: 3600 ``` ## 3、前后端参数传递错误(增删改查常见) 前后端所约束的变量名称应该一致,若还不可获取参数信息,可能是传递方式,xxx-form.... 的方式可能不适用,可以切换为json格式传输 ## 4、sql关键词作为字段名称 sql 8.0之后版本的关键词[9.3 关键字和保留字_MySQL 8.0 参考手册](https://mysql.net.cn/doc/refman/8.0/en/keywords.html#google_vignette) 在注意row、row_count 和 index都是关键词,不能作为关键词 在项目中,我将row 、 row_count 修改为 line 和 line_count index 改为 idx **一定在创建字段名的时候考虑到关键词,不然当开发到一定程度,如果不改对应名称的话还好(但后面容易歧义,因为字段名和变量名不对应),但要是改了变量名,则前后端交互的名称都必须更改,不然无法交互** 经实践:date、name无影响 ## 5、前后端时间永远差一天 时区问题,在传递展示的日期参数上添加Date来格式化展示,在DTO上的Date添加 ```java @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") ``` ## 6、端口占用并删除 netstat -aon|findstr 7009 直接找到端口号为7009的进程PID taskkill /pid 4628 -t -f 删除对应端口查出的PID进程值 ## 7、后端日期校验与数据库中无法匹对 首先在方法入口,传入参数的类所标榜的日期附带@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")注解,注意**一定要添加时区**,在查询中Date上附带注解@DateTimeFormat(pattern = "yyyy-MM-dd") (这个不带好像也行) # 十、定时处理 [Java 面试题——定时任务_定时任务面试题-CSDN博客](https://blog.csdn.net/weixin_43004044/article/details/131069663?ops_request_misc=%7B%22request%5Fid%22%3A%2287EFE262-B26E-42D0-B686-4AE0E1E71988%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=87EFE262-B26E-42D0-B686-4AE0E1E71988&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-131069663-null-null.142^v100^pc_search_result_base6&utm_term=java定时任务作用&spm=1018.2226.3001.4187) ## 概述 什么是定时任务?为什么需要定时任务? (1)定时任务是一种在指定的时间点或时间间隔内自动触发执行的任务。它能够周期性地执行一些重复性、时间敏感或计划性的操作,而无需人工干预。定时任务的需求主要有以下几个方面: - 自动化:定时任务可以实现某些操作的自动化,无需人工手动执行。这可以提高工作效率,减少人力资源的投入,特别是对于一些重复性、繁琐的操作,如数据备份、日志清理、文件转换等,通过定时任务可以自动完成,节省人力成本和时间。 - 定时业务处理:在某些业务场景中,需要按照预设的时间计划执行一些操作,如定时发送邮件、定期生成报表、定时更新缓存等。通过定时任务,可以确保这些业务操作在预定的时间点或时间间隔内得到执行,满足业务需求。 - 特定时间点的操作:有些操作需要在特定的时间点进行,如定时上线、定时下线、定时触发系统升级等。定时任务可以让这些操作在指定的时间点自动执行,避免人为因素导致的延误或错误。 - 优化资源利用:通过合理调度定时任务,可以在系统的非高峰期执行一些资源密集型的任务,充分利用资源,减少对系统性能的影响。例如,在夜间执行数据库优化、数据同步等任务,可以减少对用户业务的影响。 (2)综上所述,定时任务能够实现操作的自动化、定时业务处理、特定时间点的操作,并且可以优化资源利用。通过定时任务,可以提高工作效率,降低人工操作的成本和错误率,进一步提升系统的稳定性和可靠性。 ## cron 表达式 (1)cron 表达式是一个用于指定定时任务执行时间的字符序列。它由 6 个或 7 个域组成,中间使用空格分开,每个域分别表示秒、分、小时、日期、月份、星期和年(可省略)。cron 表达式有如下两种语法格式: Seconds Minutes Hours DayofMonth Month DayofWeek Year Seconds Minutes Hours DayofMonth Month DayofWeek ## 实现方式 实现定时任务的方式如下所示: 单机定时任务:Timer、ScheduledExecutorService、DelayQueue、Spring Boot Task; 分布式定时任务:Redis、MQ; 分布式任务调度框架:XXL-JOB、Quartz ### Quartz 支付 # 前端知识 ## 一、Vue的基本使用 ### 1、vue界面的基本组成 是由template(页面展示)、script(交互内容)和style(CSS页面样式布局) ### 2、事件绑定 vue中就两种绑定形式:和@ :表示绑定属性,:后的变量名就是等号后的值 @表示绑定事件,@绑定你自定义的事件,在触发@变量的操作后触发绑定事件 ```vue // 在script中声明 const onFinish = values =>{ console.log("Success:", values); }; ``` ### 3、声明响应式变量 vue中有两种方法声明,reactive和ref ### 4、通知组件 在vue文件中引入anti-design-vue 的通知组件,实现返回信息弹窗 ### 5、动态配置文件信息 添加全局动态信息,在总前端目录下添加文件.env.环境名称 (例如:.env.dev),配置信息) ``` # 内置变量 NODE_ENV=development # 自定义变量信息 必须以VUE_APP_ 开头 VUE_APP_SERVER=http://localhost:8000 ``` 但此时在前端静态初始时还获取不到所定义的server,因此需要在所配置的启动项中添加 --mode dev(对应你所写的.env. 的文件后缀名)以dev为例: ```xml "scripts": { "web-dev": "vue-cli-service serve --mode dev --port 9000", ... } ``` 再次基础上,添加前端统一动态访问前缀, **设置baseURL** ```js axios.defaults.baseURL = process.env.VUE_APP_SERVER; # process.env. 为固定写法 ``` ### 6、调整主框架main.js,添加子路由 设置main.js的内容包括整体网站的大体框架,具体内容由子路由内容添加 结构基本为:[ 大体框架 ( 具体子路由)],将所有子路由下的板块展示在main的大框架文件下 注意设置页面跳转,当访问根页面或起始页面,定位到welcome页面,不要定到main(没内容) #### 6.1 设置菜单 设置菜单的key ​ 定义菜单跳转子路由 将菜单的key存入ref数组中,在展示页面时获取当前的key,然后直接定位到路由上 ### 7、react响应式赋值后会失去响应式特性 赋值(=)过后会失去其响应式特性,因此在为其赋值时需要特别注意 如果是向数组中添加元素可以不赋值 - 直接调用.push(...list) 来将list元素放入react变量中,其中的 **...** 是将list拆开为一个个元素,然后push - 在react变量中添加list属性名,直接给list赋值就行,不会失去响应式(因为是给属性名赋值) ### 8、前后端Long类型数据的精度丢失 后端数据Long类型传递给前端受前端的数值限制,其展示内容可能有误(填数等等),需要后端书写精度校验,将Long类型转为String类型传给前端,但前端也需要接受Long类型数据(只是报错,给自动转吧) 还需要再需要转的Long变量上添加@JsonSerialize(using = ToStringSerializer.class) 注解 ```java // package com.jiawa.train.common.config; // // import com.fasterxml.jackson.databind.ObjectMapper; // import com.fasterxml.jackson.databind.module.SimpleModule; // import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; // import org.springframework.context.annotation.Bean; // import org.springframework.context.annotation.Configuration; // import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; // // /** // * 统一注解,解决前后端交互Long类型精度丢失的问题 // * 公众号:甲蛙全栈 // * 关联视频课程《Spring Boot + Vue3 前后端分离 实战wiki知识库系统》 // * https://coding.imooc.com/class/474.html // */ // @Configuration // public class JacksonConfig { // @Bean // public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { // ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // SimpleModule simpleModule = new SimpleModule(); // simpleModule.addSerializer(Long.class, ToStringSerializer.instance); // objectMapper.registerModule(simpleModule); // return objectMapper; // } // } ``` ### 9、前端将汉字转为拼音 使用插件 pinyin-pro 即可完成转化 ### 10、父子组件之间交互 在选择车次和选择站点时,我们使用的是在父组件新增和查询中,添加子组件下拉框完成的,而子组件选中的内容应当显示在父组件上,因此涉及到父子组件之间的交互,因为使用的ant-design-vue(ADV)组件库,通过emit完成交互 以火车车站为例子: ```vue 父组件中:使用子组件 参数绑定v-model,width为传递到子组件中的变量(样式) 子组件中接收 template中: script中: props: ["modelValue", "width"], emits: ['update:modelValue', 'change'], // 此处为父子组件交互的方法 ``` 子组件与父组件交互:以change为例 当子组件自定义事件,当@触发事件后,触发交互( 截取部分) ```js setup(props, {emit}) { const trainCode = ref(); const trains = ref([]); const localWidth = ref(props.width); if (Tool.isEmpty(props.width)) { localWidth.value = "100%"; } // 利用watch,动态获取父组件的值,如果放在onMounted或其它方法里,则只有第一次有效 watch(() => props.modelValue, ()=>{ console.log("props.modelValue", props.modelValue); trainCode.value = props.modelValue; }, {immediate: true}); /** * 将当前组件的值响应给父组件 * @param value */ const onChange = (value) => { emit('update:modelValue', value); let train = trains.value.filter(item => item.code === value)[0]; if (Tool.isEmpty(train)) { train = {}; } emit('change', train); }; } ``` ### 11、前端缓存所选中的内容 在用户查出票的数据之后,用户预定票进行跳转后若为使用本地缓存的变量,则相当于重新请求,其所勾选的值会清空,让用户再次选择后筛选,大大降低用户体验,因此引入SessionStorage来缓存用户所勾选的信息,在用户选中之后会将本地变量的值赋给所规定的SessionStorage中的大写枚举类,查询时,通过该枚举类查询即可