# 項目主體搭建 - 前端:`vue3`、`element-plus`、`ts`、`axios`、`vue-router`、`pinia` - 後端:`nodejs`、`koa`、`koa-router`、`koa-body`、`jsonwebtoken` - 部署:`nginx`、`pm2`、`x ...
項目主體搭建
- 前端:
vue3
、element-plus
、ts
、axios
、vue-router
、pinia
- 後端:
nodejs
、koa
、koa-router
、koa-body
、jsonwebtoken
- 部署:
nginx
、pm2
、xshell
、騰訊雲自帶寶塔面板 - 資料庫:
mysql
、redis
- 開發軟體:
vs code
、Another Redis Desktop Manager
、Navicat Premium 15
後端主要使用的依賴包
dotenv
: 將環境變數中的變數從.env
文件載入到process.env
中jsonwebtoken
: 頒發token,不會緩存到mysql
和redis
中koa
: 快速搭建後臺服務的框架koa-body
: 解析前端傳來的參數,並將參數掛到ctx.request.body
上koa-router
: 路由中間件,處理不同url路徑的請求koa2-cors
: 處理跨域請求的中間件mysql2
: 在nodejs中連接和操作mysql資料庫,mysql
也可以,不過要自己封裝連接資料庫redis
: 在nodejs中操作redis的庫,通常用作持久化token、點贊等功能sequelize
: 基於promise的orm(對象關係映射)庫,不用寫sql語句,更方便的操作資料庫nodemon
: 自動重啟服務sequelize-automete
: 自動化為sequelize生成模型
文件劃分
常量文件配置
- 創建
.env.development
和config
文件夾,配置如下
# 資料庫ip地址
APP_HOST = 1.15.42.9
# 服務監聽埠
APP_PORT = 40001
# 資料庫名
APP_DATA_BASE = test
# 用戶名
APP_USERNAME = test
# 密碼
APP_PASSWORD = 123456
# redis地址
APP_REDIS_HOST = 1.15.42.9
# redis埠
APP_REDIS_PORT = 6379
# redis密碼
APP_REDIS_PASSWORD = 123456
# redis倉庫
APP_REDIS_DB = 15
# websocket尾碼
APP_BASE_PATH = /
# token標識
APP_JWT_SECRET = LOVE-TOKEN
# 保存文件的絕對路徑
APP_FILE_PATH = ""
# 網路url地址
APP_NETWORK_PATH = blob:http://192.168.10.20:40001/
然後在config
文件中將.env
的配置暴露出去
const dotenv = require("dotenv");
const path = process.env.NODE_ENV
? ".env.development"
: ".env.production.local";
dotenv.config({ path });
module.exports = process.env;
2.在最外層創建app.js
文件
const Koa = require("koa");
const http = require("http");
const cors = require("koa2-cors");
const WebSocket = require("ws");
const { koaBody } = require("koa-body");
const { APP_PORT, APP_BASE_PATH } = require("./src/config/index");
const router = require("./src/router/index");
const seq = require("./src/mysql/sequelize");
const PersonModel = require("./src/models/person");
const Mperson = PersonModel(seq);
// 創建一個Koa對象
const app = new Koa();
const server = http.createServer(app.callback());
// 同時需要在nginx配置/ws
const wss = new WebSocket.Server({ server, path: APP_BASE_PATH }); // 同一埠監聽不同的服務
// 使用了代理
app.proxy = true;
// 處理跨域
app.use(cors());
// 解析請求體(也可以使用koa-body)
app.use(
koaBody({
multipart: true,
// textLimit: "1mb", // 限制text body的大小,預設56kb
formLimit: "10mb", // 限製表單請求體的大小,預設56kb,前端報錯413
// encoding: "gzip", // 前端報415
formidable: {
// uploadDir: path.join(__dirname, "./static/"), // 設置文件上傳目錄
keepExtensions: true, // 保持文件的尾碼
// 最大文件上傳大小為512MB(如果使用反向代理nginx,需要設置client_max_body_size)
maxFieldsSize: 512 * 1024 * 1024,
},
})
);
app.use(router.routes());
wss.on("connection", function (ws) {
let messageIndex = 0;
ws.on("message", async function (data, isBinary) {
console.log(data);
const message = isBinary ? data : data.toString();
if (JSON.parse(message).type !== "personData") {
return;
}
const result = await Mperson.findAll();
wss.clients.forEach((client) => {
messageIndex++;
client.send(JSON.stringify(result));
});
});
ws.onmessage = (msg) => {
ws.send(JSON.stringify({ isUpdate: false, message: "pong" }));
};
ws.onclose = () => {
console.log("服務連接關閉");
};
ws.send(JSON.stringify({ isUpdate: false, message: "首次建立連接" }));
});
server.listen(APP_PORT, () => {
const host = process.env.APP_REDIS_HOST;
const port = process.env.APP_PORT;
console.log(
`環境:${
process.env.NODE_ENV ? "開發環境" : "生產環境"
},伺服器地址:http://${host}:${port}/findExcerpt`
);
});
module.exports = server;
這裡可以先不引入websocket
和Mperson
,這是後續發佈內容時才會用到。
註:app.use(cors())
必須在app.use(router.routes())
之前,不然訪問路由時會顯示跨域。
3.在package.json
中添加命令"dev": "set NODE_ENV=development && nodemon app.js"
,
然後就可以直接運行npm run dev
啟動服務了
mysql創建資料庫建表
在伺服器上開放mysql
埠3306,還有接下來使用到的redis
埠6379。
使用root連接資料庫,可以看到所有的資料庫。
註:
sequelize
6版本最低支持mysql5.7
,雖然不會報錯,但是有提示
mysql預設情況下不允許root直接連接,需要手動放開
- 進入mysql:
mysql -uroot -p
- 使用mysql:
use mysql;
- 授權給所有ip:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456(密碼)' WITH GRANT OPTION;
- 刷新許可權:
FLUSH PRIVILEGES;
redis使用密碼遠程連接後,就不需要在本地安裝redis
redis預設也是不允許遠程連接,需要手動放開。
-
使用
find / -name redis.conf
先找到redis配置文件- 將
bind 127.0.0.1
修改為bind 0.0.0.0
; - 設置
protected-mode no
; - 設置密碼
requirepass 123456
密碼123456替換為自己的。
- 將
-
進入src使用
./redis-server ../redis.conf
重新啟動
使用sequelize-automate
自動生成表模型
創建文件sequelize-automate.config.js
,然後在package.json
增加一條命令"automate": "sequelize-automate -c './sequelize-automate.config.js'"
,使用npm run automate
自動生成表模型。
註:建議自己維護
createdAt
和updatedAt
,因為在Navicat Premium 15
上創建表後,自動生成的模型雖然會自動增加createdAt
和updatedAt
,但是不會同步到資料庫上,需要刪除資料庫中的表,然後再重新啟動服務才會同步,但是當id為主鍵且不為null時,模板會生成defaultValue為null,在個別表中會報錯
const Automate = require("sequelize-automate");
const dbOptions = {
database: "test",
username: "test",
password: "123456",
dialect: "mysql",
host: "1.15.42.9",
port: 3306,
logging: false,
define: {
underscored: false,
freezeTableName: false,
charset: "utf8mb4",
timezone: "+00:00",
dialectOptions: {
collate: "utf8_general_ci",
},
timestamps: true, // 自動創建createAt和updateAt
},
};
const options = {
type: "js",
dir: "./src/models",
camelCase: true,
};
const automate = new Automate(dbOptions, options);
(async function main() {
const code = await automate.run();
console.log(code);
})();
連接mysql和redis
mysql連接,timezone預設是世界時區, "+08:00"指標準時間加8個小時,也就是北京時間
const { Sequelize } = require("sequelize");
const { APP_DATA_BASE, APP_USERNAME, APP_PASSWORD, APP_DATA_HOST } = require("../config/index");
const seq = new Sequelize(APP_DATA_BASE, APP_USERNAME, APP_PASSWORD, {
host: APP_DATA_HOST,
dialect: "mysql",
define: {
timestamps: true,
},
timezone: "+08:00"
});
seq
.authenticate()
.then(() => {
console.log("資料庫連接成功");
})
.catch((err) => {
console.log("資料庫連接失敗", err);
});
seq.sync();
// 強制同步資料庫,會先刪除表,然後創建 seq.sync({ force: true });
module.exports = seq;
redis連接,database不同環境使用不同的分片
const Redis = require("redis");
const {
APP_REDIS_HOST,
APP_REDIS_PORT,
APP_REDIS_PASSWORD,
APP_REDIS_DB
} = require("../config");
const client = Redis.createClient({
url: "redis://" + APP_REDIS_HOST + ":" + APP_REDIS_PORT,
host: APP_REDIS_HOST,
port: APP_REDIS_PORT,
password: APP_REDIS_PASSWORD,
database: APP_REDIS_DB,
});
client.connect();
client.on("connect", () => {
console.log("Redis連接成功!");
});
// 連接錯誤處理
client.on("error", (err) => {
console.error(err);
});
// client.set("age", 1);
module.exports = client;
一切都沒問題後,現在開始編寫路由代碼,也就是一個個介面
編寫登錄註冊
在controllers
新建user.js
模塊,具體邏輯:登錄和註冊是同一個介面,前端提交時,會先判斷這個用戶是否存在,不存在會將傳來的密碼進行加密,然後將用戶信息存入到資料庫,同時使jsonwebtoken
頒發token,token本身是無狀態的,也就是說,在時間到期之前不會銷毀!這裡可以在服務端維護一個令牌黑名單,用於退出登錄。
const response = require("../utils/resData");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { APP_JWT_SECRET } = require("../config/index");
const seq = require("../mysql/sequelize");
const UserModel = require("../models/user");
const Muser = UserModel(seq);
// 類定義
class User {
constructor() {}
// 註冊用戶
async register(ctx, next) {
try {
const { userName: user_name, password: pass_word } = ctx.request.body;
if (!user_name || !pass_word) {
ctx.body = response.ERROR("userNotNull");
return;
}
// 判斷用戶是否存在
const isExist = await Muser.findOne({
where: {
user_name: user_name,
},
});
if (isExist) {
const res = await Muser.findOne({
where: {
user_name: user_name,
},
});
// 密碼是否正確
if (bcrypt.compareSync(pass_word, res.dataValues.password)) {
// 登錄成功
const { password, ...data } = res.dataValues;
ctx.body = response.SUCCESS("userLogin", {
token: jwt.sign(data, APP_JWT_SECRET, { expiresIn: "30d" }),
userInfo: res.dataValues,
});
} else {
ctx.body = response.ERROR("userAlreadyExist");
}
} else {
// 加密
const salt = bcrypt.genSaltSync(10);
// hash保存的是 密文
const hash = bcrypt.hashSync(pass_word, salt);
const userInfo = await Muser.create({ user_name, password: hash });
const { password, ...data } = userInfo.dataValues;
ctx.body = response.SUCCESS("userRegister", {
token: jwt.sign(data, APP_JWT_SECRET, { expiresIn: "30d" }),
userInfo,
});
}
} catch (error) {
console.error(error);
ctx.body = response.SERVER_ERROR();
}
}
}
module.exports = new User();
增加校驗用戶是否登錄的中間件
在middleware
創建index.js
,將用戶信息掛載到ctx.state.user上,方便後續別的地方需要用到用戶信息
const jwt = require("jsonwebtoken");
const { APP_JWT_SECRET } = require("../config/index");
const response = require("../utils/resData");
// 上傳文件時,如果存在token,將token添加到state,方便後面使用,沒有或者失效,則返回null
const auth = async (ctx, next) => {
// 會返回小寫secret
const token = ctx.request.header["love-token"];
if (token) {
try {
const user = jwt.verify(token, APP_JWT_SECRET);
// 在已經頒發token介面上面添加user對象,便於使用
ctx.state.user = user;
} catch (error) {
ctx.state.user = null;
console.log(error.name);
if (error.name === "TokenExpiredError") {
ctx.body = response.ERROR("tokenExpired");
} else if (error.name === "JsonWebTokenError") {
ctx.body = response.ERROR("tokenInvaild");
} else {
ctx.body = response.ERROR("unknownError");
}
return;
}
}
await next();
};
module.exports = {
auth,
};
增加路由頁面
將controllers
所有文件都導入到index.js
中,然後再暴露出去,這樣在router文件就只需要引入controllers/index.js
即可
const hotword = require("./hotword");
const issue = require("./issue");
const person = require("./person");
const user = require("./user");
const common = require("./common");
const wallpaper = require("./wallpaper");
const fileList = require("./fileList");
const ips = require("./ips");
module.exports = {
hotword,
issue,
person,
user,
common,
wallpaper,
fileList,
ips,
};
const router = require("koa-router")();
const {
hotword,
person,
issue,
common,
user,
wallpaper,
fileList,
ips,
} = require("../controllers/index");
const { auth } = require("../middleware/index");
// router.get("/", async (ctx) => {
// ctx.body = "歡迎訪問該介面";
// });
router.get("/wy/find", hotword.findHotword);
router.get("/wy/pageQuery", hotword.findPageHotword);
// 登錄才能刪除,修改評論(協商緩存)
router.get("/findExcerpt", person.findExcerpt);
router.get("/addExcerpt", person.addExcerpt);
router.get("/updateExcerpt", auth, person.updateExcerpt);
router.get("/delExcerpt", auth, person.delExcerpt);
// 不走緩存
router.post("/findIssue", issue.findIssue);
router.post("/addIssue", issue.addIssue);
router.post("/delIssue", issue.delIssue);
router.post("/editIssue", issue.editIssue);
router.post("/register/user", user.register);
router.post("/upload/file", common.uploadFile);
router.post("/paste/upload/file", common.pasteUploadFile);
// router.get("/download/file/:name", common.downloadFile);
// 強緩存
router.get("/wallpaper/findList", wallpaper.findPageWallpaper);
// 文件列表
router.post("/file/list", auth, fileList.findFileLsit);
router.post("/save/list", auth, fileList.saveFileInfo);
router.post("/delete/file", auth, fileList.delFile);
// ip
router.post("/find/ipList", (ctx, next) => ips.findIpsList(ctx, next));
module.exports = router;
註:需要用戶登錄的介面在路由前加
auth
中間件即可
獲取ip、上傳、下載
獲取ip
獲取ip可以單獨提取出來,當作中間件,這裡當用戶訪問我的ip地址頁面時,會自動在資料庫中添加記錄,同時使用redis存儲當前ip,10分鐘內再次訪問不會再增加。
註:當服務線上上使用nginx反向代理時,需要在
app.js
添加app.proxy = true
,同時需要配置nginx來獲取用戶真實ip
const response = require("../utils/resData");
const { getClientIP } = require("../utils/common");
const seq = require("../mysql/sequelize");
const IpsModel = require("../models/ips");
const MIps = IpsModel(seq);
const client = require("../utils/redis");
const { reqIp } = require("../api/ip");
class Ips {
constructor() {}
async findIpsList(ctx, next) {
try {
if (!process.env.NODE_ENV) {
await this.addIps(ctx, next);
}
const { size, page } = ctx.request.body;
const total = await MIps.findAndCountAll();
const data = await MIps.findAll({
order: [["id", "DESC"]],
limit: parseInt(size),
offset: parseInt(size) * (page - 1),
});
ctx.body = response.SUCCESS("common", { total: total.count, data });
} catch (error) {
console.error(error);
ctx.body = response.SERVER_ERROR();
}
}
async addIps(ctx, next) {
try {
const ip = getClientIP(ctx);
const res = await client.get(ip);
// 沒有才在redis中設置
if (!res) {
// 需要將值轉為字元串
await client.set(ip, new Date().toString(), {
EX: 10 * 60, // 以秒為單位存儲10分鐘
NX: true, // 鍵不存在時才進行set操作
});
if (ip.length > 6) {
const obj = {
id: Date.now(),
ip,
};
const info = await reqIp({ ip });
if (info.code === 200) {
obj.operator = info.ipdata.isp;
obj.address = info.adcode.o;
await MIps.create(obj);
} else {
console.log("ip介面請求失敗");
}
}
}
} catch (error) {
console.error(error);
}
await next();
}
}
module.exports = new Ips();
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Public-Network-URL http://$http_host$request_uri;
proxy_pass http://127.0.0.1:40001;
}
上傳
配置文件.env需要配置好絕對路徑:APP_FILE_PATH = /任意位置
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const response = require("../utils/resData");
const { APP_NETWORK_PATH, APP_FILE_PATH } = require("../config/index");
class Common {
constructor() {}
async uploadFile(ctx, next) {
try {
const { file } = ctx.request.files;
// 檢查文件夾是否存在,不存在則創建文件夾
if (!fs.existsSync(APP_FILE_PATH)) {
APP_FILE_PATH && fs.mkdirSync(APP_FILE_PATH);
}
// 上傳的文件具體地址
let filePath = path.join(
APP_FILE_PATH || __dirname,
`${APP_FILE_PATH ? "./" : "../static/"}${file.originalFilename}`
);
// 創建可讀流(預設一次讀64kb)
const reader = fs.createReadStream(file.filepath);
// 創建可寫流
const upStream = fs.createWriteStream(filePath);
// 可讀流通過管道寫入可寫流
reader.pipe(upStream);
const fileInfo = {
id: Date.now(),
url: APP_NETWORK_PATH + file.originalFilename,
name: file.originalFilename,
size: file.size,
type: file.originalFilename.match(/[^.]+$/)[0],
};
ctx.body = response.SUCCESS("common", fileInfo);
} catch (error) {
console.error(error);
ctx.body = response.ERROR("upload");
}
}
async pasteUploadFile(ctx, next) {
try {
const { file } = ctx.request.body;
const dataBuffer = Buffer.from(file, "base64");
// 生成隨機40個字元的hash
const hash = crypto.randomBytes(20).toString("hex");
// 文件名
const filename = hash + ".png";
let filePath = path.join(
APP_FILE_PATH || __dirname,
`${APP_FILE_PATH ? "./" : "../static/"}${filename}`
);
// 以寫入模式打開文件,文件不存在則創建
const fd = fs.openSync(filePath, "w");
// 寫入
fs.writeSync(fd, dataBuffer);
// 關閉
fs.closeSync(fd);
const fileInfo = {
id: Date.now(),
url: APP_NETWORK_PATH + filename,
name: filename,
size: file.size || "",
type: 'png',
};
ctx.body = response.SUCCESS("common", fileInfo);
} catch (error) {
console.error(error);
ctx.body = response.ERROR("upload");
}
}
}
module.exports = new Common();
下載
直接配置nginx即可,當客戶端請求路徑以 /static
開頭的靜態資源時,nginx 會從指定的文件系統路徑 /www/wwwroot/note.loveverse.top/static
中獲取相應的文件。用於將 /static
路徑下的靜態資源標記為下載類型,用戶訪問這些資源時告訴瀏覽器下載文件
location /static {
add_header Content-Type application/x-download;
alias /www/wwwroot/note.loveverse.top/static;
}
小程式登錄和公眾號驗證
小程式
在.env
中新增如配置,在middleware/index.js
中增加refresh
中間件
# 微信公眾號appID
APP_ID = wx862588761a1a5465
# 微信公眾號appSecret
APP_SECRET = a06938aae54d2f72a41e4710854354534
# 微信公眾號token
APP_TOKEN = 543543
# 微信小程式appID
APP_MINI_ID = wx663ca454434243
# 微信小程式密鑰
APP_MINI_SECRET = ee17b15f95fcd597243243432432
const refresh = async (ctx, next) => {
// 將openId掛載到ids,供全局使用
const token = ctx.request.header["mini-love-token"];
if (token) {
try {
// -1說明沒有設置過期時間,-2表示不存在該鍵
const ttl = await client.ttl(token);
if (ttl === -2) {
ctx.body = response.WARN("token已失效,請重新登錄", 401);
return;
}
// 過期時間小於一個月,增加過期時間
if (ttl < 30 * 24 * 60 * 60) {
await client.expire(token, 60 * 24 * 60 * 60);
}
// 掛載openid
const ids = await client.get(token);
const info = ids.split(":");
ctx.state.ids = {
sessionId: info[0],
openId: info[1],
};
} catch (error) {
ctx.state.ids = null;
console.error(error);
ctx.body = response.ERROR("redis獲取token失敗");
return;
}
} else {
ctx.body = response.WARN("請先進行登錄", 401);
return;
}
await next();
};
公眾號驗證
安裝xml2js
,將包含 XML 數據的字元串解析為 JavaScript 對象傳給公眾號。在utils/constant.js
添加常量,然後編寫響應公眾號的介面chat.js
。這裡公眾號調試難免會需要將開發環境映射到公網,這裡使用xshell隧道做內網穿透
const template = `
<xml>
<ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>
<FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>
<CreateTime><%=createTime%></CreateTime>
<MsgType><![CDATA[<%=msgType%>]]></MsgType>
<% if (msgType === 'news') { %>
<ArticleCount><%=content.length%></ArticleCount>
<Articles>
<% content.forEach(function(item){ %>
<item>
<Title><![CDATA[<%-item.title%>]]></Title>
<Description><![CDATA[<%-item.description%>]]></Description>
<PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic || item.thumb_url %>]]></PicUrl>
<Url><![CDATA[<%-item.url%>]]></Url>
</item>
<% }); %>
</Articles>
<% } else if (msgType === 'music') { %>
<Music>
<Title><![CDATA[<%-content.title%>]]></Title>
<Description><![CDATA[<%-content.description%>]]></Description>
<MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>
<HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>
</Music>
<% } else if (msgType === 'voice') { %>
<Voice>
<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
</Voice>
<% } else if (msgType === 'image') { %>
<Image>
<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
</Image>
<% } else if (msgType === 'video') { %>
<Video>
<MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
<Title><![CDATA[<%-content.title%>]]></Title>
<Description><![CDATA[<%-content.description%>]]></Description>
</Video>
<% } else { %>
<Content><![CDATA[<%-content%>]]></Content>
<% } %>
</xml>
`;
module.exports = { template };
const crypto = require("crypto");
const xml2js = require("xml2js");
const ejs = require("ejs");
const {
template
} = require("../utils/constant");
const { APP_ID, APP_SECRET, APP_TOKEN } = require("../config/index");
const response = require("../utils/resData");
const wechat = {
appID: APP_ID,
appSecret: APP_SECRET,
token: APP_TOKEN,
};
const compiled = ejs.compile(template);
function reply(content = "", fromUsername, toUsername) {
const info = {};
let type = "text";
info.content = content;
if (Array.isArray(content)) {
type = "news";
} else if (typeof content === "object") {
if (content.hasOwnProperty("type")) {
type = content.type;
info.content = content.content;
} else {
type = "music";
}
}
info.msgType = type;
info.createTime = new Date().getTime();
info.fromUsername = fromUsername;
info.toUsername = toUsername;
return compiled(info);
}
class Wechat {
constructor() {}
// 公眾號驗證
async verifyWechat(ctx, next) {
try {
let {
signature = "",
timestamp = "",
nonce = "",
echostr = "",
} = ctx.query;
const token = wechat.token;
// 驗證token
let str = [token, timestamp, nonce].sort().join("");
let sha1 = crypto.createHash("sha1").update(str).digest("hex");
if (sha1 !== signature) {
ctx.body = "token驗證失敗";
} else {
// 驗證成功
if (JSON.stringify(ctx.request.body) === "{}") {
ctx.body = echostr;
} else {
// 解析公眾號xml
let obj = await xml2js.parseStringPromise(ctx.request.body);
let xmlObj = {};
for (const item in obj.xml) {
xmlObj[item] = obj.xml[item][0];
}
console.info("[ xmlObj.Content ] >", xmlObj);
// 文本消息
if (xmlObj.MsgType === "text") {
const replyMessageXml = reply(
str,
xmlObj.ToUserName,
xmlObj.FromUserName
);
ctx.type = "application/xml";
ctx.body = replyMessageXml;
// 關註消息
} else if (xmlObj.MsgType === "event") {
if (xmlObj.Event === "subscribe") {
const str = msg.attendion;
const replyMessageXml = reply(
str,
xmlObj.ToUserName,
xmlObj.FromUserName
);
ctx.type = "application/xml";
ctx.body = replyMessageXml;
} else {
ctx.body = null;
}
// 其他消息
} else {
const str = msg.other;
const replyMessageXml = reply(
str,
xmlObj.ToUserName,
xmlObj.FromUserName
);
ctx.type = "application/xml";
ctx.body = replyMessageXml;
}
}
}
} catch (error) {
console.error(error);
// 返回500,公眾號會報錯
ctx.body = null;
}
}
}
module.exports = new Wechat();
註:公眾號和小程式是另一個項目,所以不在github上
調試
以腳本命令的方式運行,可以看到完整的數據列印,排查問題更方便
存在的問題
websocket
服務端沒封裝,應該單獨分出一個文件來寫。- ip地址記錄只有訪問ip列表這個頁面才會增加,一直停留在其他頁面無法獲取ip記錄
- pseron介面測試協商緩存不生效,返回304,但是瀏覽器還是顯示200
sequelize-automate
自動生成的createdAt
和updatedAt
沒有同步到資料庫中- 上傳文件緩慢
等等還有其他一些未列舉的問題
代碼鏈接
https://github.com/loveverse/love-blog
參考文章
https://github.com/jj112358/node-api(nodejs從0到1更加詳細版)
https://www.freesion.com/article/34551095837/(xshell內網穿透原理)