Files
wangjia b5ab92a57e
ci / server (push) Failing after 13s
ci / design-tokens (push) Failing after 11s
fix: 应用 xhigh 代码评审的跨端修复
来自 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>
2026-06-13 11:50:08 +08:00

86 lines
2.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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_JSON17G{"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)
}