微信小程序 登录&支付

发布于 2019-05-08  35 次阅读


最近做微信小程序,在登录与支付上遇上不少坑(尤其是支付).

本文主要记录后端部分,前端部分只是略微带过.后端采用了JAVA,使用了Nutz框架.

微信登录

微信登录比较简单,首先小程序端调用wx.login()函数获取code,然后将code传给后端,后端请求https://api.weixin.qq.com/sns/jscode2session接口获取openid等信息,最后后端生成自己的登录状态返回给前端,注意不要暴露了session_key.

接口文档如下:https://developers.weixin.qq.com/miniprogram/dev/api-backend/code2Session.html

代码实现

resources/custom下创建配置文件weixin.properties

weixin.appId=*****
weixin.appSecret=*****

注意dao.js中的配置

...
conf: {
        type: "org.nutz.ioc.impl.PropertiesProxy",
        fields: {
            paths: ["custom/"]
        }
    },
...

在用户模块中注入配置的值

    @Inject("java:$conf.get('weixin.appId')")
    private String appId;

    @Inject("java:$conf.get('weixin.appSecret')")
    private String appSecret;

登录(这边采用了Jwt来登录)

    @POST
    @Api(name = "登录")
    public Object login(@Param("code")String code) throws Exception {
        NutMap re=new NutMap();
        String url="https://api.weixin.qq.com/sns/jscode2session?appid="+appId+"&secret="+appSecret+"&js_code="+code+"&grant_type=authorization_code";
        Response response=Http.get(url);
        Map data= (Map) Json.fromJson(response.getContent());
        if(data.containsKey("errcode")){
            return re.setv("code",1).setv("msg",data.get("errmsg"));
        }
        User user= dao.fetch(User.class, (String) data.get("openid"));
        if(user==null){
            user=new User();
            user.setOpenid((String) data.get("openid"));
            dao.insert(user);
        }
        return re.setv("code",0).setv("token",JwtUtils.generate(user.getUid()));
    }

注意点

  • 小程序的wx.request()函数使用post时,默认为json,如果服务端不使用json,注意将header['content-type'] 设为 application/x-www-form-urlencoded
  • 对于用户的头像和昵称之类的,可以直接使用微信小程序的open-data来显示

微信支付

一开始没找到SDK,直接写的...不太好写,后来找到了SDK,虽然微信支付开发文档小程序部分没有提供SDK,但是可以使用JSAPI的SDK

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

注意:这个sdk有个巨坑,后面说

必要准备

在写代码之前,需要获得三个值,微信支付的商户编号mch_id和api密钥key,还有微信小程序的appId

注意如果小程序需要开通微信支付,必须是企业账号才行,个人账号是不行的

代码实现

参数配置

继承WXPayConfig,定义一些必要的参数(由于没有使用到证书,所以没有进行相关设置)

public class MyConfig extends WXPayConfig {

    @Override
    public String getAppID() {
        return "微信小程序的AppId";
    }

    @Override
    public String getMchID() {
        return "微信支付的商户号";
    }

    @Override
    public String getKey() {
        return "密钥,注意是商户的密钥,不是小程序的";
    }

    @Override
    public InputStream getCertStream() {
        return null;
    }

    @Override
    public IWXPayDomain getWXPayDomain() {
        return new IWXPayDomain() {
            @Override
            public void report(String domain, long elapsedTimeMillis, Exception ex) {

            }

            @Override
            public DomainInfo getDomain(WXPayConfig config) {
                return new DomainInfo("api.mch.weixin.qq.com",false);
            }
        };
    }
}

统一下单和二次签名

小程序的交易类型为"JSAPI"

注意文档中虽然写着openid必填为否,但是交易类型为JSAPI时,openid必填

        MyConfig config=new MyConfig();
        WXPay wxPay=new WXPay(config);
        Map<String,String> data=new HashMap<>();
        data.put("appid",appId);
        data.put("mch_id",config.getMchID());
        data.put("nonce_str", WXPayUtil.generateNonceStr());
        data.put("body","约马--预约费用");
        data.put("out_trade_no",rid);
        data.put("total_fee", String.valueOf(reservation.getPrice()));
        data.put("spbill_create_ip", Lang.getIP(Mvcs.getReq()));
        data.put("notify_url","https://xxxxxx/pay/callback");
        data.put("trade_type","JSAPI");
        data.put("openid",user.getOpenid());
        data.put("sign",WXPayUtil.generateSignature(data,config.getKey()));
        System.out.println(data);
        Map<String,String> resp=wxPay.unifiedOrder(data);

        if ("SUCCESS".equals(resp.get("return_code"))) {
            Map<String, String> reData = new HashMap<>();
            reData.put("appId", config.getAppID());
            reData.put("nonceStr", resp.get("nonce_str"));
            reData.put("package", "prepay_id=" + resp.get("prepay_id"));
            reData.put("signType","MD5");
            reData.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
            String newSign = WXPayUtil.generateSignature(reData, config.getKey());
            resp.put("paySign",newSign);
            resp.put("timeStamp", reData.get("timeStamp"));
            return re.setv("code",0).setv("data",resp);
        } else {
            return re.setv("code",4).setv("msg","调起支付失败");
        }

小程序端发起支付

小程序端使用wx.requestPayment() 函数即可发起支付,参数即为二次签名后后端返回的数据

注意点

注意这里有个巨坑,文档中写着签名类型必填为否,默认为MD5,但是sdk中默认为HMAC-SHA256,注意在WXPay.java中做如下修改

    public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
        this.config = config;
        this.notifyUrl = notifyUrl;
        this.autoReport = autoReport;
        this.useSandbox = useSandbox;
        if (useSandbox) {
            this.signType = SignType.MD5; // 沙箱环境
        }
        else {
            this.signType = SignType.MD5;//这边改成MD5!!!
        }
        this.wxPayRequest = new WXPayRequest(config);
    }

回调函数

注意读取xml的方法,还有处理时要进行并发控制,对于处理过的信息直接返回成功即可

    @At("/callback")
    @Ok("raw")
    @Fail("raw")
    public String callback(HttpServletRequest request) throws Exception {
        String successReturn="<xml>\n" +
                "\n" +
                "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
                "</xml>";
        String failReturn="<xml>\n" +
                "\n" +
                "  <return_code><![CDATA[FAIL]]></return_code>\n" +
                "</xml>";

        BufferedReader reader=request.getReader();
        String line;
        StringBuilder xmlString=new StringBuilder();
        while ((line=reader.readLine())!=null){
            xmlString.append(line);
        }
        final Map<String,String> map= WXPayUtil.xmlToMap(xmlString.toString());
        System.out.println(map);
        if("SUCCESS".equals(map.get("return_code"))){
            //注意要验证签名
            if(!WXPayUtil.isSignatureValid(map,new MyConfig().getKey())){
                return failReturn;
            }
            //如果处理如果被重复回调时,需要返回SUCCESS
            ......
            //处理时要注意进行并发控制
            ......
        }
        return failReturn;
    }