├── nodemon.json ├── .leanignore ├── .gitignore ├── public └── stylesheets │ └── style.css ├── views ├── error.ejs ├── index.ejs └── todos.ejs ├── server-cluster.js ├── utils.js ├── wxpay.js ├── server.js ├── package.json ├── routes └── weixin.js ├── cloud.js ├── app.js ├── order.js └── README.md /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["views/*", "public/*"] 3 | } 4 | -------------------------------------------------------------------------------- /.leanignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .avoscloud/ 3 | .leancloud/ 4 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | start.sh 4 | .avoscloud 5 | .leancloud 6 | 7 | # VIM 8 | *~ 9 | *.swp 10 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | a { 6 | color: #00b7ff; 7 | } 8 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 5 | 6 | 7 | 8 |

<%= message %>

9 |

<%= error.status %>

10 |
<%= error.stack %>
11 | 12 | 13 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LeanEngine 5 | 6 | 7 | 8 |

LeanEngine

9 |

这是 LeanEngine 的示例应用

10 |

当前时间:<%= currentTime %>

11 |

一个简单的「TODO 列表」示例

12 | 13 | 14 | -------------------------------------------------------------------------------- /server-cluster.js: -------------------------------------------------------------------------------- 1 | var cluster = require('cluster'); 2 | 3 | // 进程数量建议设置为可用的 CPU 数量 4 | var workers = process.env.LEANCLOUD_AVAILABLE_CPUS || 1; 5 | 6 | if (cluster.isMaster) { 7 | for (var i = 0; i < workers; i++) { 8 | cluster.fork(); 9 | } 10 | 11 | cluster.on('exit', (worker, code, signal) => { 12 | console.log('worker %s died, restarting...', worker.process.pid); 13 | cluster.fork(); 14 | }); 15 | } else { 16 | require('./server.js'); 17 | } 18 | -------------------------------------------------------------------------------- /views/todos.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo 5 | 6 | 7 | 8 |

<%= title %>

9 |
10 | 11 | 12 |
13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const wxpay = require('./wxpay'); 2 | 3 | const validateSign = results => { 4 | const sign = wxpay.sign(results); 5 | if (sign !== results.sign) { 6 | const error = new Error('微信返回参数签名结果不正确'); 7 | error.code = 'INVALID_RESULT_SIGN'; 8 | throw error; 9 | }; 10 | return results; 11 | }; 12 | 13 | const handleError = results => { 14 | if (results.return_code === 'FAIL') { 15 | throw new Error(results.return_msg); 16 | } 17 | if (results.result_code !== 'SUCCESS') { 18 | const error = new Error(results.err_code_des); 19 | error.code = results.err_code; 20 | throw error; 21 | } 22 | return results; 23 | }; 24 | 25 | module.exports = { 26 | validateSign, 27 | handleError, 28 | }; -------------------------------------------------------------------------------- /wxpay.js: -------------------------------------------------------------------------------- 1 | const WXPay = require('weixin-pay'); 2 | 3 | if (!process.env.WEIXIN_APPID) throw new Error('environment variable WEIXIN_APPID missing'); 4 | if (!process.env.WEIXIN_MCHID) throw new Error('environment variable WEIXIN_MCHID missing'); 5 | if (!process.env.WEIXIN_PAY_SECRET) throw new Error('environment variable WEIXIN_PAY_SECRET missing'); 6 | if (!process.env.WEIXIN_NOTIFY_URL) throw new Error('environment variable WEIXIN_NOTIFY_URL missing'); 7 | 8 | const wxpay = WXPay({ 9 | appid: process.env.WEIXIN_APPID, 10 | mch_id: process.env.WEIXIN_MCHID, 11 | partner_key: process.env.WEIXIN_PAY_SECRET, //微信商户平台 API secret,非小程序 secret 12 | // pfx: fs.readFileSync('./wxpay_cert.p12'), //微信商户平台证书,暂不需要 13 | }); 14 | 15 | module.exports = wxpay; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var AV = require('leanengine'); 3 | 4 | AV.init({ 5 | appId: process.env.LEANCLOUD_APP_ID, 6 | appKey: process.env.LEANCLOUD_APP_KEY, 7 | masterKey: process.env.LEANCLOUD_APP_MASTER_KEY 8 | }); 9 | AV.Cloud.useMasterKey(); 10 | 11 | var app = require('./app'); 12 | 13 | // 端口一定要从环境变量 `LEANCLOUD_APP_PORT` 中获取。 14 | // LeanEngine 运行时会分配端口并赋值到该变量。 15 | var PORT = parseInt(process.env.LEANCLOUD_APP_PORT || process.env.PORT || 3000); 16 | 17 | app.listen(PORT, function (err) { 18 | console.log('Node app is running on port:', PORT); 19 | 20 | // 注册全局未捕获异常处理器 21 | process.on('uncaughtException', function(err) { 22 | console.error("Caught exception:", err.stack); 23 | }); 24 | process.on('unhandledRejection', function(reason, p) { 25 | console.error("Unhandled Rejection at: Promise ", p, " reason: ", reason.stack); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weapp-pay-getting-started", 3 | "version": "1.0.0", 4 | "description": "A sample weapp-pay app", 5 | "private": true, 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "dev": "nodemon server.js" 10 | }, 11 | "keywords": [ 12 | "LeanCloud", 13 | "LeanEngine", 14 | "Weapp", 15 | "weixin-pay" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "body-parser": "1.12.3", 20 | "connect-timeout": "^1.7.0", 21 | "cookie-parser": "^1.3.5", 22 | "debug": "^2.6.0", 23 | "ejs": "2.3.1", 24 | "express": "4.12.3", 25 | "leanengine": "git+https://github.com/leancloud/leanengine-node-sdk.git#v2", 26 | "uuid": "^3.0.1", 27 | "weixin-pay": "^1.1.7" 28 | }, 29 | "devDependencies": { 30 | "nodemon": "^1.11.0" 31 | }, 32 | "engines": { 33 | "node": "6.x" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /routes/weixin.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const AV = require('leanengine'); 3 | const Order = require('../order'); 4 | const wxpay = require('../wxpay'); 5 | const { validateSign } = require('../utils'); 6 | 7 | const format = '___-_-_ _:_:__'; 8 | const formatTime = time => 9 | new Date( 10 | time.split('') 11 | .map((value, index) => value + format[index]) 12 | .join('').replace(/_/g, '') 13 | ); 14 | 15 | // 微信支付成功通知 16 | router.post('/pay-callback', wxpay.useWXCallback((msg, req, res, next) => { 17 | // 处理商户业务逻辑 18 | validateSign(msg); 19 | const { 20 | result_code, 21 | err_code, 22 | err_code_des, 23 | out_trade_no, 24 | time_end, 25 | transaction_id, 26 | bank_type, 27 | } = msg; 28 | new AV.Query(Order).equalTo('tradeId', out_trade_no).first({ 29 | useMasterKey: true, 30 | }).then(order => { 31 | if (!order) throw new Error(`找不到订单${out_trade_no}`); 32 | if (order.status === 'SUCCESS') return; 33 | 34 | return order.save({ 35 | status: result_code, 36 | errorCode: err_code, 37 | errorCodeDes: err_code_des, 38 | paidAt: formatTime(time_end), 39 | transactionId: transaction_id, 40 | bankType: bank_type, 41 | }, { 42 | useMasterKey: true, 43 | }); 44 | }).then(() => { 45 | res.success(); 46 | }).catch(error => res.fail(error.message)); 47 | })); 48 | 49 | module.exports = router; 50 | -------------------------------------------------------------------------------- /cloud.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4'); 2 | const AV = require('leanengine'); 3 | const Order = require('./order'); 4 | const wxpay = require('./wxpay'); 5 | 6 | /** 7 | * 一个简单的云代码方法 8 | */ 9 | AV.Cloud.define('hello', function(request, response) { 10 | response.success('Hello world!'); 11 | }); 12 | 13 | /** 14 | * 小程序创建订单 15 | */ 16 | AV.Cloud.define('order', (request, response) => { 17 | const user = request.currentUser; 18 | if (!user) { 19 | return response.error(new Error('用户未登录')); 20 | } 21 | const authData = user.get('authData'); 22 | if (!authData || !authData.lc_weapp) { 23 | return response.error(new Error('当前用户不是小程序用户')); 24 | } 25 | const order = new Order(); 26 | order.tradeId = uuid().replace(/-/g, ''); 27 | order.status = 'INIT'; 28 | order.user = request.currentUser; 29 | order.productDescription = 'LeanCloud-小程序支付测试'; 30 | order.amount = 1; 31 | order.ip = request.meta.remoteAddress; 32 | if (!(order.ip && /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(order.ip))) { 33 | order.ip = '127.0.0.1'; 34 | } 35 | order.tradeType = 'JSAPI'; 36 | const acl = new AV.ACL(); 37 | // 只有创建订单的用户可以读,没有人可以写 38 | acl.setPublicReadAccess(false); 39 | acl.setPublicWriteAccess(false); 40 | acl.setReadAccess(user, true); 41 | acl.setWriteAccess(user, false); 42 | order.setACL(acl); 43 | order.place().then(() => { 44 | console.log(`预订单创建成功:订单号 [${order.tradeId}] prepayId [${order.prepayId}]`); 45 | const payload = { 46 | appId: process.env.WEIXIN_APPID, 47 | timeStamp: String(Math.floor(Date.now() / 1000)), 48 | package: `prepay_id=${order.prepayId}`, 49 | signType: 'MD5', 50 | nonceStr: String(Math.random()), 51 | } 52 | payload.paySign = wxpay.sign(payload); 53 | response.success(payload); 54 | }).catch(error => { 55 | console.error(error); 56 | response.error(error); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express'); 3 | var timeout = require('connect-timeout'); 4 | var path = require('path'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var weixin = require('./routes/weixin'); 8 | var AV = require('leanengine'); 9 | 10 | var app = express(); 11 | 12 | // 设置模板引擎 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'ejs'); 15 | app.use(express.static('public')); 16 | 17 | // 设置默认超时时间 18 | app.use(timeout('15s')); 19 | 20 | // 加载云函数定义 21 | require('./cloud'); 22 | // 加载云引擎中间件 23 | app.use(AV.express()); 24 | 25 | app.use(bodyParser.json()); 26 | app.use(bodyParser.urlencoded({ extended: false })); 27 | app.use(cookieParser()); 28 | 29 | app.get('/', function(req, res) { 30 | res.render('index', { currentTime: new Date() }); 31 | }); 32 | 33 | // 可以将一类的路由单独保存在一个文件中 34 | app.use('/weixin', weixin); 35 | 36 | app.use(function(req, res, next) { 37 | // 如果任何一个路由都没有返回响应,则抛出一个 404 异常给后续的异常处理器 38 | if (!res.headersSent) { 39 | var err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | } 43 | }); 44 | 45 | // error handlers 46 | app.use(function(err, req, res, next) { 47 | if (req.timedout && req.headers.upgrade === 'websocket') { 48 | // 忽略 websocket 的超时 49 | return; 50 | } 51 | 52 | var statusCode = err.status || 500; 53 | if (statusCode === 500) { 54 | console.error(err.stack || err); 55 | } 56 | if (req.timedout) { 57 | console.error('请求超时: url=%s, timeout=%d, 请确认方法执行耗时很长,或没有正确的 response 回调。', req.originalUrl, err.timeout); 58 | } 59 | res.status(statusCode); 60 | // 默认不输出异常详情 61 | var error = {} 62 | if (app.get('env') === 'development') { 63 | // 如果是开发环境,则将异常堆栈输出到页面,方便开发调试 64 | error = err; 65 | } 66 | res.render('error', { 67 | message: err.message, 68 | error: error 69 | }); 70 | }); 71 | 72 | module.exports = app; 73 | -------------------------------------------------------------------------------- /order.js: -------------------------------------------------------------------------------- 1 | const AV = require('leanengine'); 2 | const wxpay = require('./wxpay'); 3 | const { 4 | validateSign, 5 | handleError, 6 | } = require('./utils'); 7 | 8 | class Order extends AV.Object { 9 | get tradeId() { return this.get('tradeId'); } 10 | set tradeId(value) { this.set('tradeId', value); } 11 | 12 | get amount() { return this.get('amount'); } 13 | set amount(value) { this.set('amount', value); } 14 | 15 | get user() { return this.get('user'); } 16 | set user(value) { this.set('user', value); } 17 | 18 | get productDescription() { return this.get('productDescription'); } 19 | set productDescription(value) { this.set('productDescription', value); } 20 | 21 | get status() { return this.get('status'); } 22 | set status(value) { this.set('status', value); } 23 | 24 | get ip() { return this.get('ip'); } 25 | set ip(value) { this.set('ip', value); } 26 | 27 | get tradeType() { return this.get('tradeType'); } 28 | set tradeType(value) { this.set('tradeType', value); } 29 | 30 | get prepayId() { return this.get('prepayId'); } 31 | set prepayId(value) { this.set('prepayId', value); } 32 | 33 | place() { 34 | return new Promise((resolve, reject) => { 35 | // 参数文档: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1 36 | wxpay.createUnifiedOrder({ 37 | openid: this.user.get('authData').lc_weapp.openid, 38 | body: this.productDescription, 39 | out_trade_no: this.tradeId, 40 | total_fee: this.amount, 41 | spbill_create_ip: this.ip, 42 | notify_url: process.env.WEIXIN_NOTIFY_URL, 43 | trade_type: this.tradeType, 44 | }, function(err, result) { 45 | console.log(err, result); 46 | if (err) return reject(err); 47 | return resolve(result); 48 | }); 49 | }).then(handleError).then(validateSign).then(({ 50 | prepay_id, 51 | }) => { 52 | this.prepayId = prepay_id; 53 | return this.save(); 54 | }); 55 | } 56 | } 57 | AV.Object.register(Order); 58 | 59 | module.exports = Order; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小程序微信支付示例 2 | 3 | 小程序微信支付「后端商户系统」。配合 LeanCloud 小程序 SDK 快速实现小程序微信支付功能。 4 | 5 | ## 部署 6 | 7 | ### 配置环境变量 8 | 9 | 开始之前,请确保已经按照下面的步骤完成了环境变量的配置: 10 | 11 | 1. 进入应用控制台 - 云引擎 - 设置 12 | 2. 绑定域名 13 | 3. 添加并保存以下环境变量 14 | - `WEIXIN_APPID`:小程序 AppId 15 | - `WEIXIN_MCHID`:微信支付商户号 16 | - `WEIXIN_PAY_SECRET`:微信支付 API 密钥([微信商户平台](https://pay.weixin.qq.com) - 账户设置 - API安全 - 密钥设置) 17 | - `WEIXIN_NOTIFY_URL`:`https://{{yourdomain}}/weixin/pay-callback`,其中 `yourdomain` 是第二步中绑定的域名 18 | 19 |
20 | Example 21 | ![image](https://cloud.githubusercontent.com/assets/175227/22236906/7c651c80-e243-11e6-819b-007d5862bdbf.png) 22 |
23 | 24 | ### 本地开发 25 | 26 | 首先确认本机已经安装 [Node.js](http://nodejs.org/) 运行环境和 [LeanCloud 命令行工具](https://leancloud.cn/docs/leanengine_cli.html),然后执行下列指令: 27 | 28 | ``` 29 | $ git clone https://github.com/leancloud/weapp-pay-getting-started.git 30 | $ cd weapp-pay-getting-started 31 | ``` 32 | 33 | 安装依赖: 34 | 35 | ``` 36 | npm install 37 | ``` 38 | 39 | 登录并关联应用: 40 | 41 | ``` 42 | lean login 43 | lean checkout 44 | ``` 45 | 46 | 启动项目: 47 | 48 | ``` 49 | lean up 50 | ``` 51 | 52 | 之后你就可以在 [localhost:3001](http://localhost:3001) 调试云函数了。 53 | 54 | ### 部署 55 | 56 | 部署到预备环境(若无预备环境则直接部署到生产环境): 57 | ``` 58 | lean deploy 59 | ``` 60 | 61 | ## 支付流程 62 | 63 | 1. 登录用户在小程序客户端通过 JavaScript SDK 调用名为 `order` 的云函数下单。 64 | 2. `order` 函数调用微信支付统一下单 API,创建「预订单」并保存在 Order 表中,返回签名过的预订单信息。 65 | 3. 在小程序客户端调用支付 API,传入 2 中返回的预订单信息,发起支付。 66 | 4. 支付成功后,微信通知 `/weixin/pay-callback` 支付成功,pay-callback 将对应的 order 状态更新为 `SUCCESS`。 67 | 68 | 客户端的实例代码参见 [leancloud/leantodo-weapp](https://github.com/leancloud/leantodo-weapp)。 69 | 70 | ## 相关文档 71 | 72 | * 小程序 73 | * [在小程序中使用 LeanCloud](https://leancloud.cn/docs/weapp.html) 74 | * [小程序支付客户端示例项目(LeanTodo)](https://github.com/leancloud/leantodo-weapp) 75 | * 支付 76 | * [小程序客户端发起支付 API](https://mp.weixin.qq.com/debug/wxadoc/dev/api/api-pay.html) 77 | * [微信支付统一下单 API 参数与错误码](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1) 78 | * [微信支付结果通知参数](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7) 79 | * 云引擎 80 | * [云函数开发指南](https://leancloud.cn/docs/leanengine_cloudfunction_guide-node.html) 81 | * [网站托管开发指南](https://leancloud.cn/docs/leanengine_webhosting_guide-node.html) 82 | * [JavaScript 开发指南](https://leancloud.cn/docs/leanstorage_guide-js.html) 83 | * [JavaScript SDK API](https://leancloud.github.io/javascript-sdk/docs/) 84 | * [Node.js SDK API](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md) 85 | * [命令行工具使用指南](https://leancloud.cn/docs/cloud_code_commandline.html) 86 | * [云引擎常见问题和解答](https://leancloud.cn/docs/leanengine_faq.html) 87 | --------------------------------------------------------------------------------