文档提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s

This commit is contained in:
2026-04-11 11:27:43 +08:00
parent e9ff14df0e
commit 8f66ca7bcb
15 changed files with 3775 additions and 3 deletions

View File

@@ -0,0 +1,349 @@
# 后台管理套餐相关接口代码结构探索
## 📋 概览
套餐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 和数据库中都有定义,用于按天计算套餐有效期。