refactor: 项目架构重构 - 目录标准化 + 清理冗余
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/ - 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿) - 更新 package.json 元数据(移除 wdkj 命名) - 完善三级 .gitignore(根 + server + client) - 重写 README.md 和 CHANGELOG.md - 工具脚本移至 scripts/
This commit is contained in:
Executable
+492
@@ -0,0 +1,492 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User, Admin, AdminLog, LoginLog } = require('../models')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const weChatService = require('../utils/weixin')
|
||||
const logger = require('../utils/logger')
|
||||
const axios = require('axios')
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*/
|
||||
const getClientIp = (req) => {
|
||||
return req.headers['x-forwarded-for'] ||
|
||||
req.headers['x-real-ip'] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.socket.remoteAddress ||
|
||||
'0.0.0.0'
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析User-Agent
|
||||
*/
|
||||
const parseUserAgent = (userAgent) => {
|
||||
if (!userAgent) return { device: '', os: '', browser: '' }
|
||||
|
||||
const device = /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent) ? 'mobile' : 'desktop'
|
||||
|
||||
let os = ''
|
||||
if (/Windows/i.test(userAgent)) os = 'Windows'
|
||||
else if (/Mac OS X/i.test(userAgent)) os = 'MacOS'
|
||||
else if (/Android/i.test(userAgent)) os = 'Android'
|
||||
else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) os = 'iOS'
|
||||
else if (/Linux/i.test(userAgent)) os = 'Linux'
|
||||
else os = 'Unknown'
|
||||
|
||||
let browser = ''
|
||||
if (/MicroMessenger/i.test(userAgent)) browser = 'WeChat'
|
||||
else if (/Chrome/i.test(userAgent)) browser = 'Chrome'
|
||||
else if (/Safari/i.test(userAgent)) browser = 'Safari'
|
||||
else if (/Firefox/i.test(userAgent)) browser = 'Firefox'
|
||||
else if (/Edge/i.test(userAgent)) browser = 'Edge'
|
||||
else browser = 'Unknown'
|
||||
|
||||
return { device, os, browser }
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录日志
|
||||
*/
|
||||
const recordLoginLog = async (userId, openid, type, method, source, req, success = true, failReason = '') => {
|
||||
try {
|
||||
const ip = getClientIp(req)
|
||||
const userAgent = req.headers['user-agent'] || ''
|
||||
const { device, os, browser } = parseUserAgent(userAgent)
|
||||
|
||||
await LoginLog.create({
|
||||
userId,
|
||||
openid,
|
||||
type,
|
||||
method,
|
||||
source,
|
||||
ip,
|
||||
userAgent,
|
||||
device,
|
||||
os,
|
||||
browser,
|
||||
success,
|
||||
failReason
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('记录登录日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序登录
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { code, userInfo, source = 'miniapp' } = req.body
|
||||
|
||||
// 方式1: 通过 code 登录(推荐)
|
||||
let openid = null
|
||||
if (code) {
|
||||
const sessionData = await weChatService.code2Session(code)
|
||||
openid = sessionData.openid
|
||||
}
|
||||
|
||||
// 方式2: 开发环境直接使用 openid (已废弃,仅用于本地调试)
|
||||
if (!openid && process.env.NODE_ENV === 'development' && req.body.openid) {
|
||||
openid = req.body.openid
|
||||
}
|
||||
|
||||
if (!openid) {
|
||||
return ApiResponse.error(res, '无法获取用户标识', 400)
|
||||
}
|
||||
|
||||
// 查找或创建用户
|
||||
let user = await User.findOne({ openid })
|
||||
let isNewUser = false
|
||||
|
||||
if (!user) {
|
||||
// 新用户
|
||||
user = new User({
|
||||
openid,
|
||||
nickName: userInfo?.nickName || '星辰旅行者',
|
||||
avatarUrl: userInfo?.avatarUrl || '',
|
||||
unlockedDims: [1]
|
||||
})
|
||||
isNewUser = true
|
||||
logger.info(`新用户注册: ${openid}`)
|
||||
} else {
|
||||
// 老用户,更新登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
if (userInfo?.nickName) user.nickName = userInfo.nickName
|
||||
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
|
||||
}
|
||||
|
||||
await user.save()
|
||||
|
||||
// 记录登录日志
|
||||
await recordLoginLog(
|
||||
user._id,
|
||||
openid,
|
||||
isNewUser ? 'register' : 'login',
|
||||
'wechat_miniapp',
|
||||
source,
|
||||
req,
|
||||
true
|
||||
)
|
||||
|
||||
// 生成 JWT token(可选,小程序通常不需要)
|
||||
const token = jwt.sign(
|
||||
{ openid, userId: user._id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
openid,
|
||||
isNewUser,
|
||||
userInfo: {
|
||||
nickName: user.nickName,
|
||||
avatarUrl: user.avatarUrl
|
||||
},
|
||||
firstPlayDate: user.createdAt
|
||||
}, isNewUser ? '注册成功' : '登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* WebView 静默登录
|
||||
* POST /api/auth/webview-login
|
||||
* 用于微信小程序 webview 内嵌 H5 的自动登录
|
||||
*/
|
||||
router.post('/webview-login', async (req, res) => {
|
||||
try {
|
||||
const { code, userInfo } = req.body
|
||||
|
||||
if (!code) {
|
||||
return ApiResponse.error(res, '缺少登录凭证', 400)
|
||||
}
|
||||
|
||||
// 通过 code 获取 openid
|
||||
let openid = null
|
||||
try {
|
||||
const sessionData = await weChatService.code2Session(code)
|
||||
openid = sessionData.openid
|
||||
} catch (error) {
|
||||
logger.error('WebView登录获取session失败:', error)
|
||||
return ApiResponse.error(res, '登录凭证无效', 401)
|
||||
}
|
||||
|
||||
if (!openid) {
|
||||
return ApiResponse.error(res, '无法获取用户标识', 400)
|
||||
}
|
||||
|
||||
// 查找或创建用户
|
||||
let user = await User.findOne({ openid })
|
||||
let isNewUser = false
|
||||
|
||||
if (!user) {
|
||||
// 新用户 - 使用微信提供的信息创建账户
|
||||
user = new User({
|
||||
openid,
|
||||
nickName: userInfo?.nickName || '星辰旅行者',
|
||||
avatarUrl: userInfo?.avatarUrl || '',
|
||||
unlockedDims: [1]
|
||||
})
|
||||
isNewUser = true
|
||||
logger.info(`WebView新用户注册: ${openid}`)
|
||||
} else {
|
||||
// 老用户,更新登录时间和信息
|
||||
user.lastLoginAt = new Date()
|
||||
if (userInfo?.nickName) user.nickName = userInfo.nickName
|
||||
if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl
|
||||
logger.info(`WebView老用户登录: ${openid}`)
|
||||
}
|
||||
|
||||
await user.save()
|
||||
|
||||
// 记录登录日志
|
||||
await recordLoginLog(
|
||||
user._id,
|
||||
openid,
|
||||
isNewUser ? 'register' : 'auto_login',
|
||||
'wechat_webview',
|
||||
'webview',
|
||||
req,
|
||||
true
|
||||
)
|
||||
|
||||
// 生成 JWT token
|
||||
const token = jwt.sign(
|
||||
{ openid, userId: user._id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
openid,
|
||||
isNewUser,
|
||||
userInfo: {
|
||||
nickName: user.nickName,
|
||||
avatarUrl: user.avatarUrl
|
||||
},
|
||||
firstPlayDate: user.createdAt
|
||||
}, isNewUser ? '注册成功' : '自动登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('WebView登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
* POST /api/auth/admin/login
|
||||
*/
|
||||
router.post('/admin/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
|
||||
// 查找管理员
|
||||
const admin = await Admin.findOne({ username })
|
||||
if (!admin) {
|
||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isMatch = await admin.validatePassword(password)
|
||||
if (!isMatch) {
|
||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if (admin.status !== 'active') {
|
||||
return ApiResponse.error(res, '账户已禁用', 403)
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
admin.lastLogin = new Date()
|
||||
admin.lastLoginIp = req.ip
|
||||
admin.loginCount += 1
|
||||
await admin.save()
|
||||
|
||||
// 记录日志
|
||||
await AdminLog.create({
|
||||
adminId: admin._id,
|
||||
adminName: admin.username,
|
||||
action: 'login',
|
||||
resource: 'system',
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
})
|
||||
|
||||
// 生成 JWT token
|
||||
const token = jwt.sign(
|
||||
{ adminId: admin._id, role: admin.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
// 获取权限
|
||||
const permissions = Admin.getPermissionsByRole(admin.role)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
adminId: admin._id,
|
||||
username: admin.username,
|
||||
role: admin.role,
|
||||
permissions
|
||||
}, '登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('管理员登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* H5环境用户名密码登录
|
||||
* POST /api/auth/user/login
|
||||
*/
|
||||
router.post('/user/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
|
||||
// 查找用户(显式包含密码字段)
|
||||
const user = await User.findOne({ username }).select('+password')
|
||||
if (!user) {
|
||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isMatch = await user.validatePassword(password)
|
||||
if (!isMatch) {
|
||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||
}
|
||||
|
||||
// 更新登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
// 生成 JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user._id, openid: user.openid },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
userId: user._id,
|
||||
openid: user.openid,
|
||||
userInfo: {
|
||||
nickName: user.nickName,
|
||||
avatarUrl: user.avatarUrl
|
||||
},
|
||||
firstPlayDate: user.createdAt
|
||||
}, '登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('用户登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* H5环境用户注册
|
||||
* POST /api/auth/user/register
|
||||
*/
|
||||
router.post('/user/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password, nickName, avatarUrl } = req.body
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({ username })
|
||||
if (existingUser) {
|
||||
return ApiResponse.error(res, '用户名已存在', 400)
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const user = new User({
|
||||
username,
|
||||
password,
|
||||
nickName: nickName || '星辰旅行者',
|
||||
avatarUrl: avatarUrl || '',
|
||||
unlockedDims: [1]
|
||||
})
|
||||
await user.save()
|
||||
|
||||
logger.info(`新用户注册: ${username}`)
|
||||
|
||||
// 生成 JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user._id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
userId: user._id,
|
||||
userInfo: {
|
||||
nickName: user.nickName,
|
||||
avatarUrl: user.avatarUrl
|
||||
}
|
||||
}, '注册成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('用户注册失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return ApiResponse.unauthorized(res)
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
const user = await User.findById(decoded.userId)
|
||||
|
||||
if (!user) {
|
||||
return ApiResponse.notFound(res, '用户不存在')
|
||||
}
|
||||
|
||||
return ApiResponse.success(res, user)
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取用户信息失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* WebView Token 交换接口
|
||||
* POST /api/auth/webview-token
|
||||
* 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token
|
||||
* 小程序在 URL 中传递 openid,H5 用这个接口换取 token
|
||||
*/
|
||||
router.post('/webview-token', async (req, res) => {
|
||||
try {
|
||||
const { openid } = req.body
|
||||
|
||||
if (!openid) {
|
||||
return ApiResponse.error(res, '缺少用户标识', 400)
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = await User.findOne({ openid })
|
||||
|
||||
if (!user) {
|
||||
return ApiResponse.error(res, '用户不存在', 404)
|
||||
}
|
||||
|
||||
// 生成新的 JWT token
|
||||
const token = jwt.sign(
|
||||
{ openid, userId: user._id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRE }
|
||||
)
|
||||
|
||||
// 更新最后登录时间
|
||||
user.lastLoginAt = new Date()
|
||||
await user.save()
|
||||
|
||||
// 记录登录日志
|
||||
await recordLoginLog(
|
||||
user._id,
|
||||
openid,
|
||||
'webview_auto_login',
|
||||
'wechat_webview',
|
||||
'webview_h5',
|
||||
req,
|
||||
true
|
||||
)
|
||||
|
||||
logger.info(`WebView Token交换成功: ${openid}`)
|
||||
|
||||
return ApiResponse.success(res, {
|
||||
token,
|
||||
openid,
|
||||
userInfo: {
|
||||
nickName: user.nickName,
|
||||
avatarUrl: user.avatarUrl
|
||||
}
|
||||
}, '登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('WebView Token交换失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user