Online Education Website
中大型公司项目开发流程
需求评审(产品/设计/前端/后端/测试/运营)
->UI 设计
-> 开发(前端架构-> 开发/ 后端架构-> 开发)
-> 前端后端联调
-> 项目提测
->BugFix
-> 回归测试
-> 运维和开发部署上线
-> 灰度发布
-> 全量发布
-> 维护和运营
需求分析和架构设计
功能需求
- 首页视频列表
- 视频详情 (自己开发)
- 微信扫码登录
- 下单微信支付
- 我的订单列表 (自己开发)
架构设计
- 前端后端分离 -> 方案:node 渲染
- 动静分离 -> 方案:静态资源如 HTML,js 放在 cdn 或者 nginx 服务器上
- 后端技术选择:IDEA + Springboot2.0 + redis4.0+ HttpClient + MySQL + ActiveMQ 消息队列
- 前端技术选择:HTML5 + bootstrapt + jQuery
- 测试要求:首页和视频详情页 qps 单机 qps 要求 2000+
数据库设计
实体对象:矩形
属性:椭圆
关系:菱形
实体列表
video
video_order
user
comment
chapter
episode
数据库初始化脚本
# ************************************************************
# Sequel Pro SQL dump
# Version 4499
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 119.23.28.97 (MySQL 5.7.22)
# Database: xdclass
# Generation Time: 2018-06-22 15:04:45 +0000
# ************************************************************
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
# Dump of table chapter
# ------------------------------------------------------------
DROP TABLE IF EXISTS `chapter`;
CREATE TABLE `chapter` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`video_id` int(11) DEFAULT NULL COMMENT '视频主键',
`title` varchar(128) DEFAULT NULL COMMENT '章节名称',
`ordered` int(11) DEFAULT NULL COMMENT '章节顺序',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
# Dump of table comment
# ------------------------------------------------------------
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`content` varchar(256) DEFAULT NULL COMMENT '内容',
`user_id` int(11) DEFAULT NULL,
`head_img` varchar(128) DEFAULT NULL COMMENT '用户头像',
`name` varchar(128) DEFAULT NULL COMMENT '昵称',
`point` double(5,2) DEFAULT NULL COMMENT '评分,10分满分',
`up` int(11) DEFAULT NULL COMMENT '点赞数',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`order_id` int(11) DEFAULT NULL COMMENT '订单id',
`video_id` int(11) DEFAULT NULL COMMENT '视频id',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
# Dump of table episode
# ------------------------------------------------------------
DROP TABLE IF EXISTS `episode`;
CREATE TABLE `episode` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(524) DEFAULT NULL COMMENT '集标题',
`num` int(10) DEFAULT NULL COMMENT '第几集',
`duration` varchar(64) DEFAULT NULL COMMENT '时长 分钟,单位',
`cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
`video_id` int(10) DEFAULT NULL COMMENT '视频id',
`summary` varchar(256) DEFAULT NULL COMMENT '集概述',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`chapter_id` int(11) DEFAULT NULL COMMENT '章节主键id',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
# Dump of table user
# ------------------------------------------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`openid` varchar(128) DEFAULT NULL COMMENT '微信openid',
`name` varchar(128) DEFAULT NULL COMMENT '昵称',
`head_img` varchar(524) DEFAULT NULL COMMENT '头像',
`phone` varchar(64) DEFAULT '' COMMENT '手机号',
`sign` varchar(524) DEFAULT '全栈工程师' COMMENT '用户签名',
`sex` tinyint(2) DEFAULT '-1' COMMENT '0表示女,1表示男',
`city` varchar(64) DEFAULT NULL COMMENT '城市',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# Dump of table video
# ------------------------------------------------------------
DROP TABLE IF EXISTS `video`;
CREATE TABLE `video` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(524) DEFAULT NULL COMMENT '视频标题',
`summary` varchar(1026) DEFAULT NULL COMMENT '概述',
`cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
`view_num` int(10) DEFAULT '0' COMMENT '观看数',
`price` int(11) DEFAULT NULL COMMENT '价格,分',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`online` int(5) DEFAULT '0' COMMENT '0表示未上线,1表示上线',
`point` double(11,2) DEFAULT '8.70' COMMENT '默认8.7,最高10分',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `video` WRITE;
/*!40000 ALTER TABLE `video` DISABLE KEYS */;
INSERT INTO `video` (`id`, `title`, `summary`, `cover_img`, `view_num`, `price`, `create_time`, `online`, `point`)
VALUES
(1,'SpringBoot+Maven整合Websocket课程','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',12,1000,NULL,0,8.70),
(2,'2018年 6.2新版本ELK ElasticSearch ','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',43,500,NULL,0,9.70),
(3,'JMeter接口测试入门到实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',53,123,NULL,0,8.70),
(4,'Spring Boot2.x零基础入门到高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',23,199,NULL,0,6.20),
(5,'亿级流量处理搜索','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',64,10,NULL,0,9.10),
(6,'reidis消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',12,10,NULL,0,6.70),
(7,'谷歌面试题','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',52,23,NULL,0,5.10),
(8,'js高级前端视频','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',54,442,NULL,0,8.70),
(9,'List消息队列高级实战','这是概要','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',13,32,NULL,0,4.30);
/*!40000 ALTER TABLE `video` ENABLE KEYS */;
UNLOCK TABLES;
# Dump of table video_order
# ------------------------------------------------------------
DROP TABLE IF EXISTS `video_order`;
CREATE TABLE `video_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`openid` varchar(32) DEFAULT NULL COMMENT '用户标示',
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单唯一标识',
`state` int(11) DEFAULT NULL COMMENT '0表示未支付,1表示已支付',
`create_time` datetime DEFAULT NULL COMMENT '订单生成时间',
`notify_time` datetime DEFAULT NULL COMMENT '支付回调时间',
`total_fee` int(11) DEFAULT NULL COMMENT '支付金额,单位分',
`nickname` varchar(32) DEFAULT NULL COMMENT '微信昵称',
`head_img` varchar(128) DEFAULT NULL COMMENT '微信头像',
`video_id` int(11) DEFAULT NULL COMMENT '视频主键',
`video_title` varchar(128) DEFAULT NULL COMMENT '视频名称',
`video_img` varchar(256) DEFAULT NULL COMMENT '视频图片',
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`ip` varchar(64) DEFAULT NULL COMMENT '用户ip地址',
`del` int(5) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `video_order` WRITE;
/*!40000 ALTER TABLE `video_order` DISABLE KEYS */;
INSERT INTO `video_order` (`id`, `openid`, `out_trade_no`, `state`, `create_time`, `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`, `video_img`, `user_id`, `ip`, `del`)
VALUES
(1,'werwewfwe','dasfweqdqf',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小D','xxx',1,'SpringBoot视频','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',1,'192.154.2.32',0),
(2,'3452333','gasdfdf',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小X','xxx',2,'2018年 6.2新版本ELK ElasticSearch ','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',2,'192.154.2.32',0),
(3,'sfsd','432werew',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小C','xxx',3,'JMeter接口测试入门到实战','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',3,'192.154.2.32',0),
(4,'werqwe','3432',1,'2018-07-12 00:00:00','2018-07-12 00:00:00',12,'小D','xxx',2,'2018年 6.2新版本ELK ElasticSearch ','https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png',1,'192.154.2.32',0);
/*!40000 ALTER TABLE `video_order` ENABLE KEYS */;
UNLOCK TABLES;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
字段冗余
可以有效避免关联查询时所引起的目标表的访问量较高。
热部署
项目分层分包及资源文件处理
1、基本目录结构
controller
service
impl
mapper
utils
domain
config
intercepter
dto
2、application.properties配置文件
配置启动端口
server.port=8082
开源工具的优缺点选择和抽象方法的建议
1、开源工具
好处:开发方便,使用简单,使用aop方式进行分页,只需要引入相关依赖,然后PageHelper.startPage(page, size); 开启分页
弊端:对于分库分表等情况下使用有问题,深度分页逻辑判断会复杂
mysql资料:
深度分页常用案例:
https://www.cnblogs.com/lpfuture/p/5772055.html
https://blog.csdn.net/li772030428/article/details/52839987
推荐书籍:
https://book.douban.com/subject/23008813/
2、封装的好坏
关于抽象和不抽象的选择,比如tk这些工具,通用mapper,service,controller
好处:
代码量大大减少,开发新模块可以马上进行使用
弊端:
对应过度封装,新手等比较难理解,不能保证团队里面所有人都有对应的水平,或者有高度封装的思想,也不是过度封装
3、课程案例:
分页采用pageHelper
封装通用工具类,如缓存操作等
利于解耦,如切换缓存框架
IDEA 自带工具逆向生成 POJO
1、IDEA连接数据库
菜单View→Tool Windows→Database打开数据库工具窗口
2、左上角添加按钮“+”,选择数据库类型
3、mysql主机,账户密码
4、通过IDEA生成实体类
选中一张表,右键--->Scripted Extensions--->选择Generate POJOS.clj或者Generate POJOS.groovy,选择需要存放的路径,完成
5、自定义Scripted Extensions ---> GO to Script Directory ---> extensions ---> extensions ---> schema ---> Generate POJOs.groovy
自定义包名 net.xdclass.xdvideo.domain
常用类型java.util.Date
配置文件自动映射到属性和实体类配置
application.properties
#应用启动端口设置
server.port=8081
#=================================微信相关==================
# 公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=554801238f17fdsdsdd6f96b382fe548215e9
WeChatConfig
package net.xdclass.xdvideo.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* 微信配置类
*/
@Configuration
@PropertySource(value = "classpath:application.properties")
@Data
public class WeChatConfig {
/**
* 公众号appid
*/
@Value("${wxpay.appid}")
private String appId;
/**
* 公众号密钥
*/
@Value("${wxpay.appsecret}")
private String appsecret;
}
整合 MyBatis 访问数据库和阿里巴巴数据源
引入依赖
<!-- mybatis起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- MySQL的JDBC驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
配置文件中加入数据源相关的配置
application.properties
#应用启动端口设置
server.port=8081
#=================================微信相关==================
# 公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=554801238f17fdsdsdd6f96b382fe548215e9
#=================================数据库相关================
#可以自动识别
#spring.datasource.driver-class-name =com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.201:3306/xdclass?useUnicode=true&characterEncoding=utf-8
spring.datasource.username =root
spring.datasource.password =123456
#如果不使用默认的数据源 (com.zaxxer.hikari.HikariDataSource)
spring.datasource.type =com.alibaba.druid.pool.DruidDataSource
#=================================mybatis相关================
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#增加打印sql语句,一般用于本地开发测试
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
启动类添加 Mapper 扫描
package net.xdclass.xdvideo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("net.xdclass.xdvideo.mapper")
public class XdvideoApplication {
public static void main(String[] args) {
SpringApplication.run(XdvideoApplication.class, args);
}
}
mapper 类(测试)
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface VideoMapper {
@Select("select * from video")
List<Video> findAll();
}
controller(测试)
VideoController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.mapper.VideoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class VideoController {
@Autowired
private VideoMapper videoMapper;
@RequestMapping("test_db")
public Object testDB(){
System.out.println(weChatConfig.getAppId()); //wx5beac15ca207cdd40c
return videoMapper.findAll();
}
}
视频列表 CRUD
VideoMapper
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface VideoMapper {
@Select("select * from video")
List<Video> findAll();
@Select("SELECT * FROM video WHERE id = #{id}")
Video findById(int id);
@Update("UPDATE video SET title=#{title} WHERE id =#{id}")
int update(Video video);
@Delete("DELETE FROM video WHERE id =#{id}")
int delete(int id);
@Insert("INSERT INTO `video` ( `title`, `summary`, " +
"`cover_img`, `view_num`, `price`, `create_time`," +
" `online`, `point`)" +
"VALUES" +
"(#{title}, #{summary}, #{coverImg}, #{viewNum}, #{price},#{createTime}" +
",#{online},#{point});")
//保存对象,获取数据库自增id
@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
int save(Video video);
}
VideoService
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.Video;
import java.util.List;
public interface VideoService {
List<Video> findAll();
Video findById(int id);
int update(Video video);
int delete(int id);
int save(Video video);
}
VideoServiceImpl
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Howard
*/
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideoMapper videoMapper;
@Override
public List<Video> findAll() {
return videoMapper.findAll();
}
@Override
public Video findById(int id) {
return videoMapper.findById(id);
}
@Override
public int update(Video video) {
return videoMapper.update(video);
}
@Override
public int delete(int id) {
return videoMapper.delete(id);
}
@Override
public int save(Video video) {
int rows = videoMapper.save(video);
System.out.println("保存对象的id= "+video.getId());
return rows;
}
}
VideoController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @RequestBody 请求体映射实体类 : 需要指定http头为 content-type为application/json charset=utf-8
*/
@RestController
@RequestMapping("/api/v1/video")
public class VideoController {
@Autowired
private VideoService videoService;
/**
* 分页接口
* @param page 当前第几页,默认是第一页
* @param size 每页显示几条
* @return
*/
@GetMapping("page")
public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
@RequestParam(value = "size",defaultValue = "10")int size){
return videoService.findAll();
}
/**
* 根据Id找视频
* @param videoId
* @return
*/
@GetMapping("find_by_id")
public Object findById(@RequestParam(value = "video_id",required = true)int videoId){
return videoService.findById(videoId);
}
}
VideoAdminController
package net.xdclass.xdvideo.controller.admin;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin/api/v1/video")
public class VideoAdminController {
@Autowired
private VideoService videoService;
/**
* 根据id删除视频
* @param videoId
* @return
*/
@DeleteMapping("del_by_id")
public Object delById(@RequestParam(value = "video_id",required = true)int videoId){
return videoService.delete(videoId);
}
/**
* 根据id更新视频
* @param video
* @return
*/
@PutMapping("update_by_id")
public Object update(@RequestBody Video video){
return videoService.update(video);
}
/**
* 保存视频对象
* @param video
* @return
*/
@PostMapping("save")
public Object save(@RequestBody Video video){
return videoService.save(video);
}
}
动态 SQL 语句 Mybaties SqlProvider
只更新有值的属性,忽略空值属性。Reference
@UpdateProvider(type=VideoSqlProvider.class,method="updateVideo") 更新
@InsertProvider 插入
@DeleteProvider 删除
@SelectProvider 查询
VideoProvider
package net.xdclass.xdvideo.provider;
import net.xdclass.xdvideo.domain.Video;
import org.apache.ibatis.jdbc.SQL;
/**
* video构建动态sql语句
*/
public class VideoProvider {
/**
* 更新video动态语句
* @param video
* @return
*/
public String updateVideo(final Video video){
return new SQL(){{
UPDATE("video");
//条件写法.
if(video.getTitle()!=null){
SET("title=#{title}");
}
if(video.getSummary()!=null){
SET("summary=#{summary}");
}
if(video.getCoverImg()!=null){
SET("cover_img=#{coverImg}");
}
if(video.getViewNum()!=null){
SET("view_num=#{viewNum}");
}
if(video.getPrice()!=null){
SET("price=#{price}");
}
if(video.getOnline()!=null){
SET("online=#{online}");
}
if(video.getPoint()!=null){
SET("point=#{point}");
}
WHERE("id=#{id}");
}}.toString();
}
}
修改 VideoMapper 中的 update 方法(通过 Provider 实现动态 SQL)
//@Update("UPDATE video SET title=#{title} WHERE id =#{id}")
@UpdateProvider(type = VideoProvider.class,method = "updateVideo")
int update(Video video);
PageHelper 分页插件使用
原理
引入依赖
<!-- 分页插件依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.2.0</version>
</dependency>
MyBatisConfig
package net.xdclass.xdvideo.config;
import com.github.pagehelper.PageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* mybatis分页插件配置
*/
@Configuration
public class MyBatisConfig {
@Bean
public PageHelper pageHelper(){
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
// 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用
p.setProperty("offsetAsPageNum","true");
//设置为true时,使用RowBounds分页会进行count查询
p.setProperty("rowBoundsWithCount","true");
p.setProperty("reasonable","true");
pageHelper.setProperties(p);
return pageHelper;
}
}
使用 PageHelper 进行分页
/**
* 分页接口
* @param page 当前第几页,默认是第一页
* @param size 每页显示几条
* @return
*/
@GetMapping("page")
public Object pageVideo(@RequestParam(value = "page",defaultValue = "1")int page,
@RequestParam(value = "size",defaultValue = "10")int size){
PageHelper.startPage(page,size);
List<Video> list = videoService.findAll();
PageInfo<Video> pageInfo = new PageInfo<>(list);
Map<String,Object> data = new HashMap<>();
data.put("total_size",pageInfo.getTotal());//总条数
data.put("total_page",pageInfo.getPages());//总页数
data.put("current_page",page);//当前页T
data.put("data",pageInfo.getList());//数据
return data;
}
测试
//http://localhost:8081/api/v1/video/page?size=3
{
"data": [
{
"id": 2,
"title": "葵花宝典",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 43,
"price": 500,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 9.7
},
{
"id": 3,
"title": "JMeter接口测试入门到实战",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 53,
"price": 123,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 8.7
},
{
"id": 4,
"title": "Spring Boot2.x零基础入门到高级实战",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 23,
"price": 199,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 6.2
}
],
"total_size": 9,
"total_page": 3,
"current_page": 1
}
PageInfo 属性分析
{
"pageNum": 1,
"pageSize": 3,
"size": 3,
"orderBy": null,
"startRow": 1,
"endRow": 3,
"total": 9,
"pages": 3,
"list": [
{
"id": 2,
"title": "葵花宝典",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 43,
"price": 500,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 9.7
},
{
"id": 3,
"title": "JMeter接口测试入门到实战",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 53,
"price": 123,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 8.7
},
{
"id": 4,
"title": "Spring Boot2.x零基础入门到高级实战",
"summary": "这是概要",
"coverImg": "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/upload/video/video_cover.png",
"viewNum": 23,
"price": 199,
"createTime": "2019-11-09T16:00:00.000+0000",
"online": 0,
"point": 6.2
}
],
"prePage": 0,
"nextPage": 2,
"isFirstPage": true,
"isLastPage": false,
"hasPreviousPage": false,
"hasNextPage": true,
"navigatePages": 8,
"navigatepageNums": [
1,
2,
3
],
"navigateFirstPage": 1,
"navigateLastPage": 3,
"firstPage": 1,
"lastPage": 3
}
单机和分布式应用下登录校验
单机 Tomcat 应用登录检验
sesssion保存在浏览器和应用服务器会话之间
用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,
客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId
分布式应用中 session 共享
真实的应用不可能单节点部署,所以就有个多节点登录 session 共享的问题需要解决
1)tomcat支持session共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐
2)使用redis存储token:
服务端使用UUID生成随机64位或者128位token,放入redis中,然后返回给客户端并存储在cookie中
用户每次访问都携带此token,服务端去redis中校验是否有此用户即可
JWT JSON wen token 通过加解密算法生成 token 校验
JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。
JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息
JWT 登录校验(封装通用方法)
简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息,格式如下:
{
id:888,
name:'小D',
expire:10000
}
加密
funtion 加密(object, appsecret){
xxxx
return base64( token);
}
解密
function 解密(token ,appsecret){
xxxx
//成功返回true,失败返回false
}
优点:
1)生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
2)存储在客户端,不占用服务端的内存资源
缺点:
token是经过base64编码,所以客户端可以使用base64进行解码(获取明文),因此token加密前的对象不应该包含敏感信息,如用户权限,密码等。
2、JWT格式组成 头部、负载、签名
header+payload+signature
头部:主要是描述签名算法
负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
3、关于jwt客户端存储
可以存储在cookie,localstorage和sessionStorage(关闭浏览器之后就需要重新登录)里
引入依赖
```xml
<!-- JWT引来 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
JwtUtils
package net.xdclass.xdvideo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import net.xdclass.xdvideo.domain.User;
import java.util.Date;
/**
* jwt工具类
*/
public class JwtUtils {
//主题
public static final String SUBJECT = "xdclass";
//过期时间
public static final long EXPIRE = 1000*60*60*24*7; //过期时间,毫秒,一周
//秘钥
public static final String APPSECRET = "xd666";
/**
* 生成jwt
* @param user
* @return
*/
public static String geneJsonWebToken(User user){
if(user == null || user.getId() == null || user.getName() == null
|| user.getHeadImg()==null){
return null;
}
String token = Jwts.builder().setSubject(SUBJECT)
.claim("id",user.getId())
.claim("name",user.getName())
.claim("img",user.getHeadImg())
.setIssuedAt(new Date()) //发行时间
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE)) //期满时间
.signWith(SignatureAlgorithm.HS256,APPSECRET).compact();
return token;
}
/**
* 校验token
* @param token
* @return
*/
public static Claims checkJWT(String token ){
try{
final Claims claims = Jwts.parser().setSigningKey(APPSECRET).
parseClaimsJws(token).getBody();
return claims;
}catch (Exception e){ } //解密失败或者过期都会抛异常
return null;
}
}
测试(模拟登录认证)
package net.xdclass.xdvideo;
import io.jsonwebtoken.Claims;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.junit.Test;
public class CommonTest {
/**
* 加密测试,生成token
*/
@Test
public void testGeneJwt(){
//已获取用户信息,查询数据库验证登录信息是否正确
User user = new User();
user.setId(999);
user.setHeadImg("www.xdclass.net");
user.setName("xd");
String token = JwtUtils.geneJsonWebToken(user);
System.out.println(token);
}
/**
* 解密测试,解密token,验证合法性
*/
@Test
public void testCheck(){
//正确token
//String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";
//模拟被修改过的token
String token = "2eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaWQiOjk5OSwibmFtZSI6InhkIiwiaW1nIjoid3d3LnhkY2xhc3MubmV0IiwiaWF0IjoxNTgxNjYxNzczLCJleHAiOjE1ODIyNjY1NzN9.mdeKcCeH02-O2t3_KPtCg4AeqV9GC8WkHBqdBJJ7k-s";
Claims claims = JwtUtils.checkJWT(token);
if(claims != null){
String name = (String)claims.get("name");
String img = (String)claims.get("img");
int id =(Integer) claims.get("id");
System.out.println(name);
System.out.println(img);
System.out.println(id);
}else{
System.out.println("非法token");
}
}
}
登录方式的优缺点
手机号或者邮箱注册
优点
- 企业获取了用户的基本资料信息,利于后续业务发展:推送营销类信息
- 用户可以用个手机号或者邮箱获取对应的 app 福利:注册送优惠券
- 反馈信息的时候方便,直接报手机号即可:账户出问题,被盗等
缺点
- 步骤多
- 如果站点不安全,如站点被攻击,泄漏了个人信息,如手机号,密码等
- 少量不良企业贩卖个人信息,如手机号
OAuth2.0 一键授权登录
优点
- 使用快捷,用户体验好,数据相对安全
缺点
- 反馈问题麻烦,比较难知道唯一标识 openid
- 如果是企业下面有多个应用,其中有应用不支持 Auth2.0 登录,则没法做到用户信息打通,积分不能复用等…… 如 app 接入了微信授权登录,但是网站没有,则打不通,或者授权方只提供了一种终端授权,则信息无法打通,
选择方式
- 看企业和实际业务情况
- 务必区分,普通密码和核心密码
微信开放平台简介
微信开放平台介绍(申请里面的网站应用需要企业资料)
微信开放平台
什么是 appid、appsecret、授权码 code
- appid:应用的唯一 id
- appsecret:应用的唯一密钥
- 授权码:应用发起扫码授权(A->B 发起授权,想获取授权用户信息,那 a 必须携带授权码,才可以向 B 获取授权信息)
- 授权回调域:xdclass.net/api/v1/wechat/callback?code=23&xxxxx
微信 Oauth2.0 交互流程
通过 access_token 可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。
第一步:请求 CODE
扫码 URL 示例
https://open.weixin.qq.com/connect/qrconnect?appid=wx25bc3c42ddcc664f&redirect_uri=https://api.xdclass.net/pub/api/v1/wechat/user/callback1&response_type=code&scope=snsapi_login&state=https://xdclass.net/#/?login_type=pc#wechat_redirect
返回说明
用户允许授权后,将会重定向到 redirect_uri 的网址上,并且带上 code 和 state 参数
redirect_uri?code=CODE&state=STATE
第二步:通过 code 获取 access_token
通过 code 获取 access_token
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 应用唯一标识,在微信开放平台提交应用审核通过后获得 |
secret | 是 | 应用密钥 AppSecret,在微信开放平台提交应用审核通过后获得 |
code | 是 | 填写第一步获取的 code 参数 |
grant_type | 是 | 填 authorization_code |
返回说明
正确的返回:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
第三步:通过 access_token 调用接口
获取 access_token 后,进行接口调用,有以下前提:
1. access_token有效且未超时;
2. 微信用户已授权给第三方应用帐号相应接口作用域(scope)。
对于接口作用域(scope),能调用的接口有以下:
授权作用域(scope) | 接口 | 接口说明 |
---|---|---|
snsapi_base | /sns/oauth2/access_token | 通过 code 换取 access_token、refresh_token 和已授权 scope |
snsapi_base | /sns/oauth2/refresh_token | 刷新或续期 access_token 使用 |
snsapi_base | /sns/auth | 检查 access_token 有效性 |
snsapi_userinfo | /sns/userinfo | 获取用户个人信息 |
获取用户个人信息(UnionID 机制)
接口说明
此接口用于获取用户个人信息。开发者可通过 OpenID 来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的 unionid 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的。请注意,在用户修改微信头像后,旧的微信头像 URL 将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像 URL 失效后的异常情况。
请求说明
http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 调用凭证 |
openid | 是 | 普通用户的标识,对当前开发者帐号唯一 |
lang | 否 | 国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为 zh-CN |
返回说明
正确的 JSON 返回结果:
{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
调用频率限制
接口名 | 频率限制 |
---|---|
通过 code 换取 access_token | 1 万/分钟 |
刷新 access_token | 5 万/分钟 |
获取用户基本信息 | 5 万/分钟 |
URL 获取
1、增加结果工具类,JsonData; 增加 application.properties 配置
微信开放平台二维码连接
2、拼接 url
增加结果工具类
JsonData
package net.xdclass.xdvideo.domain;
import java.io.Serializable;
/**
* 功能描述:工具类
*
* <p> 创建时间:May 14, 2018 7:58:06 PM </p>
*/
public class JsonData implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer code; // 状态码 0 表示成功,1表示处理中,-1表示失败
private Object data; // 数据
private String msg;// 描述
public JsonData() {
}
public JsonData(Integer code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
// 成功,传入数据
public static JsonData buildSuccess() {
return new JsonData(0, null, null);
}
// 成功,传入数据
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data, null);
}
// 失败,传入描述信息
public static JsonData buildError(String msg) {
return new JsonData(-1, null, msg);
}
// 失败,传入描述信息,状态码
public static JsonData buildError(String msg, Integer code) {
return new JsonData(code, null, msg);
}
// 成功,传入数据,及描述信息
public static JsonData buildSuccess(Object data, String msg) {
return new JsonData(0, data, msg);
}
// 成功,传入数据,及状态码
public static JsonData buildSuccess(Object data, int code) {
return new JsonData(code, data, null);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
return "JsonData [code=" + code + ", data=" + data + ", msg=" + msg
+ "]";
}
}
测试
//http://localhost:8081/api/v1/video/test_config
{
"code": 0,
"data": "wx5beac15ca207cdd40c",
"msg": null
}
application.properties
#=================================微信相关==================
#公众号
wxpay.appid=wx5beac15ca207cdd40c
wxpay.appsecret=5548012f33417fdsdsdd6f96b382fe548215e9
#微信开放平台配置
wxopen.appid=wx025575eac69a2d5b
wxopen.appsecret=f5b6730c592ac15b8b1a5aeb8948a9f3
#重定向url
wxopen.redirect_url=http://16webtest.ngrok.xiaomiqiu.cn
WeChatConfig
WechatController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
/**
* 拼装微信扫一扫登录url
* @return
*/
@GetMapping("login_url")
@ResponseBody
public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
return JsonData.buildSuccess(qrcodeUrl);
}
}
测试(获取二维码 URI)
//http://localhost:8081/api/v1/wechat/login_url?access_page=www.xdclass.net
{
"code": 0,
"data": "https://open.weixin.qq.com/connect/qrconnect?appid=wx025575eac69a2d5b&redirect_uri=http://16webtest.ngrok.xiaomiqiu.cn&response_type=code&scope=snsapi_login&state=www.xdclass.net#wechat_redirect",
"msg": null
}
//从返回的JSON中可以提取二维码URI
https://open.weixin.qq.com/connect/qrconnect?appid=wx025575eac69a2d5b&redirect_uri=http://16webtest.ngrok.xiaomiqiu.cn&response_type=code&scope=snsapi_login&state=www.xdclass.net#wechat_redirect
HttpClient 根据授权回调获取 access_token
思路整理
第五步需要使用 HttpClient 拼装 code + appid + appsecret,向微信发起请求。
1. 用户请求第三方平台,第三方平台发起微信开放平台二维码连接(appid + callbackUrl + accessPage)
2.第三方平台收到微信返回的 JSON数据,并从中提取出data(二维码连接),返回前端,前端通过js发起新的请求,获取二维码图片。
3.微信用户扫码并且确认。
4.微信重定向到第三方平台,并且带上授权临时票据(code)
5.第三方平台通过 code + appid + appsecret 向微信发起请求,换取access_token
6.微信返回access_token
引入依赖
<!-- httpclient 相关依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
<!-- gson工具,封装http的时候使用 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
HTTP 工具类(封装 get post 请求)
package net.xdclass.xdvideo.utils;
import com.google.gson.Gson;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 封装http get post
*/
public class HttpUtils {
private static final Gson gson = new Gson();
/**
* get方法
* @param url
* @return
*/
public static Map<String,Object> doGet(String url){
Map<String,Object> map = new HashMap<>();
CloseableHttpClient httpClient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000) //连接超时
.setConnectionRequestTimeout(5000)//请求超时
.setSocketTimeout(5000)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);
try{
HttpResponse httpResponse = httpClient.execute(httpGet);
if(httpResponse.getStatusLine().getStatusCode() == 200){
//将Entity转为字符串
String jsonResult = EntityUtils.toString( httpResponse.getEntity());
//将字符串转为 Map<String,Object>
map = gson.fromJson(jsonResult,map.getClass());
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return map;
}
/**
* 封装post
* @return
*/
public static String doPost(String url, String data,int timeout){
CloseableHttpClient httpClient = HttpClients.createDefault();
//超时设置
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(timeout) //连接超时
.setConnectionRequestTimeout(timeout)//请求超时
.setSocketTimeout(timeout)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.addHeader("Content-Type","text/html; chartset=UTF-8");
if(data != null && data instanceof String){ //使用字符串传参
StringEntity stringEntity = new StringEntity(data,"UTF-8");
httpPost.setEntity(stringEntity);
}
try{
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String result = EntityUtils.toString(httpEntity);
return result;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
开发授权回调接口接收微信重定向数据
前置条件:ngrok 映射回调域名到本机
返回说明
用户允许授权后,将会重定向到 redirect_uri 的网址上,并且带上 code 和 state 参数
redirect_uri?code=CODE&state=STATE
&emsp 若用户禁止授权,则重定向后不会带上 code 参数,仅会带上 state 参数
redirect_uri?state=STATE
WechatController 新增 Mapping /user/callback
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private UserService userService;
/**
* 拼装微信扫一扫登录url
* @return
*/
@GetMapping("login_url")
@ResponseBody
public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
return JsonData.buildSuccess(qrcodeUrl);
}
/**
* 保存微信 code ,并且拼装请求,获取access_token
* @param code
* @param state
* @param response
*/
@GetMapping("/user/callback")
public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
String state, HttpServletResponse response){
User user = userService.saveWeChatUser(code);
if (user) {
//生成jwt
}
}
}
WeChatConfig 新增
- 开放平台获取 access_token 地址
- 获取用户信息
返回说明
正确的返回:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
参数说明
参数 | 说明 |
---|---|
access_token | 接口调用凭证 |
expires_in | access_token 接口调用凭证超时时间,单位(秒) |
refresh_token | 用户刷新 access_token |
openid | 授权用户唯一标识 |
scope | 用户授权的作用域,使用逗号(,)分隔 |
unionid | 当且仅当该网站应用已获得该用户的 userinfo 授权时,才会出现该字段。 |
UserService
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.User;
/**
*用户业务接口类
*/
public interface UserService {
User saveWeChatUser(String code);
}
获取用户个人信息(UnionID 机制)
通过 access_token 获取微信用户头像和昵称等基本信息
接口说明
此接口用于获取用户个人信息。开发者可通过 OpenID 来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的 unionid 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的。请注意,在用户修改微信头像后,旧的微信头像 URL 将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像 URL 失效后的异常情况。
请求说明
http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 调用凭证 |
openid | 是 | 普通用户的标识,对当前开发者帐号唯一 |
lang | 否 | 国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为 zh-CN |
UserServiceImpl
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.HttpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private UserMapper userMapper;
@Override
public User saveWeChatUser(String code) {
//获取token:拼装请求 OpenAppid + OpenAppsecret + code(临时票据)
String accessTokenUrl = String.format(WeChatConfig.getOpenAccessTokenUrl(),weChatConfig.getOpenAppid(),weChatConfig.getOpenAppsecret(),code);
//从微信回复的数据中提取: access_token + openid
Map<String ,Object> baseMap = HttpUtils.doGet(accessTokenUrl);
if(baseMap == null || baseMap.isEmpty()){ return null; }
String accessToken = (String)baseMap.get("access_token");
String openId = (String) baseMap.get("openid");
//先查询本地数据库
User dbUser = userMapper.findByopenid(openId);
if(dbUser!=null) { //更新用户,直接返回
return dbUser;
}
//获取用户基本信息
String userInfoUrl = String.format(WeChatConfig.getOpenUserInfoUrl(),accessToken,openId);
//获取access_token
Map<String ,Object> baseUserMap = HttpUtils.doGet(userInfoUrl);
if(baseUserMap == null || baseUserMap.isEmpty()){ return null; }
String nickname = (String)baseUserMap.get("nickname");
Double sexTemp = (Double) baseUserMap.get("sex");
int sex = sexTemp.intValue();
String province = (String)baseUserMap.get("province");
String city = (String)baseUserMap.get("city");
String country = (String)baseUserMap.get("country");
String headimgurl = (String)baseUserMap.get("headimgurl");
StringBuilder sb = new StringBuilder(country).append("||").append(province).append("||").append(city);
String finalAddress = sb.toString();
try {
//解决乱码
nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");
finalAddress = new String(finalAddress.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
User user = new User();
user.setName(nickname);
user.setHeadImg(headimgurl);
user.setCity(finalAddress);
user.setOpenid(openId);
user.setSex(sex);
user.setCreateTime(new Date());
userMapper.save(user);
return user;
}
}
UserMapper
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
/**
* 根据主键id查找
* @param userId
* @return
*/
@Select("select * from user where id = #{id}")
User findByid(@Param("id") int userId);
/**
* 根据openid找用户
* @param openid
* @return
*/
@Select("select * from user where openid = #{openid}")
User findByopenid(@Param("openid") String openid);
/**
* 保存用户新
* @param user
* @return
*/
@Insert("INSERT INTO `user` ( `openid`, `name`, `head_img`, `phone`, `sign`, `sex`, `city`, `create_time`)" +
"VALUES" +
"(#{openid},#{name},#{headImg},#{phone},#{sign},#{sex},#{city},#{createTime});")
@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
int save(User user);
}
使用 JWT 生成用户 Token 回写客户端
1、获取当前页面访问地址
2、根据 User 基本信息生成 token
3、重定向到指定页面
WechatController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private UserService userService;
/**
* 拼装微信扫一扫登录url
* @return JsonData
*/
@GetMapping("login_url")
@ResponseBody
public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
return JsonData.buildSuccess(qrcodeUrl);
}
/**
* 获取用户code,保存 user,并且拼装请求,获取access_token
* 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
* 第三方服务器端,种cookie,再转发到用户当前页面地址
* @param code
* @param state 用户扫码登录时停留的页面
* @param response
*/
@GetMapping("/user/callback")
public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
String state, HttpServletResponse response) throws IOException {
User user = userService.saveWeChatUser(code);
if (user != null){
//生成jwt
String token = JwtUtils.geneJsonWebToken(user);
// state 当前用户的页面地址,需要拼接 http:// 这样才不会站内跳转
response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
}
}
}
用户登录拦截器
LoginIntercepter
package net.xdclass.xdvideo.interceoter;
import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.utils.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class LoginIntercepter implements HandlerInterceptor {
private static final Gson gson = new Gson();
/**
* 进入controller之前进行拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if(token == null ){
token = request.getParameter("token");
}
//不为空,就进行解密
if(token != null ) {
Claims claims = JwtUtils.checkJWT(token);
if(claims !=null){
//获取到 id 、 name
Integer userId = (Integer)claims.get("id");
String name = (String) claims.get("name");
//设置后续业务可能会用到的数据:这里 只设置了 id 、 name
request.setAttribute("user_id",userId);
request.setAttribute("name",name);
return true;
}
}
//解密失败 或者 没有token 就需要登录
sendJsonMessage(response,JsonData.buildError("请登录"));
return false;
}
/**
* 响应数据给前端
* @param response
* @param obj
*/
public static void sendJsonMessage(HttpServletResponse response, Object obj) throws IOException {
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
//JsonData 对象 转换为 json 格式,回写给前端
writer.print(gson.toJson(obj));
writer.close();
//刷新缓冲
response.flushBuffer();
}
}
IntercepterConfig
package net.xdclass.xdvideo.config;
import net.xdclass.xdvideo.interceoter.LoginIntercepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器配置
*/
@Configuration
public class IntercepterConfig implements WebMvcConfigurer {
/**
* 注册拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercepter()).addPathPatterns("/user/api/v1/*/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
OrderController(模拟订单接口)
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单接口
*/
@RestController
@RequestMapping("/user/api/v1/order")
public class OrderController {
@GetMapping("add")
public JsonData saveOrder(){
return JsonData.buildSuccess("下单成功");
}
}
测试(PostMan)
//错误的token
http://localhost:8081/user/api/v1/order/add?token=yyyy
{
"code":-1,
"msg":"请登录"
}
//正确的token
http://localhost:8081/user/api/v1/order/add?token=xxxx
{
"code":0,
"data":"下单成功",
"msg":null
}
第三方支付和聚合支付
什么是第三方支付
第三方支付是指具备一定实力和信誉保障的独立机构,采用与各大银行签约的方式,通过与银行支付结算系统接口对接而促成交易双方进行交易的网络支付模式。 通俗的例子: 支付宝,微信支付,百度钱包,PayPal(主要是欧美国家)、 拉卡拉(中国最大线下便民金融服务提供商)
优点:
1、支付平台降低了政府、企业、事业单位直连银行的成本,满足了企业专注发展在线业务的收付要求。
2、使用方便。对支付者而言,他所面对的是友好的界面,不必考虑背后复杂的技术操作过程
缺点:
1、风险问题,在电子支付流程中,资金都会在第三方支付服务商处滞留即出现所谓的资金沉淀,如缺乏有效的流动性管理,则可能存在资金安全和支付的风险
2、电子支付经营资格的认知、保护和发展问题
什么是聚合支付
聚合支付是相对之前的第三方支付而言的,作为对第三方支付平台服务的拓展,第三方支付是介于银行和商户之间的,而聚合支付是介于第三方支付和商户之间,出现的场景(解决的问题):
- 一堆第三方支付出现,并通过大量的钱补贴线上商家使用它们的支付,导致商户收银台堆满各种
- POS 机器,扫码设备,商户还需要去各家支付公司申请账号,结算等
- 解决的问题:聚合支付公司提供的二维码,支付多种方式支付,不再是一种,各个公司的竞争,就是支付渠道和方式的支持
申请微信支付介绍和不同场景的支付方式
1、什么是微信商户平台:
地址:https://pay.weixin.qq.com
提供给商家使用,用于查看交易数据,提现等信息
2、常用的支付方式 公众号支付,扫码支付,app 支付,小程序支付
官方地址:https://pay.weixin.qq.com/wiki/doc/api/index.html
案例演示: https://pay.weixin.qq.com/guide/webbased_payment.shtml
3、微信支付申请流程 https://pay.weixin.qq.com/guide/qrcode_payment.shtml
1)申请公众号(服务号) 认证费 300
2)开通微信支付
微信网页扫码支付
1、扫码支付文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=2_2
2、名称理解
appid:公众号唯一标识
appsecret:公众号的秘钥
mch_id:商户号,申请微信支付的时候分配的
key:支付交易过程生成签名的秘钥,设置路径
微信商户平台(pay.weixin.qq.com)--> 账户中心--> 账户设置-->API 安全--> 密钥设置
3、和微信支付交互方式
1、post 方式提交
2、XML 格式的协议
3、签名算法 MD5
4、交互业务规则 先判断协议字段返回,再判断业务返回,最后判断交易状态
5、接口交易单位为 分
6、交易类型:JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app 支付
7、商户订单号规则:
商户支付的订单号由商户自定义生成,仅支持使用字母、数字、中划线-、下划线_、竖线 |、星号*这些英文半角字符的组合,请勿使用汉字或全角等特殊字符,微信支付要求商户订单号保持唯一性
8、安全规范:
签名算法:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
校验工具:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
9、采用微信支付扫码模式二(不依赖商户平台设置回调 url)
时序图
什么是时序图
是一种 UML 交互图,描述了对象之间传递消息的时间顺序, 用来表示用例中的行为顺序, 是强调消息时间 顺序的交互图;通俗解释:就是交互流程图 (把大象装冰箱分几步)
时序图包括四个元素
- 对象(Object):时序图中的对象在交互中扮演的角色就是对象,使用矩形将对象名称包含起来, 名称下有下划线
- 生命线(Lifeline):生命线是一条垂直的虚线, 这条虚线表示对象的存在, 在时序图中, 每个对象都有生命线
- 激活(Activation):代表时序图中对象执行一项操作的时期, 表示该对象被占用以完成某个任务,当对象处于激活时期, 生命线可以拓宽为矩形
- 消息(Message):对象之间的交互是通过相互发消息来实现的,箭头上面标出消息名,一个对象可以请求(要求)另一个对象做某件事件,消息从源对象指向目标对象,消息一旦发送便将控制从源对象转移到目标对象,息的阅读顺序是严格自上而下的
- 消息交互中的实线:请求消息
- 消息交互中的虚线:响应返回消息
- 自己调用自己的方法:反身消息
支付时序图讲解和统一下单接口
支付时序图
统一下单接口介绍
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按扫码、JSAPI、APP 等不同场景生成交易串调起支付。
支付状态转变如下:
接口链接
URL 地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数: Reference
微信支付订单接口开发之订单增删改查
VideoOrderMapper
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.VideoOrder;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 订单dao层
*/
public interface VideoOrderMapper {
/**
* 保存订单,返回包含主键
* @param videoOrder
* @return
*/
@Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
" `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
" `video_img`, `user_id`, `ip`, `del`)" +
"VALUES" +
"(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
"#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
int insert(VideoOrder videoOrder);
/**
* 根据主键查找订单
* @param id
* @return
*/
@Select("select * from video_order where id=#{order_id} and del=0")
VideoOrder findById(@Param("order_id") int id);
/**
* 根据交易订单号获取订单对象
* @param outTradeNo
* @return
*/
@Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);
/**
* 逻辑删除订单
* @param id
* @param userId
* @return
*/
@Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
int del(@Param("id") int id, @Param("userId") int userId);
/**
* 查找我的全部订单
* @param userId
* @return
*/
@Select("select * from video_order where user_id =#{userId}")
List<VideoOrder> findMyOrderList(int userId);
/**
* 根据微信回调订单流水号更新订单状态
* @param videoOrder
* @return
*/
@Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
" where out_trade_no=#{outTradeNo} and state=0 and del=0")
int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
}
IDE 生成订单接口单元测试和断言开发
1、 核心注解
@RunWith(SpringRunner.class)
@SpringBootTest
2、根据公司情况,写单元测试,核心接口一定要写,非核心的尽量写
3、断言类型,可以细化(比如 id=2)
VideoServiceTest
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.mapper.VideoMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class VideoServiceTest {
@Autowired
private VideoService videoService;
@Test
public void findAll() {
List<Video> list = videoService.findAll();
assertNotNull(list);
for (Video video : list) {
System.out.println(video.getTitle());
}
}
@Test
public void findById() {
Video video = videoService.findById(2);
assertNotNull(video);
}
@Test
public void update() {
}
@Test
public void delete() {
}
@Test
public void save() {
}
}
VideoOrderMapperTest
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.VideoOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class VideoOrderMapperTest {
@Autowired
private VideoOrderMapper videoOrderMapper;
@Test
public void insert() {
VideoOrder videoOrder = new VideoOrder();
videoOrder.setDel(0);
videoOrder.setTotalFee(111);
videoOrder.setHeadImg("xxxxdfsdfs");
videoOrder.setVideoTitle("springBoot高级视频教程");
videoOrderMapper.insert(videoOrder);
assertNotNull(videoOrder.getId());
}
@Test
public void findById() {
VideoOrder videoOrder = videoOrderMapper.findById(1);
assertNotNull(videoOrder);
}
@Test
public void findByOutTradeNo() {
}
@Test
public void del() {
}
@Test
public void findMyOrderList() {
}
@Test
public void updateVideoOderByOutTradeNo() {
}
}
封装常用工具类 CommonUtils 和 WXpayUtils
CommonUtils 包含方法 md5,uuid 等
CommonUtils
package net.xdclass.xdvideo.utils;
import java.security.MessageDigest;
import java.util.UUID;
/**
* 常用工具类的封装,md5,uuid等
*/
public class CommonUtils {
/**
* 生成 uuid, 即用来标识一笔单,也用做 nonce_str
* @return
*/
public static String generateUUID(){
String uuid = UUID.randomUUID().toString().
replaceAll("-","").substring(0,32);
return uuid;
}
/**
* md5常用工具类
* @param data
* @return
*/
public static String MD5(String data){
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte [] array = md5.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
微信支付工具类,XML 转 map,map 转 XML,生成签名
从微信开发者文档获取部分代码 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
Reference
WXPayUtil
package net.xdclass.xdvideo.utils;
import org.w3c.dom.Entity;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
/**
* 微信支付工具类,xml转map,map转xml,生成签名
*/
public class WXPayUtil {
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成微信支付sign
* @return
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();
//生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
while (it.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}
sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* 校验签名
*
* @param params
* @param key
* @return
*/
public static boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);
String weixinPaySign = params.get("sign").toUpperCase();
return weixinPaySign.equals(sign);
}
}
微信支付 Controller 下单 API 接口开发
1、开发 controller,开发期间不加入拦截器登录校验
2、iputils 工具类介绍(用于获取下单客户端的 IP 地址)
3、加入微信支付配置
#微信商户平台
application.properties
#支付配置
#微信商户平台
wxpay.mer_id=1503808832
wxpay.key=xdclasss20182018xdclass2018x018d
wxpay.callback=16web.tunnel.qydev.com/pub/api/v1/wechat/order/callback1
WeChatConfig
IpUtils
package net.xdclass.xdvideo.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
}
VideoOrderDto
package net.xdclass.xdvideo.dto;
import net.xdclass.xdvideo.domain.VideoOrder;
/**
* 订单数据传输对象
*/
public class VideoOrderDto extends VideoOrder {
}
VideoController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 订单接口
*/
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {
@Autowired
private VideoOrderService videoOrderService;
@GetMapping("add")
public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
HttpServletRequest request) throws Exception {
//获取用户的ip地址
String ip = IpUtils.getIpAddr(request);
//int userId = request.getAttribute("user_id");
//开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
int userId = 1;
VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setUserId(userId);
videoOrderDto.setVideoId(videoId);
videoOrderDto.setIp(ip);
//Dto转换保存
videoOrderService.save(videoOrderDto);
return JsonData.buildSuccess("下单成功");
}
}
VideoOrderService
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
/**
* 订单接口
*/
public interface VideoOrderService {
/**
* 下单接口
* @param videoOrderDto
* @return
*/
VideoOrder save(VideoOrderDto videoOrderDto);
}
微信支付下单 API 接口(保存订单)和签名开发
VideoOrderServiceImpl
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.SortedMap;
import java.util.TreeMap;
@Service
public class VideoOrderServiceImpl implements VideoOrderService {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private VideoMapper videoMapper;
@Autowired
private VideoOrderMapper videoOrderMapper;
@Autowired
private UserMapper userMapper;
@Override
public VideoOrder save(VideoOrderDto videoOrderDto) throws Exception {
//查找视频信息
Video video = videoMapper.findById(videoOrderDto.getVideoId());
//查找用户信息
User user = userMapper.findByid(videoOrderDto.getUserId());
//生成订单
VideoOrder videoOrder = new VideoOrder();
videoOrder.setTotalFee(video.getPrice());
videoOrder.setVideoImg(video.getCoverImg());
videoOrder.setVideoTitle(video.getTitle());
videoOrder.setCreateTime(new Date());
videoOrder.setVideoId(video.getId());
videoOrder.setState(0);
videoOrder.setUserId(user.getId());
videoOrder.setHeadImg(user.getHeadImg());
videoOrder.setNickname(user.getName());
videoOrder.setDel(0);
videoOrder.setIp(videoOrderDto.getIp());
videoOrder.setOutTradeNo(CommonUtils.generateUUID());
videoOrderMapper.insert(videoOrder);
//生成签名
unifiedOrder(videoOrder);
//获取codeurl
//生成二维码
return null;
}
/**
* 统一下单方法 :生成签名
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign签名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map转xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
//统一下单
return "";
}
}
测试
http://localhost:8081/api/v1/order/add?video_id=2
{
"code": 0,
"data": "下单成功",
"msg": null
}
校验工具
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
发送数据
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xml>
<appid>wx5beac15ca207cdd40c</appid>
<body>葵花宝典</body>
<mch_id>1503808832</mch_id>
<nonce_str>c4d160dfe45147918dbc7fbf7b5e3205</nonce_str>
<notify_url>16web.tunnel.qydev.com/pub/api/v1/wechat/order/callback1</notify_url>
<out_trade_no>a6ed4257042c429abb720a4f7cc1222c</out_trade_no>
<sign>D293F6AD0C05DDB39CE34E96FC5CECA5</sign>
<spbill_create_ip>0:0:0:0:0:0:0:1</spbill_create_ip>
<total_fee>500</total_fee>
<trade_type>NATIVE</trade_type>
</xml>
商户 key
xdclasss20182018xdclass2018x018d
验证通过说明校验过程正确
调用微信统一下单接口实战
1、配置统一下单接口
WeChatConfig
/**
* 统一下单url
*/
private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
2、发送请求验证
微信统一下单响应
<xml><return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx5beac15ca207c40c]]></appid>
<mch_id><![CDATA[1503809911]]></mch_id>
<nonce_str><![CDATA[Go5gDC2CYL5HvizG]]></nonce_str>
<sign><![CDATA[BC62592B9A94F5C914FAAD93ADE7662B]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx262207318328044f75c9ebec2216783076]]></prepay_id>
<trade_type><![CDATA[NATIVE]]></trade_type>
<code_url><![CDATA[weixin://wxpay/bizpayurl?pr=hFq9fX6]]></code_url>
</xml>
3、获取 code_url
遇到问题,根据错误码解决:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
VideoOrderService
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
/**
* 订单接口
*/
public interface VideoOrderService {
/**
* 下单接口
* @param videoOrderDto
* @return
*/
String save(VideoOrderDto videoOrderDto) throws Exception;
}
VideoOrderServiceImpl
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@Service
public class VideoOrderServiceImpl implements VideoOrderService {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private VideoMapper videoMapper;
@Autowired
private VideoOrderMapper videoOrderMapper;
@Autowired
private UserMapper userMapper;
@Override
public String save(VideoOrderDto videoOrderDto) throws Exception {
//查找视频信息
Video video = videoMapper.findById(videoOrderDto.getVideoId());
//查找用户信息
User user = userMapper.findByid(videoOrderDto.getUserId());
//生成订单
VideoOrder videoOrder = new VideoOrder();
videoOrder.setTotalFee(video.getPrice());
videoOrder.setVideoImg(video.getCoverImg());
videoOrder.setVideoTitle(video.getTitle());
videoOrder.setCreateTime(new Date());
videoOrder.setVideoId(video.getId());
videoOrder.setState(0);
videoOrder.setUserId(user.getId());
videoOrder.setHeadImg(user.getHeadImg());
videoOrder.setNickname(user.getName());
videoOrder.setDel(0);
videoOrder.setIp(videoOrderDto.getIp());
videoOrder.setOutTradeNo(CommonUtils.generateUUID());
videoOrderMapper.insert(videoOrder);
//获取codeurl
String codeUrl = unifiedOrder(videoOrder);
return codeUrl;
}
/**
* 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign签名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map转xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
//统一下单
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(null == orderStr) {
return null;
}
Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
System.out.println(unifiedOrderMap.toString());
if(unifiedOrderMap != null) {
//获取二维码url
return unifiedOrderMap.get("code_url");
}
return "";
}
}
OrderController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 订单接口
*/
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {
@Autowired
private VideoOrderService videoOrderService;
@GetMapping("add")
public JsonData saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
HttpServletRequest request) throws Exception {
//获取用户的ip地址
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.11";
//int userId = request.getAttribute("user_id");
//开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
int userId = 1;
VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setUserId(userId);
videoOrderDto.setVideoId(videoId);
videoOrderDto.setIp(ip);
//获取codeUrl
String codeUrl = videoOrderService.save(videoOrderDto);
//生成二维码
return JsonData.buildSuccess("下单成功");
}
}
谷歌二维码工具根据 code_url 生成扫一扫支付二维码
将链接生成二维码图片(code_url)
参考
https://www.cnblogs.com/lanxiamo/p/6293580.html
https://blog.csdn.net/shenfuli/article/details/68923393
https://coolshell.cn/articles/10590.html
引入依赖
<!-- google二维码生成包 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>2.0</version>
</dependency>
OrderController
package net.xdclass.xdvideo.controller;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* 订单接口
*/
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {
@Autowired
private VideoOrderService videoOrderService;
@GetMapping("add")
public void saveOrder(@RequestParam(value = "video_id",required = true)int videoId,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
//获取用户的ip地址
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.11";
//int userId = request.getAttribute("user_id");
//开发controller,开发期间不加入拦截器登录校验,所以这里手动赋值 userId
int userId = 1;
VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setUserId(userId);
videoOrderDto.setVideoId(videoId);
videoOrderDto.setIp(ip);
//获取codeUrl
String codeUrl = videoOrderService.save(videoOrderDto);
if(codeUrl == null) {
throw new NullPointerException();
}
try{
//生成二维码的配置
Map<EncodeHintType,Object> hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//编码类型
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
//构建矩阵图对象
BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
OutputStream out = response.getOutputStream();
//返回矩阵图给微信客户端
MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
}catch (Exception e){
e.printStackTrace();
}
}
}
测试
http://localhost:8081/api/v1/order/add?video_id=2
返回 二维码
如何知道用户是否支付成功
- 通过前段定时器轮询该用户的最新订单的状态
微信支付扫码回调开发实战
Reference
回调数据如下
<xml>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<attach><![CDATA[支付测试]]></attach>
<bank_type><![CDATA[CFT]]></bank_type>
<fee_type><![CDATA[CNY]]></fee_type>
<is_subscribe><![CDATA[Y]]></is_subscribe>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
<out_trade_no><![CDATA[1409811653]]></out_trade_no>
<result_code><![CDATA[SUCCESS]]></result_code>
<return_code><![CDATA[SUCCESS]]></return_code>
<sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
<time_end><![CDATA[20140903131540]]></time_end>
<total_fee>1</total_fee>
<coupon_fee><![CDATA[10]]></coupon_fee>
<coupon_count><![CDATA[1]]></coupon_count>
<coupon_type><![CDATA[CASH]]></coupon_type>
<coupon_id><![CDATA[10000]]></coupon_id>
<coupon_fee><![CDATA[100]]></coupon_fee>
<trade_type><![CDATA[JSAPI]]></trade_type>
<transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
</xml>
使用 Ngrock 本地接收微信回调,并开发回调接口
注意点:
回调要用 post 方式,微信文档没有写回调的通知方式
可以用这个注解 @RequestMapping
application.properties
#支付配置
#微信商户平台
wxpay.mer_id=1503808832
wxpay.key=xdclasss20182018xdclass2018x018d
wxpay.callback=16web.tunnel.qydev.com/api/v1/wechat/order/callback
WechatController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.utils.JwtUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Map;
@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private UserService userService;
/**
* 拼装微信扫一扫登录url
* @return JsonData
*/
@GetMapping("login_url")
@ResponseBody
public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
return JsonData.buildSuccess(qrcodeUrl);
}
/**
* 获取用户code,保存 user,并且拼装请求,获取access_token
* 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
* 第三方服务器端,种cookie,再转发到用户当前页面地址
* @param code
* @param state 用户扫码登录时停留的页面
* @param response
*/
@GetMapping("/user/callback")
public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
String state, HttpServletResponse response) throws IOException {
User user = userService.saveWeChatUser(code);
if (user != null){
//生成jwt
String token = JwtUtils.geneJsonWebToken(user);
// state 当前用户的页面地址,需要拼接 http:// 这样才不会站内跳转
response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
}
}
/**
* 微信支付回调: post方式 所以需要使用 @RequestMapping
*/
@RequestMapping("/order/callback")
public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream inputStream = request.getInputStream();
//BufferedReader是包装设计模式,性能更搞
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
StringBuffer sb = new StringBuffer();
String line ;
while ((line = in.readLine()) != null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
System.out.println(callbackMap.toString());
}
}
微信回调处理之更新订单状态和幂等性讲解
微信回调通知规则(通知频率为 15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
幂等性: 同样的参数和值,不管调用你的接口多少次,响应结果都和调用一次是一样的
1、校验签名是否正确,防止伪造回调
2、查询订单是否已经更新
3、若没更新则更新订单状态
4、回应微信,SUCCESS 或者 FAIL
response.setContentType("text/xml");
response.getWriter().println("success");
WXPayUtil 添加方法(获取有序 map)
/**
* 获取有序map
* @param map
* @return
*/
public static SortedMap<String,String> getSortedMap(Map<String,String> map){
SortedMap<String, String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()){
String key = (String)it.next();
String value = map.get(key);
String temp = "";
if( null != value){
temp = value.trim();
}
sortedMap.put(key,temp);
}
return sortedMap;
}
VideoOrderService
package net.xdclass.xdvideo.service;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
/**
* 订单接口
*/
public interface VideoOrderService {
/**
* 下单接口
* @param videoOrderDto
* @return
*/
String save(VideoOrderDto videoOrderDto) throws Exception;
/**
* 根据流水号查找订单
* @param outTradeNo
* @return
*/
VideoOrder findByOutTradeNo(String outTradeNo);
/**
* 根据流水号更新订单
* @param videoOrder
* @return
*/
int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
}
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@Service
public class VideoOrderServiceImpl implements VideoOrderService {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private VideoMapper videoMapper;
@Autowired
private VideoOrderMapper videoOrderMapper;
@Autowired
private UserMapper userMapper;
@Override
public String save(VideoOrderDto videoOrderDto) throws Exception {
//查找视频信息
Video video = videoMapper.findById(videoOrderDto.getVideoId());
//查找用户信息
User user = userMapper.findByid(videoOrderDto.getUserId());
//生成订单
VideoOrder videoOrder = new VideoOrder();
videoOrder.setTotalFee(video.getPrice());
videoOrder.setVideoImg(video.getCoverImg());
videoOrder.setVideoTitle(video.getTitle());
videoOrder.setCreateTime(new Date());
videoOrder.setVideoId(video.getId());
videoOrder.setState(0);
videoOrder.setUserId(user.getId());
videoOrder.setHeadImg(user.getHeadImg());
videoOrder.setNickname(user.getName());
videoOrder.setDel(0);
videoOrder.setIp(videoOrderDto.getIp());
videoOrder.setOutTradeNo(CommonUtils.generateUUID());
videoOrderMapper.insert(videoOrder);
//获取codeurl
String codeUrl = unifiedOrder(videoOrder);
return codeUrl;
}
@Override
public VideoOrder findByOutTradeNo(String outTradeNo) {
return videoOrderMapper.findByOutTradeNo(outTradeNo);
}
@Override
public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
}
/**
* 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign签名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map转xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
//统一下单
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(null == orderStr) {
return null;
}
Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
System.out.println(unifiedOrderMap.toString());
if(unifiedOrderMap != null) {
//获取二维码url
return unifiedOrderMap.get("code_url");
}
return "";
}
}
VideoOrderMapper
package net.xdclass.xdvideo.mapper;
import net.xdclass.xdvideo.domain.VideoOrder;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 订单dao层
*/
public interface VideoOrderMapper {
/**
* 保存订单,返回包含主键
* @param videoOrder
* @return
*/
@Insert("INSERT INTO `video_order` (`openid`, `out_trade_no`, `state`, `create_time`," +
" `notify_time`, `total_fee`, `nickname`, `head_img`, `video_id`, `video_title`," +
" `video_img`, `user_id`, `ip`, `del`)" +
"VALUES" +
"(#{openid},#{outTradeNo},#{state},#{createTime},#{notifyTime},#{totalFee}," +
"#{nickname},#{headImg},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{del});")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
int insert(VideoOrder videoOrder);
/**
* 根据主键查找订单
* @param id
* @return
*/
@Select("select * from video_order where id=#{order_id} and del=0")
VideoOrder findById(@Param("order_id") int id);
/**
* 根据交易订单号获取订单对象
* @param outTradeNo
* @return
*/
@Select("select * from video_order where out_trade_no=#{out_trade_no} and del=0")
VideoOrder findByOutTradeNo(@Param("out_trade_no") String outTradeNo);
/**
* 逻辑删除订单
* @param id
* @param userId
* @return
*/
@Update("update video_order set del=1 where id=#{id} and user_id =#{userId}")
int del(@Param("id") int id, @Param("userId") int userId);
/**
* 查找我的全部订单
* @param userId
* @return
*/
@Select("select * from video_order where user_id =#{userId}")
List<VideoOrder> findMyOrderList(int userId);
/**
* 根据微信回调订单流水号更新订单状态
* @param videoOrder
* @return
*/
@Update("update video_order set state=#{state}, notify_time=#{notifyTime}, openid=#{openid}" +
" where out_trade_no=#{outTradeNo} and state=0 and del=0")
int updateVideoOderByOutTradeNo(VideoOrder videoOrder);
}
WechatController
package net.xdclass.xdvideo.controller;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.JsonData;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.service.UserService;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.JwtUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
@Controller
@RequestMapping("/api/v1/wechat")
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private UserService userService;
@Autowired
private VideoOrderService videoOrderService;
/**
* 拼装微信扫一扫登录url
* @return JsonData
*/
@GetMapping("login_url")
@ResponseBody
public JsonData loginUrl(@RequestParam(value = "access_page",required = true)String accessPage) throws UnsupportedEncodingException {
String redirectUrl = weChatConfig.getOpenRedirectUrl(); //获取开放平台重定向地址
String callbackUrl = URLEncoder.encode(redirectUrl,"GBK"); //进行编码
String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppid(),callbackUrl,accessPage);
return JsonData.buildSuccess(qrcodeUrl);
}
/**
* 获取用户code,保存 user,并且拼装请求,获取access_token
* 因为重定向,所以第三方服务器端可以获取用户当前 页面访问地址
* 第三方服务器端,种cookie,再转发到用户当前页面地址
* @param code
* @param state 用户扫码登录时停留的页面
* @param response
*/
@GetMapping("/user/callback")
public void wechatUserCallback(@RequestParam(value = "code",required = true) String code,
String state, HttpServletResponse response) throws IOException {
User user = userService.saveWeChatUser(code);
if (user != null){
//生成jwt
String token = JwtUtils.geneJsonWebToken(user);
// state 当前用户的页面地址,需要拼接 http:// 这样才不会站内跳转
response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
}
}
/**
* 微信支付回调: post方式 所以需要使用 @RequestMapping
*/
@RequestMapping("/order/callback")
public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream inputStream = request.getInputStream();
//BufferedReader是包装设计模式,性能更搞
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
StringBuffer sb = new StringBuffer();
String line ;
while ((line = in.readLine()) != null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());
//返回给前端,告诉用户支付完成
System.out.println(callbackMap.toString());
SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);
//判断签名是否正确
if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
//验证业务码状态是否为 SUCCESS
if("SUCCESS".equals(sortedMap.get("result_code"))){
//拿到流水号
String outTradeNo = sortedMap.get("out_trade_no");
//根据流水号查找订单
VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);
if(dbVideoOrder != null && dbVideoOrder.getState()==0){ //判断逻辑看业务场景
VideoOrder videoOrder = new VideoOrder();
videoOrder.setOpenid(sortedMap.get("openid"));
videoOrder.setOutTradeNo(outTradeNo);
videoOrder.setNotifyTime(new Date());
videoOrder.setState(1);
int rows = videoOrderService.updateVideoOderByOutTradeNo(videoOrder);
//通知微信订单处理成功
if(rows == 1){
response.setContentType("text/xml");
response.getWriter().println("success");
return;
}
}
}
}
//都处理失败
response.setContentType("text/xml");
response.getWriter().println("fail");
}
}
测试
1、扫码支付
2、数据库 video_order 中的 订单状态 state = 1
微信支付之下单事务处理
Reference
1、SpringBoot 开启事务,启动类里面增加 @EnableTransactionManagement
2、需要事务的方法上加 @Transactional(propagation = Propagation.REQUIRED) 隔离级别采用默认即可。
VideoOrderServiceImpl
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(VideoOrderDto videoOrderDto) throws Exception { …… }
3、aop 的管理事务的好处和选择: 增,删,改 开启事务 (有的方法只操作一个数据,只有一条语句,加事务会影响性能,2 条以上的修改操作才会加事务)
全局异常处理
XdException
package net.xdclass.xdvideo.exception;
/**
* 自定义异常类
*/
public class XdException extends RuntimeException {
/**
* 状态码
*/
private Integer code;
/**
* 异常消息
*/
private String msg;
public XdException(int code, String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
XdExceptionHandler
package net.xdclass.xdvideo.exception;
import net.xdclass.xdvideo.domain.JsonData;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 异常处理控制器
*/
@ControllerAdvice
public class XdExceptionHandler {
/**
* 这里可以捕获Exception 然后使用instanceof 配合多分支 if 进行 细力度异常的使用
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public JsonData Handler(Exception e){
if(e instanceof XdException){
XdException xdException = (XdException)e;
return JsonData.buildError(xdException.getMsg(),xdException.getCode());
}else{
return JsonData.buildError("全局异常,未知错误");
}
}
}
使用 Logback 整合 SpringBoot 打点日志
Logback 是 SpringBoot 默认的日志系统。
1、日志不仅用于排查问题,查看应用运行情况
2、更可以用于统计,虽然统计可以通过数据库进行统计,但是存在风险,如果用日志,并且建立一个日志系统用于分析,这样就方便产品和运营人员进行查看分析数据
3、写日志,可以加缓冲 buffer,也可也进行异步
参考资料:https://blog.csdn.net/zhuyucheng123/article/details/21524549
logback-spring.xml
项目中日志路径(启动项目自动创建如下目录和文件)
app_log/log
app.info.2020-02-19.log
app.err.2020-02-19.log
app.data.2020-02-19.log
service 层埋点
VideoOrderServiceImpl
package net.xdclass.xdvideo.service.impl;
import net.xdclass.xdvideo.config.WeChatConfig;
import net.xdclass.xdvideo.domain.User;
import net.xdclass.xdvideo.domain.Video;
import net.xdclass.xdvideo.domain.VideoOrder;
import net.xdclass.xdvideo.dto.VideoOrderDto;
import net.xdclass.xdvideo.mapper.UserMapper;
import net.xdclass.xdvideo.mapper.VideoMapper;
import net.xdclass.xdvideo.mapper.VideoOrderMapper;
import net.xdclass.xdvideo.service.VideoOrderService;
import net.xdclass.xdvideo.utils.CommonUtils;
import net.xdclass.xdvideo.utils.HttpUtils;
import net.xdclass.xdvideo.utils.WXPayUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
@Service
public class VideoOrderServiceImpl implements VideoOrderService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private Logger dataLogger = LoggerFactory.getLogger("dataLogger");
@Autowired
private WeChatConfig weChatConfig;
@Autowired
private VideoMapper videoMapper;
@Autowired
private VideoOrderMapper videoOrderMapper;
@Autowired
private UserMapper userMapper;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(VideoOrderDto videoOrderDto) throws Exception {
//打点
dataLogger.info("module=video_order`api=save`user_id={}`video_id={}",videoOrderDto.getUserId(),videoOrderDto.getVideoId());
//查找视频信息
Video video = videoMapper.findById(videoOrderDto.getVideoId());
//查找用户信息
User user = userMapper.findByid(videoOrderDto.getUserId());
//生成订单
VideoOrder videoOrder = new VideoOrder();
videoOrder.setTotalFee(video.getPrice());
videoOrder.setVideoImg(video.getCoverImg());
videoOrder.setVideoTitle(video.getTitle());
videoOrder.setCreateTime(new Date());
videoOrder.setVideoId(video.getId());
videoOrder.setState(0);
videoOrder.setUserId(user.getId());
videoOrder.setHeadImg(user.getHeadImg());
videoOrder.setNickname(user.getName());
videoOrder.setDel(0);
videoOrder.setIp(videoOrderDto.getIp());
videoOrder.setOutTradeNo(CommonUtils.generateUUID());
videoOrderMapper.insert(videoOrder);
//获取codeurl
String codeUrl = unifiedOrder(videoOrder);
return codeUrl;
}
@Override
public VideoOrder findByOutTradeNo(String outTradeNo) {
return videoOrderMapper.findByOutTradeNo(outTradeNo);
}
@Override
public int updateVideoOderByOutTradeNo(VideoOrder videoOrder) {
return videoOrderMapper.updateVideoOderByOutTradeNo(videoOrder);
}
/**
* 统一下单方法 : 生成签名,发送数据,解析微信返回数据,拿到code_url(微信二维码)
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());
params.put("mch_id", weChatConfig.getMchId());
params.put("nonce_str",CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());
params.put("out_trade_no",videoOrder.getOutTradeNo());
params.put("total_fee",videoOrder.getTotalFee().toString());
params.put("spbill_create_ip",videoOrder.getIp());
params.put("notify_url",weChatConfig.getPayCallbackUrl());
params.put("trade_type","NATIVE");
//sign签名
String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
params.put("sign",sign);
//map转xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
//统一下单
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(null == orderStr) {
return null;
}
Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
System.out.println(unifiedOrderMap.toString());
if(unifiedOrderMap != null) {
//获取二维码url
return unifiedOrderMap.get("code_url");
}
return "";
}
}
测试
http://localhost:8081/api/v1/order/add?video_id=2
查看日志文件 app.data.2020-02-19.log 内容
2020-02-19 14:08:09.022`module=video_order`api=save`user_id=1`video_id=2`
跨域和对应的处理方法
1、跨域:浏览器同源策略
1995 年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。
最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"
协议相同 http https
域名相同 www.xdcass.net
端口相同 80 81
一句话:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域
浏览器控制台跨域提示:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
2、解决方法
1)JSONP
2)Http 响应头配置允许跨域
nginx 层配置(第一种方式)
https://www.cnblogs.com/hawk-whu/p/6725699.html
程序代码中处理(第二种方式)
SpringBoot 自带配置
Cors 注意点:假如接口逻辑报错,则跨域配置不生效,可以考虑做全局异常处理,这样即使接口运行出错,跨域也依然生效
package net.xdclass.xdvideo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class Cors extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")指定为线上的静态资源的域
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH")
.allowCredentials(true).maxAge(3600);
}
}