# 后台管理套餐相关接口代码结构探索

## 📋 概览

套餐（Package/PackageSeries）相关功能采用标准的分层架构：
```
Handler → Service → Store → Model
```

---

## 1️⃣ 核心文件位置

### Model 层（数据模型）
| 文件 | 路径 | 说明 |
|------|------|------|
| **Package** | `internal/model/package.go:28-56` | 套餐主模型，包含 `duration_days` 字段 |
| **PackageSeries** | `internal/model/package.go:10-26` | 套餐系列模型 |
| **PackageUsage** | `internal/model/package.go:58-90` | 套餐使用情况模型 |
| **PackageUsageDailyRecord** | `internal/model/package.go:92-107` | 套餐流量日记录模型 |

### DTO 层（数据传输对象）
| 文件 | 路径 | 说明 |
|------|------|------|
| **PackageResponse** | `internal/model/dto/package_dto.go:71-99` | 套餐响应结构，包含 `duration_days` 字段 |
| **CreatePackageRequest** | `internal/model/dto/package_dto.go:3-19` | 创建套餐请求 |
| **UpdatePackageRequest** | `internal/model/dto/package_dto.go:21-36` | 更新套餐请求 |
| **PackageListRequest** | `internal/model/dto/package_dto.go:38-47` | 套餐列表请求 |
| **PackagePageResult** | `internal/model/dto/package_dto.go:125-131` | 套餐分页结果 |

### Handler 层（HTTP 处理）
| 文件 | 路径 | 说明 |
|------|------|------|
| **PackageHandler** | `internal/handler/admin/package.go` | 套餐管理处理器 |
| 方法 | 行号 | 功能 |
| `List()` | 22-34 | 获取套餐列表 |
| `Create()` | 36-48 | 创建套餐 |
| `Get()` | 50-62 | 获取套餐详情 |
| `Update()` | 64-81 | 更新套餐 |
| `Delete()` | 83-94 | 删除套餐 |
| `UpdateStatus()` | 96-112 | 更新套餐状态 |
| `UpdateShelfStatus()` | 114-130 | 更新上架状态 |
| `UpdateRetailPrice()` | 132-148 | 更新零售价 |

### Service 层（业务逻辑）
| 文件 | 路径 | 说明 |
|------|------|------|
| **Service** | `internal/service/package/service.go:19-38` | 套餐服务主类 |
| 依赖 | 说明 | |
| `packageStore` | 套餐数据访问 | |
| `packageSeriesStore` | 套餐系列数据访问 | |
| `packageAllocationStore` | 套餐分配数据访问 | |
| `shopSeriesAllocationStore` | 店铺系列分配数据访问 | |

**关键方法**（`service.go` 第 40-80 行）：
- `Create()` - 创建套餐，包含虚流量和周期类型校验
- `List()` - 列表查询，支持代理用户过滤
- `Get()` - 获取详情
- `Update()` - 更新套餐
- `Delete()` - 删除套餐
- `UpdateStatus()` - 更新状态
- `UpdateShelfStatus()` - 更新上架状态
- `UpdateRetailPrice()` - 更新零售价

### Store 层（数据访问）
| 文件 | 路径 | 说明 |
|------|------|------|
| **PackageStore** | `internal/store/postgres/package_store.go:15-21` | 套餐数据访问 |
| 方法 | 行号 | 功能 |
| `Create()` | 23-25 | 创建套餐 |
| `GetByID()` | 27-33 | 按 ID 查询 |
| `GetByCode()` | 35-41 | 按编码查询 |
| `Update()` | 43-45 | 更新套餐 |
| `Delete()` | 47-49 | 删除套餐 |
| `List()` | 51-100+ | 列表查询（支持代理用户过滤） |

**其他相关 Store**：
- `PackageSeriesStore` - `internal/store/postgres/package_series_store.go`
- `PackageUsageStore` - `internal/store/postgres/package_usage_store.go`
- `PackageUsageDailyRecordStore` - `internal/store/postgres/package_usage_daily_record_store.go`
- `ShopPackageAllocationStore` - `internal/store/postgres/shop_package_allocation_store.go`

---

## 2️⃣ API 路由注册

### 主路由注册入口
**文件**：`internal/routes/admin.go:71-78`

```go
if handlers.PackageSeries != nil {
    registerPackageSeriesRoutes(authGroup, handlers.PackageSeries, doc, basePath)
}
if handlers.Package != nil {
    registerPackageRoutes(authGroup, handlers.Package, doc, basePath)
}
if handlers.PackageUsage != nil {
    registerPackageUsageRoutes(authGroup, handlers.PackageUsage, doc, basePath)
}
```

### 套餐路由详情
**文件**：`internal/routes/package.go:11-78`

| 方法 | 路径 | 处理器 | 说明 |
|------|------|--------|------|
| GET | `/packages` | `List()` | 套餐列表 |
| POST | `/packages` | `Create()` | 创建套餐 |
| GET | `/packages/:id` | `Get()` | 获取详情 |
| PUT | `/packages/:id` | `Update()` | 更新套餐 |
| DELETE | `/packages/:id` | `Delete()` | 删除套餐 |
| PATCH | `/packages/:id/status` | `UpdateStatus()` | 更新状态 |
| PATCH | `/packages/:id/shelf` | `UpdateShelfStatus()` | 更新上架状态 |
| PATCH | `/packages/:id/retail-price` | `UpdateRetailPrice()` | 更新零售价 |

### 套餐系列路由
**文件**：`internal/routes/package_series.go:11-62`

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/package-series` | 列表 |
| POST | `/package-series` | 创建 |
| GET | `/package-series/:id` | 详情 |
| PUT | `/package-series/:id` | 更新 |
| DELETE | `/package-series/:id` | 删除 |
| PATCH | `/package-series/:id/status` | 更新状态 |

### 套餐使用记录路由
**文件**：`internal/routes/package_usage.go:12-23`

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/package-usage/:id/daily-records` | 获取流量详单 |

---

## 3️⃣ 关键字段详解

### Package 模型中的 duration_days 字段
**位置**：`internal/model/package.go:46`

```go
DurationDays int `gorm:"column:duration_days;type:int;comment:套餐天数(calendar_type=by_day时必填)" json:"duration_days"`
```

**说明**：
- 当 `calendar_type = "by_day"` 时必填
- 当 `calendar_type = "natural_month"` 时不需要
- 用于按天计算套餐有效期

### PackageResponse DTO 中的 duration_days
**位置**：`internal/model/dto/package_dto.go:96`

```go
DurationDays *int `json:"duration_days,omitempty" description:"套餐天数(calendar_type=by_day时有值)"`
```

**说明**：
- 可选字段（指针类型）
- 仅当 `calendar_type = "by_day"` 时返回值

### 创建请求中的 duration_days
**位置**：`internal/model/dto/package_dto.go:16`

```go
DurationDays *int `json:"duration_days" validate:"omitempty,min=1,max=3650" minimum:"1" maximum:"3650" description:"套餐天数(calendar_type=by_day时必填)"`
```

**验证规则**：
- 可选字段
- 当提供时，必须在 1-3650 之间
- 当 `calendar_type = "by_day"` 时必须提供

---

## 4️⃣ 业务逻辑关键点

### 套餐周期类型校验（Service 层）
**位置**：`internal/service/package/service.go:65-80`

```go
// 校验套餐周期类型和时长配置
calendarType := constants.PackageCalendarTypeByDay // 默认按天
if req.CalendarType != nil {
    calendarType = *req.CalendarType
}
if calendarType == constants.PackageCalendarTypeNaturalMonth {
    // 自然月套餐：必须提供 duration_months
    if req.DurationMonths <= 0 {
        return nil, errors.New(errors.CodeInvalidParam, "自然月套餐必须提供有效的duration_months")
    }
} else if calendarType == constants.PackageCalendarTypeByDay {
    // 按天套餐：必须提供 duration_days
    if req.DurationDays == nil || *req.DurationDays <= 0 {
        return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days")
    }
}
```

### 虚流量配置校验
**位置**：`internal/service/package/service.go:51-63`

```go
if req.EnableVirtualData {
    if req.VirtualDataMB == nil || *req.VirtualDataMB <= 0 {
        return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时，虚流量额度必须大于0")
    }
    realDataMB := int64(0)
    if req.RealDataMB != nil {
        realDataMB = *req.RealDataMB
    }
    if *req.VirtualDataMB > realDataMB {
        return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度")
    }
}
```

### 代理用户套餐过滤
**位置**：`internal/store/postgres/package_store.go:57-66`

```go
// 代理用户额外过滤：只能看到已分配（allocation.status=启用）的套餐
userType := middleware.GetUserTypeFromContext(ctx)
shopID := middleware.GetShopIDFromContext(ctx)
isAgent := userType == constants.UserTypeAgent && shopID > 0
if isAgent {
    query = query.Joins("INNER JOIN tb_shop_package_allocation ON tb_shop_package_allocation.package_id = tb_package.id AND tb_shop_package_allocation.deleted_at IS NULL").
        Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?",
            shopID, constants.StatusEnabled)
}
```

---

## 5️⃣ 相关常量

**位置**：`pkg/constants/` 目录

常用常量：
- `PackageCalendarTypeNaturalMonth` - 自然月
- `PackageCalendarTypeByDay` - 按天
- `UserTypeAgent` - 代理用户类型
- `StatusEnabled` - 启用状态

---

## 6️⃣ 数据库表结构

### tb_package（套餐表）
| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| package_code | varchar(100) | 套餐编码（唯一） |
| package_name | varchar(255) | 套餐名称 |
| series_id | bigint | 套餐系列 ID |
| package_type | varchar(50) | 套餐类型（formal/addon） |
| duration_months | int | 套餐时长（月数） |
| real_data_mb | bigint | 真流量额度（MB） |
| virtual_data_mb | bigint | 虚流量额度（MB） |
| enable_virtual_data | boolean | 是否启用虚流量 |
| **duration_days** | **int** | **套餐天数（按天时必填）** |
| calendar_type | varchar(20) | 套餐周期类型（natural_month/by_day） |
| data_reset_cycle | varchar(20) | 流量重置周期（daily/monthly/yearly/none） |
| expiry_base | varchar(30) | 到期时间基准（from_activation/from_purchase） |
| cost_price | bigint | 成本价（分） |
| suggested_retail_price | bigint | 建议售价（分） |
| shelf_status | int | 上架状态（1-上架/2-下架） |
| status | int | 状态（1-启用/2-禁用） |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
| deleted_at | timestamp | 删除时间 |

### tb_package_series（套餐系列表）
| 字段 | 类型 | 说明 |
|------|------|------|
| id | bigint | 主键 |
| series_code | varchar(100) | 系列编码（唯一） |
| series_name | varchar(255) | 系列名称 |
| description | text | 描述 |
| enable_one_time_commission | boolean | 是否启用一次性佣金 |
| one_time_commission_config | jsonb | 一次性佣金规则配置 |
| status | int | 状态（1-启用/2-禁用） |

---

## 7️⃣ 完整请求/响应示例

### 创建套餐（按天）
**请求**：
```json
POST /api/admin/packages
{
  "package_code": "PKG_DAY_30",
  "package_name": "30天套餐",
  "package_type": "formal",
  "duration_months": 1,
  "real_data_mb": 10240,
  "enable_virtual_data": false,
  "cost_price": 9900,
  "suggested_retail_price": 19900,
  "calendar_type": "by_day",
  "duration_days": 30,
  "data_reset_cycle": "monthly",
  "expiry_base": "from_activation"
}
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "package_code": "PKG_DAY_30",
    "package_name": "30天套餐",
    "package_type": "formal",
    "duration_months": 1,
    "real_data_mb": 10240,
    "enable_virtual_data": false,
    "cost_price": 9900,
    "suggested_retail_price": 19900,
    "calendar_type": "by_day",
    "duration_days": 30,
    "data_reset_cycle": "monthly",
    "expiry_base": "from_activation",
    "status": 1,
    "shelf_status": 2,
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z"
  },
  "msg": "success"
}
```

---

## 📌 总结

| 层级 | 关键文件 | 核心类/函数 |
|------|---------|-----------|
| **Model** | `internal/model/package.go` | `Package`、`PackageSeries`、`PackageUsage` |
| **DTO** | `internal/model/dto/package_dto.go` | `PackageResponse`、`CreatePackageRequest` |
| **Handler** | `internal/handler/admin/package.go` | `PackageHandler` |
| **Service** | `internal/service/package/service.go` | `Service` |
| **Store** | `internal/store/postgres/package_store.go` | `PackageStore` |
| **Routes** | `internal/routes/package.go` | `registerPackageRoutes()` |
| **Route Registry** | `internal/routes/admin.go:71-78` | 路由注册入口 |

**关键字段**：`duration_days` 在 Model、DTO 和数据库中都有定义，用于按天计算套餐有效期。
