Bootstrap

项目实战

导语:
现在可以开始项目实战开发了,关于项目搭建就是普通的springboot项目搭建,数据库需要注意的问题就是字段属性要设置正确,比如金额就要用BigDecimal而字段值就要用32、64、128的倍数设置更好。

项目目录:

项目目录
还是按照传统的三层结构:Controller、Service、Dao(repository)编写代码,我们这里按照从底往上的顺序进行编写并以Order类的操作为例,首先开发Dao层

Dao层

我们这里采用的是JPA的方式操作数据库

/**
 * 查找订单详情
 * Created by KHM
 * 2017/7/27 9:59
 */
public interface OrderDetailRepository extends JpaRepository<OrderDetail, String>{

    List<OrderDetail> findByOrderId(String orderId);
}

Service层接口和Impl

/**
 * 订单service层
 * Created by KHM
 * 2017/7/27 11:04
 */
public interface OrderService {

    //创建订单
    OrderDTO create(OrderDTO orderDTO);

    //查询单个订单详情
    OrderDTO findOne(String orderId);

    //查询订单总列表(买家用)
    Page<OrderDTO> findList(String buyerOpenid, Pageable pageable);

    //取消订单
    OrderDTO cancel(OrderDTO orderDTO);

    //完结订单
    OrderDTO finish(OrderDTO orderDTO);

    //支付订单
    OrderDTO paid(OrderDTO orderDTO);

    //查询订单列表(卖家管理系统用的)
    Page<OrderDTO> findList(Pageable pageable);

}
/**
 * Created by KHM
 * 2017/7/27 11:28
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderDetailRepository orderDetailRepository;

    @Autowired
    private OrderMasterRepository orderMasterRepository;

    @Autowired
    private PayService payService;

    @Autowired
    private WebSocket webSocket;

    @Override
    @Transactional//事务管理,一旦失败就回滚
    public OrderDTO create(OrderDTO orderDTO) {
        //设置下订单id(是个随机,这里调用了根据时间产生6位随机数的方法)
        String orderId = KeyUtil.genUniqueKey();
        //给总价赋值
        BigDecimal orderAmount = new BigDecimal(BigInteger.ZERO);

        //List<CartDTO> cartDTOList = new ArrayList<>();

        //1.查询商品(数量,价格)
        for (OrderDetail orderDetail : orderDTO.getOrderDetailList()){
            ProductInfo productInfo = productService.findOne(orderDetail.getProductId());
                if(productInfo == null){
                    throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
                }
            //2.计算总价=单价*数量+orderAmount
            orderAmount = productInfo.getProductPrice()
                    .multiply(new BigDecimal(orderDetail.getProductQuantity()))
                    .add(orderAmount);

            //3.订单详情入库(OrderMaster和orderDetail)
            //利用BeanUtils方法把前端查找出来的productInfo商品信息复制给订单详情
            BeanUtils.copyProperties(productInfo, orderDetail);//先复制,再赋值
            orderDetail.setDetailId(KeyUtil.genUniqueKey());
            orderDetail.setOrderId(orderId);

            orderDetailRepository.save(orderDetail);

           /* CartDTO cartDTO = new CartDTO(orderDetail.getProductId(), orderDetail.getProductQuantity());
            cartDTOList.add(cartDTO);*/
        }

        //3.订单总表入库(OrderMaster和orderDetail)
        OrderMaster orderMaster = new OrderMaster();
        orderDTO.setOrderId(orderId);
        BeanUtils.copyProperties(orderDTO, orderMaster);
        orderMaster.setOrderAmount(orderAmount);//是一个整个订单的总价,所以在foe循环之外设置
        orderMaster.setOrderStatus(OrderStatusEnum.New.getCode());
        orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
        orderMasterRepository.save(orderMaster);

        //4.扣库存
        List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream().map(e ->
                new CartDTO(e.getProductId(), e.getProductQuantity())
        ).collect(Collectors.toList());
        productService.decreaseStock(cartDTOList);

        //发送websocket消息
        webSocket.sendMessage(orderDTO.getOrderId());

        return orderDTO;
    }
这里需要注意的问题主要是Impl层逻辑的编写,我们需要用到的注解有:
  • @Service
  • @Slf4j日志需要
  • @Transactional//事务管理,一旦失败就回滚(主要用在要对数据库进行操作的方法上)
  • 然后是一些技巧:善于使用工具类做开发,当需要例如随机数或者特殊字段的时候最好声明在方法开始,方便之后的调用
  • 多多关注java的新特性,对代码的优化很有帮助
  • 最重要的,写一个方法前先对这个方法的逻辑做个列举,第一步是什么、第二步是什么,再去编写具体的代码

Controller层

/**
 * 购买操作
 * Created by KHM
 * 2017/7/30 16:48
 */
@RestController
@RequestMapping("/buyer/order")
@Slf4j
public class BuyerOrderController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private BuyerService buyerService;

    //创建订单
    @PostMapping(value = "/create")
    public ResultVO<Map<String, String>> creat(@Valid OrderForm orderForm,
                                               BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            log.error("【创建订单】 参数不正确, orderForm={}", orderForm);
            throw new SellException(ResultEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
        }

        OrderDTO orderDTO = OrderFormZOrderDTOConverter.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<>();
        map.put("orderId", createResult.getOrderId());
        return ResultVOUtil.success(map);
    }

    //订单列表
    @GetMapping(value = "/list")
    public ResultVO<List<OrderDTO>> list(@RequestParam("openid") String openid,
                                         @RequestParam(value = "page", defaultValue = "0") Integer page,
                                         @RequestParam(value = "size", defaultValue = "10") Integer size){
        if(StringUtils.isNullOrEmpty(openid)){
            log.error("【查询订单列表】 openid为空");
            throw new SellException(ResultEnum.PARAM_ERROR);
        }

        PageRequest request = new PageRequest(page, size);
        Page<OrderDTO> orderDTOPage = orderService.findList(openid, request);
        //只用返回当前页面的数据集合就行了,因为前端传过来的就是第几页和每一页的size(一般都会定好)
        return ResultVOUtil.success(orderDTOPage.getContent());
    }

    //订单详情
    @GetMapping("/detail")
    public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid,
                                     @RequestParam("orderId") String orderId){
       /* //TODO 不安全的做法,改进
        OrderDTO orderDTO = orderService.findOne(orderId);*/
       OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId);

        return ResultVOUtil.success(orderDTO);
    }

    //取消订单
    @PostMapping("/cancel")
    public ResultVO cancel(@RequestParam("openid") String openid,
                           @RequestParam("orderId") String orderId){
       /* //TODO 不安全的做法,改进
        OrderDTO orderDTO =  orderService.findOne(orderId);
        orderService.cancle(orderDTO);*/
        buyerService.cancelOrder(openid, orderId);
        return ResultVOUtil.success();
    }

}
Controller需要注意的问题是:
  • 请求方式一定要写清楚Post和Get不要弄错了
  • 需要前端做表单验证的时候就要用@Valid+前端表单类
  • 做好几乎所有的出现null或者错误的if判断去抛出异常
  • 返回要用包装类VO去返回

关于Exception、几种包装类、工具类的介绍

一般情况下有两种注册异常类的方法:
/**
 * Created by KHM
 * 2017/7/27 17:34
 */
@Getter
public class SellException extends RuntimeException {

    private Integer code;

    public SellException(ResultEnum resultEnum) {
        //把枚举中自己定义的message传到父类的构造方法里,相当于覆盖message
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }

    //而这个是需要自己去填写code的新的meg,不一定是枚举中的模糊的说法,可以把具体的错误信息信使出来
    public SellException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}
善用工具类,反正只要是能单独拆分出来使代码看上去更优雅的代码都可以单独写出来当做工具类

工具类

    包装类:但凡是需要把原dataobject(数据库对应的实体类)组合或者拆分的新类我们都用包装类来代替,可大致
分为以下几种包装类:
  • 返回给前端的VO对象,主要按照前端API开发
/**
 * 需要返回的商品详情
 * Created by KHM
 * 2017/7/26 17:54
 */
@Data
public class ProductInfoVO implements Serializable {

    private static final long serialVersionUID = -3013889380494680036L;

    //为了防止多个name造成混淆,所以要细起名,但为了和返回对象名一致,所以用这个注解
    //其实也不是造成混淆,主要原因还是为了和原productId对象中属性名一致并且为了和前端API一致,才要在这里起别名,让他在返回时实例化成别的名字
    @JsonProperty("id")
    private String productId;

    @JsonProperty("name")
    private String productName;

    @JsonProperty("price")
    private BigDecimal productPrice;

    @JsonProperty("description")
    private String productDescription;

    @JsonProperty("icon")
    private String productIcon;

}
  • 前端和后端都包装的DTO对象,主要也是按照前端API开发,但是这个和VO的区别是DTO包含了整个原始对象,而VO缩减了一些属性
/**
 * DTO类用来关联dataobject中有联系的类,比如创建订单就需要订单总表和订单详情表两种数据,
 * 所以就需要一种包含了这两种实体的包装类把他们联系起来
 * 因为用dataobject来关联的话会破坏映射的数据库的关系
 * Created by KHM
 * 2017/7/27 11:10
 */
@Data
//@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
//@JsonInclude(JsonInclude.Include.NON_NULL)//可以让为null属性不返回
public class OrderDTO {

    //订单id
    //@Id,不需要此注解了,因为这不是关联数据库的类
    private String orderId;

    //买家姓名
    private String buyerName;

    //买家手机号
    private String buyerPhone;

    //买家地址
    private String buyerAddress;

    //买家微信openid
    private String buyerOpenid;

    //订单总金额
    private BigDecimal orderAmount;

    //订单状态,默认为0新下单
    private Integer orderStatus;

    //支付状态,默认为0未支付
    private Integer payStatus;

    //创建时间
    @JsonSerialize(using = Date2LongSerializer.class)
    private Date createTime;

    //更新时间
    @JsonSerialize(using = Date2LongSerializer.class)
    private Date updateTime;

    //@Transient//为了方便关联订单总表和详情表,把此字段加在这.用此注解就可以让程序在与数据库关联时忽略此字段,但是更规范的写法就是创建新的DTO
    private List<OrderDetail> orderDetailList; //= new ArrayList<>();(配置中配置了如果为null就不返回)

    @JsonIgnore//在返回json的时候回忽略这个属性
    public OrderStatusEnum getOrderStatusEnum() {
        return EnumUtil.getByCode(orderStatus, OrderStatusEnum.class);
    }

    @JsonIgnore
    public PayStatusEnum getPayStatusEnum() {
        return EnumUtil.getByCode(payStatus, PayStatusEnum.class);
    }


#配置了这个就不会返回为NULL的参数
  jackson:
    default-property-inclusion: non_null
}
总结:这些常规的后台开发最主要要的问题在于逻辑,开发每一个方法前要先想清楚这个方法的步奏,怎样写最方便,什么
时候该用到工具类,返回时要不要用包装对象,时刻在可能出现错误的地方打上日志和抛出异常,单元测试一定要贯穿三层
,Dao和Service层用Slf4j,Controller用Postman。这样才能保证开发不会出问题找不到。

微信支付篇:http://blog.csdn.net/qq_31783173/article/details/77618374

关于FreeMarker+ibootstrap+ModelAndView

    假如你是个后端人员又需要自己写后台管理界面的时候,这套组合可谓是快速开发的神器了,用ibootstrap完成html
代码的实现,放在FreeMarker中,不用AJAX即可完成数据的交互,详情请查看FreeMarker:?

用分布式Session完成用户信息的登录判断

什么是分布式系统:
    旨在支持应用程序和服务的开发,可以利用物理架构
由多个自治的处理元素,不共享主内存,但通过网络发送消息合作。
                                  --Leslie Lamport
三个特点和三个概念

三个特点和三个概念

Session
  • 广义的session:会话控制,不是普通的Http的Session
  • 可以理解为一种Key-value的机制
  • 它的关键点在于怎么设置Key和获取对应的value
  • 第一种:SessionId客户端在请求服务端的时候,服务端会在Http的Header里面设置key和value,而客户端的cookie会把这个保存,后续的请求里面会自动的带上
  • 第二种:token,我们需要手动在Http的Header或Url里设置token这个字段,服务器获得请求后在从Url或者Header里取出token进行验证,安全要求比较严格的时候需要配合签名一起使用
  • 共同点:区局唯一
分布式系统中的session问题
  • 当我们使用分布式系统运行时,会有多台服务器,怎么放呢?有两种方式
  • 水平扩展:就是在多台服务器上部署一样的程序,就是集群
  • 垂直扩展:其实就是拆分服务,不同Url负载均衡到不同的服务器上去
  • 然后,当用户进行登录时,第一次可能在A服务器上,第二次可能就跑到B服务器上了,B服务器没有用户的Session,就以为没有登录,IPHash的解决方案还是优缺点不适用,真正的解决方案是什么呢?
  • 加一台服务器装上Redis来专门保存用户的session信息,当其他服务器需要session信息的时候都去找他要

我们知道常规的登录、登录就是验证信息,存储浏览状态和让浏览状态失效,我们这里使用的第二种:token的方式,自己设置一个token字段,然后手动添加到cookie中,还有失效时间;登出的时候先清除redis的token,之后我们在访问其他页面的时候就可以通过cookie和redis的验证了,但这里似乎没有做关闭浏览器清除session的设置,具体代码实现:

/**
 * 卖家用户登录管理
 * Created by Akk_Mac
 * Date: 2017/8/30 上午9:31
 */
@Controller
@RequestMapping("/seller")
public class SellerUserController {

    @Autowired
    private SellerService sellerService;

    //redis的service,这里主要用stringredis
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ProjectUrlConfig projectUrlConfig;

    @RequestMapping("/login")
    public ModelAndView login(@RequestParam(value = "username", required=false ) String username,
                              @RequestParam(value = "password", required = false) String password,
                              HttpServletResponse response,
                      Map<String, Object> map) {
        //1. 由于我们这里没有申请微信开放平台,所以就不用扫码登录了
        if(username == null && password == null){
            return new ModelAndView("common/login");
        }

        SellerInfo sellerInfo = sellerService.findSellerInfoByUsername(username);
        if(sellerInfo == null && !sellerInfo.getPassword().equals(password)) {
            map.put("msg", ResultEnum.LGOIN_FAIL.getMessage());
            //map.put("url", "/sell/seller/order/list");
            return new ModelAndView("common/login", map);
        }
        //2. 设置token至redis(用什么UUID设置)
        String token = UUID.randomUUID().toString();
        Integer expire = RedisConstant.EXPIRE;//token过期时间
        //(key:token_ 为开头的格式String.format是格式设置方法, value=这里先设置为username, 过期时间, 时间单位)
        redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), username, expire, TimeUnit.SECONDS);

        //3. 设置token至cookie
        CookieUtil.set(response, CookieConstant.TOKEN, token, expire);

        //这里不是跳转到模板而是地址所以要用redirect,而且跳转最好用绝对地址
        return new ModelAndView("redirect:" + projectUrlConfig.getSell()  + "/sell/seller/order/list");

    }

    @RequestMapping("/logout")
    public ModelAndView logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Map<String, Object> map) {
        //1. 从cookie中查询
        Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
        if(cookie != null) {
            //2. 清除redis
            redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));

            //3. 清除cookie
            CookieUtil.set(response, CookieConstant.TOKEN, null, 0);
        }
        map.put("msg", ResultEnum.LOGOUT_SUCCESS.getMessage());
        map.put("url", "/sell/seller/login");
        return new ModelAndView("common/success", map);
    }
}

AOP切面编程

/**
 * AOP切面编程验证登录
 * Created by Akk_Mac
 * Date: 2017/8/30 上午11:05
 */
@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //拦截除了登录登出之外的操作,这是设置拦截范围
    @Pointcut("execution(public * com.akk.controller.Seller*.*(..))" +
            "&& !execution(public * com.akk.controller.SellerUserController.*(..))")
    public void verify(){}

    @Before("verify()")
    public void doVerify() {

        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

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

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

项目部署

    原本SpringBoot的部署是最好把项目打包成war包放在tomcat上,但我发现,我这里用打war的方法,要
去掉spring-boot-starter-web包中的内置tomcat容器,这样会使websocket类出现问题,检测不到javax
包,试了几次没有办法,就用了另一种方法,打成Jar包的形式,先用控制台运行的方式在服务器运行,这样是有
点隐患的,但这个冲突还没有解决。打jar包详情见:http://blog.csdn.net/xiao__gui/article/details/47341385
;