短报文教学

This commit is contained in:
2026-03-27 17:57:07 +08:00
commit 4da5047bf2
27 changed files with 1073 additions and 0 deletions

96
app/controller/home.js Normal file
View File

@@ -0,0 +1,96 @@
const BaseController = require('../core/base_controller.js');
const fileName = "home";
class HomeController extends BaseController {
// token状态查询接口
async tokenStatus() {
const { ctx } = this;
try {
const uid = this.decryptRequest(ctx.request.body.uid).split('?')[0];
let rdmcode = await this.checkUid(ctx, uid);
if (!rdmcode) {
this.setResponse(ctx, 401, 'token 过期或错误', "none");
return;
}
const result = await ctx.service.home.tokenStatus(uid);
if (result == "error") {
this.setResponse(ctx, 500, '服务器错误', "none");
return;
}
if (result) {
this.setResponse(ctx, 200, 'token 状态查询成功', "deconstruction", result);
} else {
this.setResponse(ctx, 404, 'token 状态查询失败', "none");
}
} catch (error) {
const blockName = fileName + "--" + "tokenStatus";
ctx.app.middleware.log('../../controllerLog', blockName + ">>>>" + error);
this.setResponse(ctx, 500, '服务器错误', "none");
}
}
// 用户获取短期token的接口
async getUserToken() {
const { ctx } = this;
try {
const uid = this.decryptRequest(ctx.request.body.uid).split('?')[0];
if (uid == "") {
this.setResponse(ctx, 404, '获取临时 token 失败', "none");
return;
}
let rdmcode = await this.checkUid(ctx);
if (!rdmcode) {
this.setResponse(ctx, 401, 'token 过期或错误', "none");
return;
}
const result = await ctx.service.home.getUserToken(uid);
if (result == "error") {
this.setResponse(ctx, 500, '服务器错误', "none");
return;
}
if (result) {
this.setResponse(ctx, 200, '获取临时 token 成功', "none");
} else {
this.setResponse(ctx, 404, '获取临时 token 失败', "none");
}
} catch (error) {
const blockName = fileName + "--" + "getUserToken";
ctx.app.middleware.log('../../controllerLog', blockName + ">>>>" + error);
this.setResponse(ctx, 500, '服务器错误', "none");
}
}
// 用户延续短期token有效时间的接口
async lifeHold() {
const { ctx } = this;
try {
const uid = this.decryptRequest(ctx.request.body.uid).split('?')[0];
if (uid == "") {
this.setResponse(ctx, 404, '延续临时 token 有效时间失败', "none");
return;
}
let rdmcode = await this.checkUid(ctx);
if (!rdmcode) {
this.setResponse(ctx, 401, 'token 过期或错误', "none");
return;
}
const result = await ctx.service.home.lifeHold(uid);
if (result == "error") {
this.setResponse(ctx, 500, '服务器错误', "none");
return;
}
if (result) {
this.setResponse(ctx, 200, '延续临时 token 有效时间成功', "none");
} else {
this.setResponse(ctx, 404, '延续临时 token 有效时间失败', "none");
}
} catch (error) {
const blockName = fileName + "--" + "lifeHold";
ctx.app.middleware.log('../../controllerLog', blockName + ">>>>" + error);
this.setResponse(ctx, 500, '服务器错误', "none");
}
}
}
module.exports = HomeController;

View File

@@ -0,0 +1,73 @@
const BaseController = require('../core/base_controller.js');
const fileName = "teaching";
class TeachingController extends BaseController {
// 获取最近发送的一条信息
async getMsg() {
const { ctx } = this;
try {
const redis = ctx.app.redis;
const token = ctx.request.query.token;
this.banManager(redis, token);
const result = await ctx.service.teaching.getMsg(redis, token);
if (result == "error") {
this.setResponse(ctx, 500, '服务器错误', "none");
} else if (result.code == 401) {
this.setResponse(ctx, 't401', 'token 过期或错误', "none");
} else if (result.code == 403) {
this.setResponse(ctx, 403, '此 token 已进入黑名单,请等待解封后再使用', "none");
} else if (result.code == 200) {
delete result.code;
const data = {
userData: result.userData,
responseTime: new Date()
};
this.setResponse(ctx, 200, '最近消息查询成功', "deconstruction", data);
} else {
this.setResponse(ctx, 404, '最近消息查询失败', "none");
}
} catch (error) {
const blockName = fileName + "--" + "getMsg";
ctx.app.middleware.log('../../controllerLog', blockName + ">>>>" + error);
this.setResponse(ctx, 500, '服务器错误', "none");
}
}
// 获取若干条历史消息
async getHistory() {
const { ctx } = this;
try {
const redis = ctx.app.redis;
const { token, limit, offset } = ctx.request.query;
this.banManager(redis, token);
const result = await ctx.service.teaching.getHistory(redis, token, limit, offset);
if (result == "error") {
this.setResponse(ctx, 500, '服务器错误', "none");
} else if (result.code == 401) {
this.setResponse(ctx, 't401', 'token 过期或错误', "none");
} else if (result.code == 403) {
this.setResponse(ctx, 403, '此 token 已进入黑名单,请等待解封后再使用', "none");
} else if (result.code == 200) {
delete result.code;
let historyMsg = [];
result.userData.rows.forEach(element => {
historyMsg.push(element.dataValues);
});
const data = {
userData: historyMsg,
total: result.userData.count
};
this.setResponse(ctx, 200, '历史消息查询成功', "deconstruction", data);
} else {
this.setResponse(ctx, 404, '历史消息查询失败', "none");
}
} catch (error) {
const blockName = fileName + "--" + "getHistory";
ctx.app.middleware.log('../../controllerLog', blockName + ">>>>" + error);
this.setResponse(ctx, 500, '服务器错误', "none");
}
}
}
module.exports = TeachingController;

110
app/core/base_controller.js Normal file
View File

@@ -0,0 +1,110 @@
const { Controller } = require('egg');
const sha256 = require("crypto-js/sha256");
const CryptoJS = require("crypto-js");
const KEY = CryptoJS.enc.Utf8.parse('BDQH');
class BaseController extends Controller {
shaForm = (body, salt) => {
return sha256(sha256(body).toString() + salt).toString();
}
encryptRequest = (body) => {
return CryptoJS.AES.encrypt(body, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
decryptRequest = (body) => {
return CryptoJS.AES.decrypt(body, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
}
checkUid = async (ctx, uid) => {
const token = ctx.request.header.authorization;
const redis = ctx.app.redis;
let rdmcode;
if (!uid) {
const [buser, cuser] = await Promise.all([
redis.get('db0').get(token),
redis.get('db1').get(token),
]);
rdmcode = buser || cuser;
} else {
const redisType = {
"B": "db0",
"C": "db1"
};
const lastCode = uid[uid.length - 1];
rdmcode = await redis.get(redisType[lastCode]).get(token);
}
if (!rdmcode) {
return false;
}
return rdmcode;
};
setResponse = (ctx, code, msg, model, data = {}) => {
const modelType = {
"none": { code, msg },
"deconstruction": { code, msg, ...data },
"polymerization": { code, msg, data }
}
ctx.body = modelType[model];
}
// 黑名单管理器
banManager = async (redis, token) => {
// 基本元素
const now = new Date().getTime();
let reex = 1209600;
let tokenContainer = await redis.get('db4').get('tokenContainer');
let tokenCheck = await redis.get('db4').get(token);
let ex = await redis.get('db4').ttl(token);
tokenCheck = JSON.parse(tokenCheck);
// 判断存放教学接口 token 的容器是否存在,不存在则创建并设置过期时间
if (!tokenContainer == null) {
reex = await redis.get('db4').ttl('tokenContainer');
if (reex <= 600) {
reex = 1209600;
await redis.get('db4').set('tokenContainer', JSON.stringify(JSON.parse(tokenContainer)), 'EX', reex);
}
} else {
let createContainer = {};
createContainer[token] = { num: 1, firstTime: now };
await redis.get('db4').set('tokenContainer', JSON.stringify(createContainer), 'EX', reex);
}
tokenContainer = JSON.parse(tokenContainer);
if (tokenContainer !== null && tokenCheck !== null) {
// 黑名单回收器
for (let item in tokenContainer) {
if (tokenContainer[item].firstTime < now - 1000) {
delete tokenContainer[item];
}
}
// 黑名单释放器
if (tokenCheck.attacksMark && now - tokenCheck.banAt > 3600000) {
await redis.get('db4').set(token, JSON.stringify({ uid: tokenCheck.uid, attacksMark: false }), 'EX', ex);
}
// 黑名单检查器
if (!tokenContainer[token]) {
tokenContainer[token] = { num: 1, firstTime: now }
} else {
tokenContainer[token].num += 1;
if (tokenContainer[token].num > 10) {
delete tokenContainer[token];
// 黑名单标记器
await redis.get('db4').set(token, JSON.stringify({ uid: tokenCheck.uid, attacksMark: true, banAt: now }), 'EX', ex);
}
}
await redis.get('db4').set('tokenContainer', JSON.stringify(tokenContainer), 'EX', reex);
}
}
}
module.exports = BaseController;

85
app/core/base_service.js Normal file
View File

@@ -0,0 +1,85 @@
const { Service } = require('egg');
const sha256 = require("crypto-js/sha256");
const CryptoJS = require("crypto-js");
const KEY = CryptoJS.enc.Utf8.parse('BDQH');
class BaseService extends Service {
shaForm = (body, salt) => {
return sha256(sha256(body).toString() + salt).toString();
}
encryptRequest = (body) => {
return CryptoJS.AES.encrypt(body, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
decryptRequest = (body) => {
return CryptoJS.AES.decrypt(body, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
}
// 随机盐生成函数
generateRandomString = () => {
const lowerCaseLetters = 'abcdefghijklmnopqrstuvwxyz';
const upperCaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
let result = '';
// 生成5位随机小写字母
for (let i = 0; i < 5; i++) {
const randomIndex = Math.floor(Math.random() * lowerCaseLetters.length);
result += lowerCaseLetters[randomIndex];
}
// 生成5位随机大写字母
for (let i = 0; i < 5; i++) {
const randomIndex = Math.floor(Math.random() * upperCaseLetters.length);
result += upperCaseLetters[randomIndex];
}
// 生成2位随机数字
for (let i = 0; i < 2; i++) {
const randomIndex = Math.floor(Math.random() * numbers.length);
result += numbers[randomIndex];
}
// 打乱整个字符串
const characters = result.split('');
for (let i = characters.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[characters[i], characters[j]] = [characters[j], characters[i]];
}
return characters.join('');
}
// 教学接口 token 验证,验证 token 是否存在,是否在黑名单中
Verify = async (redis, teachingToken) => {
const token = await redis.get('db4').get(teachingToken);
if (!token) {
return 401;
} else if (JSON.parse(token).attacksMark) {
return 403;
}
return JSON.parse(token).uid;
}
shunt = (uid) => {
return uid[uid.length - 1];
}
isAllDigits = (str) => {
const regex = /^\d+$/;
return regex.test(str);
}
}
module.exports = BaseService;

72
app/middleware/log.js Normal file
View File

@@ -0,0 +1,72 @@
// 错误日志
const fs = require('fs');
const os = require('os');
const path = require('path');
const dirCheck = (dirPath) => {
// 检查文件夹是否存在
fs.access(dirPath, fs.constants.F_OK, (err) => {
if (err) {
// 文件夹不存在
console.log('文件夹不存在,正在创建...');
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) throw err;
console.log('文件夹已创建');
});
} else {
// 文件夹存在
console.log('文件夹已存在');
}
});
}
module.exports = (filePath, msg) => {
// 获取时间
let now = new Date(),
times = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
second: now.getSeconds()
};
// 获取操作系统类型
const type = os.type();
console.log(`操作系统类型: ${type}`);
filePath = path.join(__dirname, filePath);
dirCheck(filePath);
const yearPath = filePath + '/' + times.year;
dirCheck(yearPath);
const monthPath = yearPath + '/' + times.month;
dirCheck(monthPath);
const dayPath = monthPath + '/' + times.day + '.txt';
const logTime = '[' + times.year + '-' + times.month + '-' + times.day + '>>>' + times.hour + ':' + times.minute + ':' + times.second + ']' + '>>>';
let crlf;
// 判断操作系统类型,设置换行符
if (type === 'Windows_NT') {
crlf = '\n';
} else {
crlf = '\r\n';
}
// 读取文件内容
fs.readFile(dayPath, 'utf8', (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
// 文件不存在,直接写入新内容(这里实际上还是使用了覆盖的方式)
fs.writeFile(dayPath, logTime + msg + crlf, (err) => {
if (err) throw err;
console.log('文件已创建并写入新内容');
});
} else {
throw err;
}
} else {
// 文件存在,追加新内容
fs.appendFile(dayPath, logTime + msg + crlf, (err) => {
if (err) throw err;
console.log('新内容已追加到文件');
});
}
});
};

24
app/model/BShortMsg.js Normal file
View File

@@ -0,0 +1,24 @@
'use strict';
module.exports = (app) => {
const { INTEGER, CHAR, TEXT, DATE } = app.Sequelize;
const BShortMsg = app.model.define('bshort-msg', {
id: { type: INTEGER(11), primaryKey: true, autoIncrement: true },
user_uid: CHAR(15),
card_id: CHAR(8),
receiver_address: CHAR(8),
original_msg: TEXT,
send_type: INTEGER(1),
encode_type: INTEGER(1),
msg_data: TEXT,
created_at: DATE,
},{
freezeTableName: true,
timestamp: true,
createdAt: true,
updatedAt: false,
});
return BShortMsg;
};

18
app/model/BuserToken.js Normal file
View File

@@ -0,0 +1,18 @@
'use strict';
module.exports = (app) => {
const { INTEGER, CHAR } = app.Sequelize;
const BuserToken = app.model.define('buser-token', {
id: { type: INTEGER(8), primaryKey: true, autoIncrement: true },
user_uid: CHAR(15),
token: CHAR(64),
},{
freezeTableName: true,
timestamp: false,
createdAt: false,
updatedAt: false,
});
return BuserToken;
};

24
app/model2/CShortMsg.js Normal file
View File

@@ -0,0 +1,24 @@
'use strict';
module.exports = (app) => {
const { INTEGER, CHAR, TEXT, DATE } = app.Sequelize;
const CShortMsg = app.model2.define('cshort-msg', {
id: { type: INTEGER(11), primaryKey: true, autoIncrement: true },
user_uid: CHAR(15),
card_id: CHAR(8),
receiver_address: CHAR(8),
original_msg: TEXT,
send_type: INTEGER(1),
encode_type: INTEGER(1),
msg_data: TEXT,
created_at: DATE,
},{
freezeTableName: true,
timestamp: true,
createdAt: true,
updatedAt: false,
});
return CShortMsg;
};

19
app/router.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* @param {Egg.Application} app - egg application
*/
module.exports = async (app) => {
const { router, controller, redis } = app;
// 服务接口
// token状态查询接口
router.post('/tokenStatus', controller.home.tokenStatus);
// 用户获取短期token的接口
router.post('/getUserToken', controller.home.getUserToken);
// 用户延续短期token有效时间的接口
router.post('/lifeHold', controller.home.lifeHold);
// 教学接口
// 获取最近发送的一条信息
router.get('/getMsg', controller.teaching.getMsg)
// 获取若干条历史消息
router.get('/getHistory', controller.teaching.getHistory)
};

88
app/service/home.js Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
const BaseService = require('../core/base_service.js');
const sha256 = require("crypto-js/sha256");
const fileName = "home";
class HomeService extends BaseService {
// token 状态查询接口
async tokenStatus(uid) {
const { ctx } = this;
const redis = ctx.app.redis;
try {
let result = {};
const tokenUser = await redis.get('db4').get(uid);
if (!tokenUser) {
const where = { user_uid: uid };
const attributes = ['token'];
result.remainder = false;
result.token = await ctx.model.BuserToken.findOne({ attributes, where });
result.type = result.token == null ? 'none' : "long";
} else {
result.remainder = await redis.get('db4').ttl(uid);
result.token = JSON.parse(tokenUser).token;
result.type = JSON.parse(tokenUser).lifeHold ? "lifeHold" : "short";
}
const token = await redis.get('db4').get(result.token);
if(token) {
result.attacksMark = JSON.parse(token).attacksMark;
result.banAt = result.attacksMark ? JSON.parse(token).banAt : 0;
}
return result ? result : false;
} catch (error) {
const blockName = fileName + "--" + "tokenStatus";
ctx.app.middleware.log('../../serviceLog', blockName + ">>>>" + error);
return 'error';
}
}
// 用户获取短期 token 的接口
async getUserToken(uid) {
const { ctx } = this;
const redis = ctx.app.redis;
try {
const token = this.shaForm(uid, this.generateRandomString());
const checkUser = await redis.get('db4').get(uid);
let tokenUser, tokenMark;
if (!checkUser) {
const where = { user_uid: uid };
const attributes = ['token'];
const result = await ctx.model.BuserToken.findOne({ attributes, where });
if (result == null) {
// 设置过期时间为一周
tokenUser = await redis.get('db4').set(uid, JSON.stringify({ token, lifeHold: false }), 'EX', 604800);
tokenMark = await redis.get('db4').set(token, JSON.stringify({ uid, attacksMark: false }), 'EX', 604800);
}
}
return tokenUser && tokenMark ? true : false;
} catch (error) {
const blockName = fileName + "--" + "getUserToken";
ctx.app.middleware.log('../../serviceLog', blockName + ">>>>" + error);
return 'error';
}
}
// 用户延续短期 token 有效时间的接口
async lifeHold(uid) {
const { ctx } = this;
const redis = ctx.app.redis;
try {
const user = await redis.get('db4').get(uid);
let tokenUser, tokenMark;
if (!JSON.parse(user).lifeHold) {
let ex = await redis.get('db4').ttl(uid) + 604800;
tokenUser = await redis.get('db4').set(uid, JSON.stringify({ token: JSON.parse(user).token, lifeHold: true }), 'EX', ex);
tokenMark = await redis.get('db4').expire(JSON.parse(user).token, ex);
}
return tokenUser && tokenMark ? true : false;
} catch (error) {
const blockName = fileName + "--" + "lifeHold";
ctx.app.middleware.log('../../serviceLog', blockName + ">>>>" + error);
return 'error';
}
}
}
module.exports = HomeService;

100
app/service/teaching.js Normal file
View File

@@ -0,0 +1,100 @@
'use strict';
const BaseService = require('../core/base_service.js');
const fileName = "teaching";
class TeachingService extends BaseService {
// 获取最近发送的一条信息
async getMsg(redis, token) {
const { ctx } = this;
try {
const verifyResult = await this.Verify(redis, token);
let result = { code: verifyResult };
if (verifyResult == 401) {
const where = { token };
const attributes = ['token'];
const dbResult = await ctx.model.BuserToken.findOne({ attributes, where });
result.code = dbResult ? dbResult.dataValus.user_uid : 401;
}
if (result.code !== 401 && result.code !== 403) {
const uidLastCode = this.shunt(result.code);
const model = {
"B": ctx.model.BShortMsg,
"C": ctx.model2.CShortMsg
};
const attributes = ['card_id', 'encode_type', 'msg_data', 'created_at'];
const where = { user_uid: result.code };
const limit = 1;
const order = [['created_at', 'DESC']];
const dbOptions = { attributes, limit, order, where };
const dbResult = await model[uidLastCode].findOne(dbOptions);
if (dbResult) {
result.userData = dbResult.dataValues;
result.code = 200;
} else {
result.code = false;
}
}
return result.code ? result : false;
} catch (error) {
const blockName = fileName + "--" + "getMsg";
ctx.app.middleware.log('../../serviceLog', blockName + ">>>>" + error);
return 'error';
}
}
// 获取若干条历史消息
async getHistory(redis, token, limit, offset) {
const { ctx } = this;
try {
const verifyResult = await this.Verify(redis, token);
let result = { code: verifyResult };
if (verifyResult == 401) {
const where = { token };
const attributes = ['token'];
const dbResult = await ctx.model.BuserToken.findOne({ attributes, where });
result.code = dbResult ? dbResult.dataValus.user_uid : 401;
}
if (result.code !== 401 && result.code !== 403) {
const uidLastCode = this.shunt(result.code);
const model = {
"B": ctx.model.BShortMsg,
"C": ctx.model2.CShortMsg
};
const attributes = ['card_id', 'encode_type', 'msg_data', 'created_at'];
const where = { user_uid: result.code };
const order = [['created_at', 'DESC']];
if (limit == undefined || !this.isAllDigits(limit)) {
limit = 10
} else {
limit = parseInt(limit);
}
if (offset == undefined || !this.isAllDigits(offset)) {
offset = 1
} else {
offset = parseInt(offset);
}
offset = (offset - 1) * limit;
const dbOptions = { attributes, limit, offset, order, where };
const dbResult = await model[uidLastCode].findAndCountAll(dbOptions);
if (dbResult.rows.length !== 0) {
result.userData = {
rows: dbResult.rows,
count: dbResult.count
};
result.code = 200;
} else {
result.code = false;
}
}
return result.code ? result : false;
} catch (error) {
const blockName = fileName + "--" + "getHistory";
ctx.app.middleware.log('../../serviceLog', blockName + ">>>>" + error);
return 'error';
}
}
}
module.exports = TeachingService;