feat: sync project

This commit is contained in:
2026-03-20 20:58:24 +08:00
parent 04bb11dfff
commit 9a6ebe80c5
32 changed files with 3613 additions and 156 deletions

View File

@@ -1,6 +1,7 @@
package storage
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
@@ -15,20 +16,26 @@ import (
)
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
path string
mu sync.Mutex
recentViews map[string]time.Time
}
func NewJSONStore(path string) (*JSONStore, error) {
if err := ensureFile(path); err != nil {
if err := ensureProductsFile(path); err != nil {
return nil, err
}
return &JSONStore{path: path}, nil
return &JSONStore{
path: path,
recentViews: make(map[string]time.Time),
}, nil
}
func ensureFile(path string) error {
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)
@@ -72,6 +79,22 @@ func (s *JSONStore) ListActive() ([]models.Product, error) {
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()
@@ -100,13 +123,18 @@ func (s *JSONStore) Update(id string, patch models.Product) (models.Product, err
}
for i, item := range items {
if item.ID == id {
item.Name = patch.Name
item.Price = patch.Price
item.Quantity = patch.Quantity
item.CoverURL = patch.CoverURL
item.ScreenshotURLs = normalizeProduct(patch).ScreenshotURLs
item.Description = patch.Description
item.Active = patch.Active
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 {
@@ -139,6 +167,43 @@ func (s *JSONStore) Toggle(id string, active bool) (models.Product, error) {
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()
@@ -192,8 +257,75 @@ func normalizeProduct(item models.Product) models.Product {
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)
}
}
}

View File

@@ -0,0 +1,140 @@
package storage
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/google/uuid"
"mengyastore-backend/internal/models"
)
type OrderStore struct {
path string
mu sync.Mutex
}
func NewOrderStore(path string) (*OrderStore, error) {
if err := ensureOrdersFile(path); err != nil {
return nil, err
}
return &OrderStore{path: path}, nil
}
func (s *OrderStore) Count() (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
items, err := s.readAll()
if err != nil {
return 0, err
}
return len(items), nil
}
func (s *OrderStore) ListByAccount(account string) ([]models.Order, error) {
s.mu.Lock()
defer s.mu.Unlock()
items, err := s.readAll()
if err != nil {
return nil, err
}
matched := make([]models.Order, 0)
for i := len(items) - 1; i >= 0; i-- {
if items[i].UserAccount == account {
matched = append(matched, items[i])
}
}
return matched, nil
}
func (s *OrderStore) Confirm(id string) (models.Order, error) {
s.mu.Lock()
defer s.mu.Unlock()
items, err := s.readAll()
if err != nil {
return models.Order{}, err
}
for i, item := range items {
if item.ID == id {
if item.Status == "completed" {
return item, nil
}
items[i].Status = "completed"
if err := s.writeAll(items); err != nil {
return models.Order{}, err
}
return items[i], nil
}
}
return models.Order{}, fmt.Errorf("order not found")
}
func (s *OrderStore) Create(order models.Order) (models.Order, error) {
s.mu.Lock()
defer s.mu.Unlock()
items, err := s.readAll()
if err != nil {
return models.Order{}, err
}
order.ID = uuid.NewString()
order.CreatedAt = time.Now()
items = append(items, order)
if err := s.writeAll(items); err != nil {
return models.Order{}, err
}
return order, nil
}
func (s *OrderStore) readAll() ([]models.Order, error) {
bytes, err := os.ReadFile(s.path)
if err != nil {
return nil, fmt.Errorf("read orders: %w", err)
}
var items []models.Order
if err := json.Unmarshal(bytes, &items); err != nil {
return nil, fmt.Errorf("parse orders: %w", err)
}
return items, nil
}
func (s *OrderStore) writeAll(items []models.Order) error {
bytes, err := json.MarshalIndent(items, "", " ")
if err != nil {
return fmt.Errorf("encode orders: %w", err)
}
if err := os.WriteFile(s.path, bytes, 0o644); err != nil {
return fmt.Errorf("write orders: %w", err)
}
return nil
}
func ensureOrdersFile(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.Order{}
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
}

View File

@@ -0,0 +1,128 @@
package storage
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
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
}
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 (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
}
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) GetTotalVisits() (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
data, err := s.read()
if err != nil {
return 0, err
}
return data.TotalVisits, nil
}
func (s *SiteStore) read() (siteData, error) {
bytes, err := os.ReadFile(s.path)
if err != nil {
return siteData{}, fmt.Errorf("read site data: %w", err)
}
var data siteData
if err := json.Unmarshal(bytes, &data); err != nil {
return siteData{}, fmt.Errorf("parse site data: %w", err)
}
return data, nil
}
func (s *SiteStore) write(data siteData) error {
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("encode site data: %w", err)
}
if err := os.WriteFile(s.path, bytes, 0o644); err != nil {
return fmt.Errorf("write site data: %w", err)
}
return nil
}
func (s *SiteStore) cleanupRecentVisits(now time.Time) {
for key, last := range s.recentVisits {
if now.Sub(last) >= visitCooldown {
delete(s.recentVisits, key)
}
}
}
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
}