├── 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 |
13 |
14 | <% for(var i=0; i
15 | - <%= todos[i].get('content') %>
16 | <% } %>
17 |
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 | 
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 |
--------------------------------------------------------------------------------