學習來自 YT: compiletab
影片: Build & Deploy Full Stack E-commerce Website | Redux | MERN Stack Project
Github assets: 連結
Thank you, teacher
建立 & 部署全端商業網站
影片時間戳
00:00:00 – Introduction
00:01:56 – Demo
00:05:26 – Installation & set up
05:50:25 – Admin UI
07:20:25 – Backend setup
07:32:40 – user routes
08:05:40 – Products routes
09:18:56 – cart routes
10:13:26 – checkout routes
10:57:55 – Admin routes
11:58:30 – Redux
Backend (後端)
npm init -y 初始化建立 package.json 檔案
安裝套件
- express
- mongoose
- dotenv
- jsonwebtoken
- bcryptjs
- cors
- nodemon
// backend - 後端
// TERMINAL - 終端機
npm install express mongoose dotenv jsonwebtoken bcryptjs cors nodemon
後端環境建立
// backend/server.js
// 建立伺服器的進入點
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
// backend/package.json
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node backend/server.js",
"dev": "nodemon backend/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.12.1",
"nodemon": "^3.1.9"
}
}
// backend/.env
PORT=9000
使用 MongoDB Altas 建立資料庫
- New Project – 新建專案
- Name Your Project: rabbit – 命名你的專案: rabbit
- Create Project – 建立專案
- Create a cluster – 建立集群
- Deploy your cluster – 部署你的集群
Free、AWS – 免費、亞馬遜雲端服務
Create Deployment – 建立部署 - Connect to Cluster0 – 連接到集群
Copy – 複製
Create Database User – 建立資料庫使用者 - Add a connection IP Address – 增加連接的IP地址
Add IP ADDRESS – 增加IP地址
ALLOW ACCESS FROM ANYWHERE – 允許來自任何地方的訪問
(我這裡先設定一星期)
CONFIRM – 確認 - Choose a connection method – 選擇連接方法
Connect to your application – 連接你的應用程式
Using this connection string in your application – 使用連接字串在你的應用程式
// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://<username>:<password>@<cluster-address>/<database-name>?retryWrites=true&w=majority&appName=<app-name>
// backend/config/db.js
const mongoose = require("mongoose");
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log("MongoDB connected successfully");
} catch (err) {
console.error("MongoDB connection failed.", err);
process.exit(1);
}
};
module.exports = connectDB;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
製作處理用戶功能
USERS (用戶)
- Register
- Login
- Profile
User Schema (用戶模式)
Field Name (欄位名稱) | Type (類型) | Constraints (約束) |
_id | ObjectId | Primary key, auto-generated by MongoDB |
name | String | Required, trimmed of whitespace |
String | Required, unique, trimmed, email validated | |
password | String | Required, minimum length: 6 |
role | String | Enum: [“customer”, “admin”], Default: “customer” |
createdAt | Date | Auto-generated timestamp |
updateAt | Date | Auto-updated timestamp |
// backend/models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
match: [/.+\@.+\..+/, "Please enter a valid email address"],
},
password: {
type: String,
required: true,
minLength: 6,
},
role: {
type: String,
enum: ["customer", "admin"],
default: "customer",
},
},
{
timestamps: true,
}
);
// Password Hash middleware - 密碼哈希中介軟體
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Match User entered password to Hashed password - 將用戶輸入的密碼與哈希密碼匹配
userSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model("User", userSchema);
// backend/routes/userRoutes.js
const express = require("express");
const User = require("../models/User");
const jwt = require("jsonwebtoken");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/users/register - @路由、使用 POST 方法、API 的路徑
// @desc Register a new user - @描述 註冊一個新用戶
// @access Public - @訪問權限 公開
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
try {
// Registration logic - 註冊邏輯
// 測試1
// res.send({ name, email, password });
let user = await User.findOne({ email });
if (user) return res.status(400).json({ message: "User already exists" });
user = new User({ name, email, password });
await user.save();
// 測試2
// res.status(201).json({
// user: {
// _id: user._id,
// name: user.name,
// email: user.email,
// role: user.role,
// },
// });
// Create JWT Payload - 建立 JWT 負載
const payload = { user: { id: user._id, role: user.role } };
// Sign and return the token along with user data - 簽署並返回令牌以及用戶資料
jwt.sign(
payload,
process.env.JWT_SECRET,
{
expiresIn: "40h",
},
(err, token) => {
if (err) throw err;
// Send the user and token in response
res.status(201).json({
user: {
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
},
token,
});
}
);
} catch (error) {
console.log(error);
res.status(500).send("Server Error");
}
});
// @route POST /api/user/login - @路由、使用 POST 方法、API 的路徑
// @desc Authenticate user - @描述 - 驗證用戶
// @access Public - @訪問權限 公開
router.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
// Find ther user by email
let user = await User.findOne({
email,
});
if (!user)
return res.status(400).json({
message: "Invalid Credentials",
});
const isMatch = await user.matchPassword(password);
if (!isMatch)
return res.status(400).json({ message: "Invalid Credentials" });
// Create JWT Payload - 建立 JWT 負載
const payload = { user: { id: user._id, role: user.role } };
// Sign and return the token along with user data - 簽署並返回令牌以及用戶資料
jwt.sign(
payload,
process.env.JWT_SECRET,
{
expiresIn: "40h",
},
(err, token) => {
if (err) throw err;
// Send the user and token in response
res.json({
user: {
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
},
token,
});
}
);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/users/profile - @路由、使用 POST 方法、API 的路徑
// @desc Get logged-in user's profile (Protected Route) - @描述 - 獲取已登入用戶的個人資料 (受保護的路由)
// @access Private - @訪問權限 私人
router.get("/profile", protect, async (req, res) => {
res.json(req.user);
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Postman – Postman Agent
- Create Workspace – 建立工作區
- Name: Rabbit – 名稱: Rabbit
Create – 建立 - Create Collection – 建立集合
Users - Add a request – 新增請求
Register、POST、http://localhost:9000/api/users/register
Login、POST、http://localhost:9000/api/users/login
Profile、GET、http://localhost:9000/api/users/profile
Save – 儲存 - Body > raw
- Send
// Postman - Users/Register
// Body > raw
{
"name": "John",
"email": "john@example.com",
"password": 123456
}
// Postman - Users/Login
// Body > raw
{
"email": "john1@example.com",
"password": 123456
}
// Postman - Users/Profile
// Headers
Key: Authorization
Value: Bearer 你的Token
查看 MongoDB Atlas
- Browse Collection (瀏覽集合) > users
// backend/middleware/authMiddleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");
// Middleware to protect routes
const protect = async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
try {
token = req.headers.authorization.split(" ")[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.user.id).select("-password"); // Exclude password - 排除密碼
next();
} catch (error) {
console.error("Token verification failed:", error);
res.status(401).json({ message: "Not authorized, token failed" });
}
} else {
res.status(401).json({ message: "Not authorized, no token provided" });
}
};
module.exports = { protect };
// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://<username>:<password>@<cluster-address>/<database-name>?retryWrites=true&w=majority&appName=<app-name>
JWT_SECRET=你的JWT密鑰
後端製作流程 (個人整理)
- 初始化專案
- 創建 .env 文件
- 製作 server.js (伺服器入口)
- 製作資料庫配置 config/db.js
- 製作資料模型 models/User.js
- 製作路由 routes/userRoutes.js
- 製作中介軟體 middleware/authMiddleware.js
製作產品功能
PRODUCTS (產品)
- Create Product
- Update Product
- Delete Product
- All Products
- Single Product
- Best Seller
- Similar Products
- New Arrivals
Product Schema (產品模式)
Field Name (欄位名稱) | Type (類型) | Constraints/Description (約束/描述) |
_id | ObjectId | Auto-generated by MondoDB |
name | String | Required, trimmed |
description | String | Required |
price | Number | Required |
discountPrice | Number | Optional |
countInStock | Number | Required, default: 0 |
sku | String | Required, unique |
category | String | Required |
brand | String | Optional |
sizes | [String] | Required, example: [‘S’, ‘M’, ‘L’] |
colors | [String] | Required, example: [‘Red’, ‘Blue’] |
collections | String | Required, example: ‘Summercollection’ |
material | String | Optional |
gender | String | Enum: [‘Men’, ‘Women’, ‘Unisex’] |
images | Array of Objects | Required, each object contains: – url: String, required – altText: String, optional |
isFeatured | Boolean | Default: false |
isPublished | Boolean | Default: false |
rating | Number | Default: 0 |
numReviews | Number | Default: 0 |
tags | [String] | Optional |
user | ObjectId | Reference to User, required |
metaTitle | String | Optional |
metaDescription | String | Optional |
metaKeywords | String | Optional |
dimensions | Object | Optional, contains: – length: Number – width: Number – height: Number |
weight | Number | Optional |
createdAt | Date | Auto-generated by timestamps |
updatedAt | Date | Auto-updated by timestamps |
Create Product (建立產品)
// backend/models/Product.js
const mongoose = require("mongoose");
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
discountPrice: {
type: Number,
},
countInStock: {
type: Number,
required: true,
default: 0,
},
sku: {
type: String,
unique: true,
required: true,
},
category: {
type: String,
required: true,
},
brand: {
type: String,
},
sizes: {
type: [String],
required: true,
},
colors: {
type: [String],
required: true,
},
collections: {
type: String,
required: true,
},
material: {
type: String,
},
gender: {
type: String,
enum: ["Men", "Women", "Unisex"],
},
images: [
{
url: {
type: String,
required: true,
},
altText: {
type: String,
},
},
],
isFeatured: {
type: Boolean,
default: false,
},
isPublished: {
type: Boolean,
default: false,
},
rating: {
type: Number,
default: 0,
},
numReviews: {
type: Number,
default: 0,
},
tags: [String],
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
metaTitle: {
type: String,
},
metaDescription: {
type: String,
},
metaKeywords: {
type: String,
},
dimensions: {
length: Number,
width: Number,
height: Number,
},
weight: Number,
},
{ timestamps: true }
);
module.exports = mongoose.model("Product", productSchema);
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 - 建立新的產品
// @access Private/Admin - @訪問權限 - 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Products - Add a request – 新增請求
Create、POST、http://localhost:9000/api/products - Body > raw
- Headers > Key、Value
// Postman - Products/Create
// Body > raw
{
"name": "Classic Denim Jacket",
"description": "A timeless denim jacket perfect for any season. Comfortable fit and durable material",
"price": 59.99,
"discountPrice": 49.99,
"countInStock": 200,
"category": "Apparel",
"brand": "UrbanWear",
"sizes": ["XS", "S", "M", "L", "XL"],
"colors": ["Blue", "Black"],
"collections": "Spring Collection",
"material": "Denim",
"gender": "Unisex",
"images": [
{
"url": "https://picsum.photos/seed/denim1/500/500",
"altText": "Front view of the denim jacket"
},
{
"url": "https://picsum.photos/seed/denim2/500/500",
"altText": "Back view of the denim jacket"
}
],
"isFeatured": true,
"isPublished": true,
"tags": ["denim", "jacket", "casual", "spring"],
"dimensions": {
"length": 12,
"width": 8,
"height": 1
},
"weight": 1.5,
"sku": "CLTH12345"
}
// Postman - Products/Create
// Headers
Key: Authorization
Value: Bearer 你的Token
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
查看 MongoDB Atlas
- Browse collections (瀏覽集合) > products
// backend/middleware/authMiddleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");
// Middleware to protect routes
const protect = async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
try {
token = req.headers.authorization.split(" ")[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.user.id).select("-password"); // Exclude password - 排除密碼
next();
} catch (error) {
console.error("Token verification failed:", error);
res.status(401).json({ message: "Not authorized, token failed" });
}
} else {
res.status(401).json({ message: "Not authorized, no token provided" });
}
};
// Middleware to check if the user is an admin - 中介軟體檢查用戶是否為管理員
const admin = (req, res, next) => {
if (req.user && req.user.role === "admin") {
next();
} else {
res.status(403).json({ message: "Not authorized as an admin" });
}
};
module.exports = { protect, admin };
手動調整 MongoDB 資料庫
- customer 手動調整為 admin
使用 Postman API 測試工具
- Postman – Products/Create 測試管理員功能
name、sku 都需要做調整
// Postman - Products/Create
// Body > raw
{
"name": "Classic Denim Jeans",
"description": "A timeless denim jacket perfect for any season. Comfortable fit and durable material",
"price": 59.99,
"discountPrice": 49.99,
"countInStock": 200,
"sku": "CLTH123456",
"category": "Apparel",
"brand": "UrbanWear",
"sizes": [
"XS",
"S",
"M",
"L",
"XL"
],
"colors": [
"Blue",
"Black"
],
"collections": "Spring Collection",
"material": "Denim",
"gender": "Unisex",
"images": [
{
"url": "https://picsum.photos/seed/denim1/500/500",
"altText": "Front view of the denim jacket",
"_id": "67d0f15ef9aec7935e64f4be"
},
{
"url": "https://picsum.photos/seed/denim2/500/500",
"altText": "Back view of the denim jacket",
"_id": "67d0f15ef9aec7935e64f4bf"
}
],
"isFeatured": true,
"isPublished": true,
"rating": 0,
"numReviews": 0,
"tags": [
"denim",
"jacket",
"casual",
"spring"
],
"user": "67ced561a2b8dd3a07c40918",
"dimensions": {
"length": 12,
"width": 8,
"height": 1
},
"weight": 1.5,
"_id": "67d0f15ef9aec7935e64f4bd",
"createdAt": "2025-03-12T02:28:46.647Z",
"updatedAt": "2025-03-12T02:28:46.647Z",
"__v": 0
}
Update Product (更新產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
Update、PUT、http://localhost:9000/api/products/67d0f15ef9aec7935e64f4bd - Headers > Key、Value
- Body > raw
// Postman - Products/Update
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Products/Update
// Body > raw
{
"name": "Winter Jacket"
}
查看 MongoDB Atlas 資料庫
- 查看是否有更新產品名稱
- 測試更新 name、description、sizes 欄位的值
// Postman - Products/Update
// Body > raw
{
"name": "Winter Jacket long Sleeves",
"description": "Long Sleeves Jackets",
"sizes": ["S", "M"]
}
Delete Product (刪除產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
Delete、DELETE、http://localhost:9000/api/products/67d0f15ef9aec7935e64f4bd - Headers > Key、Value
// Postman - Products/Delete
// Headers
Key: Authorization
Value: Bearer 你的Token
查看 MongoDB Atlas 資料庫
- 查看是否有刪除產品
下載使用產品資料
// backend/data/products.js
// product.js:
const products = [
{
name: "Classic Oxford Button-Down Shirt",
description:
"This classic Oxford shirt is tailored for a polished yet casual look. Crafted from high-quality cotton, it features a button-down collar and a comfortable, slightly relaxed fit. Perfect for both formal and casual occasions, it comes with long sleeves, a button placket, and a yoke at the back. The shirt is finished with a gently rounded hem and adjustable button cuffs.",
price: 39.99,
discountPrice: 34.99,
countInStock: 20,
sku: "OX-SH-001",
category: "Top Wear",
brand: "Urban Threads",
sizes: ["S", "M", "L", "XL", "XXL"],
colors: ["Red", "Blue", "Yellow"],
collections: "Business Casual",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=39",
altText: "Classic Oxford Button-Down Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=40",
altText: "Classic Oxford Button-Down Shirt Back View",
},
],
rating: 4.5,
numReviews: 12,
},
{
name: "Slim-Fit Stretch Shirt",
description:
"A versatile slim-fit shirt perfect for business or evening events. Designed with a fitted silhouette, the added stretch provides maximum comfort throughout the day. Features a crisp turn-down collar, button placket, and adjustable cuffs.",
price: 29.99,
discountPrice: 24.99,
countInStock: 35,
sku: "SLIM-SH-002",
category: "Top Wear",
brand: "Modern Fit",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "Navy Blue", "Burgundy"],
collections: "Formal Wear",
material: "Cotton Blend",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=41",
altText: "Slim-Fit Stretch Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=42",
altText: "Slim-Fit Stretch Shirt Back View",
},
],
rating: 4.8,
numReviews: 15,
},
{
name: "Casual Denim Shirt",
description:
"This casual denim shirt is made from lightweight cotton denim. It features a regular fit, snap buttons, and a straight hem. With Western-inspired details, this shirt is perfect for layering or wearing solo.",
price: 49.99,
discountPrice: 44.99,
countInStock: 15,
sku: "CAS-DEN-003",
category: "Top Wear",
brand: "Street Style",
sizes: ["S", "M", "L", "XL", "XXL"],
colors: ["Light Blue", "Dark Wash"],
collections: "Casual Wear",
material: "Denim",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=43",
altText: "Casual Denim Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=44",
altText: "Casual Denim Shirt Back View",
},
],
rating: 4.6,
numReviews: 8,
},
{
name: "Printed Resort Shirt",
description:
"Designed for summer, this printed resort shirt is perfect for vacation or weekend getaways. It features a relaxed fit, short sleeves, and a camp collar. The all-over tropical print adds a playful vibe.",
price: 29.99,
discountPrice: 22.99,
countInStock: 25,
sku: "PRNT-RES-004",
category: "Top Wear",
brand: "Beach Breeze",
sizes: ["S", "M", "L", "XL"],
colors: ["Tropical Print", "Navy Palms"],
collections: "Vacation Wear",
material: "Viscose",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=45",
altText: "Printed Resort Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=1",
altText: "Printed Resort Shirt Back View",
},
],
rating: 4.4,
numReviews: 10,
},
{
name: "Slim-Fit Easy-Iron Shirt",
description:
"A slim-fit, easy-iron shirt in woven cotton fabric with a fitted silhouette. Features a turn-down collar, classic button placket, and a yoke at the back. Long sleeves and adjustable button cuffs with a rounded hem.",
price: 34.99,
discountPrice: 29.99,
countInStock: 30,
sku: "SLIM-EIR-005",
category: "Top Wear",
brand: "Urban Chic",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Gray"],
collections: "Business Wear",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=47",
altText: "Slim-Fit Easy-Iron Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=2",
altText: "Slim-Fit Easy-Iron Shirt Front View",
},
],
rating: 5,
numReviews: 14,
},
{
name: "Polo T-Shirt with Ribbed Collar",
description:
"A wardrobe classic, this polo t-shirt features a ribbed collar and cuffs. Made from 100% cotton, it offers breathability and comfort throughout the day. Tailored in a slim fit with a button placket at the neckline.",
price: 24.99,
discountPrice: 19.99,
countInStock: 50,
sku: "POLO-TSH-006",
category: "Top Wear",
brand: "Polo Classics",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Navy", "Red"],
collections: "Casual Wear",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=3",
altText: "Polo T-Shirt Front View",
},
{
url: "https://picsum.photos/500/500?random=4",
altText: "Polo T-Shirt Back View",
},
],
rating: 4.3,
numReviews: 22,
},
{
name: "Oversized Graphic T-Shirt",
description:
"An oversized graphic t-shirt that combines comfort with street style. Featuring bold prints across the chest, this relaxed fit tee offers a modern vibe, perfect for pairing with jeans or joggers.",
price: 19.99,
discountPrice: 15.99,
countInStock: 40,
sku: "OVS-GRF-007",
category: "Top Wear",
brand: "Street Vibes",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "Gray"],
collections: "Streetwear",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=5",
altText: "Oversized Graphic T-Shirt Front View",
},
],
rating: 4.6,
numReviews: 30,
},
{
name: "Regular-Fit Henley Shirt",
description:
"A modern take on the classic Henley shirt, this regular-fit style features a buttoned placket and ribbed cuffs. Made from a soft cotton blend with a touch of elastane for stretch.",
price: 22.99,
discountPrice: 18.99,
countInStock: 35,
sku: "REG-HEN-008",
category: "Top Wear",
brand: "Heritage Wear",
sizes: ["S", "M", "L", "XL"],
colors: ["Heather Gray", "Olive", "Black"],
collections: "Casual Wear",
material: "Cotton Blend",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=6",
altText: "Regular-Fit Henley Shirt Front View",
},
],
rating: 4.5,
numReviews: 25,
},
{
name: "Long-Sleeve Thermal Tee",
description:
"Stay warm with this long-sleeve thermal tee, made from soft cotton with a waffle-knit texture. Ideal for layering in cooler months, the slim-fit design ensures a snug yet comfortable fit.",
price: 27.99,
discountPrice: 22.99,
countInStock: 20,
sku: "LST-THR-009",
category: "Top Wear",
brand: "Winter Basics",
sizes: ["S", "M", "L", "XL", "XXL"],
colors: ["Charcoal", "Dark Green", "Navy"],
collections: "Winter Essentials",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=7",
altText: "Long-Sleeve Thermal Tee Front View",
},
],
rating: 4.4,
numReviews: 18,
},
{
name: "V-Neck Classic T-Shirt",
description:
"A classic V-neck t-shirt for everyday wear. This regular-fit tee is made from breathable cotton and features a clean, simple design with a flattering V-neckline. Lightweight fabric and soft texture make it perfect for casual looks.",
price: 14.99,
discountPrice: 11.99,
countInStock: 60,
sku: "VNECK-CLS-010",
category: "Top Wear",
brand: "Everyday Comfort",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Black", "Navy"],
collections: "Basics",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=8",
altText: "V-Neck Classic T-Shirt Front View",
},
],
rating: 4.7,
numReviews: 28,
},
{
name: "Slim Fit Joggers",
description:
"Slim-fit joggers with an elasticated drawstring waist. Features ribbed hems and side pockets. Ideal for casual outings or workouts.",
price: 40,
discountPrice: 35,
countInStock: 20,
sku: "BW-001",
category: "Bottom Wear",
brand: "ActiveWear",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "Gray", "Navy"],
collections: "Casual Collection",
material: "Cotton Blend",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=9",
altText: "Slim Fit Joggers Front View",
},
],
rating: 4.5,
numReviews: 12,
},
{
name: "Cargo Joggers",
description:
"Relaxed-fit cargo joggers featuring multiple pockets for functionality. Drawstring waist and cuffed hems for a modern look.",
price: 45,
discountPrice: 40,
countInStock: 15,
sku: "BW-002",
category: "Bottom Wear",
brand: "UrbanStyle",
sizes: ["S", "M", "L", "XL"],
colors: ["Olive", "Black"],
collections: "Urban Collection",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=10",
altText: "Cargo Joggers Front View",
},
],
rating: 4.7,
numReviews: 20,
},
{
name: "Tapered Sweatpants",
description:
"Tapered sweatpants designed for comfort. Elastic waistband with adjustable drawstring, perfect for lounging or athletic activities.",
price: 35,
discountPrice: 30,
countInStock: 25,
sku: "BW-003",
category: "Bottom Wear",
brand: "ChillZone",
sizes: ["S", "M", "L", "XL"],
colors: ["Gray", "Charcoal", "Blue"],
collections: "Lounge Collection",
material: "Fleece",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=11",
altText: "Tapered Sweatpants Front View",
},
],
rating: 4.3,
numReviews: 18,
},
{
name: "Denim Jeans",
description:
"Classic slim-fit denim jeans with a slight stretch for comfort. Features a zip fly and five-pocket styling for a timeless look.",
price: 60,
discountPrice: 50,
countInStock: 30,
sku: "BW-004",
category: "Bottom Wear",
brand: "DenimCo",
sizes: ["S", "M", "L", "XL"],
colors: ["Dark Blue", "Light Blue"],
collections: "Denim Collection",
material: "Denim",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=12",
altText: "Denim Jeans Front View",
},
],
rating: 4.6,
numReviews: 22,
},
{
name: "Chino Pants",
description:
"Slim-fit chino pants made from stretch cotton twill. Features a button closure and front and back pockets. Ideal for both casual and semi-formal wear.",
price: 55,
discountPrice: 48,
countInStock: 40,
sku: "BW-005",
category: "Bottom Wear",
brand: "CasualLook",
sizes: ["S", "M", "L", "XL"],
colors: ["Beige", "Navy", "Black"],
collections: "Smart Casual Collection",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=13",
altText: "Chino Pants Front View",
},
],
rating: 4.8,
numReviews: 15,
},
{
name: "Track Pants",
description:
"Comfortable track pants with an elasticated waistband and tapered leg. Features side stripes for a sporty look. Ideal for athletic and casual wear.",
price: 40,
discountPrice: 35,
countInStock: 20,
sku: "BW-006",
category: "Bottom Wear",
brand: "SportX",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "Red", "Blue"],
collections: "Activewear Collection",
material: "Polyester",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=14",
altText: "Track Pants Front View",
},
],
rating: 4.2,
numReviews: 17,
},
{
name: "Slim Fit Trousers",
description:
"Tailored slim-fit trousers with belt loops and a hook-and-eye closure. Suitable for formal occasions or smart-casual wear.",
price: 65,
discountPrice: 55,
countInStock: 15,
sku: "BW-007",
category: "Bottom Wear",
brand: "ExecutiveStyle",
sizes: ["M", "L", "XL"],
colors: ["Gray", "Black"],
collections: "Office Wear",
material: "Polyester",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=15",
altText: "Slim Fit Trousers Front View",
},
],
rating: 4.7,
numReviews: 10,
},
{
name: "Cargo Pants",
description:
"Loose-fit cargo pants with multiple utility pockets. Features adjustable ankle cuffs and a drawstring waist for versatility and comfort.",
price: 50,
discountPrice: 45,
countInStock: 25,
sku: "BW-008",
category: "Bottom Wear",
brand: "StreetWear",
sizes: ["S", "M", "L", "XL"],
colors: ["Olive", "Brown", "Black"],
collections: "Street Style Collection",
material: "Cotton",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=16",
altText: "Cargo Pants Front View",
},
],
rating: 4.5,
numReviews: 13,
},
{
name: "Relaxed Fit Sweatpants",
description:
"Relaxed-fit sweatpants made from soft fleece fabric. Features an elastic waist and adjustable drawstring for a custom fit.",
price: 35,
discountPrice: 30,
countInStock: 35,
sku: "BW-009",
category: "Bottom Wear",
brand: "LoungeWear",
sizes: ["S", "M", "L", "XL"],
colors: ["Gray", "Black", "Navy"],
collections: "Lounge Collection",
material: "Fleece",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=17",
altText: "Relaxed Fit Sweatpants Front View",
},
],
rating: 4.3,
numReviews: 14,
},
{
name: "Formal Dress Pants",
description:
"Classic formal dress pants with a slim fit. Made from lightweight, wrinkle-resistant fabric for a polished look at the office or formal events.",
price: 70,
discountPrice: 60,
countInStock: 20,
sku: "BW-010",
category: "Bottom Wear",
brand: "ElegantStyle",
sizes: ["M", "L", "XL"],
colors: ["Black", "Navy"],
collections: "Formal Collection",
material: "Polyester",
gender: "Men",
images: [
{
url: "https://picsum.photos/500/500?random=18",
altText: "Formal Dress Pants Front View",
},
],
rating: 4.9,
numReviews: 8,
},
{
name: "High-Waist Skinny Jeans",
description:
"High-waist skinny jeans in stretch denim with a button and zip fly. Features a flattering fit that hugs your curves and enhances your silhouette.",
price: 50,
discountPrice: 45,
countInStock: 30,
sku: "BW-W-001",
category: "Bottom Wear",
brand: "DenimStyle",
sizes: ["XS", "S", "M", "L", "XL"],
colors: ["Dark Blue", "Black", "Light Blue"],
collections: "Denim Collection",
material: "Denim",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=19",
altText: "High-Waist Skinny Jeans",
},
],
rating: 4.8,
numReviews: 20,
},
{
name: "Wide-Leg Trousers",
description:
"Flowy, wide-leg trousers with a high waist and side pockets. Perfect for an elegant look that combines comfort and style.",
price: 60,
discountPrice: 55,
countInStock: 25,
sku: "BW-W-002",
category: "Bottom Wear",
brand: "ElegantWear",
sizes: ["S", "M", "L", "XL"],
colors: ["Beige", "Black", "White"],
collections: "Formal Collection",
material: "Polyester",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=20",
altText: "Wide-Leg Trousers Front View",
},
],
rating: 4.7,
numReviews: 15,
},
{
name: "Stretch Leggings",
description:
"Soft, stretch leggings in a high-rise style. Perfect for lounging, working out, or casual wear, with a smooth fit that flatters your body.",
price: 25,
discountPrice: 20,
countInStock: 40,
sku: "BW-W-003",
category: "Bottom Wear",
brand: "ComfyFit",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "Gray", "Navy"],
collections: "Activewear Collection",
material: "Cotton Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=21",
altText: "Stretch Leggings Front View",
},
],
rating: 4.5,
numReviews: 30,
},
{
name: "Pleated Midi Skirt",
description:
"Elegant pleated midi skirt with a high waistband and soft fabric that drapes beautifully. Ideal for both formal and casual occasions.",
price: 55,
discountPrice: 50,
countInStock: 20,
sku: "BW-W-004",
category: "Bottom Wear",
brand: "ChicStyle",
sizes: ["S", "M", "L"],
colors: ["Pink", "Navy", "Black"],
collections: "Spring Collection",
material: "Polyester",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=22",
altText: "Pleated Midi Skirt Front View",
},
],
rating: 4.6,
numReviews: 18,
},
{
name: "Flared Palazzo Pants",
description:
"High-waist palazzo pants with a loose, flowing fit. Comfortable and stylish, making them perfect for casual outings or beach days.",
price: 45,
discountPrice: 40,
countInStock: 35,
sku: "BW-W-005",
category: "Bottom Wear",
brand: "BreezyVibes",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Beige", "Light Blue"],
collections: "Summer Collection",
material: "Linen Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=23",
altText: "Flared Palazzo Pants Front View",
},
],
rating: 4.4,
numReviews: 22,
},
{
name: "High-Rise Joggers",
description:
"Comfortable high-rise joggers with an elastic waistband and drawstring for a perfect fit. Great for lounging or working out.",
price: 40,
discountPrice: 35,
countInStock: 30,
sku: "BW-W-006",
category: "Bottom Wear",
brand: "ActiveWear",
sizes: ["XS", "S", "M", "L"],
colors: ["Black", "Gray", "Pink"],
collections: "Loungewear Collection",
material: "Cotton Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=24",
altText: "High-Rise Joggers Front View",
},
],
rating: 4.3,
numReviews: 25,
},
{
name: "Paperbag Waist Shorts",
description:
"Stylish paperbag waist shorts with a belted waist and wide legs. Perfect for summer outings and keeping cool in style.",
price: 35,
discountPrice: 30,
countInStock: 20,
sku: "BW-W-007",
category: "Bottom Wear",
brand: "SunnyStyle",
sizes: ["S", "M", "L"],
colors: ["White", "Khaki", "Blue"],
collections: "Summer Collection",
material: "Cotton",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=25",
altText: "Paperbag Waist Shorts Front View",
},
],
rating: 4.5,
numReviews: 19,
},
{
name: "Stretch Denim Shorts",
description:
"Comfortable stretch denim shorts with a high-waisted fit and raw hem. Perfect for pairing with your favorite tops during warmer months.",
price: 40,
discountPrice: 35,
countInStock: 25,
sku: "BW-W-008",
category: "Bottom Wear",
brand: "DenimStyle",
sizes: ["S", "M", "L", "XL"],
colors: ["Blue", "Black", "White"],
collections: "Denim Collection",
material: "Denim",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=26",
altText: "Stretch Denim Shorts Front View",
},
],
rating: 4.7,
numReviews: 15,
},
{
name: "Culottes",
description:
"Wide-leg culottes with a flattering high waist and cropped length. The perfect blend of comfort and style for any casual occasion.",
price: 50,
discountPrice: 45,
countInStock: 30,
sku: "BW-W-009",
category: "Bottom Wear",
brand: "ChicStyle",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "White", "Olive"],
collections: "Casual Collection",
material: "Polyester",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=27",
altText: "Culottes Front View",
},
],
rating: 4.6,
numReviews: 23,
},
{
name: "Classic Pleated Trousers",
description:
"Timeless pleated trousers with a tailored fit. A wardrobe essential for workwear or formal occasions.",
price: 70,
discountPrice: 65,
countInStock: 25,
sku: "BW-W-010",
category: "Bottom Wear",
brand: "ElegantWear",
sizes: ["S", "M", "L", "XL"],
colors: ["Navy", "Black", "Gray"],
collections: "Formal Collection",
material: "Wool Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=28",
altText: "Classic Pleated Trousers Front View",
},
],
rating: 4.8,
numReviews: 20,
},
{
name: "Knitted Cropped Top",
description:
"A stylish knitted cropped top with a flattering fitted silhouette. Perfect for pairing with high-waisted jeans or skirts for a casual look.",
price: 40,
discountPrice: 35,
countInStock: 25,
sku: "TW-W-001",
category: "Top Wear",
brand: "ChicKnit",
sizes: ["S", "M", "L"],
colors: ["Beige", "White"],
collections: "Knits Collection",
material: "Cotton Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=29",
altText: "Knitted Cropped Top",
},
],
rating: 4.6,
numReviews: 15,
},
{
name: "Boho Floral Blouse",
description:
"Flowy boho blouse with floral patterns, featuring a relaxed fit and balloon sleeves. Ideal for casual summer days.",
price: 50,
discountPrice: 45,
countInStock: 30,
sku: "TW-W-002",
category: "Top Wear",
brand: "BohoVibes",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Pink"],
collections: "Summer Collection",
material: "Viscose",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=30",
altText: "Boho Floral Blouse",
},
],
rating: 4.7,
numReviews: 20,
},
{
name: "Casual T-Shirt",
description:
"A soft, breathable casual t-shirt with a classic fit. Features a round neckline and short sleeves, perfect for everyday wear.",
price: 25,
discountPrice: 20,
countInStock: 50,
sku: "TW-W-003",
category: "Top Wear",
brand: "ComfyTees",
sizes: ["S", "M", "L", "XL"],
colors: ["Black", "White", "Gray"],
collections: "Essentials",
material: "Cotton",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=31",
altText: "Casual T-Shirt",
},
],
rating: 4.5,
numReviews: 25,
},
{
name: "Off-Shoulder Top",
description:
"An elegant off-shoulder top with ruffled sleeves and a flattering fit. Ideal for adding a touch of femininity to your outfit.",
price: 45,
discountPrice: 40,
countInStock: 35,
sku: "TW-W-004",
category: "Top Wear",
brand: "Elegance",
sizes: ["S", "M", "L"],
colors: ["Red", "White", "Blue"],
collections: "Evening Collection",
material: "Polyester",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=32",
altText: "Off-Shoulder Top",
},
],
rating: 4.7,
numReviews: 18,
},
{
name: "Lace-Trimmed Cami Top",
description:
"A delicate cami top with lace trim and adjustable straps. The lightweight fabric makes it perfect for layering or wearing alone during warmer weather.",
price: 35,
discountPrice: 30,
countInStock: 40,
sku: "TW-W-005",
category: "Top Wear",
brand: "DelicateWear",
sizes: ["S", "M", "L"],
colors: ["Black", "White"],
collections: "Lingerie-Inspired",
material: "Silk Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=33",
altText: "Lace-Trimmed Cami Top",
},
],
rating: 4.8,
numReviews: 22,
},
{
name: "Graphic Print Tee",
description:
"A trendy graphic print tee with a relaxed fit. Pair it with jeans or skirts for a cool and casual look.",
price: 30,
discountPrice: 25,
countInStock: 45,
sku: "TW-W-006",
category: "Top Wear",
brand: "StreetStyle",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Black"],
collections: "Urban Collection",
material: "Cotton",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=34",
altText: "Graphic Print Tee",
},
],
rating: 4.6,
numReviews: 30,
},
{
name: "Ribbed Long-Sleeve Top",
description:
"A cozy ribbed long-sleeve top that offers comfort and style. Perfect for layering during cooler months.",
price: 55,
discountPrice: 50,
countInStock: 30,
sku: "TW-W-007",
category: "Top Wear",
brand: "ComfortFit",
sizes: ["S", "M", "L", "XL"],
colors: ["Gray", "Pink", "Brown"],
collections: "Fall Collection",
material: "Cotton Blend",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=35",
altText: "Ribbed Long-Sleeve Top",
},
],
rating: 4.7,
numReviews: 26,
},
{
name: "Ruffle-Sleeve Blouse",
description:
"A lightweight ruffle-sleeve blouse with a flattering fit. Perfect for a feminine touch to any outfit.",
price: 45,
discountPrice: 40,
countInStock: 20,
sku: "TW-W-008",
category: "Top Wear",
brand: "FeminineWear",
sizes: ["S", "M", "L"],
colors: ["White", "Navy", "Lavender"],
collections: "Summer Collection",
material: "Viscose",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=36",
altText: "Ruffle-Sleeve Blouse",
},
],
rating: 4.5,
numReviews: 19,
},
{
name: "Classic Button-Up Shirt",
description:
"A versatile button-up shirt that can be dressed up or down. Made from soft fabric with a tailored fit, it's perfect for both casual and formal occasions.",
price: 60,
discountPrice: 55,
countInStock: 25,
sku: "TW-W-009",
category: "Top Wear",
brand: "ClassicStyle",
sizes: ["S", "M", "L", "XL"],
colors: ["White", "Light Blue", "Black"],
collections: "Office Collection",
material: "Cotton",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=37",
altText: "Classic Button-Up Shirt",
},
],
rating: 4.8,
numReviews: 25,
},
{
name: "V-Neck Wrap Top",
description:
"A chic v-neck wrap top with a tie waist. Its elegant style makes it perfect for both casual and semi-formal occasions.",
price: 50,
discountPrice: 45,
countInStock: 30,
sku: "TW-W-010",
category: "Top Wear",
brand: "ChicWrap",
sizes: ["S", "M", "L"],
colors: ["Red", "Black", "White"],
collections: "Evening Collection",
material: "Polyester",
gender: "Women",
images: [
{
url: "https://picsum.photos/500/500?random=38",
altText: "V-Neck Wrap Top",
},
],
rating: 4.7,
numReviews: 22,
},
];
module.exports = products;
// backend/seeder.js
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const Product = require("./models/Product");
const User = require("./models/User");
const products = require("./data/products");
dotenv.config();
// Conntect to mongoDB - 連接到 MongoDB
mongoose.connect(process.env.MONGO_URI);
// Function to seed data - 數據初始化函數
const seedData = async () => {
try {
// Clear existing data - 清除現有數據
await Product.deleteMany();
await User.deleteMany();
// Create a default admin User 建立默認管理員用戶
const createdUser = await User.create({
name: "Admin User",
email: "admin@example.com",
password: "123456",
role: "admin",
});
// Assign the default user ID to each product - 將默認用戶 ID 分配給每個產品
const userID = createdUser._id;
const sampleProducts = products.map((product) => {
return { ...product, user: userID };
});
// Insert the products into the database - 將產品插入資料庫
await Product.insertMany(sampleProducts);
console.log("Product data seeded successfully!");
process.exit();
} catch (error) {
console.error("Error seeding the data:", error);
process.exit(1);
}
};
seedData();
// backend/package.json
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node backend/server.js",
"dev": "nodemon backend/server.js",
"seed": "node seeder.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.12.1",
"nodemon": "^3.1.9"
}
}
初始化資料庫資料
- npm run seed
查看 MongoDB Atlas 資料庫
- 查看資料是否有初始化資料庫
All Products (全部產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品,並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
try {
const {
collection,
size,
color,
gender,
minPrice,
maxPrice,
sortBy,
search,
category,
material,
brand,
limit,
} = req.query;
let query = {};
// Filter logic - 過濾邏輯
if (collection && collection.toLocaleLowerCase() !== "all") {
query.collections = collection;
}
if (category && category.toLocaleLowerCase() !== "all") {
query.category = category;
}
if (material) {
query.material = { $in: material.split(",") };
}
if (brand) {
query.brand = { $in: brand.split(",") };
}
if (size) {
query.sizes = { $in: size.split(",") };
}
if (color) {
query.colors = { $in: [color] };
}
if (gender) {
query.gender = gender;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
];
}
// Sort Logic - 排序邏輯
let sort = {};
if (sortBy) {
switch (sortBy) {
case "priceAsc":
sort = { price: 1 };
break;
case "priceDesc":
sort = { price: -1 };
break;
case "popularity":
sort = { rating: -1 };
break;
default:
break;
}
}
// Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
let products = await Product.find(query)
.sort(sort)
.limit(Number(limit) || 0);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
All Products、GET、http://localhost:9000/api/products/ - 測試分類篩選
http://localhost:9000/api/products/?category=Top Wear - 測試分類、材質篩選
http://localhost:9000/api/products/?category=Top Wear&material=Cotton - 測試分類、材質、性別篩選
http://localhost:9000/api/products/?category=Top Wear&material=Cotton&gender=Women - 測試分類、材質、性別、搜尋篩選
http://localhost:9000/api/products/?category=Top Wear&material=Cotton&gender=Women&search=Casual - 測試上升排序
http://localhost:9000/api/products/?sortBy=priceAsc - 測試下降排序
http://localhost:9000/api/products/?sortBy=priceDesc - 測試最大價格
http://localhost:9000/api/products/?maxPrice=30 - 測試最大價格、最小價格
http://localhost:9000/api/products/?maxPrice=50&minPrice=30
Single Product (單一產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品,並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
try {
const {
collection,
size,
color,
gender,
minPrice,
maxPrice,
sortBy,
search,
category,
material,
brand,
limit,
} = req.query;
let query = {};
// Filter logic - 過濾邏輯
if (collection && collection.toLocaleLowerCase() !== "all") {
query.collections = collection;
}
if (category && category.toLocaleLowerCase() !== "all") {
query.category = category;
}
if (material) {
query.material = { $in: material.split(",") };
}
if (brand) {
query.brand = { $in: brand.split(",") };
}
if (size) {
query.sizes = { $in: size.split(",") };
}
if (color) {
query.colors = { $in: [color] };
}
if (gender) {
query.gender = gender;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
];
}
// Sort Logic - 排序邏輯
let sort = {};
if (sortBy) {
switch (sortBy) {
case "priceAsc":
sort = { price: 1 };
break;
case "priceDesc":
sort = { price: -1 };
break;
case "popularity":
sort = { rating: -1 };
break;
default:
break;
}
}
// Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
let products = await Product.find(query)
.sort(sort)
.limit(Number(limit) || 0);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
res.json(product);
} else {
res.status(404).json({ message: "Product Not Found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
Product Details、GET、http://localhost:9000/api/products/67d13557ef893a6a945de533
Similar Products (相似產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品,並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
try {
const {
collection,
size,
color,
gender,
minPrice,
maxPrice,
sortBy,
search,
category,
material,
brand,
limit,
} = req.query;
let query = {};
// Filter logic - 過濾邏輯
if (collection && collection.toLocaleLowerCase() !== "all") {
query.collections = collection;
}
if (category && category.toLocaleLowerCase() !== "all") {
query.category = category;
}
if (material) {
query.material = { $in: material.split(",") };
}
if (brand) {
query.brand = { $in: brand.split(",") };
}
if (size) {
query.sizes = { $in: size.split(",") };
}
if (color) {
query.colors = { $in: [color] };
}
if (gender) {
query.gender = gender;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
];
}
// Sort Logic - 排序邏輯
let sort = {};
if (sortBy) {
switch (sortBy) {
case "priceAsc":
sort = { price: 1 };
break;
case "priceDesc":
sort = { price: -1 };
break;
case "popularity":
sort = { rating: -1 };
break;
default:
break;
}
}
// Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
let products = await Product.find(query)
.sort(sort)
.limit(Number(limit) || 0);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
res.json(product);
} else {
res.status(404).json({ message: "Product Not Found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) => {
const { id } = req.params;
// console.log(id); // 測試
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: "Product Not Found" });
}
const similarProducts = await Product.find({
_id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
gender: product.gender,
category: product.category,
}).limit(4);
res.json(similarProducts);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
Similar Products、GET、http://localhost:9000/api/products/similar/:id - Params > Path Variables > Key、Value
// Postman - Products/Similar Products
// Params > Path Variables > Key、Value
Key: id
Value: 67d13557ef893a6a945de533
Best Seller (暢銷產品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品,並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
try {
const {
collection,
size,
color,
gender,
minPrice,
maxPrice,
sortBy,
search,
category,
material,
brand,
limit,
} = req.query;
let query = {};
// Filter logic - 過濾邏輯
if (collection && collection.toLocaleLowerCase() !== "all") {
query.collections = collection;
}
if (category && category.toLocaleLowerCase() !== "all") {
query.category = category;
}
if (material) {
query.material = { $in: material.split(",") };
}
if (brand) {
query.brand = { $in: brand.split(",") };
}
if (size) {
query.sizes = { $in: size.split(",") };
}
if (color) {
query.colors = { $in: [color] };
}
if (gender) {
query.gender = gender;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
];
}
// Sort Logic - 排序邏輯
let sort = {};
if (sortBy) {
switch (sortBy) {
case "priceAsc":
sort = { price: 1 };
break;
case "priceDesc":
sort = { price: -1 };
break;
case "popularity":
sort = { rating: -1 };
break;
default:
break;
}
}
// Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
let products = await Product.find(query)
.sort(sort)
.limit(Number(limit) || 0);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/best-seller - @路由、使用 GET 方法、API 的路徑
// @dec Retrieve the product with highest rating - 檢索評分最高的產品
// @access Public - @訪問權限 公開
// Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/best-seller", async (req, res) => {
try {
// res.send("this should work"); // 測試
const bestSeller = await Product.findOne().sort({ rating: -1 });
if (bestSeller) {
res.json(bestSeller);
} else {
res.status(404).json({ message: "No best seller found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
res.json(product);
} else {
res.status(404).json({ message: "Product Not Found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) => {
const { id } = req.params;
// console.log(id); // 測試
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: "Product Not Found" });
}
const similarProducts = await Product.find({
_id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
gender: product.gender,
category: product.category,
}).limit(4);
res.json(similarProducts);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
- Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
Best Seller、GET、http://localhost:9000/api/products/best-seller
New Arrivals (新到商品)
// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
const product = new Product({
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
user: req.user._id, // Reference to the admin user who created it
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const {
name,
description,
price,
discountPrice,
countInStock,
category,
brand,
sizes,
colors,
collections,
material,
gender,
images,
isFeatured,
isPublished,
tags,
dimensions,
weight,
sku,
} = req.body;
// Find product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Update product fields - 更新產品欄位
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.discountPrice = discountPrice || product.discountPrice;
product.countInStock = countInStock || product.countInStock;
product.category = category || product.category;
product.brand = brand || product.brand;
product.sizes = sizes || product.sizes;
product.colors = colors || product.colors;
product.collections = collections || product.collections;
product.material = material || product.material;
product.gender = gender || product.gender;
product.images = images || product.images;
product.isFeatured =
isFeatured !== undefined ? isFeatured : product.isFeatured;
product.isPublished =
isPublished !== undefined ? isPublished : product.isPublished;
product.tags = tags || product.tags;
product.dimensions = dimensions || product.dimensions;
product.weight = weight || product.weight;
product.sku = sku || product.sku;
// Save the updated product - 儲存更新的產品
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({
message: "Product not found",
});
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
// Find the product by ID - 根據 ID 查找產品
const product = await Product.findById(req.params.id);
if (product) {
// Remove the product from DB - 從資料庫移除產品
await product.deleteOne();
res.json({ message: "Product remove" });
} else {
res.status(404).json({ message: "Product not found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品,並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
try {
const {
collection,
size,
color,
gender,
minPrice,
maxPrice,
sortBy,
search,
category,
material,
brand,
limit,
} = req.query;
let query = {};
// Filter logic - 過濾邏輯
if (collection && collection.toLocaleLowerCase() !== "all") {
query.collections = collection;
}
if (category && category.toLocaleLowerCase() !== "all") {
query.category = category;
}
if (material) {
query.material = { $in: material.split(",") };
}
if (brand) {
query.brand = { $in: brand.split(",") };
}
if (size) {
query.sizes = { $in: size.split(",") };
}
if (color) {
query.colors = { $in: [color] };
}
if (gender) {
query.gender = gender;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = Number(minPrice);
if (maxPrice) query.price.$lte = Number(maxPrice);
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
];
}
// Sort Logic - 排序邏輯
let sort = {};
if (sortBy) {
switch (sortBy) {
case "priceAsc":
sort = { price: 1 };
break;
case "priceDesc":
sort = { price: -1 };
break;
case "popularity":
sort = { rating: -1 };
break;
default:
break;
}
}
// Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
let products = await Product.find(query)
.sort(sort)
.limit(Number(limit) || 0);
res.json(products);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/best-seller - @路由、使用 GET 方法、API 的路徑
// @dec Retrieve the product with highest rating - 檢索評分最高的產品
// @access Public - @訪問權限 公開
// Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/best-seller", async (req, res) => {
try {
// res.send("this should work"); // 測試
const bestSeller = await Product.findOne().sort({ rating: -1 });
if (bestSeller) {
res.json(bestSeller);
} else {
res.status(404).json({ message: "No best seller found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/new-arrivals - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve latest 8 products - Creation date - @描述 根據創建日期檢索最新的 8 款產品
// @access Public - @訪問權限 公開
// New Arrivals 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/new-arrivals", async (req, res) => {
try {
// Fetch latest 8 products - 獲取最新的8款產品
const newArrivals = await Product.find().sort({ createdAt: -1 }).limit(8);
res.json(newArrivals);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
res.json(product);
} else {
res.status(404).json({ message: "Product Not Found" });
}
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) => {
const { id } = req.params;
// console.log(id); // 測試
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: "Product Not Found" });
}
const similarProducts = await Product.find({
_id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
gender: product.gender,
category: product.category,
}).limit(4);
res.json(similarProducts);
} catch (error) {
console.error(error);
res.status(500).send("Server Error");
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- Add request – 新增請求
New Arrivals、GET、http://localhost:9000/api/products/new-arrivals
製作購物車功能
Cart (購物車)
- Create
- Read
- Update
- Delete
- Merge
說明購物車流程
- Guest User → Creates Cart
- Creates Cart → Logs In (Login Event) →MERGE→ Converts to User Cart
Cart Schema (購物車模式)
Field (欄位) | Type (類型) | Reference (參考) | Required (必填) | Default (預設) | Description (描述) |
user | ObjectId | User | No | – | Reference to the logged-in user owning the cart. |
guestId | String | – | No | – | Unique identifier for a guest user’s cart. |
products | Array of CartItemSchema | – | Yes | – | List of products in the cart. |
totalPrice | Number | – | Yes | 0 | Total price of all items in the cart. |
timestamps | Object | – | Auto-Managed | createdAt, updatedAt |
Automatically managed by Mongoose. |
CartItem Schema (Nested in products) (購物車項目模式 (巢狀於產品中))
Field (欄位) | Type (類型) | Reference (參考) | Required (必填) | Default (預設) | Description (描述) |
productId | ObjectId | Product | Yes | – | Reference to the product added to the cart. |
name | String | – | No | – | Name of the product. |
image | String | – | No | – | URL of the product image. |
price | String | – | No | – | Price of the product. |
size | String | – | No | – | Size of the product (e.g., M, L). |
color | String | – | No | – | Color of the product (e.g., Red, Blue). |
quantity | Number | – | No | 1 | Quantity of the product in the cart. |
Create Cart (建立購物車)
// backend/models/Cart.js
const mongoose = require("mongoose");
const cartItemSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
name: String,
image: String,
price: String,
size: String,
color: String,
quantity: {
type: Number,
default: 1,
},
},
{ _id: false }
);
const cartSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
guestId: {
type: String,
},
products: [cartItemSchema],
totalPrice: {
type: Number,
required: true,
default: 0,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Cart", cartSchema);
// backend/seeder.js
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const Product = require("./models/Product");
const User = require("./models/User");
const Cart = require("./models/Cart");
const products = require("./data/products");
dotenv.config();
// Conntect to mongoDB - 連接到 MongoDB
mongoose.connect(process.env.MONGO_URI);
// Function to seed data - 數據初始化函數
const seedData = async () => {
try {
// Clear existing data - 清除現有數據
await Product.deleteMany();
await User.deleteMany();
await Cart.deleteMany();
// Create a default admin User 建立默認管理員用戶
const createdUser = await User.create({
name: "Admin User",
email: "admin@example.com",
password: "123456",
role: "admin",
});
// Assign the default user ID to each product - 將默認用戶 ID 分配給每個產品
const userID = createdUser._id;
const sampleProducts = products.map((product) => {
return { ...product, user: userID };
});
// Insert the products into the database - 將產品插入資料庫
await Product.insertMany(sampleProducts);
console.log("Product data seeded successfully!");
process.exit();
} catch (error) {
console.error("Error seeding the data:", error);
process.exit(1);
}
};
seedData();
// backend/routes/cartRoute.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) => {
if (userId) {
return await Cart.findOne({ user: userId });
} else if (guestId) {
return await Cart.findOne({ guestId });
}
return null;
};
// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ message: "Product not found" });
// Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
let cart = await getCart(userId, guestId);
// If the cart exists,update it - 如果購物車存在,則更新它
if (cart) {
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// If the product already exists, update the quantity
cart.products[productIndex].quantity += quantity;
} else {
// add new product - 增加新產品
cart.products.push({
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
});
}
// Recalculate the total price - 重新計算總價格
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
// Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
const newCart = await Cart.create({
user: userId ? userId : undefined,
guestId: guestId ? guestId : "guest_" + new Date().getTime(),
products: [
{
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
},
],
totalPrice: product.price * quantity,
});
return res.status(201).json(newCart);
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Cart - Add a request – 新增請求
Create、POST、http://localhost:9000/api/cart - Body > raw
- 測試相同的 guestId
- 測試 userId
// Postman - Cart/Create
// Body > raw
{
"productId": "67d13557ef893a6a945de536",
"size": "M",
"color": "Red",
"quantity": 1
}
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
// Postman - Cart/Create
// Body > raw
// 測試相同的 guestId
{
"guestId": "guest_1741855828784",
"productId": "67d13557ef893a6a945de536",
"size": "M",
"color": "Red",
"quantity": 2
}
// Postman - Cart/Create
// Body > raw
// 測試 userId
{
"userId": "67d13557ef893a6a945de525",
"productId": "67d13557ef893a6a945de536",
"size": "M",
"color": "Red",
"quantity": 2
}
查看 MongoDB Atlas 資料庫
- 查看購物車數量是否有增加
- 查看 user 屬性是否有新增
Update Cart (更新購物車)
// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) => {
if (userId) {
return await Cart.findOne({ user: userId });
} else if (guestId) {
return await Cart.findOne({ guestId });
}
return null;
};
// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ message: "Product not found" });
// Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
let cart = await getCart(userId, guestId);
// If the cart exists,update it - 如果購物車存在,則更新它
if (cart) {
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// If the product already exists, update the quantity
cart.products[productIndex].quantity += quantity;
} else {
// add new product - 增加新產品
cart.products.push({
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
});
}
// Recalculate the total price - 重新計算總價格
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
// Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
const newCart = await Cart.create({
user: userId ? userId : undefined,
guestId: guestId ? guestId : "guest_" + new Date().getTime(),
products: [
{
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
},
],
totalPrice: product.price * quantity,
});
return res.status(201).json(newCart);
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// update quantity - 更新數量
if (quantity > 0) {
cart.products[productIndex].quantity = quantity;
} else {
cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
}
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Cart – 集合: 購物車
- Add request – 新增請求
Update、PUT、http://localhost:9000/api/cart - Body > raw
- Headers > Key、Value
// Postman - Cart/Update
// Body > raw
{
"userId": "67d13557ef893a6a945de525",
"productId": "67d13557ef893a6a945de536",
"size": "M",
"color": "Red",
"quantity": 6
}
// Postman - Cart/Update
// Headers
Key: Authorization
Value: Bearer 你的Token
查看 MongoDB Atlas 資料庫
- 查看購物車數量是否有更新
- 測試數量更改為0時是否會從購物車移除
Delete Cart (移除購物車)
// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) => {
if (userId) {
return await Cart.findOne({ user: userId });
} else if (guestId) {
return await Cart.findOne({ guestId });
}
return null;
};
// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ message: "Product not found" });
// Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
let cart = await getCart(userId, guestId);
// If the cart exists,update it - 如果購物車存在,則更新它
if (cart) {
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// If the product already exists, update the quantity
cart.products[productIndex].quantity += quantity;
} else {
// add new product - 增加新產品
cart.products.push({
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
});
}
// Recalculate the total price - 重新計算總價格
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
// Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
const newCart = await Cart.create({
user: userId ? userId : undefined,
guestId: guestId ? guestId : "guest_" + new Date().getTime(),
products: [
{
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
},
],
totalPrice: product.price * quantity,
});
return res.status(201).json(newCart);
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// update quantity - 更新數量
if (quantity > 0) {
cart.products[productIndex].quantity = quantity;
} else {
cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
}
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) => {
const { productId, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
cart.products.splice(productIndex, 1);
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Cart – 集合: 購物車
- Add request – 新增請求
Delete、DELETE、http://localhost:9000/api/cart - Headers > Key、Value
- Body > raw
// Postman - Cart/Delete
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Cart/Delete
// Body > raw
{
"userId": "67d13557ef893a6a945de525",
"productId": "67d13557ef893a6a945de536",
"size": "M",
"color": "Red"
}
查看 MongoDB Atlas 資料庫
- 查看購物車數量是否有刪除
Read Cart (查看購物車)
// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) => {
if (userId) {
return await Cart.findOne({ user: userId });
} else if (guestId) {
return await Cart.findOne({ guestId });
}
return null;
};
// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ message: "Product not found" });
// Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
let cart = await getCart(userId, guestId);
// If the cart exists,update it - 如果購物車存在,則更新它
if (cart) {
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// If the product already exists, update the quantity
cart.products[productIndex].quantity += quantity;
} else {
// add new product - 增加新產品
cart.products.push({
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
});
}
// Recalculate the total price - 重新計算總價格
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
// Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
const newCart = await Cart.create({
user: userId ? userId : undefined,
guestId: guestId ? guestId : "guest_" + new Date().getTime(),
products: [
{
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
},
],
totalPrice: product.price * quantity,
});
return res.status(201).json(newCart);
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// update quantity - 更新數量
if (quantity > 0) {
cart.products[productIndex].quantity = quantity;
} else {
cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
}
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) => {
const { productId, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
cart.products.splice(productIndex, 1);
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
// @route GET /api/cart - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's or guest user's cart - @描述 獲取已登入用戶或訪客用戶的購物車
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
const { userId, guestId } = req.query;
try {
const cart = await getCart(userId, guestId);
if (cart) {
res.json(cart);
} else {
res.status(404).json({ message: "Cart not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
使用 Postman API 測試工具
- Collections: Cart – 集合: 購物車
- Add request – 新增請求
Cart Details、GET、http://localhost:9000/api/cart - Headers > Key、Value
- Params > Key、Value
// Postman - Cart/Cart Details
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Cart/Cart Details
// Params
Key: userId
Value: 你的 userId
Merge Cart (合併購物車)
// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) => {
if (userId) {
return await Cart.findOne({ user: userId });
} else if (guestId) {
return await Cart.findOne({ guestId });
}
return null;
};
// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
const product = await Product.findById(productId);
if (!product) return res.status(404).json({ message: "Product not found" });
// Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
let cart = await getCart(userId, guestId);
// If the cart exists,update it - 如果購物車存在,則更新它
if (cart) {
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// If the product already exists, update the quantity
cart.products[productIndex].quantity += quantity;
} else {
// add new product - 增加新產品
cart.products.push({
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
});
}
// Recalculate the total price - 重新計算總價格
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
// Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
const newCart = await Cart.create({
user: userId ? userId : undefined,
guestId: guestId ? guestId : "guest_" + new Date().getTime(),
products: [
{
productId,
name: product.name,
image: product.images[0].url,
price: product.price,
size,
color,
quantity,
},
],
totalPrice: product.price * quantity,
});
return res.status(201).json(newCart);
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) => {
const { productId, quantity, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
// update quantity - 更新數量
if (quantity > 0) {
cart.products[productIndex].quantity = quantity;
} else {
cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
}
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) => {
const { productId, size, color, guestId, userId } = req.body;
try {
let cart = await getCart(userId, guestId);
if (!cart) return res.status(404).json({ message: "Cart not found" });
const productIndex = cart.products.findIndex(
(p) =>
p.productId.toString() === productId &&
p.size === size &&
p.color === color
);
if (productIndex > -1) {
cart.products.splice(productIndex, 1);
cart.totalPrice = cart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await cart.save();
return res.status(200).json(cart);
} else {
return res.status(404).json({ message: "Product not found in cart" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Server Error" });
}
});
// @route GET /api/cart - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's or guest user's cart - @描述 獲取已登入用戶或訪客用戶的購物車
// @access Public - @訪問權限 公開
router.get("/", async (req, res) => {
const { userId, guestId } = req.query;
try {
const cart = await getCart(userId, guestId);
if (cart) {
res.json(cart);
} else {
res.status(404).json({ message: "Cart not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route POST /api/cart/merge - @路由、使用 POST 方法、API 的路徑
// @desc Merge guest cart into user cart on login - @描述 登入時將訪客購物車合併到用戶購物車
// @access Private - @訪問權限 私人
router.post("/merge", protect, async (req, res) => {
const { guestId } = req.body;
try {
// Find the guest cart and user cart - 查找訪客購物車和用戶購物車
const guestCart = await Cart.findOne({ guestId });
const userCart = await Cart.findOne({ user: req.user._id });
if (guestCart) {
if (guestCart.products.length === 0) {
return res.status(400).json({ message: "Guest cart is empty" });
}
if (userCart) {
// Merge guest cart into user cart - 將訪客購物車合併到用戶購物車
guestCart.products.forEach((guestItem) => {
const productIndex = userCart.products.findIndex(
(item) =>
item.productId.toString() === guestItem.productId.toString() &&
item.size === guestItem.size &&
item.color === guestItem.color
);
if (productIndex > -1) {
// If the items exists in the user cart, update the quantity - 如果商品在用戶購物車中存在,則更新數量
userCart.products[productIndex].quantity += guestItem.quantity;
} else {
// Otherwise, add the guest item to the cart - 否則,將訪客商品增加到購物車
userCart.products.push(guestItem);
}
});
userCart.totalPrice = userCart.products.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
await userCart.save();
// Remove the guest cart after merging - 合併後刪除訪客購物車
try {
await Cart.findOneAndDelete({ guestId });
} catch (error) {
console.error("Error deleting guest cart:", error);
}
res.status(200).json(guestCart);
} else {
// If the user has no existing cart, assign the guest cart to the user - 如果用戶沒有現有的購物車,則將訪客購物車分配給用戶
guestCart.user = req.user._id;
guestCart.guestId = undefined;
await guestCart.save();
res.status(200).json(guestCart);
}
} else {
if (userCart) {
// Guest cart has already been merged, return user cart - 訪客購物車已經合併,返回用戶購物車
return res.status(200).json(userCart);
}
res.status(404).json({ message: "Guest cart not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
初始化資料庫資料
- npm run seed
查看 MongoDB Atlas 資料庫
- 查看資料是否有初始化資料庫
使用 Postman API 測試工具
- Collections: Products – 集合: 產品
- 取得產品 _id
All Products、GET、http://localhost:9000/api/products/ - Collections: Cart – 集合: 購物車
- 建立購物車
Create、POST、http://localhost:9000/api/cart - Body > raw
- Add request – 新增請求
Merge、POST、http://localhost:9000/api/cart/merge - Body > raw
- Headers > Key、Value
// Postman - Cart/Create
// Body > raw
{
"productId": "67d5186884e2f1407fcf0388",
"size": "S",
"color": "Red",
"quantity": 1
}
// Postman - Cart/Merge
// Body > raw
{
"guestId": "guest_1742019216937"
}
// Postman - Cart/Merge
// Headers
Key: Authorization
Value: Bearer 你的Token
製作結帳功能
Checkout (結帳)
- Create
- Pay
- Finalize
CheckoutItem Schema (結帳項目模式)
Field (欄位) | Type (類型) | Required (必填) | Description (描述) |
productId | ObjectId (ref: Product) | Yes | The ID of the product |
name | String | Yes | The name of the product. |
image | String | Yes | The URL of the product image. |
price | Number | Yes | The price of the product. |
size | String | No | The size of the product (e.g., apparel). |
color | String | No | The color of the product. |
quantity | Number | Yes | The quantity of the product in the cart. |
Checkout Schema (結帳模式)
Field (欄位) | Type (類型) | Required (必填) | Description (描述) |
user | ObjectId (ref: User) | Yes | The ID of the user associated with the checkout. |
checkoutItems | Array of CheckoutItems | Yes | The list of items included in the checkout. |
shippingAddress | Object | Yes | Contains shipping details (address, city, etc.). |
address | String | Yes | Shipping address of the user. |
city | String | Yes | City for shipping. |
postalCode | String | Yes | Postal code for shipping. |
country | String | Yes | Country for shipping. |
paymentMethod | String | Yes | Payment method used (e.g., Paypal). |
totalPrice | Number | Yes | Total price of all items in the checkout. |
isPaid | Boolean | No | Indicates whether the checkout is paid. |
paidAt | Date | No | Timestamp when the payment was made. |
paymentStatus | String | No | Status of the payment (pending, paid, etc.). |
paymentDetails | Mixed | No | Stores details about the payment (e.g., transaction ID). |
isFinalized | Boolean | No | Indicates if the checkout has been converted to an order. |
finalizedAt | Date | No | Timestamp when the checkout was finalized. |
timestamps | Date | Auto | Mongoose will auto-create createdAt and updatedAt. |
OrderItem Schema (訂單項目模式)
Field (欄位) | Type (類型) | Required (必填) | Description (描述) |
productId | ObjectId (ref: Product) | Yes | The ID of the product. |
name | String | Yes | The name of the product. |
image | String | Yes | The URL of the product image. |
price | Number | Yes | The price of the product. |
size | String | No | Size of the product (e.g., apparel). |
color | String | No | Color of the product. |
quantity | Number | Yes | The quantity of the product in the order. |
Order Schema (訂單模式)
Field (欄位) | Type (類型) | Required (必填) | Description (描述) |
user | ObjectId (ref: User) | Yes | The ID of the user who placed the order. |
orderItems | Array of OrderItems | Yes | The list of items included in the order. |
shippingAddress | Object | Yes | Shipping details including address, ciy, etc. |
address | String | Yes | The full shipping address. |
city | String | Yes | The ciy for shipping. |
postalCode | String | Yes | The postal code for shipping. |
country | String | Yes | The country for shipping. |
paymentMethod | String | Yes | Payment method used (e.g., Credit Card, PayPal). |
totalPrice | Number | Yes | Total price of all items in the order. |
isPaid | Boolean | No | Indicates whether the order has been paid. |
paidAt | Date | No | Timestamp when the payment was made. |
isDelivered | Boolean | No | Indicates whether the order has been delivered. |
deliveredAt | Date | No | Timestamp when the order was delivered. |
paymentStatus | String | No | Payment status (pending, paid, etc.). |
status | String | No | Order status (Processing, Shipped, Delivered, Cancelled). |
timestamps | Date | Auto | Auto-created fields for createdAt and updatedAt. |
// backend/models/Checkout.js
// 少了 quantity
const mongoose = require("mongoose");
const checkoutItemSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
name: {
type: String,
required: true,
},
image: {
type: String,
requied: true,
},
price: {
type: Number,
requied: true,
},
},
{ _id: false }
);
const checkoutSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
requied: true,
},
checkoutItems: [checkoutItemSchema],
shippingAddress: {
address: { type: String, required: true },
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true },
},
paymentMethod: {
type: String,
required: true,
},
totalPrice: {
type: Number,
required: true,
},
isPaid: {
type: Boolean,
default: false,
},
paidAt: {
type: Date,
},
paymentStatus: {
type: String,
default: "pending",
},
paymentDetails: {
type: mongoose.Schema.Types.Mixed, // store payment-related details(transaction ID, paypal response) - 存儲與支付相關的詳細資訊(如交易 ID 和 PayPal 回應)
},
isFinalized: {
type: Boolean,
default: false,
},
finalizedAt: {
type: Date,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Checkout", checkoutSchema);
// backend/models/Order.js
const mongoose = require("mongoose");
const orderItemSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
name: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
size: String,
color: String,
quantity: {
type: Number,
required: true,
},
},
{ _id: false }
);
const orderSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
orderItems: [orderItemSchema],
shippingAddress: {
address: { type: String, required: true },
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true },
},
paymentMethod: {
type: String,
required: true,
},
totalPrice: {
type: Number,
required: true,
},
isPaid: {
type: Boolean,
default: false,
},
paidAt: {
type: Date,
},
isDelivered: {
type: Boolean,
default: false,
},
deliveredAt: {
type: Date,
},
paymentStatus: {
type: String,
default: "pending",
},
status: {
type: String,
enum: ["Processing", "Shipped", "Delivered", "Cancelled"],
default: "Processing",
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Order", orderSchema);
// backend/routes/checkoutRoutes.js
// 創建最終訂單有錯誤
const express = require("express");
const Checkout = require("../models/Checkout");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/checkout - @路由、使用 POST 方法、API 的路徑
// @desc Create a new checkout session - @描述 創建新的結帳會話
// @access Private - @訪問權限 私人
router.post("/", protect, async (req, res) => {
const { checkoutItems, shippingAddress, paymentMethod, totalPrice } =
req.body;
if (!checkoutItems || checkoutItems.length === 0) {
return res.status(400).json({ message: "No items in checkout" });
}
try {
// Create a new checkout session - 創建新的結帳會話
const newCheckout = await Checkout.create({
user: req.user._id,
checkoutItems: checkoutItems,
shippingAddress,
paymentMethod,
totalPrice,
paymentStatus: "Pending",
isPaid: false,
});
console.log(`Checkout created for user: ${req.user._id}`);
res.status(201).json(newCheckout);
} catch (error) {
console.error("Error creating checkout session:", error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/checkout/:id/pay - @路由、使用 PUT 方法、API 的路徑
// @desc Update checkout to mark as paid after successful payment - @描述 要在成功付款後更新結帳狀態為已付款
// @access Private
router.put("/:id/pay", protect, async (req, res) => {
const { paymentStatus, paymentDetails } = req.body;
try {
const checkout = await Checkout.findById(req.params.id);
if (!checkout) {
return res.status(404).json({ message: "Checkout not found" });
}
if (paymentStatus === "paid") {
checkout.isPaid = true;
checkout.paymentStatus = paymentStatus;
checkout.paymentDetails = paymentDetails;
checkout.paidAt = Date.now();
await checkout.save();
res.status(200).json(checkout);
} else {
res.status(400).json({ message: "Invalid Payment Status" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route POST /api/checkout/:id/finalize - @路由、使用 POST 方法、API 的路徑
// @desc Finalize checkout and convert to an order after payment confirmation - @描述 將結帳完成並在付款確認後轉換為訂單
// @access Private - @訪問權限 私人
router.post("/:id/finalize", protect, async (req, res) => {
try {
const checkout = await Checkout.findById(req.params.id);
if (!checkout) {
return res.status(404).json({ message: "Checkout not found" });
}
if (checkout.isPaid && !checkout.isFinalized) {
// Create final order based on the checkout details - 根據結帳詳情創建最終訂單
const finalOrder = await Order.create({
user: checkout.user,
orderItems: checkout.orderItems,
shippingAddress: checkout.shippingAddress,
paymentMethod: checkout.paymentMethod,
totalPrice: checkout.totalPrice,
isPaid: true,
paidAt: checkout.paidAt,
isDelivered: false,
paymentStatus: "paid",
paymentDetails: checkout.paymentDetails,
});
// Mark the checkout as finalized - 將結帳標記為已完成
checkout.isFinalized = true;
checkout.finalizedAt = Date.now();
await checkout.save();
// Delete the cart associated with the user - 刪除與用戶相關的購物車
await Cart.findOneAndDelete({ user: checkout.user });
res.status(201).json(finalOrder);
} else if (checkout.isFinalized) {
res.status(400).json({ message: "Checkout already finalized" });
} else {
res.status(400).json({ message: "Checkout is not paid" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Checkout - Add a request – 新增請求
Create、POST、http://localhost:9000/api/checkout - Headers > Key、Value
- Body > raw
- Add request – 新增請求
Pay、PUT、http://localhost:9000/api/checkout/:id/pay - Params > Path Variables > Key、Value
- Headers > Key、Value
- Body > raw
- Duplicate – 複製 Pay 請求
Finalize、POST、http://localhost:9000/api/checkout/:id/finalize - Body > raw
清空內容
// Postman - Checkout/Create
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Checkout/Create
// Body > raw
{
"checkoutItems": [
{
"productId": "67d5186884e2f1407fcf0388",
"name": "Classic T-shirt",
"image": "https://picsum.photos/seed/denim1/500/500",
"price": 19
}
],
"shippingAddress": {
"address": "123 main street",
"city": "New York",
"postalCode": "10001",
"country": "USA"
},
"paymentMethod": "PayPal",
"totalPrice": 19
}
// Postman - Checkout/Pay
// Params > Path Variables > Key、Value
Key: id
Value: 67d782e2320689ef987bfad1
// Postman - Checkout/Pay
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Checkout/Pay
// Body > raw
{
"paymentStatus": "paid",
"paymentDetails": {
"transactionId": "txn_123456789",
"paymentGateway": "PayPal",
"amount": 19,
"currency": "USD"
}
}
// Postman - Checkout/Finalize
// Body > raw - 清空內容
查看 MongoDB Atlas
- Browse collections (瀏覽集合) > checkouts
- Browse collections > orders
發現 orders > orderItems 有錯誤
路由 checkoutRoutes.js 除錯 - 測試 isFinalized 結帳完成功能
出現錯誤、開始除錯
// backend/routes/checkoutRoutes.js
const express = require("express");
const Checkout = require("../models/Checkout");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// @route POST /api/checkout - @路由、使用 POST 方法、API 的路徑
// @desc Create a new checkout session - @描述 創建新的結帳會話
// @access Private - @訪問權限 私人
router.post("/", protect, async (req, res) => {
const { checkoutItems, shippingAddress, paymentMethod, totalPrice } =
req.body;
if (!checkoutItems || checkoutItems.length === 0) {
return res.status(400).json({ message: "No items in checkout" });
}
try {
// Create a new checkout session - 創建新的結帳會話
const newCheckout = await Checkout.create({
user: req.user._id,
checkoutItems: checkoutItems,
shippingAddress,
paymentMethod,
totalPrice,
paymentStatus: "Pending",
isPaid: false,
});
console.log(`Checkout created for user: ${req.user._id}`);
res.status(201).json(newCheckout);
} catch (error) {
console.error("Error creating checkout session:", error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/checkout/:id/pay - @路由、使用 PUT 方法、API 的路徑
// @desc Update checkout to mark as paid after successful payment - @描述 要在成功付款後更新結帳狀態為已付款
// @access Private
router.put("/:id/pay", protect, async (req, res) => {
const { paymentStatus, paymentDetails } = req.body;
try {
const checkout = await Checkout.findById(req.params.id);
if (!checkout) {
return res.status(404).json({ message: "Checkout not found" });
}
if (paymentStatus === "paid") {
checkout.isPaid = true;
checkout.paymentStatus = paymentStatus;
checkout.paymentDetails = paymentDetails;
checkout.paidAt = Date.now();
await checkout.save();
res.status(200).json(checkout);
} else {
res.status(400).json({ message: "Invalid Payment Status" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route POST /api/checkout/:id/finalize - @路由、使用 POST 方法、API 的路徑
// @desc Finalize checkout and convert to an order after payment confirmation - @描述 將結帳完成並在付款確認後轉換為訂單
// @access Private - @訪問權限 私人
router.post("/:id/finalize", protect, async (req, res) => {
try {
const checkout = await Checkout.findById(req.params.id);
if (!checkout) {
return res.status(404).json({ message: "Checkout not found" });
}
if (checkout.isPaid && !checkout.isFinalized) {
// Create final order based on the checkout details - 根據結帳詳情創建最終訂單
const finalOrder = await Order.create({
user: checkout.user,
orderItems: checkout.checkoutItems,
shippingAddress: checkout.shippingAddress,
paymentMethod: checkout.paymentMethod,
totalPrice: checkout.totalPrice,
isPaid: true,
paidAt: checkout.paidAt,
isDelivered: false,
paymentStatus: "paid",
paymentDetails: checkout.paymentDetails,
});
// Mark the checkout as finalized - 將結帳標記為已完成
checkout.isFinalized = true;
checkout.finalizedAt = Date.now();
await checkout.save();
// Delete the cart associated with the user - 刪除與用戶相關的購物車
await Cart.findOneAndDelete({ user: checkout.user });
res.status(201).json(finalOrder);
} else if (checkout.isFinalized) {
res.status(400).json({ message: "Checkout already finalized" });
} else {
res.status(400).json({ message: "Checkout is not paid" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
使用 Postman API 工具
- Checkout/Create 除錯
// Postman - Checkout/Create
// Body > raw
{
"checkoutItems": [
{
"productId": "67d5186884e2f1407fcf0388",
"name": "Classic T-shirt",
"image": "https://picsum.photos/seed/denim1/500/500",
"price": 19,
"quantity": 1
}
],
"shippingAddress": {
"address": "123 main street",
"city": "New York",
"postalCode": "10001",
"country": "USA"
},
"paymentMethod": "PayPal",
"totalPrice": 19
}
Checkout Schema 除錯
// backend/models/Checkout.js
const mongoose = require("mongoose");
const checkoutItemSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
name: {
type: String,
required: true,
},
image: {
type: String,
requied: true,
},
price: {
type: Number,
requied: true,
},
quantity: {
type: Number,
required: true,
},
},
{ _id: false }
);
const checkoutSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
requied: true,
},
checkoutItems: [checkoutItemSchema],
shippingAddress: {
address: { type: String, required: true },
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true },
},
paymentMethod: {
type: String,
required: true,
},
totalPrice: {
type: Number,
required: true,
},
isPaid: {
type: Boolean,
default: false,
},
paidAt: {
type: Date,
},
paymentStatus: {
type: String,
default: "pending",
},
paymentDetails: {
type: mongoose.Schema.Types.Mixed, // store payment-related details(transaction ID, paypal response) - 存儲與支付相關的詳細資訊(如交易 ID 和 PayPal 回應)
},
isFinalized: {
type: Boolean,
default: false,
},
finalizedAt: {
type: Date,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Checkout", checkoutSchema);
查看 MongoDB Atlas
- 查看 checkout 集合是否能正確顯示 quantity 屬性
使用 Postman API 測試工具
- 複製 Checkout/Create 的 _id
- 更新 Checkout/Pay 的 id
- 更新 Checkout/Finalize 的 id
// Postman - Checkout/Pay
// Params > Path Variables > Key、Value
Key: id
Value: 67d78fdaae89363d137c322e
// Postman - Checkout/Finalize
// Params > Path Variables > Key、Value
Key: id
Value: 67d78fdaae89363d137c322e
查看 MongoDB Atlas 資料庫
- 查看 orders > orderItems 資料是否能正常建立、不會被結帳操作而被清空
製作訂單功能
// backend/routes/orderRoutes.js
const express = require("express");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();
// @route GET /api/orders/my-orders - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's orders - @描述 獲取登入用戶的訂單
// @access Private - @訪問權限 私人
router.get("/my-orders", protect, async (req, res) => {
try {
// Find orders for the authenticated user - 查找已驗證用戶的訂單
const orders = await Order.find({ user: req.user._id }).sort({
createdAt: -1,
}); // sort by most recent orders - 按最新訂單排序
res.json(orders);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route GET /api/orders/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get order details by ID - @描述 根據 ID 獲取訂單詳情
// @access Private - @訪問權限 私人
router.get("/:id", protect, async (req, res) => {
try {
const order = await Order.findById(req.params.id).populate(
"user",
"name email"
);
if (!order) {
return res.status(404).json({ message: "Order not found" });
}
// Return the full order details - 返回完整的訂單詳情
res.json(order);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Orders - Add a request – 新增請求
My orders、GET、http://localhost:9000/api/orders/my-orders - Headers > Key、Value
- Duplicate – 複製 My orders 請求
Order Details、GET、http://localhost:9000/api/orders/:id
複製 Orders/My orders 的 _id - Params > Path Variables > Key、Value
// Postman - Orders/My orders
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Orders/Order Details
// Params > Path Variables > Key、Value
Key: id
Value: 67d789fd320689ef987bfada
製作圖片上傳功能 (管理員)
- upload images to the products in the admin section (在管理區域將圖片上傳至產品)
- Cloudinary – Getting Started (開始使用)
- View API Keys
- 安裝相關套件 multer、cloudinary、streamifier
// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://<username>:<password>@<cluster-address>/<database-name>?retryWrites=true&w=majority&appName=<app-name>
JWT_SECRET=你的JWT密鑰
CLOUDINARY_CLOUD_NAME=你的Clound name
CLOUDINARY_API_KEY=你的API Key
CLOUDINARY_API_SECRET=你的API secret
// backend - 後端
// TERMINAL - 終端機
npm install multer cloudinary streamifier
// backend/routes/uploadRoutes.js
const express = require("express");
const multer = require("multer");
const cloudinary = require("cloudinary").v2;
const streamifier = require("streamifier");
require("dotenv").config();
const router = express.Router();
// Cloudinary Configuration - Cloudinary 配置
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
// Multer setup using memory storage - 使用記憶體存儲的 Multer 設置
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post("/", upload.single("image"), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: "No file uploaded" });
}
// Function to handle the stream upload to Cloudinary - 處理將資料流上傳到 Cloudinary 的函式
const streamUpload = (fileBuffer) => {
return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream((error, result) => {
if (result) {
resolve(result);
} else {
reject(error);
}
});
// Use streamifier to convert file buffer to a stream - 使用 streamifier 將檔案緩衝區轉換為資料流
streamifier.createReadStream(fileBuffer).pipe(stream);
});
};
// Call the streamUpload function - 呼叫 streamUpload 函數
const result = await streamUpload(req.file.buffer);
// Respond with the uploaded image URL - 回應並提供上傳的圖片 URL
res.json({ imageUrl: result.secure_url });
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Upload - Add a request – 新增請求
Create、POST、http://localhost:9000/api/upload - Headers > Key、Value
- Body > form-data > Key、Value
// Postman - Upload/Create
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Upload/Create
// Body > form-data
Key: image / File
Value: Select files > New file from local machine
Photo by Ayo Ogunseinde on Unsplash
製作電子信箱訂閱功能
// backend/models/Subscriber.js
const mongoose = require("mongoose");
const subscriberSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
subscribedAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model("Subscriber", subscriberSchema);
// backend/routes/subscribeRoute.js
const express = require("express");
const router = express.Router();
const Subscriber = require("../models/Subscriber");
// @route POST /api/subscribe - @路由、使用 POST 方法、API 的路徑
// @desc Handle newsletter subscription - @描述 處理電子報訂閱
// @access Public - @訪問權限 公開
router.post("/subscribe", async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: "Email is required" });
}
try {
// Check if the email is already subscribed
let subscriber = await Subscriber.findOne({ email });
if (subscriber) {
return res.status(400).json({ message: "Email is already subscribed" });
}
// Create a new subscriber - 創建新的訂閱者
subscriber = new Subscriber({ email });
await subscriber.save();
res
.status(201)
.json({ message: "Successfully subscribed to the newsletter!" });
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Subscribe - Add a request – 新增請求
subscribe、POST、http://localhost:9000/api/subscribe - Body > raw
// Postman - Subscribe/subscribe
// Body > raw
{
"email": "hi@example.com"
}
製作用戶管理功能 (管理員)
// backend/routes/adminRoutes.js
const express = require("express");
const User = require("../models/User");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route GET /api/admin/users - @路由、使用 GET 方法、API 的路徑
// @desc Get all users (Admin only) - @描述 取得所有用戶 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) => {
try {
const users = await User.find({});
res.json(users);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route POST /api/admin/users - @路由、使用 POST 方法、API 的路徑
// @desc Add a new user (admin only) - @描述 新增用戶 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) => {
const { name, email, password, role } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ message: "User already exists" });
}
user = new User({
name,
email,
password,
role: role || "customer",
});
await user.save();
res.status(201).json({ message: "User created successfully", user });
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/admin/users/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update user info (admin only) - Name, email and role - @描述 更新用戶資訊 (僅限管理員) - 姓名、電子郵件和角色
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (user) {
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
user.role = req.body.role || user.role;
}
const updatedUser = await user.save();
res.json({ message: "User updated successfully", user: updatedUser });
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route DELETE /api/admin/users/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a user - @描述 刪除用戶
// @access Private/Admin - @訪問權限 私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (user) {
await user.deleteOne();
res.json({ message: "User deleted successfully" });
} else {
res.status(404).json({ message: "User not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);
// Admin - 管理員
app.use("/api/admin/users", adminRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Admin User - Add a request – 新增請求
All users、GET、http://localhost:9000/api/admin/users
Create User、POST、http://localhost:9000/api/admin/users - Headers > Key、Value
- Body > raw
- Duplicate – 複製 Create User 請求
Update User、PUT、http://localhost:9000/api/admin/users/:id - Headers > Key、Value
- Body > raw
- 複製 MongoDB Atlas 使用者的 _id
- Params > Path Variables > Key、Value
- Duplicate – 複製 Update User 請求
Delete User、DELETE、http://localhost:9000/api/admin/users/:id
// Postman - Admin User/All users
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Admin User/Create User
// Body > raw
{
"name": "Jimmy",
"email": "jimmy@example.com",
"password": "123456"
}
// Postman - Admin User/Update User
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Admin User/Update User
// Body > raw
{
"name": "Jimmy1",
"email": "jimmy1@example.com"
}
// Postman - Admin User/Update User
// Params > Path Variables > Key、Value
Key: id
Value: 67d8eafb4e9e9ace1c78eb25
查看 MongoDB Atlas
- Browse Collection (瀏覽集合) > users
製作列出所有產品功能 (管理員)
// backend/routes/productAdminRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route GET /api/admin/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products (Admin only) - @描述 取得所有產品 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) => {
try {
const products = await Product.find({});
res.json(products);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");
const productAdminRoutes = require("./routes/productAdminRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);
// Admin - 管理員
app.use("/api/admin/users", adminRoutes);
app.use("/api/admin/products", productAdminRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Admin Product - Add a request – 新增請求
Products、GET、http://localhost:9000/api/admin/products - Headers > Key、Value
// Postman - Admin Product/Products
// Headers
Key: Authorization
Value: Bearer 你的Token
製作訂單管理功能 (管理員)
// backend/routes/adminOrderRoutes.js
const express = require("express");
const Order = require("../models/Order");
const { protect, admin } = require("../middleware/authMiddleware");
const router = express.Router();
// @route GET /api/admin/orders - @路由、使用 GET 方法、API 的路徑
// @desc Get all order (Admin only) - @描述 取得所有訂單 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) => {
try {
const orders = await Order.find({}).populate("user", "name email");
res.json(orders);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route PUT /api/admin/orders/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update order status - @描述 更新訂單狀態
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (order) {
order.status = req.body.status || order.status;
order.isDelivered =
req.body.status === "Delivered" ? true : order.isDelivered;
order.deliveredAt =
req.body.status === "Delivered" ? Date.now() : order.deliveredAt;
const updatedOrder = await order.save();
res.json(updatedOrder);
} else {
res.status(404).json({ message: "Order not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
// @route DELETE /api/admin/orders/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete an order - @描述 刪除訂單
// @access Private/Admin - @訪問權限 私人/管理員
router.delete("/:id", protect, admin, async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (order) {
await order.deleteOne();
res.json({ message: "Order removed" });
} else {
res.status(404).json({ message: "Order not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server Error" });
}
});
module.exports = router;
// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");
const productAdminRoutes = require("./routes/productAdminRoutes");
const adminOrderRoutes = require("./routes/adminOrderRoutes");
const app = express();
app.use(express.json());
app.use(cors());
dotenv.config();
// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;
// Connect to MongoDB - 連接到 MongoDB
connectDB();
app.get("/", (req, res) => {
res.send("WELCOME TO RABBIT API!");
});
// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);
// Admin - 管理員
app.use("/api/admin/users", adminRoutes);
app.use("/api/admin/products", productAdminRoutes);
app.use("/api/admin/orders", adminOrderRoutes);
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
使用 Postman API 測試工具
- Create new collection – 建立新的集合
Admin Orders - Add a request – 新增請求
Orders、GET、http://localhost:9000/api/admin/orders - Headers > Key、Value
- Duplicate – 複製 Orders 請求
Update Status、PUT、http://localhost:9000/api/admin/orders/:id - 複製 MongoDB Atlas 訂單的 _id
- Params > Path Variables > Key、Value
- Body > raw
測試修改狀態 Processing - Duplicate – 複製 Update Status 請求
Delete、DELETE、http://localhost:9000/api/admin/orders/:id - 複製 MongoDB Atlas 訂單的 _id
測試刪除訂單
// Postman - Admin Orders/Orders
// Headers
Key: Authorization
Value: Bearer 你的Token
// Postman - Admin Orders/Update Status
// Params > Path Variables > Key、Value
Key: id
Value: 67d789fd320689ef987bfada
// Postman - Admin Orders/Update Status
// Body > raw
{
"status": "Delivered"
}
查看 MongoDB Atlas 資料庫
- Browse collections (瀏覽集合) > orders
查看是否有刪除訂單
以上完成這個專案後端的部分。