第1章 准备工作
1.1 微信开放平台
微信扫码登录,需要在微信开放平台注册账号被认证为开发者才能接入官网地址:https://open.weixin.qq.com/
1.1.1 注册账号并认证成为开发者
下图就是官网,我们点击注册,然后根据要求填写必要的信息,点击提交即可。
1.1.2 创建网站应用获取应用AppID 和 AppSecret
1.2 搭建内网穿透
使用免费的内网穿透,官网地址:https://suidao.io/#/
1.2.1 注册账号并登录
先登录网站,然后注册账号。
注册完账号后,就可以登录了
1.2.2 创建隧道
先创建隧道。
- 得到外网访问路径:
1.2.3 下载客户端
下载解压后直接双击SuiDao.Client.exe运行即可
1.3 修改微信开放平台应用授权回调域
把它修改成内网穿透的地址即可,如图所示:
第2章 后端项目搭建以及开发
2.1 从gitee码云上获取后端demo
gitee地址:https://gitee.com/77jubao2015/springbootdemo
2.1.1 引入相关架包
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.2 创建数据库xueden_wxcode
打开数据库客户端创建数据库,如图说是:
2.3 修改配置文件application.yml
2.3.1 修改数据库连接账号和密码
修改为自己本地电脑的即可
2.3.2 配置 AppID 和 AppSecret 以及授权回调域
代码如下所示:
wxcode:
appId: wx7aa745fb92387941
appSecret: xxxxxxxxxxxxxx
redirectUri: http://wxcode.sh1.k9s.run:2271/wechat/callback
frontUrl: http://localhost:8080/
2.4 编写一个WechatConfig配置类
代码如下所示:
package cn.xueden.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**微信开放平台配置文件
* @Auther:梁志杰
* @Date:2021/11/13
* @Description:cn.xueden.config
* @version:1.0
*/
@Configuration
@Data
@ConfigurationProperties(prefix = "wxcode")
public class WechatConfig {
private String appId;
private String appSecret;
private String redirectUri;
}
2.5 编写一个WeChatHttpUtils工具类
代码如下所示:
package cn.xueden.utils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
/**
* @Auther:梁志杰
* @Date:2021/11/13
* @Description:cn.xueden.utils
* @version:1.0
*/
public class WeChatHttpUtils {
public static CloseableHttpClient getClient(){
HttpClientBuilder builder = HttpClientBuilder.create();
return builder.build();
}
}
2.6 编写WxApiController控制类
2.6.1 编写一个获取code方法getWxCode
生成微信二维码,代码如下所示:
//1、生成微信二维码
@GetMapping("login")
public String getWxCode() {
//固定地址,拼接参数
//微信开放平台授权baseUrl 固定格式
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
//对redirect_url进行URLEncoder编码
String redirectUrl = wechatConfig.getRedirectUri();
try {
//对URL进行utf-8的编码
URLEncoder.encode(redirectUrl, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = String.format( //向指定字符串中按顺序替换%s
baseUrl,
wechatConfig.getAppId(),
wechatConfig.getRedirectUri(),
"Xueden" //自定义(随意设置)
);
//请求微信地址
return "redirect:" + url;
}
2.6.2 编写一个callback方法
通过code获取access_token 并把扫描人信息添加到数据库里
代码如下所示:
@GetMapping("callback")
//1、获取code值,临时票据、类似于验证码(该数据为扫码后跳转时微信方传来)
public String callback(String code,String state, Model model) {
//2、拿着code请求微信固定的地址,得到两个值access_token 和 openid
String baseAccessTokenUrl =
"https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//3、拼接三个参数:id 密钥 code值
String accessTokenUrl = String.format(
baseAccessTokenUrl,
wechatConfig.getAppId(),
wechatConfig.getAppSecret(),
code
);
HttpGet httpGet = null;
CloseableHttpResponse response = null;
URIBuilder uriBuilder = null;
WxMember wxMember = new WxMember();
try {
//请求这个拼接好的地址,得到两个值access_token 和 openid
//使用httpClient发送请求,得到返回结果
httpGet = new HttpGet(accessTokenUrl);
response = WeChatHttpUtils.getClient().execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode();
JSONObject jsonObject = JSON.parseObject(EntityUtils.toString(response.getEntity()));
String access_token = jsonObject.getString("access_token");
String openid = jsonObject.getString("openid");
String unionid = jsonObject.getString("unionid");
// 获取扫码人信息
uriBuilder = new URIBuilder("https://api.weixin.qq.com/sns/userinfo");
uriBuilder.setParameter("access_token",access_token);
uriBuilder.setParameter("openid",openid);
uriBuilder.setParameter("lang","zh_CN");
httpGet.setHeader("Accept", "application/json");
httpGet.addHeader("Content-type","application/json; charset=utf-8");
httpGet = new HttpGet(uriBuilder.build());
response = WeChatHttpUtils.getClient().execute(httpGet);
JSONObject jsonUserinfo = JSON.parseObject(EntityUtils.toString(response.getEntity()));
log.info("access_token{},openid{},unionid{},获取信息{}",access_token, openid,unionid,jsonUserinfo);
wxMember.setCity(jsonUserinfo.getString("city"));
wxMember.setCountry(jsonUserinfo.getString("country"));
wxMember.setProvince(jsonUserinfo.getString("province"));
wxMember.setHeadimgurl(jsonUserinfo.getString("headimgurl"));
String nickname = new String(jsonUserinfo.getString("nickname").getBytes("ISO-8859-1"), "UTF-8");
wxMember.setNickname(nickname);
wxMember.setOpenid(openid);
wxMember.setUnionid(unionid);
wxMember.setSex(jsonUserinfo.getInteger("sex"));
wxMemberService.saveOrUpdate(wxMember);
} catch (Exception e) {
e.printStackTrace();
}
model.addAttribute("wxmember",wxMember);
model.addAttribute("frontUrl", wechatConfig.getFrontUrl());
return "result";
}
2.6.3 编写一个获取用户列表的方法
代码如下所示:
/**
* 带条件分页查询用户列表
*/
@GetMapping
public ResponseEntity<Object> getList(WxMemberQueryCriteria criteria, PageVo pageVo) throws Exception {
int pageNo = pageVo.getPageIndex()-1;
Pageable pageable = PageRequest.of( pageNo<0?0:pageNo, pageVo.getPageSize() , Sort.Direction.DESC, "id" );
return new ResponseEntity<>(wxMemberService.getList(criteria,pageable), HttpStatus.OK);
}
2.6.4 编写一个获取用户列表的业务接口和实现方法
代码如下所示:
/**
*
* @param criteria
* @param pageable
* @return
*/
Object getList(WxMemberQueryCriteria criteria, Pageable pageable);
/**
* 根据条件分页获取系统管理员列表信息
* @param criteria 查询条件
* @param pageable 分页信息
* @return
*/
@Override
public Object getList(WxMemberQueryCriteria criteria, Pageable pageable) {
Page<WxMember> page = wxMemberRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable);
return PageUtil.toPage(page);
}
2.6.5 编写用户实体类WxMember
代码如下所示:
package cn.xueden.domain;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import javax.validation.constraints.NotNull;
import javax.persistence.*;
import java.sql.Timestamp;
/**
* @Auther:梁志杰
* @Date:2021/5/16
* @Description:cn.xueden
* @version:1.0
*/
@Data
@Entity
@Table(name="t_member")
@org.hibernate.annotations.Table(appliesTo = "t_member",comment="微信用户信息表")
public class WxMember {
/**
* 自增 id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@NotNull(groups = Update.class)
private Long id;
/**
* 用户openid
*/
private String openid;
/**
* 用户unionid
*/
private String unionid;
/**
* 用户昵称
*/
private String nickname;
/**
* 性别
*/
private Integer sex;
/**
* 用户所属省份
*/
private String province;
/**
* 用户所属城市
*/
private String city;
/**
* 用户所属国家
*/
private String country;
/**
* 用户头像
*/
private String headimgurl;
/**
* 创建时间
*/
@Column(name = "create_time",nullable = false)
@CreationTimestamp
private Timestamp createTime;
public @interface Update {}
}
2.6.6 编写一个查询参数类WxMemberQueryCriteria
代码如下所示:
package cn.xueden.dto;
import cn.xueden.annotation.EnableXuedenQuery;
import lombok.Data;
/**功能描述:查询条件
* @Auther:梁志杰
* @Date:2021/5/16
* @Description:cn.xueden.dto
* @version:1.0
*/
@Data
public class WxMemberQueryCriteria {
/**
* 根据性别查询
*/
@EnableXuedenQuery
private int sex;
/**
* 根据昵称模糊查询
*/
@EnableXuedenQuery(blurry = "nickname")
private String nickname;
}
2.6.7 添加一个result.html页面
在resources文件夹下新建一个名为templates文件夹,并在此文件夹下新建result.html页面
代码如下所示:
<!DOCTYPE html>
<html lang="ch" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
<link rel="icon" th:href="@{resource/favicon.ico}" type="image/x-icon"/>
<title>登录跳转中</title>
</head>
<body>
登录中..
<script th:inline="javascript">
var response = [[${wxmember}]];
var frontUrl = [[${frontUrl}]];
window.onload = function () {
window.opener.postMessage(response, frontUrl);
window.close();
}
</script>
</body>
</html>
第3章 前端项目搭建以及开发
3.1 从gitee获取前端demo
gitee地址:https://gitee.com/77jubao2015/wxpaydemo
3.2 修改订单列表组件index.vue
代码如下所示:
<template>
<div>
<div class="search__example--wrap">
<com-search
:data="searchData"
@search-submit="searchSubmit"
@reset-submit="resetSubmit"
/>
</div>
<com-table
v-loading="loading"
:columns="columns"
:data="tableData"
:pagination="{
currentPage: defalutParams.pageIndex,
total: total,
onSizeChange: handleSizeChange,
onCurrentChange: handleCurrentChange
}"
@selection-change="handleSelectionChange"
>
<template #sex="scope">
<el-tag
:type="scope.row.sex === 0
? 'success'
: (scope.row.sex === 1
? 'warning'
: 'danger')"
>{{ scope.row.sex === 0
? '男性'
: (scope.row.sex === 1
? '女性'
: '未知') }}
</el-tag>
</template>
</com-table>
<com-dialog v-model="dialogVisible" :title="title">
<info-write
v-if="comName === 'InfoWrite'"
:info="info"
@close="toggleVisible"
@success="success"
/>
<face-pay
v-if="comName === 'FacePay'"
:info="info"
@close="toggleVisible"
@success="success"
/>
<detail
v-if="comName === 'Detail'"
:info="info"
@close="toggleVisible"
/>
</com-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import InfoWrite from './components/InfoWrite.vue'
import Detail from './components/Detail.vue'
import FacePay from './components/FacePay.vue'
import { useExample } from '@/hooks/useExample'
import { Message } from '_c/Message'
import { getListApi, delsApi, nativePayApi, huaBeiPayApi } from './api'
import { formatTime } from '@/utils'
const searchData = [
{
label: '用户昵称',
value: '',
itemType: 'input',
field: 'nickname',
placeholder: '请输入用户昵称',
clearable: true
},
{
label: '用户性别',
value: '',
itemType: 'select',
field: 'sex',
options: [{
title: '男性',
value: '0'
}, {
title: '女性',
value: '1'
}]
}
]
const columns = [
{
field: 'createTime',
label: '登录时间',
formatter: (row: any, column: any, cellValue: any, index: number) => {
return formatTime(row.createTime, 'yyyy-MM-dd HH:mm:ss')
}
},
{
field: 'nickname',
label: '昵称'
},
{
field: 'openid',
label: 'openid',
showOverflowTooltip: true
},
{
field: 'sex',
label: '性别',
slots: {
default: 'sex'
}
},
{
field: 'unionid',
label: 'unionid'
}
]
export default defineComponent({
// name: 'ExampleDialog',
components: {
InfoWrite,
Detail,
FacePay
},
setup() {
const info = ref<any>(null)
const {
defalutParams,
tableData,
loading,
total,
dialogVisible,
title,
currentChange,
sizeChange,
handleSelectionChange,
selectionData,
delData,
comName,
toggleVisible
} = useExample()
// 请求数据
async function getExampleList(data?: any): Promise<void> {
try {
const res = await getListApi({
params: Object.assign(defalutParams, data || {})
})
total.value = res.totalElements
tableData.value = res.content
} finally {
loading.value = false
}
}
// 查询
function searchSubmit(data: any) {
// 该方法重置了一些默认参数
currentChange(1)
getExampleList(data)
}
// 重置
function resetSubmit(data: any) {
// 该方法重置了一些默认参数
currentChange(1)
getExampleList(data)
}
// 展示多少条
function handleSizeChange(val: number) {
// 该方法重置了一些默认参数
sizeChange(val)
getExampleList()
}
// 展示第几页
function handleCurrentChange(val: number) {
// 该方法重置了一些默认参数
currentChange(val)
getExampleList()
}
// 删除多选
function dels(item?: any) {
delData(async() => {
let ids: number[] = []
if (item.id) {
ids.push(item.id)
} else {
ids = selectionData.value.map((v: any) => {
return v.id
})
}
const res = await delsApi({
data: JSON.stringify(ids)
})
if (res.status === 200) {
Message.success(res.message)
getExampleList()
}
}, { hiddenVerify: item.id, text: '此操作将申请退款, 是否继续?' })
}
// 打开弹窗
function open(row: any, component: string) {
comName.value = component
title.value = !row ? '新增' : (component === 'Detail' ? '刷脸付' : '当面付(打开支付宝扫一扫)')
info.value = row || null
toggleVisible(true)
}
// 成功之后的回调
function success(type: string) {
if (type === 'add') {
currentChange(1)
}
toggleVisible()
getExampleList()
}
getExampleList()
// 调用支付宝网站支付
async function getAliDet(id: number) {
try {
const res = await nativePayApi({
params: {
id: id
}
})
if (res.status === 200) {
const body = document.querySelector('body')
if (body != null) {
body.innerHTML = res.message // 查找到当前页面的body,将后台返回的form替换掉他的内容
}
document.forms[0].setAttribute('target', '_blank') // 新开窗口跳转
document.forms[0].submit() // 执行submit表单提交,让页面重定向,跳转到支付宝页面
}
} catch (e) {
console.log(e)
}
}
// 调用支付宝花呗分期支付
async function getHuaBeiDet(id: number) {
try {
const res = await huaBeiPayApi({
params: {
id: id
}
})
if (res.status === 200) {
console.info('返回信息:', res)
}
} catch (e) {
console.log(e)
}
}
return {
info, open,
searchData, searchSubmit, resetSubmit,
columns,
defalutParams,
loading,
tableData,
total,
title,
dialogVisible,
handleSizeChange,
handleCurrentChange,
handleSelectionChange,
dels,
close, success,
comName,
toggleVisible,
getAliDet,
getHuaBeiDet
}
}
})
</script>
<style>
</style>
3.3 修改api.ts文件
代码如下所示:
export const getListApi = ({ params }: PropsData): any => {
return fetch({ url: '/wechat', method: 'get', params })
}
3.4 修改登录页面
代码所示:
<template>
<div class="login-wrap" @keydown.enter="login">
<div class="login-con">
<el-card class="box-card">
<template #header>
<span class="login--header">登录</span>
</template>
<el-form
ref="loginForm"
:model="form"
:rules="rules"
class="login-form"
>
<el-form-item prop="userName">
<el-input
v-model="form.userName"
placeholder="请输入账号 admin or test"
class="form--input"
>
<template #prefix>
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
</template>
</el-input>
</el-form-item>
<el-form-item prop="passWord">
<el-input
v-model="form.passWord"
show-password
:minlength="3"
:maxlength="18"
placeholder="请输入密码 admin or test"
class="form--input"
>
<template #prefix>
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
type="primary"
class="login--button"
@click="login"
>
登录
</el-button>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
type="success"
class="login--button"
@click="wxlogin"
>
微信扫码登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { permissionStore } from '@/store/modules/permission'
import { appStore } from '@/store/modules/app'
import wsCache from '@/cache'
import { ElNotification } from 'element-plus'
interface FormModule {
userName: string,
passWord: string
}
interface RulesModule {
userName: any[],
passWord: any[]
}
export default defineComponent({
name: 'Login',
setup() {
const { push, addRoute, currentRoute } = useRouter()
const loginForm = ref<HTMLElement | null>(null)
const loading = ref<boolean>(false)
const redirect = ref<string>('')
watch(() => {
return currentRoute.value
}, (route) => {
redirect.value = (route.query && route.query.redirect) as string
}, {
immediate: true
})
const page = reactive({
width: window.screen.width * 0.5,
height: window.screen.height * 0.5
})
const form = reactive<FormModule>({
userName: '',
passWord: ''
})
const rules = reactive<RulesModule>({
userName: [{ required: true, message: '请输入账号' }],
passWord: [{ required: true, message: '请输入密码' }]
})
async function login(): Promise<void> {
const formWrap = unref(loginForm) as any
if (!formWrap) return
loading.value = true
try {
formWrap.validate(async(valid: boolean) => {
if (valid) {
wsCache.set(appStore.userInfo, form)
permissionStore.GenerateRoutes().then(() => {
permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
await addRoute(route.name!, route) // 动态添加可访问路由表
})
permissionStore.SetIsAddRouters(true)
push({ path: redirect.value || '/' })
})
} else {
console.log('error submit!!')
return false
}
})
} catch (err) {
console.log(err)
} finally {
loading.value = false
}
}
async function resolveSocialLogin(e: any): Promise<void> {
console.info('传入参数', e.data)
wsCache.set(appStore.userInfo, e.data)
permissionStore.GenerateRoutes().then(() => {
permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
await addRoute(route.name!, route) // 动态添加可访问路由表
})
permissionStore.SetIsAddRouters(true)
push({ path: redirect.value || '/' })
})
}
async function wxlogin(): Promise<void> {
const url = 'http://wxcode.sh1.k9s.run:2271/wechat/login'
window.open(url, 'newWindow', `resizable=yes, height=${page.height}, width=${page.width}, top=10%, left=10%, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=no, status=no`)
window.addEventListener('message', resolveSocialLogin, false)
}
ElNotification({
title: '提示',
message: '账号 admin 为 前端 控制路由权限,账号 test 为 后端 控制路由权限。密码与账号相同',
duration: 60000
})
return {
loginForm,
loading, redirect, form, rules, page,
login,
wxlogin
}
}
})
</script>
<style lang="less" scoped>
.login-wrap {
width: 100%;
height: 100%;
background-image: url('~@/assets/img/login-bg.jpg');
background-size: cover;
background-position: center;
position: relative;
.box-card {
width: 400px;
.login--header {
font-size: 24px;
font-weight: 600;
}
.svg-container {
color: #889aa4;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.form--input {
width: 100%;
@{deep}(.el-input__inner) {
padding-left: 40px;
}
}
.login--button {
width: 100%;
}
}
.login-con {
position: absolute;
right: 160px;
top: 50%;
transform: translateY(-60%);
}
}
</style>
e’, resolveSocialLogin, false)
}
ElNotification({
title: '提示',
message: '账号 admin 为 前端 控制路由权限,账号 test 为 后端 控制路由权限。密码与账号相同',
duration: 60000
})
return {
loginForm,
loading, redirect, form, rules, page,
login,
wxlogin
}
}
})