# 积分商城（Points Mall）接口需求说明

本文档用于对接 aura-web 的「积分商城」页面数据与兑换流程，供后端实现接口与数据表结构时参考。

## 页面结构对应的数据

页面 UI 由 4 个区域组成：

1. 头部商品分类导航栏（横向）
2. 公告栏
3. banner 区 + 促销活动入口（2 个入口卡片）
4. 商品内容区（商品列表 + 搜索/排序/分页）

## 统一约定

- BaseURL：`https://api.hoyidata.com/aura/v1`
- 鉴权：沿用当前登录态（Cookie / Session）或 `Authorization: Bearer <token>`（以现有登录实现为准），接口需校验登录态
- 图片字段：返回绝对 URL（推荐 CDN 域名），前端不拼接
- 时间字段：建议统一使用毫秒时间戳（number）
- 失败格式：沿用现有 aura 接口的错误格式（HTTP Status + JSON message/error）
- `GET /points-mall/overview` 已废弃（HTTP 410），前端需按数据维度分别调用 /me、/categories、/announcements、/banners、/promo-entries

## 接口列表（前端必需）

### 1) 我的积分

`GET /points-mall/me`

**Response**
```json
{
  "points": 1280,
  "level": "Lv.2",
  "totalSpentUSD": 0.174
}
```

### 2) 分类

`GET /points-mall/categories`

**Response**
```json
[
  { "id": "all", "name": "全部", "sort": 0 },
  { "id": "digital", "name": "虚拟权益", "sort": 1 }
]
```

### 3) 公告

`GET /points-mall/announcements`

**Response**
```json
[
  {
    "id": "a1",
    "title": "公告：积分规则升级中",
    "content": "本期暂不开放兑换，页面仅用于 UI 预览。",
    "linkUrl": "https://..."
  }
]
```

### 4) Banner

`GET /points-mall/banners`

**Response**
```json
[
  {
    "id": "b1",
    "title": "限时上新",
    "subtitle": "Up to 25% Off",
    "imageUrl": "https://cdn.../banner.png",
    "linkUrl": "https://..."
  }
]
```

### 5) 促销入口

`GET /points-mall/promo-entries`

**Response**
```json
[
  {
    "id": "p1",
    "title": "促销活动",
    "subtitle": "本周精选",
    "iconUrl": "https://cdn.../promo.png",
    "linkUrl": "https://..."
  },
  {
    "id": "p2",
    "title": "积分任务",
    "subtitle": "快速涨积分",
    "iconUrl": "https://cdn.../tasks.png",
    "linkUrl": "https://..."
  }
]
```

### 6) 商品列表（搜索/筛选/排序/分页）

`GET /points-mall/products`

**Query**
- `categoryId`：可选（分类 ID；不传代表全部）
- `q`：可选（搜索关键字；匹配 name/subtitle）
- `sort`：可选，默认 `popular`
  - `popular`：热度优先（建议按 sold 或近期兑换量）
  - `newest`：最新上架
  - `price_asc`：积分从低到高
  - `price_desc`：积分从高到低
- `page`：可选，默认 `1`
- `pageSize`：可选，默认 `24`

**Response**
```json
{
  "page": 1,
  "pageSize": 24,
  "total": 120,
  "items": [
    {
      "id": "prd_001",
      "categoryId": "digital",
      "name": "Claude Pro 月卡",
      "subtitle": "兑换后获得激活码",
      "coverUrl": "https://cdn.../cover.png",
      "pointsPrice": 1999,
      "stock": 99,
      "sold": 12,
      "tags": ["热卖", "限时"]
    }
  ]
}
```

### 7) 商品详情

`GET /points-mall/products/{productId}`

**Response（建议）**
```json
{
  "id": "prd_001",
  "categoryId": "digital",
  "name": "Claude Pro 月卡",
  "subtitle": "兑换后获得激活码",
  "coverUrl": "https://cdn.../cover.png",
  "imageUrls": ["https://cdn.../1.png", "https://cdn.../2.png"],
  "pointsPrice": 1999,
  "stock": 99,
  "sold": 12,
  "tags": ["热卖", "限时"],
  "description": "富文本/Markdown 均可，建议 Markdown",
  "type": "virtual",
  "delivery": {
    "mode": "code",
    "tips": "兑换成功后在【订单详情】查看兑换码"
  }
}
```

### 8) 创建兑换订单（扣积分）

`POST /points-mall/orders`

**Request**
```json
{
  "productId": "prd_001",
  "quantity": 1
}
```

**Response**
```json
{
  "orderId": "ord_001",
  "status": "paid",
  "pointsCost": 1999,
  "remainingPoints": 281
}
```

**关键规则**
- 需要幂等：建议支持 `Idempotency-Key` header，避免重复扣积分
- 校验库存与用户积分
- 虚拟商品/实物商品可共用该接口，但实物商品需要补充收货信息（可扩展字段）

### 9) 订单列表 / 订单详情

`GET /points-mall/orders`

**Query（建议）**
- `page` / `pageSize`
- `status`：可选

`GET /points-mall/orders/{orderId}`

**Response（建议）**
```json
{
  "id": "ord_001",
  "status": "paid",
  "createdAt": 1730000000000,
  "product": {
    "id": "prd_001",
    "name": "Claude Pro 月卡",
    "coverUrl": "https://cdn.../cover.png"
  },
  "quantity": 1,
  "pointsCost": 1999,
  "delivery": {
    "mode": "code",
    "code": "XXXX-YYYY-ZZZZ"
  }
}
```

### 10) 积分流水（可选，但强烈建议）

用于解释积分变动与对账。

`GET /points-mall/me/points-ledger`

**Query（建议）**
- `page` / `pageSize`

**Response（建议）**
```json
{
  "page": 1,
  "pageSize": 20,
  "total": 300,
  "items": [
    {
      "id": "pl_001",
      "createdAt": 1730000000000,
      "change": -1999,
      "balance": 281,
      "reason": "兑换 Claude Pro 月卡",
      "bizType": "points_mall_order",
      "bizId": "ord_001"
    }
  ]
}
```

## 数据表结构建议（后端实现参考）

说明：下述为建议表结构，字段类型可按现有数据库规范调整；建议均包含 `created_at / updated_at` 与必要索引。

### 1) `points_mall_categories`（分类）
- `id`（PK, string/uuid）
- `name`（varchar）
- `sort`（int）
- `enabled`（bool）

索引：
- `enabled, sort`

### 2) `points_mall_announcements`（公告）
- `id`（PK）
- `title`（varchar）
- `content`（text）
- `link_url`（varchar, nullable）
- `start_at`（bigint, nullable）
- `end_at`（bigint, nullable）
- `enabled`（bool）
- `sort`（int）

索引：
- `enabled, sort`
- `start_at, end_at`

### 3) `points_mall_banners`（banner）
- `id`（PK）
- `title`（varchar）
- `subtitle`（varchar, nullable）
- `image_url`（varchar）
- `link_url`（varchar, nullable）
- `start_at`（bigint, nullable）
- `end_at`（bigint, nullable）
- `enabled`（bool）
- `sort`（int）

索引：
- `enabled, sort`
- `start_at, end_at`

### 4) `points_mall_promo_entries`（促销入口）
- `id`（PK）
- `title`（varchar）
- `subtitle`（varchar, nullable）
- `icon_url`（varchar, nullable）
- `link_url`（varchar, nullable）
- `enabled`（bool）
- `sort`（int）

索引：
- `enabled, sort`

### 5) `points_mall_products`（商品）
- `id`（PK）
- `category_id`（FK -> categories.id）
- `name`（varchar）
- `subtitle`（varchar, nullable）
- `description`（text / markdown）
- `cover_url`（varchar）
- `image_urls`（json/text，数组）
- `type`（enum: `virtual` | `physical`）
- `points_price`（int）
- `stock`（int）
- `sold`（int，累计兑换量，或可由订单聚合）
- `tags`（json/text，数组）
- `enabled`（bool）
- `start_at`（bigint, nullable）
- `end_at`（bigint, nullable）
- `sort`（int）

索引：
- `enabled, start_at, end_at`
- `category_id, enabled, sort`
- 搜索：`name`/`subtitle` 建议全文索引或 LIKE（视数据库而定）

### 6) `points_mall_orders`（订单）
- `id`（PK）
- `user_id`（FK）
- `product_id`（FK）
- `quantity`（int）
- `points_cost`（int）
- `status`（enum: `created` | `paid` | `delivered` | `canceled` | `refunded`）
- `delivery_mode`（enum: `code` | `shipping`）
- `delivery_payload`（json，虚拟码/物流信息等）
- `idempotency_key`（varchar, nullable，建议唯一索引 + user_id 组合）

索引：
- `user_id, created_at desc`
- `status, created_at desc`
- `user_id, idempotency_key`（唯一）

### 7) `points_ledger`（积分流水）
- `id`（PK）
- `user_id`（FK）
- `change`（int，正负）
- `balance`（int）
- `reason`（varchar）
- `biz_type`（varchar）
- `biz_id`（varchar）

索引：
- `user_id, created_at desc`
- `biz_type, biz_id`

## 前端实现已对齐的字段

前端已按以下字段读取：
- 分类：`id/name/sort`
- 公告：`title/content/linkUrl`
- banner：`title/subtitle/imageUrl/linkUrl`
- 促销入口：`title/subtitle/iconUrl/linkUrl`
- 商品列表：`id/categoryId/name/subtitle/coverUrl/pointsPrice/stock/sold/tags`

如后端字段命名需要用 snake_case，可在接口层做映射，但建议直接使用上述 camelCase，减少前端 mapping。
