# SME-OmniStore 多租户 SaaS 电商中台

一套基于 **FastAPI + Nuxt 3** 的多租户 SaaS 电商系统，支持前台商城与后台管理。

---

## 📁 项目结构

```
sme-omnistore/
├── backend/                      # FastAPI 后端
│   ├── main.py                   # 应用入口 / 路由注册 / CORS 配置
│   ├── install.py                # 自动安装脚本（建库建表 + 种子数据）
│   ├── .env                      # 环境变量（勿提交）
│   ├── .installed                # 安装锁文件
│   └── app/
│       ├── config.py             # Settings 配置类（数据库 / Redis / JWT）
│       ├── api/
│       │   ├── deps.py           # get_db / get_current_user / get_admin_user
│       │   └── routers/         # API 路由
│       │       ├── auth.py       # 管理员登录 / 当前用户
│       │       ├── dashboard.py  # 数据看板（统计 / 营收趋势）
│       │       ├── products.py   # 后台商品 CRUD
│       │       ├── orders.py     # 后台订单管理
│       │       ├── customers.py  # 后台客户管理
│       │       └── store/       # 前台商城 API（公开）
│       │           ├── auth.py   # 顾客注册 / 登录 / 个人信息
│       │           ├── products.py  # 商品列表 / 详情 / 分类 / Banner
│       │           ├── orders.py # 创建订单 / 我的订单 / 订单详情
│       │           ├── coupons.py  # 优惠券验证
│       │           └── reviews.py  # 商品评价
│       ├── schemas/             # Pydantic 模型
│       │   ├── common.py        # PageResult 分页结构
│       │   ├── auth.py          # LoginRequest / TokenResponse / UserInfo
│       │   ├── product.py       # ProductCreate / ProductOut / ProductListOut
│       │   ├── customer.py      # CustomerOut
│       │   ├── order.py        # OrderOut / OrderDetailOut / OrderItemOut
│       │   └── dashboard.py    # DashboardStats / RevenueTrendItem
│       └── core/models/        # SQLAlchemy 模型（ORM）
│           ├── product.py       # Product
│           ├── category.py     # Category
│           ├── order.py        # Order / OrderItem
│           ├── customer.py     # Customer
│           ├── discount.py     # Discount（优惠券）
│           ├── review.py       # ProductReview
│           └── user.py         # User（管理员）
│
└── frontend/                     # Nuxt 3 前台（TSUNIVIEW）
    └── store/
        ├── nuxt.config.ts       # Nuxt 配置（代理 / 模块）
        ├── composables/
        │   └── useApi.ts       # 统一 API 请求 composable
        ├── stores/
        │   ├── auth.ts         # 顾客认证状态（localStorage）
        │   └── cart.ts         # 购物车状态
        └── pages/
            ├── index.vue               # 首页（Banner / 推荐商品 / 分类）
            ├── auth/
            │   ├── login.vue           # 顾客登录
            │   └── register.vue        # 顾客注册
            ├── products/
            │   ├── index.vue          # 商品列表（筛选 / 排序 / 分页）
            │   └── [slug].vue         # 商品详情
            ├── cart.vue               # 购物车
            ├── checkout/
            │   └── index.vue         # 确认订单 / 提交
            ├── account/
            │   └── index.vue         # 我的订单 / 地址管理
            └── wishlist.vue           # 收藏夹
```

---

## ⚙️ 环境要求 & 启动

### 后端

```bash
cd backend
pip install -r requirements.txt          # 安装依赖
python install.py                        # 自动建库建表 + 种子数据
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```

> 首次运行 `install.py` 会创建 MySQL 数据库和默认租户数据。
> 再次运行检测到 `.installed` 锁文件会跳过初始化。

**初始管理员账号：**
- 邮箱：`admin@example.com`
- 密码：`Admin@2026!`

**初始租户：**
- 域名：`localhost`
- 租户 ID：`1`

### 前台

```bash
cd frontend/store
npm install
npm run dev                              # 开发模式 → http://localhost:5173
npm run build && npm start               # 生产模式
```

---

## 🔧 环境变量（backend/.env）

| 变量 | 说明 | 示例 |
|------|------|------|
| `DATABASE_URL` | MySQL 连接串 | `mysql+aiomysql://sme_user:sme_pass123@localhost:3306/sme_omnistore` |
| `REDIS_URL` | Redis 连接串 | `redis://localhost:6379/0` |
| `SECRET_KEY` | JWT 签名密钥 | `change-me`（生产必须更换） |
| `DEBUG` | 调试模式，开启则 CORS 全放行 | `true` |
| `ALLOWED_ORIGINS` | 允许的 Origins（DEBUG=false时生效） | `http://localhost:5173` |

---

## 🌐 API 文档

启动后访问：**http://localhost:8000/docs**（Swagger UI）
或 **http://localhost:8000/redoc**（ReDoc）

---

## 认证方式

### 管理员认证（后台 API）

请求头：`Authorization: Bearer <token>`

token 通过 `POST /api/auth/login` 获取，有效期 7 天。

### 顾客认证（前台 API）

请求头：`Authorization: Bearer <token>`

token 通过 `POST /api/store/auth/login`（顾客登录）或 `POST /api/store/auth/register`（注册）获取，有效期 30 天。

---

## 📦 后台管理 API（需管理员登录）

### 认证

#### `POST /api/auth/login`
管理员登录。

**Request Body：**
```json
{
  "email": "admin@example.com",
  "password": "Admin@2026!"
}
```

**Response：**
```json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 1,
    "email": "admin@example.com",
    "name": "admin",
    "role": "admin"
  }
}
```

---

#### `GET /api/auth/me`
获取当前登录管理员信息。

**Response：**
```json
{
  "id": 1,
  "email": "admin@example.com",
  "name": "admin",
  "role": "admin"
}
```

---

### 数据看板

#### `GET /api/dashboard/stats`
核心统计指标（需登录）。

**Response：**
```json
{
  "revenue_today": "1200.00",
  "revenue_month": "35600.00",
  "orders_today": 8,
  "orders_pending": 3,
  "customers_total": 156,
  "products_active": 42
}
```

---

#### `GET /api/dashboard/trend`
近 7 日营收趋势（需登录）。

**Response：**
```json
[
  { "date": "04/23", "revenue": "3200.00" },
  { "date": "04/24", "revenue": "4100.00" },
  { "date": "04/25", "revenue": "2800.00" },
  { "date": "04/26", "revenue": "5100.00" },
  { "date": "04/27", "revenue": "3900.00" },
  { "date": "04/28", "revenue": "4300.00" },
  { "date": "04/29", "revenue": "2850.00" }
]
```

---

### 商品管理

#### `GET /api/products`
商品列表（需登录）。

**Query 参数：**
| 参数 | 类型 | 说明 |
|------|------|------|
| `page` | int | 页码，默认 1 |
| `page_size` | int | 每页数量，默认 10，最大 100 |
| `keyword` | string | 搜索商品名称 / SKU |
| `category` | string | 分类名称 |
| `status` | string | active / draft / archived |

**Response：**
```json
{
  "items": [
    {
      "id": 1, "name": "简约皮质夹克", "sku": "SKU-0001",
      "category": "服装", "base_price": "499.00",
      "stock_qty": 50, "status": "active",
      "cover_url": "https://...",
      "slug": "product-1", "sales_count": 128,
      "created_at": "2025-04-01T00:00:00"
    }
  ],
  "total": 42,
  "page": 1,
  "page_size": 10
}
```

---

#### `POST /api/products`
创建商品（需登录）。

**Request Body：**
```json
{
  "name": "经典运动鞋",
  "sku": "SKU-A001",
  "category": "鞋靴",
  "base_price": 299.00,
  "cost_price": 120.00,
  "stock_qty": 100,
  "status": "active",
  "description": "轻便舒适，适合日常穿着"
}
```

---

#### `GET /api/products/{product_id}`
商品详情（需登录）。

---

#### `PUT /api/products/{product_id}`
更新商品（需登录）。支持部分更新（PATCH 语义）。

---

#### `DELETE /api/products/{product_id}`
删除商品（需登录）。

---

### 订单管理

#### `GET /api/orders`
订单列表（需登录）。

**Query 参数：**
| 参数 | 类型 | 说明 |
|------|------|------|
| `page` | int | 页码 |
| `page_size` | int | 每页数量 |
| `keyword` | string | 搜索订单号 |
| `status` | string | pending / paid / shipped / completed / cancelled |

**Response：**
```json
{
  "items": [
    {
      "id": 1,
      "order_no": "ORD-20250429-A1B2C3",
      "status": "pending",
      "grand_total": "358.00",
      "items_count": 2,
      "customer_name": "张三",
      "created_at": "2025-04-29T10:30:00"
    }
  ],
  "total": 15,
  "page": 1,
  "page_size": 10
}
```

---

#### `GET /api/orders/{order_id}`
订单详情（需登录）。

**Response：**
```json
{
  "id": 1,
  "order_no": "ORD-20250429-A1B2C3",
  "status": "pending",
  "grand_total": "358.00",
  "subtotal": "338.00",
  "shipping_fee": "20.00",
  "discount_amount": "0.00",
  "items_count": 2,
  "customer_name": "张三",
  "shipping_address": {
    "name": "张三", "phone": "13800138000",
    "province": "广东省", "city": "深圳市",
    "district": "南山区", "address": "科技路1号"
  },
  "remark": "请尽快发货",
  "created_at": "2025-04-29T10:30:00",
  "items": [
    {
      "id": 1, "product_id": 5,
      "product_name": "经典运动鞋",
      "sku": "SKU-A001",
      "qty": 1,
      "unit_price": "299.00",
      "subtotal": "299.00"
    }
  ]
}
```

---

#### `PUT /api/orders/{order_id}/status`
更新订单状态（需登录）。

**Request Body：** `{ "status": "shipped" }`

**可选状态：** `pending` / `paid` / `shipped` / `completed` / `cancelled`

---

### 客户管理

#### `GET /api/customers`
客户列表（需登录）。

**Query 参数：** `page`, `page_size`, `keyword`（支持姓名 / 邮箱 / 手机号搜索）

**Response：**
```json
{
  "items": [
    {
      "id": 1, "name": "张三",
      "email": "zhangsan@example.com",
      "phone": "13800138000",
      "is_vip": false,
      "created_at": "2025-04-20T08:00:00"
    }
  ],
  "total": 156,
  "page": 1,
  "page_size": 10
}
```

---

#### `GET /api/customers/{customer_id}`
客户详情（需登录）。

---

## 🏪 前台商城 API（公开，无需登录）

> 前台 API 默认使用 `tenant_id=2`。部分端点接受查询参数覆盖。

### 商品

#### `GET /api/store/products`
商品列表（公开）。

**Query 参数：**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `page` | int | 1 | 页码 |
| `page_size` | int | 12 | 每页数量，最大 100 |
| `keyword` | string | - | 搜索名称 / SKU |
| `category` | string | - | 分类名称 |
| `sort` | string | popular | 排序：popular / newest / price_asc / price_desc |

**Response：**
```json
{
  "items": [
    {
      "id": 1, "name": "简约皮质夹克",
      "sku": "SKU-0001", "category": "服装",
      "base_price": "499.00", "stock_qty": 50,
      "status": "active", "cover_url": "https://...",
      "slug": "product-1", "sales_count": 128,
      "created_at": "2025-04-01T00:00:00"
    }
  ],
  "total": 60,
  "page": 1,
  "page_size": 12
}
```

---

#### `GET /api/store/products/featured`
推荐商品列表（公开）。

**Query 参数：** `limit`（默认 8，最大 50）

**Response：** `ProductListOut[]`

---

#### `GET /api/store/products/{slug}`
商品详情（公开）。支持 slug 或数字 ID 查询。

**Response：**
```json
{
  "id": 1, "name": "简约皮质夹克",
  "sku": "SKU-0001", "category": "服装",
  "description": "这是一款精心设计的高品质商品...",
  "base_price": "499.00", "cost_price": "200.00",
  "stock_qty": 50, "low_stock_threshold": 5,
  "status": "active", "weight_grams": 0,
  "slug": "product-1",
  "meta_title": "", "meta_description": "",
  "cover_url": "https://...",
  "sales_count": 128,
  "created_at": "2025-04-01T00:00:00",
  "tenant_id": 2
}
```

---

#### `GET /api/store/categories`
分类列表（公开）。

**Response：** `["服装", "鞋靴", "配饰", "包袋", "运动户外"]`

---

#### `GET /api/store/banners`
首页 Banner（公开）。

**Response：**
```json
[
  {
    "id": 1,
    "title": "夏季新品上市",
    "subtitle": "探索最新时尚单品",
    "url": "/products?category=新品上市",
    "cta": "立即选购",
    "image": "https://picsum.photos/seed/banner1/1200/400"
  }
]
```

---

### 顾客认证

#### `POST /api/store/auth/register`
顾客注册。

**Request Body：**
```json
{
  "name": "李四",
  "email": "lisi@example.com",
  "password": "password123",
  "phone": "13900139000",
  "tenant_id": 2
}
```

**Response：**
```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "user": {
    "id": 2, "name": "李四",
    "email": "lisi@example.com",
    "phone": "13900139000", "is_vip": false
  }
}
```

---

#### `POST /api/store/auth/login`
顾客登录。

**Request Body：**
```json
{
  "account": "lisi@example.com",
  "password": "password123",
  "tenant_id": 2
}
```

**Response：** 同注册接口。account 支持邮箱或手机号。

---

#### `GET /api/store/auth/me`
获取当前顾客信息（需顾客登录）。

**Response：**
```json
{
  "id": 2, "name": "李四",
  "email": "lisi@example.com",
  "phone": "13900139000", "is_vip": false
}
```

---

#### `PATCH /api/store/auth/me`
更新个人信息（需顾客登录）。

**Request Body：**
```json
{ "name": "李四（新）", "phone": "13900139001" }
```

---

### 订单

#### `POST /api/store/orders`
创建订单（需顾客登录）。

**Request Body：**
```json
{
  "items": [
    { "product_id": 1, "qty": 2, "variant_id": null, "variant": null }
  ],
  "recv_name": "李四",
  "recv_phone": "13900139000",
  "recv_province": "广东省",
  "recv_city": "深圳市",
  "recv_district": "南山区",
  "recv_addr": "科技路1号",
  "pay_method": "alipay",
  "coupon_discount": 0.0,
  "coupon_code": null,
  "remark": "请尽快发货"
}
```

**Response：**
```json
{
  "id": 1,
  "order_no": "ORD-20250429-A1B2C3",
  "status": "pending",
  "total": 358.0,
  "created_at": "2025-04-29T10:30:00"
}
```

**运费规则：** 订单金额 ≥ ¥99 免运费，否则 ¥10

---

#### `GET /api/store/orders`
我的订单列表（需顾客登录）。

**Query 参数：** `page`（默认1）, `page_size`（默认10）, `status`（订单状态）

**Response：**
```json
{
  "items": [
    {
      "id": 1,
      "order_no": "ORD-20250429-A1B2C3",
      "status": "pending",
      "total": 358.0,
      "created_at": "2025-04-29T10:30:00"
    }
  ]
}
```

---

#### `GET /api/store/orders/{order_no}`
订单详情（需顾客登录，只能查看自己的订单）。

---

### 优惠券

#### `POST /api/store/coupons/validate`
验证优惠券（公开）。

**Request Body：**
```json
{
  "code": "SUMMER20",
  "order_amount": 299.00,
  "tenant_id": 2
}
```

**Response：**
```json
{
  "id": 1,
  "code": "SUMMER20",
  "name": "满减券（SUMMER20）",
  "type": "fixed",
  "value": 20.0,
  "min_amount": 100.0,
  "discount": 20.0
}
```

**优惠计算：**
- `fixed`：减免固定金额（不超过订单金额）
- `percentage`：按订单金额的百分比减免

**错误响应（400/404）：**
- 优惠券无效/不存在 → `{"detail": "优惠券无效或不存在"}`
- 尚未生效 / 已过期 / 已领完 → `{"detail": "该优惠券已过期"}`
- 订单金额未达门槛 → `{"detail": "该券需满 ¥100 方可使用，当前 ¥80.00"}`

---

### 商品评价

#### `GET /api/store/reviews/product/{product_id}`
商品评价列表（公开）。

**Query 参数：** `page`（默认1）, `page_size`（默认10）, `rating`（筛选星级 1-5）

**Response：**
```json
{
  "total": 42,
  "avg_rating": 4.6,
  "distribution": { "5": 28, "4": 10, "3": 3, "2": 1, "1": 0 },
  "items": [
    {
      "id": 1, "rating": 5,
      "content": "质量非常好，穿上去很舒服，尺码标准...",
      "images": [], "variant": null,
      "customer_name": "张三",
      "created_at": "2025-04-25T14:30:00",
      "reply": "感谢您的支持！",
      "helpful_count": 12,
      "verified": true
    }
  ]
}
```

---

#### `POST /api/store/reviews`
提交商品评价（需顾客登录，每人每商品限一次）。

**Request Body：**
```json
{
  "product_id": 1,
  "rating": 5,
  "content": "质量非常好，穿上去很舒服，尺码标准，非常满意！",
  "images": ["https://example.com/review1.jpg"],
  "variant": null
}
```

**Response：** `{ "message": "评价已提交，审核通过后将公开显示" }`

> 评价提交后状态为 `pending`，需后台审核后才会出现在列表中。

---

#### `POST /api/store/reviews/{review_id}/helpful`
标记评价有用（公开，无需登录，每评价可多次调用累加）。

**Response：** `{ "helpful_count": 13 }`

---

## 🛠️ 前台页面说明

| 页面 | 路由 | 说明 |
|------|------|------|
| 首页 | `/` | Banner 轮播 / 推荐商品 / 分类导航 |
| 商品列表 | `/products` | 筛选（分类/价格区间/排序）/ 分页 |
| 商品详情 | `/products/{slug}` | 商品信息 / 评价星级 / 收藏 |
| 购物车 | `/cart` | 购物车管理（客户端 localStorage 持久化） |
| 确认订单 | `/checkout` | 地址填写 / 优惠券验证 / 提交订单 |
| 顾客登录 | `/auth/login` | 邮箱或手机号 + 密码 |
| 顾客注册 | `/auth/register` | 姓名 / 邮箱 / 密码 |
| 我的订单 | `/account` | 订单列表 / 收货地址管理 |
| 收藏夹 | `/wishlist` | 收藏商品（客户端 localStorage 持久化） |

---

## 🔐 前端 API 层（useApi.ts）

`frontend/store/composables/useApi.ts` 是统一 API 请求入口：

```typescript
const base = "http://192.168.50.139:8000/api"  // 浏览器 CSR 直连后端

export function useApi() {
  return {
    getProducts(params),           // GET /store/products
    getFeaturedProducts(limit),    // GET /store/products/featured
    getProduct(slug),              // GET /store/products/{slug}
    getBanners(),                  // GET /store/banners
    getCategories(),               // GET /store/categories
  }
}
```

> 当前 `useMock` 强制为 `false`，始终直连真实后端，不走 mock 数据。

---

## 🗄️ 数据库模型

### Product（商品）
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 主键 |
| `tenant_id` | int | 租户 ID |
| `name` | str(120) | 商品名称 |
| `slug` | str | URL slug |
| `sku` | str(64) | SKU 编码 |
| `category` | str(64) | 分类名称 |
| `base_price` | Decimal | 售价 |
| `cost_price` | Decimal | 成本价 |
| `stock_qty` | int | 库存数量 |
| `status` | str | active / draft / archived |
| `cover_url` | str | 封面图 URL |
| `sales_count` | int | 销量 |

### Order（订单）
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 主键 |
| `tenant_id` | int | 租户 ID |
| `customer_id` | int | 顾客 ID |
| `order_no` | str | 订单号（ORD-YYYYMMDD-XXXXXX） |
| `status` | str | pending / paid / shipped / completed / cancelled |
| `grand_total` | Decimal | 订单总金额 |
| `shipping_address` | JSON | 收货地址 |
| `remark` | str | 备注 |

### Customer（顾客）
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 主键 |
| `tenant_id` | int | 租户 ID |
| `name` | str | 姓名 |
| `email` | str | 邮箱（唯一） |
| `phone` | str | 手机号 |
| `tags` | JSON | 标签（如 `{vip: true}`） |

### Discount（优惠券）
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | str | 优惠码 |
| `type` | str | fixed / percentage / buy_x_get_y |
| `value` | Decimal | 优惠值 |
| `min_order_amount` | Decimal | 最低消费金额 |
| `usage_limit` | int | 总发放数量 |
| `used_count` | int | 已使用数量 |
| `start_at` / `end_at` | datetime | 有效期 |

### ProductReview（商品评价）
| 字段 | 类型 | 说明 |
|------|------|------|
| `product_id` | int | 商品 ID |
| `customer_id` | int | 顾客 ID |
| `rating` | int | 星级 1-5 |
| `content` | str | 评价内容 |
| `status` | str | pending / approved / rejected |
| `helpful_count` | int | 有用标记数 |

---

## 🐛 已知问题 & 待办

### P0 — 网络层（阻塞）
- **浏览器无法访问 `http://192.168.50.139:8000`**
- 服务器 `curl http://127.0.0.1:8000` 正常，外部浏览器访问时 TCP 连接被拒绝
- **修复：** 在服务器执行 `sudo ufw disable` 或 `sudo ufw allow 8000/tcp`

### P1 — checkout/index.vue 缺少 Authorization header
- `POST /store/orders` 未带 `Authorization: Bearer <token>`
- 后端 `create_order` 需要 `get_current_customer`（顾客登录）
- **修复：** 加入 `headers: { Authorization: \`Bearer ${token}\` }`

### P1 — account/index.vue 使用 mock 数据
- 订单和地址列表来自 Pinia Store，刷新后页面空白
- **修复：** 在 `setup()` 中调用 `GET /store/orders` 填充真实数据

### P2 — products/index.vue 绕过 useApi()
- 第 207 行直接用 `$fetch('/store/products', { base: '...' })`，没有走 useApi()
- **修复：** 改用 `useApi().getProducts()` 或补全完整参数

### P2 — nuxt.config.ts SSR 代理缺失
- 商品详情页 `isr: 60`（SSR）时，`/api/store/...` 在服务端发到 Nuxt Server（port 3000），无代理会 404
- **修复：** 在 `nitro.routeRules` 中配置 SSR 路由代理

---

## 📝 更新日志

### 2026-04
- 实现完整的前台商城 API（商品 / 分类 / Banner / 评价）
- 实现顾客认证流程（注册 / 登录 / JWT 30天）
- 实现订单创建与查询（支持购物车 + 收货地址）
- 实现优惠券验证 API
- 前后端代码同步至 workspace
