refactor: 重构项目结构,迁移后端至 mengyastore-backend-go,新增 Java 后端、前端功能更新及部署文档

This commit is contained in:
2026-03-27 15:10:53 +08:00
parent 84874707f5
commit 63781358b2
71 changed files with 2123 additions and 250 deletions

View File

@@ -0,0 +1,99 @@
package storage
import (
"sync"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"mengyastore-backend/internal/database"
"mengyastore-backend/internal/models"
)
type ChatStore struct {
db *gorm.DB
mu sync.Mutex
lastSent map[string]time.Time
}
func NewChatStore(db *gorm.DB) (*ChatStore, error) {
return &ChatStore{db: db, lastSent: make(map[string]time.Time)}, nil
}
func chatRowToModel(row database.ChatMessageRow) models.ChatMessage {
return models.ChatMessage{
ID: row.ID,
AccountID: row.AccountID,
AccountName: row.AccountName,
Content: row.Content,
SentAt: row.SentAt,
FromAdmin: row.FromAdmin,
}
}
func (s *ChatStore) GetMessages(accountID string) ([]models.ChatMessage, error) {
var rows []database.ChatMessageRow
if err := s.db.Where("account_id = ?", accountID).Order("sent_at ASC").Find(&rows).Error; err != nil {
return nil, err
}
msgs := make([]models.ChatMessage, len(rows))
for i, r := range rows {
msgs[i] = chatRowToModel(r)
}
return msgs, nil
}
func (s *ChatStore) ListConversations() (map[string][]models.ChatMessage, error) {
var rows []database.ChatMessageRow
if err := s.db.Order("account_id, sent_at ASC").Find(&rows).Error; err != nil {
return nil, err
}
result := make(map[string][]models.ChatMessage)
for _, r := range rows {
result[r.AccountID] = append(result[r.AccountID], chatRowToModel(r))
}
return result, nil
}
func (s *ChatStore) SendUserMessage(accountID, accountName, content string) (models.ChatMessage, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if last, ok := s.lastSent[accountID]; ok && time.Since(last) < time.Second {
return models.ChatMessage{}, true, nil
}
s.lastSent[accountID] = time.Now()
row := database.ChatMessageRow{
ID: uuid.New().String(),
AccountID: accountID,
AccountName: accountName,
Content: content,
SentAt: time.Now(),
FromAdmin: false,
}
if err := s.db.Create(&row).Error; err != nil {
return models.ChatMessage{}, false, err
}
return chatRowToModel(row), false, nil
}
func (s *ChatStore) SendAdminMessage(accountID, content string) (models.ChatMessage, error) {
row := database.ChatMessageRow{
ID: uuid.New().String(),
AccountID: accountID,
AccountName: "管理员",
Content: content,
SentAt: time.Now(),
FromAdmin: true,
}
if err := s.db.Create(&row).Error; err != nil {
return models.ChatMessage{}, err
}
return chatRowToModel(row), nil
}
func (s *ChatStore) ClearConversation(accountID string) error {
return s.db.Where("account_id = ?", accountID).Delete(&database.ChatMessageRow{}).Error
}

View File

@@ -0,0 +1,140 @@
package storage
import (
"fmt"
"github.com/google/uuid"
"gorm.io/gorm"
"mengyastore-backend/internal/database"
"mengyastore-backend/internal/models"
)
type OrderStore struct {
db *gorm.DB
}
func NewOrderStore(db *gorm.DB) (*OrderStore, error) {
return &OrderStore{db: db}, nil
}
func orderRowToModel(row database.OrderRow) models.Order {
return models.Order{
ID: row.ID,
ProductID: row.ProductID,
ProductName: row.ProductName,
UserAccount: row.UserAccount,
UserName: row.UserName,
Quantity: row.Quantity,
DeliveredCodes: row.DeliveredCodes,
Status: row.Status,
DeliveryMode: row.DeliveryMode,
Note: row.Note,
ContactPhone: row.ContactPhone,
ContactEmail: row.ContactEmail,
NotifyEmail: row.NotifyEmail,
CreatedAt: row.CreatedAt,
}
}
func (s *OrderStore) Create(order models.Order) (models.Order, error) {
if order.ID == "" {
order.ID = uuid.NewString()
}
if len(order.DeliveredCodes) == 0 {
order.DeliveredCodes = []string{}
}
row := database.OrderRow{
ID: order.ID,
ProductID: order.ProductID,
ProductName: order.ProductName,
UserAccount: order.UserAccount,
UserName: order.UserName,
Quantity: order.Quantity,
DeliveredCodes: database.StringSlice(order.DeliveredCodes),
Status: order.Status,
DeliveryMode: order.DeliveryMode,
Note: order.Note,
ContactPhone: order.ContactPhone,
ContactEmail: order.ContactEmail,
NotifyEmail: order.NotifyEmail,
}
if err := s.db.Create(&row).Error; err != nil {
return models.Order{}, err
}
order.CreatedAt = row.CreatedAt
return order, nil
}
func (s *OrderStore) GetByID(id string) (models.Order, error) {
var row database.OrderRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Order{}, fmt.Errorf("order not found")
}
return orderRowToModel(row), nil
}
func (s *OrderStore) Confirm(id string) (models.Order, error) {
var row database.OrderRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Order{}, fmt.Errorf("order not found")
}
if err := s.db.Model(&row).Update("status", "completed").Error; err != nil {
return models.Order{}, err
}
row.Status = "completed"
return orderRowToModel(row), nil
}
func (s *OrderStore) ListByAccount(account string) ([]models.Order, error) {
var rows []database.OrderRow
if err := s.db.Where("user_account = ?", account).Order("created_at DESC").Find(&rows).Error; err != nil {
return nil, err
}
orders := make([]models.Order, len(rows))
for i, r := range rows {
orders[i] = orderRowToModel(r)
}
return orders, nil
}
func (s *OrderStore) ListAll() ([]models.Order, error) {
var rows []database.OrderRow
if err := s.db.Order("created_at DESC").Find(&rows).Error; err != nil {
return nil, err
}
orders := make([]models.Order, len(rows))
for i, r := range rows {
orders[i] = orderRowToModel(r)
}
return orders, nil
}
func (s *OrderStore) CountPurchasedByAccount(account, productID string) (int, error) {
var total int64
// 统计 pending手动待发货和 completed 两种状态,防止用户快速下单绕过购买数量限制。
err := s.db.Model(&database.OrderRow{}).
Where("user_account = ? AND product_id = ? AND status IN ?", account, productID, []string{"pending", "completed"}).
Select("COALESCE(SUM(quantity), 0)").Scan(&total).Error
return int(total), err
}
// Count 返回所有订单的总数量。
func (s *OrderStore) Count() (int, error) {
var count int64
if err := s.db.Model(&database.OrderRow{}).Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}
// Delete 根据 ID 删除单条订单。
func (s *OrderStore) Delete(id string) error {
return s.db.Delete(&database.OrderRow{}, "id = ?", id).Error
}
// UpdateCodes 更新订单的已发货卡密列表。
func (s *OrderStore) UpdateCodes(id string, codes []string) error {
return s.db.Model(&database.OrderRow{}).Where("id = ?", id).
Update("delivered_codes", database.StringSlice(codes)).Error
}

View File

@@ -0,0 +1,328 @@
package storage
import (
"crypto/sha256"
"fmt"
"strings"
"sync"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"mengyastore-backend/internal/database"
"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 ProductStore struct {
db *gorm.DB
mu sync.Mutex
recentViews map[string]time.Time
}
func NewProductStore(db *gorm.DB) (*ProductStore, error) {
return &ProductStore{
db: db,
recentViews: make(map[string]time.Time),
}, nil
}
// rowToModel 将数据库行(含卡密)转换为业务模型。
func rowToModel(row database.ProductRow, codes []string) models.Product {
return models.Product{
ID: row.ID,
Name: row.Name,
Price: row.Price,
DiscountPrice: row.DiscountPrice,
Tags: row.Tags,
CoverURL: row.CoverURL,
ScreenshotURLs: row.ScreenshotURLs,
VerificationURL: row.VerificationURL,
Description: row.Description,
Active: row.Active,
RequireLogin: row.RequireLogin,
MaxPerAccount: row.MaxPerAccount,
TotalSold: row.TotalSold,
ViewCount: row.ViewCount,
DeliveryMode: row.DeliveryMode,
ShowNote: row.ShowNote,
ShowContact: row.ShowContact,
Codes: codes,
Quantity: len(codes),
CreatedAt: row.CreatedAt,
}
}
func (s *ProductStore) loadCodes(productID string) ([]string, error) {
var rows []database.ProductCodeRow
if err := s.db.Where("product_id = ?", productID).Find(&rows).Error; err != nil {
return nil, err
}
codes := make([]string, len(rows))
for i, r := range rows {
codes[i] = r.Code
}
return codes, nil
}
func (s *ProductStore) replaceCodes(productID string, codes []string) error {
if err := s.db.Where("product_id = ?", productID).Delete(&database.ProductCodeRow{}).Error; err != nil {
return err
}
if len(codes) == 0 {
return nil
}
rows := make([]database.ProductCodeRow, 0, len(codes))
for _, code := range codes {
rows = append(rows, database.ProductCodeRow{ProductID: productID, Code: code})
}
return s.db.CreateInBatches(rows, 100).Error
}
func (s *ProductStore) ListAll() ([]models.Product, error) {
var rows []database.ProductRow
if err := s.db.Order("created_at DESC").Find(&rows).Error; err != nil {
return nil, err
}
products := make([]models.Product, 0, len(rows))
for _, row := range rows {
codes, _ := s.loadCodes(row.ID)
products = append(products, rowToModel(row, codes))
}
return products, nil
}
func (s *ProductStore) ListActive() ([]models.Product, error) {
var rows []database.ProductRow
if err := s.db.Where("active = ?", true).Order("created_at DESC").Find(&rows).Error; err != nil {
return nil, err
}
products := make([]models.Product, 0, len(rows))
for _, row := range rows {
// 公开接口不暴露卡密,但需要统计剩余库存数量
var count int64
s.db.Model(&database.ProductCodeRow{}).Where("product_id = ?", row.ID).Count(&count)
row.Active = true
p := rowToModel(row, nil)
p.Quantity = int(count)
p.Codes = nil
products = append(products, p)
}
return products, nil
}
func (s *ProductStore) GetByID(id string) (models.Product, error) {
var row database.ProductRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Product{}, fmt.Errorf("product not found")
}
codes, _ := s.loadCodes(id)
return rowToModel(row, codes), nil
}
func (s *ProductStore) Create(p models.Product) (models.Product, error) {
p = normalizeProduct(p)
p.ID = uuid.NewString()
now := time.Now()
p.CreatedAt = now
row := database.ProductRow{
ID: p.ID,
Name: p.Name,
Price: p.Price,
DiscountPrice: p.DiscountPrice,
Tags: database.StringSlice(p.Tags),
CoverURL: p.CoverURL,
ScreenshotURLs: database.StringSlice(p.ScreenshotURLs),
VerificationURL: p.VerificationURL,
Description: p.Description,
Active: p.Active,
RequireLogin: p.RequireLogin,
MaxPerAccount: p.MaxPerAccount,
TotalSold: p.TotalSold,
ViewCount: p.ViewCount,
DeliveryMode: p.DeliveryMode,
ShowNote: p.ShowNote,
ShowContact: p.ShowContact,
CreatedAt: now,
}
if err := s.db.Create(&row).Error; err != nil {
return models.Product{}, err
}
if err := s.replaceCodes(p.ID, p.Codes); err != nil {
return models.Product{}, err
}
p.Quantity = len(p.Codes)
return p, nil
}
func (s *ProductStore) Update(id string, patch models.Product) (models.Product, error) {
var row database.ProductRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Product{}, fmt.Errorf("product not found")
}
normalized := normalizeProduct(patch)
if err := s.db.Model(&row).Updates(map[string]interface{}{
"name": normalized.Name,
"price": normalized.Price,
"discount_price": normalized.DiscountPrice,
"tags": database.StringSlice(normalized.Tags),
"cover_url": normalized.CoverURL,
"screenshot_urls": database.StringSlice(normalized.ScreenshotURLs),
"verification_url": normalized.VerificationURL,
"description": normalized.Description,
"active": normalized.Active,
"require_login": normalized.RequireLogin,
"max_per_account": normalized.MaxPerAccount,
"delivery_mode": normalized.DeliveryMode,
"show_note": normalized.ShowNote,
"show_contact": normalized.ShowContact,
}).Error; err != nil {
return models.Product{}, err
}
if err := s.replaceCodes(id, normalized.Codes); err != nil {
return models.Product{}, err
}
var updated database.ProductRow
s.db.First(&updated, "id = ?", id)
codes, _ := s.loadCodes(id)
return rowToModel(updated, codes), nil
}
func (s *ProductStore) Toggle(id string, active bool) (models.Product, error) {
if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id).Update("active", active).Error; err != nil {
return models.Product{}, err
}
var row database.ProductRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Product{}, fmt.Errorf("product not found")
}
codes, _ := s.loadCodes(id)
return rowToModel(row, codes), nil
}
func (s *ProductStore) IncrementSold(id string, count int) error {
return s.db.Model(&database.ProductRow{}).Where("id = ?", id).
UpdateColumn("total_sold", gorm.Expr("total_sold + ?", count)).Error
}
func (s *ProductStore) IncrementView(id, fingerprint string) (models.Product, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
s.cleanupRecentViews(now)
key := buildViewKey(id, fingerprint)
if lastViewedAt, ok := s.recentViews[key]; ok && now.Sub(lastViewedAt) < viewCooldown {
var row database.ProductRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Product{}, false, fmt.Errorf("product not found")
}
return rowToModel(row, nil), false, nil
}
if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id).
UpdateColumn("view_count", gorm.Expr("view_count + 1")).Error; err != nil {
return models.Product{}, false, err
}
s.recentViews[key] = now
var row database.ProductRow
if err := s.db.First(&row, "id = ?", id).Error; err != nil {
return models.Product{}, false, fmt.Errorf("product not found")
}
return rowToModel(row, nil), true, nil
}
func (s *ProductStore) Delete(id string) error {
if err := s.db.Where("product_id = ?", id).Delete(&database.ProductCodeRow{}).Error; err != nil {
return err
}
return s.db.Delete(&database.ProductRow{}, "id = ?", id).Error
}
// normalizeProduct 对商品字段进行规范化处理(清理空白、设默认值等)。
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)
if item.DeliveryMode == "" {
item.DeliveryMode = "auto"
}
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 == "" || 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 *ProductStore) cleanupRecentViews(now time.Time) {
for key, lastViewedAt := range s.recentViews {
if now.Sub(lastViewedAt) >= viewCooldown {
delete(s.recentViews, key)
}
}
}

View File

@@ -0,0 +1,146 @@
package storage
import (
"strconv"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"mengyastore-backend/internal/database"
)
type SiteStore struct {
db *gorm.DB
}
func NewSiteStore(db *gorm.DB) (*SiteStore, error) {
return &SiteStore{db: db}, nil
}
func (s *SiteStore) get(key string) (string, error) {
var row database.SiteSettingRow
// `key` 是 MySQL 保留字,需用反引号转义以避免 SQL 语法错误。
if err := s.db.Where("`key` = ?", key).First(&row).Error; err != nil {
return "", nil // 键不存在时返回零值
}
return row.Value, 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) {
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
}
current++
if err := s.set("totalVisits", strconv.Itoa(current)); err != nil {
return 0, err
}
return current, nil
}
func (s *SiteStore) GetMaintenance() (enabled bool, reason string, err error) {
v, err := s.get("maintenance")
if err != nil {
return false, "", err
}
enabled = v == "true"
reason, err = s.get("maintenanceReason")
return enabled, reason, err
}
func (s *SiteStore) SetMaintenance(enabled bool, reason string) error {
v := "false"
if enabled {
v = "true"
}
if err := s.set("maintenance", v); err != nil {
return err
}
return s.set("maintenanceReason", reason)
}
// RecordVisit 递增访问计数,返回 (总访问量, 是否计入, 错误)。
// 去重逻辑由上层 handler 的内存指纹完成,此处无条件累加。
func (s *SiteStore) RecordVisit(_ string) (int, bool, error) {
total, err := s.IncrementVisits()
return total, true, err
}
// SMTPConfig 存储数据库中的发件 SMTP 配置。
type SMTPConfig struct {
Enabled bool `json:"enabled"`
Email string `json:"email"`
Password string `json:"password"`
FromName string `json:"fromName"`
Host string `json:"host"`
Port string `json:"port"`
}
// IsConfiguredEmail 判断邮件通知是否已启用且 SMTP 配置完整。
func (c SMTPConfig) IsConfiguredEmail() bool {
return c.Enabled && c.Email != "" && c.Password != "" && c.Host != ""
}
func (s *SiteStore) GetSMTPConfig() (SMTPConfig, error) {
cfg := SMTPConfig{
Enabled: true, // 默认启用
Host: "smtp.qq.com",
Port: "465",
}
if v, _ := s.get("smtpEnabled"); v == "false" {
cfg.Enabled = false
}
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 {
enabledVal := "true"
if !cfg.Enabled {
enabledVal = "false"
}
pairs := [][2]string{
{"smtpEnabled", enabledVal},
{"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
}
}
return nil
}

View File

@@ -0,0 +1,38 @@
package storage
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"mengyastore-backend/internal/database"
)
type WishlistStore struct {
db *gorm.DB
}
func NewWishlistStore(db *gorm.DB) (*WishlistStore, error) {
return &WishlistStore{db: db}, nil
}
func (s *WishlistStore) Get(accountID string) ([]string, error) {
var rows []database.WishlistRow
if err := s.db.Where("account_id = ?", accountID).Find(&rows).Error; err != nil {
return nil, err
}
ids := make([]string, len(rows))
for i, r := range rows {
ids[i] = r.ProductID
}
return ids, nil
}
func (s *WishlistStore) Add(accountID, productID string) error {
return s.db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&database.WishlistRow{AccountID: accountID, ProductID: productID}).Error
}
func (s *WishlistStore) Remove(accountID, productID string) error {
return s.db.Where("account_id = ? AND product_id = ?", accountID, productID).
Delete(&database.WishlistRow{}).Error
}