feat: major update - MySQL, chat, wishlist, PWA, admin overhaul
This commit is contained in:
@@ -1,128 +1,135 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"mengyastore-backend/internal/database"
|
||||
)
|
||||
|
||||
const visitCooldown = 6 * time.Hour
|
||||
|
||||
type siteData struct {
|
||||
TotalVisits int `json:"totalVisits"`
|
||||
}
|
||||
|
||||
type SiteStore struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
recentVisits map[string]time.Time
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSiteStore(path string) (*SiteStore, error) {
|
||||
if err := ensureSiteFile(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SiteStore{
|
||||
path: path,
|
||||
recentVisits: make(map[string]time.Time),
|
||||
}, nil
|
||||
func NewSiteStore(db *gorm.DB) (*SiteStore, error) {
|
||||
return &SiteStore{db: db}, nil
|
||||
}
|
||||
|
||||
func (s *SiteStore) RecordVisit(fingerprint string) (int, bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
s.cleanupRecentVisits(now)
|
||||
|
||||
key := buildSiteVisitKey(fingerprint)
|
||||
if last, ok := s.recentVisits[key]; ok && now.Sub(last) < visitCooldown {
|
||||
data, err := s.read()
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return data.TotalVisits, false, nil
|
||||
func (s *SiteStore) get(key string) (string, error) {
|
||||
var row database.SiteSettingRow
|
||||
if err := s.db.First(&row, "key = ?", key).Error; err != nil {
|
||||
return "", nil // key not found → return zero value
|
||||
}
|
||||
return row.Value, nil
|
||||
}
|
||||
|
||||
data, err := s.read()
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
data.TotalVisits++
|
||||
s.recentVisits[key] = now
|
||||
if err := s.write(data); err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return data.TotalVisits, true, nil
|
||||
func (s *SiteStore) set(key, value string) error {
|
||||
return s.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||
}).Create(&database.SiteSettingRow{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func (s *SiteStore) GetTotalVisits() (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, err := s.read()
|
||||
v, err := s.get("totalVisits")
|
||||
if err != nil || v == "" {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := strconv.Atoi(v)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *SiteStore) IncrementVisits() (int, error) {
|
||||
current, err := s.GetTotalVisits()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return data.TotalVisits, nil
|
||||
current++
|
||||
if err := s.set("totalVisits", strconv.Itoa(current)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (s *SiteStore) read() (siteData, error) {
|
||||
bytes, err := os.ReadFile(s.path)
|
||||
func (s *SiteStore) GetMaintenance() (enabled bool, reason string, err error) {
|
||||
v, err := s.get("maintenance")
|
||||
if err != nil {
|
||||
return siteData{}, fmt.Errorf("read site data: %w", err)
|
||||
return false, "", err
|
||||
}
|
||||
var data siteData
|
||||
if err := json.Unmarshal(bytes, &data); err != nil {
|
||||
return siteData{}, fmt.Errorf("parse site data: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
enabled = v == "true"
|
||||
reason, err = s.get("maintenanceReason")
|
||||
return enabled, reason, err
|
||||
}
|
||||
|
||||
func (s *SiteStore) write(data siteData) error {
|
||||
bytes, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode site data: %w", err)
|
||||
func (s *SiteStore) SetMaintenance(enabled bool, reason string) error {
|
||||
v := "false"
|
||||
if enabled {
|
||||
v = "true"
|
||||
}
|
||||
if err := os.WriteFile(s.path, bytes, 0o644); err != nil {
|
||||
return fmt.Errorf("write site data: %w", err)
|
||||
if err := s.set("maintenance", v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return s.set("maintenanceReason", reason)
|
||||
}
|
||||
|
||||
func (s *SiteStore) cleanupRecentVisits(now time.Time) {
|
||||
for key, last := range s.recentVisits {
|
||||
if now.Sub(last) >= visitCooldown {
|
||||
delete(s.recentVisits, key)
|
||||
// RecordVisit increments the visit counter. Returns (totalVisits, counted, error).
|
||||
// For simplicity, every call increments (fingerprint dedup is handled in-memory by the handler layer).
|
||||
func (s *SiteStore) RecordVisit(_ string) (int, bool, error) {
|
||||
total, err := s.IncrementVisits()
|
||||
return total, true, err
|
||||
}
|
||||
|
||||
// SMTPConfig holds the mail sender configuration stored in the DB.
|
||||
type SMTPConfig struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FromName string `json:"fromName"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
// IsConfiguredEmail returns true if the SMTP config is ready to send mail.
|
||||
func (c SMTPConfig) IsConfiguredEmail() bool {
|
||||
return c.Email != "" && c.Password != "" && c.Host != ""
|
||||
}
|
||||
|
||||
func (s *SiteStore) GetSMTPConfig() (SMTPConfig, error) {
|
||||
cfg := SMTPConfig{
|
||||
Host: "smtp.qq.com",
|
||||
Port: "465",
|
||||
}
|
||||
if v, _ := s.get("smtpEmail"); v != "" {
|
||||
cfg.Email = v
|
||||
}
|
||||
if v, _ := s.get("smtpPassword"); v != "" {
|
||||
cfg.Password = v
|
||||
}
|
||||
if v, _ := s.get("smtpFromName"); v != "" {
|
||||
cfg.FromName = v
|
||||
}
|
||||
if v, _ := s.get("smtpHost"); v != "" {
|
||||
cfg.Host = v
|
||||
}
|
||||
if v, _ := s.get("smtpPort"); v != "" {
|
||||
cfg.Port = v
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (s *SiteStore) SetSMTPConfig(cfg SMTPConfig) error {
|
||||
pairs := [][2]string{
|
||||
{"smtpEmail", cfg.Email},
|
||||
{"smtpPassword", cfg.Password},
|
||||
{"smtpFromName", cfg.FromName},
|
||||
{"smtpHost", cfg.Host},
|
||||
{"smtpPort", cfg.Port},
|
||||
}
|
||||
for _, p := range pairs {
|
||||
if err := s.set(p[0], p[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildSiteVisitKey(fingerprint string) string {
|
||||
sum := sha256.Sum256([]byte("site|" + fingerprint))
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func ensureSiteFile(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir data dir: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat site file: %w", err)
|
||||
}
|
||||
initial := siteData{TotalVisits: 0}
|
||||
bytes, err := json.MarshalIndent(initial, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("init site json: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
||||
return fmt.Errorf("write site json: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user