diff --git a/internal/model/dto/record_query_dto.go b/internal/model/dto/record_query_dto.go index f1f727d0..22b51b97 100644 --- a/internal/model/dto/record_query_dto.go +++ b/internal/model/dto/record_query_dto.go @@ -103,18 +103,22 @@ type TrafficCollectionFailureListResponse struct { type CardDailyUsageListRequest struct { RecordDateRangeQuery CarrierID *uint `query:"carrier_id" validate:"omitempty,min=1" description:"运营商账户ID"` - MinUsageMB *float64 `query:"min_usage_mb" validate:"omitempty,min=0" description:"单日用量下限(MB),用于排查异常大流量"` + MinUsageMB *float64 `query:"min_usage_mb" validate:"omitempty,min=0" description:"当日已结算增量下限(MB),用于排查异常大流量"` } -// CardDailyUsageItem 卡日流量。 +// CardDailyUsageItem 卡日流量(卡日快照 tb_card_flow_day)。 type CardDailyUsageItem struct { - ID uint `json:"id" description:"记录ID"` - IotCardID uint `json:"iot_card_id" description:"卡ID"` - ICCID string `json:"iccid" description:"ICCID"` - CarrierID uint `json:"carrier_id" description:"运营商账户ID"` - Date string `json:"date" description:"日期"` - UsageMB float64 `json:"usage_mb" description:"当日流量(MB)"` - UpdatedAt time.Time `json:"updated_at" description:"更新时间"` + ID uint `json:"id" description:"记录ID"` + IotCardID uint `json:"iot_card_id" description:"卡ID"` + ICCID string `json:"iccid" description:"ICCID"` + CarrierID uint `json:"carrier_id" description:"运营商账户ID"` + Date string `json:"date" description:"日期(上海)"` + SettledMB float64 `json:"settled_mb" description:"当日已结算增量合计(MB),即当日用量"` + MonthUsedMB float64 `json:"month_used_mb" description:"当日结束时的本月自然月累计(MB)"` + VendorUsedMB float64 `json:"vendor_used_mb" description:"当天最后一次写入的运营商原始读数(MB)"` + StaleGuarded bool `json:"stale_guarded" description:"当天是否触发重置日防旧值"` + ObsCount int `json:"obs_count" description:"当天写入次数"` + UpdatedAt time.Time `json:"updated_at" description:"更新时间"` } // CardDailyUsageListResponse 卡日流量分页结果。 diff --git a/internal/routes/record_query.go b/internal/routes/record_query.go index 70727650..16f477d7 100644 --- a/internal/routes/record_query.go +++ b/internal/routes/record_query.go @@ -25,7 +25,7 @@ func registerRecordQueryRoutes(router fiber.Router, asset *admin.AssetHandler, r }) Register(router, doc, basePath, "GET", "/card-daily-usages", asset.ListCardDailyUsages, RouteSpec{ Summary: "卡日流量记录", - Description: recordQueryDescription + "按日期范围(含首尾)查询;min_usage_mb 可筛异常大流量。", + Description: recordQueryDescription + "数据来自卡日快照(每卡每天一条);按日期范围(含首尾)查询;min_usage_mb 按当日已结算增量筛异常大流量。", Tags: []string{"记录查询"}, Input: new(dto.CardDailyUsageListRequest), Output: new(dto.CardDailyUsageListResponse), Auth: true, }) Register(router, doc, basePath, "GET", "/package-usage-daily-records", asset.ListPackageUsageDailyRecords, RouteSpec{ diff --git a/internal/service/asset/record_query.go b/internal/service/asset/record_query.go index 54df7978..426f6b6c 100644 --- a/internal/service/asset/record_query.go +++ b/internal/service/asset/record_query.go @@ -237,13 +237,13 @@ func (s *Service) ListCardDailyUsages(ctx context.Context, req *dto.CardDailyUsa return resp, err } type row struct { - model.CardDailyUsage + model.CardFlowDay ICCID string CarrierID uint } - query := s.db.WithContext(ctx).Table("tb_card_daily_usage AS u"). + query := s.db.WithContext(ctx).Table("tb_card_flow_day AS u"). Select("u.*, c.iccid, c.carrier_id").Joins("LEFT JOIN tb_iot_card c ON c.id = u.iot_card_id"). - Where("u.date >= ? AND u.date <= ?", from.Format(recordDateLayout), to.Format(recordDateLayout)) + Where("u.flow_date >= ? AND u.flow_date <= ?", from.Format(recordDateLayout), to.Format(recordDateLayout)) if !scope.all { query = query.Where("u.iot_card_id IN ?", scope.cardIDs) } @@ -251,16 +251,17 @@ func (s *Service) ListCardDailyUsages(ctx context.Context, req *dto.CardDailyUsa query = query.Where("c.carrier_id = ?", *req.CarrierID) } if req.MinUsageMB != nil { - query = query.Where("u.usage_mb >= ?", *req.MinUsageMB) + query = query.Where("u.settled_mb >= ?", *req.MinUsageMB) } - rows, total, err := pageQuery[row](query, "u.date DESC, u.id DESC", page, size) + rows, total, err := pageQuery[row](query, "u.flow_date DESC, u.id DESC", page, size) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡日流量失败") } for _, item := range rows { resp.Items = append(resp.Items, dto.CardDailyUsageItem{ ID: item.ID, IotCardID: item.IotCardID, ICCID: item.ICCID, CarrierID: item.CarrierID, - Date: item.Date.Format(recordDateLayout), UsageMB: item.UsageMB, UpdatedAt: item.UpdatedAt, + Date: item.FlowDate.Format(recordDateLayout), SettledMB: item.SettledMB, MonthUsedMB: item.MonthUsedMB, + VendorUsedMB: item.VendorUsedMB, StaleGuarded: item.StaleGuarded, ObsCount: item.ObsCount, UpdatedAt: item.UpdatedAt, }) } resp.Total = total diff --git a/migrations/000273_add_record_query_indexes.down.sql b/migrations/000273_add_record_query_indexes.down.sql index 2d7d079d..7bf2211e 100644 --- a/migrations/000273_add_record_query_indexes.down.sql +++ b/migrations/000273_add_record_query_indexes.down.sql @@ -1,6 +1,6 @@ DROP INDEX IF EXISTS idx_package_usage_adjustment_time; DROP INDEX IF EXISTS idx_package_usage_daily_record_day; -DROP INDEX IF EXISTS idx_card_daily_usage_date_usage; +DROP INDEX IF EXISTS idx_card_flow_day_date_settled; DROP INDEX IF EXISTS idx_card_stop_resume_record_command; DROP INDEX IF EXISTS idx_card_stop_resume_record_scene; DROP INDEX IF EXISTS idx_card_stop_resume_record_time; diff --git a/migrations/000273_add_record_query_indexes.up.sql b/migrations/000273_add_record_query_indexes.up.sql index 2650424a..7d56d057 100644 --- a/migrations/000273_add_record_query_indexes.up.sql +++ b/migrations/000273_add_record_query_indexes.up.sql @@ -6,7 +6,8 @@ CREATE INDEX IF NOT EXISTS idx_card_stop_resume_record_time ON tb_card_stop_resu CREATE INDEX IF NOT EXISTS idx_card_stop_resume_record_scene ON tb_card_stop_resume_record (scene, created_at DESC); CREATE INDEX IF NOT EXISTS idx_card_stop_resume_record_command ON tb_card_stop_resume_record (command_id); -CREATE INDEX IF NOT EXISTS idx_card_daily_usage_date_usage ON tb_card_daily_usage (date, usage_mb DESC); +-- 卡日流量取采集链路写入的卡日快照 tb_card_flow_day(tb_card_daily_usage 为已停用旧表)。 +CREATE INDEX IF NOT EXISTS idx_card_flow_day_date_settled ON tb_card_flow_day (flow_date, settled_mb DESC); CREATE INDEX IF NOT EXISTS idx_package_usage_daily_record_day ON tb_package_usage_daily_record (date); diff --git a/openspec/changes/refactor-audit-log-persistence/design.md b/openspec/changes/refactor-audit-log-persistence/design.md index 37ac182a..c3504e39 100644 --- a/openspec/changes/refactor-audit-log-persistence/design.md +++ b/openspec/changes/refactor-audit-log-persistence/design.md @@ -214,7 +214,7 @@ | 采集成功表 | `(observed_at DESC)`、`(carrier_id, observed_at DESC)`、`(decision, observed_at DESC)`,保留现有 `(iot_card_id, observed_at DESC)` | | 采集失败表 | `(iot_card_id, observed_at DESC)`、`(observed_at DESC)`、`(carrier_id, observed_at DESC)`、`(error_category, observed_at DESC)`、`(observation_id)` | | 停复机记录 | 现有三个升序索引保留(B-tree 可反向扫描,直接支持倒序分页);新增 `(created_at DESC)`、`(scene, created_at DESC)`、`(command_id)` | -| 卡日流量 | 新增 `(date, usage_mb DESC)` | +| 卡日流量(卡日快照 `tb_card_flow_day`;`tb_card_daily_usage` 为已停用旧表) | 新增 `(flow_date, settled_mb DESC)` | | 套餐日流量详单 | 新增 `(package_usage_id, date DESC)`、`(date)` | | 冲正流水 | 新增 `(iot_card_id, created_at DESC)`、`(created_at DESC)` | | 退款后处理尝试 | `(refund_id, created_at DESC)`、`(created_at DESC)`、`(result, created_at DESC)`、`(error_code, created_at DESC)` | diff --git a/openspec/changes/refactor-audit-log-persistence/specs/operational-record-query/spec.md b/openspec/changes/refactor-audit-log-persistence/specs/operational-record-query/spec.md index 4f78ec82..760a69a0 100644 --- a/openspec/changes/refactor-audit-log-persistence/specs/operational-record-query/spec.md +++ b/openspec/changes/refactor-audit-log-persistence/specs/operational-record-query/spec.md @@ -88,7 +88,7 @@ | 记录 | 筛选条件 | |---|---| | 套餐日流量详单 | 资产标识、套餐使用记录、套餐、运营商账户、日期范围 | -| 卡日流量 | 资产标识、运营商账户、日期范围、用量下限 | +| 卡日流量(卡日快照) | 资产标识、运营商账户、日期范围、当日已结算增量下限 | | 套餐流量冲正流水 | 资产标识、套餐使用记录、操作人、调整方向(补扣或退回)、时间范围 | 现有套餐日流量详单接口 SHALL 增加分页。现有单套餐冲正流水接口 SHALL 增加时间范围筛选。