Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
# Final Project for CS3321 Spring 2025

电商平台管理系统

CS3321 数据库技术大作业

## 项目概述

本项目旨在设计和实现一个电商平台管理系统。项目采用前后端分离的架构,前端使用 Vue3,后端使用 FastAPI,数据库使用 MySQL+PyMySQL。

支持的功能:
- 用户端:
- 商品浏览
- 商品搜索
- 商品详情
- 购物车管理
- 下单
- (模拟)支付
- 订单查询
- 商家端:
- 商品管理
- 店铺管理
- 订单管理
- 管理员端:
- 审核管理

## 项目结构

```
doc/ # 需求分析、概念设计、ER图等文档
src/ # 源代码
├── backend/ # 后端代码
├── frontend/ # 前端代码
└── static/ # 静态资源
```

其它请见各个目录下的 README.md 文件。
68 changes: 68 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# README: src/

## 运行后端

**注意** 运行python脚本请以 `src` 目录作为工作目录,以避免导入错误。

要运行后端需要的步骤如下:

1. 安装需要的 Python 包。推荐使用 3.13 版本的 Python。可以使用以下命令安装:

```bash
pip install -r backend/requirements.txt
```
2. 创建数据库。开发时使用了 MySQL 8.4.5。请按照`backend/README.md`中的说明创建数据库并创建对应的`.env`文件。
3. 运行后端。可以使用以下命令:

```bash
uvicorn backend.main:app --reload
```

这将启动 FastAPI 后端应用,并监听在默认的 `http://127.0.0.1:8000 ` 地址。


**如何创建数据库schema和样例数据**

在运行后端之前,需要先创建数据库 schema。`backend/scripts` 内包含了创建数据库 schema 和样例数据的脚本。

重置数据库(清空并重新创建schema)。可以选择操作 dev 或 test 数据库。

**如何运行后端测试**

```bash
python -m unittest discover -s backend/test
```

```bash
python -m backend.scripts.reset dev
```

```bash
python -m backend.scripts.reset test
```

创建数据库 schema 和样例数据。这将在 dev 数据库中创建 schema,并插入一些样例数据。

```bash
python -m backend.scripts.make_demo_data
```

创建

## 运行前端

运行前端需要的步骤如下:

1. 在 `frontend` 目录下安装需要的 Node.js 包。可以使用以下命令安装:

```bash
cd frontend
npm install
```
2. 运行前端。可以使用以下命令:

```bash
npm run dev
```
这将启动 Vite 开发服务器,并监听在默认的 `http://localhost:5173/` 地址。
3. 在浏览器中访问 `http://localhost:5173/` ,即可看到前端页面。
34 changes: 34 additions & 0 deletions src/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# 后端

## 后端结构

后端使用 FastAPI 框架,主要负责处理业务逻辑、数据库交互和 API 提供。以下是后端的主要目录结构:

```
├── app
│ ├── api # API 相关代码
│ ├── core # 核心配置和设置
│ ├── crud # 数据库 CRUD 操作
│ ├── dependencies # 依赖注入和共享资源
│ ├── schemas # Pydantic 模型和数据验证
│ ├── services # 业务逻辑和服务层
│ └── utils # 工具函数和通用方法
├── logs
├── scripts # 数据库脚本和初始化脚本
├── sql # SQL 相关文件,包括DDL、触发器等
│ ├── ddl
│ ├── trigger
│ ├── triggers
│ └── utils
└── test # 测试代码
├── integration # 集成测试
│ ├── api_endpoints
│ ├── crud
│ ├── services
│ └── test_sql_objects
└── unit # 单元测试
├── api
├── core
├── crud
└── service
```


## MySQL 数据库配置

本文档介绍在本地环境成功安装 MySQL 服务器后,如何为本项目创建数据库、用户以及配置必要的环境变量。
Expand Down
6 changes: 2 additions & 4 deletions src/backend/app/core/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
import os
from loguru import logger
from pathlib import Path
import datetime # 用于日志文件名中的日期
import logging # ⭐ 导入标准 logging 模块
import logging

# --- 日志文件存放路径配置 ---
BACKEND_ROOT_DIR = Path(__file__).resolve().parent.parent.parent
LOGS_DIR = BACKEND_ROOT_DIR / "logs"


# --- Loguru 拦截处理器 ---
# ⭐ 定义一个处理器,将标准 logging 模块的日志记录转发给 Loguru
class InterceptHandler(logging.Handler):
def __init__(self, extra_depth=6):
super().__init__()
Expand Down Expand Up @@ -86,7 +84,7 @@ def setup_logging():
diagnose=True
)

# 5. 配置标准 logging 模块以使用 InterceptHandler
# 5. 配置标准 logging 模块以使用 InterceptHandler
# 这会捕获由 Uvicorn 等库发出的日志
logging.basicConfig(handlers=[InterceptHandler()], level=max(logging.INFO, numeric_log_level), force=True)

Expand Down
13 changes: 2 additions & 11 deletions src/backend/app/crud/address_crud.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# src/backend/app/crud/address_crud.py
from typing import Optional, List, Dict, Any
from sqlalchemy import Connection, text, exc # 导入 exc 用于异常处理
from loguru import logger # 假设在模块级别导入了 logger
from sqlalchemy import Connection, text, exc
from loguru import logger

from backend.app.schemas.address_schema import AddressCreateRequest, AddressUpdateRequest

Expand Down Expand Up @@ -90,12 +90,6 @@ def create_address(
if new_address_id is None: # Fallback if lastrowid is not supported/returned
logger.warning(
"lastrowid not available after address insert. This might indicate an issue or specific DB driver behavior.")
# As a fallback, you might try to select the row if there's a unique constraint you can use,
# but it's less reliable than lastrowid. For now, we'll proceed assuming lastrowid works or get_address_by_id will be used.
# If create_address *must* return the created object, and lastrowid fails, this is problematic.
# One option is to not call get_address_by_id if new_address_id is None and return None or raise.
# For now, we let it try get_address_by_id.

logger.info(f"Address created for UserID {user_id} with AddressID {new_address_id} by ActorID {actor_id}.")
# 获取并返回新创建的地址的完整信息
return self.get_address_by_id(conn, address_id=new_address_id, actor_id=actor_id) # type: ignore
Expand Down Expand Up @@ -198,9 +192,6 @@ def update_address_details(
logger.info(f"No fields to update for AddressID {address_id}. Returning current data.")
return self.get_address_by_id(conn, address_id=address_id, actor_id=actor_id)

# DDL 中没有 LastUpdatedDate 字段,如果需要,应添加并在这里更新
# update_fields.append("LastUpdatedDate = UTC_TIMESTAMP()")

update_stmt_str = f"UPDATE {self.table_name} SET {', '.join(update_fields)} WHERE AddressID = :AddressID_param"

try:
Expand Down
9 changes: 1 addition & 8 deletions src/backend/app/crud/cartitem_crud.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# src/backend/app/crud/cart_item_crud.py
from typing import Optional, List, Dict, Any
from sqlalchemy import Connection, text, exc # Import exc for exception handling
from loguru import logger # Using loguru as per previous discussions
import datetime
from loguru import logger


# Assuming your Pydantic schemas would be defined elsewhere, e.g., cart_item_schema.py
# For this CRUD, we'll use basic types for input where schemas would normally be used.

class CartItemCRUD:
__instance: Optional["CartItemCRUD"] = None

Expand Down Expand Up @@ -126,9 +122,6 @@ def add_item_to_cart(
SET Quantity = :quantity, PriceAtAddition = :price_at_addition, AddedDate = UTC_TIMESTAMP()
WHERE CartItemID = :cart_item_id
""")
# Note: Updating PriceAtAddition on quantity change is a business decision.
# Some carts keep the original price, others update. Here, we update.
# AddedDate is also updated to reflect the "last modified" time for this cart entry.
try:
conn.execute(update_stmt, {
"quantity": new_quantity,
Expand Down
4 changes: 1 addition & 3 deletions src/backend/app/crud/order_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
from decimal import Decimal # For monetary values
import datetime

from loguru import logger # Assuming loguru is used
from loguru import logger

# Import Pydantic schemas for type hinting where appropriate,
# though CRUD methods primarily deal with basic types and dicts.
from backend.app.schemas.order_schema import OrderStatusEnum # For type hinting status


Expand Down
2 changes: 1 addition & 1 deletion src/backend/app/crud/order_item_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional, List, Dict, Any
from sqlalchemy import Connection, text, exc # Import exc for exception handling
from decimal import Decimal # For PriceAtPurchase and Subtotal
from loguru import logger # Assuming you are using loguru
from loguru import logger


class OrderItemCRUD:
Expand Down
6 changes: 1 addition & 5 deletions src/backend/app/crud/payment_transaction_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@
from decimal import Decimal # For TotalAmount
import datetime

from loguru import logger # Assuming loguru is used
from loguru import logger

from backend.app.schemas.order_schema import PaymentTransactionStatusEnum


# Assuming PaymentTransactionStatusEnum is defined in your order_schema.py or a common enums file
# from backend.app.schemas.order_schema import PaymentTransactionStatusEnum
# For this CRUD, we'll use string for status type hint if enum is not directly imported here.

class PaymentTransactionCRUD:
__instance: Optional["PaymentTransactionCRUD"] = None

Expand Down
5 changes: 0 additions & 5 deletions src/backend/app/crud/product_change_request_crud_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import datetime


# 假设这些枚举在您的 schema 文件中定义并可导入
from backend.app.schemas.product_change_request_schema_v2 import \
ProductChangeRequestTypeApiEnum as TypeEnum, ProductChangeRequestStatusApiEnum as StatusEnum
from backend.app.utils.json import DecimalEncoder
Expand Down Expand Up @@ -404,10 +403,6 @@ def cancel_request( # This method now means "cancel by merchant"
# LastUpdatedDate 会由数据库的 ON UPDATE CURRENT_TIMESTAMP 自动处理

try:
# Assuming ProductChangeRequestStatusApiEnum is available or using string literals
# from backend.app.schemas.product_change_request_schema_v2 import ProductChangeRequestStatusApiEnum
# cancelled_status = ProductChangeRequestStatusApiEnum.CANCELLED_BY_USER.value
# pending_status = ProductChangeRequestStatusApiEnum.PENDING_APPROVAL.value
cancelled_status = StatusEnum.CANCELLED_BY_USER
pending_status = StatusEnum.PENDING_APPROVAL

Expand Down
3 changes: 0 additions & 3 deletions src/backend/app/crud/product_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,9 +472,6 @@ def update_product_stock(
"stock_change": stock_change
}
)

# # 提交事务确保更新生效
# conn.commit()

# 获取更新后的商品信息
return self.get_product_by_id(conn, product_id=product_id, actor_id=actor_id)
Expand Down
18 changes: 9 additions & 9 deletions src/backend/app/crud/store_change_request_crud_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

# 导入枚举类并使用别名
from backend.app.schemas.store_change_request_schema_v2 import (
StoreChangeRequestTypeEnum as TypeEnum, # ProductChangeRequestTypeApiEnum
StoreChangeRequestStatusEnum as StatusEnum, # ProductChangeRequestStatusApiEnum
StoreChangeRequestTypeEnum as TypeEnum,
StoreChangeRequestStatusEnum as StatusEnum,
)

# StoreStatusEnum from store_schema might be needed if ProposedData_JSON for store creation includes it
Expand Down Expand Up @@ -249,7 +249,7 @@ def create_request_create_store(
conn=conn,
requesting_user_id=requesting_user_id,
store_id=None,
request_type=TypeEnum.STORE_CREATE.value, # ⭐ Use enum value
request_type=TypeEnum.STORE_CREATE.value,
proposed_data_json=proposed_data_json,
submitter_notes=submitter_notes,
actor_id=actor_id if actor_id is not None else requesting_user_id,
Expand All @@ -272,7 +272,7 @@ def create_request_update_store(
conn=conn,
requesting_user_id=requesting_user_id,
store_id=store_id,
request_type=TypeEnum.STORE_UPDATE.value, # ⭐ Use enum value
request_type=TypeEnum.STORE_UPDATE.value,
proposed_data_json=proposed_data_json,
submitter_notes=submitter_notes,
actor_id=actor_id if actor_id is not None else requesting_user_id,
Expand All @@ -294,7 +294,7 @@ def create_request_delete_store(
conn=conn,
requesting_user_id=requesting_user_id,
store_id=store_id,
request_type=TypeEnum.STORE_DELETE.value, # ⭐ Use enum value
request_type=TypeEnum.STORE_DELETE.value,
proposed_data_json=None,
submitter_notes=submitter_notes,
actor_id=actor_id if actor_id is not None else requesting_user_id,
Expand Down Expand Up @@ -323,9 +323,9 @@ def cancel_request_by_user(
result = conn.execute(
update_stmt,
{
"cancelled_status": StatusEnum.CANCELLED_BY_USER.value, # ⭐ Use enum value
"cancelled_status": StatusEnum.CANCELLED_BY_USER.value,
"ChangeRequestID": change_request_id,
"pending_status": StatusEnum.PENDING_APPROVAL.value, # ⭐ Use enum value
"pending_status": StatusEnum.PENDING_APPROVAL.value,
},
)
if result.rowcount > 0:
Expand Down Expand Up @@ -426,8 +426,8 @@ def update_request_store_id_and_status_applied(
set_clauses: List[str] = ["Status = :AppliedStatus"]
params: Dict[str, Any] = {
"ChangeRequestID_param": change_request_id,
"AppliedStatus": StatusEnum.APPLIED.value, # ⭐ Use enum value
"ApprovedStatus": StatusEnum.APPROVED.value, # ⭐ Use enum value
"AppliedStatus": StatusEnum.APPLIED.value,
"ApprovedStatus": StatusEnum.APPROVED.value,
}

# 只有在 applied_store_id 提供时才尝试更新 StoreID 列
Expand Down
5 changes: 2 additions & 3 deletions src/backend/app/crud/store_crud.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# src/backend/app/crud/store_crud.py
from typing import Optional, List, Dict, Any
from sqlalchemy import Connection, text, exc # Import exc for exception handling
# Decimal is not used in Store DDL, but datetime is for CreationDate
import datetime

from loguru import logger # Assuming loguru is used
from loguru import logger

# Import Pydantic schemas for type hinting where appropriate for Enums
from backend.app.schemas.store_schema import StoreStatusEnum
Expand Down Expand Up @@ -38,7 +37,7 @@ def _set_actor_session_variable(conn: Connection, actor_id: Optional[int]):
try:
if actor_id is not None:
conn.execute(
text("SET @actor_id = :actor_id"), # Assuming your trigger uses @actor_id
text("SET @actor_id = :actor_id"),
{"actor_id": actor_id}
)
else:
Expand Down
Loading