- Introduced a pause mechanism at the end of scrolling for both Ladder and Ranking boards, allowing the last item to be fully visible before triggering a data refresh. - Updated scrolling logic to prevent immediate resets, improving user experience during data loading. - Added a consistent pause duration across components to standardize behavior during scrolling interactions.
745 lines
22 KiB
JavaScript
745 lines
22 KiB
JavaScript
const jwt = require("jsonwebtoken");
|
||
const axios = require("axios");
|
||
const crypto = require("crypto");
|
||
const QRCode = require("qrcode");
|
||
const { User, LadderUser, Store, Match, MatchGame, PointRecord } = require("../models");
|
||
const {
|
||
generateMemberCode,
|
||
success,
|
||
error,
|
||
calculateDistance,
|
||
formatDateTime,
|
||
getFullUrl,
|
||
normalizeAvatarUrl,
|
||
normalizeAvatarForClient,
|
||
} = require("../utils/helper");
|
||
const { LADDER_LEVEL_NAMES } = require("../config/constants");
|
||
const { Op } = require("sequelize");
|
||
|
||
class UserController {
|
||
// 微信登录(获取 session_key,用于后续手机号解密)
|
||
login = async (req, res) => {
|
||
try {
|
||
const { code } = req.body;
|
||
|
||
if (!code) {
|
||
return res.status(400).json(error("缺少登录code", 400));
|
||
}
|
||
|
||
// 获取微信openid和session_key
|
||
const wxRes = await axios.get(
|
||
"https://api.weixin.qq.com/sns/jscode2session",
|
||
{
|
||
params: {
|
||
appid: process.env.WX_APPID,
|
||
secret: process.env.WX_SECRET,
|
||
js_code: code,
|
||
grant_type: "authorization_code",
|
||
},
|
||
},
|
||
);
|
||
|
||
if (wxRes.data.errcode) {
|
||
return res
|
||
.status(400)
|
||
.json(error("微信登录失败: " + wxRes.data.errmsg, 400));
|
||
}
|
||
|
||
const { openid, unionid, session_key } = wxRes.data;
|
||
|
||
// 查找用户
|
||
let user = await User.findOne({ where: { openid } });
|
||
let isNewUser = !user;
|
||
const hasPhone = !!(user?.phone);
|
||
|
||
// 返回登录信息(包含session_key用于后续手机号解密)
|
||
const encryptedSessionKey = this.encryptSessionKey(session_key, openid);
|
||
|
||
const responseData = {
|
||
openid,
|
||
unionid,
|
||
sessionKey: encryptedSessionKey,
|
||
isNewUser,
|
||
hasPhone,
|
||
};
|
||
|
||
if (hasPhone && user) {
|
||
// 已有手机号的老用户:直接生成 token 登录,无需再次授权手机号
|
||
const token = jwt.sign(
|
||
{ userId: user.id },
|
||
process.env.JWT_SECRET,
|
||
{ expiresIn: process.env.JWT_EXPIRES_IN || "7d" }
|
||
);
|
||
const ladderUsers = await LadderUser.findAll({
|
||
where: { user_id: user.id, status: 1 },
|
||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||
});
|
||
responseData.token = token;
|
||
responseData.userInfo = {
|
||
id: user.id,
|
||
nickname: user.nickname,
|
||
avatar: normalizeAvatarForClient(user.avatar, req),
|
||
phone: user.phone,
|
||
gender: user.gender,
|
||
memberCode: user.member_code,
|
||
totalPoints: user.total_points,
|
||
ladderUsers: ladderUsers.map((lu) => ({
|
||
id: lu.id,
|
||
storeId: lu.store_id,
|
||
storeName: lu.store?.name,
|
||
realName: lu.real_name,
|
||
level: lu.level,
|
||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||
powerScore: lu.power_score,
|
||
})),
|
||
};
|
||
} else {
|
||
responseData.userInfo = user
|
||
? {
|
||
id: user.id,
|
||
nickname: user.nickname,
|
||
avatar: normalizeAvatarForClient(user.avatar, req),
|
||
phone: user.phone,
|
||
gender: user.gender,
|
||
memberCode: user.member_code,
|
||
totalPoints: user.total_points,
|
||
}
|
||
: null;
|
||
}
|
||
|
||
res.json(
|
||
success(
|
||
responseData,
|
||
isNewUser ? "请授权手机号完成注册" : "登录成功",
|
||
),
|
||
);
|
||
} catch (err) {
|
||
console.error("登录失败:", err);
|
||
res.status(500).json(error("登录失败"));
|
||
}
|
||
};
|
||
|
||
// 加密session_key
|
||
encryptSessionKey(sessionKey, openid) {
|
||
const key = crypto
|
||
.createHash("md5")
|
||
.update(process.env.JWT_SECRET + openid)
|
||
.digest();
|
||
const iv = Buffer.alloc(16, 0);
|
||
const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
|
||
let encrypted = cipher.update(sessionKey, "utf8", "base64");
|
||
encrypted += cipher.final("base64");
|
||
return encrypted;
|
||
}
|
||
|
||
// 解密session_key
|
||
decryptSessionKey(encryptedSessionKey, openid) {
|
||
const key = crypto
|
||
.createHash("md5")
|
||
.update(process.env.JWT_SECRET + openid)
|
||
.digest();
|
||
const iv = Buffer.alloc(16, 0);
|
||
const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
|
||
let decrypted = decipher.update(encryptedSessionKey, "base64", "utf8");
|
||
decrypted += decipher.final("utf8");
|
||
return decrypted;
|
||
}
|
||
|
||
// 手机号授权登录(解密手机号并完成注册/登录)
|
||
phoneLogin = async (req, res) => {
|
||
try {
|
||
const {
|
||
openid,
|
||
unionid,
|
||
sessionKey,
|
||
encryptedData,
|
||
iv,
|
||
nickname,
|
||
avatar,
|
||
gender,
|
||
} = req.body;
|
||
|
||
if (!openid || !sessionKey || !encryptedData || !iv) {
|
||
return res.status(400).json(error("参数不完整", 400));
|
||
}
|
||
|
||
// 解密session_key
|
||
let realSessionKey;
|
||
try {
|
||
realSessionKey = this.decryptSessionKey(sessionKey, openid);
|
||
} catch (e) {
|
||
return res.status(400).json(error("会话已过期,请重新登录", 400));
|
||
}
|
||
|
||
// 解密手机号
|
||
let phone;
|
||
try {
|
||
phone = this.decryptPhoneNumber(realSessionKey, encryptedData, iv);
|
||
} catch (e) {
|
||
console.error("手机号解密失败:", e);
|
||
return res.status(400).json(error("手机号解密失败,请重新授权", 400));
|
||
}
|
||
|
||
if (!phone) {
|
||
return res.status(400).json(error("获取手机号失败", 400));
|
||
}
|
||
|
||
const normalizedGender =
|
||
gender === undefined || gender === null || gender === ""
|
||
? undefined
|
||
: Number(gender);
|
||
if (
|
||
normalizedGender !== undefined &&
|
||
normalizedGender !== 1 &&
|
||
normalizedGender !== 2
|
||
) {
|
||
return res.status(400).json(error("性别参数无效", 400));
|
||
}
|
||
|
||
// 查找或创建用户
|
||
let user = await User.findOne({ where: { openid } });
|
||
|
||
if (!user) {
|
||
// 尝试通过手机号查找已存在的自动生成用户(可能由管理员后台生成)
|
||
// 只有当 openid 是自动生成的格式时才允许合并
|
||
const existingUser = await User.findOne({ where: { phone } });
|
||
|
||
if (existingUser && existingUser.openid.startsWith("AUTO_GEN_")) {
|
||
// 合并账号:更新 openid 为真实的微信 openid
|
||
console.log(
|
||
`合并账号: 将自动生成用户 ${existingUser.id} (${existingUser.openid}) 更新为微信用户 ${openid}`,
|
||
);
|
||
|
||
const updateData = {
|
||
openid,
|
||
unionid,
|
||
};
|
||
if (nickname) updateData.nickname = nickname;
|
||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||
if (
|
||
(existingUser.gender === 0 || existingUser.gender === null) &&
|
||
normalizedGender === undefined
|
||
) {
|
||
return res.status(400).json(error("性别必填", 400));
|
||
}
|
||
if (normalizedGender !== undefined)
|
||
updateData.gender = normalizedGender;
|
||
|
||
await existingUser.update(updateData);
|
||
if (nickname) {
|
||
await LadderUser.update(
|
||
{ real_name: nickname },
|
||
{ where: { user_id: existingUser.id } },
|
||
);
|
||
}
|
||
user = existingUser;
|
||
} else {
|
||
// 新用户注册
|
||
if (normalizedGender === undefined) {
|
||
return res.status(400).json(error("性别必填", 400));
|
||
}
|
||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||
const normalizedAvatar = avatar
|
||
? normalizeAvatarUrl(avatar, req)
|
||
: "";
|
||
user = await User.create({
|
||
openid,
|
||
unionid,
|
||
phone,
|
||
member_code: generateMemberCode(),
|
||
nickname: nickname || "新用户",
|
||
avatar: normalizedAvatar,
|
||
gender: normalizedGender,
|
||
status: 1,
|
||
});
|
||
|
||
// 关联已存在的天梯用户(通过手机号)
|
||
await this.linkLadderUsers(user.id, phone);
|
||
}
|
||
} else {
|
||
// 更新用户信息
|
||
const updateData = { phone };
|
||
if (nickname) updateData.nickname = nickname;
|
||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||
if (normalizedGender !== undefined)
|
||
updateData.gender = normalizedGender;
|
||
|
||
await user.update(updateData);
|
||
if (nickname) {
|
||
await LadderUser.update(
|
||
{ real_name: nickname },
|
||
{ where: { user_id: user.id } },
|
||
);
|
||
}
|
||
// 如果手机号变化,重新关联天梯用户
|
||
await this.linkLadderUsers(user.id, phone);
|
||
}
|
||
|
||
// 生成token
|
||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
||
expiresIn: process.env.JWT_EXPIRES_IN || "7d",
|
||
});
|
||
|
||
// 获取关联的天梯用户信息
|
||
const ladderUsers = await LadderUser.findAll({
|
||
where: { user_id: user.id, status: 1 },
|
||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||
});
|
||
|
||
res.json(
|
||
success(
|
||
{
|
||
token,
|
||
userInfo: {
|
||
id: user.id,
|
||
nickname: user.nickname,
|
||
avatar: normalizeAvatarForClient(user.avatar, req),
|
||
phone: user.phone,
|
||
gender: user.gender,
|
||
memberCode: user.member_code,
|
||
totalPoints: user.total_points,
|
||
ladderUsers: ladderUsers.map((lu) => ({
|
||
id: lu.id,
|
||
storeId: lu.store_id,
|
||
storeName: lu.store?.name,
|
||
realName: lu.real_name,
|
||
level: lu.level,
|
||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||
powerScore: lu.power_score,
|
||
})),
|
||
},
|
||
},
|
||
"登录成功",
|
||
),
|
||
);
|
||
} catch (err) {
|
||
console.error("手机号登录失败:", err);
|
||
res.status(500).json(error("登录失败"));
|
||
}
|
||
};
|
||
|
||
// 解密微信手机号
|
||
decryptPhoneNumber(sessionKey, encryptedData, iv) {
|
||
const sessionKeyBuffer = Buffer.from(sessionKey, "base64");
|
||
const encryptedDataBuffer = Buffer.from(encryptedData, "base64");
|
||
const ivBuffer = Buffer.from(iv, "base64");
|
||
|
||
const decipher = crypto.createDecipheriv(
|
||
"aes-128-cbc",
|
||
sessionKeyBuffer,
|
||
ivBuffer,
|
||
);
|
||
decipher.setAutoPadding(true);
|
||
|
||
let decoded = decipher.update(encryptedDataBuffer, "binary", "utf8");
|
||
decoded += decipher.final("utf8");
|
||
|
||
const result = JSON.parse(decoded);
|
||
return result.phoneNumber || result.purePhoneNumber;
|
||
}
|
||
|
||
// 关联天梯用户(通过手机号)
|
||
async linkLadderUsers(userId, phone) {
|
||
// 查找所有未关联的天梯用户(相同手机号)
|
||
const unlinkedLadderUsers = await LadderUser.findAll({
|
||
where: {
|
||
phone,
|
||
user_id: null,
|
||
status: 1,
|
||
},
|
||
});
|
||
|
||
// 批量更新关联
|
||
if (unlinkedLadderUsers.length > 0) {
|
||
await LadderUser.update(
|
||
{ user_id: userId },
|
||
{ where: { phone, user_id: null } },
|
||
);
|
||
console.log(
|
||
`已关联 ${unlinkedLadderUsers.length} 个天梯用户到用户 ${userId}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 更新用户资料(头像、昵称)
|
||
updateProfile = async (req, res) => {
|
||
try {
|
||
const { nickname, avatar, gender } = req.body;
|
||
const user = req.user;
|
||
|
||
const updateData = {};
|
||
if (nickname) updateData.nickname = nickname;
|
||
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||
if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req);
|
||
if (gender !== undefined) {
|
||
const normalizedGender = Number(gender);
|
||
if (normalizedGender !== 1 && normalizedGender !== 2) {
|
||
return res.status(400).json(error("性别参数无效", 400));
|
||
}
|
||
updateData.gender = normalizedGender;
|
||
}
|
||
|
||
await user.update(updateData);
|
||
|
||
// 用户修改昵称时,同步更新该用户下所有天梯用户的 real_name,保持两边一致
|
||
if (nickname) {
|
||
await LadderUser.update(
|
||
{ real_name: nickname },
|
||
{ where: { user_id: user.id } },
|
||
);
|
||
}
|
||
|
||
res.json(
|
||
success(
|
||
{
|
||
nickname: user.nickname,
|
||
avatar: normalizeAvatarForClient(user.avatar, req),
|
||
gender: user.gender,
|
||
},
|
||
"更新成功",
|
||
),
|
||
);
|
||
} catch (err) {
|
||
console.error("更新资料失败:", err);
|
||
res.status(500).json(error("更新失败"));
|
||
}
|
||
};
|
||
|
||
// 获取用户信息(支持 store_id 查询参数,返回该门店的积分)
|
||
getInfo = async (req, res) => {
|
||
try {
|
||
const user = req.user;
|
||
const { store_id: storeId } = req.query;
|
||
|
||
// 获取天梯信息
|
||
const ladderUsers = await LadderUser.findAll({
|
||
where: { user_id: user.id, status: 1 },
|
||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||
});
|
||
|
||
// 按门店的积分:从 PointRecord 汇总该门店的积分变动
|
||
let storePoints = null;
|
||
if (storeId) {
|
||
const sumResult = await PointRecord.sum("points", {
|
||
where: {
|
||
user_id: user.id,
|
||
store_id: storeId,
|
||
},
|
||
});
|
||
storePoints = Number(sumResult) || 0;
|
||
}
|
||
|
||
const result = {
|
||
id: user.id,
|
||
nickname: user.nickname,
|
||
avatar: normalizeAvatarForClient(user.avatar, req),
|
||
phone: user.phone,
|
||
gender: user.gender,
|
||
memberCode: user.member_code,
|
||
totalPoints: user.total_points,
|
||
ladderUsers: ladderUsers.map((lu) => ({
|
||
id: lu.id,
|
||
storeId: lu.store_id,
|
||
storeName: lu.store?.name,
|
||
realName: lu.real_name,
|
||
level: lu.level,
|
||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||
powerScore: lu.power_score,
|
||
matchCount: lu.match_count,
|
||
winCount: lu.win_count,
|
||
})),
|
||
};
|
||
if (storePoints !== null) {
|
||
result.storePoints = storePoints;
|
||
}
|
||
res.json(success(result));
|
||
} catch (err) {
|
||
console.error("获取用户信息失败:", err);
|
||
res.status(500).json(error("获取用户信息失败"));
|
||
}
|
||
};
|
||
|
||
// 更新用户信息
|
||
updateInfo = async (req, res) => {
|
||
try {
|
||
const { nickname, avatar, phone, gender } = req.body;
|
||
const user = req.user;
|
||
|
||
const newNickname = nickname || user.nickname;
|
||
await user.update({
|
||
nickname: newNickname,
|
||
avatar: avatar || user.avatar,
|
||
phone: phone || user.phone,
|
||
gender: gender !== undefined ? gender : user.gender,
|
||
});
|
||
|
||
if (nickname) {
|
||
await LadderUser.update(
|
||
{ real_name: nickname },
|
||
{ where: { user_id: user.id } },
|
||
);
|
||
}
|
||
|
||
res.json(success(null, "更新成功"));
|
||
} catch (err) {
|
||
console.error("更新用户信息失败:", err);
|
||
res.status(500).json(error("更新失败"));
|
||
}
|
||
};
|
||
|
||
// 获取会员码
|
||
getMemberCode = async (req, res) => {
|
||
try {
|
||
res.json(
|
||
success({
|
||
memberCode: req.user.member_code,
|
||
}),
|
||
);
|
||
} catch (err) {
|
||
console.error("获取会员码失败:", err);
|
||
res.status(500).json(error("获取失败"));
|
||
}
|
||
};
|
||
|
||
// 通过会员码查询用户
|
||
getByMemberCode = async (req, res) => {
|
||
try {
|
||
const { code } = req.params;
|
||
const { store_id } = req.query;
|
||
|
||
const user = await User.findOne({
|
||
where: { member_code: code, status: 1 },
|
||
});
|
||
|
||
if (!user) {
|
||
return res.status(404).json(error("用户不存在", 404));
|
||
}
|
||
|
||
// 获取该门店的天梯信息
|
||
let ladderUser = null;
|
||
if (store_id) {
|
||
ladderUser = await LadderUser.findOne({
|
||
where: { user_id: user.id, store_id, status: 1 },
|
||
});
|
||
}
|
||
|
||
res.json(
|
||
success({
|
||
id: user.id,
|
||
nickname: user.nickname,
|
||
avatar: user.avatar,
|
||
gender: user.gender,
|
||
ladderUser: ladderUser
|
||
? {
|
||
id: ladderUser.id,
|
||
realName: ladderUser.real_name,
|
||
level: ladderUser.level,
|
||
powerScore: ladderUser.power_score,
|
||
}
|
||
: null,
|
||
}),
|
||
);
|
||
} catch (err) {
|
||
console.error("查询用户失败:", err);
|
||
res.status(500).json(error("查询失败"));
|
||
}
|
||
};
|
||
|
||
// 获取用户天梯信息
|
||
getLadderInfo = async (req, res) => {
|
||
try {
|
||
const { store_id } = req.query;
|
||
const user = req.user;
|
||
|
||
const where = { user_id: user.id, status: 1 };
|
||
if (store_id) {
|
||
where.store_id = store_id;
|
||
}
|
||
|
||
const ladderUsers = await LadderUser.findAll({
|
||
where,
|
||
include: [{ model: Store, as: "store", attributes: ["id", "name"] }],
|
||
});
|
||
|
||
res.json(
|
||
success(
|
||
ladderUsers.map((lu) => ({
|
||
id: lu.id,
|
||
storeId: lu.store_id,
|
||
storeName: lu.store?.name,
|
||
realName: lu.real_name,
|
||
gender: lu.gender,
|
||
level: lu.level,
|
||
levelName: LADDER_LEVEL_NAMES[lu.level] || `Lv${lu.level}`,
|
||
powerScore: lu.power_score,
|
||
matchCount: lu.match_count,
|
||
winCount: lu.win_count,
|
||
monthlyMatchCount: lu.monthly_match_count,
|
||
lastMatchTime: formatDateTime(lu.last_match_time),
|
||
})),
|
||
),
|
||
);
|
||
} catch (err) {
|
||
console.error("获取天梯信息失败:", err);
|
||
res.status(500).json(error("获取失败"));
|
||
}
|
||
};
|
||
|
||
// 获取当前门店
|
||
getCurrentStore = async (req, res) => {
|
||
try {
|
||
const { latitude, longitude } = req.query;
|
||
const user = req.user;
|
||
|
||
// 获取用户参与天梯的门店
|
||
const ladderUsers = await LadderUser.findAll({
|
||
where: { user_id: user.id, status: 1 },
|
||
include: [{ model: Store, as: "store" }],
|
||
order: [["last_match_time", "DESC"]],
|
||
});
|
||
|
||
if (ladderUsers.length > 0) {
|
||
// 有参与天梯的门店,返回最近参与比赛的门店
|
||
const lu = ladderUsers[0];
|
||
return res.json(
|
||
success({
|
||
storeId: lu.store_id,
|
||
storeName: lu.store?.name,
|
||
storeAddress: lu.store?.address,
|
||
ladderUserId: lu.id,
|
||
source: "last_match",
|
||
}),
|
||
);
|
||
}
|
||
|
||
// 没有天梯门店,返回最近的门店
|
||
if (latitude && longitude) {
|
||
const stores = await Store.findAll({
|
||
where: { status: 1 },
|
||
});
|
||
|
||
if (stores.length > 0) {
|
||
let nearestStore = stores[0];
|
||
let minDistance = Infinity;
|
||
|
||
for (const store of stores) {
|
||
if (store.latitude && store.longitude) {
|
||
const dist = calculateDistance(
|
||
parseFloat(latitude),
|
||
parseFloat(longitude),
|
||
parseFloat(store.latitude),
|
||
parseFloat(store.longitude),
|
||
);
|
||
if (dist < minDistance) {
|
||
minDistance = dist;
|
||
nearestStore = store;
|
||
}
|
||
}
|
||
}
|
||
|
||
return res.json(
|
||
success({
|
||
storeId: nearestStore.id,
|
||
storeName: nearestStore.name,
|
||
storeAddress: nearestStore.address,
|
||
ladderUserId: null,
|
||
source: "nearest",
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 返回第一个门店
|
||
const firstStore = await Store.findOne({ where: { status: 1 } });
|
||
res.json(
|
||
success(
|
||
firstStore
|
||
? {
|
||
storeId: firstStore.id,
|
||
storeName: firstStore.name,
|
||
storeAddress: firstStore.address,
|
||
ladderUserId: null,
|
||
source: "default",
|
||
}
|
||
: null,
|
||
),
|
||
);
|
||
} catch (err) {
|
||
console.error("获取当前门店失败:", err);
|
||
res.status(500).json(error("获取失败"));
|
||
}
|
||
};
|
||
|
||
// 生成会员二维码图片
|
||
getQrcode = async (req, res) => {
|
||
try {
|
||
const user = req.user;
|
||
|
||
if (!user.member_code) {
|
||
return res.status(400).json(error("会员码不存在", 400));
|
||
}
|
||
|
||
// 生成二维码配置
|
||
const qrOptions = {
|
||
errorCorrectionLevel: "M",
|
||
type: "image/png",
|
||
margin: 2,
|
||
width: 300,
|
||
color: {
|
||
dark: "#1A1A1A",
|
||
light: "#FFFFFF",
|
||
},
|
||
};
|
||
|
||
// 生成二维码为 base64
|
||
const qrcodeDataUrl = await QRCode.toDataURL(user.member_code, qrOptions);
|
||
|
||
res.json(
|
||
success({
|
||
memberCode: user.member_code,
|
||
qrcode: qrcodeDataUrl,
|
||
}),
|
||
);
|
||
} catch (err) {
|
||
console.error("生成二维码失败:", err);
|
||
res.status(500).json(error("生成二维码失败"));
|
||
}
|
||
};
|
||
|
||
// 直接返回二维码图片
|
||
getQrcodeImage = async (req, res) => {
|
||
try {
|
||
const user = req.user;
|
||
|
||
if (!user.member_code) {
|
||
return res.status(400).send("会员码不存在");
|
||
}
|
||
|
||
// 生成二维码配置
|
||
const qrOptions = {
|
||
errorCorrectionLevel: "M",
|
||
type: "png",
|
||
margin: 2,
|
||
width: 300,
|
||
color: {
|
||
dark: "#1A1A1A",
|
||
light: "#FFFFFF",
|
||
},
|
||
};
|
||
|
||
// 设置响应头
|
||
res.setHeader("Content-Type", "image/png");
|
||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||
|
||
// 直接输出二维码图片
|
||
await QRCode.toFileStream(res, user.member_code, qrOptions);
|
||
} catch (err) {
|
||
console.error("生成二维码图片失败:", err);
|
||
res.status(500).send("生成二维码失败");
|
||
}
|
||
};
|
||
}
|
||
|
||
module.exports = new UserController();
|