目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

Online Education Website

中大型公司项目开发流程

需求评审(产品/设计/前端/后端/测试/运营)
流程图
->UI 设计
-> 开发(前端架构-> 开发/ 后端架构-> 开发)
-> 前端后端联调
-> 项目提测
->BugFix
-> 回归测试
-> 运维和开发部署上线
-> 灰度发布
-> 全量发布
-> 维护和运营

需求分析和架构设计

功能需求

  • 首页视频列表
  • 视频详情 (自己开发)
  • 微信扫码登录
  • 下单微信支付
  • 我的订单列表 (自己开发)

架构设计
架构设计

  • 前端后端分离 -> 方案:node 渲染
  • 动静分离 -> 方案:静态资源如 HTML,js 放在 cdn 或者 nginx 服务器上
  • 后端技术选择:IDEA + Springboot2.0 + redis4.0+ HttpClient + MySQL + ActiveMQ 消息队列
  • 前端技术选择:HTML5 + bootstrapt + jQuery
  • 测试要求:首页和视频详情页 qps 单机 qps 要求 2000+

数据库设计

实体对象:矩形
属性:椭圆
关系:菱形

ER图

实体列表

video
 video_order
 user
 comment
 chapter   
 episode

数据库初始化脚本

# ************************************************************
# Sequel Pro SQL dump
# Version 4499
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 119.23.28.97 (MySQL 5.7.22)
# Database: xdclass
# Generation Time: 2018-06-22 15:04:45 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table chapter
# ------------------------------------------------------------

DROP TABLE IF EXISTS `chapter`;

CREATE TABLE `chapter` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `video_id` int(11) DEFAULT NULL COMMENT '视频主键',
  `title` varchar(128) DEFAULT NULL COMMENT '章节名称',
  `ordered` int(11) DEFAULT NULL COMMENT '章节顺序',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;



# Dump of table comment
# ------------------------------------------------------------

DROP TABLE IF EXISTS `comment`;

CREATE TABLE `comment` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `content` varchar(256) DEFAULT NULL COMMENT '内容',
  `user_id` int(11) DEFAULT NULL,
  `head_img` varchar(128) DEFAULT NULL COMMENT '用户头像',
  `name` varchar(128) DEFAULT NULL COMMENT '昵称',
  `point` double(5,2) DEFAULT NULL COMMENT '评分,10分满分',
  `up` int(11) DEFAULT NULL COMMENT '点赞数',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `order_id` int(11) DEFAULT NULL COMMENT '订单id',
  `video_id` int(11) DEFAULT NULL COMMENT '视频id',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;



# Dump of table episode
# ------------------------------------------------------------

DROP TABLE IF EXISTS `episode`;

CREATE TABLE `episode` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(524) DEFAULT NULL COMMENT '集标题',
  `num` int(10) DEFAULT NULL COMMENT '第几集',
  `duration` varchar(64) DEFAULT NULL COMMENT '时长 分钟,单位',
  `cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
  `video_id` int(10) DEFAULT NULL COMMENT '视频id',
  `summary` varchar(256) DEFAULT NULL COMMENT '集概述',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `chapter_id` int(11) DEFAULT NULL COMMENT '章节主键id',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;



# Dump of table user
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `openid` varchar(128) DEFAULT NULL COMMENT '微信openid',
  `name` varchar(128) DEFAULT NULL COMMENT '昵称',
  `head_img` varchar(524) DEFAULT NULL COMMENT '头像',
  `phone` varchar(64) DEFAULT '' COMMENT '手机号',
  `sign` varchar(524) DEFAULT '全栈工程师' COMMENT '用户签名',
  `sex` tinyint(2) DEFAULT '-1' COMMENT '0表示女,1表示男',
  `city` varchar(64) DEFAULT NULL COMMENT '城市',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



# Dump of table video
# ------------------------------------------------------------

DROP TABLE IF EXISTS `video`;

CREATE TABLE `video` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(524) DEFAULT NULL COMMENT '视频标题',
  `summary` varchar(1026) DEFAULT NULL COMMENT '概述',
  `cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
  `view_num` int(10) DEFAULT '0' COMMENT '观看数',
  `price` int(11) DEFAULT NULL COMMENT '价格,分',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `online` int(5) DEFAULT '0' COMMENT '0表示未上线,1表示上线',
  `point` double(11,2) DEFAULT '8.70' COMMENT '默认8.7,最高10分',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `video` WRITE;
/*!40000 ALTER TABLE `video` DISABLE KEYS */;

INSERT INTO `video` (`id`, `title`, `summary`, `cover_img`, `view_num`, `price`, `create_time`, `online`, `point`)
VALUES
	(1,'SpringBoot+Maven整合Websocket课程','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',12,1000,NULL,0,8.70),
	(2,'2018年 6.2新版本ELK ElasticSearch ','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',43,500,NULL,0,9.70),
	(3,'JMeter接口测试入门到实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',53,123,NULL,0,8.70),
	(4,'Spring Boot2.x零基础入门到高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',23,199,NULL,0,6.20),
	(5,'亿级流量处理搜索','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',64,10,NULL,0,9.10),
	(6,'reidis消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',12,10,NULL,0,6.70),
	(7,'谷歌面试题','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',52,23,NULL,0,5.10),
	(8,'js高级前端视频','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',54,442,NULL,0,8.70),
	(9,'List消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',13,32,NULL,0,4.30);

/*!40000 ALTER TABLE `video` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table video_order
# ------------------------------------------------------------

DROP TABLE IF EXISTS `video_order`;

CREATE TABLE `video_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `openid` varchar(32) DEFAULT NULL COMMENT '用户标示',
  `out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单唯一标识',
  `state` int(11) DEFAULT NULL COMMENT '0表示未支付,1表示已支付',
  `create_time` datetime DEFAULT NULL COMMENT '订单生成时间',
  `notify_time` datetime DEFAULT NULL COMMENT '支付回调时间',
  `total_fee` int(11) DEFAULT NULL COMMENT '支付金额,单位分',
  `nickname` varchar(32) DEFAULT NULL COMMENT '微信昵称',
  `head_img` varchar(128) DEFAULT NULL COMMENT '微信头像',
  `video_id` int(11) DEFAULT NULL COMMENT '视频主键',
  `video_title` varchar(128) DEFAULT NULL COMMENT '视频名称',
  `video_img` varchar(256) DEFAULT NULL COMMENT '视频图片',
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `ip` varchar(64) DEFAULT NULL COMMENT '用户ip地址',
  `del` int(5) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `video_order` WRITE;
/*!40000 ALTER TABLE `video_order` DISABLE KEYS */;

INSERT INTO `video_order` (`id`, `openid`, `out_trade_no`, `state`, `create_time`, `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`, `video_img`, `user_id`, `ip`, `del`)
VALUES
	(1,'werwewfwe','dasfweqdqf',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小D','xxx',1,'SpringBoot视频','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',1,'192.154.2.32',0),
	(2,'3452333','gasdfdf',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小X','xxx',2,'2018年 6.2新版本ELK ElasticSearch ','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',2,'192.154.2.32',0),
	(3,'sfsd','432werew',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小C','xxx',3,'JMeter接口测试入门到实战','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',3,'192.154.2.32',0),
	(4,'werqwe','3432',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小D','xxx',2,'2018年 6.2新版本ELK ElasticSearch ','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',1,'192.154.2.32',0);

/*!40000 ALTER TABLE `video_order` ENABLE KEYS */;
UNLOCK TABLES;



/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

字段冗余
 可以有效避免关联查询时所引起的目标表的访问量较高。

热部署

项目分层分包及资源文件处理

1、基本目录结构 
 controller
 service
 	impl
 mapper
 utils
 domain
 config
 intercepter
 dto

 2、application.properties配置文件
 配置启动端口
 server.port=8082

开源工具的优缺点选择和抽象方法的建议

1、开源工具
 好处:开发方便,使用简单,使用aop方式进行分页,只需要引入相关依赖,然后PageHelper.startPage(page, size);  开启分页
 弊端:对于分库分表等情况下使用有问题,深度分页逻辑判断会复杂

 mysql资料:
 深度分页常用案例:
https://www.cnblogs.com/lpfuture/p/5772055.html
https://blog.csdn.net/li772030428/article/details/52839987
 推荐书籍:
 https://book.douban.com/subject/23008813/

2、封装的好坏
 关于抽象和不抽象的选择,比如tk这些工具,通用mapper,service,controller
 好处:
 代码量大大减少,开发新模块可以马上进行使用
 弊端:
 对应过度封装,新手等比较难理解,不能保证团队里面所有人都有对应的水平,或者有高度封装的思想,也不是过度封装

3、课程案例:
 分页采用pageHelper 
 封装通用工具类,如缓存操作等
 利于解耦,如切换缓存框架

IDEA 自带工具逆向生成 POJO

1、IDEA连接数据库
 菜单View→Tool Windows→Database打开数据库工具窗口

 2、左上角添加按钮“+”,选择数据库类型

 3、mysql主机,账户密码

 4、通过IDEA生成实体类
 选中一张表,右键--->Scripted Extensions--->选择Generate POJOS.clj或者Generate POJOS.groovy,选择需要存放的路径,完成

5、自定义Scripted Extensions ---> GO to Script Directory --->  extensions  --->  extensions --->  schema ---> Generate POJOs.groovy
自定义包名 net.xdclass.xdvideo.domain
常用类型java.util.Date

配置文件自动映射到属性和实体类配置

application.properties

#应用启动端口设置
server.port=8081
#=================================微信相关==================
# 公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=554801238f17fdsdsdd6f96b382fe548215e9

WeChatConfig

package net.xdclass.xdvideo.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

/**
 * 微信配置类
 */
@Configuration
@PropertySource(value = "classpath:application.properties")
@Data
public class WeChatConfig {

    /**
     * 公众号appid
     */
    @Value("${wxpay.appid}")
    private String appId;

    /**
     * 公众号密钥
     */
    @Value("${wxpay.appsecret}")
    private String appsecret;
}

整合 MyBatis 访问数据库和阿里巴巴数据源

引入依赖

<!-- mybatis起步依赖 -->
		 <dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>

		<!-- MySQL的JDBC驱动包 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- Druid数据源 -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.6</version>
		</dependency>

配置文件中加入数据源相关的配置
application.properties

#应用启动端口设置
server.port=8081
#=================================微信相关==================
# 公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=554801238f17fdsdsdd6f96b382fe548215e9

#=================================数据库相关================
#可以自动识别
#spring.datasource.driver-class-name =com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.201:3306/xdclass?useUnicode=true&characterEncoding=utf-8
spring.datasource.username =root
spring.datasource.password =123456
#如果不使用默认的数据源 (com.zaxxer.hikari.HikariDataSource)
spring.datasource.type =com.alibaba.druid.pool.DruidDataSource

#=================================mybatis相关================
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#增加打印sql语句,一般用于本地开发测试
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

启动类添加 Mapper 扫描

package net.xdclass.xdvideo;

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

@SpringBootApplication
@MapperScan("net.xdclass.xdvideo.mapper")
public class XdvideoApplication {
	public static void main(String[] args) {
		SpringApplication.run(XdvideoApplication.class, args);
	}
}

mapper 类(测试)

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface VideoMapper {
    @Select("select * from video")
    List<Video> findAll();
}

controller(测试)
VideoController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.mapper.VideoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class VideoController {
	@Autowired
	private VideoMapper videoMapper;
	@RequestMapping("test_db")
	public Object testDB(){
		System.out.println(weChatConfig.getAppId()); //wx5beac15ca207cdd40c
		return videoMapper.findAll();
	}
}

视频列表 CRUD

VideoMapper

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface VideoMapper {
    @Select("select * from video")
    List<Video> findAll();

    @Select("SELECT * FROM video WHERE id = #{id}")
    Video findById(int id);

    @Update("UPDATE video SET title=#{title} WHERE id =#{id}")
    int update(Video video);

    @Delete("DELETE FROM video WHERE id =#{id}")
    int delete(int id);

    @Insert("INSERT INTO `video` ( `title`, `summary`, " +
            "`cover_img`, `view_num`, `price`, `create_time`," +
            " `online`, `point`)" +
            "VALUES" +
            "(#{title}, #{summary}, #{coverImg}, #{viewNum}, #{price},#{createTime}" +
            ",#{online},#{point});")
    //保存对象,获取数据库自增id
    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
    int save(Video video);
}

VideoService

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.Video;
import java.util.List;

public interface VideoService {

    List<Video> findAll();

    Video findById(int id);

    int update(Video video);

    int delete(int id);

    int save(Video video);
}

VideoServiceImpl

package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author Howard
 */
@Service
public class VideoServiceImpl implements VideoService {
    @Autowired
    private VideoMapper videoMapper;

    @Override
    public List<Video> findAll() {
        return videoMapper.findAll();
    }

    @Override
    public Video findById(int id) {
        return videoMapper.findById(id);
    }

    @Override
    public int update(Video video) {
        return videoMapper.update(video);
    }

    @Override
    public int delete(int id) {
        return videoMapper.delete(id);
    }

    @Override
    public int save(Video video) {
        int rows = videoMapper.save(video);
        System.out.println("保存对象的id= "+video.getId());
        return rows;
    }
}

VideoController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @RequestBody 请求体映射实体类 : 需要指定http头为 content-type为application/json charset=utf-8
 */
@RestController
@RequestMapping("/api/v1/video")
public class VideoController {
    @Autowired
    private VideoService videoService;

    /**
     * 分页接口
     * @param page 当前第几页,默认是第一页
     * @param size  每页显示几条
     * @return
     */
    @GetMapping("page")
    public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
                            @RequestParam(value = "size",defaultValue = "10")int size){
        return videoService.findAll();
    }

    /**
     * 根据Id找视频
     * @param videoId
     * @return
     */
    @GetMapping("find_by_id")
    public Object findById(@RequestParam(value = "video_id",required = true)int videoId){

        return videoService.findById(videoId);
    }
}

VideoAdminController

package net.xdclass.xdvideo.controller.admin;

import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/admin/api/v1/video")
public class VideoAdminController {
    @Autowired
    private VideoService videoService;

    /**
     * 根据id删除视频
     * @param videoId
     * @return
     */
    @DeleteMapping("del_by_id")
    public Object delById(@RequestParam(value = "video_id",required = true)int videoId){

        return videoService.delete(videoId);
    }

    /**
     * 根据id更新视频
     * @param video
     * @return
     */
    @PutMapping("update_by_id")
    public Object update(@RequestBody Video video){
        return videoService.update(video);
    }

    /**
     * 保存视频对象
     * @param video
     * @return
     */
    @PostMapping("save")
    public Object save(@RequestBody Video video){
        return videoService.save(video);
    }
}

动态 SQL 语句 Mybaties SqlProvider

 只更新有值的属性,忽略空值属性。Reference

@UpdateProvider(type=VideoSqlProvider.class,method="updateVideo")  更新  
@InsertProvider   插入  
@DeleteProvider   删除  
@SelectProvider   查询

VideoProvider

package net.xdclass.xdvideo.provider;

import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.jdbc.SQL;

/**
 * video构建动态sql语句
 */
public class VideoProvider {

    /**
     * 更新video动态语句
     * @param video
     * @return
     */
    public String updateVideo(final Video video){
        return new SQL(){{

            UPDATE("video");

            //条件写法.
            if(video.getTitle()!=null){
                SET("title=#{title}");
            }
            if(video.getSummary()!=null){
                SET("summary=#{summary}");
            }
            if(video.getCoverImg()!=null){
                SET("cover_img=#{coverImg}");
            }
            if(video.getViewNum()!=null){
                SET("view_num=#{viewNum}");
            }
            if(video.getPrice()!=null){
                SET("price=#{price}");
            }
            if(video.getOnline()!=null){
                SET("online=#{online}");
            }
            if(video.getPoint()!=null){
                SET("point=#{point}");
            }
            WHERE("id=#{id}");
        }}.toString();
    }
}

修改 VideoMapper 中的 update 方法(通过 Provider 实现动态 SQL)

//@Update("UPDATE video SET title=#{title} WHERE id =#{id}")
    @UpdateProvider(type = VideoProvider.class,method = "updateVideo")
    int update(Video video);

PageHelper 分页插件使用

原理
PageHelper

引入依赖

<!-- 分页插件依赖 -->
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper</artifactId>
			<version>4.2.0</version>
		</dependency>

MyBatisConfig

package net.xdclass.xdvideo.config;

import com.github.pagehelper.PageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * mybatis分页插件配置
 */
@Configuration
public class MyBatisConfig {
    @Bean
    public PageHelper pageHelper(){
        PageHelper pageHelper = new PageHelper();
        Properties p = new Properties();

        // 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用
        p.setProperty("offsetAsPageNum","true");

        //设置为true时,使用RowBounds分页会进行count查询
        p.setProperty("rowBoundsWithCount","true");
        p.setProperty("reasonable","true");
        pageHelper.setProperties(p);
        return pageHelper;
    }
}

使用 PageHelper 进行分页

/**
     * 分页接口
     * @param page 当前第几页,默认是第一页
     * @param size  每页显示几条
     * @return
     */
    @GetMapping("page")
    public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
                            @RequestParam(value = "size",defaultValue = "10")int size){
        PageHelper.startPage(page,size);

        List<Video> list = videoService.findAll();

        PageInfo<Video> pageInfo = new PageInfo<>(list);
        Map<String,Object> data = new HashMap<>();
        data.put("total_size",pageInfo.getTotal());//总条数
        data.put("total_page",pageInfo.getPages());//总页数
        data.put("current_page",page);//当前页T
        data.put("data",pageInfo.getList());//数据
        return data;
    }

测试

//http://localhost:8081/api/v1/video/page?size=3
{
    "data": [
        {
            "id": 2,
            "title": "葵花宝典",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 43,
            "price": 500,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 9.7
        },
        {
            "id": 3,
            "title": "JMeter接口测试入门到实战",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 53,
            "price": 123,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 8.7
        },
        {
            "id": 4,
            "title": "Spring Boot2.x零基础入门到高级实战",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 23,
            "price": 199,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 6.2
        }
    ],
    "total_size": 9,
    "total_page": 3,
    "current_page": 1
}

PageInfo 属性分析

{
    "pageNum": 1,
    "pageSize": 3,
    "size": 3,
    "orderBy": null,
    "startRow": 1,
    "endRow": 3,
    "total": 9,
    "pages": 3,
    "list": [
        {
            "id": 2,
            "title": "葵花宝典",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 43,
            "price": 500,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 9.7
        },
        {
            "id": 3,
            "title": "JMeter接口测试入门到实战",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 53,
            "price": 123,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 8.7
        },
        {
            "id": 4,
            "title": "Spring Boot2.x零基础入门到高级实战",
            "summary": "这是概要",
            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
            "viewNum": 23,
            "price": 199,
            "createTime": "2019-11-09T16:00:00.000+0000",
            "online": 0,
            "point": 6.2
        }
    ],
    "prePage": 0,
    "nextPage": 2,
    "isFirstPage": true,
    "isLastPage": false,
    "hasPreviousPage": false,
    "hasNextPage": true,
    "navigatePages": 8,
    "navigatepageNums": [
        1,
        2,
        3
    ],
    "navigateFirstPage": 1,
    "navigateLastPage": 3,
    "firstPage": 1,
    "lastPage": 3
}

单机和分布式应用下登录校验

单机 Tomcat 应用登录检验

sesssion保存在浏览器和应用服务器会话之间  
 用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,  
 客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId

分布式应用中 session 共享
 真实的应用不可能单节点部署,所以就有个多节点登录 session 共享的问题需要解决

1)tomcat支持session共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐  
  
 2)使用redis存储token:  
  服务端使用UUID生成随机64位或者128位token,放入redis中,然后返回给客户端并存储在cookie中  
  用户每次访问都携带此token,服务端去redis中校验是否有此用户即可

JWT JSON wen token 通过加解密算法生成 token 校验
 JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。
 JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
 简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息

JWT 登录校验(封装通用方法)

简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息,格式如下:
 {
 id:888,
 name:'小D',
 expire:10000
 }

加密
funtion 加密(object, appsecret){
 xxxx
 return base64( token);
 }

解密
function 解密(token ,appsecret){
 xxxx
 //成功返回true,失败返回false
}

 优点:
 1)生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
 2)存储在客户端,不占用服务端的内存资源
 缺点:
  token是经过base64编码,所以客户端可以使用base64进行解码(获取明文),因此token加密前的对象不应该包含敏感信息,如用户权限,密码等。

2、JWT格式组成 头部、负载、签名
	header+payload+signature
 头部:主要是描述签名算法
 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token

3、关于jwt客户端存储
 可以存储在cookie,localstorage和sessionStorage(关闭浏览器之后就需要重新登录)里

引入依赖
```xml
		<!-- JWT引来 -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.7.0</version>
		</dependency>

JwtUtils

package net.xdclass.xdvideo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.xdclass.xdvideo.domain.User;

import java.util.Date;

/**
 * jwt工具类
 */
public class JwtUtils {

    //主题
    public static final String SUBJECT = "xdclass";

    //过期时间
    public static final long EXPIRE = 1000*60*60*24*7;  //过期时间,毫秒,一周

    //秘钥
    public static final  String APPSECRET = "xd666";

    /**
     * 生成jwt
     * @param user
     * @return
     */
    public static String geneJsonWebToken(User user){
        if(user == null || user.getId() == null || user.getName() == null
                || user.getHeadImg()==null){
            return null;
        }
        String token = Jwts.builder().setSubject(SUBJECT)
                .claim("id",user.getId())
                .claim("name",user.getName())
                .claim("img",user.getHeadImg())
                .setIssuedAt(new Date()) //发行时间
                .setExpiration(new Date(System.currentTimeMillis()+EXPIRE)) //期满时间
                .signWith(SignatureAlgorithm.HS256,APPSECRET).compact();
        return token;
    }


    /**
     * 校验token
     * @param token
     * @return
     */
    public static Claims checkJWT(String token ){
        try{
            final Claims claims =  Jwts.parser().setSigningKey(APPSECRET).
                    parseClaimsJws(token).getBody();
            return  claims;
        }catch (Exception e){ } //解密失败或者过期都会抛异常
        return null;
    }
}

测试(模拟登录认证)

package net.xdclass.xdvideo;

import io.jsonwebtoken.Claims;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.junit.Test;

public class CommonTest {

    /**
     * 加密测试,生成token
     */
    @Test
    public void testGeneJwt(){
        //已获取用户信息,查询数据库验证登录信息是否正确
        User user = new User();
        user.setId(999);
        user.setHeadImg("www.xdclass.net");
        user.setName("xd");

        String token = JwtUtils.geneJsonWebToken(user);
        System.out.println(token);
    }

    /**
     * 解密测试,解密token,验证合法性
     */
    @Test
    public void testCheck(){
        //正确token
        //String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";
        //模拟被修改过的token
        String token = "2eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";

        Claims claims = JwtUtils.checkJWT(token);
        if(claims != null){
            String name = (String)claims.get("name");
            String img = (String)claims.get("img");
            int id =(Integer) claims.get("id");
            System.out.println(name);
            System.out.println(img);
            System.out.println(id);
        }else{
            System.out.println("非法token");
        }
    }
}

登录方式的优缺点

手机号或者邮箱注册
优点

  • 企业获取了用户的基本资料信息,利于后续业务发展:推送营销类信息
  • 用户可以用个手机号或者邮箱获取对应的 app 福利:注册送优惠券
  • 反馈信息的时候方便,直接报手机号即可:账户出问题,被盗等

缺点

  • 步骤多
  • 如果站点不安全,如站点被攻击,泄漏了个人信息,如手机号,密码等
  • 少量不良企业贩卖个人信息,如手机号

OAuth2.0 一键授权登录
优点

  • 使用快捷,用户体验好,数据相对安全

缺点

  • 反馈问题麻烦,比较难知道唯一标识 openid
  • 如果是企业下面有多个应用,其中有应用不支持 Auth2.0 登录,则没法做到用户信息打通,积分不能复用等…… 如 app 接入了微信授权登录,但是网站没有,则打不通,或者授权方只提供了一种终端授权,则信息无法打通,

选择方式

  • 看企业和实际业务情况
  • 务必区分,普通密码和核心密码

微信开放平台简介

微信开放平台介绍(申请里面的网站应用需要企业资料)
微信开放平台

什么是 appid、appsecret、授权码 code

  • appid:应用的唯一 id
  • appsecret:应用的唯一密钥
  • 授权码:应用发起扫码授权(A->B 发起授权,想获取授权用户信息,那 a 必须携带授权码,才可以向 B 获取授权信息)
  • 授权回调域:xdclass.net/api/v1/wechat/callback?code=23&xxxxx

微信 Oauth2.0 交互流程

 通过 access_token 可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。
时序图简略版

第一步:请求 CODE
 扫码 URL 示例

https://open.weixin.qq.com/connect/qrconnect?appid=wx25bc3c42ddcc664f&redirect_uri=https://api.xdclass.net/pub/api/v1/wechat/user/callback1&response_type=code&scope=snsapi_login&state=https://xdclass.net/#/?login_type=pc#wechat_redirect

返回说明
 用户允许授权后,将会重定向到 redirect_uri 的网址上,并且带上 code 和 state 参数

redirect_uri?code=CODE&state=STATE

第二步:通过 code 获取 access_token
 通过 code 获取 access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明

参数是否必须说明
appid应用唯一标识,在微信开放平台提交应用审核通过后获得
secret应用密钥 AppSecret,在微信开放平台提交应用审核通过后获得
code填写第一步获取的 code 参数
grant_type填 authorization_code

返回说明
正确的返回:

{ 
"access_token":"ACCESS_TOKEN", 
"expires_in":7200, 
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID", 
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

第三步:通过 access_token 调用接口
 获取 access_token 后,进行接口调用,有以下前提:

1. access_token有效且未超时;
2. 微信用户已授权给第三方应用帐号相应接口作用域(scope)。

对于接口作用域(scope),能调用的接口有以下:

授权作用域(scope)接口接口说明
snsapi_base/sns/oauth2/access_token通过 code 换取 access_token、refresh_token 和已授权 scope
snsapi_base/sns/oauth2/refresh_token刷新或续期 access_token 使用
snsapi_base/sns/auth检查 access_token 有效性
snsapi_userinfo/sns/userinfo获取用户个人信息

获取用户个人信息(UnionID 机制)

接口说明
 此接口用于获取用户个人信息。开发者可通过 OpenID 来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的 unionid 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的。请注意,在用户修改微信头像后,旧的微信头像 URL 将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像 URL 失效后的异常情况。

请求说明

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

参数说明

参数是否必须说明
access_token调用凭证
openid普通用户的标识,对当前开发者帐号唯一
lang国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为 zh-CN

返回说明

正确的 JSON 返回结果:

{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

调用频率限制

接口名频率限制
通过 code 换取 access_token1 万/分钟
刷新 access_token5 万/分钟
获取用户基本信息5 万/分钟

URL 获取

1、增加结果工具类,JsonData; 增加 application.properties 配置
微信开放平台二维码连接
2、拼接 url

增加结果工具类
JsonData

package net.xdclass.xdvideo.domain;

import java.io.Serializable;

/**
 * 功能描述:工具类
 *
 * <p> 创建时间:May 14, 2018 7:58:06 PM </p>
 */
public class JsonData implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private Integer code; // 状态码 0 表示成功,1表示处理中,-1表示失败
	private Object data; // 数据
	private String msg;// 描述

	public JsonData() {
	}

	public JsonData(Integer code, Object data, String msg) {
		this.code = code;
		this.data = data;
		this.msg = msg;
	}

	// 成功,传入数据
	public static JsonData buildSuccess() {
		return new JsonData(0, null, null);
	}

	// 成功,传入数据
	public static JsonData buildSuccess(Object data) {
		return new JsonData(0, data, null);
	}

	// 失败,传入描述信息
	public static JsonData buildError(String msg) {
		return new JsonData(-1, null, msg);
	}

	// 失败,传入描述信息,状态码
	public static JsonData buildError(String msg, Integer code) {
		return new JsonData(code, null, msg);
	}

	// 成功,传入数据,及描述信息
	public static JsonData buildSuccess(Object data, String msg) {
		return new JsonData(0, data, msg);
	}

	// 成功,传入数据,及状态码
	public static JsonData buildSuccess(Object data, int code) {
		return new JsonData(code, data, null);
	}

	public Integer getCode() {
		return code;
	}

	public void setCode(Integer code) {
		this.code = code;
	}

	public Object getData() {
		return data;
	}

	public void setData(Object data) {
		this.data = data;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}

	@Override
	public String toString() {
		return "JsonData [code=" + code + ", data=" + data + ", msg=" + msg
				+ "]";
	}
}

测试

//http://localhost:8081/api/v1/video/test_config
{
    "code": 0,
    "data": "wx5beac15ca207cdd40c",
    "msg": null
}

application.properties

#=================================微信相关==================
#公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=5548012f33417fdsdsdd6f96b382fe548215e9

#微信开放平台配置
wxopen.appid=wx025575eac69a2d5b
wxopen.appsecret=f5b6730c592ac15b8b1a5aeb8948a9f3
#重定向url
wxopen.redirect_url=http://16webtest.ngrok.xiaomiqiu.cn

WeChatConfig


WechatController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {

    @Autowired
    private WeChatConfig weChatConfig;

    /**
     * 拼装微信扫一扫登录url
     * @return
     */
    @GetMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {

        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址

        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码

        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);

        return JsonData.buildSuccess(qrcodeUrl);
    }
}

测试(获取二维码 URI)

//http://localhost:8081/api/v1/wechat/login_url?access_page=www.xdclass.net
{
    "code": 0,
    "data": "https://open.weixin.qq.com/connect/qrconnect?appid=wx025575eac69a2d5b&redirect_uri=http://16webtest.ngrok.xiaomiqiu.cn&response_type=code&scope=snsapi_login&state=www.xdclass.net#wechat_redirect",
    "msg": null
}

//从返回的JSON中可以提取二维码URI
https://open.weixin.qq.com/connect/qrconnect?appid=wx025575eac69a2d5b&redirect_uri=http://16webtest.ngrok.xiaomiqiu.cn&response_type=code&scope=snsapi_login&state=www.xdclass.net#wechat_redirect

HttpClient 根据授权回调获取 access_token

思路整理
 第五步需要使用 HttpClient 拼装 code + appid + appsecret,向微信发起请求。

1. 用户请求第三方平台,第三方平台发起微信开放平台二维码连接(appid + callbackUrl + accessPage)
2.第三方平台收到微信返回的 JSON数据,并从中提取出data(二维码连接),返回前端,前端通过js发起新的请求,获取二维码图片。
3.微信用户扫码并且确认。
4.微信重定向到第三方平台,并且带上授权临时票据(code)
5.第三方平台通过 code + appid + appsecret 向微信发起请求,换取access_token
6.微信返回access_token

引入依赖

<!-- httpclient 相关依赖 -->
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
			<version>4.5.3</version>
		</dependency>

		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpmime</artifactId>
			<version>4.5</version>
		</dependency>

		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-logging</groupId>
			<artifactId>commons-logging</artifactId>
			<version>1.1.1</version>
		</dependency>
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpcore</artifactId>
		</dependency>

		<!-- gson工具,封装http的时候使用 -->
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.2</version>
		</dependency>

HTTP 工具类(封装 get post 请求)

package net.xdclass.xdvideo.utils;

import com.google.gson.Gson;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 封装http get post
 */
public class HttpUtils {

    private static  final Gson gson = new Gson();

    /**
     * get方法
     * @param url
     * @return
     */
    public static Map<String,Object> doGet(String url){

        Map<String,Object> map = new HashMap<>();
        CloseableHttpClient httpClient =  HttpClients.createDefault();

        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(5000) //连接超时
                .setConnectionRequestTimeout(5000)//请求超时
                .setSocketTimeout(5000)
                .setRedirectsEnabled(true)  //允许自动重定向
                .build();

        HttpGet httpGet = new HttpGet(url);
        httpGet.setConfig(requestConfig);

        try{
           HttpResponse httpResponse = httpClient.execute(httpGet);
           if(httpResponse.getStatusLine().getStatusCode() == 200){
               //将Entity转为字符串
               String jsonResult = EntityUtils.toString( httpResponse.getEntity());
               //将字符串转为 Map<String,Object>
               map = gson.fromJson(jsonResult,map.getClass());
           }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                httpClient.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return map;
    }

    /**
     * 封装post
     * @return
     */
    public static String doPost(String url, String data,int timeout){
        CloseableHttpClient httpClient =  HttpClients.createDefault();
        //超时设置

        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(timeout) //连接超时
                .setConnectionRequestTimeout(timeout)//请求超时
                .setSocketTimeout(timeout)
                .setRedirectsEnabled(true)  //允许自动重定向
                .build();


        HttpPost httpPost  = new HttpPost(url);
        httpPost.setConfig(requestConfig);
        httpPost.addHeader("Content-Type","text/html; chartset=UTF-8");

        if(data != null && data instanceof  String){ //使用字符串传参
            StringEntity stringEntity = new StringEntity(data,"UTF-8");
            httpPost.setEntity(stringEntity);
        }

        try{

            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();
            if(httpResponse.getStatusLine().getStatusCode() == 200){
                String result = EntityUtils.toString(httpEntity);
                return result;
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                httpClient.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }
}

开发授权回调接口接收微信重定向数据
 前置条件:ngrok 映射回调域名到本机

返回说明
 用户允许授权后,将会重定向到 redirect_uri 的网址上,并且带上 code 和 state 参数

redirect_uri?code=CODE&state=STATE

&emsp 若用户禁止授权,则重定向后不会带上 code 参数,仅会带上 state 参数

redirect_uri?state=STATE

WechatController 新增 Mapping /user/callback

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private UserService userService;

    /**
     * 拼装微信扫一扫登录url
     * @return
     */
    @GetMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {

        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址

        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码

        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);

        return JsonData.buildSuccess(qrcodeUrl);
    }

    /**
     * 保存微信 code ,并且拼装请求,获取access_token
     * @param code
     * @param state
     * @param response
     */
    @GetMapping("/user/callback")
    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
                                   String state, HttpServletResponse response){
        User user = userService.saveWeChatUser(code);
        if (user) {
            //生成jwt
        }
    }
}

WeChatConfig 新增

  • 开放平台获取 access_token 地址
  • 获取用户信息

返回说明
正确的返回:

{ 
"access_token":"ACCESS_TOKEN", 
"expires_in":7200, 
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID", 
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

参数说明

参数说明
access_token接口调用凭证
expires_inaccess_token 接口调用凭证超时时间,单位(秒)
refresh_token用户刷新 access_token
openid授权用户唯一标识
scope用户授权的作用域,使用逗号(,)分隔
unionid当且仅当该网站应用已获得该用户的 userinfo 授权时,才会出现该字段。

UserService

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.User;

/**
 *用户业务接口类
 */
public interface UserService {
     User saveWeChatUser(String code);
}

获取用户个人信息(UnionID 机制)
 通过 access_token 获取微信用户头像和昵称等基本信息

接口说明
 此接口用于获取用户个人信息。开发者可通过 OpenID 来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的 unionid 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的。请注意,在用户修改微信头像后,旧的微信头像 URL 将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像 URL 失效后的异常情况。

请求说明

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

参数说明

参数是否必须说明
access_token调用凭证
openid普通用户的标识,对当前开发者帐号唯一
lang国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为 zh-CN

UserServiceImpl

package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.HttpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private UserMapper userMapper;

    @Override
    public User saveWeChatUser(String code) {
        //获取token:拼装请求 OpenAppid + OpenAppsecret + code(临时票据)
        String accessTokenUrl = String.format(WeChatConfig.getOpenAccessTokenUrl(),weChatConfig.getOpenAppid(),weChatConfig.getOpenAppsecret(),code);

        //从微信回复的数据中提取: access_token + openid 
        Map<String ,Object> baseMap =  HttpUtils.doGet(accessTokenUrl);

        if(baseMap == null || baseMap.isEmpty()){ return  null; }
        String accessToken = (String)baseMap.get("access_token");
        String openId  = (String) baseMap.get("openid");

        //先查询本地数据库
        User dbUser = userMapper.findByopenid(openId);
        if(dbUser!=null) { //更新用户,直接返回
            return dbUser;
        }

        //获取用户基本信息
        String userInfoUrl = String.format(WeChatConfig.getOpenUserInfoUrl(),accessToken,openId);
            //获取access_token
        Map<String ,Object> baseUserMap =  HttpUtils.doGet(userInfoUrl);

        if(baseUserMap == null || baseUserMap.isEmpty()){ return  null; }

        String nickname = (String)baseUserMap.get("nickname");
        Double sexTemp  = (Double) baseUserMap.get("sex");
        int sex = sexTemp.intValue();
        String province = (String)baseUserMap.get("province");
        String city = (String)baseUserMap.get("city");
        String country = (String)baseUserMap.get("country");
        String headimgurl = (String)baseUserMap.get("headimgurl");
        StringBuilder sb = new StringBuilder(country).append("||").append(province).append("||").append(city);
        String finalAddress = sb.toString();
        try {
            //解决乱码
            nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");
            finalAddress = new String(finalAddress.getBytes("ISO-8859-1"), "UTF-8");

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        User user = new User();
        user.setName(nickname);
        user.setHeadImg(headimgurl);
        user.setCity(finalAddress);
        user.setOpenid(openId);
        user.setSex(sex);
        user.setCreateTime(new Date());
        userMapper.save(user);
        return user;
    }
}

UserMapper

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {


    /**
     * 根据主键id查找
     * @param userId
     * @return
     */
    @Select("select * from user where id = #{id}")
    User findByid(@Param("id") int userId);

    /**
     * 根据openid找用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User findByopenid(@Param("openid") String openid);

    /**
     * 保存用户新
     * @param user
     * @return
     */
    @Insert("INSERT INTO `user` ( `openid`, `name`, `head_img`, `phone`, `sign`, `sex`, `city`, `create_time`)" +
            "VALUES" +
            "(#{openid},#{name},#{headImg},#{phone},#{sign},#{sex},#{city},#{createTime});")
    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
    int save(User user);
}

使用 JWT 生成用户 Token 回写客户端

1、获取当前页面访问地址
2、根据 User 基本信息生成 token
3、重定向到指定页面

WechatController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private UserService userService;

    /**
     * 拼装微信扫一扫登录url
     * @return JsonData
     */
    @GetMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {

        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
        return JsonData.buildSuccess(qrcodeUrl);
    }


    /**
     * 获取用户code,保存 user,并且拼装请求,获取access_token
     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
     * 第三方服务器端,种cookie,再转发到用户当前页面地址
     * @param code
     * @param state  用户扫码登录时停留的页面
     * @param response
     */
    @GetMapping("/user/callback")
    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
                                   String state, HttpServletResponse response) throws IOException {
        User user = userService.saveWeChatUser(code);
        if (user != null){
            //生成jwt
            String token = JwtUtils.geneJsonWebToken(user);

            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
        }
    }
}

用户登录拦截器

LoginIntercepter

package net.xdclass.xdvideo.interceoter;

import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class LoginIntercepter implements HandlerInterceptor {

    private static final Gson gson = new Gson();

    /**
     * 进入controller之前进行拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("token");
        if(token == null ){
            token = request.getParameter("token");
        }

        //不为空,就进行解密
        if(token != null ) {
           Claims claims =  JwtUtils.checkJWT(token);
            if(claims !=null){
                //获取到 id 、 name
                Integer userId = (Integer)claims.get("id");
                String name = (String) claims.get("name");

                //设置后续业务可能会用到的数据:这里 只设置了  id 、 name
                request.setAttribute("user_id",userId);
                request.setAttribute("name",name);
                return true;
            }
        }

        //解密失败 或者 没有token 就需要登录
        sendJsonMessage(response,JsonData.buildError("请登录"));
        return false;
    }

    /**
     * 响应数据给前端
     * @param response
     * @param obj
     */
    public static void sendJsonMessage(HttpServletResponse response, Object obj) throws IOException {
        response.setContentType("application/json; charset=utf-8");
        PrintWriter writer = response.getWriter();

        //JsonData 对象 转换为 json 格式,回写给前端
        writer.print(gson.toJson(obj));
        writer.close();

        //刷新缓冲
        response.flushBuffer();
    }
}

IntercepterConfig

package net.xdclass.xdvideo.config;

import net.xdclass.xdvideo.interceoter.LoginIntercepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器配置
 */
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
    /**
     * 注册拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercepter()).addPathPatterns("/user/api/v1/*/**");
        WebMvcConfigurer.super.addInterceptors(registry);
    }

}

OrderController(模拟订单接口)

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单接口
 */
@RestController
@RequestMapping("/user/api/v1/order")
public class OrderController {
  
    @GetMapping("add")
    public JsonData saveOrder(){
        return JsonData.buildSuccess("下单成功");
    }
  
}

测试(PostMan)

//错误的token
http://localhost:8081/user/api/v1/order/add?token=yyyy
{
	"code":-1,
	"msg":"请登录"
}

//正确的token
http://localhost:8081/user/api/v1/order/add?token=xxxx
{
	"code":0,
	"data":"下单成功",
	"msg":null
}

第三方支付和聚合支付

什么是第三方支付
 第三方支付是指具备一定实力和信誉保障的独立机构,采用与各大银行签约的方式,通过与银行支付结算系统接口对接而促成交易双方进行交易的网络支付模式。 通俗的例子: 支付宝,微信支付,百度钱包,PayPal(主要是欧美国家)、 拉卡拉(中国最大线下便民金融服务提供商)

优点:
1、支付平台降低了政府、企业、事业单位直连银行的成本,满足了企业专注发展在线业务的收付要求。
2、使用方便。对支付者而言,他所面对的是友好的界面,不必考虑背后复杂的技术操作过程

缺点:
1、风险问题,在电子支付流程中,资金都会在第三方支付服务商处滞留即出现所谓的资金沉淀,如缺乏有效的流动性管理,则可能存在资金安全和支付的风险
2、电子支付经营资格的认知、保护和发展问题

什么是聚合支付
 聚合支付是相对之前的第三方支付而言的,作为对第三方支付平台服务的拓展,第三方支付是介于银行和商户之间的,而聚合支付是介于第三方支付和商户之间,出现的场景(解决的问题):

  • 一堆第三方支付出现,并通过大量的钱补贴线上商家使用它们的支付,导致商户收银台堆满各种
  • POS 机器,扫码设备,商户还需要去各家支付公司申请账号,结算等
  • 解决的问题:聚合支付公司提供的二维码,支付多种方式支付,不再是一种,各个公司的竞争,就是支付渠道和方式的支持

申请微信支付介绍和不同场景的支付方式

1、什么是微信商户平台:
地址:https://pay.weixin.qq.com
提供给商家使用,用于查看交易数据,提现等信息

2、常用的支付方式 公众号支付,扫码支付,app 支付,小程序支付
官方地址:https://pay.weixin.qq.com/wiki/doc/api/index.html
案例演示: https://pay.weixin.qq.com/guide/webbased_payment.shtml

3、微信支付申请流程 https://pay.weixin.qq.com/guide/qrcode_payment.shtml
1)申请公众号(服务号) 认证费 300
2)开通微信支付

微信网页扫码支付

1、扫码支付文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=2_2

2、名称理解
appid:公众号唯一标识
appsecret:公众号的秘钥
mch_id:商户号,申请微信支付的时候分配的
key:支付交易过程生成签名的秘钥,设置路径
微信商户平台(pay.weixin.qq.com)--> 账户中心--> 账户设置-->API 安全--> 密钥设置

3、和微信支付交互方式
1、post 方式提交
2、XML 格式的协议
3、签名算法 MD5
4、交互业务规则 先判断协议字段返回,再判断业务返回,最后判断交易状态
5、接口交易单位为 分
6、交易类型:JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app 支付
7、商户订单号规则:
商户支付的订单号由商户自定义生成,仅支持使用字母、数字、中划线-、下划线_、竖线 |、星号*这些英文半角字符的组合,请勿使用汉字或全角等特殊字符,微信支付要求商户订单号保持唯一性
8、安全规范:
签名算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
校验工具:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
9、采用微信支付扫码模式二(不依赖商户平台设置回调 url)

时序图

什么是时序图
 是一种 UML 交互图,描述了对象之间传递消息的时间顺序, 用来表示用例中的行为顺序, 是强调消息时间 顺序的交互图;通俗解释:就是交互流程图 (把大象装冰箱分几步)

时序图包括四个元素

  • 对象(Object):时序图中的对象在交互中扮演的角色就是对象,使用矩形将对象名称包含起来, 名称下有下划线
  • 生命线(Lifeline):生命线是一条垂直的虚线, 这条虚线表示对象的存在, 在时序图中, 每个对象都有生命线
  • 激活(Activation):代表时序图中对象执行一项操作的时期, 表示该对象被占用以完成某个任务,当对象处于激活时期, 生命线可以拓宽为矩形
  • 消息(Message):对象之间的交互是通过相互发消息来实现的,箭头上面标出消息名,一个对象可以请求(要求)另一个对象做某件事件,消息从源对象指向目标对象,消息一旦发送便将控制从源对象转移到目标对象,息的阅读顺序是严格自上而下的
  • 消息交互中的实线:请求消息
  • 消息交互中的虚线:响应返回消息
  • 自己调用自己的方法:反身消息

支付时序图讲解和统一下单接口

支付时序图
扫码时序图

统一下单接口介绍
 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按扫码、JSAPI、APP 等不同场景生成交易串调起支付。
支付状态转变如下:
统一下单接口状态转换机

接口链接
URL 地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数: Reference

微信支付订单接口开发之订单增删改查

VideoOrderMapper

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.VideoOrder;
import org.apache.ibatis.annotations.*;

import java.util.List;

/**
 * 订单dao层
 */
public interface VideoOrderMapper {

    /**
     * 保存订单,返回包含主键
     * @param videoOrder
     * @return
     */
    @Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
            " `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
            " `video_img`, `user_id`, `ip`, `del`)" +
            "VALUES" +
            "(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
            "#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    int insert(VideoOrder videoOrder);

    /**
     * 根据主键查找订单
     * @param id
     * @return
     */
    @Select("select * from video_order where id=#{order_id} and del=0")
    VideoOrder findById(@Param("order_id") int id);

    /**
     * 根据交易订单号获取订单对象
     * @param outTradeNo
     * @return
     */
    @Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
    VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);

    /**
     * 逻辑删除订单
     * @param id
     * @param userId
     * @return
     */
    @Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
    int del(@Param("id") int id, @Param("userId") int userId);

    /**
     * 查找我的全部订单
     * @param userId
     * @return
     */
    @Select("select * from video_order where user_id =#{userId}")
    List<VideoOrder> findMyOrderList(int userId);

    /**
     * 根据微信回调订单流水号更新订单状态
     * @param videoOrder
     * @return
     */
    @Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
            " where out_trade_no=#{outTradeNo} and state=0 and del=0")
    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);

}

IDE 生成订单接口单元测试和断言开发

1、 核心注解

@RunWith(SpringRunner.class)  
 @SpringBootTest

2、根据公司情况,写单元测试,核心接口一定要写,非核心的尽量写
3、断言类型,可以细化(比如 id=2)

VideoServiceTest

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.mapper.VideoMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class VideoServiceTest {
    @Autowired
    private VideoService videoService;
    @Test
    public void findAll() {
        List<Video> list = videoService.findAll();
        assertNotNull(list);
        for (Video video : list) {
            System.out.println(video.getTitle());
        }
    }

    @Test
    public void findById() {
        Video video = videoService.findById(2);
        assertNotNull(video);
    }

    @Test
    public void update() {
    }

    @Test
    public void delete() {
    }

    @Test
    public void save() {
    }
}

VideoOrderMapperTest

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.VideoOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class VideoOrderMapperTest {

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Test
    public void insert() {
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setDel(0);
        videoOrder.setTotalFee(111);
        videoOrder.setHeadImg("xxxxdfsdfs");
        videoOrder.setVideoTitle("springBoot高级视频教程");
        videoOrderMapper.insert(videoOrder);
        assertNotNull(videoOrder.getId());

    }

    @Test
    public void findById() {
        VideoOrder videoOrder = videoOrderMapper.findById(1);
        assertNotNull(videoOrder);
    }

    @Test
    public void findByOutTradeNo() {
    }

    @Test
    public void del() {
    }

    @Test
    public void findMyOrderList() {
    }

    @Test
    public void updateVideoOderByOutTradeNo() {
    }
}

封装常用工具类 CommonUtils 和 WXpayUtils

CommonUtils 包含方法 md5,uuid 等
CommonUtils

package net.xdclass.xdvideo.utils;

import java.security.MessageDigest;
import java.util.UUID;

/**
 * 常用工具类的封装,md5,uuid等
 */
public class CommonUtils {
    /**
     * 生成 uuid, 即用来标识一笔单,也用做 nonce_str
     * @return
     */
    public static String generateUUID(){
        String uuid = UUID.randomUUID().toString().
                replaceAll("-","").substring(0,32);

        return uuid;
    }

    /**
     * md5常用工具类
     * @param data
     * @return
     */
    public static String MD5(String data){
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte [] array = md5.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();

        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

微信支付工具类,XML 转 map,map 转 XML,生成签名
 从微信开发者文档获取部分代码 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
Reference

WXPayUtil

package net.xdclass.xdvideo.utils;

import org.w3c.dom.Entity;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;

/**
 * 微信支付工具类,xml转map,map转xml,生成签名
 */
public class WXPayUtil {

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
        org.w3c.dom.Document document = documentBuilder.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }

    /**
     * 生成微信支付sign
     * @return
     */
    public static String createSign(SortedMap<String, String> params, String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es =  params.entrySet();
        Iterator<Map.Entry<String,String>> it =  es.iterator();

        //生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
        while (it.hasNext()){
            Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
             String k = (String)entry.getKey();
             String v = (String)entry.getValue();
             if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
             }
        }

        sb.append("key=").append(key);
        String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
        return sign;
    }

    /**
     * 校验签名
     *
     * @param params
     * @param key
     * @return
     */
    public static boolean isCorrectSign(SortedMap<String, String> params, String key){
        String sign = createSign(params,key);
        String weixinPaySign = params.get("sign").toUpperCase();
        return weixinPaySign.equals(sign);
    }
}

微信支付 Controller 下单 API 接口开发

1、开发 controller,开发期间不加入拦截器登录校验
2、iputils 工具类介绍(用于获取下单客户端的 IP 地址)
3、加入微信支付配置

#微信商户平台
application.properties

#支付配置
#微信商户平台
wxpay.mer_id=1503808832
wxpay.key=xdclasss20182018xdclass2018x018d
wxpay.callback=16web.tunnel.qydev.com/pub/api/v1/wechat/order/callback1

WeChatConfig


IpUtils

package net.xdclass.xdvideo.utils;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.http.HttpServletRequest;

public class IpUtils {
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                                                                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        return ipAddress;
    }
}

VideoOrderDto

package net.xdclass.xdvideo.dto;

import net.xdclass.xdvideo.domain.VideoOrder;

/**
 * 订单数据传输对象
 */
public class VideoOrderDto extends VideoOrder {

}

VideoController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * 订单接口
 */
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {


    @Autowired
    private VideoOrderService videoOrderService;

    @GetMapping("add")
    public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
                              HttpServletRequest request) throws Exception {
        //获取用户的ip地址
        String ip = IpUtils.getIpAddr(request);

        //int userId = request.getAttribute("user_id");
        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
        int userId = 1;
        VideoOrderDto videoOrderDto = new VideoOrderDto();
        videoOrderDto.setUserId(userId);
        videoOrderDto.setVideoId(videoId);
        videoOrderDto.setIp(ip);

        //Dto转换保存
        videoOrderService.save(videoOrderDto);

        return JsonData.buildSuccess("下单成功");
    }

}

VideoOrderService

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;

/**
 * 订单接口
 */
public interface VideoOrderService {

    /**
     * 下单接口
     * @param videoOrderDto
     * @return
     */
    VideoOrder save(VideoOrderDto videoOrderDto);

}

微信支付下单 API 接口(保存订单)和签名开发

VideoOrderServiceImpl

package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.SortedMap;
import java.util.TreeMap;

@Service
public class VideoOrderServiceImpl implements VideoOrderService {


    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Autowired
    private UserMapper userMapper;

    @Override
    public VideoOrder save(VideoOrderDto videoOrderDto) throws Exception {
        //查找视频信息
        Video video =  videoMapper.findById(videoOrderDto.getVideoId());

        //查找用户信息
        User user = userMapper.findByid(videoOrderDto.getUserId());


        //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());

        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());

        videoOrderMapper.insert(videoOrder);

        //生成签名
        unifiedOrder(videoOrder);

        //获取codeurl


        //生成二维码



        return null;
    }


    /**
     * 统一下单方法 :生成签名
     * @return
     */
    private String unifiedOrder(VideoOrder videoOrder) throws Exception {

        //生成签名
        SortedMap<String,String> params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        params.put("nonce_str",CommonUtils.generateUUID());
        params.put("body",videoOrder.getVideoTitle());
        params.put("out_trade_no",videoOrder.getOutTradeNo());
        params.put("total_fee",videoOrder.getTotalFee().toString());
        params.put("spbill_create_ip",videoOrder.getIp());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
        params.put("sign",sign);

        //map转xml
        String payXml = WXPayUtil.mapToXml(params);

        System.out.println(payXml);
        //统一下单

        return "";
    }

}

测试

http://localhost:8081/api/v1/order/add?video_id=2
{
    "code": 0,
    "data": "下单成功",
    "msg": null
}

校验工具
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1

发送数据

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xml>
<appid>wx5beac15ca207cdd40c</appid>
<body>葵花宝典</body>
<mch_id>1503808832</mch_id>
<nonce_str>c4d160dfe45147918dbc7fbf7b5e3205</nonce_str>
<notify_url>16web.tunnel.qydev.com/pub/api/v1/wechat/order/callback1</notify_url>
<out_trade_no>a6ed4257042c429abb720a4f7cc1222c</out_trade_no>
<sign>D293F6AD0C05DDB39CE34E96FC5CECA5</sign>
<spbill_create_ip>0:0:0:0:0:0:0:1</spbill_create_ip>
<total_fee>500</total_fee>
<trade_type>NATIVE</trade_type>
</xml>

商户 key

xdclasss20182018xdclass2018x018d

验证通过说明校验过程正确

调用微信统一下单接口实战

1、配置统一下单接口
WeChatConfig

/**
     * 统一下单url
     */
    private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";

2、发送请求验证

微信统一下单响应  
 <xml><return_code><![CDATA[SUCCESS]]></return_code>  
 <return_msg><![CDATA[OK]]></return_msg>  
 <appid><![CDATA[wx5beac15ca207c40c]]></appid>  
 <mch_id><![CDATA[1503809911]]></mch_id>  
 <nonce_str><![CDATA[Go5gDC2CYL5HvizG]]></nonce_str>  
 <sign><![CDATA[BC62592B9A94F5C914FAAD93ADE7662B]]></sign>  
 <result_code><![CDATA[SUCCESS]]></result_code>  
 <prepay_id><![CDATA[wx262207318328044f75c9ebec2216783076]]></prepay_id>  
 <trade_type><![CDATA[NATIVE]]></trade_type>  
 <code_url><![CDATA[weixin://wxpay/bizpayurl?pr=hFq9fX6]]></code_url>  
 </xml>

3、获取 code_url
遇到问题,根据错误码解决:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

VideoOrderService

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;

/**
 * 订单接口
 */
public interface VideoOrderService {

    /**
     * 下单接口
     * @param videoOrderDto
     * @return
     */
    String save(VideoOrderDto videoOrderDto) throws Exception;
}

VideoOrderServiceImpl

package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

@Service
public class VideoOrderServiceImpl implements VideoOrderService {


    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Autowired
    private UserMapper userMapper;

    @Override
    public String save(VideoOrderDto videoOrderDto) throws Exception {
        //查找视频信息
        Video video =  videoMapper.findById(videoOrderDto.getVideoId());

        //查找用户信息
        User user = userMapper.findByid(videoOrderDto.getUserId());


        //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());

        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());

        videoOrderMapper.insert(videoOrder);

        //获取codeurl
        String codeUrl = unifiedOrder(videoOrder);
        return codeUrl;

    }
  
    /**
     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
     * @return
     */
    private String unifiedOrder(VideoOrder videoOrder) throws Exception {

        //生成签名
        SortedMap<String,String> params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        params.put("nonce_str",CommonUtils.generateUUID());
        params.put("body",videoOrder.getVideoTitle());
        params.put("out_trade_no",videoOrder.getOutTradeNo());
        params.put("total_fee",videoOrder.getTotalFee().toString());
        params.put("spbill_create_ip",videoOrder.getIp());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
        params.put("sign",sign);

        //map转xml
        String payXml = WXPayUtil.mapToXml(params);

        System.out.println(payXml);

        //统一下单
        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
        if(null == orderStr) {
            return null;
        }


        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
        System.out.println(unifiedOrderMap.toString());
        if(unifiedOrderMap != null) {
            //获取二维码url
            return unifiedOrderMap.get("code_url");
        }

        return "";
    }
  
}

OrderController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * 订单接口
 */
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {


    @Autowired
    private VideoOrderService videoOrderService;

    @GetMapping("add")
    public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
                              HttpServletRequest request) throws Exception {
        //获取用户的ip地址
        //String ip = IpUtils.getIpAddr(request);
        String ip = "120.25.1.11";

        //int userId = request.getAttribute("user_id");
        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
        int userId = 1;
        VideoOrderDto videoOrderDto = new VideoOrderDto();
        videoOrderDto.setUserId(userId);
        videoOrderDto.setVideoId(videoId);
        videoOrderDto.setIp(ip);

        //获取codeUrl
        String codeUrl = videoOrderService.save(videoOrderDto);
      
        //生成二维码
        return JsonData.buildSuccess("下单成功");
    }
}

谷歌二维码工具根据 code_url 生成扫一扫支付二维码

 将链接生成二维码图片(code_url)

参考

https://www.cnblogs.com/lanxiamo/p/6293580.html
https://blog.csdn.net/shenfuli/article/details/68923393
https://coolshell.cn/articles/10590.html

引入依赖

<!-- google二维码生成包 -->
		<dependency>
			<groupId>com.google.zxing</groupId>
			<artifactId>javase</artifactId>
			<version>3.3.0</version>
		</dependency>

		<dependency>
			<groupId>com.google.zxing</groupId>
			<artifactId>core</artifactId>
			<version>2.0</version>
		</dependency>

OrderController

package net.xdclass.xdvideo.controller;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 订单接口
 */
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {


    @Autowired
    private VideoOrderService videoOrderService;

    @GetMapping("add")
    public void saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
                              HttpServletRequest request,
                              HttpServletResponse response) throws Exception {
        //获取用户的ip地址
        //String ip = IpUtils.getIpAddr(request);
        String ip = "120.25.1.11";

        //int userId = request.getAttribute("user_id");
        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
        int userId = 1;
        VideoOrderDto videoOrderDto = new VideoOrderDto();
        videoOrderDto.setUserId(userId);
        videoOrderDto.setVideoId(videoId);
        videoOrderDto.setIp(ip);

        //获取codeUrl
        String codeUrl = videoOrderService.save(videoOrderDto);
        if(codeUrl == null) {
            throw new  NullPointerException();
        }

        try{
            //生成二维码的配置
            Map<EncodeHintType,Object> hints =  new HashMap<>();

            //设置纠错等级
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

            //编码类型
            hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");

            //构建矩阵图对象
            BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
            OutputStream out =  response.getOutputStream();

            //返回矩阵图给微信客户端
            MatrixToImageWriter.writeToStream(bitMatrix,"png",out);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

测试

http://localhost:8081/api/v1/order/add?video_id=2
返回  二维码

如何知道用户是否支付成功

  • 通过前段定时器轮询该用户的最新订单的状态

微信支付扫码回调开发实战

扫码时序图

Reference
回调数据如下

<xml>  
  <appid><![CDATA[wx2421b1c4370ec43b]]></appid>  
  <attach><![CDATA[支付测试]]></attach>  
  <bank_type><![CDATA[CFT]]></bank_type>  
  <fee_type><![CDATA[CNY]]></fee_type>  
  <is_subscribe><![CDATA[Y]]></is_subscribe>  
  <mch_id><![CDATA[10000100]]></mch_id>  
  <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>  
  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>  
  <out_trade_no><![CDATA[1409811653]]></out_trade_no>  
  <result_code><![CDATA[SUCCESS]]></result_code>  
  <return_code><![CDATA[SUCCESS]]></return_code>  
  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>  
  <time_end><![CDATA[20140903131540]]></time_end>  
  <total_fee>1</total_fee>  
<coupon_fee><![CDATA[10]]></coupon_fee>  
<coupon_count><![CDATA[1]]></coupon_count>  
<coupon_type><![CDATA[CASH]]></coupon_type>  
<coupon_id><![CDATA[10000]]></coupon_id>  
<coupon_fee><![CDATA[100]]></coupon_fee>  
  <trade_type><![CDATA[JSAPI]]></trade_type>  
  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>  
</xml>

使用 Ngrock 本地接收微信回调,并开发回调接口
注意点:
回调要用 post 方式,微信文档没有写回调的通知方式
可以用这个注解 @RequestMapping

application.properties

#支付配置
#微信商户平台
wxpay.mer_id=1503808832
wxpay.key=xdclasss20182018xdclass2018x018d
wxpay.callback=16web.tunnel.qydev.com/api/v1/wechat/order/callback

WechatController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.JwtUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Map;

@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private UserService userService;

    /**
     * 拼装微信扫一扫登录url
     * @return JsonData
     */
    @GetMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {

        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
        return JsonData.buildSuccess(qrcodeUrl);
    }


    /**
     * 获取用户code,保存 user,并且拼装请求,获取access_token
     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
     * 第三方服务器端,种cookie,再转发到用户当前页面地址
     * @param code
     * @param state  用户扫码登录时停留的页面
     * @param response
     */
    @GetMapping("/user/callback")
    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
                                   String state, HttpServletResponse response) throws IOException {
        User user = userService.saveWeChatUser(code);
        if (user != null){
            //生成jwt
            String token = JwtUtils.geneJsonWebToken(user);

            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
        }
    }


    /**
     * 微信支付回调: post方式   所以需要使用 @RequestMapping 
     */
    @RequestMapping("/order/callback")
    public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {

        InputStream inputStream =  request.getInputStream();

        //BufferedReader是包装设计模式,性能更搞
        BufferedReader in =  new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
        StringBuffer sb = new StringBuffer();
        String line ;
        while ((line = in.readLine()) != null){
            sb.append(line);
        }
        in.close();
        inputStream.close();
        Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
      

        System.out.println(callbackMap.toString());
    }

}

微信回调处理之更新订单状态和幂等性讲解

 微信回调通知规则(通知频率为 15/15/30/180/1800/1800/1800/1800/3600,单位:秒)

 幂等性: 同样的参数和值,不管调用你的接口多少次,响应结果都和调用一次是一样的

1、校验签名是否正确,防止伪造回调
2、查询订单是否已经更新
3、若没更新则更新订单状态
4、回应微信,SUCCESS 或者 FAIL
response.setContentType("text/xml");
response.getWriter().println("success");

WXPayUtil 添加方法(获取有序 map)

/**
     * 获取有序map
     * @param map
     * @return
     */
    public static SortedMap<String,String> getSortedMap(Map<String,String> map){

        SortedMap<String, String> sortedMap = new TreeMap<>();
        Iterator<String> it =  map.keySet().iterator();
        while (it.hasNext()){
            String key  = (String)it.next();
            String value = map.get(key);
            String temp = "";
            if( null != value){
                temp = value.trim();
            }
            sortedMap.put(key,temp);
        }
        return sortedMap;
    }

VideoOrderService

package net.xdclass.xdvideo.service;

import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;

/**
 * 订单接口
 */
public interface VideoOrderService {

    /**
     * 下单接口
     * @param videoOrderDto
     * @return
     */
    String save(VideoOrderDto videoOrderDto) throws Exception;

    /**
     * 根据流水号查找订单
     * @param outTradeNo
     * @return
     */
    VideoOrder findByOutTradeNo(String outTradeNo);


    /**
     * 根据流水号更新订单
     * @param videoOrder
     * @return
     */
    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
}
package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

@Service
public class VideoOrderServiceImpl implements VideoOrderService {


    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Autowired
    private UserMapper userMapper;

    @Override
    public String save(VideoOrderDto videoOrderDto) throws Exception {
        //查找视频信息
        Video video =  videoMapper.findById(videoOrderDto.getVideoId());

        //查找用户信息
        User user = userMapper.findByid(videoOrderDto.getUserId());


        //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());

        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());

        videoOrderMapper.insert(videoOrder);

        //获取codeurl
        String codeUrl = unifiedOrder(videoOrder);
        return codeUrl;

    }

    @Override
    public VideoOrder findByOutTradeNo(String outTradeNo) {

        return videoOrderMapper.findByOutTradeNo(outTradeNo);
    }

    @Override
    public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
        return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
    }

    /**
     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
     * @return
     */
    private String unifiedOrder(VideoOrder videoOrder) throws Exception {

        //生成签名
        SortedMap<String,String> params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        params.put("nonce_str",CommonUtils.generateUUID());
        params.put("body",videoOrder.getVideoTitle());
        params.put("out_trade_no",videoOrder.getOutTradeNo());
        params.put("total_fee",videoOrder.getTotalFee().toString());
        params.put("spbill_create_ip",videoOrder.getIp());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
        params.put("sign",sign);

        //map转xml
        String payXml = WXPayUtil.mapToXml(params);

        System.out.println(payXml);

        //统一下单
        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
        if(null == orderStr) {
            return null;
        }


        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
        System.out.println(unifiedOrderMap.toString());
        if(unifiedOrderMap != null) {
            //获取二维码url
            return unifiedOrderMap.get("code_url");
        }

        return "";
    }
}

VideoOrderMapper

package net.xdclass.xdvideo.mapper;

import net.xdclass.xdvideo.domain.VideoOrder;
import org.apache.ibatis.annotations.*;

import java.util.List;

/**
 * 订单dao层
 */
public interface VideoOrderMapper {

    /**
     * 保存订单,返回包含主键
     * @param videoOrder
     * @return
     */
    @Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
            " `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
            " `video_img`, `user_id`, `ip`, `del`)" +
            "VALUES" +
            "(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
            "#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    int insert(VideoOrder videoOrder);

    /**
     * 根据主键查找订单
     * @param id
     * @return
     */
    @Select("select * from video_order where id=#{order_id} and del=0")
    VideoOrder findById(@Param("order_id") int id);

    /**
     * 根据交易订单号获取订单对象
     * @param outTradeNo
     * @return
     */
    @Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
    VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);

    /**
     * 逻辑删除订单
     * @param id
     * @param userId
     * @return
     */
    @Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
    int del(@Param("id") int id, @Param("userId") int userId);

    /**
     * 查找我的全部订单
     * @param userId
     * @return
     */
    @Select("select * from video_order where user_id =#{userId}")
    List<VideoOrder> findMyOrderList(int userId);

    /**
     * 根据微信回调订单流水号更新订单状态
     * @param videoOrder
     * @return
     */
    @Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
            " where out_trade_no=#{outTradeNo} and state=0 and del=0")
    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);

}

WechatController

package net.xdclass.xdvideo.controller;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.JwtUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;

@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private UserService userService;

    @Autowired
    private VideoOrderService videoOrderService;

    /**
     * 拼装微信扫一扫登录url
     * @return JsonData
     */
    @GetMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {

        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
        return JsonData.buildSuccess(qrcodeUrl);
    }


    /**
     * 获取用户code,保存 user,并且拼装请求,获取access_token
     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
     * 第三方服务器端,种cookie,再转发到用户当前页面地址
     * @param code
     * @param state  用户扫码登录时停留的页面
     * @param response
     */
    @GetMapping("/user/callback")
    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
                                   String state, HttpServletResponse response) throws IOException {
        User user = userService.saveWeChatUser(code);
        if (user != null){
            //生成jwt
            String token = JwtUtils.geneJsonWebToken(user);

            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
        }
    }


    /**
     * 微信支付回调: post方式   所以需要使用 @RequestMapping
     */
    @RequestMapping("/order/callback")
    public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {

        InputStream inputStream =  request.getInputStream();

        //BufferedReader是包装设计模式,性能更搞
        BufferedReader in =  new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
        StringBuffer sb = new StringBuffer();
        String line ;
        while ((line = in.readLine()) != null){
            sb.append(line);
        }
        in.close();
        inputStream.close();
        Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
        //返回给前端,告诉用户支付完成
        System.out.println(callbackMap.toString());

        SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);

        //判断签名是否正确
        if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
            //验证业务码状态是否为 SUCCESS
            if("SUCCESS".equals(sortedMap.get("result_code"))){
                //拿到流水号
                String outTradeNo = sortedMap.get("out_trade_no");

                //根据流水号查找订单
                VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);

                if(dbVideoOrder != null && dbVideoOrder.getState()==0){  //判断逻辑看业务场景
                    VideoOrder videoOrder = new VideoOrder();
                    videoOrder.setOpenid(sortedMap.get("openid"));
                    videoOrder.setOutTradeNo(outTradeNo);
                    videoOrder.setNotifyTime(new Date());
                    videoOrder.setState(1);
                    int rows = videoOrderService.updateVideoOderByOutTradeNo(videoOrder);

                    //通知微信订单处理成功
                    if(rows == 1){
                        response.setContentType("text/xml");
                        response.getWriter().println("success");
                        return;
                    }
                }
            }
        }
        //都处理失败
        response.setContentType("text/xml");
        response.getWriter().println("fail");
    }
}

测试

1、扫码支付
2、数据库 video_order  中的 订单状态 state = 1

微信支付之下单事务处理

Reference
1、SpringBoot 开启事务,启动类里面增加 @EnableTransactionManagement
2、需要事务的方法上加 @Transactional(propagation = Propagation.REQUIRED) 隔离级别采用默认即可。
VideoOrderServiceImpl

@Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(VideoOrderDto videoOrderDto) throws Exception {  ……  }

3、aop 的管理事务的好处和选择: 增,删,改 开启事务 (有的方法只操作一个数据,只有一条语句,加事务会影响性能,2 条以上的修改操作才会加事务)

全局异常处理

XdException

package net.xdclass.xdvideo.exception;


/**
 * 自定义异常类
 */
public class XdException extends RuntimeException {


    /**
     * 状态码
     */
    private Integer code;
    /**
     * 异常消息
     */
    private String msg;

    public XdException(int code, String msg){
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

XdExceptionHandler

package net.xdclass.xdvideo.exception;


import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 异常处理控制器
 */
@ControllerAdvice
public class XdExceptionHandler {

    /**
     * 这里可以捕获Exception  然后使用instanceof 配合多分支 if 进行 细力度异常的使用
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public JsonData Handler(Exception e){

        if(e instanceof XdException){
            XdException xdException =  (XdException)e;
            return JsonData.buildError(xdException.getMsg(),xdException.getCode());
        }else{ 
            return JsonData.buildError("全局异常,未知错误");
        }
    }

}

使用 Logback 整合 SpringBoot 打点日志

 Logback 是 SpringBoot 默认的日志系统。
1、日志不仅用于排查问题,查看应用运行情况
2、更可以用于统计,虽然统计可以通过数据库进行统计,但是存在风险,如果用日志,并且建立一个日志系统用于分析,这样就方便产品和运营人员进行查看分析数据
3、写日志,可以加缓冲 buffer,也可也进行异步
参考资料:https://blog.csdn.net/zhuyucheng123/article/details/21524549

logback-spring.xml


项目中日志路径(启动项目自动创建如下目录和文件)

app_log/log  
 app.info.2020-02-19.log  
 app.err.2020-02-19.log  
 app.data.2020-02-19.log

service 层埋点
VideoOrderServiceImpl

package net.xdclass.xdvideo.service.impl;

import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

@Service
public class VideoOrderServiceImpl implements VideoOrderService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private Logger dataLogger = LoggerFactory.getLogger("dataLogger");

    @Autowired
    private WeChatConfig weChatConfig;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private VideoOrderMapper videoOrderMapper;

    @Autowired
    private UserMapper userMapper;

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(VideoOrderDto videoOrderDto) throws Exception {
        //打点
        dataLogger.info("module=video_order`api=save`user_id={}`video_id={}",videoOrderDto.getUserId(),videoOrderDto.getVideoId());

        //查找视频信息
        Video video =  videoMapper.findById(videoOrderDto.getVideoId());

        //查找用户信息
        User user = userMapper.findByid(videoOrderDto.getUserId());


        //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());

        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());

        videoOrderMapper.insert(videoOrder);

        //获取codeurl
        String codeUrl = unifiedOrder(videoOrder);
        return codeUrl;

    }

    @Override
    public VideoOrder findByOutTradeNo(String outTradeNo) {

        return videoOrderMapper.findByOutTradeNo(outTradeNo);
    }

    @Override
    public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
        return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
    }

    /**
     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
     * @return
     */
    private String unifiedOrder(VideoOrder videoOrder) throws Exception {

        //生成签名
        SortedMap<String,String> params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        params.put("nonce_str",CommonUtils.generateUUID());
        params.put("body",videoOrder.getVideoTitle());
        params.put("out_trade_no",videoOrder.getOutTradeNo());
        params.put("total_fee",videoOrder.getTotalFee().toString());
        params.put("spbill_create_ip",videoOrder.getIp());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
        params.put("sign",sign);

        //map转xml
        String payXml = WXPayUtil.mapToXml(params);

        System.out.println(payXml);

        //统一下单
        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
        if(null == orderStr) {
            return null;
        }


        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
        System.out.println(unifiedOrderMap.toString());
        if(unifiedOrderMap != null) {
            //获取二维码url
            return unifiedOrderMap.get("code_url");
        }

        return "";
    }

}

测试

http://localhost:8081/api/v1/order/add?video_id=2

查看日志文件 app.data.2020-02-19.log 内容

2020-02-19 14:08:09.022`module=video_order`api=save`user_id=1`video_id=2`

跨域和对应的处理方法

1、跨域:浏览器同源策略
1995 年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。
最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"

协议相同 http https
域名相同 www.xdcass.net
端口相同 80 81

一句话:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域

浏览器控制台跨域提示:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

2、解决方法

1)JSONP
2)Http 响应头配置允许跨域

nginx 层配置(第一种方式)
https://www.cnblogs.com/hawk-whu/p/6725699.html

程序代码中处理(第二种方式)
SpringBoot 自带配置

Cors 注意点:假如接口逻辑报错,则跨域配置不生效,可以考虑做全局异常处理,这样即使接口运行出错,跨域也依然生效

package net.xdclass.xdvideo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class Cors extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")指定为线上的静态资源的域
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")
                .allowCredentials(true).maxAge(3600);
    }

}

作者:Soulboy