All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s
17 KiB
17 KiB
套餐接口架构图
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