前端项目搭建&微信登录、微信支付

185 阅读8分钟

前端技术选型

技术名称说明
Vue.js是一套用于构建用户界面的渐进式JavaScript框架
Element UI库element-ui 是饿了么前端出品的基于 Vue.js的 后台组件库,方便程序员进行页面快速布局和构建
node.js简单的说 Node.js 就是运行在服务端的 JavaScript 运行环境
axios对ajax的封装, 简单来说就是ajax技术实现了局部数据的刷新,axios实现了对ajax的封装

微信登录

vue项目安装

  • 微信官方提供的生成二维码的js
npm install vue-wxlogin

如果不是vue的项目,可以直接引用官方提供的js文件,来生成二维码

页面引入

<div id="loginForm">
    <!-- 登录表单 -->
    <el-form>
    <!-- 登录按钮 -->
    <el-button/>
    <!-- 微信登录图标 -->
    <img/>
</div>

<!-- 二维码 -->
<wxlogin id="wxLoginForm" style="display:none"
         :appid="appid" :scope="scope" :redirect_uri="redirect_uri">
</wxlogin>

<script>
import wxlogin from 'vue-wxlogin'; // 引入

export default {
    name: "Header",
    components: {
        wxlogin // 声明
    },
    data() {
        return {
            appid:"wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
            scope:"snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
            redirect_uri:"http://www.pinzhi365.com/wxlogin", //重定向地址,(回调地址)
        };
    },
    methods: {
        goToLoginWX() {
            document.getElementById("loginForm").style.display = "none";
            document.getElementById("wxLoginForm").style.display = "block";
        }
    }
}
</script>

修改hosts文件

文件位置:C:\Windows\System32\drivers\etc\hosts
回调默认指定的是80端口,别忘记将tomcat的8003端口修改成80

127.0.0.1 www.pinzhi365.com

依赖

<!-- 需要使用HttpServletRequest获得参数 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.4</version>
    <scope>provided</scope>
</dependency>
<!-- 需要使用HttpClient发出请求 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>

封装HttpClient

package commons;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.net.URI;
import java.util.Map;

/**
* @BelongsProject: lagou-edu-web
* @Author: GuoAn.Sun
* @CreateTime: 2020-09-22 11:30
* @Description: httpclient的封装工具类
*/
public class HttpClientUtil {

    public static String doGet(String url) {
        return doGet(url,null);
    }

    /**
    * get请求,支持request请求方式,不支持restfull方式
    *
    * @param url 请求地址
    * @param param 参数
    * @return 响应的字符串
    */
    public static String doGet(String url, Map<String, String> param) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String resultString = "";
        CloseableHttpResponse response =null;
        try {
            // 创建url
            URIBuilder builder = new URIBuilder(url);
            if (param != null) {
                // 在url后面拼接请求参数
                for (String key : param.keySet()) {
                    builder.addParameter(key, param.get(key));
                }
            }
            URI uri = builder.build();

            // 创建http get请求
            HttpGet httpGet = new HttpGet(uri);
            // 执行请求
            response = httpClient.execute(httpGet);
            // 从响应对象中获取状态码(成功或失败的状态)
            int statusCode = response.getStatusLine().getStatusCode();
            System.out.println("响应的状态 = " + statusCode);
            // 200表示响应成功
            if (statusCode == 200) {
                // 响应的内容字符串
                resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            // 释放资源
            try {
                if (response != null) {
                    response.close();
                }

                httpClient.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return resultString;
    }
}

定义从微信返回的数据对象

public class Token {
    private String access_token;//接口调用凭证
    private String expires_in; //access_token接口调用凭证超时时间,单位(秒)
    private String refresh_token;//用户刷新access_token
    private String openid; //授权用户唯一标识
    private String scope; //用户授权的作用域,使用逗号(,)分隔
    private String unionid; //当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段。
}
public class User {
    private String openid;//普通用户的标识,对当前开发者帐号唯一
    private String nickname;//普通用户昵称
    private String sex;//普通用户性别,1为男性,2为女性
    private String province;//普通用户个人资料填写的省份
    private String city;//普通用户个人资料填写的城市
    private String country;//国家,如中国为CN
    private String headimgurl;//用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
    private String privilege;//用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
    private String unionid;//用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
}

回调函数controller

@RestController
public class TestWX {

    @RequestMapping("wxlogin")
    public Object wxlogin(HttpServletRequest request) throws IOException {
        // 1.通过扫码去微信服务器请求,返回的code值
        String code = request.getParameter("code");
        System.out.println("code = " + code);
        // 2.通过code获取access_token,定义获取access_token的url
        String getTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code="+code+"&grant_type=authorization_code";
        // 3.发请求并获得access_token
        String access_token = HttpClientUtil.doGet(getTokenUrl);
        System.out.println("access_token = "+access_token);
        // 4.将返回的json格式字符串转换成对象
        Token token = JSON.parseObject(access_token, Token.class);
        // 5.通过access_token获取获取微信用户个人信息
        String getUserUrl = "https://api.weixin.qq.com/sns/userinfo?access_token="+token.getAccess_token()+"&openid="+token.getOpenid();
        // 6.发请求并获取用户信息
        String user_token = HttpClientUtil.doGet(getUserUrl);
        // 7.将返回的json格式字符串转换成对象
        User user = JSON.parseObject(user_token, User.class);
        // 8.自定义网页逻辑
        System.out.println("昵称 = " + user.getNickname());
        
        return user;
    }
}

如果下面的错误:是因为谷歌浏览器有bug,待修复!切换别的浏览器即可

image.png

web服务的端口号必须是80!

后续

WxLoginController

@RestController
@RequestMapping("user")
public class WxLoginController {

    @Reference // 远程消费
    private UserService userService;

    private UserDTO dto = null; // 是否用微信登录成功,dto为null,则尚未登录

    @GetMapping("wxlogin")
    public String wxlogin(HttpServletRequest request, HttpServletResponseresponse) throws ServletException, IOException {
        // 1. 微信官方发给我们一个临时凭证
        String code = request.getParameter("code");
        System.out.println("【临时凭证】code = " + code);
        // 2. 通过code,去微信官方申请一个正式的token(令牌)
        String getTokenByCode_url ="https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code=" + code +"&grant_type=authorization_code";
        String tokenString = HttpClientUtil.doGet(getTokenByCode_url);
        System.out.println("tokenString = " + tokenString);
        // 将json格式的token字符串转换成实体对象,方便存和取
        Token token = JSON.parseObject(tokenString, Token.class);

        // 3. 通过token,去微信官方获取用户的信息
        String getUserByToken_url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + token.getAccess_token() + "&openid=" + token.getOpenid();
        String userinfoString = HttpClientUtil.doGet(getUserByToken_url);
        System.out.println("userinfoString = " + userinfoString);
        // 将json格式的user字符串转换成实体对象,方便存和取
        WxUser wxUser = JSON.parseObject(userinfoString, WxUser.class);
        System.out.println("微信昵称 = " + wxUser.getNickname());
        System.out.println("微信头像 = " + wxUser.getHeadimgurl());

        // 拉勾的业务流程! 需要 手机号(wxUser.getUnionid())和密码(wxUser.getUnionid()),头像和昵称

        User user = null;
        dto = new UserDTO();
        // 检测手机号是否注册
        Integer i = userService.checkPhone(wxUser.getUnionid());
        if(i == 0){
            // 未注册,自动注册并登录
            userService.register(wxUser.getUnionid(),
            wxUser.getUnionid(),wxUser.getNickname(),wxUser.getHeadimgurl());
            dto.setMessage("手机号尚未注册,系统已帮您自动注册,请牢记密码!");
            user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
        }else{
            user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
            if(user == null){
                dto.setState(300); //300表示失败
                dto.setMessage("帐号密码不匹配,登录失败!");
            }else{
                dto.setState(200); //200表示成功
                dto.setMessage("登录成功!");
            }
        }

        dto.setContent(user);

        response.sendRedirect("http://localhost:8080");
        return null;
    }

    @GetMapping("checkWxStatus")
    public UserDTO checkWxStatus(){
        return this.dto;
    }

    @GetMapping("logout")
    public Object logout(){
        this.dto = null;
        return null;
    }

}

Header.vue

<script>
    created(){

        // 当刷新页面,组件创建成功之后,立刻检测本地储存中是否存在用户对象
        this.userDTO = JSON.parse( localStorage.getItem("user") );
        if(this.userDTO != null){
            this.isLogin = true; // 已登录
        }else{
            // 去检测微信是否登录过
            this.axios
            .get("http://localhost:80/user/checkWxStatus")
            .then( (result)=>{
                this.userDTO = result.data;
                this.phone = this.userDTO.content.phone;
                this.password = this.userDTO.content.password;
                this.login();// 走普通登录
            })
            .catch( (error)=>{
                //this.$message.error("登录失败!");
            });
        }
    }

    //登出
    logout(){
        localStorage.setItem("user", null); // 将登录成功的对象信息保存到本地储存中
        this.isLogin = false; // 更新登录状态
        alert('谢谢使用,再见!');
        // 去检测微信是否登录过
        this.axios.get("http://localhost:80/user/logout")
            .then( (result)=>{
        })
            .catch( (error)=>{
            //this.$message.error("登录失败!");
        });
    }
</script>

解决二维码在谷歌浏览器的bug

谷歌浏览器调试的时候,iframe标签跨域问题导致无法跳转的bug

如果iframe未添加sandbox属性,或者sandbox属性不赋值,就代表采用默认的安全策略

即:iframe的页面将会被当做一个独立的源,并且不能提交表单,不能执行javascript脚本,也不能让包含iframe的父页面导航到其他地方,所有的插件,如flash等也全部不能起作用

简单来说iframe就只剩下一个展示数据的功能,正如他的名字一样,所有的内容都被放进了一个“单独的沙盒”

  • sandbox包含的属性及作用:
属性值说明
allow-scripts允许运行执行脚本
allow-top-navigation允许iframe能够主导window.top进行页面跳转
allow-same-origin允许同域请求,比如ajax,storage
allow-forms允许进行提交表单
allow-popups允许iframe中弹出新窗口,比如,window.open,target=”_blank”
allow-pointer-lock在iframe中可以锁定鼠标,主要和鼠标锁定有关
  • 加上 sandbox=“allow-scripts allow-top-navigation allow-same-origin” 属性,即可解决
  • 官方js:res.wx.qq.com/connect/zh_…
  • 无法修改微信服务器上的js文件,所以我们将js代码放在本地并进行修改:
created(){
    !(function(a, b, c) {
        function d(a) {
            var c = "default";
            a.self_redirect === !0
                ? (c = "true")
                : a.self_redirect === !1 && (c = "false");
                var d = b.createElement("iframe"),
                e =
                    "https://open.weixin.qq.com/connect/qrconnect?appid=" +
                    a.appid +
                    "&scope=" +
                    a.scope +
                    "&redirect_uri=" +
                    a.redirect_uri +
                    "&state=" +
                    a.state +
                    "&login_type=jssdk&self_redirect=" +
                    c +
                    "&styletype=" +
                    (a.styletype || "") +
                    "&sizetype=" +
                    (a.sizetype || "") +
                    "&bgcolor=" +
                    (a.bgcolor || "") +
                    "&rst=" +
                    (a.rst || "");
                (e += a.style ? "&style=" + a.style : ""),
                (e += a.href ? "&href=" + a.href : ""),
                (d.src = e),
                (d.frameBorder = "0"),
                (d.allowTransparency = "true"),
                (d.sandbox = "allow-scripts allow-top-navigation allow-same-origin"),
                // 允许多种请求
                (d.scrolling = "no"),
                (d.width = "300px"),
                (d.height = "400px");
            var f = b.getElementById(a.id);
            (f.innerHTML = ""), f.appendChild(d);
        }
        a.WxLogin = d;
    })(window, document);
}

Course.vue

<div id="wxLoginForm"></div>
methods: {
    // 微信登录
    goToLoginWX() {
        // 普通的登录表单隐藏
        document.getElementById("loginForm").style.display = "none";
        // 显示二维码的容器
        document.getElementById("wxLoginForm").style.display = "block";
        // 去生成二维码
        this.$nextTick(function(){
            this.createCode(); // 直接调用会报错:TypeError: Cannot read property'appendChild' of null
        });
    },
    // 生成二维码
    createCode(){
        var obj = new WxLogin({
            id:"wxLoginForm", // 显示二维码的容器
            appid: "wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
            scope: "snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
            redirect_uri: "http://www.pinzhi365.com/wxlogin", //重定向地址,(回调地址)
            href: "data:text/css;base64,加密后的样式"
        });
    },
}    
.impowerBox .qrcode {width: 200px;}
.impowerBox .title {display: none;}
.impowerBox .info {width: 200px;}
.status_icon {display: none}cs
.impowerBox .status {text-align: center;}

我们用站长工具对样式代码进行base64加密:tool.chinaz.com/Tools/Base6…

image.png

微信支付

创建二维码

1、安装 qrcodejs2 (注意:安装的是qrcodejs2,不要安装qrcode ---> 会报错)

npm install qrcodejs2 --save

2、页面中引入

<el-dialog :visible.sync="dialogFormVisible" style="width:800px;margin:0px auto;" >
<h1 style="font-size:30px;color:#00B38A">微信扫一扫支付</h1>
<div id="qrcode" style="width:210px;margin:20px auto;"></div>
</el-dialog>

<script>
import QRCode from 'qrcodejs2'; // 引入qrcodejs

export default {
    name: "Index",
    components: {
        Header,
        Footer,
        QRCode // 声明组件
    },
    data() {
        return {
            dialogFormVisible: false, // 是否显示登录框,true:显示,false:隐藏
        };
    },
    methods: {
        // 购买课程
        buy(courseid) {
            //alert("购买第【" + courseid + "】门课程成功,加油!");
            this.dialogFormVisible = true; //显示提示框

            // 待dom更新之后再用二维码渲染其内容
            this.$nextTick(function(){
                this.createCode(); // 直接调用会报错:TypeError: Cannot read property'appendChild' of null
            });
        },
        // 生成二维码
        createCode(){
            // QRCode(存放二维码的dom元素id,二维码的属性参数)
            let qrcode = new QRCode('qrcode',{
                width:200, // 二维码的宽度
                height:200, // 二维码的高度
                text:"我爱你中国" // 二维码中包含的信息
            });
        },
    },
};
</script>

名词介绍

参数说明
appid微信公众帐号或开放平台APP的唯一标识
partner商户号(配置文件中的partner:账户)
partnerkey商户密钥(密码)
sign数字签名,根据微信官方提供的密钥和一套算法生成的一个加密信息,就是为了保证交易安全
  • 如果获得这些信息?
    • 需要注册认证公众号,费用300元/次

查看自己的公众号的参数

public class PayConfig {
    //企业公众号ID
    public static String appid = "wx8397f8696b538317";
    // 财付通平台的商户帐号
    public static String partner = "1473426802";
    // 财付通平台的商户密钥
    public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
    // 回调URL
    public static String notifyurl ="http://a31ef7db.ngrok.io/WeChatPay/WeChatPayNotify";
}

SDK

pay.weixin.qq.com/wiki/doc/ap…

image.png

<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

主要使用sdk中的三个功能:
1、获取随机字符串(生成订单编号)

WXPayUtil.generateNonceStr();

2、将map转换成xml字符串(自动添加签名))

WXPayUtil.generateSignedXml(map,partnerKey);

3、将xml字符串转换整map

WXPayUtil.xmlToMap(result);

JFinal 框架

  • JFinal 是基于Java 语言的极速 web 开发框架,其核心设计目标是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展
  • 取代HttpClient
<dependency>
    <groupId>com.jfinal</groupId>
    <artifactId>jfinal</artifactId>
    <version>3.5</version>
</dependency>

构建二维码

  • Course.vue
<script>
    // 生成二维码
    createCode () {
        this.axios
            .get("http://localhost:80/order/createCode", {
                params: {
                    courseid: this.course.id, // 课程编号
                    courseid: this.course.courseName, // 课程名称
                    price: this.course.discounts, // 优惠价,非原价
                },
            })
            .then((result) => {
                console.log(result);
                let qrcode = new QRCode('qrcode',{
                    width:200,
                    height:200,
                    text:result.data.code_url // 将支付连接嵌入到二维码中
                });
            })
            .catch((error) => {
                this.$message.error("二维码生成失败!");
            });
    }
</script>
  • 支付配置
public class PayConfig {
    //企业公众号ID
    public static String appid = "wx8397f8696b538317";
    // 财付通平台的商户帐号
    public static String partner = "1473426802";
    // 财付通平台的商户密钥
    public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
    // 回调URL
    public static String notifyurl ="http://a31ef7db.ngrok.io/WeChatPay/WeChatPayNotify";
}
  • createCodeController
import com.alibaba.fastjson.JSON;
import com.github.wxpay.sdk.WXPayUtil;
import com.jfinal.kit.HttpKit;
import commons.PayConfig;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("order")
public class PayAction {

    @GetMapping("createCode")
    public Object createCode(String courseid,String coursename, String price) throws Exception {

        //1.编写商户信息
        Map<String,String> mm = new HashMap();
        mm.put("appid",PayConfig.appid); //公众账号ID
        mm.put("mch_id",PayConfig.mchid);//商户号
        mm.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符串
        mm.put("body",coursename); //商品名称
        String orderId = WXPayUtil.generateNonceStr();
        System.out.println("订单编号 = "+orderId);
        mm.put("out_trade_no",orderId); //商户订单号
        mm.put("total_fee",price+""); //订单金额,单位分
        mm.put("spbill_create_ip","127.0.0.1"); //终端IP
        mm.put("notify_url",PayConfig.notifyurl); //通知地址
        mm.put("trade_type","NATIVE"); //交易类型
        System.out.println("发送的map = "+mm.toString());

        //2.生成数字签名,并把上面的map转换成xml格式
        String xml = WXPayUtil.generateSignedXml(mm,PayConfig.partnerKey);
        System.out.println("转换后的xml = "+xml);

        //3.将数据发送给微信后台,并得到微信后台返回的数据
        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        String result = HttpKit.post(url,xml);
        System.out.println("返回的xml = "+result); //如果报错:<![CDATA[签名错误]]>商户四要素的原因,重置商户API密钥。

        //4.后台返回的xml格式,转成map,并添加两个参数
        Map<String,String> resultMap = WXPayUtil.xmlToMap(result);
        resultMap.put("orderId",orderId);
        resultMap.put("money",price+"");

        //5.将map返回给浏览器
        return resultMap;
    }
}
  • 解决乱码
coursename = new String(coursename.getBytes("ISO-8859-1"),"UTF-8");
  • 检查支付状态
  • 保存订单并更新状态