Compare commits

..

No commits in common. 'main' and 'master' have entirely different histories.
main ... master

145
.gitignore vendored

@ -1,132 +1,37 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
# 依赖文件夹
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
# 环境变量文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# 日志文件
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# DynamoDB Local files
.dynamodb/
# 编辑器配置
.vscode/
.idea/
*.swp
*.swo
# TernJS port file
.tern-port
# 操作系统临时文件
.DS_Store
Thumbs.db
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# 构建输出
dist/
build/
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# 测试覆盖率
coverage/
.nyc_output/
# 上传的图片文件夹
uploads/images/

@ -1,2 +0,0 @@
# nodejs-fullstack-learning

@ -0,0 +1,49 @@
const express = require('express');
const cors = require('cors');
require('dotenv').config(); // 加载.env环境变量
// 导入数据库连接测试和登录路由
const { testDbConnection } = require('./config/db');
const userRoutes = require('./routes/userRoutes');
// 初始化Express应用
const app = express();
// 1. 配置中间件(必须在挂载路由前)
// 解析JSON请求体登录时前端传的用户名/密码会用JSON格式
app.use(express.json());
// 解析表单请求体(兼容其他场景,如表单提交)
app.use(express.urlencoded({ extended: true }));
// 配置跨域允许uni-app前端地址访问从.env读取配置
app.use(cors({
origin: process.env.CORS_ORIGIN,
credentials: true // 允许携带Cookie后续若需记住登录状态可用
}));
// 配置静态资源服务
app.use('/uploads', express.static('uploads'));
// 2. 测试数据库连接(启动时自动验证)
testDbConnection();
// 3. 挂载路由(所有接口加/api前缀避免路径冲突
// 登录接口最终地址http://localhost:3000/api/login
app.use('/api', userRoutes);
// 4. 全局错误处理中间件(捕获接口异常,返回统一错误格式)
app.use((err, req, res, next) => {
console.error('服务器异常:', err.stack);
res.status(500).json({
code: 500,
msg: '服务器内部错误',
error: err.message
});
});
// 导入产品路由
const productRoutes = require('./routes/productRoutes');
// 挂载产品路由
app.use('/api', productRoutes);
// 导出app实例给server.js调用
module.exports = app;

@ -0,0 +1,32 @@
const { Sequelize } = require('sequelize');
// 加载 .env 里的环境变量
require('dotenv').config();
// 创建 Sequelize 实例,连接 MySQL
const sequelize = new Sequelize(
process.env.DB_NAME, // 数据库名uniapp_db
process.env.DB_USER, // 账号root
process.env.DB_PASSWORD, // 密码(从 .env 读取就是123456
{
host: process.env.DB_HOST, // 主机localhost
port: process.env.DB_PORT, // 端口3306
dialect: 'mysql', // 数据库类型mysql
pool: { // 连接池(优化性能,不用改)
max: 5,
min: 0,
idle: 10000
},
logging: false // 关闭 SQL 日志(开发阶段想调试可以改成 true
}
);
// 测试数据库连接的函数
const testDbConnection = async () => {
try {
await sequelize.authenticate();
console.log('MySQL 数据库连接成功!');
} catch (error) {
console.error('数据库连接失败:', error);
process.exit(1); // 连接失败就退出进程,避免后续报错
}
};
// 导出实例和连接测试方法,供其他文件使用
module.exports = { sequelize, testDbConnection };

@ -0,0 +1,85 @@
const Product = require('../models/Product');
const { Op } = require('sequelize');
// 获取商品列表
const getProductList = async (req, res) => {
try {
// 从查询参数获取分页信息
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
// 查询条件
const whereClause = {};
if (req.query.category) {
whereClause.category = req.query.category;
}
if (req.query.status !== undefined) {
whereClause.status = req.query.status;
}
if (req.query.name) {
whereClause.name = { [Op.like]: `%${req.query.name}%` };
}
// 查询商品列表
const { count, rows } = await Product.findAndCountAll({
where: whereClause,
limit: limit,
offset: offset,
order: [['created_at', 'DESC']]
});
return res.status(200).json({
code: 200,
msg: '获取商品列表成功',
data: {
products: rows,
pagination: {
currentPage: page,
totalPages: Math.ceil(count / limit),
totalItems: count,
itemsPerPage: limit
}
}
});
} catch (error) {
return res.status(500).json({
code: 500,
msg: '获取商品列表失败',
error: error.message
});
}
};
// 获取单个商品详情
const getProductDetail = async (req, res) => {
try {
const { id } = req.params;
const product = await Product.findByPk(id);
if (!product) {
return res.status(404).json({
code: 404,
msg: '商品不存在'
});
}
return res.status(200).json({
code: 200,
msg: '获取商品详情成功',
data: product
});
} catch (error) {
return res.status(500).json({
code: 500,
msg: '获取商品详情失败',
error: error.message
});
}
};
module.exports = {
getProductList,
getProductDetail
};

@ -0,0 +1,69 @@
// 导入 User 模型,用于操作 users 表数据
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
// 核心:登录接口逻辑
const login = async (req, res) => {
try {
// 1. 检查请求体是否存在
if (!req.body) {
return res.status(400).json({
code: 400,
msg: '用户名或密码不能为空'
});
}
// 2. 从前端请求体中获取登录参数(用户名、密码)
const { username, password } = req.body;
// 3. 验证参数是否完整(基础校验,避免空值请求)
if (!username || !password) {
return res.status(400).json({
code: 400,
msg: '用户名和密码不能为空'
});
}
// 3. 从数据库中查询该用户名对应的用户Sequelize 方法,无需手写 SQL
const user = await User.findOne({
where: { username: username } // 条件:用户名匹配
});
// 4. 判断用户是否存在及密码是否正确(统一提示,提高安全性)
if (!user || user.password !== password) {
return res.status(401).json({
code: 401,
msg: '用户名或密码不正确'
});
}
// 6. 生成JWT Token
const token = generateToken({
id: user.id,
username: user.username
});
// 7. 登录成功返回用户信息和token隐藏密码避免泄露
const { password: _, ...userInfo } = user.dataValues; // 解构删除密码字段
return res.status(200).json({
code: 200,
msg: '登录成功',
data: {
user: userInfo, // 返回用户ID、用户名、邮箱等安全信息
token: token, // 返回生成的JWT token
tokenType: 'Bearer'
}
});
} catch (error) {
// 7. 捕获异常(如数据库错误)
return res.status(500).json({
code: 500,
msg: '登录失败,服务器异常',
error: error.message
});
}
};
// 导出登录方法,供路由调用
module.exports = { login };

@ -0,0 +1,30 @@
### 核心文件和文件夹
- server.js :服务器启动文件,运行后启动后端服务
- app.js :应用主配置文件,设置中间件、路由等
- config/ :配置文件夹
- db.js :数据库连接配置
- controllers/ :控制器文件夹
- userController.js :用户相关功能(如登录)
- productController.js :产品相关功能
- models/ :数据模型文件夹
- User.js :用户数据模型
- Product.js :产品数据模型
- routes/ :路由文件夹
- userRoutes.js :用户相关接口路由
- productRoutes.js :产品相关接口路由
- utils/ :工具函数文件夹
- jwt.js JWT令牌生成工具
- .env.example :环境变量示例文件
- package.json :项目依赖配置文件
## 技术栈
### 核心技术
- Node.js JavaScript 运行环境,用于运行后端代码
- Express Web 框架,用于构建 API 接口
- MySQL :关系型数据库,存储用户和产品数据
- Sequelize ORM 库,简化数据库操作
### 辅助工具
- JWT :用于生成和验证身份令牌
- CORS :处理跨域请求,允许前端访问后端接口
- dotenv :管理环境变量,如数据库连接信息
- nodemon :开发时自动重启服务器,提高开发效率

@ -0,0 +1,26 @@
const { verifyToken } = require('../utils/jwt');
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
code: 401,
msg: '访问被拒绝,缺少令牌'
});
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({
code: 403,
msg: '令牌无效或已过期'
});
}
};
module.exports = { authenticateToken };

@ -0,0 +1,67 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const Product = sequelize.define('Product', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '商品ID'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '商品名称'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '商品描述'
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
comment: '商品价格'
},
stock: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '库存数量'
},
category: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '商品分类'
},
imageUrl: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '商品图片URL'
},
status: {
type: DataTypes.TINYINT,
defaultValue: 1,
comment: '状态1-上架0-下架'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
comment: '创建时间'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
comment: '更新时间'
}
}, {
tableName: 'products',
timestamps: false,
underscored: true, // 自动将驼峰命名转换为下划线命名
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
freezeTableName: true,
syncOnAssociation: false,
});
module.exports = Product;

@ -0,0 +1,57 @@
const { DataTypes } = require('sequelize');
// 导入数据库连接实例
const { sequelize } = require('../config/db');
// 定义 User 模型(仅映射已手动创建的 users 表,不自动建表)
const User = sequelize.define('User', {
// 字段1id对应表中 id 字段,主键自增)
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '用户ID'
},
// 字段2username对应表中 username 字段,非空唯一)
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '用户名(登录用)'
},
// 字段3password对应表中 password 字段,非空)
password: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '用户密码'
},
// 字段4email对应表中 email 字段,可选唯一)
email: {
type: DataTypes.STRING(100),
unique: true,
allowNull: true,
comment: '用户邮箱'
},
// 字段5-6createdAt/updatedAt对应表中这两个字段自动维护无需手动赋值
createdAt: {
type: DataTypes.DATE,
allowNull: false,
comment: '创建时间'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
comment: '更新时间'
}
}, {
// 模型配置:关键是关闭自动建表相关逻辑,仅做表映射
tableName: 'users', // 明确指定映射的表名(必须和手动创建的表名一致)
timestamps: true, // 开启自动维护 createdAt/updatedAt表中已手动创建这两个字段
paranoid: false, // 关闭软删除(和表结构匹配)
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
freezeTableName: true, // 冻结表名,避免 Sequelize 自动给表名加复数(这里表名已手动设为 users无需处理
syncOnAssociation: false // 关闭关联自动同步(避免模型操作影响表结构)
});
// 导出模型,供控制器调用(后续登录接口查数据用)
module.exports = User;

1648
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
{
"name": "nodejs",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "YuLeYuan",
"license": "ISC",
"description": "Node.js + Express + MySQL 全栈后端(含登录功能)",
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.2.4",
"express": "^5.2.1",
"express-validator": "^7.3.1",
"jsonwebtoken": "^9.0.3",
"mysql2": "^3.16.3",
"sequelize": "^6.37.7"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
}

@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { authenticateToken } = require('../middlewares/auth');
const { getProductList, getProductDetail } = require('../controllers/productController');
// 获取商品列表(需要认证)
router.get('/products', authenticateToken, getProductList);
// 获取单个商品详情(需要认证)
router.get('/products/:id', authenticateToken, getProductDetail);
module.exports = router;

@ -0,0 +1,13 @@
// 导入 Express 路由模块
const express = require('express');
const router = express.Router(); // 创建路由实例
// 导入控制器中的登录方法
const { login } = require('../controllers/userController');
// 定义登录接口POST /api/login前端通过这个路径调用登录
// 为什么用 POST因为登录参数密码放请求体里比 GET 更安全
router.post('/login', login);
// 导出路由,供 app.js 挂载
module.exports = router;

@ -0,0 +1,11 @@
const app = require('./app');
require('dotenv').config();
// 从.env读取端口若没配置则用3000默认值
const PORT = process.env.PORT || 3000;
// 启动服务,监听指定端口
app.listen(PORT, () => {
console.log(`后端服务已启动地址http://localhost:${PORT}`);
console.log(`登录接口地址http://localhost:${PORT}/api/login`);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@ -0,0 +1,21 @@
const jwt = require('jsonwebtoken');
/**
* 生成JWT Token
* @param {Object} payload - 要存储在token中的数据
* @returns {String} 生成的token字符串
*/
const generateToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET);
};
/**
* 验证JWT Token
* @param {String} token - 要验证的token字符串
* @returns {Object} 解码后的payload对象
*/
const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
module.exports = { generateToken, verifyToken };
Loading…
Cancel
Save