├── controllers ├── home.js ├── alipay.js └── wxpay.js ├── views ├── layouts │ └── default.hbs ├── home │ └── index.hbs ├── alipay │ ├── refund │ │ └── index.hbs │ ├── web │ │ └── index.hbs │ └── wap │ │ └── index.hbs └── wxpay │ ├── web │ └── index.hbs │ └── wap │ ├── index.hbs │ └── pay.hbs ├── routers ├── home.js ├── index.js ├── wxpay.js └── alipay.js ├── proxy ├── WXPay.js └── ALIPay.js ├── README.md ├── jsconfig.json ├── typings.json ├── test ├── kits │ └── wxpay_kit.js └── utils │ └── sign.js ├── configs ├── alipay.js ├── wxpay.js └── user.example.js ├── .eslintrc ├── utils ├── logger.js ├── request.js └── sign.js ├── package.json ├── app.js ├── .gitignore └── kits ├── alipay_kit.js ├── wxpay_kit.js └── hbs_kit.js /controllers/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 跳转至支付导航 5 | */ 6 | exports.toIndex = function(req, res) { 7 | res.render('home/index'); 8 | }; -------------------------------------------------------------------------------- /views/layouts/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{block "title"}} 7 | 8 | 9 | 10 | {{{body}}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /routers/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const homeCtrl = require('../controllers/home'); 7 | 8 | router.get('/', homeCtrl.toIndex); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /routers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | router.use('/', require('./home')); 7 | router.use('/wxpay', require('./wxpay')); 8 | router.use('/alipay', require('./alipay')); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /views/home/index.hbs: -------------------------------------------------------------------------------- 1 |

支付导航

2 |

3 | 支付宝web支付(使用pc浏览器打开) 4 |

5 |

6 | 支付宝wap支付(使用手机浏览器打开) 7 |

8 |

9 | 微信扫码支付(使用pc浏览器打开) 10 |

11 |

12 | 微信公众号支付(使用手机微信打开) 13 |

-------------------------------------------------------------------------------- /proxy/WXPay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const wxpayKit = require('../kits/wxpay_kit'); 4 | const wxpayCfg = require('../configs/wxpay'); 5 | 6 | /** 7 | * 统一下单 8 | * @param {} orderInfo 订单信息 9 | */ 10 | exports.createOrder = function(orderInfo) { 11 | return wxpayKit.request(wxpayCfg.api_url.unifiedorder, orderInfo); 12 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于node的第三方支付实现 2 | 3 | # 概述 4 | 5 | 支付在互联网产品中的应用随处可见,闲暇之余,决定使用node实现常见的三方支付,此项目包含以下几种支付方式: 6 | 7 | - 支付宝即时到帐 8 | 9 | - 支付宝手机网站支付 10 | 11 | - 微信扫码支付 12 | 13 | - 微信公众号支付 14 | 15 | 16 | # 支付宝即时到帐 17 | 18 | 主要用于pc端网页应用支付 19 | 20 | # 支付宝手机网站支付 21 | 22 | 主要用于wap端网页应用支付 23 | 24 | # 微信扫码支付 25 | 26 | 主要用于pc端网页应用支付 27 | 28 | # 微信公众号支付 29 | 30 | 主要用于微信公众号内部支付 -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "bower_components", 12 | "jspm_packages", 13 | "tmp", 14 | "temp" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /routers/wxpay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const wxpayCtrl = require('../controllers/wxpay'); 7 | 8 | router.get('/wap', wxpayCtrl.toWapSubmit); 9 | router.post('/wap', wxpayCtrl.wapSubmit); 10 | 11 | router.get('/web', wxpayCtrl.toWebSubmit); 12 | router.post('/web', wxpayCtrl.webSubmit); 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part3-pay", 3 | "dependencies": {}, 4 | "globalDependencies": { 5 | "bluebird": "registry:dt/bluebird#2.0.0+20160701023356", 6 | "express": "registry:dt/express#4.0.0+20160317120654", 7 | "express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160701212727", 8 | "node": "registry:dt/node#6.0.0+20160621231320", 9 | "serve-static": "registry:dt/serve-static#0.0.0+20160606155157" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /views/alipay/refund/index.hbs: -------------------------------------------------------------------------------- 1 | {{#extend "title"}} 支付宝退款 {{/extend}} 2 | 3 |
4 |
5 | 批次号: 6 |
7 |
8 | 批次数: 9 |
10 |
11 | 单笔数据集: 12 |
13 |
14 | 15 |
16 |
-------------------------------------------------------------------------------- /test/kits/wxpay_kit.js: -------------------------------------------------------------------------------- 1 | const wxpayKit = require('../../kits/wxpay_kit'); 2 | const wxpayCfg = require('../../configs/wxpay'); 3 | 4 | import test from 'ava'; 5 | 6 | let order = { 7 | 'goods_detail': [{ 8 | 'goods_id': 'iphone6s_16G', 9 | 'wxpay_goods_id': '1001', 10 | 'goods_name': 'iPhone6s 16G', 11 | 'goods_num': 1, 12 | 'price': 1, 13 | 'goods_category': '123456', 14 | 'body': '苹果手机' 15 | }] 16 | }; 17 | 18 | test('wxpay pay request', t => { 19 | t.pass(); 20 | }); -------------------------------------------------------------------------------- /views/alipay/web/index.hbs: -------------------------------------------------------------------------------- 1 | {{#extend "title"}} 2 | 支付宝pc端付款 3 | {{/extend}} 4 | 5 |
6 |
7 | 订单号: 8 |
9 |
10 | 商品名称: 11 |
12 |
13 | 商品描述: 14 |
15 |
16 | 总金额: 元 17 |
18 |
19 | 20 |
21 |
-------------------------------------------------------------------------------- /views/wxpay/web/index.hbs: -------------------------------------------------------------------------------- 1 | {{#extend "title"}} 微信订单提交 {{/extend}} 2 | 3 |
4 |
5 | 订单号: 6 |
7 |
8 | 商品描述: 9 |
10 |
11 | 总金额: 分 12 |
13 |
14 | 用户ip: 15 |
16 |
17 | 18 |
19 |
-------------------------------------------------------------------------------- /configs/alipay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 支付宝相关配置 5 | */ 6 | module.exports = { 7 | /** 8 | * 支付宝网关 9 | */ 10 | gateway: 'https://mapi.alipay.com/gateway.do', 11 | 12 | /** 13 | * 支付宝相关服务 14 | */ 15 | service: { 16 | /** 17 | * 手机网站支付下单 18 | */ 19 | wap_direct_pay: 'alipay.wap.create.direct.pay.by.user', 20 | 21 | /** 22 | * 即使到账支付下单 23 | */ 24 | direct_pay: 'create_direct_pay_by_user', 25 | 26 | /** 27 | * 退款 28 | */ 29 | refund_fastpay: 'refund_fastpay_by_platform_pwd', 30 | } 31 | }; -------------------------------------------------------------------------------- /routers/alipay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | const alipayCtrl = require('../controllers/alipay'); 7 | 8 | router.get('/web', alipayCtrl.toWebSubmit); 9 | router.post('/web', alipayCtrl.webSubmit); 10 | router.post('/web/notify', alipayCtrl.doWebNotify); 11 | 12 | router.get('/wap', alipayCtrl.toWapSubmit); 13 | router.post('/wap', alipayCtrl.wapSubmit); 14 | router.post('/wap/notify', alipayCtrl.doWapNotify); 15 | 16 | router.get('/refund', alipayCtrl.toRefundSubmit); 17 | router.post('/refund', alipayCtrl.refundSubmit); 18 | router.post('/refund/notify', alipayCtrl.doRefundNotify); 19 | 20 | module.exports = router; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 4, 16 | { "SwitchCase": 1 } 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single" 25 | ], 26 | "semi": [ 27 | "error", 28 | "always" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * 系统日志处理 4 | */ 5 | const bunyan = require('bunyan'); 6 | 7 | /** 8 | * request请求打印格式 9 | */ 10 | function reqSerializer(req) { 11 | return { 12 | method: req.method, 13 | url: req.uri.href, 14 | headers: req.headers, 15 | body: req.body 16 | }; 17 | } 18 | 19 | let logger = bunyan.createLogger({ 20 | name: 'hiteacher-wechat', 21 | streams: [{ 22 | level: 'info', 23 | stream: process.stdout 24 | }, { 25 | level: 'error', 26 | stream: process.stdout 27 | }], 28 | serializers: { 29 | req: reqSerializer 30 | } 31 | }); 32 | 33 | module.exports = logger; 34 | -------------------------------------------------------------------------------- /views/wxpay/wap/index.hbs: -------------------------------------------------------------------------------- 1 | {{#extend "title"}} 微信订单提交 {{/extend}} 2 | 3 |
4 |
5 | 订单号: 6 |
7 |
8 | openid: 9 |
10 |
11 | 商品描述: 12 |
13 |
14 | 总金额: 分 15 |
16 |
17 | 用户ip: 18 |
19 |
20 | 21 |
22 |
-------------------------------------------------------------------------------- /test/utils/sign.js: -------------------------------------------------------------------------------- 1 | const sign = require('../../utils/sign'); 2 | const userCfg = require('../../configs/user'); 3 | import test from 'ava'; 4 | 5 | let data = { 6 | sign: '42284ee5b6363629a639813dc7a8ecdb', 7 | result_details: '2016070821001004700239503693^0.02^REFUND_TRADE_FEE_ERROR', 8 | notify_time: '2016-07-10 18:38:30', 9 | sign_type: 'MD5', 10 | notify_type: 'batch_refund_notify', 11 | notify_id: 'a969f3515f286c40fbd0b7599b0e0c7lms', 12 | batch_no: '201607101468147101397', 13 | success_num: '0' 14 | }; 15 | 16 | test('alipay sign verify', t => { 17 | t.true(sign.verify(data, userCfg.alipay.partner_key, data.sign)); 18 | t.false(sign.verify(data, userCfg.alipay.partner_key, '123123')); 19 | }); -------------------------------------------------------------------------------- /views/alipay/wap/index.hbs: -------------------------------------------------------------------------------- 1 | {{#extend "title"}} 微信订单提交 {{/extend}} 2 | 3 |
4 |
5 | 订单号: 6 |
7 |
8 | 商品名称: 9 |
10 |
11 | 商品描述: 12 |
13 |
14 | 商品展示超链接: 15 |
16 |
17 | 总金额: 元 18 |
19 |
20 | 21 |
22 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part3-pay", 3 | "version": "1.0.0", 4 | "description": "third party pay base node", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "ava" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tinybee/part3-pay.git" 12 | }, 13 | "keywords": [ 14 | "pay" 15 | ], 16 | "author": "Jaylin Wang", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/tinybee/part3-pay/issues" 20 | }, 21 | "homepage": "https://github.com/tinybee/part3-pay#readme", 22 | "dependencies": { 23 | "bluebird": "^3.4.1", 24 | "body-parser": "^1.15.2", 25 | "bunyan": "^1.8.1", 26 | "dateformat": "^1.0.12", 27 | "express": "^4.14.0", 28 | "hbs": "^4.0.0", 29 | "node-qrcode": "0.0.4", 30 | "qrcode": "^0.4.2", 31 | "requestretry": "^1.9.0", 32 | "uuid": "^2.0.2", 33 | "wechat-oauth": "^1.1.1", 34 | "xml2json": "^0.9.1" 35 | }, 36 | "devDependencies": { 37 | "ava": "^0.15.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const hbs = require('hbs'); 6 | const logger = require('./utils/logger'); 7 | const hbsKit = require('./kits/hbs_kit'); 8 | 9 | const app = express(); 10 | 11 | //# app views setting 12 | // 13 | app.set('views', __dirname + '/views'); 14 | app.set('view engine', 'hbs'); 15 | app.engine('hbs', hbs.__express); 16 | 17 | //# hbs setting 18 | // 19 | hbs.registerPartials(__dirname + '/views'); 20 | hbs.localsAsTemplateData(app); 21 | hbsKit.registerHelper(hbs); 22 | app.set('view options', { 23 | layout: 'layouts/default' 24 | }); 25 | 26 | //# static setting 27 | // 28 | app.use(express.static(__dirname + '/public')); 29 | 30 | //# body parser setting 31 | // 32 | app.use(bodyParser.json()); 33 | app.use(bodyParser.urlencoded({ 34 | extended: false 35 | })); 36 | 37 | //# router setting 38 | // 39 | app.use(require('./routers')); 40 | 41 | app.listen('4000', function() { 42 | logger.info('app start 4000'); 43 | }); 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | public/build 28 | public/publish 29 | public/temp/upload_* 30 | public/temp/avatar_* 31 | 32 | # Dependency directory 33 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 34 | node_modules 35 | ### Example user template template 36 | ### Example user template 37 | 38 | # IntelliJ project files 39 | .idea 40 | *.iml 41 | out 42 | gen 43 | 44 | .DS_Store 45 | typings 46 | 47 | .vscode 48 | 49 | # 支付账号信息 50 | # 51 | configs/user.js 52 | 53 | # for emacs 54 | *~ 55 | [#]*[#] -------------------------------------------------------------------------------- /kits/alipay_kit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('../utils/logger'); 4 | const sign = require('../utils/sign'); 5 | const Promise = require('bluebird'); 6 | 7 | const userCfg = require('../configs/user'); 8 | const alipayCfg = require('../configs/alipay'); 9 | 10 | /** 11 | * 构建请求表单 12 | * @param {} data 13 | */ 14 | exports.buildRequestForm = function(data) { 15 | let _data = Object.assign({ 16 | partner: userCfg.alipay.partner, 17 | _input_charset: 'utf-8', 18 | sign_type: 'MD5' 19 | }, data || {}); 20 | _data.sign = sign.md5(_data, userCfg.alipay.partner_key); 21 | 22 | let sbHtml = `
`; 23 | for (let key in _data) { 24 | sbHtml += ``; 25 | } 26 | sbHtml += '
'; 27 | sbHtml += ''; 28 | 29 | logger.info(sbHtml); 30 | return Promise.resolve(sbHtml); 31 | }; -------------------------------------------------------------------------------- /utils/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * request请求的封装 4 | */ 5 | const request = require('requestretry'); 6 | const logger = require('./logger'); 7 | const Promise = require('bluebird'); 8 | 9 | /** 10 | * request请求 11 | * @param {Object} options 请求参数 12 | */ 13 | exports.request = function(options) { 14 | let _options = { 15 | gzip: true, 16 | encoding: 'utf8', 17 | headers: { 18 | 'User-Agent': 'request from node' 19 | }, 20 | maxAttempts: 3, //最大尝试次数 21 | retryDelay: 3000, // 每次重试间隔时间 22 | fullResponse: true, 23 | retryStrategy: request.RetryStrategies.HTTPOrNetworkError 24 | }; 25 | Object.assign(_options, options); 26 | let promise = new Promise((resolve, reject) => { 27 | let req = request(_options, (err, response) => { 28 | if (err) { 29 | reject(err); 30 | } else { 31 | resolve(response); 32 | } 33 | }); 34 | logger.info({ 35 | req: req._req 36 | }, 'send a request :)'); 37 | }); 38 | return promise; 39 | }; -------------------------------------------------------------------------------- /configs/wxpay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * 微信接口相关配置 4 | */ 5 | let basic_url = 'https://api.mch.weixin.qq.com'; 6 | 7 | module.exports = { 8 | /** 9 | * api请求地址 10 | */ 11 | api_url: { 12 | /** 13 | * 统一下单接口 14 | */ 15 | unifiedorder: basic_url + '/pay/unifiedorder', 16 | 17 | /** 18 | * 订单查询接口 19 | */ 20 | orderquery: basic_url + '/pay/orderquery', 21 | 22 | /** 23 | * 订单关闭接口 24 | */ 25 | orderclose: basic_url + '/pay/closeorder', 26 | 27 | /** 28 | * 申请退款接口 29 | */ 30 | refund: basic_url + '/secapi/pay/refund', 31 | 32 | /** 33 | * 查询退款接口 34 | */ 35 | refundquery: basic_url + '/pay/refundquery', 36 | 37 | /** 38 | * 下载对账单 39 | */ 40 | downloadbill: basic_url + '/pay/downloadbill', 41 | 42 | /** 43 | * 交易保障 44 | */ 45 | report: basic_url + '/payitil/report' 46 | }, 47 | trade_type: { 48 | /** 49 | * 公众号支付 50 | */ 51 | js_api: 'JSAPI', 52 | 53 | /** 54 | * 原生扫码支付 55 | */ 56 | native: 'NATIVE', 57 | 58 | /** 59 | * app支付 60 | */ 61 | app: 'APP' 62 | } 63 | }; -------------------------------------------------------------------------------- /configs/user.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * 用户相关配置 4 | */ 5 | 6 | module.exports = { 7 | /** 8 | * 用户微信账号相关配置 9 | */ 10 | wxpay: { 11 | /** 12 | * 微信分配的公众账号ID 13 | */ 14 | appid: '', 15 | 16 | /** 17 | * 微信密钥 18 | */ 19 | app_secret: '', 20 | 21 | /** 22 | * 微信支付分配的商户号 23 | */ 24 | mch_id: '', 25 | 26 | /** 27 | * 支付api加密key 28 | */ 29 | mch_key: '', 30 | 31 | /** 32 | * 微信回调地址 33 | */ 34 | notify_url: '' 35 | }, 36 | 37 | /** 38 | * 用户支付宝账号相关配置 39 | */ 40 | alipay: { 41 | /** 42 | * 支付宝分配的商户号 43 | */ 44 | partner: '', 45 | 46 | /** 47 | * 支付宝md5 48 | */ 49 | partner_key: '', 50 | 51 | /** 52 | * 支付宝卖家账号 53 | */ 54 | seller_id: '', 55 | 56 | /** 57 | * 服务器异步通知页面路径(wap支付) 58 | */ 59 | wap_notify_url: '', 60 | 61 | /** 62 | * 服务器异步通知页面路径(pc支付) 63 | */ 64 | web_notify_url: '', 65 | 66 | /** 67 | * 页面跳转同步通知页面路径(支付) 68 | */ 69 | pay_return_url: '', 70 | 71 | /** 72 | * 服务器异步通知页面路径(退款) 73 | */ 74 | refund_notify_url: '' 75 | } 76 | }; -------------------------------------------------------------------------------- /proxy/ALIPay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const alipayKit = require('../kits/alipay_kit'); 4 | const alipayCfg = require('../configs/alipay'); 5 | const userCfg = require('../configs/user'); 6 | const dateformat = require('dateformat'); 7 | 8 | /** 9 | * Web端下单 10 | * @param {} orderInfo 订单信息 11 | */ 12 | exports.submitOrderFromWeb = function(orderInfo) { 13 | let _orderInfo = Object.assign({ 14 | service: alipayCfg.service.direct_pay, 15 | payment_type: 1, 16 | seller_id: userCfg.alipay.seller_id, 17 | notify_url: userCfg.alipay.web_notify_url 18 | }, orderInfo); 19 | return alipayKit.buildRequestForm(_orderInfo); 20 | }; 21 | 22 | /** 23 | * wap端下单 24 | * @param {} orderInfo 订单信息 25 | */ 26 | exports.submitOrderFromWap = function(orderInfo) { 27 | let _orderInfo = Object.assign({ 28 | service: alipayCfg.service.wap_direct_pay, 29 | payment_type: 1, 30 | seller_id: userCfg.alipay.seller_id, 31 | notify_url: userCfg.alipay.wap_notify_url 32 | }, orderInfo); 33 | return alipayKit.buildRequestForm(_orderInfo); 34 | }; 35 | 36 | /** 37 | *支付宝退款 38 | */ 39 | exports.refundFastpay = function(refundInfo) { 40 | let _refundInfo = Object.assign({ 41 | service: alipayCfg.service.refund_fastpay, 42 | seller_user_id: userCfg.alipay.seller_id, 43 | refund_date: dateformat(new Date(), 'yyyy-mm-dd HH:MM:ss'), 44 | notify_url: userCfg.alipay.refund_notify_url, 45 | }, refundInfo); 46 | return alipayKit.buildRequestForm(_refundInfo); 47 | }; -------------------------------------------------------------------------------- /kits/wxpay_kit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uuid = require('uuid'); 4 | const crypto = require('crypto'); 5 | const parser = require('xml2json'); 6 | 7 | const logger = require('../utils/logger'); 8 | const sign = require('../utils/sign'); 9 | const requestUtil = require('../utils/request'); 10 | 11 | const userCfg = require('../configs/user'); 12 | 13 | /** 14 | * 请求api 15 | * @param {} url 请求地址 16 | * @param {} data 请求数据 17 | */ 18 | exports.request = function(url, data) { 19 | let _data = Object.assign({ 20 | appid: userCfg.wxpay.appid, 21 | mch_id: userCfg.wxpay.mch_id, 22 | nonce_str: uuid.v1().replace(/-/g, ''), 23 | notify_url: userCfg.wxpay.notify_url || '#' 24 | }, data || {}); 25 | _data.sign = sign.md5(_data, `&key=${userCfg.wxpay.mch_key}`); 26 | 27 | // let xml = parse.toXml(_data); 28 | // logger.error(xml); 29 | let jsonObj = { 30 | xml: {} 31 | }; 32 | 33 | for (let key in _data) { 34 | jsonObj.xml[key] = { 35 | '$t': _data[key] 36 | }; 37 | } 38 | logger.info(parser.toXml(jsonObj)); 39 | let _options = Object.assign({}, { 40 | url: url, 41 | form: parser.toXml(jsonObj), 42 | method: 'POST' 43 | }); 44 | 45 | let beginDate = new Date(); 46 | return requestUtil.request(_options).then(function(res) { 47 | let time = new Date() - beginDate; 48 | logger.info(`request <${url}> cost ${time}`); 49 | return JSON.parse(parser.toJson(res.body)).xml; 50 | }).catch(function(error) { 51 | logger.error({ 52 | error: error 53 | }); 54 | }); 55 | }; -------------------------------------------------------------------------------- /utils/sign.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | /** 6 | * 参数过滤 7 | * @param {} params 8 | */ 9 | let filter = function(params) { 10 | let _params = {}; 11 | for (let key in params) { 12 | if (!params[key] || key.toLocaleLowerCase() == 'sign' || key.toLocaleLowerCase() == 'sign_type') { 13 | continue; 14 | } else { 15 | _params[key] = params[key]; 16 | } 17 | } 18 | return _params; 19 | }; 20 | 21 | /** 22 | * 拼接签名字符串 23 | * @param {} params 24 | */ 25 | let createLinkString = function(params) { 26 | var linkStr = ''; 27 | for (var key in params) { 28 | linkStr += `${key}=${params[key]}&`; 29 | } 30 | linkStr = linkStr.substring(0, linkStr.length - 1); 31 | return linkStr; 32 | }; 33 | 34 | /** 35 | * 字典序排序 36 | * @param {} params 37 | */ 38 | let dictSort = function(params) { 39 | let _params = {}; 40 | let sortKey = Object.keys(params).sort(); 41 | for (let i = 0; i < sortKey.length; i++) { 42 | _params[sortKey[i]] = params[sortKey[i]]; 43 | } 44 | return _params; 45 | }; 46 | 47 | /** 48 | * md5签名 49 | * @param {} params 签名参数 50 | */ 51 | let md5 = function(params, key) { 52 | let _params = filter(params); 53 | let signStr = createLinkString(dictSort(_params)); 54 | signStr += key; 55 | return crypto.createHash('md5').update(signStr, 'utf8').digest('hex'); 56 | }; 57 | 58 | /** 59 | * md5签名 60 | * @param {} params 签名参数 61 | */ 62 | exports.md5 = md5; 63 | 64 | /** 65 | * 验证签名 66 | */ 67 | exports.verify = function(params, key, sign) { 68 | let mySign = md5(params, key); 69 | return mySign === sign; 70 | }; -------------------------------------------------------------------------------- /views/wxpay/wap/pay.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /kits/hbs_kit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.registerHelper = function(hbs) { 4 | let blocks = {}; 5 | hbs.registerHelper('extend', function(name, context) { 6 | let block = blocks[name]; 7 | if (!block) { 8 | block = blocks[name] = []; 9 | } 10 | 11 | block.push(context.fn(this)); 12 | }); 13 | 14 | hbs.registerHelper('block', function(name) { 15 | let val = (blocks[name] || []).join('\n'); 16 | blocks[name] = []; 17 | return val; 18 | }); 19 | 20 | //# 等于 21 | // 22 | hbs.registerHelper('equal', function(args1, args2, context) { 23 | if (args1 === args2) { 24 | //满足添加继续执行 25 | return context.fn(this); 26 | } else { 27 | if (typeof(args1) === 'number' && args1.toString() === args2.toString()) { 28 | return context.fn(this); 29 | } 30 | //不满足条件执行{{else}}部分 31 | return context.inverse(this); 32 | } 33 | }); 34 | 35 | //# 大于等于 36 | // 37 | hbs.registerHelper('egt', function(args1, args2, context) { 38 | if (args1 >= args2) { 39 | return context.fn(this); 40 | } else { 41 | return context.inverse(this); 42 | } 43 | 44 | }); 45 | 46 | //# 大于 47 | // 48 | hbs.registerHelper('gt', function(args1, args2, context) { 49 | if (args1 > args2) { 50 | return context.fn(this); 51 | } else { 52 | return context.inverse(this); 53 | } 54 | 55 | }); 56 | 57 | //# 小于等于 58 | // 59 | hbs.registerHelper('elt', function(args1, args2, context) { 60 | if (args1 <= args2) { 61 | return context.fn(this); 62 | } else { 63 | return context.inverse(this); 64 | } 65 | 66 | }); 67 | 68 | //# 小于 69 | // 70 | hbs.registerHelper('lt', function(args1, args2, context) { 71 | if (args1 < args2) { 72 | return context.fn(this); 73 | } else { 74 | return context.inverse(this); 75 | } 76 | 77 | }); 78 | 79 | //# count 80 | // 配合each实现遍历N次 81 | hbs.registerHelper('count', function(args1, context) { 82 | let array = []; 83 | for (let i = 1; i <= args1; i++) { 84 | array.push(i); 85 | } 86 | return context.fn(array); 87 | }); 88 | 89 | //# 加法 90 | // 91 | hbs.registerHelper('add', function(args1, args2) { 92 | return args1 + args2; 93 | }); 94 | 95 | //# 减法 96 | hbs.registerHelper('sub', function(args1, args2) { 97 | return args1 - args2; 98 | }); 99 | 100 | hbs.registerHelper('sub', function(args1, args2) { 101 | return args1 - args2; 102 | }); 103 | }; -------------------------------------------------------------------------------- /controllers/alipay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ALIpay = require('../proxy/ALIPay'); 4 | const dateformat = require('dateformat'); 5 | const userCfg = require('../configs/user'); 6 | const sign = require('../utils/sign'); 7 | const logger = require('../utils/logger'); 8 | 9 | /** 10 | * web下单首页 11 | */ 12 | exports.toWebSubmit = function(req, res) { 13 | let orderNo = new Date().getTime(); 14 | res.render('alipay/web/index', { 15 | orderNo: orderNo 16 | }); 17 | }; 18 | 19 | /** 20 | * web提交订单数据 21 | */ 22 | exports.webSubmit = function(req, res) { 23 | let orderInfo = req.body; 24 | ALIpay.submitOrderFromWeb(orderInfo).then(function(html) { 25 | res.send(html); 26 | }); 27 | }; 28 | 29 | /** 30 | * 跳转至支付宝退款 31 | */ 32 | exports.toRefundSubmit = function(req, res) { 33 | let batchNo = dateformat(new Date(), 'yyyymmdd') + new Date().getTime(); 34 | res.render('alipay/refund/index', { 35 | batchNo: batchNo 36 | }); 37 | }; 38 | 39 | /** 40 | * 跳转至支付宝退款 41 | */ 42 | exports.refundSubmit = function(req, res) { 43 | let refundInfo = req.body; 44 | ALIpay.refundFastpay(refundInfo).then(function(html) { 45 | res.send(html); 46 | }); 47 | }; 48 | 49 | 50 | /** 51 | * wap下单首页 52 | */ 53 | exports.toWapSubmit = function(req, res) { 54 | let orderNo = new Date().getTime(); 55 | res.render('alipay/wap/index', { 56 | orderNo: orderNo 57 | }); 58 | }; 59 | 60 | /** 61 | * wap提交订单数据 62 | */ 63 | exports.wapSubmit = function(req, res) { 64 | let orderInfo = req.body; 65 | ALIpay.submitOrderFromWap(orderInfo).then(function(html) { 66 | res.send(html); 67 | }); 68 | }; 69 | 70 | /** 71 | * 响应wap端回调信息 72 | */ 73 | exports.doWapNotify = function(req) { 74 | let notifyData = req.body; 75 | let result = {}; 76 | if (sign.verify(notifyData, userCfg.alipay.partner_key, notifyData.sign)) { 77 | result = notifyData; 78 | logger.info({ 79 | refundResult: result 80 | }, 'wap支付结果'); 81 | } else { 82 | logger.error('验证签名错误'); 83 | } 84 | }; 85 | 86 | /** 87 | * 响应web端回调信息 88 | */ 89 | exports.doWebNotify = function(req) { 90 | let notifyData = req.body; 91 | let result = {}; 92 | if (sign.verify(notifyData, userCfg.alipay.partner_key, notifyData.sign)) { 93 | result = notifyData; 94 | logger.info({ 95 | refundResult: result 96 | }, 'web支付结'); 97 | } else { 98 | logger.error('验证签名错误'); 99 | } 100 | }; 101 | 102 | /** 103 | * 退款回调 104 | */ 105 | exports.doRefundNotify = function(req) { 106 | let notifyData = req.body; 107 | let result = {}; 108 | if (sign.verify(notifyData, userCfg.alipay.partner_key, notifyData.sign)) { 109 | result = notifyData; 110 | logger.info({ 111 | refundResult: result 112 | }, '退款结果'); 113 | } else { 114 | logger.error('验证签名错误'); 115 | } 116 | }; -------------------------------------------------------------------------------- /controllers/wxpay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('../utils/logger'); 4 | const WXPay = require('../proxy/WXPay'); 5 | const OAuth = require('wechat-oauth'); 6 | const QRCode = require('qrcode'); 7 | const sign = require('../utils/sign'); 8 | 9 | const wxpayCfg = require('../configs/wxpay'); 10 | const userCfg = require('../configs/user'); 11 | const client = new OAuth(userCfg.wxpay.appid, userCfg.wxpay.app_secret); 12 | 13 | /** 14 | * 跳转至创建订单页面 15 | */ 16 | exports.toWapSubmit = function(req, res) { 17 | // let orderNo = new Date().getTime(); 18 | // res.render('wxpay/wap/index', { 19 | // orderNo: orderNo, 20 | // openid: 'oWEeAwq75pCS4OqTsN2LEeDB3NE8' 21 | // }); 22 | 23 | let originalUrl = req.originalUrl; 24 | let code = req.query.code; 25 | console.log(code); 26 | if (!code) { 27 | res.redirect(client.getAuthorizeURL('http://dev.jx-cloud.cc' + originalUrl, 'test', 'snsapi_base')); 28 | } else { 29 | let orderNo = new Date().getTime(); 30 | client.getAccessToken(code, function(err, result) { 31 | let openid = result.data.openid; 32 | if (!err) { 33 | res.render('wxpay/wap/index', { 34 | orderNo: orderNo, 35 | openid: openid 36 | }); 37 | } else { 38 | res.send('error'); 39 | } 40 | }); 41 | } 42 | }; 43 | 44 | /** 45 | * 提交订单 46 | */ 47 | exports.wapSubmit = function(req, res) { 48 | let orderInfo = req.body; 49 | orderInfo.trade_type = wxpayCfg.trade_type.js_api; 50 | WXPay.createOrder(orderInfo).then(function(body) { 51 | logger.info({ 52 | body: body 53 | }, 'unifiedorder result'); 54 | // 拼接支付对象 55 | // "appId" : "wx2421b1c4370ec43b", //公众号名称,由商户传入 56 | // "timeStamp":" 1395712654", //时间戳,自1970年以来的秒数 57 | // "nonceStr" : "e61463f8efa94090b1f366cccfbbb444", //随机串 58 | // "package" : "prepay_id=u802345jgfjsdfgsdg888", 59 | // "signType" : "MD5", //微信签名方式: 60 | // "paySign" : "70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名 61 | let payData = { 62 | appId: body.appid, 63 | timeStamp: new Date().getMilliseconds(), 64 | nonceStr: body.nonce_str, 65 | signType: 'MD5', 66 | package: `prepay_id=${body.prepay_id}` 67 | }; 68 | payData.paySign = sign.md5(payData, `&key=${userCfg.wxpay.mch_key}`); 69 | res.render('wxpay/wap/pay', { 70 | payData: payData 71 | }); 72 | }); 73 | }; 74 | 75 | /** 76 | * web下单页 77 | */ 78 | exports.toWebSubmit = function(req, res) { 79 | let orderNo = new Date().getTime(); 80 | res.render('wxpay/web/index', { 81 | orderNo: orderNo 82 | }); 83 | }; 84 | 85 | /** 86 | * web下单 87 | */ 88 | exports.webSubmit = function(req, res) { 89 | let orderInfo = req.body; 90 | orderInfo.trade_type = wxpayCfg.trade_type.native; 91 | WXPay.createOrder(orderInfo).then(function(body) { 92 | logger.info({ 93 | body: body 94 | }, 'unifiedorder result'); 95 | if (body.return_code == 'SUCCESS') { 96 | QRCode.draw(body.code_url, {}, function(err, canvas) { 97 | if (!err) { 98 | let ctx = canvas.getContext('2d'); 99 | ctx.height = 3000; 100 | ctx.width = 3000; 101 | let url = canvas.toDataURL('image/svg'); 102 | res.send(``); 103 | } 104 | }); 105 | } 106 | 107 | }); 108 | }; --------------------------------------------------------------------------------