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