当前位置:   article > 正文

微信公众号开发

微信公众号开发

5. 微信公众号开发

5.1 微信公众号功能概览

在这里插入图片描述

在这里插入图片描述

5.1 功能实现

首先需要进行微信公众号注册,而项目由于需要支持微信支付等高级功能,因此需要注册服务号,订阅号不具备支付功能。而服务号必须基于企业注册,因此,我们在开发过程中选择注册测试号进行功能测试和实现。
具体申请过程参考以下链接:
微信公众平台
官方文档

申请好测试号后可以在其中看到账号的基本信息,如appID以及appsecret

在这里插入图片描述
在网站中也有关于公众号开发的相关功能列表,可以根据自己的需要实现开发。本项目涉及的微信公众号功能模块:自定义菜单、消息、微信支付、授权登录等。
通过扫码关注账号可以在右侧看到用户列表。
在这里插入图片描述

(1)公众号菜单管理与菜单同步实现

① 菜单页面管理

微信自定义菜单文档地址

微信自定义菜单注意事项:

  1. 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
  2. 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
  3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

项目自定义菜单

一级菜单:直播、课程、我的
二级菜单:根据一级菜单动态设置二级菜单,直播(近期直播课程),课程(课程分类),我的(我的订单、我的课程、我的优惠券及关于我们)

说明:
​ 1、二级菜单可以是网页类型,点击跳转H5页面
​ 2、二级菜单可以是消息类型,点击返回消息

菜单功能展示:
在这里插入图片描述
菜单数据格式

自定义菜单通过后台管理设置到数据库表,数据配置好后,通过微信接口推送菜单数据到微信平台。

表结构(menu):
在这里插入图片描述
管理页面

(1)页面功能“列表、添加、修改与删除”是对menu表的操作

(2)页面功能“同步菜单与删除菜单”是对微信平台接口操作
在这里插入图片描述

① 菜单同步实现
a.获取access_token

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。
接口文档

https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明:

参数说明
grant_type获取access_token填写client_credential
appid第三方用户唯一凭证
secret第三方用户唯一凭证密钥,即appsecret

后端接口实现:

service_wechat添加配置

# 硅谷课堂微信公众平台appId
wechat.mpAppId: wx09f201e9.......
# 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 6c999765c12c5.........
  • 1
  • 2
  • 3
  • 4

添加工具类ConstantPropertiesUtil.java

@Component
public class ConstantPropertiesUtil implements InitializingBean {

    @Value("${wechat.mpAppId}")
    private String appid;

    @Value("${wechat.mpAppSecret}")
    private String appsecret;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;

    @Override
    public void afterPropertiesSet() throws Exception {
        ACCESS_KEY_ID = appid;
        ACCESS_KEY_SECRET = appsecret;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

添加工具类HttpClient.java

添加Menucontroller方法

    //获取access_token
    @GetMapping("getAccessToken")
    public Result getAccessToken() {
        try {
            //拼接请求地址
            StringBuffer buffer = new StringBuffer();
            buffer.append("https://api.weixin.qq.com/cgi-bin/token");
            buffer.append("?grant_type=client_credential");
            buffer.append("&appid=%s");
            buffer.append("&secret=%s");
            //请求地址设置参数
            String url = String.format(buffer.toString(),
                    ConstantPropertiesUtil.ACCESS_KEY_ID,
                    ConstantPropertiesUtil.ACCESS_KEY_SECRET);
            //发送http请求
            String tokenString = HttpClientUtils.get(url);
            //获取access_token
            JSONObject jsonObject = JSONObject.parseObject(tokenString);
            String access_token = jsonObject.getString("access_token");
            //返回
            return Result.ok(access_token);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail(null);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
b.同步菜单(功能实现)

接口文档

weixin-java-mp:封装好了的微信接口客户端,使用起来很方便,后续我们就使用weixin-java-mp处理微信平台接口。在实际开发中作为依赖引入。

引入依赖

    <dependencies>
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-mp</artifactId>
            <version>4.1.0</version>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

添加配置类WeChatMpConfig.java

@Component
public class WeChatMpConfig {

    @Autowired
    private ConstantPropertiesUtil constantPropertiesUtil;

    @Bean
    public WxMpService wxMpService(){
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
        return wxMpService;
    }
    @Bean
    public WxMpConfigStorage wxMpConfigStorage(){
        WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
        wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
        wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);
        return wxMpConfigStorage;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

定义Service方法

MenuService

void syncMenu();
  • 1

实现Service方法

MenuServiceImpl

    @Autowired
    private WxMpService wxMpService;
   
    @SneakyThrows
    @Override
    public void syncMenu() {
        List<MenuVo> menuVoList = this.findMenuInfo();
        //菜单
        JSONArray buttonList = new JSONArray();
        for(MenuVo oneMenuVo : menuVoList) {
            JSONObject one = new JSONObject();
            one.put("name", oneMenuVo.getName());
            JSONArray subButton = new JSONArray();
            for(MenuVo twoMenuVo : oneMenuVo.getChildren()) {
                JSONObject view = new JSONObject();
                view.put("type", twoMenuVo.getType());
                if(twoMenuVo.getType().equals("view")) {
                    view.put("name", twoMenuVo.getName());
                    view.put("url", "http://ggkt2.vipgz1.91tunnel.com/#"
                            +twoMenuVo.getUrl());
                } else {
                    view.put("name", twoMenuVo.getName());
                    view.put("key", twoMenuVo.getMeunKey());
                }
                subButton.add(view);
            }
            one.put("sub_button", subButton);
            buttonList.add(one);
        }
        //菜单
        JSONObject button = new JSONObject();
        button.put("button", buttonList);
        this.wxMpService.getMenuService().menuCreate(button.toJSONString());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

controller方法

@ApiOperation(value = "同步菜单")
@GetMapping("syncMenu")
public Result createMenu() throws WxErrorException {
    menuService.syncMenu();
    return Result.ok(null);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(2)内网穿透实现

微信服务器无法直接访问用户本地,因此需要配置内网穿透地址,为两者建立连接,根据项目需要,建立两个内网穿透地址,分别对应8333和8080端口,即开发后端网关端口与前端页面端口。
在这里插入图片描述

(3)公众号消息

① 普通消息

实现效果,如下图所示。

1、根据关键字搜索相关课程,如:输入“java”,可返回java相关的一个课程;

2、点击菜单“关于我们”,返回关于我们的介绍

3、关注或取消关注等

在这里插入图片描述
首先需实现消息接入

参考文档

接入微信公众平台开发,开发者需要按照如下步骤完成:

1、填写服务器配置

2、验证服务器地址的有效性

3、依据接口文档实现业务逻辑

① 公众号服务器配置

在测试管理 -> 接口配置信息,点击“修改”按钮,填写服务器地址(URL)和Token,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)

说明:本地测试,url改为内网穿透地址(后端)

在这里插入图片描述

② 验证来自微信服务器消息

(1)概述

开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

参数描述
signature微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp时间戳
nonce随机数
echostr随机字符串

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1、将token、timestamp、nonce三个参数进行字典序排序

2、将三个参数字符串拼接成一个字符串进行sha1加密

3、开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

(2)代码实现

创建MessageController

@RestController
@RequestMapping("/api/wechat/message")
public class MessageController {

    private static final String token = "ggkt";

    /**
     * 服务器有效性验证
     * @param request
     * @return
     */
    @GetMapping
    public String verifyToken(HttpServletRequest request) {
        String signature = request.getParameter("signature");
        String timestamp = request.getParameter("timestamp");
        String nonce = request.getParameter("nonce");
        String echostr = request.getParameter("echostr");
        log.info("signature: {} nonce: {} echostr: {} timestamp: {}", signature, nonce, echostr, timestamp);
        if (this.checkSignature(signature, timestamp, nonce)) {
            log.info("token ok");
            return echostr;
        }
        return echostr;
    }

    private boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] str = new String[]{token, timestamp, nonce};
        //排序
        Arrays.sort(str);
        //拼接字符串
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < str.length; i++) {
            buffer.append(str[i]);
        }
        //进行sha1加密
        String temp = SHA1.encode(buffer.toString());
        //与微信提供的signature进行匹对
        return signature.equals(temp);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

③ 消息接收

消息接收接口和上面的服务器校验接口地址是一样的,都是我们一开始在公众号后台配置的地址。只不过消息接收接口是一个 POST 请求。

在公众号后台配置的时候,消息加解密方式选择了明文模式,这样在后台收到的消息直接就可以处理了。微信服务器给我发来的普通文本消息格式如下:

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test]]></Content>
    <MsgId>1234567890123456</MsgId>
</xml>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
参数描述
ToUserName开发者微信号
FromUserName发送方帐号(一个OpenID)
CreateTime消息创建时间 (整型)
MsgType消息类型,文本为text
Content文本消息内容
MsgId消息id,64位整型

当我们收到微信服务器发来的消息之后,我们就进行 XML 解析,提取出来我们需要的信息,去做相关的查询操作,再将查到的结果返回给微信服务器。

项目消息接收业务实现
MessageServiceImpl.java
文本:
—> text
事件:
subscribe---->关注
unsubscribe---->取消关注
aboutUs---->关于我们
search---->关键字搜索

@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    private CourseFeignClient courseFeignClient;

    @Autowired
    private WxMpService wxMpService;

    //接收消息
    @Override
    public String receiveMessage(Map<String, String> param) {
        String content = "";
        try {
            String msgType = param.get("MsgType");
            switch(msgType){
                case "text" :
                    content = this.search(param);
                    break;
                case "event" :
                    String event = param.get("Event");
                    String eventKey = param.get("EventKey");
                    if("subscribe".equals(event)) {//关注公众号
                        content = this.subscribe(param);
                    } else if("unsubscribe".equals(event)) {//取消关注公众号
                        content = this.unsubscribe(param);
                    } else if("CLICK".equals(event) && "aboutUs".equals(eventKey)){
                        content = this.aboutUs(param);
                    } else {
                        content = "success";
                    }
                    break;
                default:
                    content = "success";
            }
        } catch (Exception e) {
            e.printStackTrace();
            content = this.text(param, "请重新输入关键字,没有匹配到相关视频课程").toString();
        }
        return content;
    }

    /**
     * 关于我们
     * @param param
     * @return
     */
    private String aboutUs(Map<String, String> param) {
        return this.text(param, "硅谷课堂现开设Java、HTML5前端+全栈、大数据、全链路UI/UE设计、人工智能、大数据运维+Python自动化、Android+HTML5混合开发等多门课程;同时,通过视频分享、谷粒学苑在线课堂、大厂学苑直播课堂等多种方式,满足了全国编程爱好者对多样化学习场景的需求,已经为行业输送了大量IT技术人才。").toString();
    }

    /**
     * 处理关注事件
     * @param param
     * @return
     */
    private String subscribe(Map<String, String> param) {
        //处理业务
        return this.text(param, "感谢你关注“硅谷课堂”,可以根据关键字搜索您想看的视频教程,如:JAVA基础、Spring boot、大数据等").toString();
    }

     /**
     * 处理取消关注事件
     * @param param
     * @return
     */
    private String unsubscribe(Map<String, String> param) {
        //处理业务
        return "success";
    }

    /**
     * 处理关键字搜索事件
     * 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
     * @param param
     * @return
     */
    private String search(Map<String, String> param) {
        String fromusername = param.get("FromUserName");
        String tousername = param.get("ToUserName");
        String content = param.get("Content");
        //单位为秒,不是毫秒
        Long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();
        List<Course> courseList = courseFeignClient.findByKeyword(content);
        if(CollectionUtils.isEmpty(courseList)) {
            text = this.text(param, "请重新输入关键字,没有匹配到相关视频课程");
        } else {
            //一次只能返回一个
            Random random = new Random();
            int num = random.nextInt(courseList.size());
            Course course = courseList.get(num);
            StringBuffer articles = new StringBuffer();
            articles.append("<item>");
            articles.append("<Title><![CDATA["+course.getTitle()+"]]></Title>");
            articles.append("<Description><![CDATA["+course.getTitle()+"]]></Description>");
            articles.append("<PicUrl><![CDATA["+course.getCover()+"]]></PicUrl>");
            articles.append("<Url><![CDATA[http://glkt.atguigu.cn/#/liveInfo/"+course.getId()+"]]></Url>");
            articles.append("</item>");

            text.append("<xml>");
            text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
            text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
            text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
            text.append("<MsgType><![CDATA[news]]></MsgType>");
            text.append("<ArticleCount><![CDATA[1]]></ArticleCount>");
            text.append("<Articles>");
            text.append(articles);
            text.append("</Articles>");
            text.append("</xml>");
        }
        return text.toString();
    }

    /**
     * 回复文本
     * @param param
     * @param content
     * @return
     */
    private StringBuffer text(Map<String, String> param, String content) {
        String fromusername = param.get("FromUserName");
        String tousername = param.get("ToUserName");
        //单位为秒,不是毫秒
        Long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();
        text.append("<xml>");
        text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
        text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
        text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
        text.append("<MsgType><![CDATA[text]]></MsgType>");
        text.append("<Content><![CDATA["+content+"]]></Content>");
        text.append("</xml>");
        return text;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
② 模板消息

接口文档
模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。
在这里插入图片描述
本项目中需要的模板消息为订单支付成功通知,可以在模板消息接口部分进行设置,在页面中会显示用于接口调用的模板ID以及模板内容。
示例模板下载
在这里插入图片描述
模板消息接口封装

MessageController

添加方法

@GetMapping("/pushPayMessage")
public Result pushPayMessage() throws WxErrorException {
    messageService.pushPayMessage(1L);
    return Result.ok();
}
  • 1
  • 2
  • 3
  • 4
  • 5

MessageService

void pushPayMessage(Long orderId);
  • 1

首先需要获取openid值、模板id值。

openid值
在这里插入图片描述
模板id值
在这里插入图片描述
MessageServiceImpl类

	//订单成功
    @Override
    public void pushPayMessage(long id) {
        //微信openid
        String openid = "o5lra......Sig0E1zqc8sQU";
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                .toUser(openid)//要推送的用户openid
                .templateId("cGMWhnB....muHlmFRWuffVo0TnQmZFg0FZ6A")//模板id
                .url("前端内网穿透网址/#/pay/"+id)//点击模板消息要访问的网址
                .build();
        //3,如果是正式版发送消息,,这里需要配置你的信息
        templateMessage.addData(new WxMpTemplateData("first", "亲爱的用户:您有一笔订单支付成功。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", "1314520", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", "java基础课程", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword3", "100", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword4", "2022-01-11", "#272727"));
        templateMessage.addData(new WxMpTemplateData("remark", "感谢你购买课程,如有疑问,随时咨询!", "#272727"));
        try {
            String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

以上只是一个测试,具体返回模板信息会在支付功能中完成实现。

(4)公众号授权

如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。因此,在微信公众号开发中需要实现授权功能。

在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“设置与开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头。对应后端内网穿透域名。
在这里插入图片描述

在这里插入图片描述
网页授权流程分为四步:

1. 引导用户进入授权页面同意授权,获取code

  • 该页面地址:
    scope为snsapi_userinfo:
    https://open.weixin.qq.com/connect/oauth2/authorize?
    appid=wxf0e81c3bee622d60&
    redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&
    response_type=code&
    scope=snsapi_userinfo&
    state=STATE#wechat_redirect
  • 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。

2. 通过 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

3. 如果需要,开发者可以刷新网页授权access_token,避免过期

4. 通过网页授权access_token和 openid 获取用户基本信息(支持 UnionID 机制)

接口文档

在这里插入图片描述

授权登录接口实现

操作模块:service-user

① 引入微信工具包

<dependencies>
    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-mp</artifactId>
        <version>2.7.0</version>
    </dependency>

    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.1</version>
    </dependency>

    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
    </dependency>
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

② 添加配置

#公众号id和秘钥
# 硅谷课堂微信公众平台appId
wechat.mpAppId: wx09f201e9.....
## 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 6c999765....1850d28055e8b6e2eda
# 授权回调获取用户信息接口地址
wechat.userInfoUrl: http://.......unnel.com/api/user/wechat/userInfo
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

③ 添加工具类

@Component
public class ConstantPropertiesUtil implements InitializingBean {

    @Value("${wechat.mpAppId}")
    private String appid;

    @Value("${wechat.mpAppSecret}")
    private String appsecret;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;

    @Override
    public void afterPropertiesSet() throws Exception {
        ACCESS_KEY_ID = appid;
        ACCESS_KEY_SECRET = appsecret;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
@Component
public class WeChatMpConfig {

    @Autowired
    private ConstantPropertiesUtil constantPropertiesUtil;

    @Bean
    public WxMpService wxMpService(){
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
        return wxMpService;
    }

    @Bean
    public WxMpConfigStorage wxMpConfigStorage(){
        WxMpInMemoryConfigStorage wxMpConfigStorage = new WxMpInMemoryConfigStorage();
        wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
        wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);
        return wxMpConfigStorage;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

④ controller类

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

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private WxMpService wxMpService;

    @Value("${wechat.userInfoUrl}")
    private String userInfoUrl;

    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
        String redirectURL = wxMpService.oauth2buildAuthorizationUrl(userInfoUrl, 
                WxConsts.OAUTH2_SCOPE_USER_INFO, 
                URLEncoder.encode(returnUrl.replace("guiguketan", "#")));
        return "redirect:" + redirectURL;
    }

    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("code") String code,
                           @RequestParam("state") String returnUrl) throws Exception {
        WxMpOAuth2AccessToken wxMpOAuth2AccessToken = this.wxMpService.oauth2getAccessToken(code);
        String openId = wxMpOAuth2AccessToken.getOpenId();

        System.out.println("【微信网页授权】openId={}"+openId);

        WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);
        System.out.println("【微信网页授权】wxMpUser={}"+JSON.toJSONString(wxMpUser));

        UserInfo userInfo = userInfoService.getByOpenid(openId);
        if(null == userInfo) {
            userInfo = new UserInfo();
            userInfo.setOpenId(openId);
            userInfo.setUnionId(wxMpUser.getUnionId());
            userInfo.setNickName(wxMpUser.getNickname());
            userInfo.setAvatar(wxMpUser.getHeadImgUrl());
            userInfo.setSex(wxMpUser.getSexId());
            userInfo.setProvince(wxMpUser.getProvince());

            userInfoService.save(userInfo);
        }
        //生成token
        String token = JwtHelper.createToken(userInfo.getId(), userInfo.getNickName());
        if(returnUrl.indexOf("?") == -1) {
            return "redirect:" + returnUrl + "?token=" + token;
        } else {
            return "redirect:" + returnUrl + "&token=" + token;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

⑤ 编写UserInfoService

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

    @Override
    public UserInfo getByOpenid(String openId) {
        QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("open_id",openId);
        UserInfo userInfo = baseMapper.selectOne(wrapper);
        return userInfo;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
JWT工具

JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。

JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

JWT最重要的作用就是对 token信息的防伪作用。

JWT的原理

一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
在这里插入图片描述

(1)公共部分

主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。

(2)私有部分

用户自定义的内容,根据实际需要真正要封装的信息。

userInfo{用户的Id,用户的昵称nickName}

(3)签名部分

SaltiP: 当前服务器的Ip地址!{linux 中配置代理服务器的ip}

主要用户对JWT生成字符串的时候,进行加密{盐值}

base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。

整合JWT

(1)在service_utils模块添加依赖

<dependencies>
	<dependency>
		<groupId>org.apache.httpcomponents</groupId>
		<artifactId>httpclient</artifactId>
	</dependency>
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt</artifactId>
	</dependency>
	<dependency>
		<groupId>joda-time</groupId>
		<artifactId>joda-time</artifactId>
	</dependency>
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

(2)添加JWT工具类JwtHelper

//生成token
public class JwtHelper {
    //token字符串有效时间
    private static long tokenExpiration = 24*60*60*1000;
    //加密编码秘钥
    private static String tokenSignKey = "123456";

    //根据userid  和  username 生成token字符串
    public static String createToken(Long userId, String userName) {
        String token = Jwts.builder()
                //设置token分类
                .setSubject("GGKT-USER")
                //token字符串有效时长
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                //私有部分(用户信息)
                .claim("userId", userId)
                .claim("userName", userName)
                //根据秘钥使用加密编码方式进行加密,对字符串压缩
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    //从token字符串获取userid
    public static Long getUserId(String token) {
        if(StringUtils.isEmpty(token)) return null;
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        Integer userId = (Integer)claims.get("userId");
        return userId.longValue();
    }

    //从token字符串获取getUserName
    public static String getUserName(String token) {
        if(StringUtils.isEmpty(token)) return "";
        Jws<Claims> claimsJws
                = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("userName");
    }

    public static void main(String[] args) {
        String token = JwtHelper.createToken(1L, "lucy");
        System.out.println(token);
        System.out.println(JwtHelper.getUserId(token));
        System.out.println(JwtHelper.getUserName(token));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

(5) 视频点播

点播功能需求

(1)点击课程中的分类,根据分类查询课程列表
(2)点击 去看看,进入课程详情页面

在这里插入图片描述
由上图可知,一方面,通过课程一级名称(如后端开发)需要返回对应的子课程列表;
另一方面,针对某一课程页面,需要显示讲师信息级课程详细信息,课程大纲等内容,同时,可以点击观看按钮,进行视频播放。

1.编写课程列表和详情接口

实现列表及课程详情展示部分。

(1)创建CourseApiController

@Api(tags = "课程")
@RestController
@RequestMapping("/api/vod/course")
public class CourseApiController {

    @Autowired
    private CourseService courseService;

    @Autowired
    private ChapterService chapterService;

    //根据课程分类查询课程列表(分页)
    @ApiOperation("根据课程分类查询课程列表")
    @GetMapping("{subjectParentId}/{page}/{limit}")
    public Result findPageCourse(@ApiParam(value = "课程一级分类ID", required = true) @PathVariable Long subjectParentId,
                                 @ApiParam(name = "page", value = "当前页码", required = true) @PathVariable Long page, 
                                 @ApiParam(name = "limit", value = "每页记录数", required = true) @PathVariable Long limit) {
        //封装条件
        CourseQueryVo courseQueryVo = new CourseQueryVo();
        courseQueryVo.setSubjectParentId(subjectParentId);
        //创建page对象
        Page<Course> pageParam = new Page<>(page,limit);
        Map<String,Object> map = courseService.findPage(pageParam,courseQueryVo);
        return Result.ok(map);
    }

    //根据ID查询课程
    @ApiOperation("根据ID查询课程")
    @GetMapping("getInfo/{courseId}")
    public Result getInfo(
            @ApiParam(value = "课程ID", required = true)
            @PathVariable Long courseId){
        Map<String, Object> map = courseService.getInfoById(courseId);
        return Result.ok(map);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

(2)编写CourseService

//课程列表
Map<String,Object> findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo);

//根据id查询课程
Map<String, Object> getInfoById(Long courseId);
  • 1
  • 2
  • 3
  • 4
  • 5

(3)编写CourseServiceImpl

//课程列表
@Override
public Map<String,Object> findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo) {
    //获取条件值
    String title = courseQueryVo.getTitle();//名称
    Long subjectId = courseQueryVo.getSubjectId();//二级分类
    Long subjectParentId = courseQueryVo.getSubjectParentId();//一级分类
    Long teacherId = courseQueryVo.getTeacherId();//讲师
    //封装条件
    QueryWrapper<Course> wrapper = new QueryWrapper<>();
    if(!StringUtils.isEmpty(title)) {
        wrapper.like("title",title);
    }
    if(!StringUtils.isEmpty(subjectId)) {
        wrapper.eq("subject_id",subjectId);
    }
    if(!StringUtils.isEmpty(subjectParentId)) {
        wrapper.eq("subject_parent_id",subjectParentId);
    }
    if(!StringUtils.isEmpty(teacherId)) {
        wrapper.eq("teacher_id",teacherId);
    }
    //调用方法查询
    Page<Course> pages = baseMapper.selectPage(pageParam, wrapper);

    long totalCount = pages.getTotal();//总记录数
    long totalPage = pages.getPages();//总页数
    long currentPage = pages.getCurrent();//当前页
    long size = pages.getSize();//每页记录数
    //每页数据集合
    List<Course> records = pages.getRecords();
    records.stream().forEach(item -> {
        this.getTeacherOrSubjectName(item);
    });

    Map<String,Object> map = new HashMap<>();
    map.put("totalCount",totalCount);
    map.put("totalPage",totalPage);
    map.put("records",records);

    return map;
}

//获取讲师和分类名称
private Course getTeacherOrSubjectName(Course course) {
    Teacher teacher = teacherService.getById(course.getTeacherId());
    if(teacher != null) {
        course.getParam().put("teacherName",teacher.getName());
    }

    Subject subjectOne = subjectService.getById(course.getSubjectParentId());
    if(subjectOne != null) {
        course.getParam().put("subjectParentTitle",subjectOne.getTitle());
    }
    Subject subjectTwo = subjectService.getById(course.getSubjectId());
    if(subjectTwo != null) {
        course.getParam().put("subjectTitle",subjectTwo.getTitle());
    }
    return course;
}

//根据id查询课程
@Override
public Map<String, Object> getInfoById(Long id) {
    //更新流量量
    Course course = baseMapper.selectById(id);
    course.setViewCount(course.getViewCount() + 1);
    baseMapper.updateById(course);

    Map<String, Object> map = new HashMap<>();
    CourseVo courseVo = baseMapper.selectCourseVoById(id);
    List<ChapterVo> chapterVoList = chapterService.getNestedTreeList(id);
    CourseDescription courseDescription = descriptionService.getById(id);
    Teacher teacher = teacherService.getById(course.getTeacherId());
    
    //TODO后续完善
    Boolean isBuy = false;
    
    map.put("courseVo", courseVo);
    map.put("chapterVoList", chapterVoList);
    map.put("description", null != courseDescription ?
            courseDescription.getDescription() : "");
    map.put("teacher", teacher);
    map.put("isBuy", isBuy);//是否购买
    return map;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

(4)编写CourseMapper

public interface CourseMapper extends BaseMapper<Course> {

    CoursePublishVo selectCoursePublishVoById(Long id);

    CourseVo selectCourseVoById(Long id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(5)编写CourseMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.ggkt.vod.mapper.CourseMapper">

    <select id="selectCoursePublishVoById" resultType="com.atguigu.ggkt.vo.vod.CoursePublishVo">
        SELECT
        c.id,
        c.title,
        c.cover,
        c.lesson_num AS lessonNum,
        c.price,
        t.name AS teacherName,
        s1.title AS subjectParentTitle,
        s2.title AS subjectTitle
        FROM
        <include refid="tables" />
        WHERE c.id = #{id}
    </select>

    <select id="selectCourseVoById" resultType="com.atguigu.ggkt.vo.vod.CourseVo">
        SELECT
        <include refid="columns" />
        FROM
        <include refid="tables" />
        WHERE c.id = #{id}
    </select>
    
    <sql id="columns">
        c.id,
        c.title,
        c.lesson_num AS lessonNum,
        c.price,
        c.cover,
        c.buy_count AS buyCount,
        c.view_count AS viewCount,
        c.status,
        c.publish_time AS publishTime,
        c.teacher_id as teacherId,
        t.name AS teacherName,
        s1.title AS subjectParentTitle,
        s2.title AS subjectTitle
    </sql>
    
    <sql id="tables">
        course c
        LEFT JOIN teacher t ON c.teacher_id = t.id
        LEFT JOIN subject s1 ON c.subject_parent_id = s1.id
        LEFT JOIN subject s2 ON c.subject_id = s2.id
    </sql>
</mapper>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
2、点播视频播放

获取视频播放参数

(1)创建VodApiController

@Api(tags = "腾讯视频点播")
@RestController
@RequestMapping("/api/vod")
public class VodApiController {

    @Autowired
    private VodService vodService;

    @GetMapping("getPlayAuth/{courseId}/{videoId}")
    public Result getPlayAuth(
            @ApiParam(value = "课程id", required = true)
            @PathVariable Long courseId,
            @ApiParam(value = "视频id", required = true)
            @PathVariable Long videoId) {
        return  Result.ok(vodService.getPlayAuth(courseId, videoId));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

(3)application.properties添加

tencent.video.appid=1312624373
  • 1

(3)VodService创建方法

//获取视频播放凭证
Map<String,Object> getPlayAuth(Long courseId, Long videoId);
  • 1
  • 2

(4)VodServiceImpl实现方法

@Value("${tencent.video.appid}")
private String appId;

//点播视频播放接口
@Override
public Map<String, Object> getPlayAuth(Long courseId, Long videoId) {
    //根据小节id获取小节对象,获取腾讯云视频id
    Video video = videoService.getById(videoId);
    if(video == null) {
        throw new GgktException(20001,"小节信息不存在");
    }

    Map<String, Object> map = new HashMap<>();
    map.put("videoSourceId",video.getVideoSourceId());
    map.put("appId",appId);
    return map;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

(6) 支付

① 订单生成

(1)用户点击菜单中“课程”的二级菜单“后端开发”
(2)点击去看看查看课程基本信息
(3)点击立即购买,生成课程订单
在这里插入图片描述
创建service_order模块

生成收费课程订单接口对实现应包括以下几步:

1.获取当前微信用户Id,课程id
2.根据用户id获取用户信息------service_user
2.根据课程id获取课程信息------service_vod
3.获取优惠券信息-----service_activity
4.添加订单信息到订单列表------service_order

接口实现
① 编写创建订单接口

(1)创建OrderInfoApiController

@RestController
@RequestMapping("api/order/orderInfo")
public class OrderInfoApiController {

    @Autowired
    private OrderInfoService orderInfoService;

    @ApiOperation("新增点播课程订单")
    @PostMapping("submitOrder")
    public Result submitOrder(@RequestBody OrderFormVo orderFormVo, HttpServletRequest request) {
        //返回订单id
        Long orderId = orderInfoService.submitOrder(orderFormVo);
        return Result.ok(orderId);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

(2)编写Service

OrderInfoService

//生成点播课程订单
Long submitOrder(OrderFormVo orderFormVo);
  • 1
  • 2

创建获取课程信息接口

操作service_vod模块

(1)CourseApiController添加方法

@ApiOperation("根据ID查询课程")
@GetMapping("inner/getById/{courseId}")
public Course getById(
        @ApiParam(value = "课程ID", required = true)
        @PathVariable Long courseId){
    return courseService.getById(courseId);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(2)service_course_client定义方法

@ApiOperation("根据ID查询课程")
@GetMapping("/api/vod/course/inner/getById/{courseId}")
Course getById(@PathVariable Long courseId);
  • 1
  • 2
  • 3

③ 创建获取优惠券接口

操作service_activity模块

(1)创建CouponInfoApiController

@Api(tags = "优惠券接口")
@RestController
@RequestMapping("/api/activity/couponInfo")
public class CouponInfoApiController {

	@Autowired
	private CouponInfoService couponInfoService;

	@ApiOperation(value = "获取优惠券")
	@GetMapping(value = "inner/getById/{couponId}")
	public CouponInfo getById(@PathVariable("couponId") Long couponId) {
		return couponInfoService.getById(couponId);
	}
    
    @ApiOperation(value = "更新优惠券使用状态")
	@GetMapping(value = "inner/updateCouponInfoUseStatus/{couponUseId}/{orderId}")
	public Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId) {
		couponInfoService.updateCouponInfoUseStatus(couponUseId, orderId);
		return true;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

(2)编写CouponInfoService

    @Override
    public void updateCouponInfoUseStatus(Long couponUseId, Long orderId) {
        CouponUse couponUse = new CouponUse();
        couponUse.setId(couponUseId);
        couponUse.setOrderId(orderId);
        couponUse.setCouponStatus("1");
        couponUse.setUsingTime(new Date());
        couponUseService.updateById(couponUse);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(3)创建service-activity-client模块定义接口

@FeignClient(value = "service-activity")
public interface CouponInfoFeignClient {

    @ApiOperation(value = "获取优惠券")
    @GetMapping(value = "/api/activity/couponInfo/inner/getById/{couponId}")
    CouponInfo getById(@PathVariable("couponId") Long couponId);
    
    /**
     * 更新优惠券使用状态
     */
    @GetMapping(value = "/api/activity/couponInfo/inner/updateCouponInfoUseStatus/{couponUseId}/{orderId}")
    Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

④ 获取当前用户id

(1)common模块引入依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(2)复制工具类到common下的service_utils模块

对于前端页面,在授权成功后返回token值,包含id和NickName,利用http请求拦截器获取localStorage里面的token值,并存到LocalStorage中,在每次发送ajax请求时获取token值,放到请求头里进行传递。
在接口中,设置好哪些路径需要token,在方法中从请求头获取token字符串,就可以得到用户的id。

⑤ 生成订单Service

(1)service_order引入依赖

(2)OrderInfoServiceImpl

@Autowired
private CourseFeignClient courseFeignClient;

@Autowired
private UserInfoFeignClient userInfoFeignClient;

@Autowired
private CouponInfoFeignClient couponInfoFeignClient;

//生成点播课程订单
@Override
public Long submitOrder(OrderFormVo orderFormVo) {
    Long userId = AuthContextHolder.getUserId();
    Long courseId = orderFormVo.getCourseId();
    Long couponId = orderFormVo.getCouponId();
    //查询当前用户是否已有当前课程的订单
    LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(OrderDetail::getCourseId, courseId);
    queryWrapper.eq(OrderDetail::getUserId, userId);
    OrderDetail orderDetailExist = orderDetailService.getOne(queryWrapper);
    if(orderDetailExist != null){
        return orderDetailExist.getId(); //如果订单已存在,则直接返回订单id
    }

    //查询课程信息
    Course course = courseFeignClient.getById(courseId);
    if (course == null) {
        throw new GlktException(ResultCodeEnum.DATA_ERROR.getCode(),
                ResultCodeEnum.DATA_ERROR.getMessage());
    }

    //查询用户信息
    UserInfo userInfo = userInfoFeignClient.getById(userId);
    if (userInfo == null) {
        throw new GlktException(ResultCodeEnum.DATA_ERROR.getCode(),
                ResultCodeEnum.DATA_ERROR.getMessage());
    }

    //优惠券金额
    BigDecimal couponReduce = new BigDecimal(0);
    if(null != couponId) {
        CouponInfo couponInfo = couponInfoFeignClient.getById(couponId);
        couponReduce = couponInfo.getAmount();
    }

    //创建订单
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setUserId(userId);
    orderInfo.setNickName(userInfo.getNickName());
    orderInfo.setPhone(userInfo.getPhone());
    orderInfo.setProvince(userInfo.getProvince());
    orderInfo.setOriginAmount(course.getPrice());
    orderInfo.setCouponReduce(couponReduce);
    orderInfo.setFinalAmount(orderInfo.getOriginAmount().subtract(orderInfo.getCouponReduce()));
    orderInfo.setOutTradeNo(OrderNoUtils.getOrderNo());
    orderInfo.setTradeBody(course.getTitle());
    orderInfo.setOrderStatus("0");
    this.save(orderInfo);

    OrderDetail orderDetail = new OrderDetail();
    orderDetail.setOrderId(orderInfo.getId());
    orderDetail.setUserId(userId);
    orderDetail.setCourseId(courseId);
    orderDetail.setCourseName(course.getTitle());
    orderDetail.setCover(course.getCover());
    orderDetail.setOriginAmount(course.getPrice());
    orderDetail.setCouponReduce(new BigDecimal(0));
    orderDetail.setFinalAmount(orderDetail.getOriginAmount().subtract(orderDetail.getCouponReduce()));
    orderDetailService.save(orderDetail);

    //更新优惠券状态
    if(null != orderFormVo.getCouponUseId()) {
        couponInfoFeignClient.updateCouponInfoUseStatus(orderFormVo.getCouponUseId(), orderInfo.getId());
    }
    return orderInfo.getId();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
② 支付功能

由于测试号功能的限制,此部分只能进行测试,不能实际实现支付。
接口文档

(1)绑定域名

先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS接口安全域名”。

说明:因为测试号不支持支付功能,需要使用正式号才能进行测试。
在这里插入图片描述
(2)商户平台配置支付目录

(3)微信支付接口开发

① 创建WXPayController

@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/api/order/wxPay")
public class WXPayController {

    @Autowired
    private WXPayService wxPayService;

    @ApiOperation(value = "下单 小程序支付")
    @GetMapping("/createJsapi/{orderNo}")
    public Result createJsapi(
            @ApiParam(name = "orderNo", value = "订单No", required = true)
            @PathVariable("orderNo") String orderNo) {
        return Result.ok(wxPayService.createJsapi(orderNo));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

② 创建WXPayService

public interface WXPayService {
	Map createJsapi(String orderNo);
}
  • 1
  • 2
  • 3

③ service_order引入依赖

<dependency>
	<groupId>com.github.wxpay</groupId>
	<artifactId>wxpay-sdk</artifactId>
	<version>0.0.3</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

④ 创建WXPayServiceImpl

@Service
@Slf4j
public class WXPayServiceImpl implements WXPayService {

	@Autowired
	private OrderInfoService orderInfoService;
	@Resource
	private UserInfoFeignClient userInfoFeignClient;

	@Override
	public Map<String, String> createJsapi(String orderNo) {
		try {

			Map<String, String> paramMap = new HashMap();
			//1、设置参数
			paramMap.put("appid", "wxf...a3a2c7eeeb");
			paramMap.put("mch_id", "14....42");
			paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
			paramMap.put("body", "test");
			paramMap.put("out_trade_no", orderNo);
			paramMap.put("total_fee", "1");
			paramMap.put("spbill_create_ip", "127.0.0.1");
			paramMap.put("notify_url", "http://....igu.cn/api/order/wxPay/notify");
			paramMap.put("trade_type", "JSAPI");
			paramMap.put("openid", "oQTXC...OCkKCImHtHoLL");

			//2、HTTPClient来根据URL访问第三方接口并且传递参数
			HttpClientUtils client = new HttpClientUtils("https://api.mch.w...in.qq.com/pay/unifiedorder");

			//client设置参数
			client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9"));
			client.setHttps(true);
			client.post();
			//3、返回第三方的数据
			String xml = client.getContent();
			Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
			if(null != resultMap.get("result_code")  && !"SUCCESS".equals(resultMap.get("result_code"))) {
				System.out.println("error1");
			}

			//4、再次封装参数
			Map<String, String> parameterMap = new HashMap<>();
			String prepayId = String.valueOf(resultMap.get("prepay_id"));
			String packages = "prepay_id=" + prepayId;
			parameterMap.put("appId", "wxf913bfa3a2c7eeeb");
			parameterMap.put("nonceStr", resultMap.get("nonce_str"));
			parameterMap.put("package", packages);
			parameterMap.put("signType", "MD5");
			parameterMap.put("timeStamp", String.valueOf(new Date().getTime()));
			String sign = WXPayUtil.generateSignature(parameterMap, "MXb72b9RfshXZD4FR...bx9LT9");

			//返回结果
			Map<String, String> result = new HashMap();
			result.put("appId", "wxf913bfa3a2c7eeeb");
			result.put("timeStamp", parameterMap.get("timeStamp"));
			result.put("nonceStr", parameterMap.get("nonceStr"));
			result.put("signType", "MD5");
			result.put("paySign", sign);
			result.put("package", packages);
			System.out.println(result);
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			return new HashMap<>();
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

订单完成支付后的效果:
请添加图片描述

(7)直播

直播第三方工具:欢拓云直播

首先完成账号的注册和基本配置,在直播管理中可以创建直播,同时主播端下载“云直播客户端”,“频道id与密码”为直播客户端的登录账号。
在这里插入图片描述
在这里插入图片描述
接口文档地址
我们可以通过SDK完成对直播接口的功能实现与配置。
SDK下载地址

1.网页端直播功能

① 模块搭建
(1)创建service_live模块
(2)添加依赖

添加直播SDK需要的依赖

<!-- 直播  -->
<dependency>
    <groupId>commons-httpclient</groupId>
    <artifactId>commons-httpclient</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
    <classifier>jdk15</classifier>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(3)集成代码

将SDK文件复制到service_live模块下。
在这里插入图片描述

(4)更改配置

更改MTCloud类配置

说明:

​ 1、更改openID与openToken

​ 2、该类官方已经做了接口集成,我们可以直接使用。

public class MTCloud {

    /**
     * 合作方ID: 合作方在欢拓平台的唯一ID
     */
    public String openID = "37013";

    /**
     * 合作方秘钥: 合作方ID对应的参数加密秘钥
     */
    public String openToken = "5cfa64c1be5f479aea8296bb4e2c37d3";
    
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

(5)在配置文件application.properties中指明openId与openToken信息

mtcloud.openId=43873
mtcloud.openToken=1f3681df876eb31474be8c479b9f1ffe
  • 1
  • 2

② 功能实现
在这里插入图片描述

由上图可知,在网页端,直播页面管理需要实现的功能:

1.获取分页列表(index)

  • 其中包括:封面,直播名称,直播时间,直播老师,头衔,创建时间。
  • 需在service_vod模块创建接口获取讲师信息getTeacherLive
  • 并在service_course_client定义接口getTeacherLive

2.添加

@Resource
private LiveCourseAccountService liveCourseAccountService;

@Resource
private LiveCourseDescriptionService liveCourseDescriptionService;

@Autowired
private CourseFeignClient teacherFeignClient;

@Resource
private MTCloud mtCloudClient;
@SneakyThrows
@Transactional(rollbackFor = {Exception.class})
@Override
public Boolean save(LiveCourseFormVo liveCourseFormVo) {
    LiveCourse liveCourse = new LiveCourse();
    BeanUtils.copyProperties(liveCourseFormVo, liveCourse);

    Teacher teacher = teacherFeignClient.getTeacherLive(liveCourseFormVo.getTeacherId());
    HashMap<Object, Object> options = new HashMap<>();
    options.put("scenes", 2);//直播类型。1: 教育直播,2: 生活直播。默认 1,说明:根据平台开通的直播类型填写
    options.put("password", liveCourseFormVo.getPassword());
    String res = mtCloudClient.courseAdd(liveCourse.getCourseName(), teacher.getId().toString(), new DateTime(liveCourse.getStartTime()).toString("yyyy-MM-dd HH:mm:ss"), new DateTime(liveCourse.getEndTime()).toString("yyyy-MM-dd HH:mm:ss"), teacher.getName(), teacher.getIntro(), options);

    System.out.println("return:: "+res);
    CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
    if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
        JSONObject object = commonResult.getData();
        liveCourse.setCourseId(object.getLong("course_id"));
        baseMapper.insert(liveCourse);

        //保存课程详情信息
        LiveCourseDescription liveCourseDescription = new LiveCourseDescription();
        liveCourseDescription.setDescription(liveCourseFormVo.getDescription());
        liveCourseDescription.setLiveCourseId(liveCourse.getId());
        liveCourseDescriptionService.save(liveCourseDescription);

        //保存课程账号信息
        LiveCourseAccount liveCourseAccount = new LiveCourseAccount();
        liveCourseAccount.setLiveCourseId(liveCourse.getId());
        liveCourseAccount.setZhuboAccount(object.getString("bid"));
        liveCourseAccount.setZhuboPassword(liveCourseFormVo.getPassword());
        liveCourseAccount.setAdminKey(object.getString("admin_key"));
        liveCourseAccount.setUserKey(object.getString("user_key"));
        liveCourseAccount.setZhuboKey(object.getString("zhubo_key"));
        liveCourseAccountService.save(liveCourseAccount);
    } else {
        String getmsg = commonResult.getmsg();
        throw new GlktException(20001,getmsg);
    }
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

需要实现的功能是在网页端添加直播的同时,在欢拓云中也添加相应的直播。
在这里插入图片描述
在这里插入图片描述
请添加图片描述

3.修改
4.删除
5.查看账号配置信息

2.公众号直播对接

① 用户观看端集成

接口文档

(1) 获取用户access_token

用户要观看直播,必须获取对应的用户access_token,通过access_token 获取观看的直播课程;

接口参数:直播id,用户id

a. 创建LiveCourseApiController

@RestController
@RequestMapping("api/live/liveCourse")
public class LiveCourseApiController {

	@Resource
	private LiveCourseService liveCourseService;

    @ApiOperation(value = "获取用户access_token")
    @GetMapping("getPlayAuth/{id}")
    public Result<JSONObject> getPlayAuth(@PathVariable Long id) {
        JSONObject object = liveCourseService.getPlayAuth(id, AuthContextHolder.getUserId());
        return Result.ok(object);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

b. LiveCourseService添加方法

JSONObject getPlayAuth(Long id, Long userId);
  • 1

c. LiveCourseServiceImpl实现方法

@SneakyThrows
@Override
public JSONObject getPlayAuth(Long id, Long userId) {
    LiveCourse liveCourse = this.getById(id);
    UserInfo userInfo = userInfoFeignClient.getById(userId);
    HashMap<Object,Object> options = new HashMap<Object, Object>();
    String res = mtCloudClient.courseAccess(liveCourse.getCourseId().toString(), userId.toString(), userInfo.getNickName(), MTCloud.ROLE_USER, 80*80*80, options);
    CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
    if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
        JSONObject object = commonResult.getData();
        System.out.println("access::"+object.getString("access_token"));
        return object;
    } else {
        throw new GgktException(20001,"获取失败");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(2)下载前端SDK

下载地址:https://open.talk-fun.com/docs/js/download.html

(3)与前端项目结合

http://localhost:8080/live.html为直播观看访问方式

(8)分享

参考文档

① 绑定域名

先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS接口安全域名”。

说明:本地测试设置内网穿透地址。
在这里插入图片描述

② 前端:引入JS文件,引入前端项目/public/index.html文件,封装分享js

④ 服务器端接口

新增ShareController类

说明:微信分享要对当前url加密处理,由于我们的url路由都是带“#”符号,服务器端接收不到,因此通过“guiguketan”单词代替了“#”。

@RestController
@RequestMapping("/api/wechat/share")
@Slf4j
public class ShareController {

    @Autowired
    private WxMpService wxMpService;

    @GetMapping("/getSignature")
    public Result getSignature(@RequestParam("url") String url) throws WxErrorException {
        String currentUrl = url.replace("guiguketan", "#");
        WxJsapiSignature jsapiSignature = wxMpService.createJsapiSignature(currentUrl);

        WxJsapiSignatureVo wxJsapiSignatureVo = new WxJsapiSignatureVo();
        BeanUtils.copyProperties(jsapiSignature, wxJsapiSignatureVo);
        wxJsapiSignatureVo.setUserEedId(Base64Util.base64Encode(AuthContextHolder.getUserId()+""));
        return Result.ok(wxJsapiSignatureVo);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

测试功能:
请添加图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/280693
推荐阅读
相关标签
  

闽ICP备14008679号