目录

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 图

实体列表

1video
2 video_order
3 user
4 comment
5 chapter   
6 episode

数据库初始化脚本

  1# ************************************************************
  2# Sequel Pro SQL dump
  3# Version 4499
  4#
  5# http://www.sequelpro.com/
  6# https://github.com/sequelpro/sequelpro
  7#
  8# Host: 119.23.28.97 (MySQL 5.7.22)
  9# Database: xdclass
 10# Generation Time: 2018-06-22 15:04:45 +0000
 11# ************************************************************
 12
 13
 14/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 15/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
 16/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
 17/*!40101 SET NAMES utf8 */;
 18/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
 19/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
 20/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
 21
 22
 23# Dump of table chapter
 24# ------------------------------------------------------------
 25
 26DROP TABLE IF EXISTS `chapter`;
 27
 28CREATE TABLE `chapter` (
 29  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 30  `video_id` int(11) DEFAULT NULL COMMENT '视频主键',
 31  `title` varchar(128) DEFAULT NULL COMMENT '章节名称',
 32  `ordered` int(11) DEFAULT NULL COMMENT '章节顺序',
 33  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
 34  PRIMARY KEY (`id`)
 35) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 36
 37
 38
 39# Dump of table comment
 40# ------------------------------------------------------------
 41
 42DROP TABLE IF EXISTS `comment`;
 43
 44CREATE TABLE `comment` (
 45  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 46  `content` varchar(256) DEFAULT NULL COMMENT '内容',
 47  `user_id` int(11) DEFAULT NULL,
 48  `head_img` varchar(128) DEFAULT NULL COMMENT '用户头像',
 49  `name` varchar(128) DEFAULT NULL COMMENT '昵称',
 50  `point` double(5,2) DEFAULT NULL COMMENT '评分,10分满分',
 51  `up` int(11) DEFAULT NULL COMMENT '点赞数',
 52  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
 53  `order_id` int(11) DEFAULT NULL COMMENT '订单id',
 54  `video_id` int(11) DEFAULT NULL COMMENT '视频id',
 55  PRIMARY KEY (`id`)
 56) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 57
 58
 59
 60# Dump of table episode
 61# ------------------------------------------------------------
 62
 63DROP TABLE IF EXISTS `episode`;
 64
 65CREATE TABLE `episode` (
 66  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 67  `title` varchar(524) DEFAULT NULL COMMENT '集标题',
 68  `num` int(10) DEFAULT NULL COMMENT '第几集',
 69  `duration` varchar(64) DEFAULT NULL COMMENT '时长 分钟,单位',
 70  `cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
 71  `video_id` int(10) DEFAULT NULL COMMENT '视频id',
 72  `summary` varchar(256) DEFAULT NULL COMMENT '集概述',
 73  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
 74  `chapter_id` int(11) DEFAULT NULL COMMENT '章节主键id',
 75  PRIMARY KEY (`id`)
 76) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 77
 78
 79
 80# Dump of table user
 81# ------------------------------------------------------------
 82
 83DROP TABLE IF EXISTS `user`;
 84
 85CREATE TABLE `user` (
 86  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 87  `openid` varchar(128) DEFAULT NULL COMMENT '微信openid',
 88  `name` varchar(128) DEFAULT NULL COMMENT '昵称',
 89  `head_img` varchar(524) DEFAULT NULL COMMENT '头像',
 90  `phone` varchar(64) DEFAULT '' COMMENT '手机号',
 91  `sign` varchar(524) DEFAULT '全栈工程师' COMMENT '用户签名',
 92  `sex` tinyint(2) DEFAULT '-1' COMMENT '0表示女,1表示男',
 93  `city` varchar(64) DEFAULT NULL COMMENT '城市',
 94  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 95  PRIMARY KEY (`id`)
 96) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 97
 98
 99
100# Dump of table video
101# ------------------------------------------------------------
102
103DROP TABLE IF EXISTS `video`;
104
105CREATE TABLE `video` (
106  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
107  `title` varchar(524) DEFAULT NULL COMMENT '视频标题',
108  `summary` varchar(1026) DEFAULT NULL COMMENT '概述',
109  `cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
110  `view_num` int(10) DEFAULT '0' COMMENT '观看数',
111  `price` int(11) DEFAULT NULL COMMENT '价格,分',
112  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
113  `online` int(5) DEFAULT '0' COMMENT '0表示未上线,1表示上线',
114  `point` double(11,2) DEFAULT '8.70' COMMENT '默认8.7,最高10分',
115  PRIMARY KEY (`id`)
116) ENGINE=InnoDB DEFAULT CHARSET=utf8;
117
118LOCK TABLES `video` WRITE;
119/*!40000 ALTER TABLE `video` DISABLE KEYS */;
120
121INSERT INTO `video` (`id`, `title`, `summary`, `cover_img`, `view_num`, `price`, `create_time`, `online`, `point`)
122VALUES
123	(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),
124	(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),
125	(3,'JMeter接口测试入门到实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',53,123,NULL,0,8.70),
126	(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),
127	(5,'亿级流量处理搜索','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',64,10,NULL,0,9.10),
128	(6,'reidis消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',12,10,NULL,0,6.70),
129	(7,'谷歌面试题','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',52,23,NULL,0,5.10),
130	(8,'js高级前端视频','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',54,442,NULL,0,8.70),
131	(9,'List消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',13,32,NULL,0,4.30);
132
133/*!40000 ALTER TABLE `video` ENABLE KEYS */;
134UNLOCK TABLES;
135
136
137# Dump of table video_order
138# ------------------------------------------------------------
139
140DROP TABLE IF EXISTS `video_order`;
141
142CREATE TABLE `video_order` (
143  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
144  `openid` varchar(32) DEFAULT NULL COMMENT '用户标示',
145  `out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单唯一标识',
146  `state` int(11) DEFAULT NULL COMMENT '0表示未支付,1表示已支付',
147  `create_time` datetime DEFAULT NULL COMMENT '订单生成时间',
148  `notify_time` datetime DEFAULT NULL COMMENT '支付回调时间',
149  `total_fee` int(11) DEFAULT NULL COMMENT '支付金额,单位分',
150  `nickname` varchar(32) DEFAULT NULL COMMENT '微信昵称',
151  `head_img` varchar(128) DEFAULT NULL COMMENT '微信头像',
152  `video_id` int(11) DEFAULT NULL COMMENT '视频主键',
153  `video_title` varchar(128) DEFAULT NULL COMMENT '视频名称',
154  `video_img` varchar(256) DEFAULT NULL COMMENT '视频图片',
155  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
156  `ip` varchar(64) DEFAULT NULL COMMENT '用户ip地址',
157  `del` int(5) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
158  PRIMARY KEY (`id`)
159) ENGINE=InnoDB DEFAULT CHARSET=utf8;
160
161LOCK TABLES `video_order` WRITE;
162/*!40000 ALTER TABLE `video_order` DISABLE KEYS */;
163
164INSERT 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`)
165VALUES
166	(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),
167	(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),
168	(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),
169	(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);
170
171/*!40000 ALTER TABLE `video_order` ENABLE KEYS */;
172UNLOCK TABLES;
173
174
175
176/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
177/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
178/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
179/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
180/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
181/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

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

热部署

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

 11、基本目录结构 
 2 controller
 3 service
 4 	impl
 5 mapper
 6 utils
 7 domain
 8 config
 9 intercepter
10 dto
11
12 2、application.properties配置文件
13 配置启动端口
14 server.port=8082

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

 11、开源工具
 2 好处:开发方便,使用简单,使用aop方式进行分页,只需要引入相关依赖,然后PageHelper.startPage(page, size);  开启分页
 3 弊端:对于分库分表等情况下使用有问题,深度分页逻辑判断会复杂
 4
 5 mysql资料:
 6 深度分页常用案例:
 7https://www.cnblogs.com/lpfuture/p/5772055.html
 8https://blog.csdn.net/li772030428/article/details/52839987
 9 推荐书籍:
10 https://book.douban.com/subject/23008813/
11
122、封装的好坏
13 关于抽象和不抽象的选择,比如tk这些工具,通用mapper,service,controller
14 好处:
15 代码量大大减少,开发新模块可以马上进行使用
16 弊端:
17 对应过度封装,新手等比较难理解,不能保证团队里面所有人都有对应的水平,或者有高度封装的思想,也不是过度封装
18
193、课程案例:
20 分页采用pageHelper 
21 封装通用工具类,如缓存操作等
22 利于解耦,如切换缓存框架

IDEA 自带工具逆向生成 POJO

 11、IDEA连接数据库
 2 菜单View→Tool Windows→Database打开数据库工具窗口
 3
 4 2、左上角添加按钮“+”,选择数据库类型
 5
 6 3、mysql主机,账户密码
 7
 8 4、通过IDEA生成实体类
 9 选中一张表,右键--->Scripted Extensions--->选择Generate POJOS.clj或者Generate POJOS.groovy,选择需要存放的路径,完成
10
115、自定义Scripted Extensions ---> GO to Script Directory --->  extensions  --->  extensions --->  schema ---> Generate POJOs.groovy
12自定义包名 net.xdclass.xdvideo.domain
13常用类型java.util.Date

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

application.properties

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

WeChatConfig

 1package net.xdclass.xdvideo.config;
 2
 3import lombok.Data;
 4import org.springframework.beans.factory.annotation.Value;
 5import org.springframework.context.annotation.Configuration;
 6import org.springframework.context.annotation.PropertySource;
 7
 8/**
 9 * 微信配置类
10 */
11@Configuration
12@PropertySource(value = "classpath:application.properties")
13@Data
14public class WeChatConfig {
15
16    /**
17     * 公众号appid
18     */
19    @Value("${wxpay.appid}")
20    private String appId;
21
22    /**
23     * 公众号密钥
24     */
25    @Value("${wxpay.appsecret}")
26    private String appsecret;
27}

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

引入依赖

 1<!-- mybatis起步依赖 -->
 2		 <dependency>
 3			<groupId>org.mybatis.spring.boot</groupId>
 4			<artifactId>mybatis-spring-boot-starter</artifactId>
 5			<version>1.3.2</version>
 6		</dependency>
 7
 8		<!-- MySQL的JDBC驱动包 -->
 9		<dependency>
10			<groupId>mysql</groupId>
11			<artifactId>mysql-connector-java</artifactId>
12			<scope>runtime</scope>
13		</dependency>
14
15		<!-- Druid数据源 -->
16		<dependency>
17			<groupId>com.alibaba</groupId>
18			<artifactId>druid</artifactId>
19			<version>1.1.6</version>
20		</dependency>

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

 1#应用启动端口设置
 2server.port=8081
 3#=================================微信相关==================
 4# 公众号
 5wxpay.appid=wx5beac15ca207cdd40c
 6wxpay.appsecret=554801238f17fdsdsdd6f96b382fe548215e9
 7
 8#=================================数据库相关================
 9#可以自动识别
10#spring.datasource.driver-class-name =com.mysql.jdbc.Driver
11spring.datasource.url=jdbc:mysql://192.168.31.201:3306/xdclass?useUnicode=true&characterEncoding=utf-8
12spring.datasource.username =root
13spring.datasource.password =123456
14#如果不使用默认的数据源 (com.zaxxer.hikari.HikariDataSource)
15spring.datasource.type =com.alibaba.druid.pool.DruidDataSource
16
17#=================================mybatis相关================
18# mybatis 下划线转驼峰配置,两者都可以
19#mybatis.configuration.mapUnderscoreToCamelCase=true
20mybatis.configuration.map-underscore-to-camel-case=true
21#增加打印sql语句,一般用于本地开发测试
22mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

启动类添加 Mapper 扫描

 1package net.xdclass.xdvideo;
 2
 3import org.mybatis.spring.annotation.MapperScan;
 4import org.springframework.boot.SpringApplication;
 5import org.springframework.boot.autoconfigure.SpringBootApplication;
 6
 7@SpringBootApplication
 8@MapperScan("net.xdclass.xdvideo.mapper")
 9public class XdvideoApplication {
10	public static void main(String[] args) {
11		SpringApplication.run(XdvideoApplication.class, args);
12	}
13}

mapper 类(测试)

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import org.apache.ibatis.annotations.Select;
 5
 6import java.util.List;
 7
 8public interface VideoMapper {
 9    @Select("select * from video")
10    List<Video> findAll();
11}

controller(测试)
VideoController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.mapper.VideoMapper;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.web.bind.annotation.RequestMapping;
 7import org.springframework.web.bind.annotation.RestController;
 8
 9@RestController
10public class VideoController {
11	@Autowired
12	private VideoMapper videoMapper;
13	@RequestMapping("test_db")
14	public Object testDB(){
15		System.out.println(weChatConfig.getAppId()); //wx5beac15ca207cdd40c
16		return videoMapper.findAll();
17	}
18}

视频列表 CRUD

VideoMapper

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import org.apache.ibatis.annotations.*;
 5
 6import java.util.List;
 7
 8public interface VideoMapper {
 9    @Select("select * from video")
10    List<Video> findAll();
11
12    @Select("SELECT * FROM video WHERE id = #{id}")
13    Video findById(int id);
14
15    @Update("UPDATE video SET title=#{title} WHERE id =#{id}")
16    int update(Video video);
17
18    @Delete("DELETE FROM video WHERE id =#{id}")
19    int delete(int id);
20
21    @Insert("INSERT INTO `video` ( `title`, `summary`, " +
22            "`cover_img`, `view_num`, `price`, `create_time`," +
23            " `online`, `point`)" +
24            "VALUES" +
25            "(#{title}, #{summary}, #{coverImg}, #{viewNum}, #{price},#{createTime}" +
26            ",#{online},#{point});")
27    //保存对象,获取数据库自增id
28    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
29    int save(Video video);
30}

VideoService

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import java.util.List;
 5
 6public interface VideoService {
 7
 8    List<Video> findAll();
 9
10    Video findById(int id);
11
12    int update(Video video);
13
14    int delete(int id);
15
16    int save(Video video);
17}

VideoServiceImpl

 1package net.xdclass.xdvideo.service.impl;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import net.xdclass.xdvideo.mapper.VideoMapper;
 5import net.xdclass.xdvideo.service.VideoService;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.stereotype.Service;
 8
 9import java.util.List;
10
11/**
12 * @author Howard
13 */
14@Service
15public class VideoServiceImpl implements VideoService {
16    @Autowired
17    private VideoMapper videoMapper;
18
19    @Override
20    public List<Video> findAll() {
21        return videoMapper.findAll();
22    }
23
24    @Override
25    public Video findById(int id) {
26        return videoMapper.findById(id);
27    }
28
29    @Override
30    public int update(Video video) {
31        return videoMapper.update(video);
32    }
33
34    @Override
35    public int delete(int id) {
36        return videoMapper.delete(id);
37    }
38
39    @Override
40    public int save(Video video) {
41        int rows = videoMapper.save(video);
42        System.out.println("保存对象的id= "+video.getId());
43        return rows;
44    }
45}

VideoController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import net.xdclass.xdvideo.service.VideoService;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.web.bind.annotation.*;
 7
 8/**
 9 * @RequestBody 请求体映射实体类 : 需要指定http头为 content-type为application/json charset=utf-8
10 */
11@RestController
12@RequestMapping("/api/v1/video")
13public class VideoController {
14    @Autowired
15    private VideoService videoService;
16
17    /**
18     * 分页接口
19     * @param page 当前第几页,默认是第一页
20     * @param size  每页显示几条
21     * @return
22     */
23    @GetMapping("page")
24    public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
25                            @RequestParam(value = "size",defaultValue = "10")int size){
26        return videoService.findAll();
27    }
28
29    /**
30     * 根据Id找视频
31     * @param videoId
32     * @return
33     */
34    @GetMapping("find_by_id")
35    public Object findById(@RequestParam(value = "video_id",required = true)int videoId){
36
37        return videoService.findById(videoId);
38    }
39}

VideoAdminController

 1package net.xdclass.xdvideo.controller.admin;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import net.xdclass.xdvideo.service.VideoService;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.web.bind.annotation.*;
 7
 8@RestController
 9@RequestMapping("/admin/api/v1/video")
10public class VideoAdminController {
11    @Autowired
12    private VideoService videoService;
13
14    /**
15     * 根据id删除视频
16     * @param videoId
17     * @return
18     */
19    @DeleteMapping("del_by_id")
20    public Object delById(@RequestParam(value = "video_id",required = true)int videoId){
21
22        return videoService.delete(videoId);
23    }
24
25    /**
26     * 根据id更新视频
27     * @param video
28     * @return
29     */
30    @PutMapping("update_by_id")
31    public Object update(@RequestBody Video video){
32        return videoService.update(video);
33    }
34
35    /**
36     * 保存视频对象
37     * @param video
38     * @return
39     */
40    @PostMapping("save")
41    public Object save(@RequestBody Video video){
42        return videoService.save(video);
43    }
44}

动态 SQL 语句 Mybaties SqlProvider

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

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

VideoProvider

 1package net.xdclass.xdvideo.provider;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import org.apache.ibatis.jdbc.SQL;
 5
 6/**
 7 * video构建动态sql语句
 8 */
 9public class VideoProvider {
10
11    /**
12     * 更新video动态语句
13     * @param video
14     * @return
15     */
16    public String updateVideo(final Video video){
17        return new SQL(){{
18
19            UPDATE("video");
20
21            //条件写法.
22            if(video.getTitle()!=null){
23                SET("title=#{title}");
24            }
25            if(video.getSummary()!=null){
26                SET("summary=#{summary}");
27            }
28            if(video.getCoverImg()!=null){
29                SET("cover_img=#{coverImg}");
30            }
31            if(video.getViewNum()!=null){
32                SET("view_num=#{viewNum}");
33            }
34            if(video.getPrice()!=null){
35                SET("price=#{price}");
36            }
37            if(video.getOnline()!=null){
38                SET("online=#{online}");
39            }
40            if(video.getPoint()!=null){
41                SET("point=#{point}");
42            }
43            WHERE("id=#{id}");
44        }}.toString();
45    }
46}

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

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

PageHelper 分页插件使用

原理
PageHelper

引入依赖

1<!-- 分页插件依赖 -->
2		<dependency>
3			<groupId>com.github.pagehelper</groupId>
4			<artifactId>pagehelper</artifactId>
5			<version>4.2.0</version>
6		</dependency>

MyBatisConfig

 1package net.xdclass.xdvideo.config;
 2
 3import com.github.pagehelper.PageHelper;
 4import org.springframework.context.annotation.Bean;
 5import org.springframework.context.annotation.Configuration;
 6
 7import java.util.Properties;
 8
 9/**
10 * mybatis分页插件配置
11 */
12@Configuration
13public class MyBatisConfig {
14    @Bean
15    public PageHelper pageHelper(){
16        PageHelper pageHelper = new PageHelper();
17        Properties p = new Properties();
18
19        // 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用
20        p.setProperty("offsetAsPageNum","true");
21
22        //设置为true时,使用RowBounds分页会进行count查询
23        p.setProperty("rowBoundsWithCount","true");
24        p.setProperty("reasonable","true");
25        pageHelper.setProperties(p);
26        return pageHelper;
27    }
28}

使用 PageHelper 进行分页

 1/**
 2     * 分页接口
 3     * @param page 当前第几页,默认是第一页
 4     * @param size  每页显示几条
 5     * @return
 6     */
 7    @GetMapping("page")
 8    public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
 9                            @RequestParam(value = "size",defaultValue = "10")int size){
10        PageHelper.startPage(page,size);
11
12        List<Video> list = videoService.findAll();
13
14        PageInfo<Video> pageInfo = new PageInfo<>(list);
15        Map<String,Object> data = new HashMap<>();
16        data.put("total_size",pageInfo.getTotal());//总条数
17        data.put("total_page",pageInfo.getPages());//总页数
18        data.put("current_page",page);//当前页T
19        data.put("data",pageInfo.getList());//数据
20        return data;
21    }

测试

 1//http://localhost:8081/api/v1/video/page?size=3
 2{
 3    "data": [
 4        {
 5            "id": 2,
 6            "title": "葵花宝典",
 7            "summary": "这是概要",
 8            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
 9            "viewNum": 43,
10            "price": 500,
11            "createTime": "2019-11-09T16:00:00.000+0000",
12            "online": 0,
13            "point": 9.7
14        },
15        {
16            "id": 3,
17            "title": "JMeter接口测试入门到实战",
18            "summary": "这是概要",
19            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
20            "viewNum": 53,
21            "price": 123,
22            "createTime": "2019-11-09T16:00:00.000+0000",
23            "online": 0,
24            "point": 8.7
25        },
26        {
27            "id": 4,
28            "title": "Spring Boot2.x零基础入门到高级实战",
29            "summary": "这是概要",
30            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
31            "viewNum": 23,
32            "price": 199,
33            "createTime": "2019-11-09T16:00:00.000+0000",
34            "online": 0,
35            "point": 6.2
36        }
37    ],
38    "total_size": 9,
39    "total_page": 3,
40    "current_page": 1
41}

PageInfo 属性分析

 1{
 2    "pageNum": 1,
 3    "pageSize": 3,
 4    "size": 3,
 5    "orderBy": null,
 6    "startRow": 1,
 7    "endRow": 3,
 8    "total": 9,
 9    "pages": 3,
10    "list": [
11        {
12            "id": 2,
13            "title": "葵花宝典",
14            "summary": "这是概要",
15            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
16            "viewNum": 43,
17            "price": 500,
18            "createTime": "2019-11-09T16:00:00.000+0000",
19            "online": 0,
20            "point": 9.7
21        },
22        {
23            "id": 3,
24            "title": "JMeter接口测试入门到实战",
25            "summary": "这是概要",
26            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
27            "viewNum": 53,
28            "price": 123,
29            "createTime": "2019-11-09T16:00:00.000+0000",
30            "online": 0,
31            "point": 8.7
32        },
33        {
34            "id": 4,
35            "title": "Spring Boot2.x零基础入门到高级实战",
36            "summary": "这是概要",
37            "coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
38            "viewNum": 23,
39            "price": 199,
40            "createTime": "2019-11-09T16:00:00.000+0000",
41            "online": 0,
42            "point": 6.2
43        }
44    ],
45    "prePage": 0,
46    "nextPage": 2,
47    "isFirstPage": true,
48    "isLastPage": false,
49    "hasPreviousPage": false,
50    "hasNextPage": true,
51    "navigatePages": 8,
52    "navigatepageNums": [
53        1,
54        2,
55        3
56    ],
57    "navigateFirstPage": 1,
58    "navigateLastPage": 3,
59    "firstPage": 1,
60    "lastPage": 3
61}

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

单机 Tomcat 应用登录检验

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

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

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

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

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

 1简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息,格式如下:
 2 {
 3 id:888,
 4 name:'小D',
 5 expire:10000
 6 }
 7
 8加密
 9funtion 加密(object, appsecret){
10 xxxx
11 return base64( token);
12 }
13
14解密
15function 解密(token ,appsecret){
16 xxxx
17 //成功返回true,失败返回false
18}
19
20 优点:
21 1)生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
22 2)存储在客户端,不占用服务端的内存资源
23 缺点:
24  token是经过base64编码,所以客户端可以使用base64进行解码(获取明文),因此token加密前的对象不应该包含敏感信息,如用户权限,密码等。
25
262、JWT格式组成 头部、负载、签名
27	header+payload+signature
28 头部:主要是描述签名算法
29 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
30 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
31
323、关于jwt客户端存储
33 可以存储在cookie,localstorage和sessionStorage(关闭浏览器之后就需要重新登录)里
34
35引入依赖
36```xml
37		<!-- JWT引来 -->
38		<dependency>
39			<groupId>io.jsonwebtoken</groupId>
40			<artifactId>jjwt</artifactId>
41			<version>0.7.0</version>
42		</dependency>

JwtUtils

 1package net.xdclass.xdvideo.utils;
 2
 3import io.jsonwebtoken.Claims;
 4import io.jsonwebtoken.Jwts;
 5import io.jsonwebtoken.SignatureAlgorithm;
 6import net.xdclass.xdvideo.domain.User;
 7
 8import java.util.Date;
 9
10/**
11 * jwt工具类
12 */
13public class JwtUtils {
14
15    //主题
16    public static final String SUBJECT = "xdclass";
17
18    //过期时间
19    public static final long EXPIRE = 1000*60*60*24*7;  //过期时间,毫秒,一周
20
21    //秘钥
22    public static final  String APPSECRET = "xd666";
23
24    /**
25     * 生成jwt
26     * @param user
27     * @return
28     */
29    public static String geneJsonWebToken(User user){
30        if(user == null || user.getId() == null || user.getName() == null
31                || user.getHeadImg()==null){
32            return null;
33        }
34        String token = Jwts.builder().setSubject(SUBJECT)
35                .claim("id",user.getId())
36                .claim("name",user.getName())
37                .claim("img",user.getHeadImg())
38                .setIssuedAt(new Date()) //发行时间
39                .setExpiration(new Date(System.currentTimeMillis()+EXPIRE)) //期满时间
40                .signWith(SignatureAlgorithm.HS256,APPSECRET).compact();
41        return token;
42    }
43
44
45    /**
46     * 校验token
47     * @param token
48     * @return
49     */
50    public static Claims checkJWT(String token ){
51        try{
52            final Claims claims =  Jwts.parser().setSigningKey(APPSECRET).
53                    parseClaimsJws(token).getBody();
54            return  claims;
55        }catch (Exception e){ } //解密失败或者过期都会抛异常
56        return null;
57    }
58}

测试(模拟登录认证)

 1package net.xdclass.xdvideo;
 2
 3import io.jsonwebtoken.Claims;
 4import net.xdclass.xdvideo.domain.User;
 5import net.xdclass.xdvideo.utils.JwtUtils;
 6import org.junit.Test;
 7
 8public class CommonTest {
 9
10    /**
11     * 加密测试,生成token
12     */
13    @Test
14    public void testGeneJwt(){
15        //已获取用户信息,查询数据库验证登录信息是否正确
16        User user = new User();
17        user.setId(999);
18        user.setHeadImg("www.xdclass.net");
19        user.setName("xd");
20
21        String token = JwtUtils.geneJsonWebToken(user);
22        System.out.println(token);
23    }
24
25    /**
26     * 解密测试,解密token,验证合法性
27     */
28    @Test
29    public void testCheck(){
30        //正确token
31        //String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";
32        //模拟被修改过的token
33        String token = "2eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";
34
35        Claims claims = JwtUtils.checkJWT(token);
36        if(claims != null){
37            String name = (String)claims.get("name");
38            String img = (String)claims.get("img");
39            int id =(Integer) claims.get("id");
40            System.out.println(name);
41            System.out.println(img);
42            System.out.println(id);
43        }else{
44            System.out.println("非法token");
45        }
46    }
47}

登录方式的优缺点

手机号或者邮箱注册
优点

  • 企业获取了用户的基本资料信息,利于后续业务发展:推送营销类信息
  • 用户可以用个手机号或者邮箱获取对应的 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 示例

1https://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 参数

1redirect_uri?code=CODE&state=STATE

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

1https://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

返回说明
正确的返回:

1{ 
2"access_token":"ACCESS_TOKEN", 
3"expires_in":7200, 
4"refresh_token":"REFRESH_TOKEN",
5"openid":"OPENID", 
6"scope":"SCOPE",
7"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
8}

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

11. access_token有效且未超时;
22. 微信用户已授权给第三方应用帐号相应接口作用域(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 失效后的异常情况。

请求说明

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

参数说明

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

返回说明

正确的 JSON 返回结果:

 1{
 2"openid":"OPENID",
 3"nickname":"NICKNAME",
 4"sex":1,
 5"province":"PROVINCE",
 6"city":"CITY",
 7"country":"COUNTRY",
 8"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
 9"privilege":[
10"PRIVILEGE1",
11"PRIVILEGE2"
12],
13"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
14}

调用频率限制

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

URL 获取

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

增加结果工具类
JsonData

 1package net.xdclass.xdvideo.domain;
 2
 3import java.io.Serializable;
 4
 5/**
 6 * 功能描述:工具类
 7 *
 8 * <p> 创建时间:May 14, 2018 7:58:06 PM </p>
 9 */
10public class JsonData implements Serializable {
11
12	/**
13	 * 
14	 */
15	private static final long serialVersionUID = 1L;
16
17	private Integer code; // 状态码 0 表示成功,1表示处理中,-1表示失败
18	private Object data; // 数据
19	private String msg;// 描述
20
21	public JsonData() {
22	}
23
24	public JsonData(Integer code, Object data, String msg) {
25		this.code = code;
26		this.data = data;
27		this.msg = msg;
28	}
29
30	// 成功,传入数据
31	public static JsonData buildSuccess() {
32		return new JsonData(0, null, null);
33	}
34
35	// 成功,传入数据
36	public static JsonData buildSuccess(Object data) {
37		return new JsonData(0, data, null);
38	}
39
40	// 失败,传入描述信息
41	public static JsonData buildError(String msg) {
42		return new JsonData(-1, null, msg);
43	}
44
45	// 失败,传入描述信息,状态码
46	public static JsonData buildError(String msg, Integer code) {
47		return new JsonData(code, null, msg);
48	}
49
50	// 成功,传入数据,及描述信息
51	public static JsonData buildSuccess(Object data, String msg) {
52		return new JsonData(0, data, msg);
53	}
54
55	// 成功,传入数据,及状态码
56	public static JsonData buildSuccess(Object data, int code) {
57		return new JsonData(code, data, null);
58	}
59
60	public Integer getCode() {
61		return code;
62	}
63
64	public void setCode(Integer code) {
65		this.code = code;
66	}
67
68	public Object getData() {
69		return data;
70	}
71
72	public void setData(Object data) {
73		this.data = data;
74	}
75
76	public String getMsg() {
77		return msg;
78	}
79
80	public void setMsg(String msg) {
81		this.msg = msg;
82	}
83
84	@Override
85	public String toString() {
86		return "JsonData [code=" + code + ", data=" + data + ", msg=" + msg
87				+ "]";
88	}
89}

测试

1//http://localhost:8081/api/v1/video/test_config
2{
3    "code": 0,
4    "data": "wx5beac15ca207cdd40c",
5    "msg": null
6}

application.properties

 1#=================================微信相关==================
 2#公众号
 3wxpay.appid=wx5beac15ca207cdd40c
 4wxpay.appsecret=5548012f33417fdsdsdd6f96b382fe548215e9
 5
 6#微信开放平台配置
 7wxopen.appid=wx025575eac69a2d5b
 8wxopen.appsecret=f5b6730c592ac15b8b1a5aeb8948a9f3
 9#重定向url
10wxopen.redirect_url=http://16webtest.ngrok.xiaomiqiu.cn

WeChatConfig

1

WechatController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.domain.JsonData;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.stereotype.Controller;
 7import org.springframework.web.bind.annotation.GetMapping;
 8import org.springframework.web.bind.annotation.RequestMapping;
 9import org.springframework.web.bind.annotation.RequestParam;
10import org.springframework.web.bind.annotation.ResponseBody;
11
12import java.io.UnsupportedEncodingException;
13import java.net.URLEncoder;
14
15@Controller
16@RequestMapping("/api/v1/wechat")
17public class WechatController {
18
19    @Autowired
20    private WeChatConfig weChatConfig;
21
22    /**
23     * 拼装微信扫一扫登录url
24     * @return
25     */
26    @GetMapping("login_url")
27    @ResponseBody
28    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
29
30        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
31
32        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
33
34        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
35
36        return JsonData.buildSuccess(qrcodeUrl);
37    }
38}

测试(获取二维码 URI)

1//http://localhost:8081/api/v1/wechat/login_url?access_page=www.xdclass.net
2{
3    "code": 0,
4    "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",
5    "msg": null
6}
7
8//从返回的JSON中可以提取二维码URI
9https://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,向微信发起请求。

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

引入依赖

 1<!-- httpclient 相关依赖 -->
 2		<dependency>
 3			<groupId>org.apache.httpcomponents</groupId>
 4			<artifactId>httpclient</artifactId>
 5			<version>4.5.3</version>
 6		</dependency>
 7
 8		<dependency>
 9			<groupId>org.apache.httpcomponents</groupId>
10			<artifactId>httpmime</artifactId>
11			<version>4.5</version>
12		</dependency>
13
14		<dependency>
15			<groupId>commons-codec</groupId>
16			<artifactId>commons-codec</artifactId>
17		</dependency>
18		<dependency>
19			<groupId>commons-logging</groupId>
20			<artifactId>commons-logging</artifactId>
21			<version>1.1.1</version>
22		</dependency>
23		<dependency>
24			<groupId>org.apache.httpcomponents</groupId>
25			<artifactId>httpcore</artifactId>
26		</dependency>
27
28		<!-- gson工具,封装http的时候使用 -->
29		<dependency>
30			<groupId>com.google.code.gson</groupId>
31			<artifactId>gson</artifactId>
32			<version>2.8.2</version>
33		</dependency>

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

  1package net.xdclass.xdvideo.utils;
  2
  3import com.google.gson.Gson;
  4import org.apache.http.HttpEntity;
  5import org.apache.http.HttpResponse;
  6import org.apache.http.client.config.RequestConfig;
  7import org.apache.http.client.methods.CloseableHttpResponse;
  8import org.apache.http.client.methods.HttpGet;
  9import org.apache.http.client.methods.HttpPost;
 10import org.apache.http.entity.StringEntity;
 11import org.apache.http.impl.client.CloseableHttpClient;
 12import org.apache.http.impl.client.HttpClients;
 13import org.apache.http.util.EntityUtils;
 14
 15import java.util.HashMap;
 16import java.util.Map;
 17
 18/**
 19 * 封装http get post
 20 */
 21public class HttpUtils {
 22
 23    private static  final Gson gson = new Gson();
 24
 25    /**
 26     * get方法
 27     * @param url
 28     * @return
 29     */
 30    public static Map<String,Object> doGet(String url){
 31
 32        Map<String,Object> map = new HashMap<>();
 33        CloseableHttpClient httpClient =  HttpClients.createDefault();
 34
 35        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(5000) //连接超时
 36                .setConnectionRequestTimeout(5000)//请求超时
 37                .setSocketTimeout(5000)
 38                .setRedirectsEnabled(true)  //允许自动重定向
 39                .build();
 40
 41        HttpGet httpGet = new HttpGet(url);
 42        httpGet.setConfig(requestConfig);
 43
 44        try{
 45           HttpResponse httpResponse = httpClient.execute(httpGet);
 46           if(httpResponse.getStatusLine().getStatusCode() == 200){
 47               //将Entity转为字符串
 48               String jsonResult = EntityUtils.toString( httpResponse.getEntity());
 49               //将字符串转为 Map<String,Object>
 50               map = gson.fromJson(jsonResult,map.getClass());
 51           }
 52
 53        }catch (Exception e){
 54            e.printStackTrace();
 55        }finally {
 56            try {
 57                httpClient.close();
 58            }catch (Exception e){
 59                e.printStackTrace();
 60            }
 61        }
 62        return map;
 63    }
 64
 65    /**
 66     * 封装post
 67     * @return
 68     */
 69    public static String doPost(String url, String data,int timeout){
 70        CloseableHttpClient httpClient =  HttpClients.createDefault();
 71        //超时设置
 72
 73        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(timeout) //连接超时
 74                .setConnectionRequestTimeout(timeout)//请求超时
 75                .setSocketTimeout(timeout)
 76                .setRedirectsEnabled(true)  //允许自动重定向
 77                .build();
 78
 79
 80        HttpPost httpPost  = new HttpPost(url);
 81        httpPost.setConfig(requestConfig);
 82        httpPost.addHeader("Content-Type","text/html; chartset=UTF-8");
 83
 84        if(data != null && data instanceof  String){ //使用字符串传参
 85            StringEntity stringEntity = new StringEntity(data,"UTF-8");
 86            httpPost.setEntity(stringEntity);
 87        }
 88
 89        try{
 90
 91            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
 92            HttpEntity httpEntity = httpResponse.getEntity();
 93            if(httpResponse.getStatusLine().getStatusCode() == 200){
 94                String result = EntityUtils.toString(httpEntity);
 95                return result;
 96            }
 97
 98        }catch (Exception e){
 99            e.printStackTrace();
100        }finally {
101            try{
102                httpClient.close();
103            }catch (Exception e){
104                e.printStackTrace();
105            }
106        }
107        return null;
108    }
109}

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

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

1redirect_uri?code=CODE&state=STATE

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

1redirect_uri?state=STATE

WechatController 新增 Mapping /user/callback

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.domain.JsonData;
 5import net.xdclass.xdvideo.service.UserService;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.stereotype.Controller;
 8import org.springframework.web.bind.annotation.GetMapping;
 9import org.springframework.web.bind.annotation.RequestMapping;
10import org.springframework.web.bind.annotation.RequestParam;
11import org.springframework.web.bind.annotation.ResponseBody;
12
13import javax.servlet.http.HttpServletResponse;
14import java.io.UnsupportedEncodingException;
15import java.net.URLEncoder;
16
17@Controller
18@RequestMapping("/api/v1/wechat")
19public class WechatController {
20
21    @Autowired
22    private WeChatConfig weChatConfig;
23
24    @Autowired
25    private UserService userService;
26
27    /**
28     * 拼装微信扫一扫登录url
29     * @return
30     */
31    @GetMapping("login_url")
32    @ResponseBody
33    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
34
35        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
36
37        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
38
39        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
40
41        return JsonData.buildSuccess(qrcodeUrl);
42    }
43
44    /**
45     * 保存微信 code ,并且拼装请求,获取access_token
46     * @param code
47     * @param state
48     * @param response
49     */
50    @GetMapping("/user/callback")
51    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
52                                   String state, HttpServletResponse response){
53        User user = userService.saveWeChatUser(code);
54        if (user) {
55            //生成jwt
56        }
57    }
58}

WeChatConfig 新增

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

返回说明
正确的返回:

1{ 
2"access_token":"ACCESS_TOKEN", 
3"expires_in":7200, 
4"refresh_token":"REFRESH_TOKEN",
5"openid":"OPENID", 
6"scope":"SCOPE",
7"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
8}

参数说明

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

UserService

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.User;
 4
 5/**
 6 *用户业务接口类
 7 */
 8public interface UserService {
 9     User saveWeChatUser(String code);
10}

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

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

请求说明

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

参数说明

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

UserServiceImpl

 1package net.xdclass.xdvideo.service.impl;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.domain.User;
 5import net.xdclass.xdvideo.mapper.UserMapper;
 6import net.xdclass.xdvideo.service.UserService;
 7import net.xdclass.xdvideo.utils.HttpUtils;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.stereotype.Service;
10
11import java.io.UnsupportedEncodingException;
12import java.util.Date;
13import java.util.Map;
14
15@Service
16public class UserServiceImpl implements UserService {
17
18    @Autowired
19    private WeChatConfig weChatConfig;
20
21    @Autowired
22    private UserMapper userMapper;
23
24    @Override
25    public User saveWeChatUser(String code) {
26        //获取token:拼装请求 OpenAppid + OpenAppsecret + code(临时票据)
27        String accessTokenUrl = String.format(WeChatConfig.getOpenAccessTokenUrl(),weChatConfig.getOpenAppid(),weChatConfig.getOpenAppsecret(),code);
28
29        //从微信回复的数据中提取: access_token + openid 
30        Map<String ,Object> baseMap =  HttpUtils.doGet(accessTokenUrl);
31
32        if(baseMap == null || baseMap.isEmpty()){ return  null; }
33        String accessToken = (String)baseMap.get("access_token");
34        String openId  = (String) baseMap.get("openid");
35
36        //先查询本地数据库
37        User dbUser = userMapper.findByopenid(openId);
38        if(dbUser!=null) { //更新用户,直接返回
39            return dbUser;
40        }
41
42        //获取用户基本信息
43        String userInfoUrl = String.format(WeChatConfig.getOpenUserInfoUrl(),accessToken,openId);
44            //获取access_token
45        Map<String ,Object> baseUserMap =  HttpUtils.doGet(userInfoUrl);
46
47        if(baseUserMap == null || baseUserMap.isEmpty()){ return  null; }
48
49        String nickname = (String)baseUserMap.get("nickname");
50        Double sexTemp  = (Double) baseUserMap.get("sex");
51        int sex = sexTemp.intValue();
52        String province = (String)baseUserMap.get("province");
53        String city = (String)baseUserMap.get("city");
54        String country = (String)baseUserMap.get("country");
55        String headimgurl = (String)baseUserMap.get("headimgurl");
56        StringBuilder sb = new StringBuilder(country).append("||").append(province).append("||").append(city);
57        String finalAddress = sb.toString();
58        try {
59            //解决乱码
60            nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");
61            finalAddress = new String(finalAddress.getBytes("ISO-8859-1"), "UTF-8");
62
63        } catch (UnsupportedEncodingException e) {
64            e.printStackTrace();
65        }
66
67        User user = new User();
68        user.setName(nickname);
69        user.setHeadImg(headimgurl);
70        user.setCity(finalAddress);
71        user.setOpenid(openId);
72        user.setSex(sex);
73        user.setCreateTime(new Date());
74        userMapper.save(user);
75        return user;
76    }
77}

UserMapper

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.User;
 4import org.apache.ibatis.annotations.Insert;
 5import org.apache.ibatis.annotations.Options;
 6import org.apache.ibatis.annotations.Param;
 7import org.apache.ibatis.annotations.Select;
 8
 9public interface UserMapper {
10
11
12    /**
13     * 根据主键id查找
14     * @param userId
15     * @return
16     */
17    @Select("select * from user where id = #{id}")
18    User findByid(@Param("id") int userId);
19
20    /**
21     * 根据openid找用户
22     * @param openid
23     * @return
24     */
25    @Select("select * from user where openid = #{openid}")
26    User findByopenid(@Param("openid") String openid);
27
28    /**
29     * 保存用户新
30     * @param user
31     * @return
32     */
33    @Insert("INSERT INTO `user` ( `openid`, `name`, `head_img`, `phone`, `sign`, `sex`, `city`, `create_time`)" +
34            "VALUES" +
35            "(#{openid},#{name},#{headImg},#{phone},#{sign},#{sex},#{city},#{createTime});")
36    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
37    int save(User user);
38}

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

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

WechatController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.domain.JsonData;
 5import net.xdclass.xdvideo.domain.User;
 6import net.xdclass.xdvideo.service.UserService;
 7import net.xdclass.xdvideo.utils.JwtUtils;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.stereotype.Controller;
10import org.springframework.web.bind.annotation.GetMapping;
11import org.springframework.web.bind.annotation.RequestMapping;
12import org.springframework.web.bind.annotation.RequestParam;
13import org.springframework.web.bind.annotation.ResponseBody;
14
15import javax.servlet.http.HttpServletResponse;
16import java.io.IOException;
17import java.io.UnsupportedEncodingException;
18import java.net.URLEncoder;
19
20@Controller
21@RequestMapping("/api/v1/wechat")
22public class WechatController {
23
24    @Autowired
25    private WeChatConfig weChatConfig;
26
27    @Autowired
28    private UserService userService;
29
30    /**
31     * 拼装微信扫一扫登录url
32     * @return JsonData
33     */
34    @GetMapping("login_url")
35    @ResponseBody
36    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
37
38        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
39        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
40        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
41        return JsonData.buildSuccess(qrcodeUrl);
42    }
43
44
45    /**
46     * 获取用户code,保存 user,并且拼装请求,获取access_token
47     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
48     * 第三方服务器端,种cookie,再转发到用户当前页面地址
49     * @param code
50     * @param state  用户扫码登录时停留的页面
51     * @param response
52     */
53    @GetMapping("/user/callback")
54    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
55                                   String state, HttpServletResponse response) throws IOException {
56        User user = userService.saveWeChatUser(code);
57        if (user != null){
58            //生成jwt
59            String token = JwtUtils.geneJsonWebToken(user);
60
61            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
62            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
63        }
64    }
65}

用户登录拦截器

LoginIntercepter

 1package net.xdclass.xdvideo.interceoter;
 2
 3import com.google.gson.Gson;
 4import io.jsonwebtoken.Claims;
 5import net.xdclass.xdvideo.domain.JsonData;
 6import net.xdclass.xdvideo.utils.JwtUtils;
 7import org.springframework.web.servlet.HandlerInterceptor;
 8
 9import javax.servlet.http.HttpServletRequest;
10import javax.servlet.http.HttpServletResponse;
11import java.io.IOException;
12import java.io.PrintWriter;
13
14public class LoginIntercepter implements HandlerInterceptor {
15
16    private static final Gson gson = new Gson();
17
18    /**
19     * 进入controller之前进行拦截
20     * @param request
21     * @param response
22     * @param handler
23     * @return
24     * @throws Exception
25     */
26    @Override
27    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
28
29        String token = request.getHeader("token");
30        if(token == null ){
31            token = request.getParameter("token");
32        }
33
34        //不为空,就进行解密
35        if(token != null ) {
36           Claims claims =  JwtUtils.checkJWT(token);
37            if(claims !=null){
38                //获取到 id 、 name
39                Integer userId = (Integer)claims.get("id");
40                String name = (String) claims.get("name");
41
42                //设置后续业务可能会用到的数据:这里 只设置了  id 、 name
43                request.setAttribute("user_id",userId);
44                request.setAttribute("name",name);
45                return true;
46            }
47        }
48
49        //解密失败 或者 没有token 就需要登录
50        sendJsonMessage(response,JsonData.buildError("请登录"));
51        return false;
52    }
53
54    /**
55     * 响应数据给前端
56     * @param response
57     * @param obj
58     */
59    public static void sendJsonMessage(HttpServletResponse response, Object obj) throws IOException {
60        response.setContentType("application/json; charset=utf-8");
61        PrintWriter writer = response.getWriter();
62
63        //JsonData 对象 转换为 json 格式,回写给前端
64        writer.print(gson.toJson(obj));
65        writer.close();
66
67        //刷新缓冲
68        response.flushBuffer();
69    }
70}

IntercepterConfig

 1package net.xdclass.xdvideo.config;
 2
 3import net.xdclass.xdvideo.interceoter.LoginIntercepter;
 4import org.springframework.context.annotation.Configuration;
 5import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 6import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 7
 8/**
 9 * 拦截器配置
10 */
11@Configuration
12public class IntercepterConfig implements WebMvcConfigurer {
13    /**
14     * 注册拦截器
15     * @param registry
16     */
17    @Override
18    public void addInterceptors(InterceptorRegistry registry) {
19        registry.addInterceptor(new LoginIntercepter()).addPathPatterns("/user/api/v1/*/**");
20        WebMvcConfigurer.super.addInterceptors(registry);
21    }
22
23}

OrderController(模拟订单接口)

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.domain.JsonData;
 4import org.springframework.web.bind.annotation.GetMapping;
 5import org.springframework.web.bind.annotation.RequestMapping;
 6import org.springframework.web.bind.annotation.RestController;
 7
 8/**
 9 * 订单接口
10 */
11@RestController
12@RequestMapping("/user/api/v1/order")
13public class OrderController {
14  
15    @GetMapping("add")
16    public JsonData saveOrder(){
17        return JsonData.buildSuccess("下单成功");
18    }
19  
20}

测试(PostMan)

 1//错误的token
 2http://localhost:8081/user/api/v1/order/add?token=yyyy
 3{
 4	"code":-1,
 5	"msg":"请登录"
 6}
 7
 8//正确的token
 9http://localhost:8081/user/api/v1/order/add?token=xxxx
10{
11	"code":0,
12	"data":"下单成功",
13	"msg":null
14}

第三方支付和聚合支付

什么是第三方支付
 第三方支付是指具备一定实力和信誉保障的独立机构,采用与各大银行签约的方式,通过与银行支付结算系统接口对接而促成交易双方进行交易的网络支付模式。 通俗的例子: 支付宝,微信支付,百度钱包,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

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import org.apache.ibatis.annotations.*;
 5
 6import java.util.List;
 7
 8/**
 9 * 订单dao层
10 */
11public interface VideoOrderMapper {
12
13    /**
14     * 保存订单,返回包含主键
15     * @param videoOrder
16     * @return
17     */
18    @Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
19            " `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
20            " `video_img`, `user_id`, `ip`, `del`)" +
21            "VALUES" +
22            "(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
23            "#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
24    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
25    int insert(VideoOrder videoOrder);
26
27    /**
28     * 根据主键查找订单
29     * @param id
30     * @return
31     */
32    @Select("select * from video_order where id=#{order_id} and del=0")
33    VideoOrder findById(@Param("order_id") int id);
34
35    /**
36     * 根据交易订单号获取订单对象
37     * @param outTradeNo
38     * @return
39     */
40    @Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
41    VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);
42
43    /**
44     * 逻辑删除订单
45     * @param id
46     * @param userId
47     * @return
48     */
49    @Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
50    int del(@Param("id") int id, @Param("userId") int userId);
51
52    /**
53     * 查找我的全部订单
54     * @param userId
55     * @return
56     */
57    @Select("select * from video_order where user_id =#{userId}")
58    List<VideoOrder> findMyOrderList(int userId);
59
60    /**
61     * 根据微信回调订单流水号更新订单状态
62     * @param videoOrder
63     * @return
64     */
65    @Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
66            " where out_trade_no=#{outTradeNo} and state=0 and del=0")
67    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
68
69}

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

1、 核心注解

1@RunWith(SpringRunner.class)  
2 @SpringBootTest

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

VideoServiceTest

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.Video;
 4import net.xdclass.xdvideo.mapper.VideoMapper;
 5import org.junit.Test;
 6import org.junit.runner.RunWith;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.boot.test.context.SpringBootTest;
 9import org.springframework.test.context.junit4.SpringRunner;
10
11import java.util.List;
12
13import static org.junit.Assert.*;
14@RunWith(SpringRunner.class)
15@SpringBootTest
16public class VideoServiceTest {
17    @Autowired
18    private VideoService videoService;
19    @Test
20    public void findAll() {
21        List<Video> list = videoService.findAll();
22        assertNotNull(list);
23        for (Video video : list) {
24            System.out.println(video.getTitle());
25        }
26    }
27
28    @Test
29    public void findById() {
30        Video video = videoService.findById(2);
31        assertNotNull(video);
32    }
33
34    @Test
35    public void update() {
36    }
37
38    @Test
39    public void delete() {
40    }
41
42    @Test
43    public void save() {
44    }
45}

VideoOrderMapperTest

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import org.junit.Test;
 5import org.junit.runner.RunWith;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.boot.test.context.SpringBootTest;
 8import org.springframework.test.context.junit4.SpringRunner;
 9
10import static org.junit.Assert.*;
11
12@RunWith(SpringRunner.class)
13@SpringBootTest
14public class VideoOrderMapperTest {
15
16    @Autowired
17    private VideoOrderMapper videoOrderMapper;
18
19    @Test
20    public void insert() {
21        VideoOrder videoOrder = new VideoOrder();
22        videoOrder.setDel(0);
23        videoOrder.setTotalFee(111);
24        videoOrder.setHeadImg("xxxxdfsdfs");
25        videoOrder.setVideoTitle("springBoot高级视频教程");
26        videoOrderMapper.insert(videoOrder);
27        assertNotNull(videoOrder.getId());
28
29    }
30
31    @Test
32    public void findById() {
33        VideoOrder videoOrder = videoOrderMapper.findById(1);
34        assertNotNull(videoOrder);
35    }
36
37    @Test
38    public void findByOutTradeNo() {
39    }
40
41    @Test
42    public void del() {
43    }
44
45    @Test
46    public void findMyOrderList() {
47    }
48
49    @Test
50    public void updateVideoOderByOutTradeNo() {
51    }
52}

封装常用工具类 CommonUtils 和 WXpayUtils

CommonUtils 包含方法 md5,uuid 等
CommonUtils

 1package net.xdclass.xdvideo.utils;
 2
 3import java.security.MessageDigest;
 4import java.util.UUID;
 5
 6/**
 7 * 常用工具类的封装,md5,uuid等
 8 */
 9public class CommonUtils {
10    /**
11     * 生成 uuid, 即用来标识一笔单,也用做 nonce_str
12     * @return
13     */
14    public static String generateUUID(){
15        String uuid = UUID.randomUUID().toString().
16                replaceAll("-","").substring(0,32);
17
18        return uuid;
19    }
20
21    /**
22     * md5常用工具类
23     * @param data
24     * @return
25     */
26    public static String MD5(String data){
27        try {
28            MessageDigest md5 = MessageDigest.getInstance("MD5");
29            byte [] array = md5.digest(data.getBytes("UTF-8"));
30            StringBuilder sb = new StringBuilder();
31            for (byte item : array) {
32                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
33            }
34            return sb.toString().toUpperCase();
35
36        }catch (Exception e){
37            e.printStackTrace();
38        }
39        return null;
40    }
41}

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

WXPayUtil

  1package net.xdclass.xdvideo.utils;
  2
  3import org.w3c.dom.Entity;
  4import org.w3c.dom.Node;
  5import org.w3c.dom.NodeList;
  6
  7import javax.xml.parsers.DocumentBuilder;
  8import javax.xml.parsers.DocumentBuilderFactory;
  9import javax.xml.transform.OutputKeys;
 10import javax.xml.transform.Transformer;
 11import javax.xml.transform.TransformerFactory;
 12import javax.xml.transform.dom.DOMSource;
 13import javax.xml.transform.stream.StreamResult;
 14import java.io.ByteArrayInputStream;
 15import java.io.InputStream;
 16import java.io.StringWriter;
 17import java.util.*;
 18
 19/**
 20 * 微信支付工具类,xml转map,map转xml,生成签名
 21 */
 22public class WXPayUtil {
 23
 24    /**
 25     * XML格式字符串转换为Map
 26     *
 27     * @param strXML XML字符串
 28     * @return XML数据转换后的Map
 29     * @throws Exception
 30     */
 31    public static Map<String, String> xmlToMap(String strXML) throws Exception {
 32        try {
 33            Map<String, String> data = new HashMap<String, String>();
 34            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 35            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
 36            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
 37            org.w3c.dom.Document doc = documentBuilder.parse(stream);
 38            doc.getDocumentElement().normalize();
 39            NodeList nodeList = doc.getDocumentElement().getChildNodes();
 40            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
 41                Node node = nodeList.item(idx);
 42                if (node.getNodeType() == Node.ELEMENT_NODE) {
 43                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
 44                    data.put(element.getNodeName(), element.getTextContent());
 45                }
 46            }
 47            try {
 48                stream.close();
 49            } catch (Exception ex) {
 50                // do nothing
 51            }
 52            return data;
 53        } catch (Exception ex) {
 54            throw ex;
 55        }
 56
 57    }
 58
 59    /**
 60     * 将Map转换为XML格式的字符串
 61     *
 62     * @param data Map类型数据
 63     * @return XML格式的字符串
 64     * @throws Exception
 65     */
 66    public static String mapToXml(Map<String, String> data) throws Exception {
 67        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 68        DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
 69        org.w3c.dom.Document document = documentBuilder.newDocument();
 70        org.w3c.dom.Element root = document.createElement("xml");
 71        document.appendChild(root);
 72        for (String key: data.keySet()) {
 73            String value = data.get(key);
 74            if (value == null) {
 75                value = "";
 76            }
 77            value = value.trim();
 78            org.w3c.dom.Element filed = document.createElement(key);
 79            filed.appendChild(document.createTextNode(value));
 80            root.appendChild(filed);
 81        }
 82        TransformerFactory tf = TransformerFactory.newInstance();
 83        Transformer transformer = tf.newTransformer();
 84        DOMSource source = new DOMSource(document);
 85        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
 86        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 87        StringWriter writer = new StringWriter();
 88        StreamResult result = new StreamResult(writer);
 89        transformer.transform(source, result);
 90        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
 91        try {
 92            writer.close();
 93        }
 94        catch (Exception ex) {
 95        }
 96        return output;
 97    }
 98
 99    /**
100     * 生成微信支付sign
101     * @return
102     */
103    public static String createSign(SortedMap<String, String> params, String key){
104        StringBuilder sb = new StringBuilder();
105        Set<Map.Entry<String, String>> es =  params.entrySet();
106        Iterator<Map.Entry<String,String>> it =  es.iterator();
107
108        //生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
109        while (it.hasNext()){
110            Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
111             String k = (String)entry.getKey();
112             String v = (String)entry.getValue();
113             if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
114                sb.append(k+"="+v+"&");
115             }
116        }
117
118        sb.append("key=").append(key);
119        String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
120        return sign;
121    }
122
123    /**
124     * 校验签名
125     *
126     * @param params
127     * @param key
128     * @return
129     */
130    public static boolean isCorrectSign(SortedMap<String, String> params, String key){
131        String sign = createSign(params,key);
132        String weixinPaySign = params.get("sign").toUpperCase();
133        return weixinPaySign.equals(sign);
134    }
135}

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

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

#微信商户平台
application.properties

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

WeChatConfig

1

IpUtils

 1package net.xdclass.xdvideo.utils;
 2
 3import java.net.InetAddress;
 4import java.net.UnknownHostException;
 5
 6import javax.servlet.http.HttpServletRequest;
 7
 8public class IpUtils {
 9    public static String getIpAddr(HttpServletRequest request) {
10        String ipAddress = null;
11        try {
12            ipAddress = request.getHeader("x-forwarded-for");
13            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
14                ipAddress = request.getHeader("Proxy-Client-IP");
15            }
16            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
17                ipAddress = request.getHeader("WL-Proxy-Client-IP");
18            }
19            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
20                ipAddress = request.getRemoteAddr();
21                if (ipAddress.equals("127.0.0.1")) {
22                    // 根据网卡取本机配置的IP
23                    InetAddress inet = null;
24                    try {
25                        inet = InetAddress.getLocalHost();
26                    } catch (UnknownHostException e) {
27                        e.printStackTrace();
28                    }
29                    ipAddress = inet.getHostAddress();
30                }
31            }
32            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
33            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
34                                                                // = 15
35                if (ipAddress.indexOf(",") > 0) {
36                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
37                }
38            }
39        } catch (Exception e) {
40            ipAddress="";
41        }
42        return ipAddress;
43    }
44}

VideoOrderDto

 1package net.xdclass.xdvideo.dto;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4
 5/**
 6 * 订单数据传输对象
 7 */
 8public class VideoOrderDto extends VideoOrder {
 9
10}

VideoController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.domain.JsonData;
 4import net.xdclass.xdvideo.dto.VideoOrderDto;
 5import net.xdclass.xdvideo.service.VideoOrderService;
 6import net.xdclass.xdvideo.utils.IpUtils;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.web.bind.annotation.GetMapping;
 9import org.springframework.web.bind.annotation.RequestMapping;
10import org.springframework.web.bind.annotation.RequestParam;
11import org.springframework.web.bind.annotation.RestController;
12
13import javax.servlet.http.HttpServletRequest;
14
15/**
16 * 订单接口
17 */
18@RestController
19//@RequestMapping("/user/api/v1/order")
20@RequestMapping("/api/v1/order")
21public class OrderController {
22
23
24    @Autowired
25    private VideoOrderService videoOrderService;
26
27    @GetMapping("add")
28    public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
29                              HttpServletRequest request) throws Exception {
30        //获取用户的ip地址
31        String ip = IpUtils.getIpAddr(request);
32
33        //int userId = request.getAttribute("user_id");
34        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
35        int userId = 1;
36        VideoOrderDto videoOrderDto = new VideoOrderDto();
37        videoOrderDto.setUserId(userId);
38        videoOrderDto.setVideoId(videoId);
39        videoOrderDto.setIp(ip);
40
41        //Dto转换保存
42        videoOrderService.save(videoOrderDto);
43
44        return JsonData.buildSuccess("下单成功");
45    }
46
47}

VideoOrderService

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import net.xdclass.xdvideo.dto.VideoOrderDto;
 5
 6/**
 7 * 订单接口
 8 */
 9public interface VideoOrderService {
10
11    /**
12     * 下单接口
13     * @param videoOrderDto
14     * @return
15     */
16    VideoOrder save(VideoOrderDto videoOrderDto);
17
18}

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

VideoOrderServiceImpl

  1package net.xdclass.xdvideo.service.impl;
  2
  3import net.xdclass.xdvideo.config.WeChatConfig;
  4import net.xdclass.xdvideo.domain.User;
  5import net.xdclass.xdvideo.domain.Video;
  6import net.xdclass.xdvideo.domain.VideoOrder;
  7import net.xdclass.xdvideo.dto.VideoOrderDto;
  8import net.xdclass.xdvideo.mapper.UserMapper;
  9import net.xdclass.xdvideo.mapper.VideoMapper;
 10import net.xdclass.xdvideo.mapper.VideoOrderMapper;
 11import net.xdclass.xdvideo.service.VideoOrderService;
 12import net.xdclass.xdvideo.utils.CommonUtils;
 13import net.xdclass.xdvideo.utils.WXPayUtil;
 14import org.springframework.beans.factory.annotation.Autowired;
 15import org.springframework.stereotype.Service;
 16
 17import java.util.Date;
 18import java.util.SortedMap;
 19import java.util.TreeMap;
 20
 21@Service
 22public class VideoOrderServiceImpl implements VideoOrderService {
 23
 24
 25    @Autowired
 26    private WeChatConfig weChatConfig;
 27
 28    @Autowired
 29    private VideoMapper videoMapper;
 30
 31    @Autowired
 32    private VideoOrderMapper videoOrderMapper;
 33
 34    @Autowired
 35    private UserMapper userMapper;
 36
 37    @Override
 38    public VideoOrder save(VideoOrderDto videoOrderDto) throws Exception {
 39        //查找视频信息
 40        Video video =  videoMapper.findById(videoOrderDto.getVideoId());
 41
 42        //查找用户信息
 43        User user = userMapper.findByid(videoOrderDto.getUserId());
 44
 45
 46        //生成订单
 47        VideoOrder videoOrder = new VideoOrder();
 48        videoOrder.setTotalFee(video.getPrice());
 49        videoOrder.setVideoImg(video.getCoverImg());
 50        videoOrder.setVideoTitle(video.getTitle());
 51        videoOrder.setCreateTime(new Date());
 52        videoOrder.setVideoId(video.getId());
 53        videoOrder.setState(0);
 54        videoOrder.setUserId(user.getId());
 55        videoOrder.setHeadImg(user.getHeadImg());
 56        videoOrder.setNickname(user.getName());
 57
 58        videoOrder.setDel(0);
 59        videoOrder.setIp(videoOrderDto.getIp());
 60        videoOrder.setOutTradeNo(CommonUtils.generateUUID());
 61
 62        videoOrderMapper.insert(videoOrder);
 63
 64        //生成签名
 65        unifiedOrder(videoOrder);
 66
 67        //获取codeurl
 68
 69
 70        //生成二维码
 71
 72
 73
 74        return null;
 75    }
 76
 77
 78    /**
 79     * 统一下单方法 :生成签名
 80     * @return
 81     */
 82    private String unifiedOrder(VideoOrder videoOrder) throws Exception {
 83
 84        //生成签名
 85        SortedMap<String,String> params = new TreeMap<>();
 86        params.put("appid",weChatConfig.getAppId());
 87        params.put("mch_id", weChatConfig.getMchId());
 88        params.put("nonce_str",CommonUtils.generateUUID());
 89        params.put("body",videoOrder.getVideoTitle());
 90        params.put("out_trade_no",videoOrder.getOutTradeNo());
 91        params.put("total_fee",videoOrder.getTotalFee().toString());
 92        params.put("spbill_create_ip",videoOrder.getIp());
 93        params.put("notify_url",weChatConfig.getPayCallbackUrl());
 94        params.put("trade_type","NATIVE");
 95
 96        //sign签名
 97        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
 98        params.put("sign",sign);
 99
100        //map转xml
101        String payXml = WXPayUtil.mapToXml(params);
102
103        System.out.println(payXml);
104        //统一下单
105
106        return "";
107    }
108
109}

测试

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

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

发送数据

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

商户 key

1xdclasss20182018xdclass2018x018d

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

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

1、配置统一下单接口
WeChatConfig

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

2、发送请求验证

 1微信统一下单响应  
 2 <xml><return_code><![CDATA[SUCCESS]]></return_code>  
 3 <return_msg><![CDATA[OK]]></return_msg>  
 4 <appid><![CDATA[wx5beac15ca207c40c]]></appid>  
 5 <mch_id><![CDATA[1503809911]]></mch_id>  
 6 <nonce_str><![CDATA[Go5gDC2CYL5HvizG]]></nonce_str>  
 7 <sign><![CDATA[BC62592B9A94F5C914FAAD93ADE7662B]]></sign>  
 8 <result_code><![CDATA[SUCCESS]]></result_code>  
 9 <prepay_id><![CDATA[wx262207318328044f75c9ebec2216783076]]></prepay_id>  
10 <trade_type><![CDATA[NATIVE]]></trade_type>  
11 <code_url><![CDATA[weixin://wxpay/bizpayurl?pr=hFq9fX6]]></code_url>  
12 </xml>

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

VideoOrderService

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import net.xdclass.xdvideo.dto.VideoOrderDto;
 5
 6/**
 7 * 订单接口
 8 */
 9public interface VideoOrderService {
10
11    /**
12     * 下单接口
13     * @param videoOrderDto
14     * @return
15     */
16    String save(VideoOrderDto videoOrderDto) throws Exception;
17}

VideoOrderServiceImpl

  1package net.xdclass.xdvideo.service.impl;
  2
  3import net.xdclass.xdvideo.config.WeChatConfig;
  4import net.xdclass.xdvideo.domain.User;
  5import net.xdclass.xdvideo.domain.Video;
  6import net.xdclass.xdvideo.domain.VideoOrder;
  7import net.xdclass.xdvideo.dto.VideoOrderDto;
  8import net.xdclass.xdvideo.mapper.UserMapper;
  9import net.xdclass.xdvideo.mapper.VideoMapper;
 10import net.xdclass.xdvideo.mapper.VideoOrderMapper;
 11import net.xdclass.xdvideo.service.VideoOrderService;
 12import net.xdclass.xdvideo.utils.CommonUtils;
 13import net.xdclass.xdvideo.utils.HttpUtils;
 14import net.xdclass.xdvideo.utils.WXPayUtil;
 15import org.springframework.beans.factory.annotation.Autowired;
 16import org.springframework.stereotype.Service;
 17
 18import java.util.Date;
 19import java.util.Map;
 20import java.util.SortedMap;
 21import java.util.TreeMap;
 22
 23@Service
 24public class VideoOrderServiceImpl implements VideoOrderService {
 25
 26
 27    @Autowired
 28    private WeChatConfig weChatConfig;
 29
 30    @Autowired
 31    private VideoMapper videoMapper;
 32
 33    @Autowired
 34    private VideoOrderMapper videoOrderMapper;
 35
 36    @Autowired
 37    private UserMapper userMapper;
 38
 39    @Override
 40    public String save(VideoOrderDto videoOrderDto) throws Exception {
 41        //查找视频信息
 42        Video video =  videoMapper.findById(videoOrderDto.getVideoId());
 43
 44        //查找用户信息
 45        User user = userMapper.findByid(videoOrderDto.getUserId());
 46
 47
 48        //生成订单
 49        VideoOrder videoOrder = new VideoOrder();
 50        videoOrder.setTotalFee(video.getPrice());
 51        videoOrder.setVideoImg(video.getCoverImg());
 52        videoOrder.setVideoTitle(video.getTitle());
 53        videoOrder.setCreateTime(new Date());
 54        videoOrder.setVideoId(video.getId());
 55        videoOrder.setState(0);
 56        videoOrder.setUserId(user.getId());
 57        videoOrder.setHeadImg(user.getHeadImg());
 58        videoOrder.setNickname(user.getName());
 59
 60        videoOrder.setDel(0);
 61        videoOrder.setIp(videoOrderDto.getIp());
 62        videoOrder.setOutTradeNo(CommonUtils.generateUUID());
 63
 64        videoOrderMapper.insert(videoOrder);
 65
 66        //获取codeurl
 67        String codeUrl = unifiedOrder(videoOrder);
 68        return codeUrl;
 69
 70    }
 71  
 72    /**
 73     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
 74     * @return
 75     */
 76    private String unifiedOrder(VideoOrder videoOrder) throws Exception {
 77
 78        //生成签名
 79        SortedMap<String,String> params = new TreeMap<>();
 80        params.put("appid",weChatConfig.getAppId());
 81        params.put("mch_id", weChatConfig.getMchId());
 82        params.put("nonce_str",CommonUtils.generateUUID());
 83        params.put("body",videoOrder.getVideoTitle());
 84        params.put("out_trade_no",videoOrder.getOutTradeNo());
 85        params.put("total_fee",videoOrder.getTotalFee().toString());
 86        params.put("spbill_create_ip",videoOrder.getIp());
 87        params.put("notify_url",weChatConfig.getPayCallbackUrl());
 88        params.put("trade_type","NATIVE");
 89
 90        //sign签名
 91        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
 92        params.put("sign",sign);
 93
 94        //map转xml
 95        String payXml = WXPayUtil.mapToXml(params);
 96
 97        System.out.println(payXml);
 98
 99        //统一下单
100        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
101        if(null == orderStr) {
102            return null;
103        }
104
105
106        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
107        System.out.println(unifiedOrderMap.toString());
108        if(unifiedOrderMap != null) {
109            //获取二维码url
110            return unifiedOrderMap.get("code_url");
111        }
112
113        return "";
114    }
115  
116}

OrderController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.domain.JsonData;
 4import net.xdclass.xdvideo.dto.VideoOrderDto;
 5import net.xdclass.xdvideo.service.VideoOrderService;
 6import net.xdclass.xdvideo.utils.IpUtils;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.web.bind.annotation.GetMapping;
 9import org.springframework.web.bind.annotation.RequestMapping;
10import org.springframework.web.bind.annotation.RequestParam;
11import org.springframework.web.bind.annotation.RestController;
12
13import javax.servlet.http.HttpServletRequest;
14
15/**
16 * 订单接口
17 */
18@RestController
19//@RequestMapping("/user/api/v1/order")
20@RequestMapping("/api/v1/order")
21public class OrderController {
22
23
24    @Autowired
25    private VideoOrderService videoOrderService;
26
27    @GetMapping("add")
28    public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
29                              HttpServletRequest request) throws Exception {
30        //获取用户的ip地址
31        //String ip = IpUtils.getIpAddr(request);
32        String ip = "120.25.1.11";
33
34        //int userId = request.getAttribute("user_id");
35        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
36        int userId = 1;
37        VideoOrderDto videoOrderDto = new VideoOrderDto();
38        videoOrderDto.setUserId(userId);
39        videoOrderDto.setVideoId(videoId);
40        videoOrderDto.setIp(ip);
41
42        //获取codeUrl
43        String codeUrl = videoOrderService.save(videoOrderDto);
44      
45        //生成二维码
46        return JsonData.buildSuccess("下单成功");
47    }
48}

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

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

参考

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

引入依赖

 1<!-- google二维码生成包 -->
 2		<dependency>
 3			<groupId>com.google.zxing</groupId>
 4			<artifactId>javase</artifactId>
 5			<version>3.3.0</version>
 6		</dependency>
 7
 8		<dependency>
 9			<groupId>com.google.zxing</groupId>
10			<artifactId>core</artifactId>
11			<version>2.0</version>
12		</dependency>

OrderController

 1package net.xdclass.xdvideo.controller;
 2
 3import com.google.zxing.BarcodeFormat;
 4import com.google.zxing.EncodeHintType;
 5import com.google.zxing.MultiFormatWriter;
 6import com.google.zxing.client.j2se.MatrixToImageWriter;
 7import com.google.zxing.common.BitMatrix;
 8import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
 9import net.xdclass.xdvideo.domain.JsonData;
10import net.xdclass.xdvideo.dto.VideoOrderDto;
11import net.xdclass.xdvideo.service.VideoOrderService;
12import net.xdclass.xdvideo.utils.IpUtils;
13import org.springframework.beans.factory.annotation.Autowired;
14import org.springframework.web.bind.annotation.GetMapping;
15import org.springframework.web.bind.annotation.RequestMapping;
16import org.springframework.web.bind.annotation.RequestParam;
17import org.springframework.web.bind.annotation.RestController;
18
19import javax.servlet.http.HttpServletRequest;
20import javax.servlet.http.HttpServletResponse;
21import java.io.OutputStream;
22import java.util.HashMap;
23import java.util.Map;
24
25/**
26 * 订单接口
27 */
28@RestController
29//@RequestMapping("/user/api/v1/order")
30@RequestMapping("/api/v1/order")
31public class OrderController {
32
33
34    @Autowired
35    private VideoOrderService videoOrderService;
36
37    @GetMapping("add")
38    public void saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
39                              HttpServletRequest request,
40                              HttpServletResponse response) throws Exception {
41        //获取用户的ip地址
42        //String ip = IpUtils.getIpAddr(request);
43        String ip = "120.25.1.11";
44
45        //int userId = request.getAttribute("user_id");
46        //开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
47        int userId = 1;
48        VideoOrderDto videoOrderDto = new VideoOrderDto();
49        videoOrderDto.setUserId(userId);
50        videoOrderDto.setVideoId(videoId);
51        videoOrderDto.setIp(ip);
52
53        //获取codeUrl
54        String codeUrl = videoOrderService.save(videoOrderDto);
55        if(codeUrl == null) {
56            throw new  NullPointerException();
57        }
58
59        try{
60            //生成二维码的配置
61            Map<EncodeHintType,Object> hints =  new HashMap<>();
62
63            //设置纠错等级
64            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
65
66            //编码类型
67            hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
68
69            //构建矩阵图对象
70            BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
71            OutputStream out =  response.getOutputStream();
72
73            //返回矩阵图给微信客户端
74            MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
75
76        }catch (Exception e){
77            e.printStackTrace();
78        }
79    }
80}

测试

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

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

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

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

扫码时序图

Reference
回调数据如下

 1<xml>  
 2  <appid><![CDATA[wx2421b1c4370ec43b]]></appid>  
 3  <attach><![CDATA[支付测试]]></attach>  
 4  <bank_type><![CDATA[CFT]]></bank_type>  
 5  <fee_type><![CDATA[CNY]]></fee_type>  
 6  <is_subscribe><![CDATA[Y]]></is_subscribe>  
 7  <mch_id><![CDATA[10000100]]></mch_id>  
 8  <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>  
 9  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>  
10  <out_trade_no><![CDATA[1409811653]]></out_trade_no>  
11  <result_code><![CDATA[SUCCESS]]></result_code>  
12  <return_code><![CDATA[SUCCESS]]></return_code>  
13  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>  
14  <time_end><![CDATA[20140903131540]]></time_end>  
15  <total_fee>1</total_fee>  
16<coupon_fee><![CDATA[10]]></coupon_fee>  
17<coupon_count><![CDATA[1]]></coupon_count>  
18<coupon_type><![CDATA[CASH]]></coupon_type>  
19<coupon_id><![CDATA[10000]]></coupon_id>  
20<coupon_fee><![CDATA[100]]></coupon_fee>  
21  <trade_type><![CDATA[JSAPI]]></trade_type>  
22  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>  
23</xml>

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

application.properties

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

WechatController

 1package net.xdclass.xdvideo.controller;
 2
 3import net.xdclass.xdvideo.config.WeChatConfig;
 4import net.xdclass.xdvideo.domain.JsonData;
 5import net.xdclass.xdvideo.domain.User;
 6import net.xdclass.xdvideo.service.UserService;
 7import net.xdclass.xdvideo.utils.JwtUtils;
 8import net.xdclass.xdvideo.utils.WXPayUtil;
 9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.stereotype.Controller;
11import org.springframework.web.bind.annotation.GetMapping;
12import org.springframework.web.bind.annotation.RequestMapping;
13import org.springframework.web.bind.annotation.RequestParam;
14import org.springframework.web.bind.annotation.ResponseBody;
15
16import javax.servlet.http.HttpServletRequest;
17import javax.servlet.http.HttpServletResponse;
18import java.io.*;
19import java.net.URLEncoder;
20import java.util.Map;
21
22@Controller
23@RequestMapping("/api/v1/wechat")
24public class WechatController {
25
26    @Autowired
27    private WeChatConfig weChatConfig;
28
29    @Autowired
30    private UserService userService;
31
32    /**
33     * 拼装微信扫一扫登录url
34     * @return JsonData
35     */
36    @GetMapping("login_url")
37    @ResponseBody
38    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
39
40        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
41        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
42        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
43        return JsonData.buildSuccess(qrcodeUrl);
44    }
45
46
47    /**
48     * 获取用户code,保存 user,并且拼装请求,获取access_token
49     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
50     * 第三方服务器端,种cookie,再转发到用户当前页面地址
51     * @param code
52     * @param state  用户扫码登录时停留的页面
53     * @param response
54     */
55    @GetMapping("/user/callback")
56    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
57                                   String state, HttpServletResponse response) throws IOException {
58        User user = userService.saveWeChatUser(code);
59        if (user != null){
60            //生成jwt
61            String token = JwtUtils.geneJsonWebToken(user);
62
63            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
64            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
65        }
66    }
67
68
69    /**
70     * 微信支付回调: post方式   所以需要使用 @RequestMapping 
71     */
72    @RequestMapping("/order/callback")
73    public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
74
75        InputStream inputStream =  request.getInputStream();
76
77        //BufferedReader是包装设计模式,性能更搞
78        BufferedReader in =  new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
79        StringBuffer sb = new StringBuffer();
80        String line ;
81        while ((line = in.readLine()) != null){
82            sb.append(line);
83        }
84        in.close();
85        inputStream.close();
86        Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
87      
88
89        System.out.println(callbackMap.toString());
90    }
91
92}

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

 微信回调通知规则(通知频率为 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)

 1/**
 2     * 获取有序map
 3     * @param map
 4     * @return
 5     */
 6    public static SortedMap<String,String> getSortedMap(Map<String,String> map){
 7
 8        SortedMap<String, String> sortedMap = new TreeMap<>();
 9        Iterator<String> it =  map.keySet().iterator();
10        while (it.hasNext()){
11            String key  = (String)it.next();
12            String value = map.get(key);
13            String temp = "";
14            if( null != value){
15                temp = value.trim();
16            }
17            sortedMap.put(key,temp);
18        }
19        return sortedMap;
20    }

VideoOrderService

 1package net.xdclass.xdvideo.service;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import net.xdclass.xdvideo.dto.VideoOrderDto;
 5
 6/**
 7 * 订单接口
 8 */
 9public interface VideoOrderService {
10
11    /**
12     * 下单接口
13     * @param videoOrderDto
14     * @return
15     */
16    String save(VideoOrderDto videoOrderDto) throws Exception;
17
18    /**
19     * 根据流水号查找订单
20     * @param outTradeNo
21     * @return
22     */
23    VideoOrder findByOutTradeNo(String outTradeNo);
24
25
26    /**
27     * 根据流水号更新订单
28     * @param videoOrder
29     * @return
30     */
31    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
32}
  1package net.xdclass.xdvideo.service.impl;
  2
  3import net.xdclass.xdvideo.config.WeChatConfig;
  4import net.xdclass.xdvideo.domain.User;
  5import net.xdclass.xdvideo.domain.Video;
  6import net.xdclass.xdvideo.domain.VideoOrder;
  7import net.xdclass.xdvideo.dto.VideoOrderDto;
  8import net.xdclass.xdvideo.mapper.UserMapper;
  9import net.xdclass.xdvideo.mapper.VideoMapper;
 10import net.xdclass.xdvideo.mapper.VideoOrderMapper;
 11import net.xdclass.xdvideo.service.VideoOrderService;
 12import net.xdclass.xdvideo.utils.CommonUtils;
 13import net.xdclass.xdvideo.utils.HttpUtils;
 14import net.xdclass.xdvideo.utils.WXPayUtil;
 15import org.springframework.beans.factory.annotation.Autowired;
 16import org.springframework.stereotype.Service;
 17
 18import java.util.Date;
 19import java.util.Map;
 20import java.util.SortedMap;
 21import java.util.TreeMap;
 22
 23@Service
 24public class VideoOrderServiceImpl implements VideoOrderService {
 25
 26
 27    @Autowired
 28    private WeChatConfig weChatConfig;
 29
 30    @Autowired
 31    private VideoMapper videoMapper;
 32
 33    @Autowired
 34    private VideoOrderMapper videoOrderMapper;
 35
 36    @Autowired
 37    private UserMapper userMapper;
 38
 39    @Override
 40    public String save(VideoOrderDto videoOrderDto) throws Exception {
 41        //查找视频信息
 42        Video video =  videoMapper.findById(videoOrderDto.getVideoId());
 43
 44        //查找用户信息
 45        User user = userMapper.findByid(videoOrderDto.getUserId());
 46
 47
 48        //生成订单
 49        VideoOrder videoOrder = new VideoOrder();
 50        videoOrder.setTotalFee(video.getPrice());
 51        videoOrder.setVideoImg(video.getCoverImg());
 52        videoOrder.setVideoTitle(video.getTitle());
 53        videoOrder.setCreateTime(new Date());
 54        videoOrder.setVideoId(video.getId());
 55        videoOrder.setState(0);
 56        videoOrder.setUserId(user.getId());
 57        videoOrder.setHeadImg(user.getHeadImg());
 58        videoOrder.setNickname(user.getName());
 59
 60        videoOrder.setDel(0);
 61        videoOrder.setIp(videoOrderDto.getIp());
 62        videoOrder.setOutTradeNo(CommonUtils.generateUUID());
 63
 64        videoOrderMapper.insert(videoOrder);
 65
 66        //获取codeurl
 67        String codeUrl = unifiedOrder(videoOrder);
 68        return codeUrl;
 69
 70    }
 71
 72    @Override
 73    public VideoOrder findByOutTradeNo(String outTradeNo) {
 74
 75        return videoOrderMapper.findByOutTradeNo(outTradeNo);
 76    }
 77
 78    @Override
 79    public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
 80        return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
 81    }
 82
 83    /**
 84     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
 85     * @return
 86     */
 87    private String unifiedOrder(VideoOrder videoOrder) throws Exception {
 88
 89        //生成签名
 90        SortedMap<String,String> params = new TreeMap<>();
 91        params.put("appid",weChatConfig.getAppId());
 92        params.put("mch_id", weChatConfig.getMchId());
 93        params.put("nonce_str",CommonUtils.generateUUID());
 94        params.put("body",videoOrder.getVideoTitle());
 95        params.put("out_trade_no",videoOrder.getOutTradeNo());
 96        params.put("total_fee",videoOrder.getTotalFee().toString());
 97        params.put("spbill_create_ip",videoOrder.getIp());
 98        params.put("notify_url",weChatConfig.getPayCallbackUrl());
 99        params.put("trade_type","NATIVE");
100
101        //sign签名
102        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
103        params.put("sign",sign);
104
105        //map转xml
106        String payXml = WXPayUtil.mapToXml(params);
107
108        System.out.println(payXml);
109
110        //统一下单
111        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
112        if(null == orderStr) {
113            return null;
114        }
115
116
117        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
118        System.out.println(unifiedOrderMap.toString());
119        if(unifiedOrderMap != null) {
120            //获取二维码url
121            return unifiedOrderMap.get("code_url");
122        }
123
124        return "";
125    }
126}

VideoOrderMapper

 1package net.xdclass.xdvideo.mapper;
 2
 3import net.xdclass.xdvideo.domain.VideoOrder;
 4import org.apache.ibatis.annotations.*;
 5
 6import java.util.List;
 7
 8/**
 9 * 订单dao层
10 */
11public interface VideoOrderMapper {
12
13    /**
14     * 保存订单,返回包含主键
15     * @param videoOrder
16     * @return
17     */
18    @Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
19            " `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
20            " `video_img`, `user_id`, `ip`, `del`)" +
21            "VALUES" +
22            "(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
23            "#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
24    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
25    int insert(VideoOrder videoOrder);
26
27    /**
28     * 根据主键查找订单
29     * @param id
30     * @return
31     */
32    @Select("select * from video_order where id=#{order_id} and del=0")
33    VideoOrder findById(@Param("order_id") int id);
34
35    /**
36     * 根据交易订单号获取订单对象
37     * @param outTradeNo
38     * @return
39     */
40    @Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
41    VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);
42
43    /**
44     * 逻辑删除订单
45     * @param id
46     * @param userId
47     * @return
48     */
49    @Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
50    int del(@Param("id") int id, @Param("userId") int userId);
51
52    /**
53     * 查找我的全部订单
54     * @param userId
55     * @return
56     */
57    @Select("select * from video_order where user_id =#{userId}")
58    List<VideoOrder> findMyOrderList(int userId);
59
60    /**
61     * 根据微信回调订单流水号更新订单状态
62     * @param videoOrder
63     * @return
64     */
65    @Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
66            " where out_trade_no=#{outTradeNo} and state=0 and del=0")
67    int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
68
69}

WechatController

  1package net.xdclass.xdvideo.controller;
  2
  3import net.xdclass.xdvideo.config.WeChatConfig;
  4import net.xdclass.xdvideo.domain.JsonData;
  5import net.xdclass.xdvideo.domain.User;
  6import net.xdclass.xdvideo.domain.VideoOrder;
  7import net.xdclass.xdvideo.service.UserService;
  8import net.xdclass.xdvideo.service.VideoOrderService;
  9import net.xdclass.xdvideo.utils.JwtUtils;
 10import net.xdclass.xdvideo.utils.WXPayUtil;
 11import org.springframework.beans.factory.annotation.Autowired;
 12import org.springframework.stereotype.Controller;
 13import org.springframework.web.bind.annotation.GetMapping;
 14import org.springframework.web.bind.annotation.RequestMapping;
 15import org.springframework.web.bind.annotation.RequestParam;
 16import org.springframework.web.bind.annotation.ResponseBody;
 17
 18import javax.servlet.http.HttpServletRequest;
 19import javax.servlet.http.HttpServletResponse;
 20import java.io.*;
 21import java.net.URLEncoder;
 22import java.util.Date;
 23import java.util.Map;
 24import java.util.SortedMap;
 25
 26@Controller
 27@RequestMapping("/api/v1/wechat")
 28public class WechatController {
 29
 30    @Autowired
 31    private WeChatConfig weChatConfig;
 32
 33    @Autowired
 34    private UserService userService;
 35
 36    @Autowired
 37    private VideoOrderService videoOrderService;
 38
 39    /**
 40     * 拼装微信扫一扫登录url
 41     * @return JsonData
 42     */
 43    @GetMapping("login_url")
 44    @ResponseBody
 45    public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
 46
 47        String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
 48        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
 49        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
 50        return JsonData.buildSuccess(qrcodeUrl);
 51    }
 52
 53
 54    /**
 55     * 获取用户code,保存 user,并且拼装请求,获取access_token
 56     * 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
 57     * 第三方服务器端,种cookie,再转发到用户当前页面地址
 58     * @param code
 59     * @param state  用户扫码登录时停留的页面
 60     * @param response
 61     */
 62    @GetMapping("/user/callback")
 63    public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
 64                                   String state, HttpServletResponse response) throws IOException {
 65        User user = userService.saveWeChatUser(code);
 66        if (user != null){
 67            //生成jwt
 68            String token = JwtUtils.geneJsonWebToken(user);
 69
 70            // state 当前用户的页面地址,需要拼接 http://  这样才不会站内跳转
 71            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
 72        }
 73    }
 74
 75
 76    /**
 77     * 微信支付回调: post方式   所以需要使用 @RequestMapping
 78     */
 79    @RequestMapping("/order/callback")
 80    public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
 81
 82        InputStream inputStream =  request.getInputStream();
 83
 84        //BufferedReader是包装设计模式,性能更搞
 85        BufferedReader in =  new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
 86        StringBuffer sb = new StringBuffer();
 87        String line ;
 88        while ((line = in.readLine()) != null){
 89            sb.append(line);
 90        }
 91        in.close();
 92        inputStream.close();
 93        Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
 94        //返回给前端,告诉用户支付完成
 95        System.out.println(callbackMap.toString());
 96
 97        SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);
 98
 99        //判断签名是否正确
100        if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
101            //验证业务码状态是否为 SUCCESS
102            if("SUCCESS".equals(sortedMap.get("result_code"))){
103                //拿到流水号
104                String outTradeNo = sortedMap.get("out_trade_no");
105
106                //根据流水号查找订单
107                VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);
108
109                if(dbVideoOrder != null && dbVideoOrder.getState()==0){  //判断逻辑看业务场景
110                    VideoOrder videoOrder = new VideoOrder();
111                    videoOrder.setOpenid(sortedMap.get("openid"));
112                    videoOrder.setOutTradeNo(outTradeNo);
113                    videoOrder.setNotifyTime(new Date());
114                    videoOrder.setState(1);
115                    int rows = videoOrderService.updateVideoOderByOutTradeNo(videoOrder);
116
117                    //通知微信订单处理成功
118                    if(rows == 1){
119                        response.setContentType("text/xml");
120                        response.getWriter().println("success");
121                        return;
122                    }
123                }
124            }
125        }
126        //都处理失败
127        response.setContentType("text/xml");
128        response.getWriter().println("fail");
129    }
130}

测试

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

微信支付之下单事务处理

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

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

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

全局异常处理

XdException

 1package net.xdclass.xdvideo.exception;
 2
 3
 4/**
 5 * 自定义异常类
 6 */
 7public class XdException extends RuntimeException {
 8
 9
10    /**
11     * 状态码
12     */
13    private Integer code;
14    /**
15     * 异常消息
16     */
17    private String msg;
18
19    public XdException(int code, String msg){
20        super(msg);
21        this.code = code;
22        this.msg = msg;
23    }
24
25    public Integer getCode() {
26        return code;
27    }
28
29    public void setCode(Integer code) {
30        this.code = code;
31    }
32
33    public String getMsg() {
34        return msg;
35    }
36
37    public void setMsg(String msg) {
38        this.msg = msg;
39    }
40}

XdExceptionHandler

 1package net.xdclass.xdvideo.exception;
 2
 3
 4import net.xdclass.xdvideo.domain.JsonData;
 5import org.springframework.web.bind.annotation.ControllerAdvice;
 6import org.springframework.web.bind.annotation.ExceptionHandler;
 7import org.springframework.web.bind.annotation.ResponseBody;
 8
 9/**
10 * 异常处理控制器
11 */
12@ControllerAdvice
13public class XdExceptionHandler {
14
15    /**
16     * 这里可以捕获Exception  然后使用instanceof 配合多分支 if 进行 细力度异常的使用
17     * @param e
18     * @return
19     */
20    @ExceptionHandler(value = Exception.class)
21    @ResponseBody
22    public JsonData Handler(Exception e){
23
24        if(e instanceof XdException){
25            XdException xdException =  (XdException)e;
26            return JsonData.buildError(xdException.getMsg(),xdException.getCode());
27        }else{ 
28            return JsonData.buildError("全局异常,未知错误");
29        }
30    }
31
32}

使用 Logback 整合 SpringBoot 打点日志

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

logback-spring.xml

1

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

1app_log/log  
2 app.info.2020-02-19.log  
3 app.err.2020-02-19.log  
4 app.data.2020-02-19.log

service 层埋点
VideoOrderServiceImpl

  1package net.xdclass.xdvideo.service.impl;
  2
  3import net.xdclass.xdvideo.config.WeChatConfig;
  4import net.xdclass.xdvideo.domain.User;
  5import net.xdclass.xdvideo.domain.Video;
  6import net.xdclass.xdvideo.domain.VideoOrder;
  7import net.xdclass.xdvideo.dto.VideoOrderDto;
  8import net.xdclass.xdvideo.mapper.UserMapper;
  9import net.xdclass.xdvideo.mapper.VideoMapper;
 10import net.xdclass.xdvideo.mapper.VideoOrderMapper;
 11import net.xdclass.xdvideo.service.VideoOrderService;
 12import net.xdclass.xdvideo.utils.CommonUtils;
 13import net.xdclass.xdvideo.utils.HttpUtils;
 14import net.xdclass.xdvideo.utils.WXPayUtil;
 15import org.slf4j.Logger;
 16import org.slf4j.LoggerFactory;
 17import org.springframework.beans.factory.annotation.Autowired;
 18import org.springframework.stereotype.Service;
 19import org.springframework.transaction.annotation.Propagation;
 20import org.springframework.transaction.annotation.Transactional;
 21
 22import java.util.Date;
 23import java.util.Map;
 24import java.util.SortedMap;
 25import java.util.TreeMap;
 26
 27@Service
 28public class VideoOrderServiceImpl implements VideoOrderService {
 29
 30    private Logger logger = LoggerFactory.getLogger(this.getClass());
 31
 32    private Logger dataLogger = LoggerFactory.getLogger("dataLogger");
 33
 34    @Autowired
 35    private WeChatConfig weChatConfig;
 36
 37    @Autowired
 38    private VideoMapper videoMapper;
 39
 40    @Autowired
 41    private VideoOrderMapper videoOrderMapper;
 42
 43    @Autowired
 44    private UserMapper userMapper;
 45
 46    @Override
 47    @Transactional(propagation = Propagation.REQUIRED)
 48    public String save(VideoOrderDto videoOrderDto) throws Exception {
 49        //打点
 50        dataLogger.info("module=video_order`api=save`user_id={}`video_id={}",videoOrderDto.getUserId(),videoOrderDto.getVideoId());
 51
 52        //查找视频信息
 53        Video video =  videoMapper.findById(videoOrderDto.getVideoId());
 54
 55        //查找用户信息
 56        User user = userMapper.findByid(videoOrderDto.getUserId());
 57
 58
 59        //生成订单
 60        VideoOrder videoOrder = new VideoOrder();
 61        videoOrder.setTotalFee(video.getPrice());
 62        videoOrder.setVideoImg(video.getCoverImg());
 63        videoOrder.setVideoTitle(video.getTitle());
 64        videoOrder.setCreateTime(new Date());
 65        videoOrder.setVideoId(video.getId());
 66        videoOrder.setState(0);
 67        videoOrder.setUserId(user.getId());
 68        videoOrder.setHeadImg(user.getHeadImg());
 69        videoOrder.setNickname(user.getName());
 70
 71        videoOrder.setDel(0);
 72        videoOrder.setIp(videoOrderDto.getIp());
 73        videoOrder.setOutTradeNo(CommonUtils.generateUUID());
 74
 75        videoOrderMapper.insert(videoOrder);
 76
 77        //获取codeurl
 78        String codeUrl = unifiedOrder(videoOrder);
 79        return codeUrl;
 80
 81    }
 82
 83    @Override
 84    public VideoOrder findByOutTradeNo(String outTradeNo) {
 85
 86        return videoOrderMapper.findByOutTradeNo(outTradeNo);
 87    }
 88
 89    @Override
 90    public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
 91        return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
 92    }
 93
 94    /**
 95     * 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
 96     * @return
 97     */
 98    private String unifiedOrder(VideoOrder videoOrder) throws Exception {
 99
100        //生成签名
101        SortedMap<String,String> params = new TreeMap<>();
102        params.put("appid",weChatConfig.getAppId());
103        params.put("mch_id", weChatConfig.getMchId());
104        params.put("nonce_str",CommonUtils.generateUUID());
105        params.put("body",videoOrder.getVideoTitle());
106        params.put("out_trade_no",videoOrder.getOutTradeNo());
107        params.put("total_fee",videoOrder.getTotalFee().toString());
108        params.put("spbill_create_ip",videoOrder.getIp());
109        params.put("notify_url",weChatConfig.getPayCallbackUrl());
110        params.put("trade_type","NATIVE");
111
112        //sign签名
113        String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
114        params.put("sign",sign);
115
116        //map转xml
117        String payXml = WXPayUtil.mapToXml(params);
118
119        System.out.println(payXml);
120
121        //统一下单
122        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
123        if(null == orderStr) {
124            return null;
125        }
126
127
128        Map<String, String> unifiedOrderMap =  WXPayUtil.xmlToMap(orderStr);
129        System.out.println(unifiedOrderMap.toString());
130        if(unifiedOrderMap != null) {
131            //获取二维码url
132            return unifiedOrderMap.get("code_url");
133        }
134
135        return "";
136    }
137
138}

测试

1http://localhost:8081/api/v1/order/add?video_id=2
2
3查看日志文件 app.data.2020-02-19.log 内容
4
52020-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 注意点:假如接口逻辑报错,则跨域配置不生效,可以考虑做全局异常处理,这样即使接口运行出错,跨域也依然生效

 1package net.xdclass.xdvideo.config;
 2
 3import org.springframework.context.annotation.Configuration;
 4import org.springframework.web.servlet.config.annotation.CorsRegistry;
 5import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 6
 7@Configuration
 8public class Cors extends WebMvcConfigurerAdapter {
 9
10    @Override
11    public void addCorsMappings(CorsRegistry registry) {
12        registry.addMapping("/**")指定为线上的静态资源的域
13                .allowedOrigins("*")
14                .allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")
15                .allowCredentials(true).maxAge(3600);
16    }
17
18}

作者:Soulboy