微信公众号
1、注册公众号
微信公众平台:https://mp.weixin.qq.com/
硅谷课堂要求基于H5,具有微信支付等高级功能的,因此需要注册服务号,订阅号不具备支付功能。
注册步骤参考官方注册文档:https://kf.qq.com/faq/120911VrYVrA151013MfYvYV.html,
注册过程仅做了解,有公司运营负责申请与认证。
2、公众号功能介绍
我们在微信公众平台扫码登录后可以发现管理页面左侧菜单栏有丰富的功能:
大概可以分为这几大模块:
首页、内容与互动、数据、广告与服务、设置与开发、新功能
作为开发人员,首先应该关注的是设置与开发模块;而作为产品运营人员与数据分析人员,关注的是内容与互动、数据及广告与服务模块。
首先我们不妨各个功能模块都点击看一看,大概了解下我们能做些什么。可以确认的是,这个微信公众平台当然不只是给开发人员使用的,它提供了很多非技术人员可在UI界面上交互操作的功能模块。
如配置消息回复、自定义菜单、发布文章等:
这个时候我们可能会想:这些功能好像非技术人员都能随意操作,那么还需要我们技术人员去开发吗?
答案是: 如果只是日常简单的推送文章,就像我们关注的大多数公众号一样,那确实不需要技术人员去开发;但是,如果你想将你们的网站嵌入进去公众号菜单里(这里指的是把前端项目的首页链接配置在自定义菜单),并且实现微信端的独立登录认证、获取微信用户信息、微信支付等高级功能,或者觉得UI交互的配置方式无法满足你的需求,你需要更加自由、随心所欲的操作,那么我们就必须启用开发者模式了,通过技术人员的手段去灵活控制公众号。
这里有一点需要注意,如果我们决定技术人员开发公众号,必须启用服务器配置,而这将导致UI界面设置的自动回复和自定义菜单失效!
我们在 设置与开发 - 基本配置 - 服务器配置 中点击启用:
至于服务器配置中的选项代表什么意思、如何填写,我们下面再讲。
3、微信公众平台测试帐号
3.1、申请测试帐号
微信公众平台接口测试帐号:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login&token=399029368&lang=zh_CN
3.2、查看测试号管理
(1)其中appID和appsecret用于后面菜单开发使用
(2)其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。本地测试,url改为内网穿透地址。
3.3、关注公众号
4、开发业务介绍
硅谷课堂涉及的微信公众号功能模块:自定义菜单、消息、微信支付、授权登录等
后台管理系统–公众号菜单管理
1、需求分析
1.1、微信自定义菜单说明
微信自定义菜单文档地址:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
微信自定义菜单注意事项:
- 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
- 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
- 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
1.2、硅谷课堂自定义菜单
一级菜单:直播、课程、我的
二级菜单:根据一级菜单动态设置二级菜单,直播(近期直播课程),课程(课程分类),我的(我的订单、我的课程、我的优惠券及关于我们)
说明:
1、二级菜单可以是网页类型,点击跳转H5页面
2、二级菜单可以是消息类型,点击返回消息
1.3、数据格式
自定义菜单通过后台管理设置到数据库表,数据配置好后,通过微信接口推送菜单数据到微信平台。
表结构(menu):
表示例数据:
1.4、管理页面
(1)页面功能“列表、添加、修改与删除”是对menu表的操作
(2)页面功能“同步菜单与删除菜单”是对微信平台接口操作
2、搭建菜单管理后端环境
2.1、创建模块service_wechat
(1)在service下创建子模块service_wechat
(2)引入依赖
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
2.2、生成菜单相关代码
2.3、创建启动类和配置文件
(1)启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.atguigu")
@MapperScan("com.atguigu.ggkt.wechat.mapper")
@ComponentScan(basePackages = "com.atguigu")
public class ServiceWechatApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceWechatApplication.class, args);
}
}
(2)配置文件
# 服务端口
server.port=8305
# 服务名
spring.application.name=service-wechat
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/glkt_wechat?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:com/atguigu/ggkt/wechat/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#公众号id和秘钥
# 硅谷课堂微信公众平台appId
wechat.mpAppId: wx09f201e9013e81d8
# 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 6c999765c12c51850d28055e8b6e2eda
2.4、配置网关
#service-wechat模块配置
#设置路由id
spring.cloud.gateway.routes[4].id=service-wechat
#设置路由的uri
spring.cloud.gateway.routes[4].uri=lb://service-wechat
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[4].predicates= Path=/*/wechat/**
3、开发菜单管理接口
3.1、编写MenuController
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController {
@Autowired
private MenuService menuService;
//获取所有菜单,按照一级和二级菜单封装
@GetMapping("findMenuInfo")
public Result findMenuInfo() {
List<MenuVo> list = menuService.findMenuInfo();
return Result.ok(list);
}
//获取所有一级菜单
@GetMapping("findOneMenuInfo")
public Result findOneMenuInfo() {
List<Menu> list = menuService.findMenuOneInfo();
return Result.ok(list);
}
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
Menu menu = menuService.getById(id);
return Result.ok(menu);
}
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody Menu menu) {
menuService.save(menu);
return Result.ok(null);
}
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody Menu menu) {
menuService.updateById(menu);
return Result.ok(null);
}
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
menuService.removeById(id);
return Result.ok(null);
}
@ApiOperation(value = "根据id列表删除")
@DeleteMapping("batchRemove")
public Result batchRemove(@RequestBody List<Long> idList) {
menuService.removeByIds(idList);
return Result.ok(null);
}
}
3.2、编写Service
(1)MenuService定义方法
public interface MenuService extends IService<Menu> {
//获取全部菜单
List<MenuVo> findMenuInfo();
//获取一级菜单
List<Menu> findOneMenuInfo();
}
(2)MenuServiceImpl实现方法
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
//获取全部菜单
@Override
public List<MenuVo> findMenuInfo() {
List<MenuVo> list = new ArrayList<>();
List<Menu> menuList = baseMapper.selectList(null);
List<Menu> oneMenuList = menuList.stream().filter(menu -> menu.getParentId().longValue() == 0).collect(Collectors.toList());
for(Menu oneMenu : oneMenuList) {
MenuVo oneMenuVo = new MenuVo();
BeanUtils.copyProperties(oneMenu, oneMenuVo);
List<Menu> twoMenuList = menuList.stream()
.filter(menu -> menu.getParentId().longValue() == oneMenu.getId())
.sorted(Comparator.comparing(Menu::getSort))
.collect(Collectors.toList());
List<MenuVo> children = new ArrayList<>();
for(Menu twoMenu : twoMenuList) {
MenuVo twoMenuVo = new MenuVo();
BeanUtils.copyProperties(twoMenu, twoMenuVo);
children.add(twoMenuVo);
}
oneMenuVo.setChildren(children);
list.add(oneMenuVo);
}
return list;
}
//获取一级菜单
@Override
public List<Menu> findOneMenuInfo() {
QueryWrapper<Menu> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id",0);
List<Menu> list = baseMapper.selectList(wrapper);
return list;
}
}
4、同步菜单(获取access_token)
4.1、文档查看
(1)进行菜单同步时候,需要获取到公众号的access_token,通过access_token进行菜单同步
接口文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
(2)调用方式
4.2、service_wechat添加配置
# 硅谷课堂微信公众平台appId
wechat.mpAppId: wx09f201e9013e81d8
# 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 6c999765c12c51850d28055e8b6e2eda
4.3、添加工具类
@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;
}
}
4.4、复制HttpClient工具类
4.5、添加Menucontroller方法
//获取access_token
@GetMapping("getAccessToken")
public Result getAccessToken() {
try {
//拼接请求地址
StringBuffer buffer = new StringBuffer();
buffer.append("https://api.weixin.qq.com/cgi-bin/token");
buffer.append("?grant_type=client_credential");
buffer.append("&appid=%s");
buffer.append("&secret=%s");
//请求地址设置参数
String url = String.format(buffer.toString(),
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET);
//发送http请求
String tokenString = HttpClientUtils.get(url);
//获取access_token
JSONObject jsonObject = JSONObject.parseObject(tokenString);
String access_token = jsonObject.getString("access_token");
//返回
return Result.ok(access_token);
} catch (Exception e) {
e.printStackTrace();
return Result.fail(null);
}
}
4.6、测试
5、同步菜单(功能实现)
接口文档:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
weixin-java-mp是封装好了的微信接口客户端,使用起来很方便,后续我们就使用weixin-java-mp处理微信平台接口。
5.1、添加配置类
@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;
}
}
5.2、定义Service方法
MenuService
void syncMenu();
5.3、实现Service方法
MenuServiceImpl
@Autowired
private WxMpService wxMpService;
/**
* 说明:
* 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
* 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“...”代替。
* 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
*/
@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());
}
5.4、controller方法
@ApiOperation(value = "同步菜单")
@GetMapping("syncMenu")
public Result createMenu() throws WxErrorException {
menuService.syncMenu();
return Result.ok(null);
}
6、删除菜单
6.1、service接口
void removeMenu();
6.2、service接口实现
@SneakyThrows
@Override
public void removeMenu() {
wxMpService.getMenuService().menuDelete();
}
6.3、controller方法
@ApiOperation(value = "删除菜单")
@DeleteMapping("removeMenu")
public Result removeMenu() {
menuService.removeMenu();
return Result.ok(null);
}
7、开发菜单管理前端
7.1、添加路由
(1)src -> router -> index.js添加路由
{
path: '/wechat',
component: Layout,
redirect: '/wechat/menu/list',
name: 'Wechat',
meta: {
title: '菜单管理',
icon: 'el-icon-refrigerator'
},
alwaysShow: true,
children: [
{
path: 'menu/list',
name: 'Menu',
component: () => import('@/views/wechat/menu/list'),
meta: { title: '菜单列表' }
}
]
},
7.2、定义接口
(1)src -> api -> wechat -> menu.js定义接口
import request from '@/utils/request'
const api_name = '/admin/wechat/menu'
export default {
findMenuInfo() {
return request({
url: `${api_name}/findMenuInfo`,
method: `get`
})
},
findOneMenuInfo() {
return request({
url: `${api_name}/findOneMenuInfo`,
method: `get`
})
},
save(menu) {
return request({
url: `${api_name}/save`,
method: `post`,
data: menu
})
},
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: `get`
})
},
updateById(menu) {
return request({
url: `${api_name}/update`,
method: `put`,
data: menu
})
},
syncMenu() {
return request({
url: `${api_name}/syncMenu`,
method: `get`
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
},
removeMenu() {
return request({
url: `${api_name}/removeMenu`,
method: `delete`
})
}
}
7.3、编写页面
(1)创建views -> wechat -> menu -> list.vue
<template>
<div class="app-container">
<!-- 工具条 -->
<el-card class="operate-container" shadow="never">
<i class="el-icon-tickets" style="margin-top: 5px"></i>
<span style="margin-top: 5px">数据列表</span>
<el-button class="btn-add" size="mini" @click="remove" style="margin-left: 10px;">删除菜单</el-button>
<el-button class="btn-add" size="mini" @click="syncMenu">同步菜单</el-button>
<el-button class="btn-add" size="mini" @click="add">添 加</el-button>
</el-card>
<el-table
:data="list"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
default-expand-all
:tree-props="{children: 'children'}">
<el-table-column label="名称" prop="name" width="350"></el-table-column>
<el-table-column label="类型" width="100">
<template slot-scope="scope">
{{ scope.row.type == 'view' ? '链接' : scope.row.type == 'click' ? '事件' : '' }}
</template>
</el-table-column>
<el-table-column label="菜单URL" prop="url" ></el-table-column>
<el-table-column label="菜单KEY" prop="meunKey" width="130"></el-table-column>
<el-table-column label="排序号" prop="sort" width="70"></el-table-column>
<el-table-column label="操作" width="170" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.parentId > 0" type="text" size="mini" @click="edit(scope.row.id)">修改</el-button>
<el-button v-if="scope.row.parentId > 0" type="text" size="mini" @click="removeDataById(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="选择一级菜单">
<el-select
v-model="menu.parentId"
placeholder="请选择">
<el-option
v-for="item in list"
:key="item.id"
:label="item.name"
:value="item.id"/>
</el-select>
</el-form-item>
<el-form-item v-if="menu.parentId == 1" label="菜单名称">
<el-select
v-model="menu.name"
placeholder="请选择"
@change="liveCourseChanged">
<el-option
v-for="item in liveCourseList"
:key="item.id"
:label="item.courseName"
:value="item"/>
</el-select>
</el-form-item>
<el-form-item v-if="menu.parentId == 2" label="菜单名称">
<el-select
v-model="menu.name"
placeholder="请选择"
@change="subjectChanged">
<el-option
v-for="item in subjectList"
:key="item.id"
:label="item.title"
:value="item"/>
</el-select>
</el-form-item>
<el-form-item v-if="menu.parentId == 3" label="菜单名称">
<el-input v-model="menu.name"/>
</el-form-item>
<el-form-item label="菜单类型">
<el-radio-group v-model="menu.type">
<el-radio label="view">链接</el-radio>
<el-radio label="click">事件</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="menu.type == 'view'" label="链接">
<el-input v-model="menu.url"/>
</el-form-item>
<el-form-item v-if="menu.type == 'click'" label="菜单KEY">
<el-input v-model="menu.meunKey"/>
</el-form-item>
<el-form-item label="排序">
<el-input v-model="menu.sort"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate()" size="small">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import menuApi from '@/api/wechat/menu'
//import liveCourseApi from '@/api/live/liveCourse'
import subjectApi from '@/api/vod/subject'
const defaultForm = {
id: null,
parentId: 1,
name: '',
nameId: null,
sort: 1,
type: 'view',
meunKey: '',
url: ''
}
export default {
// 定义数据
data() {
return {
list: [],
liveCourseList: [],
subjectList: [],
dialogVisible: false,
menu: defaultForm,
saveBtnDisabled: false
}
},
// 当页面加载时获取数据
created() {
this.fetchData()
// this.fetchLiveCourse()
this.fetchSubject()
},
methods: {
// 调用api层获取数据库中的数据
fetchData() {
console.log('加载列表')
menuApi.findMenuInfo().then(response => {
this.list = response.data
console.log(this.list)
})
},
// fetchLiveCourse() {
// liveCourseApi.findLatelyList().then(response => {
// this.liveCourseList = response.data
// this.liveCourseList.push({'id': 0, 'courseName': '全部列表'})
// })
// },
fetchSubject() {
console.log('加载列表')
subjectApi.getChildList(0).then(response => {
this.subjectList = response.data
})
},
syncMenu() {
this.$confirm('你确定上传菜单吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return menuApi.syncMenu();
}).then((response) => {
this.fetchData()
this.$message.success(response.message)
}).catch(error => {
console.log('error', error)
// 当取消时会进入catch语句:error = 'cancel'
// 当后端服务抛出异常时:error = 'error'
if (error === 'cancel') {
this.$message.info('取消上传')
}
})
},
// 根据id删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return menuApi.removeById(id)
}).then((response) => {
this.fetchData(this.page)
if (response.code) {
this.$message({
type: 'success',
message: '删除成功!'
})
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
// -------------
add(){
this.dialogVisible = true
this.menu = Object.assign({}, defaultForm)
},
edit(id) {
this.dialogVisible = true
this.fetchDataById(id)
},
fetchDataById(id) {
menuApi.getById(id).then(response => {
this.menu = response.data
})
},
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.menu.id) {
this.saveData()
} else {
this.updateData()
}
},
// 新增
saveData() {
menuApi.save(this.menu).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false;
this.fetchData(this.page)
}
})
},
// 根据id更新记录
updateData() {
menuApi.updateById(this.menu).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false;
this.fetchData(this.page)
}
})
},
// 根据id查询记录
fetchDataById(id) {
menuApi.getById(id).then(response => {
this.menu = response.data
})
},
subjectChanged(item) {
console.info(item)
this.menu.name = item.title
this.menu.url = '/course/' + item.id
},
liveCourseChanged(item) {
console.info(item)
this.menu.name = item.courseName
if(item.id == 0) {
this.menu.url = '/live'
} else {
this.menu.url = '/liveInfo/' + item.id
}
}
}
}
</script>
8、公众号菜单功能测试
(1)在手机公众号可以看到同步之后的菜单
公众号普通消息
1、实现目标
1、“硅谷课堂”公众号实现根据关键字搜索相关课程,如:输入“java”,可返回java相关的一个课程;
2、“硅谷课堂”公众号点击菜单“关于我们”,返回关于我们的介绍
3、关注或取消关注等
2、消息接入
参考文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
接入微信公众平台开发,开发者需要按照如下步骤完成:
1、填写服务器配置
2、验证服务器地址的有效性
3、依据接口文档实现业务逻辑
2.1、公众号服务器配置
在测试管理 -> 接口配置信息,点击“修改”按钮,填写服务器地址(URL)和Token,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)
说明:本地测试,url改为内网穿透地址
2.2、验证来自微信服务器消息
(1)概述
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
参数 | 描述 |
---|---|
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1、将token、timestamp、nonce三个参数进行字典序排序
2、将三个参数字符串拼接成一个字符串进行sha1加密
3、开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
(2)代码实现
创建MessageController
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController {
private static final String token = "ggkt";
/**
* 服务器有效性验证
* @param request
* @return
*/
@GetMapping
public String verifyToken(HttpServletRequest request) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
log.info("signature: {} nonce: {} echostr: {} timestamp: {}", signature, nonce, echostr, timestamp);
if (this.checkSignature(signature, timestamp, nonce)) {
log.info("token ok");
return echostr;
}
return echostr;
}
private boolean checkSignature(String signature, String timestamp, String nonce) {
String[] str = new String[]{token, timestamp, nonce};
//排序
Arrays.sort(str);
//拼接字符串
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < str.length; i++) {
buffer.append(str[i]);
}
//进行sha1加密
String temp = SHA1.encode(buffer.toString());
//与微信提供的signature进行匹对
return signature.equals(temp);
}
}
OK,完成之后,我们的校验接口就算是开发完成了。接下来就可以开发消息接收接口了。
2.3、消息接收
接下来我们来开发消息接收接口,消息接收接口和上面的服务器校验接口地址是一样的,都是我们一开始在公众号后台配置的地址。只不过消息接收接口是一个 POST 请求。
在公众号后台配置的时候,消息加解密方式选择了明文模式,这样在后台收到的消息直接就可以处理了。微信服务器给我发来的普通文本消息格式如下:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方帐号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,文本为text |
Content | 文本消息内容 |
MsgId | 消息id,64位整型 |
看到这里,大家心里大概就有数了,当我们收到微信服务器发来的消息之后,我们就进行 XML 解析,提取出来我们需要的信息,去做相关的查询操作,再将查到的结果返回给微信服务器。
这里我们先来个简单的,我们将收到的消息解析并打印出来:
/**
* 接收微信服务器发送来的消息
* @param request
* @return
* @throws Exception
*/
@PostMapping
public String receiveMessage(HttpServletRequest request) throws Exception {
WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
System.out.println(JSONObject.toJSONString(wxMpXmlMessage));
return "success";
}
private Map<String, String> parseXml(HttpServletRequest request) throws Exception {
Map<String, String> map = new HashMap<String, String>();
InputStream inputStream = request.getInputStream();
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
List<Element> elementList = root.elements();
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
inputStream.close();
inputStream = null;
return map;
}
3、配置内网穿透(ngrok)
3.1、注册用户
网址:https://ngrok.cc/login/register
3.2、实名认证
(1)注册成功之后,登录系统,进行实名认证,认证费2元,认证通过后才能开通隧道
3.3、开通隧道
(1)选择隧道管理 -> 开通隧道
最后一个是免费服务器,建议选择付费服务器,10元/月,因为免费服务器使用人数很多,经常掉线
(2)点击立即购买 -> 输入相关信息
(3)开通成功后,查看开通的隧道
这里开通了两个隧道,一个用于后端接口调用,一个用于公众号前端调用
3.4、启动隧道
(1)下载客户端工具
(2)选择windows版本
(3)解压,找到bat文件,双击启动
(4)输入隧道id,多个使用逗号隔开,最后回车就可以启动
3.5、测试
启动服务,在硅谷课堂公众号发送文本消息测试效果。
4、消息业务实现
4.1、service_vod模块创建接口
(1)创建CourseApiController方法,根据课程关键字查询课程信息
@ApiOperation("根据关键字查询课程")
@GetMapping("inner/findByKeyword/{keyword}")
public List<Course> findByKeyword(
@ApiParam(value = "关键字", required = true)
@PathVariable String keyword){
QueryWrapper<Course> queryWrapper = new QueryWrapper();
queryWrapper.like("title", keyword);
List<Course> list = courseService.list(queryWrapper);
return list;
}
4.2、创建模块定义接口
(1)service_client下创建子模块service_course_client
(2)定义根据关键字查询课程接口
@FeignClient(value = "service-vod")
public interface CourseFeignClient {
@ApiOperation("根据关键字查询课程")
@GetMapping("/api/vod/course/inner/findByKeyword/{keyword}")
List<Course> findByKeyword(@PathVariable String keyword);
}
4.3、service_wechat引入依赖
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>service_course_client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
4.4、service_wechat模块实现方法
(1)MessageService
public interface MessageService {
//接收消息
String receiveMessage(Map<String, String> param);
}
(2)MessageServiceImpl
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
private CourseFeignClient courseFeignClient;
@Autowired
private WxMpService wxMpService;
//接收消息
@Override
public String receiveMessage(Map<String, String> param) {
String content = "";
try {
String msgType = param.get("MsgType");
switch(msgType){
case "text" :
content = this.search(param);
break;
case "event" :
String event = param.get("Event");
String eventKey = param.get("EventKey");
if("subscribe".equals(event)) {//关注公众号
content = this.subscribe(param);
} else if("unsubscribe".equals(event)) {//取消关注公众号
content = this.unsubscribe(param);
} else if("CLICK".equals(event) && "aboutUs".equals(eventKey)){
content = this.aboutUs(param);
} else {
content = "success";
}
break;
default:
content = "success";
}
} catch (Exception e) {
e.printStackTrace();
content = this.text(param, "请重新输入关键字,没有匹配到相关视频课程").toString();
}
return content;
}
/**
* 关于我们
* @param param
* @return
*/
private String aboutUs(Map<String, String> param) {
return this.text(param, "硅谷课堂现开设Java、HTML5前端+全栈、大数据、全链路UI/UE设计、人工智能、大数据运维+Python自动化、Android+HTML5混合开发等多门课程;同时,通过视频分享、谷粒学苑在线课堂、大厂学苑直播课堂等多种方式,满足了全国编程爱好者对多样化学习场景的需求,已经为行业输送了大量IT技术人才。").toString();
}
/**
* 处理关注事件
* @param param
* @return
*/
private String subscribe(Map<String, String> param) {
//处理业务
return this.text(param, "感谢你关注“硅谷课堂”,可以根据关键字搜索您想看的视频教程,如:JAVA基础、Spring boot、大数据等").toString();
}
/**
* 处理取消关注事件
* @param param
* @return
*/
private String unsubscribe(Map<String, String> param) {
//处理业务
return "success";
}
/**
* 处理关键字搜索事件
* 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
* @param param
* @return
*/
private String search(Map<String, String> param) {
String fromusername = param.get("FromUserName");
String tousername = param.get("ToUserName");
String content = param.get("Content");
//单位为秒,不是毫秒
Long createTime = new Date().getTime() / 1000;
StringBuffer text = new StringBuffer();
List<Course> courseList = courseFeignClient.findByKeyword(content);
if(CollectionUtils.isEmpty(courseList)) {
text = this.text(param, "请重新输入关键字,没有匹配到相关视频课程");
} else {
//一次只能返回一个
Random random = new Random();
int num = random.nextInt(courseList.size());
Course course = courseList.get(num);
StringBuffer articles = new StringBuffer();
articles.append("<item>");
articles.append("<Title><![CDATA["+course.getTitle()+"]]></Title>");
articles.append("<Description><![CDATA["+course.getTitle()+"]]></Description>");
articles.append("<PicUrl><![CDATA["+course.getCover()+"]]></PicUrl>");
articles.append("<Url><![CDATA[http://glkt.atguigu.cn/#/liveInfo/"+course.getId()+"]]></Url>");
articles.append("</item>");
text.append("<xml>");
text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
text.append("<MsgType><![CDATA[news]]></MsgType>");
text.append("<ArticleCount><![CDATA[1]]></ArticleCount>");
text.append("<Articles>");
text.append(articles);
text.append("</Articles>");
text.append("</xml>");
}
return text.toString();
}
/**
* 回复文本
* @param param
* @param content
* @return
*/
private StringBuffer text(Map<String, String> param, String content) {
String fromusername = param.get("FromUserName");
String tousername = param.get("ToUserName");
//单位为秒,不是毫秒
Long createTime = new Date().getTime() / 1000;
StringBuffer text = new StringBuffer();
text.append("<xml>");
text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
text.append("<MsgType><![CDATA[text]]></MsgType>");
text.append("<Content><![CDATA["+content+"]]></Content>");
text.append("</xml>");
return text;
}
}
4.5、更改MessageController方法
/**
* 接收微信服务器发送来的消息
* @param request
* @return
* @throws Exception
*/
@PostMapping
public String receiveMessage(HttpServletRequest request) throws Exception {
Map<String, String> param = this.parseXml(request);
return messageService.receiveMessage(param);
}
5、测试公众号消息
(1)点击个人 -> 关于我们,返回关于我们的介绍
(2)在公众号输入关键字,返回搜索的课程信息
公众号模板消息
1、实现目标
购买课程支付成功微信推送消息
2、模板消息实现
接口文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
3、申请模板消息
首先我们需要知道,模板消息是需要申请的。
但是我们在申请时还是有一些东西要注意,这个在官方的文档有非常详细的说明。
这个大家好好看看。选择行业的时候可要谨慎些,因为这个一个月只可以修改一次。
下面看看在哪里申请,硅谷课堂已经申请过,忽略
申请之后就耐心等待,审核通过之后就会出现“广告与服务”模板消息的菜单。
4、添加模板消息
审核通过之后,我们就可以添加模板消息,进行开发了。
我们点击模板消息进入后,直接在模板库中选择你需要的消息模板添加就可以了,添加之后就会在我的模板中。会有一个模板id,这个模板id在我们发送消息的时候会用到。
模板消息如下:
我们需要模板消息:
1、订单支付成功通知;
模板库中没有的模板,可以自定义模板,审核通过后可以使用。
5、公众号测试号申请模板消息
5.1、新增测试模板
5.2、填写信息
(1)下载示例参考
(2)填写模板标题和模板内容
6、模板消息接口封装
6.1、MessageController
添加方法
@GetMapping("/pushPayMessage")
public Result pushPayMessage() throws WxErrorException {
messageService.pushPayMessage(1L);
return Result.ok();
}
6.2、service接口
MessageService
void pushPayMessage(Long orderId);
6.3、service接口实现
(1)MessageServiceImpl类
(2)openid值
(3)模板id值
@Autowired
private WxMpService wxMpService;
//TODO 暂时写成固定值测试,后续完善
@SneakyThrows
@Override
public void pushPayMessage(long orderId) {
String openid = "oepf36SawvvS8Rdqva-Cy4flFFtg";
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openid)//要推送的用户openid
.templateId("V-x2o4oTIW4rXwGzyM-YprNBQV9XmKxwpk_rQpXeGCE")//模板id
.url("http://ggkt2.vipgz1.91tunnel.com/#/pay/"+orderId)//点击模板消息要访问的网址
.build();
//3,如果是正式版发送消息,,这里需要配置你的信息
templateMessage.addData(new WxMpTemplateData("first", "亲爱的用户:您有一笔订单支付成功。", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword1", "1314520", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword2", "java基础课程", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword3", "2022-01-11", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword4", "100", "#272727"));
templateMessage.addData(new WxMpTemplateData("remark", "感谢你购买课程,如有疑问,随时咨询!", "#272727"));
String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
System.out.println(msg);
}
6.4、通过swagger测试效果
(1)在公众号可以看到发送的模板消息
微信授权登录
1、需求描述
根据流程图通过菜单进入的页面都要授权登录
2、授权登录
接口文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
说明:
1、严格按照接口文档实现;
2、应用授权作用域scope:scope为snsapi_userinfo
2.1、配置授权回调域名
(1)在公众号正式号配置
在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“设置与开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
本地测试配置内网穿透地址
(2)在公众号测试号配置
2.2、部署公众号前端页面
(1)公众号前端页面已经开发完成,直接部署使用即可
(2)启动公众号页面项目
使用命令:npm run serve
2.3、前端处理
(1)全局处理授权登录,处理页面:/src/App.vue
说明1:访问页面时首先判断是否有token信息,如果没有跳转到授权登录接口
说明2:通过localStorage存储token信息
在HTML5中,加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间很小,只有几K),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同。它只能存储字符串格式的数据,所以最好在每次存储时把数据转换成json格式,取出的时候再转换回来。
(2)前端代码实现
wechatLogin() {
// 处理微信授权登录
let token = this.getQueryString('token') || '';
if(token != '') {
window.localStorage.setItem('token', token);
}
// 所有页面都必须登录,两次调整登录,这里与接口返回208状态
token = window.localStorage.getItem('token') || '';
if (token == '') {
let url = window.location.href.replace('#', 'guiguketan')
window.location = 'http://glkt.atguigu.cn/api/user/wechat/authorize?returnUrl=' + url
}
console.log('token2:'+window.localStorage.getItem('token'));
},
3、授权登录接口
操作模块:service-user
3.1、引入微信工具包
<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>
3.2、添加配置
#公众号id和秘钥
# 硅谷课堂微信公众平台appId
wechat.mpAppId: wx09f201e9013e81d8
## 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 6c999765c12c51850d28055e8b6e2eda
# 授权回调获取用户信息接口地址
wechat.userInfoUrl: http://ggkt.vipgz1.91tunnel.com/api/user/wechat/userInfo
3.3、添加工具类
@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;
}
}
3.4、controller类
@Controller
@RequestMapping("/api/user/wechat")
public class WechatController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private WxMpService wxMpService;
@Value("${wechat.userInfoUrl}")
private String userInfoUrl;
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
String redirectURL = wxMpService.oauth2buildAuthorizationUrl(userInfoUrl,
WxConsts.OAUTH2_SCOPE_USER_INFO,
URLEncoder.encode(returnUrl.replace("guiguketan", "#")));
return "redirect:" + redirectURL;
}
@GetMapping("/userInfo")
public String userInfo(@RequestParam("code") String code,
@RequestParam("state") String returnUrl) throws Exception {
WxMpOAuth2AccessToken wxMpOAuth2AccessToken = this.wxMpService.oauth2getAccessToken(code);
String openId = wxMpOAuth2AccessToken.getOpenId();
System.out.println("【微信网页授权】openId={}"+openId);
WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);
System.out.println("【微信网页授权】wxMpUser={}"+JSON.toJSONString(wxMpUser));
UserInfo userInfo = userInfoService.getByOpenid(openId);
if(null == userInfo) {
userInfo = new UserInfo();
userInfo.setOpenId(openId);
userInfo.setUnionId(wxMpUser.getUnionId());
userInfo.setNickName(wxMpUser.getNickname());
userInfo.setAvatar(wxMpUser.getHeadImgUrl());
userInfo.setSex(wxMpUser.getSexId());
userInfo.setProvince(wxMpUser.getProvince());
userInfoService.save(userInfo);
}
//生成token
String token = JwtHelper.createToken(userInfo.getId(), userInfo.getNickName());
if(returnUrl.indexOf("?") == -1) {
return "redirect:" + returnUrl + "?token=" + token;
} else {
return "redirect:" + returnUrl + "&token=" + token;
}
}
}
3.5、编写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;
}
}
3.6、使用token
通过token传递用户信息
3.6.1、JWT介绍
JWT工具
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
JWT最重要的作用就是对 token信息的防伪作用。
3.6.2、JWT的原理
一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
(1)公共部分
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
(2)私有部分
用户自定义的内容,根据实际需要真正要封装的信息。
userInfo
(3)签名部分
SaltiP: 当前服务器的Ip地址!
主要用户对JWT生成字符串的时候,进行加密
base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
3.6.3、整合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工具类
public class JwtHelper {
//token字符串有效时间
private static long tokenExpiration = 24*60*60*1000;
//加密编码秘钥
private static String tokenSignKey = "123456";
//根据userid 和 username 生成token字符串
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
//设置token分类
.setSubject("GGKT-USER")
//token字符串有效时长
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
//私有部分(用户信息)
.claim("userId", userId)
.claim("userName", userName)
//根据秘钥使用加密编码方式进行加密,对字符串压缩
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取userid
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
//从token字符串获取getUserName
public static String getUserName(String token) {
if(StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws
= Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String)claims.get("userName");
}
public static void main(String[] args) {
String token = JwtHelper.createToken(1L, "lucy");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUserName(token));
}
}
微信支付
4.1、微信支付
接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
4.2、公众号配置
(1)绑定域名
与微信分享一致
先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS接口安全域名”。
说明:因为测试号不支持支付功能,需要使用正式号才能进行测试。
(2)商户平台配置支付目录
4.3、创建订单支付接口
(1)创建WXPayController
@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/api/order/wxPay")
public class WXPayController {
@Autowired
private WXPayService wxPayService;
@ApiOperation(value = "下单 小程序支付")
@GetMapping("/createJsapi/{orderNo}")
public Result createJsapi(
@ApiParam(name = "orderNo", value = "订单No", required = true)
@PathVariable("orderNo") String orderNo) {
return Result.ok(wxPayService.createJsapi(orderNo));
}
}
(2)创建WXPayService
public interface WXPayService {
Map createJsapi(String orderNo);
}
(3)service_order引入依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
(4)创建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", "wxf913bfa3a2c7eeeb");
paramMap.put("mch_id", "1481962542");
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://glkt.atguigu.cn/api/order/wxPay/notify");
paramMap.put("trade_type", "JSAPI");
// paramMap.put("openid", "o1R-t5trto9c5sdYt6l1ncGmY5Y");
//UserInfo userInfo = userInfoFeignClient.getById(paymentInfo.getUserId());
// paramMap.put("openid", "oepf36SawvvS8Rdqva-Cy4flFFg");
paramMap.put("openid", "oQTXC56lAy3xMOCkKCImHtHoLL");
//2、HTTPClient来根据URL访问第三方接口并且传递参数
HttpClientUtils client = new HttpClientUtils("https://api.mch.weixin.qq.com/pay/unifiedorder");
//client设置参数
client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9"));
client.setHttps(true);
client.post();
//3、返回第三方的数据
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
if(null != resultMap.get("result_code") && !"SUCCESS".equals(resultMap.get("result_code"))) {
System.out.println("error1");
}
//4、再次封装参数
Map<String, String> parameterMap = new HashMap<>();
String prepayId = String.valueOf(resultMap.get("prepay_id"));
String packages = "prepay_id=" + prepayId;
parameterMap.put("appId", "wxf913bfa3a2c7eeeb");
parameterMap.put("nonceStr", resultMap.get("nonce_str"));
parameterMap.put("package", packages);
parameterMap.put("signType", "MD5");
parameterMap.put("timeStamp", String.valueOf(new Date().getTime()));
String sign = WXPayUtil.generateSignature(parameterMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9");
//返回结果
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<>();
}
}
}
4.4、服务号测试过程
(1)修改service-user模块配置文件
wechat.mpAppId: wxf913bfa3a2c7eeeb
## 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: cd360d429e5c8db0c638d5ef9df74f6d
(2)service-user模块创建controller
@Controller
@RequestMapping("/api/user/openid")
public class GetOpenIdController {
@Autowired
private WxMpService wxMpService;
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
String userInfoUrl =
"http://ggkt.vipgz1.91tunnel.com/api/user/openid/userInfo";
String redirectURL = wxMpService
.oauth2buildAuthorizationUrl(userInfoUrl,
WxConsts.OAUTH2_SCOPE_USER_INFO,
URLEncoder.encode(returnUrl.replace("guiguketan", "#")));
return "redirect:" + redirectURL;
}
@GetMapping("/userInfo")
@ResponseBody
public String userInfo(@RequestParam("code") String code,
@RequestParam("state") String returnUrl) throws Exception {
WxMpOAuth2AccessToken wxMpOAuth2AccessToken = this.wxMpService.oauth2getAccessToken(code);
String openId = wxMpOAuth2AccessToken.getOpenId();
System.out.println("【微信网页授权】openId={}"+openId);
return openId;
}
}
(3)修改前端App.vue
......
if (token == '') {
let url = window.location.href.replace('#', 'guiguketan')
//修改认证controller路径
window.location = 'http://ggkt.vipgz1.91tunnel.com/api/user/openid/authorize?returnUrl=' + url
}
......
(4)复制返回的openid到支付接口中测试
4.5、整合点播视频支付前端
(1)trade.vue
<template>
<div>
<van-image width="100%" height="200" :src="courseVo.cover"/>
<h1 class="van-ellipsis course_title">{{ courseVo.title }}</h1>
<div class="course_teacher_price_box">
<div class="course_teacher_price">
<div class="course_price">价格:</div>
<div class="course_price_number">¥{{ courseVo.price }}</div>
</div>
</div>
<div class="course_teacher_price_box">
<div class="course_teacher_box">
<div class="course_teacher">主讲: {{ teacher.name }}</div>
<van-image :src="teacher.avatar" round width="50px" height="50px" />
</div>
</div>
<van-loading vertical="true" v-show="loading">加载中...</van-loading>
<div style="position:fixed;left:0px;bottom:50px;width:100%;height:50px;z-index:999;">
<!-- 优惠券单元格 -->
<van-coupon-cell
:coupons="coupons"
:chosen-coupon="chosenCoupon"
@click="showList = true"
/>
<!-- 优惠券列表 -->
<van-popup
v-model="showList"
round
position="bottom"
style="height: 90%; padding-top: 4px;"
>
<van-coupon-list
:coupons="coupons"
:chosen-coupon="chosenCoupon"
:disabled-coupons="disabledCoupons"
@change="onChange"
/>
</van-popup>
</div>
<van-goods-action>
<van-submit-bar :price="finalAmount" button-text="确认下单" @submit="sureOrder"/>
</van-goods-action>
</div>
</template>
<script>
import courseApi from '@/api/course'
import orderApi from '@/api/order'
import couponApi from '@/api/coupon'
export default {
data() {
return {
loading: false,
courseId: null,
courseVo: {},
teacher: {},
orderId: null,
showList:false,
chosenCoupon: -1,
coupons: [],
disabledCoupons: [],
couponId: null,
couponUseId: null,
couponReduce: 0,
finalAmount: 0
};
},
created() {
this.courseId = this.$route.params.courseId;
this.fetchData()
this.getCouponInfo();
},
methods: {
onChange(index) {
debugger
this.showList = false;
this.chosenCoupon = index;
this.couponId = this.coupons[index].id;
this.couponUseId = this.coupons[index].couponUseId;
this.couponReduce = this.coupons[index].value;
this.finalAmount = parseFloat(this.finalAmount) - parseFloat(this.couponReduce)
},
fetchData() {
debugger
this.loading = true;
courseApi.getInfo(this.courseId).then(response => {
// console.log(response.data);
this.courseVo = response.data.courseVo;
this.teacher = response.data.teacher;
//转换为分
this.finalAmount = parseFloat(this.courseVo.price)*100;
this.loading = false;
});
},
getCouponInfo() {
//debugger
couponApi.findCouponInfo().then(response => {
// console.log(response.data);
this.coupons = response.data.abledCouponsList;
this.disabledCoupons = response.data.disabledCouponsList;
});
},
sureOrder() {
//debugger
this.loading = true;
let orderFormVo = {
'courseId': this.courseId,
'couponId': this.couponId,
'couponUseId': this.couponUseId
}
orderApi.submitOrder(orderFormVo).then(response => {
console.log(response.data)
this.$router.push({ path: '/pay/'+response.data })
})
}
}
};
</script>
<style lang="scss" scoped>
.gap {
height: 10px;
}
::v-deep.van-image {
display: block;
}
.course_count {
background-color: #82848a;
color: white;
padding: 5px;
text-align: center;
border-right: 1px solid #939393;
h1 {
font-size: 14px;
margin: 0;
}
p {
margin: 0;
font-size: 16px;
}
}
.course_title {
font-size: 20px;
margin: 10px;
}
.course_teacher_price_box {
margin: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.course_teacher_price {
display: flex;
font-size: 14px;
align-items: center;
.course_price_number {
color: red;
font-size: 18px;
font-weight: bold;
}
}
.course_teacher_box {
display: flex;
justify-content: center;
align-items: center;
.course_teacher {
margin-right: 20px;
}
}
}
.course_contents {
margin: 10px;
.course_title_font {
color: #68cb9b;
font-weight: bold;
}
.course_content {
margin-bottom: 20px;
}
}
.course_chapter_list {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 14px;
}
p {
margin: 0;
}
}
</style>
(2)pay.vue
<template>
<div>
<van-image width="100%" height="200" src="https://cdn.uviewui.com/uview/swiper/1.jpg"/>
<h1 class="van-ellipsis course_title">课程名称: {{ orderInfo.courseName }}</h1>
<div class="course_teacher_price_box">
<div class="course_price">订单号:{{ orderInfo.outTradeNo }}</div>
</div>
<div class="course_teacher_price_box">
<div class="course_price">下单时间:{{ orderInfo.createTime }}</div>
</div>
<div class="course_teacher_price_box">
<div class="course_price">支付状态:{{ orderInfo.orderStatus == 'UNPAID' ? '未支付' : '已支付' }}</div>
</div>
<div class="course_teacher_price_box" v-if="orderInfo.orderStatus == 'PAID'">
<div class="course_price">支付时间:{{ orderInfo.payTime }}</div>
</div>
<van-divider />
<div class="course_teacher_price_box">
<div class="course_price">订单金额:<span style="color: red">¥{{ orderInfo.originAmount }}</span></div>
</div>
<div class="course_teacher_price_box">
<div class="course_price">优惠券金额:<span style="color: red">¥{{ orderInfo.couponReduce }}</span></div>
</div>
<div class="course_teacher_price_box">
<div class="course_price">支付金额:<span style="color: red">¥{{ orderInfo.finalAmount }}</span></div>
</div>
<van-goods-action>
<van-goods-action-button type="danger" text="支付" @click="pay" v-if="orderInfo.orderStatus == '0'"/>
<van-goods-action-button type="warning" text="去观看" @click="see" v-else/>
</van-goods-action>
<van-loading vertical="true" v-show="loading">加载中...</van-loading>
</div>
</template>
<script>
import orderApi from '@/api/order'
export default {
data() {
return {
loading: false,
orderId: null,
orderInfo: {},
showList:false,
chosenCoupon: -1,
coupons: [],
disabledCoupons: [],
couponReduce: 0,
finalAmount: 0
};
},
created() {
this.orderId = this.$route.params.orderId;
this.fetchData();
},
methods: {
fetchData() {
this.loading = true;
orderApi.getInfo(this.orderId).then(response => {
this.orderInfo = response.data;
this.finalAmount = parseFloat(this.orderInfo.finalAmount) * 100;
this.loading = false;
});
},
pay() {
this.loading = true;
orderApi.createJsapi(this.orderInfo.outTradeNo).then(response => {
console.log(response.data)
this.loading = false;
this.onBridgeReady(response.data)
})
},
onBridgeReady(data) {
let that = this;
console.log(data)
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
'appId': data.appId, //公众号ID,由商户传入
'timeStamp': data.timeStamp, //时间戳,自1970年以来的秒数
'nonceStr': data.nonceStr, //随机串
'package': data.package,
'signType': data.signType, //微信签名方式:
'paySign': data.paySign //微信签名
},
function (res) {
if (res.err_msg == 'get_brand_wcpay_request:ok') {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
console.log('支付成功')
that.queryPayStatus();
}
});
},
queryPayStatus() {
// 回调查询
orderApi.queryPayStatus(this.orderInfo.outTradeNo).then(response => {
console.log(response.data)
this.fetchData()
})
},
see() {
this.$router.push({path: '/courseInfo/' + this.orderInfo.courseId})
}
}
};
</script>
<style lang="scss" scoped>
.gap {
height: 10px;
}
::v-deep.van-image {
display: block;
}
.course_count {
background-color: #82848a;
color: white;
padding: 5px;
text-align: center;
border-right: 1px solid #939393;
h1 {
font-size: 14px;
margin: 0;
}
p {
margin: 0;
font-size: 16px;
}
}
.course_title {
font-size: 20px;
margin: 10px;
}
.course_teacher_price_box {
margin: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.course_teacher_price {
display: flex;
font-size: 14px;
align-items: center;
.course_price_number {
color: red;
font-size: 18px;
font-weight: bold;
}
}
.course_teacher_box {
display: flex;
justify-content: center;
align-items: center;
.course_teacher {
margin-right: 20px;
}
}
}
.course_contents {
margin: 10px;
.course_title_font {
color: #68cb9b;
font-weight: bold;
}
.course_content {
margin-bottom: 20px;
}
}
.course_chapter_list {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 14px;
}
p {
margin: 0;
}
}
</style>
4.6、订单详情接口
(1)OrderInfoApiController添加方法
@ApiOperation(value = "获取")
@GetMapping("getInfo/{id}")
public Result getInfo(@PathVariable Long id) {
OrderInfoVo orderInfoVo = orderInfoService.getOrderInfoVoById(id);
return Result.ok(orderInfoVo);
}
(2)OrderInfoServiceImpl实现方法
@Override
public OrderInfoVo getOrderInfoVoById(Long id) {
OrderInfo orderInfo = this.getById(id);
OrderDetail orderDetail = orderDetailService.getById(id);
OrderInfoVo orderInfoVo = new OrderInfoVo();
BeanUtils.copyProperties(orderInfo, orderInfoVo);
orderInfoVo.setCourseId(orderDetail.getCourseId());
orderInfoVo.setCourseName(orderDetail.getCourseName());
return orderInfoVo;
}
4.7、查询支付结果
(1)WXPayController添加方法
@ApiOperation(value = "查询支付状态")
@GetMapping("/queryPayStatus/{orderNo}")
public Result queryPayStatus(
@ApiParam(name = "orderNo", value = "订单No", required = true)
@PathVariable("orderNo") String orderNo) {
System.out.println("orderNo:"+orderNo);
//调用查询接口
Map<String, String> resultMap = wxPayService.queryPayStatus(orderNo);
if (resultMap == null) {//出错
return Result.fail(null).message("支付出错");
}
if ("SUCCESS".equals(resultMap.get("trade_state"))) {//如果成功
//更改订单状态,处理支付结果
String out_trade_no = resultMap.get("out_trade_no");
System.out.println("out_trade_no:"+out_trade_no);
orderInfoService.updateOrderStatus(out_trade_no);
return Result.ok(null).message("支付成功");
}
return Result.ok(null).message("支付中");
}
(2)WXPayServiceImpl实现方法
@Override
public Map queryPayStatus(String orderNo) {
try {
//1、封装参数
Map paramMap = new HashMap<>();
paramMap.put("appid", wxPayAccountConfig.getAppId());
paramMap.put("mch_id", wxPayAccountConfig.getMchId());
paramMap.put("out_trade_no", orderNo);
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
//2、设置请求
HttpClientUtils client = new HttpClientUtils("https://api.mch.weixin.qq.com/pay/orderquery");
client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, wxPayAccountConfig.getKey()));
client.setHttps(true);
client.post();
//3、返回第三方的数据
String xml = client.getContent();
Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
//6、转成Map
//7、返回
return resultMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
(3)OrderInfoServiceImpl实现方法
@Override
public void updateOrderStatus(String out_trade_no) {
//根据out_trade_no查询订单
LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfo::getOutTradeNo,out_trade_no);
OrderInfo orderInfo = baseMapper.selectOne(wrapper);
//更新订单状态 1 已经支付
orderInfo.setOrderStatus("1");
baseMapper.updateById(orderInfo);
}
微信分享
1、实现目标
1、点播课程详情页面分享
2、微信分享实现方式
参考文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
2.1、绑定域名
先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS接口安全域名”。
说明:本地测试设置内网穿透地址。
2.2、公众号测试号配置
2.3、引入JS文件
<script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js" type="text/javascript"></script>
引入前端项目/public/index.html文件
2.4、封装分享js
参考官方文档封装接口
我们需要分享的页面有直播详情页、点播课程详情页等,因此我们把分享代码封装后,在对应的页面直接引入与调用即可
新建src/util/wxShare.js文件
/**
* 微信js-sdk
* 参考文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
*/
const wxShare = {
/**
* [wxRegister 微信Api初始化]
* @param {Function} callback [ready回调函数]
*/
wxRegister(data,option) { //data是微信配置信息,option是分享的配置内容
wx.config({
debug: true, // 开启调试模式
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timestamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature,// 必填,签名,见附录1
jsApiList: [
'onMenuShareAppMessage'
] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
wx.ready(function(){
wx.onMenuShareAppMessage({
title: option.title, // 分享标题
desc: option.desc, // 分享描述
link: option.link, // 分享链接
imgUrl: option.imgUrl, // 分享图标
success() {
// 用户成功分享后执行的回调函数
// option.success()
console.log('ok');
},
cancel() {
// 用户取消分享后执行的回调函数
// option.error()
console.log('cancel');
}
});
});
wx.error(function(res){
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
//alert('error:'+JSON.stringify(res));
});
}
}
export default wxShare
2.5、服务器端接口
新增ShareController类
说明:微信分享要对当前url加密处理,由于我们的url路由都是带“#”符号,服务器端接收不到,因此通过“guiguketan”单词代替了“#”。
@RestController
@RequestMapping("/api/wechat/share")
@Slf4j
public class ShareController {
@Autowired
private WxMpService wxMpService;
@GetMapping("/getSignature")
public Result getSignature(@RequestParam("url") String url) throws WxErrorException {
String currentUrl = url.replace("guiguketan", "#");
WxJsapiSignature jsapiSignature = wxMpService.createJsapiSignature(currentUrl);
WxJsapiSignatureVo wxJsapiSignatureVo = new WxJsapiSignatureVo();
BeanUtils.copyProperties(jsapiSignature, wxJsapiSignatureVo);
wxJsapiSignatureVo.setUserEedId(Base64Util.base64Encode(AuthContextHolder.getUserId()+""));
return Result.ok(wxJsapiSignatureVo);
}
}
2.6、点播课程详情分享
页面:courseInfo.vue
(1)引入分享
import wxShare from '@/utils/wxShare'
(2)代码实现
关键代码
wxRegister() {
//说明:后台加密url必须与当前页面url一致
let url = window.location.href.replace('#', 'guiguketan')
shareApi.getSignature(url).then(response => {
console.log(response.data);
//记录分享用户
let link = '';
if(window.location.href.indexOf('?') != -1) {
link = window.location.href + '&recommend=' + response.data.userEedId;
} else {
link = window.location.href + '?recommend=' + response.data.userEedId;
}
let option = {
'title': this.courseVo.title,
'desc': this.description,
'link': link,
'imgUrl': this.courseVo.cover
}
wxShare.wxRegister(response.data, option);
});
}
2.7、测试
(1)使用手机测试,其他端测试可能会出现错误问题
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 5,2022