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:
@@ -69,6 +69,56 @@ func (h *UsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(usageResponse{Points: points})
|
||||
}
|
||||
|
||||
// DeviceUsageHandler serves GET /v1/usage/devices?days=N — per-device usage
|
||||
// totals for the authenticated account ("下分设备" breakdown).
|
||||
type DeviceUsageHandler struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
// NewDeviceUsageHandler creates a DeviceUsageHandler.
|
||||
func NewDeviceUsageHandler(svc *Service) *DeviceUsageHandler { return &DeviceUsageHandler{svc: svc} }
|
||||
|
||||
type deviceUsageResponse struct {
|
||||
Devices []DeviceUsagePoint `json:"devices"`
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *DeviceUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
userID := userIDFromContext(r)
|
||||
if userID == 0 {
|
||||
apierr.WriteJSON(w, http.StatusUnauthorized, &apierr.Error{
|
||||
Code: "UNAUTHORIZED",
|
||||
MessageZH: "请先登录",
|
||||
MessageEn: "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
days := 7
|
||||
if v := r.URL.Query().Get("days"); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 || n > 90 {
|
||||
apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
days = n
|
||||
}
|
||||
|
||||
devices, apiErr := h.svc.DeviceUsage(r.Context(), userID, days)
|
||||
if apiErr != nil {
|
||||
apierr.WriteJSON(w, http.StatusInternalServerError, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(deviceUsageResponse{Devices: devices})
|
||||
}
|
||||
|
||||
// AdsUnlockHandler serves POST /v1/ads/unlock.
|
||||
type AdsUnlockHandler struct {
|
||||
svc *Service
|
||||
|
||||
Reference in New Issue
Block a user