完善初始化更新

This commit is contained in:
2026-03-20 20:42:33 +08:00
parent 568ccb08fa
commit e6866feb29
39 changed files with 6986 additions and 2379 deletions

View File

@@ -32,6 +32,10 @@ type EmailConfig struct {
Encryption string `json:"encryption"`
}
type CheckInConfig struct {
RewardCoins int `json:"rewardCoins"`
}
type Store struct {
dataDir string
usersDir string
@@ -41,10 +45,14 @@ type Store struct {
adminConfigPath string
authConfigPath string
emailConfigPath string
checkInPath string
registrationPath string
registrationConfig RegistrationConfig
adminToken string
jwtSecret []byte
issuer string
emailConfig EmailConfig
checkInConfig CheckInConfig
mu sync.Mutex
}
@@ -85,6 +93,8 @@ func NewStore(dataDir string) (*Store, error) {
adminConfigPath: filepath.Join(configDir, "admin.json"),
authConfigPath: filepath.Join(configDir, "auth.json"),
emailConfigPath: filepath.Join(configDir, "email.json"),
checkInPath: filepath.Join(configDir, "checkin.json"),
registrationPath: filepath.Join(configDir, "registration.json"),
}
if err := store.loadOrCreateAdminConfig(); err != nil {
return nil, err
@@ -95,6 +105,12 @@ func NewStore(dataDir string) (*Store, error) {
if err := store.loadOrCreateEmailConfig(); err != nil {
return nil, err
}
if err := store.loadOrCreateCheckInConfig(); err != nil {
return nil, err
}
if err := store.loadOrCreateRegistrationConfig(); err != nil {
return nil, err
}
return store, nil
}
@@ -118,6 +134,29 @@ func (s *Store) EmailConfig() EmailConfig {
return s.emailConfig
}
func (s *Store) CheckInConfig() CheckInConfig {
s.mu.Lock()
defer s.mu.Unlock()
cfg := s.checkInConfig
if cfg.RewardCoins <= 0 {
cfg.RewardCoins = 1
}
return cfg
}
func (s *Store) UpdateCheckInConfig(cfg CheckInConfig) error {
s.mu.Lock()
defer s.mu.Unlock()
if cfg.RewardCoins <= 0 {
cfg.RewardCoins = 1
}
if err := writeJSONFile(s.checkInPath, cfg); err != nil {
return err
}
s.checkInConfig = cfg
return nil
}
func (s *Store) loadOrCreateAdminConfig() error {
if _, err := os.Stat(s.adminConfigPath); errors.Is(err, os.ErrNotExist) {
token, err := generateToken()
@@ -244,6 +283,29 @@ func (s *Store) loadOrCreateEmailConfig() error {
return nil
}
func (s *Store) loadOrCreateCheckInConfig() error {
if _, err := os.Stat(s.checkInPath); errors.Is(err, os.ErrNotExist) {
cfg := CheckInConfig{RewardCoins: 1}
if err := writeJSONFile(s.checkInPath, cfg); err != nil {
return err
}
s.checkInConfig = cfg
return nil
}
var cfg CheckInConfig
if err := readJSONFile(s.checkInPath, &cfg); err != nil {
return err
}
if cfg.RewardCoins <= 0 {
cfg.RewardCoins = 1
if err := writeJSONFile(s.checkInPath, cfg); err != nil {
return err
}
}
s.checkInConfig = cfg
return nil
}
func generateSecret() ([]byte, error) {
secret := make([]byte, 32)
_, err := rand.Read(secret)
@@ -319,6 +381,176 @@ func (s *Store) SaveUser(record models.UserRecord) error {
return writeJSONFile(path, record)
}
// RecordAuthClient 在成功认证后记录第三方应用标识clientID 须已规范化)。
func (s *Store) RecordAuthClient(account string, clientID string, displayName string) (models.UserRecord, error) {
if clientID == "" {
return models.UserRecord{}, errors.New("client id required")
}
s.mu.Lock()
defer s.mu.Unlock()
path := s.userFilePath(account)
var record models.UserRecord
if err := readJSONFile(path, &record); err != nil {
if errors.Is(err, os.ErrNotExist) {
return models.UserRecord{}, os.ErrNotExist
}
return models.UserRecord{}, err
}
now := models.NowISO()
displayName = models.ClampAuthClientName(displayName)
found := false
for i := range record.AuthClients {
if record.AuthClients[i].ClientID == clientID {
record.AuthClients[i].LastSeenAt = now
if displayName != "" {
record.AuthClients[i].DisplayName = displayName
}
found = true
break
}
}
if !found {
record.AuthClients = append(record.AuthClients, models.AuthClientEntry{
ClientID: clientID,
DisplayName: displayName,
FirstSeenAt: now,
LastSeenAt: now,
})
}
record.UpdatedAt = now
if err := writeJSONFile(path, &record); err != nil {
return models.UserRecord{}, err
}
return record, nil
}
func (s *Store) RecordVisit(account string, today string, at string) (models.UserRecord, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
path := s.userFilePath(account)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return models.UserRecord{}, false, os.ErrNotExist
}
var record models.UserRecord
if err := readJSONFile(path, &record); err != nil {
return models.UserRecord{}, false, err
}
if record.LastVisitDate == today || models.HasActivityDate(record.VisitTimes, today) {
return record, false, nil
}
if strings.TrimSpace(at) == "" {
at = models.CurrentActivityTime()
}
record.LastVisitDate = today
record.LastVisitAt = at
record.VisitTimes = append(record.VisitTimes, at)
if record.CreatedAt == "" {
record.CreatedAt = models.NowISO()
}
record.UpdatedAt = models.NowISO()
if err := writeJSONFile(path, record); err != nil {
return models.UserRecord{}, false, err
}
return record, true, nil
}
const maxLastVisitIPLen = 45
const maxLastVisitDisplayLocationLen = 512
func clampVisitMeta(ip, displayLocation string) (string, string) {
ip = strings.TrimSpace(ip)
displayLocation = strings.TrimSpace(displayLocation)
if len(ip) > maxLastVisitIPLen {
ip = ip[:maxLastVisitIPLen]
}
if len(displayLocation) > maxLastVisitDisplayLocationLen {
displayLocation = displayLocation[:maxLastVisitDisplayLocationLen]
}
return ip, displayLocation
}
// UpdateLastVisitMeta 更新用户最近一次访问的客户端 IP 与展示用地理位置(由前端调用地理接口后传入)。
func (s *Store) UpdateLastVisitMeta(account string, ip string, displayLocation string) (models.UserRecord, error) {
ip, displayLocation = clampVisitMeta(ip, displayLocation)
if ip == "" && displayLocation == "" {
rec, found, err := s.GetUser(account)
if err != nil {
return models.UserRecord{}, err
}
if !found {
return models.UserRecord{}, os.ErrNotExist
}
return rec, nil
}
s.mu.Lock()
defer s.mu.Unlock()
path := s.userFilePath(account)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return models.UserRecord{}, os.ErrNotExist
}
var record models.UserRecord
if err := readJSONFile(path, &record); err != nil {
return models.UserRecord{}, err
}
if ip != "" {
record.LastVisitIP = ip
}
if displayLocation != "" {
record.LastVisitDisplayLocation = displayLocation
}
record.UpdatedAt = models.NowISO()
if err := writeJSONFile(path, record); err != nil {
return models.UserRecord{}, err
}
return record, nil
}
func (s *Store) CheckIn(account string, today string, at string) (models.UserRecord, int, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
path := s.userFilePath(account)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return models.UserRecord{}, 0, false, os.ErrNotExist
}
var record models.UserRecord
if err := readJSONFile(path, &record); err != nil {
return models.UserRecord{}, 0, false, err
}
if record.LastCheckInDate == today || models.HasActivityDate(record.CheckInTimes, today) {
return record, 0, true, nil
}
reward := s.checkInConfig.RewardCoins
if reward <= 0 {
reward = 1
}
record.SproutCoins += reward
record.LastCheckInDate = today
if strings.TrimSpace(at) == "" {
at = models.CurrentActivityTime()
}
record.LastCheckInAt = at
record.CheckInTimes = append(record.CheckInTimes, at)
if record.CreatedAt == "" {
record.CreatedAt = models.NowISO()
}
record.UpdatedAt = models.NowISO()
if err := writeJSONFile(path, record); err != nil {
return models.UserRecord{}, 0, false, err
}
return record, reward, false, nil
}
func (s *Store) DeleteUser(account string) error {
s.mu.Lock()
defer s.mu.Unlock()