# 套餐接口架构图 ## 1. 整体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ HTTP 请求 │ │ POST /api/admin/packages │ └────────────────────────────┬────────────────────────────────────┘ │ ┌────────▼────────┐ │ 认证中间件 │ │ (AdminAuth) │ └────────┬────────┘ │ ┌────────────────────▼────────────────────┐ │ 路由注册 (routes/admin.go) │ │ registerPackageRoutes() │ │ ↓ │ │ internal/routes/package.go │ └────────────────────┬────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ PackageHandler │ │ (internal/handler/admin/package.go) │ │ │ │ - List() │ │ - Create() │ │ - Get() │ │ - Update() │ │ - Delete() │ │ - UpdateStatus() │ │ - UpdateShelfStatus() │ │ - UpdateRetailPrice() │ └────────────────────┬────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ PackageService │ │ (internal/service/package/service.go) │ │ │ │ 业务逻辑: │ │ - 虚流量校验 │ │ - 周期类型校验 │ │ - 代理权限检查 │ │ - DTO 转换 │ └────────────────────┬────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ PackageStore │ │ (internal/store/postgres/...) │ │ │ │ - PackageStore │ │ - PackageSeriesStore │ │ - ShopPackageAllocationStore │ │ - ShopSeriesAllocationStore │ └────────────────────┬────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ PostgreSQL 数据库 │ │ │ │ - tb_package │ │ - tb_package_series │ │ - tb_shop_package_allocation │ │ - tb_shop_series_allocation │ └─────────────────────────────────────────┘ ``` --- ## 2. 分层详细结构 ### Handler 层 ``` PackageHandler ├── List(c *fiber.Ctx) │ └── 解析查询参数 → 调用 Service.List() → 返回分页结果 ├── Create(c *fiber.Ctx) │ └── 解析请求体 → 调用 Service.Create() → 返回创建结果 ├── Get(c *fiber.Ctx) │ └── 解析 ID 参数 → 调用 Service.Get() → 返回详情 ├── Update(c *fiber.Ctx) │ └── 解析 ID 和请求体 → 调用 Service.Update() → 返回更新结果 ├── Delete(c *fiber.Ctx) │ └── 解析 ID 参数 → 调用 Service.Delete() → 返回删除结果 ├── UpdateStatus(c *fiber.Ctx) │ └── 解析 ID 和状态 → 调用 Service.UpdateStatus() → 返回结果 ├── UpdateShelfStatus(c *fiber.Ctx) │ └── 解析 ID 和上架状态 → 调用 Service.UpdateShelfStatus() → 返回结果 └── UpdateRetailPrice(c *fiber.Ctx) └── 解析 ID 和零售价 → 调用 Service.UpdateRetailPrice() → 返回结果 ``` ### Service 层 ``` PackageService ├── Create(ctx, req) │ ├── 校验套餐编码唯一性 │ ├── 校验虚流量配置 │ │ ├── 启用时虚流量 > 0 │ │ └── 虚流量 ≤ 真流量 │ ├── 校验周期类型和时长 │ │ ├── natural_month: 需要 duration_months │ │ └── by_day: 需要 duration_days │ ├── 查询套餐系列信息 │ ├── 创建 Package 模型 │ └── 调用 Store.Create() ├── List(ctx, req) │ ├── 获取用户类型和店铺 ID │ ├── 代理用户额外过滤 │ │ └── INNER JOIN tb_shop_package_allocation │ ├── 应用其他过滤条件 │ └── 调用 Store.List() ├── Get(ctx, id) │ └── 调用 Store.GetByID() ├── Update(ctx, id, req) │ ├── 获取现有套餐 │ ├── 更新字段 │ └── 调用 Store.Update() ├── Delete(ctx, id) │ └── 调用 Store.Delete() ├── UpdateStatus(ctx, id, status) │ └── 更新状态字段 ├── UpdateShelfStatus(ctx, id, shelfStatus) │ └── 更新上架状态字段 └── UpdateRetailPrice(ctx, id, price) └── 更新零售价字段 ``` ### Store 层 ``` PackageStore ├── Create(ctx, pkg) │ └── db.Create(pkg) ├── GetByID(ctx, id) │ └── db.First(&pkg, id) ├── GetByCode(ctx, code) │ └── db.Where("package_code = ?", code).First(&pkg) ├── Update(ctx, pkg) │ └── db.Save(pkg) ├── Delete(ctx, id) │ └── db.Delete(&Package{}, id) └── List(ctx, opts, filters) ├── 构建基础查询 ├── 代理用户 JOIN 分配表 ├── 应用过滤条件 │ ├── package_name (模糊搜索) │ ├── series_id │ ├── status │ ├── shelf_status │ └── package_type ├── 计算总数 ├── 应用分页 └── 执行查询 ``` --- ## 3. 数据模型关系 ``` ┌──────────────────────────────────────────────────────────────┐ │ tb_package (套餐表) │ ├──────────────────────────────────────────────────────────────┤ │ id (PK) │ │ package_code (UNIQUE) │ │ package_name │ │ series_id (FK → tb_package_series) │ │ package_type (formal/addon) │ │ duration_months │ │ duration_days ← 关键字段 │ │ calendar_type (natural_month/by_day) │ │ real_data_mb │ │ virtual_data_mb │ │ enable_virtual_data │ │ data_reset_cycle │ │ expiry_base │ │ cost_price │ │ suggested_retail_price │ │ shelf_status │ │ status │ │ created_at, updated_at, deleted_at │ └──────────────────────────────────────────────────────────────┘ │ │ 1:N │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ tb_package_usage (套餐使用表) │ ├──────────────────────────────────────────────────────────────┤ │ id (PK) │ │ order_id (FK) │ │ package_id (FK → tb_package) │ │ usage_type (single_card/device) │ │ iot_card_id / device_id │ │ data_limit_mb │ │ data_usage_mb │ │ activated_at, expires_at │ │ status │ │ priority │ │ master_usage_id (加油包关联主套餐) │ └──────────────────────────────────────────────────────────────┘ │ │ 1:N │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ tb_package_usage_daily_record (日记录表) │ ├──────────────────────────────────────────────────────────────┤ │ id (PK) │ │ package_usage_id (FK) │ │ date │ │ daily_usage_mb │ │ cumulative_usage_mb │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ tb_package_series (套餐系列表) │ ├──────────────────────────────────────────────────────────────┤ │ id (PK) │ │ series_code (UNIQUE) │ │ series_name │ │ description │ │ enable_one_time_commission │ │ one_time_commission_config (JSONB) │ │ status │ └──────────────────────────────────────────────────────────────┘ │ │ 1:N │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ tb_shop_package_allocation (分配表) │ ├──────────────────────────────────────────────────────────────┤ │ id (PK) │ │ shop_id (FK → tb_shop) │ │ package_id (FK → tb_package) │ │ retail_price │ │ shelf_status │ │ status │ └──────────────────────────────────────────────────────────────┘ ``` --- ## 4. 请求流程示例 ### 创建套餐(按天) ``` 1. HTTP 请求 POST /api/admin/packages { "package_code": "PKG_DAY_30", "package_name": "30天套餐", "calendar_type": "by_day", "duration_days": 30, ... } 2. Handler.Create() ├─ 解析请求体 → CreatePackageRequest └─ 调用 Service.Create(ctx, req) 3. Service.Create() ├─ 校验 package_code 唯一性 │ └─ Store.GetByCode(ctx, "PKG_DAY_30") → nil ✓ ├─ 校验虚流量配置 ✓ ├─ 校验周期类型 │ ├─ calendar_type = "by_day" ✓ │ ├─ duration_days = 30 ✓ │ └─ 校验通过 ✓ ├─ 查询套餐系列 (可选) ├─ 创建 Package 模型 └─ Store.Create(ctx, pkg) 4. Store.Create() └─ db.Create(pkg) └─ INSERT INTO tb_package (...) 5. Service 返回 PackageResponse └─ Handler 返回 HTTP 200 6. HTTP 响应 { "code": 0, "data": { "id": 1, "package_code": "PKG_DAY_30", "duration_days": 30, ... }, "msg": "success" } ``` --- ## 5. 代理用户套餐查询流程 ``` 1. 代理用户请求 GET /api/admin/packages?status=1 2. Handler.List() └─ Service.List(ctx, req) 3. Service.List() ├─ 获取用户类型: UserTypeAgent ✓ ├─ 获取店铺 ID: 10 ✓ └─ Store.List(ctx, opts, filters) 4. Store.List() ├─ 构建基础查询 │ └─ SELECT * FROM tb_package ├─ 检测代理用户 │ └─ isAgent = true ✓ ├─ 添加 JOIN 条件 │ └─ 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 条件 │ └─ WHERE tb_shop_package_allocation.shop_id = 10 │ AND tb_shop_package_allocation.status = 1 ├─ 应用其他过滤 │ └─ AND tb_package.status = 1 ├─ 计算总数 ├─ 应用分页 └─ 执行查询 5. 返回结果 └─ 只返回已分配给店铺 10 的套餐 ``` --- ## 6. 字段验证流程 ``` CreatePackageRequest ├─ package_code │ └─ validate: required, min=1, max=100 │ └─ Service: 检查唯一性 ├─ package_name │ └─ validate: required, min=1, max=255 ├─ calendar_type │ └─ validate: oneof=natural_month by_day │ └─ Service: 根据类型校验时长字段 ├─ duration_months │ └─ validate: required, min=1, max=120 │ └─ Service: 当 calendar_type=natural_month 时必填 ├─ duration_days │ └─ validate: omitempty, min=1, max=3650 │ └─ Service: 当 calendar_type=by_day 时必填 ├─ real_data_mb │ └─ validate: omitempty, min=0 ├─ virtual_data_mb │ └─ validate: omitempty, min=0 │ └─ Service: 启用虚流量时必须 > 0 且 ≤ real_data_mb ├─ enable_virtual_data │ └─ Service: 虚流量配置校验 ├─ cost_price │ └─ validate: required, min=0 └─ suggested_retail_price └─ validate: omitempty, min=0 ```