基于springboot企业微信点餐系统项目总结

心得体会

最近通过慕课网学习了《基于springboot企业微信点餐系统项目》,干货满满,既有springboot知识的学习,又有业务逻辑的处理体会。更多的是接触了企业级开发过程中各种编程心得和技巧,能够很大程度的帮助我们规范开发的流程,使得我们的代码更加优雅和实现可维护性。

git地址:https://gitee.com/jiayuan1234/sell

基于springboot

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置,也就是人们常说的“约定大于配置”。

使用体会

基于springboot进行开发,最直观的感受就是,它使得我们能在很短的时间内快速搭建一个项目环境,并且能够方便的集成各种优秀组件,通过application.yml文件进行统一配置和管理。同时它也是基于maven构建,以传统的ssm框架为基础,帮助我们更好的开发spring应用程序,很好的解决了先前“配置地狱”的苦恼,使得我们能够更加专注于业务逻辑的开发,提高效率。

项目准备

jdk:1.8.0_65

mysql:5.7.1

maven:3.3.9

redis: 3.0.6

springboot-version: 1.5.4.RELEASE

IDE:  Intellij IDEA 2017.2,navicat 12

本次学习是跟随慕课网上《基于springboot企业微信点餐系统》进行的,以Spring Boot和微信特性为核心技术栈,实现一个从下单到接单流程完整,包含买家端和卖家端前后台功能的微信点餐系统,一步步设计并开发一个中小型企业级Java应用

项目结构

通过IDEA创建springboot项目,Spring Initializr帮我们初始化了项目目录,主要包括main(java,resources),test。如下是到com.imooc这一层级,再以下均为自定义

maven依赖

spring-boot-starter-*起步依赖是SpringBoot核心之处,它提供了Spring和相关技术提供一站式服务,让开发者不在关心Spring相关配置,简化了传统的依赖注入操作。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.imooc</groupId>
	<artifactId>sell</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>sell</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

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

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

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

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

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
		</dependency>

		<dependency>
			<groupId>com.github.binarywang</groupId>
			<artifactId>weixin-java-mp</artifactId>
			<version>3.1.0</version>
		</dependency>

		<dependency>
			<groupId>cn.springboot</groupId>
			<artifactId>best-pay-sdk</artifactId>
			<version>1.2.0</version>
		</dependency>

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

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

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

		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.2.0</version>
		</dependency>

	</dependencies>

	<build>
		<finalName>sell</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

application.yml配置

替代传统的application.properties,配置和使用更为简洁

注意yml文件空格递进表示的层级配置,key-value格式,注意value值要在key值冒号后面空格一位,在IDE中可通过颜色高亮看出配置文件是否有误

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/sell?characterEncoding=utf-8&useSSL=false
  jackson:
    default-property-inclusion: non_null
  redis:
    host: ***.**.**.***
    port: 6379
server:
  context-path: /sell
wechat:
  mpAppId: wx777f4f5e7f1ccef7
  mpAppSecret: ebb4f15df7e750a88777b8b9dd88f86c
  openAppId: x6ad144e54af67d87
  openAppSecret: 91a2dd6s38a2bbccfb7e9
  mchId: 1409146202
  mchKey: c976503d34ca432c601361f969fd8d85
  keyPath: /var/weixin_cert/h5.p12
  notifyUrl: http://sell.natapcc/sell/pay/notify
  templateId:
    orderStatus: e-Cqq67QxD6YNI41iRqawEYdFavW_7pc7LyEMb-yeQ

projectUrl:
  wechatMpAuthorize: http://sell.natapp4.cc
  wechatOpenAuthorize: http://sell.natapp4.cc
  sell: http://localhost:8080

logging:
  level:
    com.imooc.dataobject.mapper: trace
mybatis:
  mapper-locations: classpath:mapper/*.xml

SpringbootApplication一键部署和启动

在 SpringbootApplication 文件中右键 Run as -> Java Application。当看到 “Tomcat started on port(s): 8080 (http)” 字样说明启动成功。

基础注解为@SpringBootApplication,用于将该类标识和注解为启动类

当使用注解式sql语句编写时,需要在此加入注解扫描来指定mapper文件,也可以直接在application.yml文件中配置mybatis.mapper-locations:classpath:mapper/*.xml. 

@EnableCaching增加对缓存的支持

package com.imooc;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.dataobject.mapper")
@EnableCaching
public class SellApplication {
	public static void main(String[] args) {
		SpringApplication.run(SellApplication.class, args);
	}
}

使用logback日志

在resources文件夹下创建logback-spring.xml文件,并在application.yml文件中加入配置(如日志打印级别)

日志级别包括:TRACE<DEBUG<INFO<WARN<ERROR,其中常用的是DEBUG、INFO和ERROR。如果logger没有被分配级别,name它将从有被分配级别的最近的父类那里继承级别,root logger默认级别是DEBUG。级别在高于或等于其logger的有效级别时为启用,否则为禁用。只有在请求级别大于有效级别时,该请求才会被执行,并打印或输出日志

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>
                %d - %msg%n
            </pattern>
        </layout>
    </appender>

    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <encoder>
            <pattern>
                %msg%n
            </pattern>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路径 /var/log/tomcat/sell/info.%d.log-->
            <fileNamePattern>/D://MyData//workspace//IDEA//sell-log//info.%d.log</fileNamePattern>
        </rollingPolicy>
    </appender>


    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>
                %msg%n
            </pattern>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路径  /var/log/tomcat/sell/error.%d.log -->
            <fileNamePattern>/D://MyData//workspace//IDEA//sell-log//error.%d.log</fileNamePattern>
        </rollingPolicy>
    </appender>

    <root level="info">
        <appender-ref ref="consoleLog" />
        <appender-ref ref="fileInfoLog" />
        <appender-ref ref="fileErrorLog" />
    </root>

</configuration>

使用lombok简化bean开发

在项目中使用Lombok可以减少很多重复代码的书写。比如说getter/setter/toString等方法的编写。

打开IDEA的Setting –> 选择Plugins选项 –> 选择Browse repositories –> 搜索lombok –> 点击安装 –> 安装完成重启IDEA –> 安装成功。该插件还可以解决IntelliJ 注解@Slf4j后找不到log问题

在pom.xml文件中添加Lombok依赖

常用注解有 @Getter,@Setter,@Data

@Data
@Entity
public class SellerInfo {

    @Id
    private String sellerId;

    private String username;

    private String password;

    private String openid;
}


使用JavaBean读取配置文件

通过@Data注解可以获得该bean中的属性值

通过@ConfigurationProperties(prefix="xxx")可以指定application.yml文件中以xxx开头的配置项,并将其配置值注入到相应的变量中,实现对配置文件的读取

通过@Component 将普通pojo实例化到spring容器中,相当于配置文件中的<bean id="" class=""/>

@Data
@ConfigurationProperties(prefix = "projectUrl")
@Component
public class ProjectUrlConfig {

    /**
     * 微信公众平台授权url
     */
    public String wechatMpAuthorize;

    /**
     * 微信开放平台授权url
     */
    public String wechatOpenAuthorize;

    /**
     * 点餐系统
     */
    public String sell;
}

自定义异常 

通过继承RuntimeException 定义一个异常类,提供构造方法完成code和message的初始化

加入@Getter注解,对外提供异常信息的获取

@Getter
public class SellException extends RuntimeException {

    private Integer code;

    public SellException(ResultEnum resultEnum){
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }

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

全局异常处理

在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。该注解很多时候是应用在全局异常处理中,类似本案例这种处理方式。

通过 @ExceptionHandler(value= xxxException.class)实现对异常的捕获,并获取异常的详细信息和错误码,处理后返回

通过@ResponseStatus(HttpStatus.FORBIDDEN)  设置响应状态码,具体状态码参考HttpStatus枚举类

@ControllerAdvice
public class SellExceptionHandler {

    @Autowired
    private ProjectUrlConfig projectUrlConfig;

    @ExceptionHandler(value= SellerAuthorizeException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)  //设置响应状态码
    public ModelAndView handlerAuthorizeException(){
        return new ModelAndView("redirect:"
                .concat(projectUrlConfig.getWechatOpenAuthorize())
                .concat("/sell/wechat/qrAuthorize")
                .concat("?returnUrl=")
                .concat(projectUrlConfig.getSell())
                .concat("/sell/seller/login"));
    }

    @ExceptionHandler(value= SellException.class)
    @ResponseBody
    public ResultVO handerSellerException(SellException e){
        return ResultVOUtil.error(e.getCode(),e.getMessage());
    }
}

设定系统常量

在开发中,我们会需要使用到一些常量并且共享,为了保持常量值统一不变和避免魔幻数字的使用,需要将此常量值进行统一维护。在这里采用接口类进行常量的定义和初始化。

接口中的常量默认用public static final修饰,符合我们的需求。

public interface CookieConstant {

    String TOKEN = "token";

    Integer EXPIRE = 7200;
}

枚举的使用

通过定义枚举类可以帮助我们对项目中一些状态值或是错误码进行统一管理,并且可以通过Getter方法获取枚举值的详细信息。

@Getter
public enum ResultEnum {

    SUCCESS(0, "成功"),

    PARAM_ERROR(1, "参数不正确"),

    PRODUCT_NOT_EXIST(10, "商品不存在"),

    PRODUCT_STOCK_ERROR(11, "商品库存不正确"),

    ORDER_NOT_EXIST(12, "订单不存在"),

    ORDERDETAIL_NOT_EXIST(13, "订单详情不存在"),

    ORDER_STATUS_ERROR(14, "订单状态不正确"),

    ORDER_UPDATE_FAIL(15, "订单更新失败"),

    ORDER_DETAIL_EMPTY(16, "订单详情为空"),

    ORDER_PAY_STATUS_ERROR(17, "订单支付状态不正确"),

    CART_EMPTY(18, "购物车为空"),

    ORDER_OWNER_ERROR(19, "该订单不属于当前用户"),

    WECHAT_MP_ERROR(20, "微信公众账号方面错误"),

    WXPAY_NOTIFY_MONEY_VERIFY_ERROR(21, "微信支付异步通知金额校验不通过"),

    ORDER_CANCEL_SUCCESS(22, "订单取消成功"),

    ORDER_FINISH_SUCCESS(23, "订单完结成功"),

    PRODUCT_STATUS_ERROR(24, "商品状态不正确"),

    LOGIN_FAIL(25, "登录失败, 登录信息不正确"),

    LOGOUT_SUCCESS(26, "登出成功"),
    ;

    private Integer code;

    private String message;

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

Aspect切面

AOP是spring框架中的一个重要内容。AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待。

使用@Aspect声明一个切面类。

使用注解完成切点表达式、环绕通知、前置通知、后置通知的声明。

通过切点表达式,实现对访问请求的拦截,并进行切面中逻辑的处理,这里进行的是登录校验

@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Pointcut("execution(public * com.imooc.controller.Seller*.*(..))" +
            "&& !execution(public * com.imooc.controller.SellerUserController.*(..))")
    public void verify() {}

    @Before("verify()")
    public void doVerify() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        //查询cookie
        Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
        if (cookie == null) {
            log.warn("【登录校验】Cookie中查不到token");
            throw new SellerAuthorizeException();
        }

        //去redis里查询
        String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
        if (StringUtils.isEmpty(tokenValue)) {
            log.warn("【登录校验】Redis中查不到token");
            throw new SellerAuthorizeException();
        }
    }
}

实体的定义

@Entity声明该类为实体类,对应数据库的表,反映数据库表和Java实体间的映射关系

@DynamicUpdate 表示update对象的时候,生成动态的update语句。如果字段有更新,Hibernate才会对该字段进行更新,比如下面的updateTime字段。参考 https://blog.csdn.net/itguangit/article/details/78696767

@JsonIgnore  作用是在将ResponseBody中的Javabean返回前端过程中,被springmvc自带的jackson转化成json字符串时,忽略这个属性。对序列化也有影响。

@Entity
@Data
@DynamicUpdate
public class ProductInfo implements Serializable{

    private static final long serialVersionUID = 6399186181668983148L;

    @Id
    private String productId;

    /** 名字. */
    private String productName;

    /** 单价. */
    private BigDecimal productPrice;

    /** 库存. */
    private Integer productStock;

    /** 描述. */
    private String productDescription;

    /** 小图. */
    private String productIcon;

    /** 状态, 0正常1下架. */
    private Integer productStatus = ProductStatusEnum.UP.getCode();

    /** 类目编号. */
    private Integer categoryType;

    private Date createTime;

    private Date updateTime;

    @JsonIgnore
    public ProductStatusEnum getProductStatusEnum() {
        return EnumUtil.getByCode(productStatus, ProductStatusEnum.class);
    }
}

dao层

这里通过继承JpaRepository实现对数据库的curd操作

public interface SellerInfoRepository extends JpaRepository<SellerInfo, String> {
    SellerInfo findByOpenid(String openid);
}

Service层

Service业务处理层接口定义

public interface SellerService {

    /**
     * 通过openid查询卖家端信息
     * @param openid
     * @return
     */
    SellerInfo findByOpenid(String openid);
}

Service业务处理层接口实现,注意加入@Service注解

@Service
public class SellerServiceImpl implements SellerService{

    @Autowired
    private SellerInfoRepository repository;

    @Override
    public SellerInfo findByOpenid(String openid) {
        return repository.findByOpenid(openid);
    }
}

Controller层

@RestController注解相当于@ResponseBody + @Controller合在一起的作用。

@RequestMapping("/path") 表示访问的请求映射路径

@Valid 用于对表单的校验,如果校验不通过,BindingResult对象不为空,并可通过bindingResult.getFieldError().getDefaultMessage()方法获取错误信息

@RestController
@RequestMapping("/buyer/order")
@Slf4j
public class BuyerOrderController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private BuyerService buyerService;

    //创建订单
    @PostMapping("/create")
    public ResultVO<Map<String,String>> create(@Valid OrderForm orderForm,
                                               BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            log.error("【创建订单】参数不正确,orderForm={}",orderForm);
            throw new SellException(ResultEnum.PARAM_ERROR.getCode(),bindingResult.getFieldError().getDefaultMessage());
        }
        OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
        if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
            log.error("【创建订单】购物车不能为空");
            throw new SellException(ResultEnum.CART_EMPTY);
        }
        OrderDTO createResult = orderService.create(orderDTO);
        Map<String,String> map = new HashMap<String,String>();
        map.put("orderId",createResult.getOrderId());
        return ResultVOUtil.success(map);
    }
}

表单校验

通过注解完成对表单填写信息的约束,并在不符合约束时将message信息返回

@Data
public class OrderForm {
    /**
     * 买家姓名
     */
    @NotEmpty(message = "姓名必填")
    private String name;

    /**
     * 买家手机号
     */
    @NotEmpty(message = "手机号必填")
    private String phone;

    /**
     * 买家地址
     */
    @NotEmpty(message = "地址必填")
    private String address;

    /**
     * 买家微信openid
     */
    @NotEmpty(message = "openId必填")
    private String openid;

    /**
     * 购物车
     */
    @NotEmpty(message = "购物车不能为空")
    private String items;
}

dto,util,converter的使用后续补充~

猜你喜欢

转载自blog.csdn.net/zhang_jiayuan/article/details/81866325