feat(server/stats): stats-overhaul Phase2 — 每设备归因 + GB 综合配额
服务端记账从「账户」细到「每设备」(每设备独立 dp_uuid),配额单位分钟→GB 按账户综合卡控。000015_per_device_usage 迁移(mysql+sqlite 双份):devices.dp_uuid + usage_device_daily 表 + plans.daily_mb。handler_grpc 按 dp_uuid 回映射 (user_id,device_id) 双写账户+每设备;usage 服务/handler 暴露 /v1/usage(/devices)。 含 sqlite_per_device / usage handler 测试。 注:此迁移 prod 已 migrate up 运行、部署二进制已内嵌;本次补提交使 git 与线上一致。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,48 @@ func (s *Store) GetUsageRange(ctx context.Context, userID int64, from, to time.T
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// DeviceUsageRow is one device's aggregated usage over a date window, joined to
|
||||
// the devices table for display metadata. It is the per-device ("下分设备")
|
||||
// counterpart of DailyUsage's account rollup.
|
||||
type DeviceUsageRow struct {
|
||||
UUID string
|
||||
Name string
|
||||
Platform string
|
||||
BytesUp uint64
|
||||
BytesDown uint64
|
||||
MinutesUsed int
|
||||
}
|
||||
|
||||
// DeviceUsageRange returns each device's summed usage over [from, to] (inclusive
|
||||
// UTC dates) for userID, one row per device, ordered by total bytes descending.
|
||||
// Devices with no usage in the window are omitted. All non-aggregate columns
|
||||
// are in GROUP BY for MySQL ONLY_FULL_GROUP_BY portability (sqlite tolerant).
|
||||
func (s *Store) DeviceUsageRange(ctx context.Context, userID int64, from, to time.Time) ([]DeviceUsageRow, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT d.uuid, d.name, d.platform,
|
||||
SUM(ud.bytes_up), SUM(ud.bytes_down), SUM(ud.minutes_used)
|
||||
FROM usage_device_daily ud
|
||||
JOIN devices d ON d.id = ud.device_id
|
||||
WHERE ud.user_id = ? AND ud.date BETWEEN ? AND ?
|
||||
GROUP BY ud.device_id, d.uuid, d.name, d.platform
|
||||
ORDER BY (SUM(ud.bytes_up) + SUM(ud.bytes_down)) DESC`,
|
||||
userID, from.UTC().Format(dateLayout), to.UTC().Format(dateLayout))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store.DeviceUsageRange: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []DeviceUsageRow
|
||||
for rows.Next() {
|
||||
var r DeviceUsageRow
|
||||
if err := rows.Scan(&r.UUID, &r.Name, &r.Platform, &r.BytesUp, &r.BytesDown, &r.MinutesUsed); err != nil {
|
||||
return nil, fmt.Errorf("store.DeviceUsageRange scan: %w", err)
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetDay returns the usage_daily row for (userID, day), or nil if none exists.
|
||||
func (s *Store) GetDay(ctx context.Context, userID int64, day time.Time) (*DailyUsage, error) {
|
||||
var u DailyUsage
|
||||
|
||||
Reference in New Issue
Block a user