自(zì)強不息    厚德載物(wù)

微(wēi)信公衆号開(kāi)發

  2023/7/20 9:00:00   【次浏覽】 本站

微(wēi)信公衆号開(kāi)發

5. 微(wēi)信公衆号開(kāi)發

5.1 微(wēi)信公衆号功能概覽

5.1 功能實現(xiàn)

(1)公衆号菜單管理(lǐ)與菜單同步實現(xiàn)

① 菜單頁面管理(lǐ)

① 菜單同步實現(xiàn)

a.獲取access_token

b.同步菜單(功能實現(xiàn))

(2)內(nèi)網穿透實現(xiàn)

(3)公衆号消息

① 普通(tōng)消息

② 模闆消息

(4)公衆号授權

JWT工(gōng)具

(5) 視(shì)頻(pín)點播

1.編寫課程列表和(hé)詳情接口

2、點播視(shì)頻(pín)播放(fàng)

(6) 支付

① 訂單生(shēng)成

② 支付功能

(7)直播

1.網頁端直播功能

2.公衆号直播對(duì)接

(8)分享

5. 微(wēi)信公衆号開(kāi)發

5.1 微(wēi)信公衆号功能概覽





5.1 功能實現(xiàn)

首先需要進行微(wēi)信公衆号注冊,而項目由于需要支持微(wēi)信支付等高(gāo)級功能,因此需要注冊服務号,訂閱号不具備支付功能。而服務号必須基于企業(yè)注冊,因此,我們在開(kāi)發過程中選擇注冊測試号進行功能測試和(hé)實現(xiàn)。

具體(tǐ)申請過程參考以下(xià)鏈接:

微(wēi)信公衆平台

官方文(wén)檔


申請好(hǎo)測試号後可以在其中看(kàn)到賬号的(de)基本信息,如appID以及appsecret



在網站中也有(yǒu)關于公衆号開(kāi)發的(de)相(xiàng)關功能列表,可以根據自(zì)己的(de)需要實現(xiàn)開(kāi)發。本項目涉及的(de)微(wēi)信公衆号功能模塊:自(zì)定義菜單、消息、微(wēi)信支付、授權登錄等。

通(tōng)過掃碼關注賬号可以在右側看(kàn)到用戶列表。



(1)公衆号菜單管理(lǐ)與菜單同步實現(xiàn)

① 菜單頁面管理(lǐ)

微(wēi)信自(zì)定義菜單文(wén)檔地(dì)址


微(wēi)信自(zì)定義菜單注意事(shì)項:


自(zì)定義菜單最多包括3個(gè)一級菜單,每個(gè)一級菜單最多包含5個(gè)二級菜單。

一級菜單最多4個(gè)漢字,二級菜單最多8個(gè)漢字,多出來(lái)的(de)部分将會以“…”代替。

創建自(zì)定義菜單後,菜單的(de)刷新策略是,在用戶進入公衆号會話(huà)頁或公衆号profile頁時(shí),如果發現(xiàn)上(shàng)一次拉取菜單的(de)請求在5分鐘(zhōng)以前,就會拉取一下(xià)菜單,如果菜單有(yǒu)更新,就會刷新客戶端的(de)菜單。測試時(shí)可以嘗試取消關注公衆賬号後再次關注,則可以看(kàn)到創建後的(de)效果。

項目自(zì)定義菜單


一級菜單:直播、課程、我的(de)

二級菜單:根據一級菜單動态設置二級菜單,直播(近(jìn)期直播課程),課程(課程分類),我的(de)(我的(de)訂單、我的(de)課程、我的(de)優惠券及關于我們)


說(shuō)明(míng):

1、二級菜單可以是網頁類型,點擊跳(tiào)轉H5頁面

2、二級菜單可以是消息類型,點擊返回消息


菜單功能展示:


菜單數據格式


自(zì)定義菜單通(tōng)過後台管理(lǐ)設置到數據庫表,數據配置好(hǎo)後,通(tōng)過微(wēi)信接口推送菜單數據到微(wēi)信平台。


表結構(menu):


管理(lǐ)頁面


(1)頁面功能“列表、添加、修改與删除”是對(duì)menu表的(de)操作(zuò)


(2)頁面功能“同步菜單與删除菜單”是對(duì)微(wēi)信平台接口操作(zuò)



① 菜單同步實現(xiàn)

a.獲取access_token

access_token是公衆号的(de)全局唯一接口調用憑據,公衆号調用各接口時(shí)都(dōu)需使用access_token。

接口文(wén)檔


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


參數說(shuō)明(míng):


參數 說(shuō)明(míng)

grant_type 獲取access_token填寫client_credential

appid 第三方用戶唯一憑證

secret 第三方用戶唯一憑證密鑰,即appsecret

後端接口實現(xiàn):


service_wechat添加配置


# 矽谷課堂微(wēi)信公衆平台appId

wechat.mpAppId: wx09f201e9.......

# 矽谷課堂微(wēi)信公衆平台api秘鑰

wechat.mpAppSecret: 6c999765c12c5.........

1

2

3

4

添加工(gōng)具類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;

    }

}


添加工(gōng)具類HttpClient.java


添加Menucontroller方法


    //獲取access_token

    @GetMapping("getAccessToken")

    public Result getAccessToken() {

        try {

            //拼接請求地(dì)址

            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");

            //請求地(dì)址設置參數

            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);

        }

    }



b.同步菜單(功能實現(xiàn))

接口文(wén)檔


weixin-java-mp:封裝好(hǎo)了的(de)微(wēi)信接口客戶端,使用起來(lái)很(hěn)方便,後續我們就使用weixin-java-mp處理(lǐ)微(wēi)信平台接口。在實際開(kāi)發中作(zuò)為(wèi)依賴引入。


引入依賴


    <dependencies>

        <dependency>

            <groupId>com.github.binarywang</groupId>

            <artifactId>weixin-java-mp</artifactId>

            <version>4.1.0</version>

        </dependency>

    </dependencies>


添加配置類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;

    }

}


定義Service方法


MenuService


void syncMenu();

1

實現(xiàn)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());

    }


controller方法


@ApiOperation(value = "同步菜單")

@GetMapping("syncMenu")

public Result createMenu() throws WxErrorException {

    menuService.syncMenu();

    return Result.ok(null);

}


(2)內(nèi)網穿透實現(xiàn)

微(wēi)信服務器(qì)無法直接訪問(wèn)用戶本地(dì),因此需要配置內(nèi)網穿透地(dì)址,為(wèi)兩者建立連接,根據項目需要,建立兩個(gè)內(nèi)網穿透地(dì)址,分别對(duì)應8333和(hé)8080端口,即開(kāi)發後端網關端口與前端頁面端口。



(3)公衆号消息

① 普通(tōng)消息

實現(xiàn)效果,如下(xià)圖所示。


1、根據關鍵字搜索相(xiàng)關課程,如:輸入“java”,可返回java相(xiàng)關的(de)一個(gè)課程;


2、點擊菜單“關于我們”,返回關于我們的(de)介紹


3、關注或取消關注等



首先需實現(xiàn)消息接入


參考文(wén)檔


接入微(wēi)信公衆平台開(kāi)發,開(kāi)發者需要按照(zhào)如下(xià)步驟完成:


1、填寫服務器(qì)配置


2、驗證服務器(qì)地(dì)址的(de)有(yǒu)效性


3、依據接口文(wén)檔實現(xiàn)業(yè)務邏輯


① 公衆号服務器(qì)配置


在測試管理(lǐ) -> 接口配置信息,點擊“修改”按鈕,填寫服務器(qì)地(dì)址(URL)和(hé)Token,其中URL是開(kāi)發者用來(lái)接收微(wēi)信消息和(hé)事(shì)件(jiàn)的(de)接口URL。Token可由開(kāi)發者可以任意填寫,用作(zuò)生(shēng)成簽名(該Token會和(hé)接口URL中包含的(de)Token進行比對(duì),從(cóng)而驗證安全性)


說(shuō)明(míng):本地(dì)測試,url改為(wèi)內(nèi)網穿透地(dì)址(後端)




② 驗證來(lái)自(zì)微(wēi)信服務器(qì)消息


(1)概述


開(kāi)發者提交信息後,微(wēi)信服務器(qì)将發送GET請求到填寫的(de)服務器(qì)地(dì)址URL上(shàng),GET請求攜帶參數如下(xià)表所示:


參數 描述

signature 微(wēi)信加密簽名,signature結合了開(kāi)發者填寫的(de)token參數和(hé)請求中的(de)timestamp參數、nonce參數。

timestamp 時(shí)間(jiān)戳

nonce 随機(jī)數

echostr 随機(jī)字符串

開(kāi)發者通(tōng)過檢驗signature對(duì)請求進行校(xiào)驗(下(xià)面有(yǒu)校(xiào)驗方式)。若确認此次GET請求來(lái)自(zì)微(wēi)信服務器(qì),請原樣返回echostr參數內(nèi)容,則接入生(shēng)效,成為(wèi)開(kāi)發者成功,否則接入失敗。加密/校(xiào)驗流程如下(xià):


1、将token、timestamp、nonce三個(gè)參數進行字典序排序


2、将三個(gè)參數字符串拼接成一個(gè)字符串進行sha1加密


3、開(kāi)發者獲得加密後的(de)字符串可與signature對(duì)比,标識該請求來(lái)源于微(wēi)信


(2)代碼實現(xiàn)


創建MessageController


@RestController

@RequestMapping("/api/wechat/message")

public class MessageController {


    private static final String token = "ggkt";


    /**

     * 服務器(qì)有(yǒu)效性驗證

     * @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());

        //與微(wēi)信提供的(de)signature進行匹對(duì)

        return signature.equals(temp);

    }

}


③ 消息接收


消息接收接口和(hé)上(shàng)面的(de)服務器(qì)校(xiào)驗接口地(dì)址是一樣的(de),都(dōu)是我們一開(kāi)始在公衆号後台配置的(de)地(dì)址。隻不過消息接收接口是一個(gè) POST 請求。


在公衆号後台配置的(de)時(shí)候,消息加解密方式選擇了明(míng)文(wén)模式,這(zhè)樣在後台收到的(de)消息直接就可以處理(lǐ)了。微(wēi)信服務器(qì)給我發來(lái)的(de)普通(tōng)文(wén)本消息格式如下(xià):


<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>


參數 描述

ToUserName 開(kāi)發者微(wēi)信号

FromUserName 發送方帳号(一個(gè)OpenID)

CreateTime 消息創建時(shí)間(jiān) (整型)

MsgType 消息類型,文(wén)本為(wèi)text

Content 文(wén)本消息內(nèi)容

MsgId 消息id,64位整型

當我們收到微(wēi)信服務器(qì)發來(lái)的(de)消息之後,我們就進行 XML 解析,提取出來(lái)我們需要的(de)信息,去(qù)做相(xiàng)關的(de)查詢操作(zuò),再将查到的(de)結果返回給微(wēi)信服務器(qì)。


項目消息接收業(yè)務實現(xiàn)

MessageServiceImpl.java

文(wén)本:

—> text

事(shì)件(jiàn):

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, "請重新輸入關鍵字,沒有(yǒu)匹配到相(xiàng)關視(shì)頻(pín)課程").toString();

        }

        return content;

    }


    /**

     * 關于我們

     * @param param

     * @return

     */

    private String aboutUs(Map<String, String> param) {

        return this.text(param, "矽谷課堂現(xiàn)開(kāi)設Java、HTML5前端+全棧、大數據、全鏈路(lù)UI/UE設計(jì)、人(rén)工(gōng)智能、大數據運維+Python自(zì)動化(huà)、Android+HTML5混合開(kāi)發等多門課程;同時(shí),通(tōng)過視(shì)頻(pín)分享、谷粒學苑在線課堂、大廠(chǎng)學苑直播課堂等多種方式,滿足了全國(guó)編程愛好(hǎo)者對(duì)多樣化(huà)學習(xí)場(chǎng)景的(de)需求,已經為(wèi)行業(yè)輸送了大量IT技(jì)術人(rén)才。").toString();

    }


    /**

     * 處理(lǐ)關注事(shì)件(jiàn)

     * @param param

     * @return

     */

    private String subscribe(Map<String, String> param) {

        //處理(lǐ)業(yè)務

        return this.text(param, "感謝(xiè)你(nǐ)關注“矽谷課堂”,可以根據關鍵字搜索您想看(kàn)的(de)視(shì)頻(pín)教程,如:JAVA基礎、Spring boot、大數據等").toString();

    }


     /**

     * 處理(lǐ)取消關注事(shì)件(jiàn)

     * @param param

     * @return

     */

    private String unsubscribe(Map<String, String> param) {

        //處理(lǐ)業(yè)務

        return "success";

    }


    /**

     * 處理(lǐ)關鍵字搜索事(shì)件(jiàn)

     * 圖文(wén)消息個(gè)數;當用戶發送文(wén)本、圖片、語音(yīn)、視(shì)頻(pín)、圖文(wén)、地(dì)理(lǐ)位置這(zhè)六種消息時(shí),開(kāi)發者隻能回複1條圖文(wén)消息;其餘場(chǎng)景最多可回複8條圖文(wén)消息

     * @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");

        //單位為(wèi)秒,不是毫秒

        Long createTime = new Date().getTime() / 1000;

        StringBuffer text = new StringBuffer();

        List<Course> courseList = courseFeignClient.findByKeyword(content);

        if(CollectionUtils.isEmpty(courseList)) {

            text = this.text(param, "請重新輸入關鍵字,沒有(yǒu)匹配到相(xiàng)關視(shì)頻(pín)課程");

        } else {

            //一次隻能返回一個(gè)

            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();

    }


    /**

     * 回複文(wén)本

     * @param param

     * @param content

     * @return

     */

    private StringBuffer text(Map<String, String> param, String content) {

        String fromusername = param.get("FromUserName");

        String tousername = param.get("ToUserName");

        //單位為(wèi)秒,不是毫秒

        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;

    }

}


② 模闆消息

接口文(wén)檔

模闆消息僅用于公衆号向用戶發送重要的(de)服務通(tōng)知,隻能用于符合其要求的(de)服務場(chǎng)景中,如信用卡刷卡通(tōng)知,商品購買成功通(tōng)知等。


本項目中需要的(de)模闆消息為(wèi)訂單支付成功通(tōng)知,可以在模闆消息接口部分進行設置,在頁面中會顯示用于接口調用的(de)模闆ID以及模闆內(nèi)容。

示例模闆下(xià)載


模闆消息接口封裝


MessageController


添加方法


@GetMapping("/pushPayMessage")

public Result pushPayMessage() throws WxErrorException {

    messageService.pushPayMessage(1L);

    return Result.ok();

}


MessageService


void pushPayMessage(Long orderId);

1

首先需要獲取openid值、模闆id值。


openid值


模闆id值


MessageServiceImpl類


//訂單成功

    @Override

    public void pushPayMessage(long id) {

        //微(wēi)信openid

        String openid = "o5lra......Sig0E1zqc8sQU";

        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()

                .toUser(openid)//要推送的(de)用戶openid

                .templateId("cGMWhnB....muHlmFRWuffVo0TnQmZFg0FZ6A")//模闆id

                .url("前端內(nèi)網穿透網址/#/pay/"+id)//點擊模闆消息要訪問(wèn)的(de)網址

                .build();

        //3,如果是正式版發送消息,,這(zhè)裏需要配置你(nǐ)的(de)信息

        templateMessage.addData(new WxMpTemplateData("first", "親愛的(de)用戶:您有(yǒu)一筆(bǐ)訂單支付成功。", "#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", "感謝(xiè)你(nǐ)購買課程,如有(yǒu)疑問(wèn),随時(shí)咨詢!", "#272727"));

        try {

            String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);

        } catch (WxErrorException e) {

            e.printStackTrace();

        }

    }


以上(shàng)隻是一個(gè)測試,具體(tǐ)返回模闆信息會在支付功能中完成實現(xiàn)。


(4)公衆号授權

如果用戶在微(wēi)信客戶端中訪問(wèn)第三方網頁,公衆号可以通(tōng)過微(wēi)信網頁授權機(jī)制,來(lái)獲取用戶基本信息,進而實現(xiàn)業(yè)務邏輯。因此,在微(wēi)信公衆号開(kāi)發中需要實現(xiàn)授權功能。


在微(wēi)信公衆号請求用戶網頁授權之前,開(kāi)發者需要先到公衆平台官網中的(de)“設置與開(kāi)發 - 接口權限 - 網頁服務 - 網頁帳号 - 網頁授權獲取用戶基本信息”的(de)配置選項中,修改授權回調域名。請注意,這(zhè)裏填寫的(de)是域名(是一個(gè)字符串),而不是URL,因此請勿加 http:// 等協議(yì)頭。對(duì)應後端內(nèi)網穿透域名。




網頁授權流程分為(wèi)四步:


1. 引導用戶進入授權頁面同意授權,獲取code


該頁面地(dì)址:

scope為(wèi)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

如果用戶同意授權,頁面将跳(tiào)轉至 redirect_uri/?code=CODE&state=STATE。

2. 通(tōng)過 code 換取網頁授權access_token


獲取 code 後,請求以下(xià)鏈接獲取access_token:

https://api.weixin.qq.com/sns/oauth2/access_token?

appid=APPID&

secret=SECRET&

code=CODE&

grant_type=authorization_code

3. 如果需要,開(kāi)發者可以刷新網頁授權access_token,避免過期


4. 通(tōng)過網頁授權access_token和(hé) openid 獲取用戶基本信息(支持 UnionID 機(jī)制)


接口文(wén)檔




授權登錄接口實現(xiàn)


操作(zuò)模塊:service-user


① 引入微(wēi)信工(gōng)具包


<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>


② 添加配置


#公衆号id和(hé)秘鑰

# 矽谷課堂微(wēi)信公衆平台appId

wechat.mpAppId: wx09f201e9.....

## 矽谷課堂微(wēi)信公衆平台api秘鑰

wechat.mpAppSecret: 6c999765....1850d28055e8b6e2eda

# 授權回調獲取用戶信息接口地(dì)址

wechat.userInfoUrl: http://.......unnel.com/api/user/wechat/userInfo


③ 添加工(gōng)具類


@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;

    }

}


@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;

    }

}


④ 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("【微(wēi)信網頁授權】openId={}"+openId);


        WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);

        System.out.println("【微(wēi)信網頁授權】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);

        }

        //生(shēng)成token

        String token = JwtHelper.createToken(userInfo.getId(), userInfo.getNickName());

        if(returnUrl.indexOf("?") == -1) {

            return "redirect:" + returnUrl + "?token=" + token;

        } else {

            return "redirect:" + returnUrl + "&token=" + token;

        }

    }

}


⑤ 編寫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;

    }

}


JWT工(gōng)具

JWT(Json Web Token)是為(wèi)了在網絡應用環境間(jiān)傳遞聲明(míng)而執行的(de)一種基于JSON的(de)開(kāi)放(fàng)标準。


JWT的(de)聲明(míng)一般被用來(lái)在身(shēn)份提供者和(hé)服務提供者間(jiān)傳遞被認證的(de)用戶身(shēn)份信息,以便于從(cóng)資源服務器(qì)獲取資源。比如用在用戶登錄上(shàng)。


JWT最重要的(de)作(zuò)用就是對(duì) token信息的(de)防僞作(zuò)用。


JWT的(de)原理(lǐ)


一個(gè)JWT由三個(gè)部分組成:公共部分、私有(yǒu)部分、簽名部分。最後由這(zhè)三者組合進行base64編碼得到JWT。



(1)公共部分


主要是該JWT的(de)相(xiàng)關配置參數,比如簽名的(de)加密算(suàn)法、格式類型、過期時(shí)間(jiān)等等。


(2)私有(yǒu)部分


用戶自(zì)定義的(de)內(nèi)容,根據實際需要真正要封裝的(de)信息。


userInfo{用戶的(de)Id,用戶的(de)昵稱nickName}


(3)簽名部分


SaltiP: 當前服務器(qì)的(de)Ip地(dì)址!{linux 中配置代理(lǐ)服務器(qì)的(de)ip}


主要用戶對(duì)JWT生(shēng)成字符串的(de)時(shí)候,進行加密{鹽值}


base64編碼,并不是加密,隻是把明(míng)文(wén)信息變成了不可見的(de)字符串。但(dàn)是其實隻要用一些工(gōng)具就可以把base64編碼解成明(míng)文(wén),所以不要在JWT中放(fàng)入涉及私密的(de)信息。


整合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>


(2)添加JWT工(gōng)具類JwtHelper


//生(shēng)成token

public class JwtHelper {

    //token字符串有(yǒu)效時(shí)間(jiān)

    private static long tokenExpiration = 24*60*60*1000;

    //加密編碼秘鑰

    private static String tokenSignKey = "123456";


    //根據userid  和(hé)  username 生(shēng)成token字符串

    public static String createToken(Long userId, String userName) {

        String token = Jwts.builder()

                //設置token分類

                .setSubject("GGKT-USER")

                //token字符串有(yǒu)效時(shí)長(cháng)

                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))

                //私有(yǒu)部分(用戶信息)

                .claim("userId", userId)

                .claim("userName", userName)

                //根據秘鑰使用加密編碼方式進行加密,對(duì)字符串壓縮

                .signWith(SignatureAlgorithm.HS512, tokenSignKey)

                .compressWith(CompressionCodecs.GZIP)

                .compact();

        return token;

    }


    //從(cóng)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();

    }


    //從(cóng)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));

    }

}


(5) 視(shì)頻(pín)點播

點播功能需求


(1)點擊課程中的(de)分類,根據分類查詢課程列表

(2)點擊 去(qù)看(kàn)看(kàn),進入課程詳情頁面



由上(shàng)圖可知,一方面,通(tōng)過課程一級名稱(如後端開(kāi)發)需要返回對(duì)應的(de)子(zǐ)課程列表;

另一方面,針對(duì)某一課程頁面,需要顯示講師(shī)信息級課程詳細信息,課程大綱等內(nèi)容,同時(shí),可以點擊觀看(kàn)按鈕,進行視(shì)頻(pín)播放(fàng)。


1.編寫課程列表和(hé)詳情接口

實現(xiàn)列表及課程詳情展示部分。


(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) {

        //封裝條件(jiàn)

        CourseQueryVo courseQueryVo = new CourseQueryVo();

        courseQueryVo.setSubjectParentId(subjectParentId);

        //創建page對(duì)象

        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);

    }

}


(2)編寫CourseService


//課程列表

Map<String,Object> findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo);


//根據id查詢課程

Map<String, Object> getInfoById(Long courseId);


(3)編寫CourseServiceImpl


//課程列表

@Override

public Map<String,Object> findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo) {

    //獲取條件(jiàn)值

    String title = courseQueryVo.getTitle();//名稱

    Long subjectId = courseQueryVo.getSubjectId();//二級分類

    Long subjectParentId = courseQueryVo.getSubjectParentId();//一級分類

    Long teacherId = courseQueryVo.getTeacherId();//講師(shī)

    //封裝條件(jiàn)

    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;

}


//獲取講師(shī)和(hé)分類名稱

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;

}


(4)編寫CourseMapper


public interface CourseMapper extends BaseMapper<Course> {


    CoursePublishVo selectCoursePublishVoById(Long id);


    CourseVo selectCourseVoById(Long id);

}


(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>


2、點播視(shì)頻(pín)播放(fàng)

獲取視(shì)頻(pín)播放(fàng)參數


(1)創建VodApiController


@Api(tags = "騰訊視(shì)頻(pín)點播")

@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 = "視(shì)頻(pín)id", required = true)

            @PathVariable Long videoId) {

        return  Result.ok(vodService.getPlayAuth(courseId, videoId));

    }

}


(3)application.properties添加


tencent.video.appid=1312624373

1

(3)VodService創建方法


//獲取視(shì)頻(pín)播放(fàng)憑證

Map<String,Object> getPlayAuth(Long courseId, Long videoId);

1

2

(4)VodServiceImpl實現(xiàn)方法


@Value("${tencent.video.appid}")

private String appId;


//點播視(shì)頻(pín)播放(fàng)接口

@Override

public Map<String, Object> getPlayAuth(Long courseId, Long videoId) {

    //根據小(xiǎo)節id獲取小(xiǎo)節對(duì)象,獲取騰訊雲視(shì)頻(pín)id

    Video video = videoService.getById(videoId);

    if(video == null) {

        throw new GgktException(20001,"小(xiǎo)節信息不存在");

    }


    Map<String, Object> map = new HashMap<>();

    map.put("videoSourceId",video.getVideoSourceId());

    map.put("appId",appId);

    return map;

}



(6) 支付

① 訂單生(shēng)成

(1)用戶點擊菜單中“課程”的(de)二級菜單“後端開(kāi)發”

(2)點擊去(qù)看(kàn)看(kàn)查看(kàn)課程基本信息

(3)點擊立即購買,生(shēng)成課程訂單


創建service_order模塊


生(shēng)成收費(fèi)課程訂單接口對(duì)實現(xiàn)應包括以下(xià)幾步:


1.獲取當前微(wēi)信用戶Id,課程id

2.根據用戶id獲取用戶信息------service_user

2.根據課程id獲取課程信息------service_vod

3.獲取優惠券信息-----service_activity

4.添加訂單信息到訂單列表------service_order


接口實現(xiàn)

① 編寫創建訂單接口


(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);

    }

}


(2)編寫Service


OrderInfoService


//生(shēng)成點播課程訂單

Long submitOrder(OrderFormVo orderFormVo);

1

2

創建獲取課程信息接口


操作(zuò)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);

}


(2)service_course_client定義方法


@ApiOperation("根據ID查詢課程")

@GetMapping("/api/vod/course/inner/getById/{courseId}")

Course getById(@PathVariable Long courseId);


③ 創建獲取優惠券接口


操作(zuò)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;

}

}


(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);

    }


(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);


}


④ 獲取當前用戶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>


(2)複制工(gōng)具類到common下(xià)的(de)service_utils模塊


對(duì)于前端頁面,在授權成功後返回token值,包含id和(hé)NickName,利用http請求攔截器(qì)獲取localStorage裏面的(de)token值,并存到LocalStorage中,在每次發送ajax請求時(shí)獲取token值,放(fàng)到請求頭裏進行傳遞。

在接口中,設置好(hǎo)哪些路(lù)徑需要token,在方法中從(cóng)請求頭獲取token字符串,就可以得到用戶的(de)id。


⑤ 生(shēng)成訂單Service


(1)service_order引入依賴


(2)OrderInfoServiceImpl


@Autowired

private CourseFeignClient courseFeignClient;


@Autowired

private UserInfoFeignClient userInfoFeignClient;


@Autowired

private CouponInfoFeignClient couponInfoFeignClient;


//生(shēng)成點播課程訂單

@Override

public Long submitOrder(OrderFormVo orderFormVo) {

    Long userId = AuthContextHolder.getUserId();

    Long courseId = orderFormVo.getCourseId();

    Long couponId = orderFormVo.getCouponId();

    //查詢當前用戶是否已有(yǒu)當前課程的(de)訂單

    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());

    }


    //優惠券金(jīn)額

    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();

}


② 支付功能

由于測試号功能的(de)限制,此部分隻能進行測試,不能實際實現(xiàn)支付。

接口文(wén)檔


(1)綁定域名


先登錄微(wēi)信公衆平台進入“設置與開(kāi)發”,“公衆号設置”的(de)“功能設置”裏填寫“JS接口安全域名”。


說(shuō)明(míng):因為(wèi)測試号不支持支付功能,需要使用正式号才能進行測試。


(2)商戶平台配置支付目錄


(3)微(wēi)信支付接口開(kāi)發


① 創建WXPayController


@Api(tags = "微(wēi)信支付接口")

@RestController

@RequestMapping("/api/order/wxPay")

public class WXPayController {


    @Autowired

    private WXPayService wxPayService;


    @ApiOperation(value = "下(xià)單 小(xiǎo)程序支付")

    @GetMapping("/createJsapi/{orderNo}")

    public Result createJsapi(

            @ApiParam(name = "orderNo", value = "訂單No", required = true)

            @PathVariable("orderNo") String orderNo) {

        return Result.ok(wxPayService.createJsapi(orderNo));

    }

}


② 創建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>


④ 創建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來(lái)根據URL訪問(wèn)第三方接口并且傳遞參數

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、返回第三方的(de)數據

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<>();

}

}

}



訂單完成支付後的(de)效果:



(7)直播

直播第三方工(gōng)具:歡拓雲直播


首先完成賬号的(de)注冊和(hé)基本配置,在直播管理(lǐ)中可以創建直播,同時(shí)主播端下(xià)載“雲直播客戶端”,“頻(pín)道(dào)id與密碼”為(wèi)直播客戶端的(de)登錄賬号。



接口文(wén)檔地(dì)址

我們可以通(tōng)過SDK完成對(duì)直播接口的(de)功能實現(xiàn)與配置。

SDK下(xià)載地(dì)址


1.網頁端直播功能

① 模塊搭建

(1)創建service_live模塊

(2)添加依賴


添加直播SDK需要的(de)依賴


<!-- 直播  -->

<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>


(3)集成代碼


将SDK文(wén)件(jiàn)複制到service_live模塊下(xià)。



(4)更改配置


更改MTCloud類配置


說(shuō)明(míng):


1、更改openID與openToken


2、該類官方已經做了接口集成,我們可以直接使用。


public class MTCloud {


    /**

     * 合作(zuò)方ID: 合作(zuò)方在歡拓平台的(de)唯一ID

     */

    public String openID = "37013";


    /**

     * 合作(zuò)方秘鑰: 合作(zuò)方ID對(duì)應的(de)參數加密秘鑰

     */

    public String openToken = "5cfa64c1be5f479aea8296bb4e2c37d3";

    

    ...

}


(5)在配置文(wén)件(jiàn)application.properties中指明(míng)openId與openToken信息


mtcloud.openId=43873

mtcloud.openToken=1f3681df876eb31474be8c479b9f1ffe

1

2

② 功能實現(xiàn)



由上(shàng)圖可知,在網頁端,直播頁面管理(lǐ)需要實現(xiàn)的(de)功能:


1.獲取分頁列表(index)


其中包括:封面,直播名稱,直播時(shí)間(jiān),直播老(lǎo)師(shī),頭銜,創建時(shí)間(jiān)。

需在service_vod模塊創建接口獲取講師(shī)信息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: 生(shēng)活直播。默認 1,說(shuō)明(míng):根據平台開(kāi)通(tōng)的(de)直播類型填寫

    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;

}


需要實現(xiàn)的(de)功能是在網頁端添加直播的(de)同時(shí),在歡拓雲中也添加相(xiàng)應的(de)直播。





3.修改

4.删除

5.查看(kàn)賬号配置信息


2.公衆号直播對(duì)接

① 用戶觀看(kàn)端集成


接口文(wén)檔


(1) 獲取用戶access_token


用戶要觀看(kàn)直播,必須獲取對(duì)應的(de)用戶access_token,通(tōng)過access_token 獲取觀看(kàn)的(de)直播課程;


接口參數:直播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);

    }


}


b. LiveCourseService添加方法


JSONObject getPlayAuth(Long id, Long userId);

1

c. LiveCourseServiceImpl實現(xiàn)方法


@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,"獲取失敗");

    }

}


(2)下(xià)載前端SDK


下(xià)載地(dì)址:https://open.talk-fun.com/docs/js/download.html


(3)與前端項目結合


http://localhost:8080/live.html為(wèi)直播觀看(kàn)訪問(wèn)方式


(8)分享

參考文(wén)檔


① 綁定域名


先登錄微(wēi)信公衆平台進入“設置與開(kāi)發”,“公衆号設置”的(de)“功能設置”裏填寫“JS接口安全域名”。


說(shuō)明(míng):本地(dì)測試設置內(nèi)網穿透地(dì)址。



② 前端:引入JS文(wén)件(jiàn),引入前端項目/publicindex.html文(wén)件(jiàn),封裝分享js


④ 服務器(qì)端接口


新增ShareController類


說(shuō)明(míng):微(wēi)信分享要對(duì)當前url加密處理(lǐ),由于我們的(de)url路(lù)由都(dōu)是帶“#”符号,服務器(qì)端接收不到,因此通(tōng)過“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);

    }


}


手機(jī)掃碼查看(kàn)當前文(wén)章(zhāng):

微(wēi)信公衆号開(kāi)發

如本網轉載稿涉及版權等問(wèn)題,請作(zuò)者見稿後在兩周內(nèi)速來(lái)電(diàn)與我們聯系, 詳見版權聲明(míng)

  上(shàng)一篇:微(wēi)信公衆号及小(xiǎo)程序開(kāi)發入門(二)

 下(xià)一篇:【Android -- 開(kāi)源庫】騰訊 TBS 浏覽器(qì) SDK 接入