本次实操依赖三个核心工具,均为开源免费,配置步骤如下:
完成工具安装后,在桌面新建`member-lock`文件夹,终端进入后执行依赖初始化:
```bash npm init -y npm install express mysql2 redis dayjs jsonwebtoken cors --save ```打开MySQL Workbench或终端(终端命令`mysql -uroot -proot123456`),执行以下完整SQL脚本:
```sql CREATE DATABASE IF NOT EXISTS member_lock DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE member_lock; -- 用户基础表(含会员等级映射) CREATE TABLE IF NOT EXISTS `users` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', `phone` VARCHAR(11) NOT NULL UNIQUE COMMENT '手机号', `level` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '会员等级:1普通 2白银 3黄金 4钻石', `total_points` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总积分', `used_points` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已用积分', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), INDEX `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 连续签到记录表(用Redis替代大部分,但留一张月签到备份历史用) CREATE TABLE IF NOT EXISTS `sign_history` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '历史记录ID', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', `sign_date` DATE NOT NULL COMMENT '签到日期', `reward_points` INT UNSIGNED NOT NULL COMMENT '本次签到积分', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间', PRIMARY KEY (`id`), UNIQUE KEY `idx_user_date` (`user_id`, `sign_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 专属优惠券表 CREATE TABLE IF NOT EXISTS `coupons` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '券ID', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '领取用户ID', `type` TINYINT UNSIGNED NOT NULL COMMENT '券类型:1连续7天专属满减券', `threshold` INT UNSIGNED NOT NULL COMMENT '满减门槛(分)', `discount` INT UNSIGNED NOT NULL COMMENT '折扣金额(分)', `expire_time` DATETIME NOT NULL COMMENT '过期时间', `status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态:1未使用 2已使用 3已过期', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发放时间', PRIMARY KEY (`id`), INDEX `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ```在`member-lock`文件夹下创建三个文件:`db.js`(数据库连接)、`redis.js`(Redis连接)、`app.js`(主业务逻辑)。

可直接复制的完整代码:
```javascript const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'root123456', database: 'member_lock', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); module.exports = pool; ```可直接复制的完整代码:
```javascript const redis = require('redis'); const client = redis.createClient({ socket: { port: 6379, host: 'localhost' } }); client.on('error', (err) => console.log('Redis连接错误', err)); client.connect(); module.exports = client; ```可直接复制的完整代码(已包含注册、登录、签到、查连续签到天数、查专属券功能):
```javascript const express = require('express'); const cors = require('cors'); const dayjs = require('dayjs'); const pool = require('./db'); const redisClient = require('./redis'); const app = express(); const PORT = 3000; app.use(cors()); app.use(express.json()); // 生成简易JWT(仅用于演示,生产环境需加secret和过期时间) const generateToken = (userId) => `token_${userId}`; // 注册接口(仅需手机号) app.post('/api/register', async (req, res) => { const { phone } = req.body; if (!/^1[3-9]\d{9}$/.test(phone)) return res.json({ code: 400, msg: '手机号格式错误' }); try { const [result] = await pool.execute('INSERT INTO users (phone) VALUES (?)', [phone]); const token = generateToken(result.insertId); res.json({ code: 200, msg: '注册成功', data: { userId: result.insertId, token } }); } catch (err) { if (err.code === 'ER_DUP_ENTRY') return res.json({ code: 400, msg: '手机号已注册' }); res.json({ code: 500, msg: '服务器错误' }); } }); // 登录接口(复用注册逻辑) app.post('/api/login', async (req, res) => { const { phone } = req.body; if (!/^1[3-9]\d{9}$/.test(phone)) return res.json({ code: 400, msg: '手机号格式错误' }); try { const [rows] = await pool.execute('SELECT id FROM users WHERE phone = ?', [phone]); if (rows.length === 0) return res.json({ code: 404, msg: '用户未注册' }); const token = generateToken(rows[0].id); res.json({ code: 200, msg: '登录成功', data: { userId: rows[0].id, token } }); } catch (err) { res.json({ code: 500, msg: '服务器错误' }); } }); // 验证token中间件(演示版,直接解析userId) const auth = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token?.startsWith('token_')) return res.json({ code: 401, msg: '未登录' }); req.userId = parseInt(token.split('_')[1]); next(); }; // 核心锁客接口:签到 app.post('/api/sign', auth, async (req, res) => { const userId = req.userId; const today = dayjs().format('YYYY-MM-DD'); const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); const signKey = `sign:${userId}`; const streakKey = `streak:${userId}`; try { // 检查是否已签到 const isSigned = await redisClient.hGet(signKey, today); if (isSigned) return res.json({ code: 400, msg: '今日已签到' }); // 获取连续签到天数 let streak = 0; const lastStreakDay = await redisClient.hGet(signKey, yesterday); if (lastStreakDay) { streak = parseInt(await redisClient.get(streakKey) || 0); } streak += 1; if (streak > 7) streak = 1; // 计算积分:普通日10,连续第3天20,连续第7天50 let rewardPoints = 10; if (streak === 3) rewardPoints = 20; if (streak === 7) rewardPoints = 50; // 开启事务 const connection = await pool.getConnection(); await connection.beginTransaction(); try { // 更新用户积分 await connection.execute('UPDATE users SET total_points = total_points + ? WHERE id = ?', [rewardPoints, userId]); // 写入月签到历史 await connection.execute('INSERT INTO sign_history (user_id, sign_date, reward_points) VALUES (?, ?, ?)', [userId, today, rewardPoints]); // 连续7天发放专属满减券:满1000分(10元)减200分(2元),7天后过期 if (streak === 7) { const expireTime = dayjs().add(7, 'day').format('YYYY-MM-DD HH:mm:ss'); await connection.execute('INSERT INTO coupons (user_id, type, threshold, discount, expire_time) VALUES (?, 1, 1000, 200, ?)', [userId, expireTime]); } await connection.commit(); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); } // 更新Redis await redisClient.hSet(signKey, today, streak); await redisClient.expire(signKey, 86400 32); // 保留32天月签到记录 await redisClient.set(streakKey, streak); await redisClient.expire(streakKey, 86400 2); // 连续签到超时时间设为2天(防止漏签后1天还能续) res.json({ code: 200, msg: '签到成功', data: { streak, rewardPoints, is7DayCoupon: streak === 7 } }); } catch (err) { res.json({ code: 500, msg: '服务器错误' }); } }); // 查询接口:当前连续签到天数 app.get('/api/streak', auth, async (req, res) => { const userId = req.userId; const streak = parseInt(await redisClient.get(`streak:${userId}`) || 0); res.json({ code: 200, msg: '查询成功', data: { streak } }); }); // 查询接口:专属未过期券 app.get('/api/coupons', auth, async (req, res) => { const userId = req.userId; const [rows] = await pool.execute('SELECT id, threshold, discount, expire_time FROM coupons WHERE user_id = ? AND status = 1 AND expire_time > NOW()', [userId]); res.json({ code: 200, msg: '查询成功', data: { coupons: rows } }); }); app.listen(PORT, () => console.log(`服务已启动:http://localhost:${PORT}`)); ```终端进入`member-lock`文件夹,执行以下命令启动服务:
```bash node app.js ```服务启动成功后,用Postman或Apifox(推荐,下载地址 https://www.apifox.cn/download)测试接口,测试流程按顺序:
演示版为了零门槛省略了安全和性能细节,生产环境可按以下操作10分钟优化:
易频IT社区是综合性互联网IT技术门户网站,专注分享网络技术、服务器运维、网络安全、编程开发、系统架构、云计算、大数据等行业干货,实时更新IT行业资讯、零基础教程、实战案例,为IT从业者、技术爱好者提供专业的学习交流平台。
Copyright © 2021-2026 易频IT社区. All Rights Reserved. 备案号:闽ICP备2023013482号 网站地图