小柒味来 - 项目开发总结与经验归纳

一、前言

本文把项目中常用的中间件、框架与技术点做一个汇总:包括 Nginx、JWT、Redis、Spring Cache、AOP、事务、定时任务、WebSocket、OSS、POI 等


二、技术点

每个条目格式:概念场景实现要点 / 快速示例

1. Nginx

专门写了一篇,请移至多功能服务器 - Nginx

  • 概念:通过调度算法把请求分发到多台应用服务器,实现水平扩展与容灾。
  • 场景:流量分发、静态资源加速、反向代理
  • 要点:常用算法:轮询、最小连接、IP hash。配合 health check 与 SSL。

1.1 正向代理

  • 概念:客户端通过代理访问外网。

1.2 反向代理

  • 概念:对外表现为单一服务器,内部转发到后端服务

2. MD5 加密

  • 特点:固定长度、不可逆

注意单纯 MD5 不够安全,一定要配合盐值等加强的算法。

  • 示例
private static final String SALT = "qi";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

3. Swagger

Swagger的详细介绍在接口文档生成 - Swagger

  • 作用:自动生成在线 API 文档。

  • 集成要点

    1. 引入 Knife4j(swagger的ui增强版)依赖
    <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
            </dependency>
    
    1. 在配置类中添加配置
    @Bean
        public Docket docket1(){
            log.info("准备生成接口文档...");
            ApiInfo apiInfo = new ApiInfoBuilder()
                    .title("小柒味来项目接口文档")
                    .version("2.0")
                    .description("小柒味来项目接口文档")
                    .build();
    
            Docket docket = new Docket(DocumentationType.SWAGGER_2)
                    .groupName("管理端接口")
                    .apiInfo(apiInfo)
                    .select()
                    //指定生成接口需要扫描的包
                    .apis(RequestHandlerSelectors.basePackage("com.qi.controller.admin"))
                    .paths(PathSelectors.any())
                    .build();
    
            return docket;
        }
    
    1. 使用注解标注接口:
    @Api(tags = "用户模块")
    @ApiModel
    @ApiModelProperty
    @ApiOperation("查询用户")
    
    1. Knife4j的访问地址为:http://localhost:8080/doc.html

4. JWT

  • 组成:Header(类型+算法) + Payload(载荷) + Signature(签名)。
  • 场景:身份认证、授权、服务间信息传递(无状态)。
  • 实现要点
    1. 引入 JWT 库;
    2. 封装工具类负责生成或解析 token;
    3. 在拦截器或过滤器中验证 token 并从载荷取用户信息。

5. 拦截器 Interceptor

  • 概念:基于 Spring 的请求拦截(本质与 AOP 相关)。

  • 场景:登录校验、权限控制、性能监控、请求日志。

  • 实现

    1. 实现 HandlerInterceptor,重写 preHandlepostHandleafterCompletion
    @Component
    @Slf4j
    public class JwtTokenUserInterceptor implements HandlerInterceptor {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        /**
         * 校验jwt
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断当前拦截到的是Controller的方法还是其他资源
            if (!(handler instanceof HandlerMethod)) {
                //当前拦截到的不是动态方法,直接放行
                return true;
            }
    
            //1、从请求头中获取令牌
            String token = request.getHeader(jwtProperties.getUserTokenName());
    
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
                Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
                log.info("当前用户id:", userId);
                BaseContext.setCurrentId(userId);
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
        }
    }
    
    1. 在配置类中注册并设置拦截路径。
        @Autowired
        private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
        /**
         * 注册自定义拦截器
         *
         * @param registry
         */
        protected void addInterceptors(InterceptorRegistry registry) {
            log.info("开始注册自定义拦截器...");
            registry.addInterceptor(jwtTokenAdminInterceptor)
                    .addPathPatterns("/admin/**")
                    .excludePathPatterns("/admin/employee/login");
        }
    
    1. preHandlepostHandleafterCompletion详解
    方法名 调用时机 功能 典型用途 返回值
    preHandle Controller 方法执行前 权限校验、日志记录、请求参数处理 登录验证、Token 校验、请求限流 返回 true 放行,返回 false 拦截请求
    postHandle Controller 方法执行后,视图渲染前 调整返回数据或视图 统一响应包装、添加公共数据
    afterCompletion 整个请求处理完成后(视图渲染结束) 清理资源、记录日志、处理异常 性能监控、日志记录、异常处理 无返回值,即使 preHandle 返回 false 也可能调用

7. 异常处理 — 全局异常处理器

  • 注解@RestControllerAdvice + @ExceptionHandler

  • 作用:统一捕获异常、返回规范化错误响应、避免暴露堆栈信息。

  • 实例

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex) {
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    /*
     * 捕获SQL异常
     * */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        log.error("异常信息:{}", message);
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String msg = split[2] + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);

        } else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }

    }
}

8. AOP 切面

  • 概念:把横切关注点(如日志、权限、事务)从业务中抽离。

  • 使用场景:操作日志、权限校验、性能统计、事务增强等。

  • 实现步骤

    1. 定义自定义注解
    /*
    * 自定义注解,用于标记和记录方法的执行信息
    * */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
        //数据库操作类型:INSERT、UPDATE
        OperationType value();
    }
    
    1. 编写带 @Aspect 的切面类,定义通知类型(@Before / @After / @Around
    @Aspect
    @Component
    @Slf4j
    public class AutoFillAspect {
        /**
         * 切入点方法
         * 定义了需要进行自动填充的切入点
         * 该切入点匹配使用了@AutoFill注解的Mapper接口中的所有方法
         */
        @Pointcut("@annotation(com.qi.annotation.AutoFill) && execution(* com.qi.mapper.*.*(..))")
        public void autoFillPointCut() {
        }
    
        /**
         * 在执行切入点方法之前进行自动填充
         *
         * @param joinPoint 切入点对象,包含执行方法的信息
         */
        @Before("autoFillPointCut()")
        public void autoFill(JoinPoint joinPoint) {
            // 记录自动填充开始的日志
            log.info("开始进行公共字段自动填充。。。。");
    
            // 获取方法签名和@AutoFill注解
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
    
            // 获取操作类型(插入或更新)
            OperationType operationType = autoFill.value();
    
            // 获取方法参数
            Object[] args = joinPoint.getArgs();
            if (args == null || args.length == 0) {
                return;
            }
            Object entity = args[0];
    
            // 获取当前时间和用户ID
            LocalDateTime now = LocalDateTime.now();
            Long currentId = BaseContext.getCurrentId();
    
            // 根据操作类型进行相应的字段填充
            if (operationType == OperationType.INSERT) {
                try {
                    // 对于插入操作,填充创建时间和用户以及更新时间和用户
                    Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                    Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
    
                    setCreateTime.invoke(entity, now);
                    setCreateUser.invoke(entity, currentId);
                    setUpdateTime.invoke(entity, now);
                    setUpdateUser.invoke(entity, currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else if (operationType == OperationType.UPDATE) {
                try {
                    // 对于更新操作,仅填充更新时间和用户
                    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                    setUpdateTime.invoke(entity, now);
                    setUpdateUser.invoke(entity, currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    1. 在目标方法/类上加注解
        @AutoFill(value = OperationType.INSERT)
        void insert(Dish dishDTO);
    

9. Redis

详细使用请移步存储中间件-Redis

  • 概念:基于内存的 key-value 非关系型数据库。

  • 场景:缓存、消息队列、排行榜、分布式锁等。

  • 接入步骤

    1. 引入依赖(spring-boot-starter-data-redis)
    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    1. 配置 Redis 连接
    1. 定义与注入 RedisTemplate

      • 自定义序列化
      @Bean
          public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
              // 记录创建RedisTemplate对象的日志
              log.info("开始创建redis对象");
              // 实例化RedisTemplate
              RedisTemplate redisTemplate = new RedisTemplate();
              // 设置Redis连接工厂
              redisTemplate.setConnectionFactory(redisConnectionFactory);
              // 设置键的序列化方式为StringRedisSerializer,确保键以字符串形式存储
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              // 返回配置好的RedisTemplate实例
              return redisTemplate;
          }
      
      • 注入
          @Autowired
          private RedisTemplate redisTemplate;
      
          @Test
          public void testRedisTemplate(){
              System.out.println(redisTemplate);
              //string数据操作
              ValueOperations valueOperations = redisTemplate.opsForValue();
              //hash类型的数据操作
              HashOperations hashOperations = redisTemplate.opsForHash();
              //list类型的数据操作
              ListOperations listOperations = redisTemplate.opsForList();
              //set类型数据操作
              SetOperations setOperations = redisTemplate.opsForSet();
              //zset类型数据操作
              ZSetOperations zSetOperations = redisTemplate.opsForZSet();
          }
      

10. Redis 序列化方式

  • JdkSerializationRedisSerializer:Java 默认(体积大)。
  • StringRedisSerializer:字符串序列化。
  • GenericToStringSerializer:泛型转字符串。
  • Jackson2JsonRedisSerializer / GenericJackson2JsonRedisSerializer:JSON 序列化(常用,兼容性好)

11. Spring Cache

  • 概念:通过注解简化缓存操作。

  • 常用注解

    • @EnableCaching(启动类)
    • @Cacheable(读取并缓存)
    • @CachePut(更新缓存)
    • @CacheEvict(清理缓存)
  • 流程

    1. 引入缓存实现(如 Redis)
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    1. 开启缓存
    /**
     * 1、开启基于注解的缓存 @EnableCaching
     */
    @SpringBootApplication
    @EnableCaching  //开启缓存
    public class CacheApplication{
        public static void main(String[] args) {
            SpringApplication.run(CacheApplication.class, args);
        }
    
    }
    
    1. 标注方法
    @Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
    public Employee getEmp(Integer id){
        System.out.println("查询"+id+"号员工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;
    }
    

12. HttpClient

  • 作用:发送 HTTP 请求并接收响应(服务间调用、第三方 API)

  • 官方示例Http Client 快速入门

  • 流程

    1. 创建 HttpClient
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
    
    1. 构造请求(GET/POST);
        try{
                URIBuilder builder = new URIBuilder(url);
                if(paramMap != null){
                    for (String key : paramMap.keySet()) {
                        builder.addParameter(key,paramMap.get(key));
                    }
                }
                URI uri = builder.build();	
        //创建GET请求
        HttpGet httpGet = new HttpGet(uri);
        //发送请求
        response = httpClient.execute(httpGet);
    
    1. 执行并处理响应,最后释放连接。
        //判断响应状态
                if (response.getStatusLine().getStatusCode() == 200) {
                    result = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    response.close();
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    

13. 微信小程序

内容较多,请移步小柒味来 - 微信登录与商品浏览功能开发查看流程

  • 要点:使用微信官方开发文档(登录、会话、API 权限)。
  • 登录文档:微信小程序登录流程(wx.login、后端换取session_key)等。

14. Spring Task

  • 场景:订单超时处理、定时推送等。

  • 流程

    1. 启动类启用注解@EnableScheduling

    2. 给需要定时的任务加上注解@Scheduled(cron = "0/5 * * * * ?")

  • 注意:长任务考虑异步或分布式调度,cron 表达式要谨慎测试


15. WebSocket(实时双向通信)

  • 概念:基于 TCP 的全双工通信,适合实时推送(订单通知、催单)。

  • 流程

    1. 后端建立 WebSocket 服务器端点;
    2. 前端(小程序/网页)建立连接并处理消息。

16. 阿里云 OSS

  • 用途:存储图片、视频、文件等静态资源。

  • 步骤

    1. 引入OSS SDK依赖
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
    </dependency>
    
    1. 在配置文件(YAML)写入 endpointaccessKeyIdaccessKeySecretbucketName
      alioss:
        endpoint: ${qi.alioss.endpoint}
        access-key-id: ${qi.alioss.access-key-id}
        access-key-secret: ${qi.alioss.access-key-secret}
        bucket-name: ${qi.alioss.bucket-name}
    
    1. 编写上传工具类(参考快速使用 OSS SDK - 对象存储 OSS - 阿里云)并处理文件流
    @Data
    @AllArgsConstructor
    @Slf4j
    public class AliOssUtil {
    
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    
        /**
         * 文件上传
         *
         * @param bytes
         * @param objectName
         * @return
         */
        public String upload(byte[] bytes, String objectName) {
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
            try {
                // 创建PutObject请求。
                ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
            } catch (OSSException oe) {
                System.out.println("Caught an OSSException, which means your request made it to OSS, "
                        + "but was rejected with an error response for some reason.");
                System.out.println("Error Message:" + oe.getErrorMessage());
                System.out.println("Error Code:" + oe.getErrorCode());
                System.out.println("Request ID:" + oe.getRequestId());
                System.out.println("Host ID:" + oe.getHostId());
            } catch (ClientException ce) {
                System.out.println("Caught an ClientException, which means the client encountered "
                        + "a serious internal problem while trying to communicate with OSS, "
                        + "such as not being able to access the network.");
                System.out.println("Error Message:" + ce.getMessage());
            } finally {
                if (ossClient != null) {
                    ossClient.shutdown();
                }
            }
    
            //文件访问路径规则 https://BucketName.Endpoint/ObjectName
            StringBuilder stringBuilder = new StringBuilder("https://");
            stringBuilder
                    .append(bucketName)
                    .append(".")
                    .append(endpoint)
                    .append("/")
                    .append(objectName);
    
            log.info("文件上传到:{}", stringBuilder.toString());
    
            return stringBuilder.toString();
        }
    }
    

    详细请看小柒味来 - 菜品管理模块开发中的文件上传部分


17. 事务处理

  • 概念:保持一组操作的原子性(ACID)。
  • 注解@Transactional(常用属性:rollbackForpropagation
  • 传播行为REQUIRED(默认)、REQUIRES_NEW 等,设计时注意嵌套调用的事务边界。

18. Apache ECharts

  • 用途:前端数据可视化(图表、报表)
  • 接入:前端引入 ECharts,通过接口拿数据并渲染图表。
  • 快速上手

19. Apache POI

  • 用途:生成/读取 Excel 报表

  • 快速流程

    1. new XSSFWorkbook()
    2. 导入模板文件并进行填充
    3. setCellValue();写出并关闭流。
      参考:POI 官方 QuickGuide
    @Override
        public void exportBusinessData(HttpServletResponse httpServletResponse) {
            //查询数据库,获取营业数据--查询最近30天的运营数据
            LocalDate dateBegin = LocalDate.now().minusDays(30);
            LocalDate dateEnd = LocalDate.now().minusDays(1);
    
            //查询概览数据
            BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
    
            //通过POI将数据写入到Excel中
            InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
    
            try {
                //基于模板创建一个新的Excel文件
                XSSFWorkbook excel = new XSSFWorkbook(in);
    
                //获取表格文件的Sheet页
                XSSFSheet sheet1 = excel.getSheet("Sheet1");
    
                //填充时间数据
                sheet1.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);
    
                //获取第四行
                XSSFRow row = sheet1.getRow(3);
                row.getCell(2).setCellValue(businessDataVO.getTurnover());
                row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
                row.getCell(6).setCellValue(businessDataVO.getNewUsers());
    
                //获得第五行VO
                row = sheet1.getRow(4);
                row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
                row.getCell(4).setCellValue(businessDataVO.getUnitPrice()
    
                //填充明细数据
                for (int i = 0; i < 30; i++) {
                    LocalDate date = dateBegin.plusDays(i);
                    //查询某一天的营业数据
                    BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
    
                    //获得某一行
                    row = sheet1.getRow(7 + i);
                    row.getCell(1).setCellValue(date.toString());
                    row.getCell(2).setCellValue(businessData.getTurnover());
                    row.getCell(3).setCellValue(businessData.getValidOrderCount());
                    row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
                    row.getCell(5).setCellValue(businessData.getUnitPrice());
                    row.getCell(6).setCellValue(businessData.getNewUsers());
                }
    
                //3. 通过输出流将Excel文件下载到客户端浏览器
                ServletOutputStream out = httpServletResponse.getOutputStream();
                excel.write(out);
    
                //关闭资源
                out.close();
                excel.close();
    
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

20. 排除依赖

  • 用途:避免传递依赖冲突或不需要的依赖引入。
<dependency>
  <groupId>some</groupId>
  <artifactId>lib</artifactId>
  <exclusions>
    <exclusion>
      <groupId>bad</groupId>
      <artifactId>conflict</artifactId>
    </exclusion>
  </exclusions>
</dependency>

21. @ConfigurationProperties

  • 用途:把 YAML/Properties 的配置批量注入到 Bean。
  • 优点:比 @Value 更适合批量/数组配置,支持 prefix 映射。
  • 使用@ConfigurationProperties(prefix="xxx") + @EnableConfigurationProperties@Component
@Component
@ConfigurationProperties(prefix = "qi.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

三、工作流程图

  1. 业务功能总览

四、具体业务流程图(按流程列出并留图位)

  1. 拦截器(请求进入 -> 拦截器校验 -> 放行/抛异常)
  2. 登录(前端登录 -> 后端校验 -> 返回 JWT)
  3. 公共字段填充(请求到达 -> AOP 填充公共字段如 createTime/createBy)
  4. 新增
  5. 店铺营业状态
  6. 微信小程序登录
  7. 用户端查看菜品 — Redis 缓存
  8. 用户查看套餐 — Spring Cache 注解缓存
  9. 添加菜品/套餐至购物车
  10. 用户下单
  11. 用户支付(第三方支付回调处理,幂等与订单状态更新)
  12. 订单状态定时修改 — Spring Task(超时未支付自动关闭)
  13. 来单/催单 — WebSocket
  14. 导出报表 — POI

项目中用到的大部分知识点就已经整理完了,在项目中遇到的小问题整理请看:小柒味来 - 零碎知识点学习记录


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub