Files
junhong_cmp_fiber/docs/package_structure_summary.md
huang 8f66ca7bcb
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s
文档提交
2026-04-11 11:27:43 +08:00

350 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后台管理套餐相关接口代码结构探索
## 📋 概览
套餐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 和数据库中都有定义,用于按天计算套餐有效期。