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:
@@ -85,6 +85,49 @@ func (svc *Service) UsageCurve(ctx context.Context, userID int64, days int) ([]U
|
||||
return points, nil
|
||||
}
|
||||
|
||||
// DeviceUsagePoint is one device's aggregated usage over the requested window,
|
||||
// used by the stats page's per-device ("下分设备") breakdown.
|
||||
type DeviceUsagePoint struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"` // ios | android | windows | macos
|
||||
BytesUp uint64 `json:"bytes_up"`
|
||||
BytesDown uint64 `json:"bytes_down"`
|
||||
MinutesUsed int `json:"minutes_used"`
|
||||
}
|
||||
|
||||
// DeviceUsage returns per-device usage totals for userID over the last `days`
|
||||
// (UTC), busiest device first. days is clamped to [1, 90]. Devices with no
|
||||
// usage in the window are omitted (the response is never nil — empty slice).
|
||||
func (svc *Service) DeviceUsage(ctx context.Context, userID int64, days int) ([]DeviceUsagePoint, *apierr.Error) {
|
||||
if days < 1 {
|
||||
days = 7
|
||||
}
|
||||
if days > 90 {
|
||||
days = 90
|
||||
}
|
||||
|
||||
today := utcToday()
|
||||
from := today.AddDate(0, 0, -(days - 1))
|
||||
|
||||
rows, err := svc.store.DeviceUsageRange(ctx, userID, from, today)
|
||||
if err != nil {
|
||||
return nil, apierr.ErrInternal
|
||||
}
|
||||
points := make([]DeviceUsagePoint, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
points = append(points, DeviceUsagePoint{
|
||||
UUID: r.UUID,
|
||||
Name: r.Name,
|
||||
Platform: r.Platform,
|
||||
BytesUp: r.BytesUp,
|
||||
BytesDown: r.BytesDown,
|
||||
MinutesUsed: r.MinutesUsed,
|
||||
})
|
||||
}
|
||||
return points, nil
|
||||
}
|
||||
|
||||
// TodaySummary is the /v1/me today_usage block.
|
||||
type TodaySummary struct {
|
||||
MinutesUsed int `json:"minutes_used"`
|
||||
|
||||
Reference in New Issue
Block a user