统一使用Node.js 16.18.0版本,避免高版本兼容性问题,执行以下命令安装所有依赖:
``` npm install express@4.18.2 mysql2@3.6.0 wechatpay-node-v3@2.1.3 jsonwebtoken@9.0.2 cors@2.8.5 ```在MySQL 8.0+版本中执行以下建表语句,所有字段可直接复用无需修改:
```sql -- 模块配置表 CREATE TABLE `module_config` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `module_key` varchar(32) NOT NULL COMMENT '模块唯一标识', `module_name` varchar(64) NOT NULL COMMENT '模块名称', `module_desc` varchar(255) DEFAULT '' COMMENT '模块描述', `price` int unsigned NOT NULL COMMENT '售价,单位为分', `valid_days` int unsigned NOT NULL DEFAULT 365 COMMENT '有效天数', `is_active` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '是否上架 1上架 0下架', `sort` int unsigned NOT NULL DEFAULT '0' COMMENT '排序权重', PRIMARY KEY (`id`), UNIQUE KEY `uk_module_key` (`module_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入默认测试模块 INSERT INTO `module_config` (`module_key`, `module_name`, `module_desc`, `price`, `valid_days`, `is_active`, `sort`) VALUES ('archive_import', '档案批量导入', '支持Excel批量导入多类档案自动分类', 9900, 365, 1, 1), ('archive_borrow', '档案借阅管理', '支持线上申请、审批、归还全流程追踪', 19900, 365, 1, 2), ('archive_destroy', '档案销毁管理', '支持销毁审批、操作日志留痕', 29900, 365, 1, 3); -- 用户已购模块表 CREATE TABLE `user_module` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `user_id` int unsigned NOT NULL, `module_id` int unsigned NOT NULL, `expire_time` datetime NOT NULL COMMENT '到期时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_module` (`user_id`,`module_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `pay_order` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int unsigned NOT NULL, `module_id` int unsigned NOT NULL, `pay_amount` int unsigned NOT NULL COMMENT '支付金额,单位分', `pay_status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已关闭', `pay_time` datetime DEFAULT NULL, `transaction_id` varchar(64) DEFAULT '' COMMENT '支付平台流水号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ```新建config.js文件,填入以下可直接复用的配置,仅需替换自身商户相关参数:
```js const fs = require('fs') module.exports = { jwtSecret: '自定义32位随机字符串作为jwt加密密钥', dbConfig: { host: '你的数据库地址', user: '数据库用户名', password: '数据库密码', database: '你的库名' }, payConfig: { appid: '你的小程序/公众号APPID', mchid: '你的微信支付商户号', publicKey: fs.readFileSync('./cert/apiclient_cert.pem'), privateKey: fs.readFileSync('./cert/apiclient_key.pem'), serial_no: '你的微信支付证书序列号', notify_url: 'https://你的域名/api/pay/notify' } } ```注意:微信支付证书需从商户平台下载,放在项目根目录的cert文件夹下,权限设置为600避免泄露
新建routes/module.js文件,实现返回带用户购买状态的模块列表接口:
```js const express = require('express') const router = express.Router() const jwt = require('jsonwebtoken') const { jwtSecret, dbConfig } = require('../config') const mysql = require('mysql2/promise') router.get('/list', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] if (!token) return res.json({code: 401, msg: '请登录'}) try { const decode = jwt.verify(token, jwtSecret) const userId = decode.userId const conn = await mysql.createConnection(dbConfig) // 查询所有上架模块 const [modules] = await conn.execute('SELECT id, module_key, module_name, module_desc, price, valid_days FROM module_config WHERE is_active = 1 ORDER BY sort ASC') // 查询用户已购未过期模块 const [userModules] = await conn.execute('SELECT module_id FROM user_module WHERE user_id = ? AND expire_time > NOW()', [userId]) const userModuleIds = userModules.map(item => item.module_id) // 拼接购买状态 const result = modules.map(item => ({ ...item, has_bought: userModuleIds.includes(item.id) })) await conn.end() res.json({code: 200, data: result}) } catch (e) { res.json({code: 500, msg: '查询失败'}) } }) module.exports = router ```
必须在所有模块相关接口添加JWT校验,禁止未登录用户查询模块信息
新建routes/pay.js文件,实现下单与回调逻辑:
```js const express = require('express') const router = express.Router() const jwt = require('jsonwebtoken') const { jwtSecret, dbConfig, payConfig } = require('../config') const mysql = require('mysql2/promise') const WechatPay = require('wechatpay-node-v3') const pay = new WechatPay(payConfig) // 下单接口 router.post('/createOrder', async (req, res) => { const token = req.headers.authorization?.split(' ')[1] const { moduleId } = req.body if (!token || !moduleId) return res.json({code: 400, msg: '参数错误'}) try { const decode = jwt.verify(token, jwtSecret) const userId = decode.userId const conn = await mysql.createConnection(dbConfig) // 校验模块是否上架 const [modules] = await conn.execute('SELECT id, price, module_name FROM module_config WHERE id = ? AND is_active = 1', [moduleId]) if (modules.length === 0) return res.json({code: 400, msg: '模块未上架'}) // 生成订单号:前缀+时间戳+6位随机数 const orderNo = 'ARCHIVE' + Date.now() + Math.random().toString().slice(-6) await conn.execute('INSERT INTO pay_order (order_no, user_id, module_id, pay_amount) VALUES (?, ?, ?, ?)', [orderNo, userId, moduleId, modules[0].price]) // 调用微信支付统一下单 const params = await pay.transactions_jsapi({ description: modules[0].module_name, out_trade_no: orderNo, amount: { total: modules[0].price, currency: 'CNY' }, payer: { openid: decode.openid } }) await conn.end() res.json({code: 200, data: params}) } catch (e) { res.json({code: 500, msg: '下单失败'}) } }) // 支付回调接口 router.post('/notify', async (req, res) => { const signature = req.headers['wechatpay-signature'] const serial = req.headers['wechatpay-serial'] const timestamp = req.headers['wechatpay-timestamp'] const nonce = req.headers['wechatpay-nonce'] // 验签 const verify = pay.verifySignature(nonce, timestamp, req.body, signature, serial) if (!verify) return res.status(400).json({code: 'FAIL', message: '验签失败'}) // 解密回调数据 const result = pay.decipher_gcm(req.body.resource.ciphertext, req.body.resource.associated_data, req.body.resource.nonce, payConfig.key) if (result.trade_state === 'SUCCESS') { const conn = await mysql.createConnection(dbConfig) // 校验订单状态,避免重复处理 const [orders] = await conn.execute('SELECT id, user_id, module_id FROM pay_order WHERE order_no = ? AND pay_status = 0', [result.out_trade_no]) if (orders.length > 0) { // 更新订单状态 await conn.execute('UPDATE pay_order SET pay_status = 1, pay_time = NOW(), transaction_id = ? WHERE order_no = ?', [result.transaction_id, result.out_trade_no]) // 给用户开通模块,有效期1年 await conn.execute('REPLACE INTO user_module (user_id, module_id, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 YEAR))', [orders[0].user_id, orders[0].module_id]) } await conn.end() } res.json({code: 'SUCCESS', message: '处理成功'}) }) module.exports = router ```以Vue3项目为例,在router/index.js中添加全局前置守卫:
```js router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('token') // 公共页面直接放行 if (to.meta.public) return next() if (!token) return next('/login') // 未缓存已购模块则请求后端 if (!store.state.userModules.length) { const res = await axios.get('/api/module/list') if (res.code === 200) { store.commit('setUserModules', res.data.filter(item => item.has_bought).map(item => item.module_key)) } } // 校验当前路由所需模块权限 if (to.meta.moduleKey && !store.state.userModules.includes(to.meta.moduleKey)) { return next('/pay') } next() }) ```必须同时在后端所有模块业务接口添加权限校验,禁止仅靠前端拦截绕过付费
自定义权限指令,控制未购买模块的入口隐藏:
```js // main.js中注册全局指令 app.directive('hasModule', { mounted(el, binding) { const userModules = store.state.userModules if (!userModules.includes(binding.value)) { el.remove() } } }) // 页面使用
档案整理行业认证,这玩意儿到底是不是你的职场“硬通货”?