package storage import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/google/uuid" "mengyastore-backend/internal/models" ) const defaultCoverURL = "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png" const viewCooldown = 6 * time.Hour const maxScreenshotURLs = 5 type JSONStore struct { path string mu sync.Mutex recentViews map[string]time.Time } func NewJSONStore(path string) (*JSONStore, error) { if err := ensureProductsFile(path); err != nil { return nil, err } return &JSONStore{ path: path, recentViews: make(map[string]time.Time), }, nil } func ensureProductsFile(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 data file: %w", err) } initial := []models.Product{} bytes, err := json.MarshalIndent(initial, "", " ") if err != nil { return fmt.Errorf("init json: %w", err) } if err := os.WriteFile(path, bytes, 0o644); err != nil { return fmt.Errorf("write init json: %w", err) } return nil } func (s *JSONStore) ListAll() ([]models.Product, error) { s.mu.Lock() defer s.mu.Unlock() return s.readAll() } func (s *JSONStore) ListActive() ([]models.Product, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return nil, err } active := make([]models.Product, 0, len(items)) for _, item := range items { if item.Active { active = append(active, item) } } return active, nil } func (s *JSONStore) GetByID(id string) (models.Product, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return models.Product{}, err } for _, item := range items { if item.ID == id { return item, nil } } return models.Product{}, fmt.Errorf("product not found") } func (s *JSONStore) Create(p models.Product) (models.Product, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return models.Product{}, err } p = normalizeProduct(p) p.ID = uuid.NewString() now := time.Now() p.CreatedAt = now p.UpdatedAt = now items = append(items, p) if err := s.writeAll(items); err != nil { return models.Product{}, err } return p, nil } func (s *JSONStore) Update(id string, patch models.Product) (models.Product, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return models.Product{}, err } for i, item := range items { if item.ID == id { normalized := normalizeProduct(patch) item.Name = normalized.Name item.Price = normalized.Price item.DiscountPrice = normalized.DiscountPrice item.Tags = normalized.Tags item.CoverURL = normalized.CoverURL item.ScreenshotURLs = normalized.ScreenshotURLs item.VerificationURL = normalized.VerificationURL item.Codes = normalized.Codes item.Quantity = normalized.Quantity item.Description = normalized.Description item.Active = normalized.Active item.UpdatedAt = time.Now() items[i] = item if err := s.writeAll(items); err != nil { return models.Product{}, err } return item, nil } } return models.Product{}, fmt.Errorf("product not found") } func (s *JSONStore) Toggle(id string, active bool) (models.Product, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return models.Product{}, err } for i, item := range items { if item.ID == id { item.Active = active item.UpdatedAt = time.Now() items[i] = item if err := s.writeAll(items); err != nil { return models.Product{}, err } return item, nil } } return models.Product{}, fmt.Errorf("product not found") } func (s *JSONStore) IncrementView(id, fingerprint string) (models.Product, bool, error) { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return models.Product{}, false, err } now := time.Now() s.cleanupRecentViews(now) key := buildViewKey(id, fingerprint) if lastViewedAt, ok := s.recentViews[key]; ok && now.Sub(lastViewedAt) < viewCooldown { for _, item := range items { if item.ID == id { return item, false, nil } } return models.Product{}, false, fmt.Errorf("product not found") } for i, item := range items { if item.ID == id { item.ViewCount++ item.UpdatedAt = now items[i] = item s.recentViews[key] = now if err := s.writeAll(items); err != nil { return models.Product{}, false, err } return item, true, nil } } return models.Product{}, false, fmt.Errorf("product not found") } func (s *JSONStore) Delete(id string) error { s.mu.Lock() defer s.mu.Unlock() items, err := s.readAll() if err != nil { return err } filtered := make([]models.Product, 0, len(items)) for _, item := range items { if item.ID != id { filtered = append(filtered, item) } } if err := s.writeAll(filtered); err != nil { return err } return nil } func (s *JSONStore) readAll() ([]models.Product, error) { bytes, err := os.ReadFile(s.path) if err != nil { return nil, fmt.Errorf("read products: %w", err) } var items []models.Product if err := json.Unmarshal(bytes, &items); err != nil { return nil, fmt.Errorf("parse products: %w", err) } for i, item := range items { items[i] = normalizeProduct(item) } return items, nil } func (s *JSONStore) writeAll(items []models.Product) error { for i, item := range items { items[i] = normalizeProduct(item) } bytes, err := json.MarshalIndent(items, "", " ") if err != nil { return fmt.Errorf("encode products: %w", err) } if err := os.WriteFile(s.path, bytes, 0o644); err != nil { return fmt.Errorf("write products: %w", err) } return nil } func normalizeProduct(item models.Product) models.Product { item.CoverURL = strings.TrimSpace(item.CoverURL) if item.CoverURL == "" { item.CoverURL = defaultCoverURL } if item.Tags == nil { item.Tags = []string{} } item.Tags = sanitizeTags(item.Tags) if item.ScreenshotURLs == nil { item.ScreenshotURLs = []string{} } if len(item.ScreenshotURLs) > maxScreenshotURLs { item.ScreenshotURLs = item.ScreenshotURLs[:maxScreenshotURLs] } if item.Codes == nil { item.Codes = []string{} } if item.DiscountPrice <= 0 || item.DiscountPrice >= item.Price { item.DiscountPrice = 0 } item.VerificationURL = strings.TrimSpace(item.VerificationURL) item.Codes = sanitizeCodes(item.Codes) item.Quantity = len(item.Codes) return item } func sanitizeCodes(codes []string) []string { clean := make([]string, 0, len(codes)) seen := map[string]bool{} for _, code := range codes { trimmed := strings.TrimSpace(code) if trimmed == "" { continue } if seen[trimmed] { continue } seen[trimmed] = true clean = append(clean, trimmed) } return clean } func sanitizeTags(tags []string) []string { clean := make([]string, 0, len(tags)) seen := map[string]bool{} for _, tag := range tags { t := strings.TrimSpace(tag) if t == "" { continue } key := strings.ToLower(t) if seen[key] { continue } seen[key] = true clean = append(clean, t) if len(clean) >= 20 { break } } return clean } func buildViewKey(id, fingerprint string) string { sum := sha256.Sum256([]byte(id + "|" + fingerprint)) return fmt.Sprintf("%x", sum) } func (s *JSONStore) cleanupRecentViews(now time.Time) { for key, lastViewedAt := range s.recentViews { if now.Sub(lastViewedAt) >= viewCooldown { delete(s.recentViews, key) } } }