目录
一、前台首页显示
1、Banner
2、首页讲师课程
二、登录
1、JWT+token实现单点登录
?1、JWT
2、JWT的原则
3、JWT使用
2、单点登录
3、腾讯云短信注册/手机号登录
1、手机号登录
2、注册
三、课程详情页
1、课程购买模块
2、视频播放模块
3、课程评论模块
评论显示
添加评论
初始化评论区用户头像
4、其他课程基本信息显示模块
5、全部课程模块
四、课程微信支付功能
1、购买按钮跳转
2、创建订单方法
3、进入支付页面
4、监听支付状态
前台系统使用基于Vue的Nuxt.js轻量级服务端渲染框架开发
一、前台首页显示
1、Banner
利用SpringCache做缓存,本质上是利用Rdis做缓存
首先配置一个配置类
@Configuration
@EnableCaching//开启缓存
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
配置reidsTemplate,设置它的序列化方式,主要就是配置redis的序列化方式,默认SpringCache的序列化方式是jdk序列化,详情见下面链接中SpringCache部分2021-09-30 商城分布式高级篇技术总结_Young的博客-CSDN博客
2、首页讲师课程
这里没有用到缓存,因为可能会经常更换
/**
* 根据id降序查出前8个课程与4个老师并返回,用于前端首页展示
* @return 查出的课程和讲师列表
*/
@GetMapping("indexTeacherCourseInfo")
public R getTeacherCourse(){
//根据id降序查出前4位老师
QueryWrapper<EduTeacher> teacherWrapper = new QueryWrapper<>();
teacherWrapper.orderByDesc("id");
teacherWrapper.last("limit 4");
List<EduTeacher> teacherList = teacherService.list(teacherWrapper);
//根据id降序查出前8个课程
QueryWrapper<EduCourse> courseWrapper = new QueryWrapper<>();
courseWrapper.orderByDesc("id");
courseWrapper.last("limit 8");
List<EduCourse> courseList = courseService.list(courseWrapper);
return R.ok().data("teachers",teacherList).data("courses",courseList);
}
首页显示8个热门课程和4个热门讲师,分页查询,这里按照id降序查询
二、登录
1、JWT+token实现单点登录
?1、JWT
JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示
{
"alg": "HS256",
"typ": "JWT"
}
有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法。
2、JWT的原则
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
3、JWT使用
1、添加jwt工具依赖
<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
2、创建JWT工具类
/**
* @author
*/
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24;
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}
2、单点登录
1、登录接口
前端会传递一个member对象
/**
* 用户登录方法
*
* @param member 用户信息
* @return token用于实现SSO
*/
@PostMapping("login")
public R loginUser(@RequestBody Member member) {
//调用service方法实现登录
//返回token值,使用jwt生成
String token = memberService.login(member);
return R.ok().data("token", token);
}
2、判断接收的信息
/**
* 用户登录方法
*/
@Override
public String login(Member member) {
//获取手机号码和密码
String mobile = member.getMobile();
String password = member.getPassword();
//判空
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
throw new GuliException(20001,"登陆失败,手机号码或密码不能为空");
}
//判断手机号码是否正确
QueryWrapper<Member> wrapper = new QueryWrapper<>();
wrapper.eq("mobile",mobile);
Member user = this.getOne(wrapper);
if (user == null){
throw new GuliException(20001,"该手机号码未注册");
}
//判断密码是否正确
/*
由于数据库中存储的是MD5加密的密码,不能使用明文密码直接比较,应转为md5
*/
if (!MD5.encrypt(password).equals(user.getPassword())){
throw new GuliException(20001,"密码不正确");
}
//判断用户是否被禁用
if (user.getIsDisabled()){
throw new GuliException(20001,"用户被禁用");
}
//登录成功,使用JWT生成一个token,注意不能使用前端回传的对象,要使用数据库查出的对象
return JwtUtils.getJwtToken(user.getId(), user.getNickname());
}
如果登录通过,那么会利用JWT工具类生成一个token字符串返回
2、前端拦截
登录成功后,设置一个cookie值loginToken,作用域为localhost,前端拦截器发现这个值会拦截下,取出值放入请求头中
// http request 拦截器
service.interceptors.request.use(
config => {
//debugger
if (cookie.get('loginToken')) {
config.headers['token'] = cookie.get('loginToken');
}
return config
},
err => {
return Promise.reject(err);
}),
?如下:
那么后面的请求都会携带这个cookie
3、实现单点登录关键步骤
?紧接着,调用下面的方法,通过cookie中的token字段请求接口,解析出用户的信息再次放入cookie
//3 在utils/request.js中写一个http request 拦截器,用于将token放入请求头
//4 调用方法,通过token获取用户信息
login.getUserIdByToken()
.then((res) => {
// this.loginInfo=res.data.data.userInfo
//5 放入cookie
cookie.set('userInfo',JSON.stringify(res.data.data.userInfo),{domain: 'localhost'})
//跳转到首页
// this.$router.push({ path: "/" });用另一种方法
window.location.href="/"
})
接口如下:
/**
* 根据请求头中的token获取用户信息
*
* @param req request请求
* @return 用户信息
*/
@GetMapping("getMemberInfoByToken")
public R getMemberInfoByToken(HttpServletRequest req) {
//调用JwtUtils工具类,利用请求头获取token进行解析得到用户id
String memberId = JwtUtils.getMemberIdByJwtToken(req);
Member member = memberService.getById(memberId);
return R.ok().data("userInfo", member);
}
利用工具类,从请求头中获取token并解析出用户信息返回?
每次在我们进入抽取的页面公共头时,都会调用通过cookie获取用户信息的方法
?方法如下:
//创建方法,从cookie中获取用户信息
getUserInfoFromCookie() {
var userStr = cookie.get('userInfo');
//把字符串转为json对象
if (userStr) {
//TODO 这块在手机号登录需要这样写,目前已解决
this.loginInfo = JSON.parse(userStr);
// this.loginInfo = userStr;
}
},
?从cookie中获取用户信息,由于它是一个字符串,用json解析,当拿到后,登录表单会判断是否为空,不为空则显示用户信息,为空仍然显示登录,实现完毕
3、腾讯云短信注册/手机号登录
1、手机号登录
引入依赖
<!--腾讯短信服务依赖-->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.270</version>
</dependency>
?点击登录后请求接口
/**
* 用户登录方法
*/
@Override
public String login(Member member) {
//获取手机号码和密码
String mobile = member.getMobile();
String password = member.getPassword();
//判空
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
throw new GuliException(20001,"登陆失败,手机号码或密码不能为空");
}
//判断手机号码是否正确
QueryWrapper<Member> wrapper = new QueryWrapper<>();
wrapper.eq("mobile",mobile);
Member user = this.getOne(wrapper);
if (user == null){
throw new GuliException(20001,"该手机号码未注册");
}
//判断密码是否正确
/*
由于数据库中存储的是MD5加密的密码,不能使用明文密码直接比较,应转为md5
*/
if (!MD5.encrypt(password).equals(user.getPassword())){
throw new GuliException(20001,"密码不正确");
}
//判断用户是否被禁用
if (user.getIsDisabled()){
throw new GuliException(20001,"用户被禁用");
}
//登录成功,使用JWT生成一个token,注意不能使用前端回传的对象,要使用数据库查出的对象
return JwtUtils.getJwtToken(user.getId(), user.getNickname());
}
登录成功后返回token
2、注册
点击发送短信,请求接口
/**
* 发送短信方法
* @param phone 手机号码
* @return 成功或失败信息
*/
@PostMapping("sendCode/{phone}")
public R sendCode(@PathVariable("phone") String phone){
//从Redis中取验证码,取到就返回
String codeInRedis = redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(codeInRedis)){
return R.ok();
}
//若Redis中没有,调用service发送短信方法
boolean res = msmService.send(phone);
if (res){
return R.ok();
}else {
return R.error().message("短信发送失败");
}
}
?因为验证码存入redis,按一定规则,每个用户设置一个固定的key对应验证码,发送前,先尝试取验证阿妈,如果不为空,返回成功,如果为空,调用我们写的发送方法。
/**
* 发送短信方法
*/
@Override
public boolean send(String phone) {
//生成四位数验证码交腾讯云发送
String code = RandomUtil.getFourBitRandom();
//发送验证码
boolean res = TxCode.SendCode("+86" + phone, code);
if (res){
//发送成功后将验证码放到Redis中设置存活时间5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
return true;
}else {
throw new GuliException(20001,"验证码发送失败");
}
}
发送短信的SendCode()方法如下:
public class TxCode {
private static final String SECRET_ID = "AKID2tTnqc7LJ0bvzlGUR7WfYAUXIn1ye4D5";
private static final String SECRET_KEY= "d06vSjjSt3wYuznLccKmysVsxlHXWDLC";
public static boolean SendCode(String phoneNumber,String code) {
try {
// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
Credential cred = new Credential(SECRET_ID, SECRET_KEY);
// 实例化要请求产品的client对象,clientProfile是可选的
SmsClient client = new SmsClient(cred, "ap-guangzhou");
// 实例化一个请求对象,每个接口都会对应一个request对象
SendSmsRequest req = new SendSmsRequest();
String[] phoneNumberSet = {phoneNumber};
req.setPhoneNumberSet(phoneNumberSet);
req.setSmsSdkAppId("1400563681");
req.setSignName("Young的平台");
req.setTemplateId("1089470");
String[] templateParamSet = {code, "5"};
req.setTemplateParamSet(templateParamSet);
// 返回的resp是一个SendSmsResponse的实例,与请求对象对应
SendSmsResponse resp = client.SendSms(req);
// 输出json格式的字符串回包
System.out.println(SendSmsResponse.toJsonString(resp));
//获取发送状态,若成功则返回true
for (SendStatus sendStatus : resp.getSendStatusSet()) {
if ("send success".equals(sendStatus.getMessage()) &&
"Ok".equals(sendStatus.getCode())){
return true;
}
}
} catch (TencentCloudSDKException e) {
e.printStackTrace();
}
//若不成,返回false
return false;
}
}
发送成功,则返回true,失败返回false?
三、课程详情页
1、课程购买模块
用户已经登录
点击课程,进入详情页面,会调用初始化方法,初始化课程的各个信息填充页面,如果课程价格为0或购买状态为已经购买,那么显示立即观看,否则显示立即购买
课程详情页初始化
//判断是否购买
initCourseInfo() {
course.getCourseInfo(this.courseId).then((res) => {
console.log("getCourseInfo-->",res.data.data);
(this.courseBaseInfo = res.data.data.courseDetails),
(this.chapterVideoList = res.data.data.chapterVideos),
(this.isBuy = res.data.data.isBuy);
});
},
请求接口
/**
* 课程详情页查询
* @param courseId 课程id
* @return 查询到的详情页信息
*/
@GetMapping("getCustCourseInfo/{courseId}")
public R getCustCourseInfo(@PathVariable String courseId,
HttpServletRequest req){
//根据课程id查询课程信息
CourseDetailsVo courseDetailsVo = courseService.getBaseCourseInfo(courseId);
//根据课程id查询章节小节信息
List<ChapterVo> chapterVoList = chapterService.getChapterVideoByCourseId(courseId);
//查询购买状态
//需要对id进行判空,否则会造成isBuy无值现象
String memberId = JwtUtils.getMemberIdByJwtToken(req);
boolean isBuy = false;
if (!StringUtils.isEmpty(memberId)){
isBuy = orderClient.queryBuyStatus(memberId, courseId);
}
return R.ok().data("courseDetails",courseDetailsVo).data("chapterVideos",chapterVoList).data("isBuy",isBuy);
}
通过jwt解析用户信息,解析出用户的id,然后传入查询课程状态的方法,返回值isBuy代表状态
@Override
public boolean queryBuyStatus(String userId, String courseId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("member_id",userId);
wrapper.eq("course_id",courseId);
wrapper.select("status");
Order order = this.getOne(wrapper);
if (order != null){
return order.getStatus() == 1;
}
return false;
}
这个课程状态决定了显示立即观看还是立即购买
2、视频播放模块
该页面一加载,会获取三个信息,课程基本信息,章节小节信息,isBuy字段
视频树形菜单通过章节小节信息展示,绑定一个超链接跳转到阿里云存储课程id的动态路由,请求阿里云获取视频播放凭证
?接口如下:
/**
* 通过视频id获取阿里云加密视频的播放凭证
* @param videoId 视频id
* @return 视频的播放凭证
*/
@GetMapping("getPlayAuth/{videoId}")
public R getPlayAuth(@PathVariable String videoId){
try{
DefaultAcsClient client = InitVodClient.initVodClient();
//创建获取凭证的request和response
GetVideoPlayAuthRequest req = new GetVideoPlayAuthRequest();
//设置视频id
req.setVideoId(videoId);
//得到凭证
GetVideoPlayAuthResponse resp = client.getAcsResponse(req);
String playAuth = resp.getPlayAuth();
System.out.println("播放凭证:【"+playAuth+"】");
return R.ok().data("playAuth",playAuth);
}catch (Exception e){
e.printStackTrace();
throw new GuliException(20001,"视频凭证获取失败");
}
}
待页面渲染完毕,执行mounted()中的阿里云播放器组件,填入凭证和视频id即可播放?
3、课程评论模块
评论显示
调用分页查询方法查询所有评论,用户头像,评论时间
添加评论
初始化时先取出cookie中用户信息赋值,添加评论时判断用户是否登录,登录了调用添加方法,没登录调用路由跳转到登录页面
初始化评论区用户头像
//评论输入框左边图片初始化
initCommentInfo() {
comment
.initCommentInfo()
.then((res) => {
this.com.content = res.data.data.content;
this.com.nickname = res.data.data.nickname;
this.com.avatar = res.data.data.avatar;
})
.catch((error) => {
console.log(error);
});
}
接口:?
@GetMapping("initCommentInfo")
public R initCommentInfo(@Param("req") HttpServletRequest req){
//从请求头拿出cookie解析出用户id
String memberId = JwtUtils.getMemberIdByJwtToken(req);
//远程调用查询用户信息
R res = ucenterClient.getMemberByMemberId(memberId);
Map<String, Object> data = res.getData();
//将查到的用户信息转换为字符串类型
String memberJson = (String) data.get("memberJson");
Gson gson = new Gson();
//解析成对象
Member member = gson.fromJson(memberJson, Member.class);
System.out.println("member----->"+member);
//拿出用户名和头像返回前端
String nickname = member.getNickname();
String avatar = member.getAvatar();
return R.ok().data("nickname",nickname).data("avatar",avatar);
}
前端回显?
4、其他课程基本信息显示模块
按照组件进行绑定即可
5、全部课程模块
进入页面初始化全部课程并且分页,初始化一级菜单,把一级分类,课程销量,人气,价格封装成一个vo进行排序。
核心方法,切换分页,默认为1,默认大小为8,会携带我们封装的vo进行分页查询。
//3 分页切换方法
gotoPage(page) {
course.getCustCourse(page, this.limit, this.searchObj).then((res) => {
this.dataInfo = res.data.data;
});
}
?按照条件拼接sql,然后调用此方法进行条件查询带分页
四、课程微信支付功能
1、购买按钮跳转
首先点击立即购买,如果用户信息不为空,调用生成订单方法,成功则携带订单号跳转至订单页面,如果信息为空提示登录才能购买
createOrders() {
if (this.userInfo && this.userInfo != null) {
order.createOrder(this.courseId).then((res) => {
//返回订单号
//跳转到支付
this.$router.push({ path: "/order/" + res.data.data.orderId });
});
} else {
// window.location.href='/login'
this.$message({
type: "warning",
message: "登录后才能购买!",
});
}
}
2、创建订单方法
/**
* 根据课程id和token中的用户id生成订单,返回订单号
*/
@Override
public String createOrder(String courseId,String memberIdByJwtToken) {
//通过远程调用获取用户信息,此类在公共模块中,因为我们需要Member类
//不能跨模块调用,因此写了一个一样的
MemberOrder memberOrder = ucenterClient.getMemberOrder(memberIdByJwtToken);
//通过远程调用获取课程信息
CourseDetailsVoOrder courseOrder = eduClient.getCourseOrder(courseId);
//创建Order对象,设置需要的数据
Order order = new Order();
order.setOrderNo(OrderNoUtil.getOrderNo());
order.setCourseId(courseId);
order.setCourseTitle(courseOrder.getTitle());
order.setCourseCover(courseOrder.getCover());
order.setTeacherName("test");
order.setTotalFee(courseOrder.getPrice());
order.setMemberId(memberIdByJwtToken);
order.setMobile(memberOrder.getMobile());
order.setNickname(memberOrder.getNickname());
order.setStatus(0);//是否支付0未1已
order.setPayType(1);//支付类型1微信2支付宝
order.setIsDeleted(0);
//保存到数据库
baseMapper.insert(order);
return order.getOrderNo();
}
将订单保存到数据库,订单号返回给前端,此时订单状态仍然为0,未购买,携带订单id跳转到支付界面
3、进入支付页面
?去支付按钮跳转:
//去支付按钮
toPay(){
this.$router.push({path: '/pay/'+this.order.orderNo})
}
携带着订单id,进入生成二维码的页面?
?通过订单id调用生成二维码方法
/**
* 生成支付二维码
* @param {订单编号} orderId
* @returns map集合
*/
createQrCode(orderId) {
return request({
url: `/eduorder/paylog/createQrCode/${orderId}`,
method: 'get'
})
}
接口如下:
通过订单id获取订单信息
设置请求参数,以及回调地址,
利用HttpClient发送post请求,返回值为xml格式,利用微信提供的工具类转换为map,获取里面的二维码地址和操作状态,以及可以设置一些我们需要的金额,订单号等参数
/**
* 生成微信支付二维码
*/
@Override
public Map createQrCode(String orderNo) {
try {
//1 根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no", orderNo);
Order order = orderService.getOne(wrapper);
//2 使用map设置生成二维码需要的参数
Map map = new HashMap();
map.put("appid", "wx74862e0dfcf69954");
map.put("mch_id", "1558950191");
map.put("nonce_str", WXPayUtil.generateNonceStr());
map.put("body", order.getCourseTitle());//课程名
map.put("out_trade_no", orderNo);//课程编号
map.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue() + "");
map.put("spbill_create_ip", "127.0.0.1");//服务器域名
map.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");//回调
map.put("trade_type", "NATIVE");//生成二维码的支付类型
//3 发送httpclient请求,传递参数xml格式,微信支付提供的固定地址
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
client.setXmlParam(WXPayUtil.generateSignedXml(map, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);//支持https
//4 得到发送请求返回结果
client.post();
String content = client.getContent();//返回的是xml格式,需要转换为map返回
Map<String, String> contentMap = WXPayUtil.xmlToMap(content);
//5 contentMap返回的值中,没有页面需要的其他数据,再封装成map
Map resMap = new HashMap<>();
resMap.put("out_trade_no", orderNo);
resMap.put("course_id", order.getCourseId());
resMap.put("total_fee", order.getTotalFee());
resMap.put("result_code", contentMap.get("result_code"));//二维码操作状态
resMap.put("code_url", contentMap.get("code_url"));//二维码地址
return resMap;
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001, "支付失败!");
}
}
4、监听支付状态
//每隔3秒调用一次查询订单状态方法
mounted() {
//计时器方法
this.timer = setInterval(() => {
//该参数为我们发送请求时的订单号
this.getPayStatus(this.payObj.out_trade_no);
}, 3000);
}
接口如下
/**
* 查询支付状态,并修改支付表和订单表
* @param orderNo 订单编号
* @return 支付状态
*/
@GetMapping("queryPayStatus/{orderNo}")
public R queryPayStatus(@PathVariable String orderNo) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_id",orderNo);
Map<String,String> map = payService.queryPayStatus(orderNo);
System.out.println("查询订单状态的map---->"+map);
if (map.isEmpty()){
return R.error().message("支付出错!");
}
//map不为空,获取订单状态返回
if (map.get("trade_state").equals("SUCCESS")){
//添加记录到支付表,并且更新订单表
payService.updateOrderStatus(map);
return R.ok().message("支付成功!");
}
return R.ok().code(25000).message("支付中...");
}
queryPayStatus()方法:
//根据订单号查询订单支付状态
@Override
public Map<String, String> queryPayStatus(String orderNo) {
try {
//1 封装参数
Map m = new HashMap<>();
m.put("appid", "wx74862e0dfcf69954");
m.put("mch_id", "1558950191");
m.put("out_trade_no", orderNo);
m.put("nonce_str", WXPayUtil.generateNonceStr());
//2 发送httpclient请求
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
client.setXmlParam(WXPayUtil.generateSignedXml(m, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
client.setHttps(true);
//3 发送
client.post();
String xml = client.getContent();
//4、转成Map
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
//5、返回
return resultMap;
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001, "查询支付状态失败!");
}
}
如果成功,调用保存支付记录方法payService.updateOrderStatus(map);保存支付信息,这个方法也会更新订单表中status字段,同时保存信息到支付记录表
//向支付表添加记录,同时更新订单表支付状态
@Override
public void updateOrderStatus(Map<String, String> map) {
//从查询订单状态返回的map中查找订单号
String orderNo = map.get("out_trade_no");
//根据订单号查询订单信息
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_no", orderNo);
Order order = orderService.getOne(wrapper);
//更新订单表状态
if (order.getStatus().intValue() == 1) {
return;
}
order.setStatus(1);//已支付
orderService.updateById(order);
//支付完成后向支付表添加数据
PayLog payLog = new PayLog();
payLog.setOrderNo(order.getOrderNo());//支付订单号
payLog.setPayTime(new Date());
payLog.setPayType(1);//支付类型
payLog.setTotalFee(order.getTotalFee());//总金额(分)
payLog.setTradeState(map.get("trade_state"));//支付状态
payLog.setTransactionId(map.get("transaction_id"));//流水号,微信返回的
payLog.setAttr(JSONObject.toJSONString(map));
baseMapper.insert(payLog);//插入到支付日志表
}
最后,携带课程信息跳转到课程页面,显示课程状态?
|