b5ab92a57e
来自 xhigh code review 的正确性/健壮性修复,覆盖全部五端: - server:鉴权 fail-closed、计量交叉校验与配额扣穿处理、WS 网关并发与关闭顺序、 billing 行锁、redis Lua 过期与设备槽刷新、config 解析 - desktop:会话 epoch 防串话、WS 重连与 401 处理、api 客户端复用、统一 usePoll 轮询 - android:握手时序、请求头封装、账户状态派生、按需重组 - ios:finalize 宽限、串行采集、错误文案服务端优先、删除死代码 CommitController Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
// Package user /v1/me 聚合、设备登记、应用更新检查。
|
||
package user
|
||
|
||
import (
|
||
"net/http"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
|
||
"dudu/server/internal/auth"
|
||
"dudu/server/internal/quota"
|
||
"dudu/server/internal/store"
|
||
"dudu/server/pkg/protocol"
|
||
)
|
||
|
||
type Handlers struct {
|
||
DB *gorm.DB
|
||
Quota *quota.Manager
|
||
// AppVersions 启动时由 config 解析好的平台→版本信息(17G);nil/空表示未配置。
|
||
AppVersions map[string]protocol.AppLatestResponse
|
||
}
|
||
|
||
// Me GET /v1/me(需登录):余额/试用/账户态聚合 + 设备心跳登记。
|
||
func (h *Handlers) Me(c *gin.Context) {
|
||
uid := auth.UserID(c)
|
||
var u store.User
|
||
if err := h.DB.First(&u, "id = ?", uid).Error; err != nil {
|
||
c.JSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized))
|
||
return
|
||
}
|
||
snap, err := h.Quota.Get(c, uid)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
|
||
return
|
||
}
|
||
h.touchDevice(c, uid)
|
||
|
||
state := protocol.AccountOK
|
||
switch {
|
||
case snap.BalanceSeconds > 0:
|
||
state = protocol.AccountOK
|
||
case snap.TrialUsedToday < protocol.TrialDailySeconds:
|
||
state = protocol.AccountTrial
|
||
default:
|
||
state = protocol.AccountQuota
|
||
}
|
||
c.JSON(http.StatusOK, protocol.MeResponse{
|
||
UserID: uid,
|
||
NicknameMasked: auth.MaskNickname(u.Nickname),
|
||
BalanceSeconds: snap.BalanceSeconds,
|
||
TrialDailyLimit: protocol.TrialDailySeconds,
|
||
TrialUsedToday: snap.TrialUsedToday,
|
||
AccountState: state,
|
||
})
|
||
}
|
||
|
||
// touchDevice 设备登记/心跳(X-Device-ID / X-Platform / X-App-Version 头)。
|
||
func (h *Handlers) touchDevice(c *gin.Context, uid string) {
|
||
did := c.GetHeader("X-Device-ID")
|
||
if did == "" {
|
||
return
|
||
}
|
||
dev := store.Device{
|
||
ID: did, UserID: uid,
|
||
Platform: c.GetHeader("X-Platform"), AppVersion: c.GetHeader("X-App-Version"),
|
||
LastSeen: time.Now(),
|
||
}
|
||
_ = h.DB.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "id"}},
|
||
DoUpdates: clause.AssignmentColumns([]string{"user_id", "platform", "app_version", "last_seen"}),
|
||
}).Create(&dev).Error
|
||
}
|
||
|
||
// AppLatest GET /v1/app/latest?platform=mac|win|android|ios 🔓
|
||
// 版本信息来自启动时 config 解析好的 APP_LATEST_JSON(17G,{"mac":{"version":...,"url":...},...}),
|
||
// MVP 不建表;未配置/未知平台返回 204。
|
||
func (h *Handlers) AppLatest(c *gin.Context) {
|
||
if v, ok := h.AppVersions[c.Query("platform")]; ok {
|
||
c.JSON(http.StatusOK, v)
|
||
return
|
||
}
|
||
c.Status(http.StatusNoContent)
|
||
}
|