All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s
350 lines
12 KiB
Markdown
350 lines
12 KiB
Markdown
# 后台管理套餐相关接口代码结构探索
|
||
|
||
## 📋 概览
|
||
|
||
套餐(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 和数据库中都有定义,用于按天计算套餐有效期。
|