完善初始化更新
This commit is contained in:
106
sproutgate-backend/internal/models/activity.go
Normal file
106
sproutgate-backend/internal/models/activity.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var activityLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
|
||||
|
||||
const (
|
||||
// 日与时刻之间留空格,避免「20日11点」粘连难读
|
||||
ActivityTimeLayout = "2006年1月2日 15点04分05秒"
|
||||
// 历史数据可能无空格,解析时兼容
|
||||
activityTimeLayoutLegacy = "2006年1月2日15点04分05秒"
|
||||
ActivityDateLayout = "2006-01-02"
|
||||
)
|
||||
|
||||
func CurrentActivityDate() string {
|
||||
return time.Now().In(activityLocation).Format(ActivityDateLayout)
|
||||
}
|
||||
|
||||
func CurrentActivityTime() string {
|
||||
return FormatActivityTime(time.Now())
|
||||
}
|
||||
|
||||
func FormatActivityTime(t time.Time) string {
|
||||
return t.In(activityLocation).Format(ActivityTimeLayout)
|
||||
}
|
||||
|
||||
func ParseActivityTime(value string) (time.Time, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return time.Time{}, false
|
||||
}
|
||||
layouts := []string{ActivityTimeLayout, activityTimeLayoutLegacy, time.RFC3339Nano, time.RFC3339}
|
||||
for _, layout := range layouts {
|
||||
if parsed, err := time.ParseInLocation(layout, value, activityLocation); err == nil {
|
||||
return parsed.In(activityLocation), true
|
||||
}
|
||||
if parsed, err := time.Parse(layout, value); err == nil {
|
||||
return parsed.In(activityLocation), true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func ActivityDate(value string) (string, bool) {
|
||||
parsed, ok := ParseActivityTime(value)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return parsed.In(activityLocation).Format(ActivityDateLayout), true
|
||||
}
|
||||
|
||||
func HasActivityDate(values []string, date string) bool {
|
||||
for _, value := range values {
|
||||
if parsedDate, ok := ActivityDate(value); ok && parsedDate == date {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ActivitySummary(values []string, fallbackDate string) (days int, streak int, lastAt string) {
|
||||
dateSet := make(map[string]struct{}, len(values))
|
||||
var latest time.Time
|
||||
hasLatest := false
|
||||
|
||||
for _, value := range values {
|
||||
parsed, ok := ParseActivityTime(value)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dateKey := parsed.In(activityLocation).Format(ActivityDateLayout)
|
||||
dateSet[dateKey] = struct{}{}
|
||||
if !hasLatest || parsed.After(latest) {
|
||||
latest = parsed
|
||||
hasLatest = true
|
||||
lastAt = FormatActivityTime(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dateSet) == 0 && strings.TrimSpace(fallbackDate) != "" {
|
||||
dateSet[strings.TrimSpace(fallbackDate)] = struct{}{}
|
||||
days = 1
|
||||
streak = 1
|
||||
return
|
||||
}
|
||||
|
||||
days = len(dateSet)
|
||||
if !hasLatest {
|
||||
return
|
||||
}
|
||||
|
||||
cursor := time.Date(latest.In(activityLocation).Year(), latest.In(activityLocation).Month(), latest.In(activityLocation).Day(), 0, 0, 0, 0, activityLocation)
|
||||
for {
|
||||
key := cursor.Format(ActivityDateLayout)
|
||||
if _, ok := dateSet[key]; !ok {
|
||||
break
|
||||
}
|
||||
streak++
|
||||
cursor = cursor.AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
40
sproutgate-backend/internal/models/authclient.go
Normal file
40
sproutgate-backend/internal/models/authclient.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxAuthClientIDLen = 64
|
||||
MaxAuthClientNameLen = 128
|
||||
)
|
||||
|
||||
var authClientIDRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,63}$`)
|
||||
|
||||
// AuthClientEntry 记录某第三方应用曾用本账号完成认证(登录 / 校验令牌 / 拉取 me)。
|
||||
type AuthClientEntry struct {
|
||||
ClientID string `json:"clientId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
FirstSeenAt string `json:"firstSeenAt"`
|
||||
LastSeenAt string `json:"lastSeenAt"`
|
||||
}
|
||||
|
||||
func NormalizeAuthClientID(raw string) (string, bool) {
|
||||
s := strings.TrimSpace(raw)
|
||||
if s == "" || len(s) > MaxAuthClientIDLen {
|
||||
return "", false
|
||||
}
|
||||
if !authClientIDRe.MatchString(s) {
|
||||
return "", false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
func ClampAuthClientName(raw string) string {
|
||||
s := strings.TrimSpace(raw)
|
||||
if len(s) > MaxAuthClientNameLen {
|
||||
return s[:MaxAuthClientNameLen]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -8,4 +8,5 @@ type PendingUser struct {
|
||||
CodeHash string `json:"codeHash"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
InviteCode string `json:"inviteCode,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,52 +1,144 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserRecord struct {
|
||||
Account string `json:"account"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins int `json:"sproutCoins"`
|
||||
SecondaryEmails []string `json:"secondaryEmails,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
AvatarURL string `json:"avatarUrl,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Account string `json:"account"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins int `json:"sproutCoins"`
|
||||
LastCheckInDate string `json:"lastCheckInDate,omitempty"`
|
||||
LastCheckInAt string `json:"lastCheckInAt,omitempty"`
|
||||
LastVisitDate string `json:"lastVisitDate,omitempty"`
|
||||
LastVisitAt string `json:"lastVisitAt,omitempty"`
|
||||
LastVisitIP string `json:"lastVisitIp,omitempty"`
|
||||
LastVisitDisplayLocation string `json:"lastVisitDisplayLocation,omitempty"`
|
||||
CheckInTimes []string `json:"checkInTimes,omitempty"`
|
||||
VisitTimes []string `json:"visitTimes,omitempty"`
|
||||
SecondaryEmails []string `json:"secondaryEmails,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
AvatarURL string `json:"avatarUrl,omitempty"`
|
||||
WebsiteURL string `json:"websiteUrl,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Banned bool `json:"banned"`
|
||||
BanReason string `json:"banReason,omitempty"`
|
||||
BannedAt string `json:"bannedAt,omitempty"`
|
||||
AuthClients []AuthClientEntry `json:"authClients,omitempty"`
|
||||
}
|
||||
|
||||
type UserPublic struct {
|
||||
Account string `json:"account"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins int `json:"sproutCoins"`
|
||||
SecondaryEmails []string `json:"secondaryEmails,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
AvatarURL string `json:"avatarUrl,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Account string `json:"account"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins int `json:"sproutCoins"`
|
||||
LastCheckInDate string `json:"lastCheckInDate,omitempty"`
|
||||
LastCheckInAt string `json:"lastCheckInAt,omitempty"`
|
||||
LastVisitDate string `json:"lastVisitDate,omitempty"`
|
||||
CheckInDays int `json:"checkInDays"`
|
||||
CheckInStreak int `json:"checkInStreak"`
|
||||
LastVisitAt string `json:"lastVisitAt,omitempty"`
|
||||
LastVisitIP string `json:"lastVisitIp,omitempty"`
|
||||
LastVisitDisplayLocation string `json:"lastVisitDisplayLocation,omitempty"`
|
||||
VisitDays int `json:"visitDays"`
|
||||
VisitStreak int `json:"visitStreak"`
|
||||
SecondaryEmails []string `json:"secondaryEmails,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
AvatarURL string `json:"avatarUrl,omitempty"`
|
||||
WebsiteURL string `json:"websiteUrl,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Banned bool `json:"banned,omitempty"`
|
||||
BanReason string `json:"banReason,omitempty"`
|
||||
BannedAt string `json:"bannedAt,omitempty"`
|
||||
AuthClients []AuthClientEntry `json:"authClients,omitempty"`
|
||||
}
|
||||
|
||||
func (u UserRecord) Public() UserPublic {
|
||||
checkInDays, checkInStreak, lastCheckInAt := ActivitySummary(u.CheckInTimes, u.LastCheckInDate)
|
||||
visitDays, visitStreak, lastVisitAt := ActivitySummary(u.VisitTimes, u.LastVisitDate)
|
||||
if strings.TrimSpace(u.LastCheckInAt) != "" {
|
||||
lastCheckInAt = u.LastCheckInAt
|
||||
}
|
||||
if strings.TrimSpace(u.LastVisitAt) != "" {
|
||||
lastVisitAt = u.LastVisitAt
|
||||
}
|
||||
return UserPublic{
|
||||
Account: u.Account,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Level: u.Level,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Level: u.Level,
|
||||
SproutCoins: u.SproutCoins,
|
||||
LastCheckInDate: u.LastCheckInDate,
|
||||
LastCheckInAt: lastCheckInAt,
|
||||
LastVisitDate: u.LastVisitDate,
|
||||
CheckInDays: checkInDays,
|
||||
CheckInStreak: checkInStreak,
|
||||
LastVisitAt: lastVisitAt,
|
||||
VisitDays: visitDays,
|
||||
VisitStreak: visitStreak,
|
||||
SecondaryEmails: u.SecondaryEmails,
|
||||
Phone: u.Phone,
|
||||
AvatarURL: u.AvatarURL,
|
||||
WebsiteURL: u.WebsiteURL,
|
||||
Bio: u.Bio,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// PublicProfile 在 Public 基础上附带最近访问 IP / 展示用地理位置,仅供「用户公开主页」接口使用。
|
||||
func (u UserRecord) PublicProfile() UserPublic {
|
||||
p := u.Public()
|
||||
p.LastVisitIP = strings.TrimSpace(u.LastVisitIP)
|
||||
p.LastVisitDisplayLocation = strings.TrimSpace(u.LastVisitDisplayLocation)
|
||||
return p
|
||||
}
|
||||
|
||||
// OwnerPublic 包含仅本人/管理员可见的字段(如最近访问 IP),勿用于公开资料接口。
|
||||
func (u UserRecord) OwnerPublic() UserPublic {
|
||||
p := u.Public()
|
||||
p.LastVisitIP = strings.TrimSpace(u.LastVisitIP)
|
||||
p.LastVisitDisplayLocation = strings.TrimSpace(u.LastVisitDisplayLocation)
|
||||
p.Banned = u.Banned
|
||||
p.BanReason = strings.TrimSpace(u.BanReason)
|
||||
p.BannedAt = strings.TrimSpace(u.BannedAt)
|
||||
if len(u.AuthClients) > 0 {
|
||||
p.AuthClients = append([]AuthClientEntry(nil), u.AuthClients...)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type UserShowcase struct {
|
||||
Account string `json:"account"`
|
||||
Username string `json:"username"`
|
||||
Level int `json:"level"`
|
||||
SproutCoins int `json:"sproutCoins"`
|
||||
AvatarURL string `json:"avatarUrl,omitempty"`
|
||||
WebsiteURL string `json:"websiteUrl,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
}
|
||||
|
||||
func (u UserRecord) Showcase() UserShowcase {
|
||||
return UserShowcase{
|
||||
Account: u.Account,
|
||||
Username: u.Username,
|
||||
Level: u.Level,
|
||||
SproutCoins: u.SproutCoins,
|
||||
AvatarURL: u.AvatarURL,
|
||||
WebsiteURL: u.WebsiteURL,
|
||||
Bio: u.Bio,
|
||||
}
|
||||
}
|
||||
|
||||
func NowISO() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user