Allen 8 лет назад
Сommit
4df9fb9443
16 измененных файлов с 2677 добавлено и 0 удалено
  1. 131 0
      README.md
  2. 60 0
      app.js
  3. 90 0
      bin/www
  4. 19 0
      package.json
  5. 1 0
      public/MP_verify_McIcGTBFz35y0f9M.txt
  6. 92 0
      public/get-weixin-code.html
  7. 23 0
      public/index.html
  8. 8 0
      public/stylesheets/style.css
  9. 9 0
      routes/index.js
  10. 9 0
      routes/users.js
  11. 38 0
      routes/wechatpay.js
  12. 6 0
      views/error.jade
  13. 5 0
      views/index.jade
  14. 7 0
      views/layout.jade
  15. 1116 0
      yarn-error.log
  16. 1063 0
      yarn.lock

+ 131 - 0
README.md

@@ -0,0 +1,131 @@
+# 微信支付接入指南
+
+本文讲述 如何使用 NodeJs 接入微信支付的流程指引与注意事项
+
+## 准备
+在接入之前需要学习与准备一些内容
+
+- 待接入应用的
+    - 应用ID appid
+    - 商户ID mch_id
+    - APIKEY(于微信商户平台设置与获取)
+    - AppSecret
+- 微信商户 微信公众 平台内授权业务运行的域名
+- 了解微信 `OAuth2.0` 运作流程
+- 了解两款能提升开发效率的外部库
+    - [tenpay][1] 
+    - [wechat-oauth][2]
+
+如果你看完了上方的内容但不理解,请下载`PHP DEMO`并运行,了解工作机制. 或通读`微信支付官方文档`.
+
+## 着手接入
+
+## 获取用户授权
+
+微信OAuth2.0授权登录让微信用户使用微信身份安全登录第三方应用或网站,在微信用户授权登录已接入微信OAuth2.0的第三方应用后,第三方可以获取到用户的接口调用凭证(access_token),通过access_token可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。
+
+微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有server端的应用授权。该模式整体流程为:
+
+1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
+2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
+3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
+
+获取access_token时序图:
+
+![how-to-get-access_token](https://res.wx.qq.com/op_res/D0wkkHSbtC6VUSHX4WsjP5ssg5mdnEmXO8NGVGF34dxS9N1WCcq6wvquR4K_Hcut)
+
+在你的项目中引入 `wechat-ouath`, 并实例化
+
+```Javascript
+const OAuth = require('wechat-oauth');
+
+const oauthClient = new OAuth('appid', 'appecret');
+```
+
+在你的业务逻辑中生成用户授权过程需要访问的URL, 比如:
+
+```Javascript
+let url = oauthClient.getAuthorizeURLForWebsite('重定向URL');
+```
+
+用户访问该条URL后,CODE会通过参数的形式传递至重定向URL中,像是这样的: `redirect_uri?code=CODE&state=STATE`
+
+在业务中自行捕获返回CODE,用于获取 `openid` 以创建订单
+
+```Javascript
+oauthClient.getAccessToken('code', function (err, result) {
+  let accessToken = result.data.access_token;
+  let openId = result.data.openid;
+});
+```
+
+## 创建订单
+
+当我们获取到 `openId` 后,可以在业务逻辑中创建订单,并发起支付请求了.
+首先引入 `tenpay` 并实例化:
+```Javascript
+const tenpay = require('tenpay');
+const config = {
+  appid: '公众号ID',
+  mchid: '微信商户号',
+  partnerKey: '微信支付安全密钥',
+  pfx: require('fs').readFileSync('证书文件路径'),
+  notify_url: '支付回调网址',
+  spbill_create_ip: 'IP地址'
+};
+// 方式一
+const api = new tenpay(config);
+//方式二
+const api = tenpay.init(config);
+
+// 沙盒模式(用于微信支付验收)
+const sandboxAPI = await tenpay.sandbox(config);
+```
+
+Config说明:
+
+- `appid` - 公众号ID(必填)
+- `mchid` - 微信商户号(必填)
+- `partnerKey` - 微信支付安全密钥(必填, 在微信商户管理界面获取)
+- `pfx` - 证书文件(选填, 在微信商户管理界面获取)
+  - 当不需要调用依赖证书的API时可不填此参数
+  - 若业务流程中使用了依赖证书的API则需要在初始化时传入此参数
+- `notify_url` - 支付结果通知回调地址(选填)
+  - 可以在初始化的时候传入设为默认值, 不传则需在调用相关API时传入
+  - 调用相关API时传入新值则使用新值
+- `refund_url` - 退款结果通知回调地址(选填)
+  - 可以在初始化的时候传入设为默认值, 不传则使用微信商户后台配置
+  - 调用相关API时传入新值则使用新值
+- `spbill_create_ip` - IP地址(选填)
+  - 可以在初始化的时候传入设为默认值, 不传则默认值为`127.0.0.1`
+  - 调用相关API时传入新值则使用新值
+
+实例化完成后,尝试使用 `统一下单API` 创建一笔订单:
+```Javascript
+const router = express.Router();
+
+router.get('/', async function (req, res, next) {
+    let result = await api.unifiedOrder({
+        out_trade_no: '商户内部订单号', //应自行维护订单号
+        body: '商品简单描述',
+        total_fee: 100,
+        openid: '用户openid'
+    });
+
+    console.log(result);
+});
+```
+支付结果会在支付动作结束后返回到 `result` 中
+
+## 结语
+
+至此,我们已经完成了微信支付的接入.
+
+本文若有未涵盖到的部分,可参见 `微信支付接入` 文档自行查阅所需信息.
+
+作者 [@Allen][3]     
+2017 年 3月 28日  
+
+[1]: https://github.com/befinal/node-tenpay
+[2]: https://github.com/node-webot/wechat-oauth
+[3]: https://xxss.ga

+ 60 - 0
app.js

@@ -0,0 +1,60 @@
+const express = require('express');
+const path = require('path');
+const favicon = require('serve-favicon');
+const logger = require('morgan');
+const cookieParser = require('cookie-parser');
+const bodyParser = require('body-parser');
+
+const index = require('./routes/index');
+const users = require('./routes/users');
+const wechatpay = require('./routes/wechatpay');
+
+const app = express();
+
+//解决跨域问题
+app.all('*', (req, res, next) => {
+  res.header("Access-Control-Allow-Origin", req.headers.origin); //设置来源
+  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
+  res.header("Access-Control-Max-Age", "604800000");
+  res.header("Access-Control-Allow-Credentials", true);
+  res.header("X-Powered-By", ' 3.2.1');
+  // res.header("Content-Type", "application/json;charset=utf-8");
+  next();
+});
+
+// view engine setup
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'jade');
+
+// uncomment after placing your favicon in /public
+//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
+app.use(logger('dev'));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.use('/', index);
+app.use('/users', users);
+app.use('/wechatpay', wechatpay)
+
+// catch 404 and forward to error handler
+app.use(function(req, res, next) {
+  var err = new Error('Not Found');
+  err.status = 404;
+  next(err);
+});
+
+// error handler
+app.use(function(err, req, res, next) {
+  // set locals, only providing error in development
+  res.locals.message = err.message;
+  res.locals.error = req.app.get('env') === 'development' ? err : {};
+
+  // render the error page
+  res.status(err.status || 500);
+  res.render('error');
+});
+
+module.exports = app;

+ 90 - 0
bin/www

@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var app = require('../app');
+var debug = require('debug')('wechat-pay:server');
+var http = require('http');
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+var port = normalizePort(process.env.PORT || '3000');
+app.set('port', port);
+
+/**
+ * Create HTTP server.
+ */
+
+var server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+  var port = parseInt(val, 10);
+
+  if (isNaN(port)) {
+    // named pipe
+    return val;
+  }
+
+  if (port >= 0) {
+    // port number
+    return port;
+  }
+
+  return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  var bind = typeof port === 'string'
+    ? 'Pipe ' + port
+    : 'Port ' + port;
+
+  // handle specific listen errors with friendly messages
+  switch (error.code) {
+    case 'EACCES':
+      console.error(bind + ' requires elevated privileges');
+      process.exit(1);
+      break;
+    case 'EADDRINUSE':
+      console.error(bind + ' is already in use');
+      process.exit(1);
+      break;
+    default:
+      throw error;
+  }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+  var addr = server.address();
+  var bind = typeof addr === 'string'
+    ? 'pipe ' + addr
+    : 'port ' + addr.port;
+  debug('Listening on ' + bind);
+}

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "wechat-pay",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "start": "node ./bin/www"
+  },
+  "dependencies": {
+    "body-parser": "~1.18.2",
+    "cookie-parser": "~1.4.3",
+    "debug": "~2.6.9",
+    "express": "~4.15.5",
+    "jade": "~1.11.0",
+    "morgan": "~1.9.0",
+    "serve-favicon": "~2.4.5",
+    "tenpay": "^2.0.11",
+    "wechat-oauth": "^1.2.1"
+  }
+}

+ 1 - 0
public/MP_verify_McIcGTBFz35y0f9M.txt

@@ -0,0 +1 @@
+McIcGTBFz35y0f9M

+ 92 - 0
public/get-weixin-code.html

@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <title>微信登录</title>
+</head>
+
+<body>
+    <script>
+        var GWC = {
+            version: '1.1.1',
+            urlParams: {},
+            appendParams: function (url, params) {
+                if (params) {
+                    var baseWithSearch = url.split('#')[0];
+                    var hash = url.split('#')[1];
+                    for (var key in params) {
+                        var attrValue = params[key];
+                        if (attrValue !== undefined) {
+                            var newParam = key + "=" + attrValue;
+                            if (baseWithSearch.indexOf('?') > 0) {
+                                var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
+                                if (oldParamReg.test(baseWithSearch)) {
+                                    baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
+                                } else {
+                                    baseWithSearch += "&" + newParam;
+                                }
+                            } else {
+                                baseWithSearch += "?" + newParam;
+                            }
+                        }
+                    }
+
+                    if (hash) {
+                        url = baseWithSearch + '#' + hash;
+                    } else {
+                        url = baseWithSearch;
+                    }
+                }
+                return url;
+            },
+            getUrlParams: function () {
+                var pairs = location.search.substring(1).split('&');
+                for (var i = 0; i < pairs.length; i++) {
+                    var pos = pairs[i].indexOf('=');
+                    if (pos === -1) {
+                        continue;
+                    }
+                    GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
+                }
+            },
+            doRedirect: function () {
+                var code = GWC.urlParams['code'];
+                var appId = GWC.urlParams['appid'];
+                var scope = GWC.urlParams['scope'] || 'snsapi_base';
+                var state = GWC.urlParams['state'];
+                var isMp = GWC.urlParams['isMp']; //isMp为true时使用开放平台作授权登录,false为网页扫码登录
+                var baseUrl;
+                var redirectUri;
+
+                if (!code) {
+                    baseUrl = "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect";
+                    if (scope == 'snsapi_login' && !isMp) {
+                        baseUrl = "https://open.weixin.qq.com/connect/qrconnect";
+                    }
+                    //第一步,没有拿到code,跳转至微信授权页面获取code
+                    redirectUri = GWC.appendParams(baseUrl, {
+                        'appid': appId,
+                        'redirect_uri': encodeURIComponent(location.href),
+                        'response_type': 'code',
+                        'scope': scope,
+                        'state': state,
+                    });
+                } else {
+                    //第二步,从微信授权页面跳转回来,已经获取到了code,再次跳转到实际所需页面
+                    redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
+                        'code': code,
+                        'state': state
+                    });
+                }
+
+                location.href = redirectUri;
+            }
+        };
+
+        GWC.getUrlParams();
+        GWC.doRedirect();
+    </script>
+</body>
+
+</html>

+ 23 - 0
public/index.html

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+	<meta charset="UTF-8">
+	<title>Usestudio wechat pay</title>
+	<script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
+</head>
+
+<body>
+	<h1>Hello there!</h1>
+	<div id="login_container"></div>
+</body>
+<script>
+	let obj = new WxLogin({
+		id: "login_container",
+		appid: "wxb558057b16b95dd5",
+		scope: "wxapi.1473.cn",
+		redirect_uri: "http://wxapi.1473.cn/wechatpay",
+	});
+</script>
+
+</html>

+ 8 - 0
public/stylesheets/style.css

@@ -0,0 +1,8 @@
+body {
+  padding: 50px;
+  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+  color: #00B7FF;
+}

+ 9 - 0
routes/index.js

@@ -0,0 +1,9 @@
+var express = require('express');
+var router = express.Router();
+
+/* GET home page. */
+router.get('/', function(req, res, next) {
+  res.render('index', { title: 'Express' });
+});
+
+module.exports = router;

+ 9 - 0
routes/users.js

@@ -0,0 +1,9 @@
+var express = require('express');
+var router = express.Router();
+
+/* GET users listing. */
+router.get('/', function(req, res, next) {
+  res.send('respond with a resource');
+});
+
+module.exports = router;

+ 38 - 0
routes/wechatpay.js

@@ -0,0 +1,38 @@
+const express = require('express');
+const router = express.Router();
+const OAuth = require('wechat-oauth');
+const tenpay = require('tenpay');
+
+const config = {
+    appid: 'wxb558057b16b95dd5',
+    mchid: '1391101602',
+    partnerKey: 'WLfN6xZt60JL2Pj1HFb72VA48IPofN3n'
+};
+
+const wechatApi = new tenpay(config);
+
+// const oauthClient = new OAuth('wxb558057b16b95dd5', '46237a17fbc57f5ebc5cc93c2b8cc505');
+const oauthClient = new OAuth('wxae62986df7490c49', '68dfe0d7c768d650847b307700db04ef');
+
+/* router.get('/', async function (req, res, next) {
+    let result = await api.micropay({
+        out_trade_no: `testOrder${new Date().getTime()}`,
+        body: 'Test order',
+        total_fee: 100,
+        auth_code: '46237a17fbc57f5ebc5cc93c2b8cc505'
+    });
+
+    console.log(result);
+}); */
+
+router.get('/', (req, res, next) => {
+    let url = oauthClient.getAuthorizeURLForWebsite('http://www.1473.cn/wechatpay');
+    console.log(url);
+    res.send(url);
+})
+
+router.get('/wechatpay', (res, req, next) => {
+    console.log(res);
+})
+
+module.exports = router;

+ 6 - 0
views/error.jade

@@ -0,0 +1,6 @@
+extends layout
+
+block content
+  h1= message
+  h2= error.status
+  pre #{error.stack}

+ 5 - 0
views/index.jade

@@ -0,0 +1,5 @@
+extends layout
+
+block content
+  h1= title
+  p Welcome to #{title}

+ 7 - 0
views/layout.jade

@@ -0,0 +1,7 @@
+doctype html
+html
+  head
+    title= title
+    link(rel='stylesheet', href='/stylesheets/style.css')
+  body
+    block content

Разница между файлами не показана из-за своего большого размера
+ 1116 - 0
yarn-error.log


Разница между файлами не показана из-за своего большого размера
+ 1063 - 0
yarn.lock