微(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):