spark-server/internal/module/client/model/repo.go
DESKTOP-L8OUL9F\gfl0034 53426af122 feat: 多广场 channel、音乐公开 API 与订阅 package_name
支持 square/squarenovel/squaresteps/squarehealth 实例与 category 筛选;音乐增加免登录 grouped/track;订阅套餐按 package_name 区分;补 migration 0030/0031 与 OpenAPI。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 09:02:28 +08:00

375 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"context"
"errors"
"strings"
"time"
"gorm.io/gorm"
"spark-server/internal/module/client/entity"
)
type Repo struct {
db *gorm.DB
}
func NewRepo(db *gorm.DB) *Repo {
if db == nil {
return nil
}
return &Repo{db: db}
}
type legalRow struct {
DocKey string `gorm:"column:doc_key;primaryKey"`
Lang string `gorm:"column:lang;primaryKey"`
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
Version string `gorm:"column:version"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (legalRow) TableName() string { return "t_legal_documents" }
type feedbackRow struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserID *int64 `gorm:"column:user_id"`
AuthType string `gorm:"column:auth_type"`
Email string `gorm:"column:email"`
Contact string `gorm:"column:contact"`
Content string `gorm:"column:content"`
AppVersion string `gorm:"column:app_version"`
Language string `gorm:"column:language"`
Status string `gorm:"column:status"`
CreatedAt time.Time `gorm:"column:created_at"`
}
func (feedbackRow) TableName() string { return "t_feedback" }
type planRow struct {
PlanID string `gorm:"column:plan_id;primaryKey"`
Lang string `gorm:"column:lang;primaryKey"`
PackageName string `gorm:"column:package_name;primaryKey"`
Title string `gorm:"column:title"`
Subtitle string `gorm:"column:subtitle"`
PriceDisplay string `gorm:"column:price_display"`
PriceCents int `gorm:"column:price_cents"`
Currency string `gorm:"column:currency"`
PeriodDays int `gorm:"column:period_days"`
Badge string `gorm:"column:badge"`
SortOrder int `gorm:"column:sort_order"`
Enabled bool `gorm:"column:enabled"`
ProductID string `gorm:"column:product_id"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (planRow) TableName() string { return "t_subscription_plans" }
type passwordRow struct {
UserID int64 `gorm:"column:user_id;primaryKey"`
PasswordHash string `gorm:"column:password_hash"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (passwordRow) TableName() string { return "t_user_passwords" }
func (r *Repo) GetLegal(ctx context.Context, lang string) (privacy, terms *entity.LegalDocument, err error) {
var rows []legalRow
if err = r.db.WithContext(ctx).Where("lang = ?", lang).Find(&rows).Error; err != nil {
return nil, nil, err
}
for i := range rows {
d := &entity.LegalDocument{
Key: rows[i].DocKey, Lang: rows[i].Lang, Title: rows[i].Title,
Content: rows[i].Content, Version: rows[i].Version,
}
switch rows[i].DocKey {
case entity.DocPrivacy:
privacy = d
case entity.DocTerms:
terms = d
}
}
return privacy, terms, nil
}
func (r *Repo) UpsertLegal(ctx context.Context, d *entity.LegalDocument) error {
if d == nil {
return errors.New("client: nil legal document")
}
row := legalRow{
DocKey: d.Key, Lang: d.Lang, Title: d.Title, Content: d.Content,
Version: d.Version, UpdatedAt: time.Now(),
}
if row.Version == "" {
row.Version = "1.0"
}
return r.db.WithContext(ctx).Save(&row).Error
}
func (r *Repo) InsertFeedback(ctx context.Context, f *entity.Feedback) error {
status := f.Status
if status == "" {
status = "open"
}
authType := f.AuthType
if authType == "" {
authType = entity.FeedbackAuthGuest
}
row := feedbackRow{
UserID: f.UserID, AuthType: authType, Email: f.Email, Contact: f.Contact,
Content: f.Content, AppVersion: f.AppVersion, Language: f.Language,
Status: status, CreatedAt: f.CreatedAt,
}
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
return err
}
f.ID = row.ID
f.Status = status
f.AuthType = authType
return nil
}
func (r *Repo) ListFeedback(ctx context.Context, page, size int, authType string) ([]entity.Feedback, int64, error) {
if page < 0 {
page = 0
}
if size <= 0 || size > 200 {
size = 50
}
q := r.db.WithContext(ctx).Model(&feedbackRow{})
if authType == entity.FeedbackAuthLogin || authType == entity.FeedbackAuthGuest {
q = q.Where("auth_type = ?", authType)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []feedbackRow
listQ := r.db.WithContext(ctx).Order("id DESC").Offset(page * size).Limit(size)
if authType == entity.FeedbackAuthLogin || authType == entity.FeedbackAuthGuest {
listQ = listQ.Where("auth_type = ?", authType)
}
err := listQ.Find(&rows).Error
if err != nil {
return nil, 0, err
}
out := make([]entity.Feedback, 0, len(rows))
for i := range rows {
row := &rows[i]
out = append(out, entity.Feedback{
ID: row.ID, UserID: row.UserID, AuthType: row.AuthType, Email: row.Email,
Contact: row.Contact, Content: row.Content, AppVersion: row.AppVersion,
Language: row.Language, Status: row.Status, CreatedAt: row.CreatedAt,
})
}
return out, total, nil
}
func (r *Repo) UpdateFeedbackStatus(ctx context.Context, id int64, status string) error {
return r.db.WithContext(ctx).Model(&feedbackRow{}).Where("id = ?", id).Update("status", status).Error
}
func (r *Repo) ListPlans(ctx context.Context, lang, packageName string) ([]entity.SubscriptionPlan, error) {
var rows []planRow
err := r.db.WithContext(ctx).
Where("lang = ? AND enabled = 1", lang).
Order("sort_order ASC, plan_id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
return filterPlansByPackage(mapPlans(rows), packageName), nil
}
func (r *Repo) ListPlansAdmin(ctx context.Context, lang, packageName string) ([]entity.SubscriptionPlan, error) {
q := r.db.WithContext(ctx).Order("package_name ASC, lang ASC, sort_order ASC, plan_id ASC")
if lang != "" {
q = q.Where("lang = ?", lang)
}
if packageName != "" {
q = q.Where("package_name = ?", packageName)
}
var rows []planRow
if err := q.Find(&rows).Error; err != nil {
return nil, err
}
return mapPlans(rows), nil
}
func mapPlans(rows []planRow) []entity.SubscriptionPlan {
out := make([]entity.SubscriptionPlan, 0, len(rows))
for i := range rows {
row := &rows[i]
out = append(out, entity.SubscriptionPlan{
PlanID: row.PlanID, Lang: row.Lang, PackageName: row.PackageName,
Title: row.Title, Subtitle: row.Subtitle,
PriceDisplay: row.PriceDisplay, PriceCents: row.PriceCents, Currency: row.Currency,
PeriodDays: row.PeriodDays, Badge: row.Badge, SortOrder: row.SortOrder,
Enabled: row.Enabled, ProductID: row.ProductID,
})
}
return out
}
// filterPlansByPackage:有精确包名则用之,否则回退 package_name 为空的默认档。
func filterPlansByPackage(items []entity.SubscriptionPlan, packageName string) []entity.SubscriptionPlan {
packageName = strings.TrimSpace(packageName)
if packageName == "" {
// 无包名:仅返回默认兜底,避免多包混在一起
out := make([]entity.SubscriptionPlan, 0, len(items))
for _, p := range items {
if strings.TrimSpace(p.PackageName) == "" {
out = append(out, p)
}
}
if len(out) > 0 {
return out
}
return items
}
want := strings.ToLower(packageName)
exact := make([]entity.SubscriptionPlan, 0)
fallback := make([]entity.SubscriptionPlan, 0)
for _, p := range items {
pkg := strings.TrimSpace(p.PackageName)
if pkg != "" && strings.ToLower(pkg) == want {
exact = append(exact, p)
} else if pkg == "" {
fallback = append(fallback, p)
}
}
if len(exact) > 0 {
return exact
}
return fallback
}
func (r *Repo) UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error {
if p == nil {
return errors.New("client: nil subscription plan")
}
row := planRow{
PlanID: p.PlanID, Lang: p.Lang, PackageName: strings.TrimSpace(p.PackageName),
Title: p.Title, Subtitle: p.Subtitle,
PriceDisplay: p.PriceDisplay, PriceCents: p.PriceCents, Currency: p.Currency,
PeriodDays: p.PeriodDays, Badge: p.Badge, SortOrder: p.SortOrder,
Enabled: p.Enabled, ProductID: p.ProductID, UpdatedAt: time.Now(),
}
if row.Currency == "" {
row.Currency = "USD"
}
return r.db.WithContext(ctx).Save(&row).Error
}
func (r *Repo) GetPasswordHash(ctx context.Context, userID int64) (string, error) {
var row passwordRow
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Take(&row).Error
if err == gorm.ErrRecordNotFound {
return "", nil
}
if err != nil {
return "", err
}
return row.PasswordHash, nil
}
func (r *Repo) SetPasswordHash(ctx context.Context, userID int64, hash string) error {
row := passwordRow{UserID: userID, PasswordHash: hash, UpdatedAt: time.Now()}
return r.db.WithContext(ctx).Save(&row).Error
}
type developerAppRow struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
SourceAppCode string `gorm:"column:source_app_code"`
TargetAppName string `gorm:"column:target_app_name"`
TargetPackageName string `gorm:"column:target_package_name"`
Subtitle string `gorm:"column:subtitle"`
IconURL string `gorm:"column:icon_url"`
StoreURL string `gorm:"column:store_url"`
FallbackURL string `gorm:"column:fallback_url"`
SchemaURL string `gorm:"column:schema_url"`
SortOrder int `gorm:"column:sort_order"`
Enabled bool `gorm:"column:enabled"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (developerAppRow) TableName() string { return "t_developer_apps" }
func (r *Repo) ListDeveloperApps(ctx context.Context, sourceAppCode string, enabledOnly bool) ([]entity.DeveloperApp, error) {
q := r.db.WithContext(ctx).Where("source_app_code = ?", sourceAppCode)
if enabledOnly {
q = q.Where("enabled = 1")
}
var rows []developerAppRow
if err := q.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
return nil, err
}
out := make([]entity.DeveloperApp, 0, len(rows))
for i := range rows {
out = append(out, mapDeveloperApp(&rows[i]))
}
return out, nil
}
func (r *Repo) UpsertDeveloperApp(ctx context.Context, a *entity.DeveloperApp) error {
now := time.Now()
var existing developerAppRow
err := r.db.WithContext(ctx).
Where("source_app_code = ? AND target_package_name = ?", a.SourceAppCode, a.TargetPackageName).
Take(&existing).Error
if err == nil {
existing.TargetAppName = a.TargetAppName
existing.Subtitle = a.Subtitle
if a.IconURL != "" {
existing.IconURL = a.IconURL
}
if a.StoreURL != "" {
existing.StoreURL = a.StoreURL
}
if a.FallbackURL != "" {
existing.FallbackURL = a.FallbackURL
}
if a.SchemaURL != "" {
existing.SchemaURL = a.SchemaURL
}
existing.SortOrder = a.SortOrder
existing.Enabled = a.Enabled
existing.UpdatedAt = now
if err := r.db.WithContext(ctx).Save(&existing).Error; err != nil {
return err
}
a.ID = existing.ID
return nil
}
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
row := developerAppRow{
SourceAppCode: a.SourceAppCode, TargetAppName: a.TargetAppName,
TargetPackageName: a.TargetPackageName, Subtitle: a.Subtitle, IconURL: a.IconURL,
StoreURL: a.StoreURL, FallbackURL: a.FallbackURL, SchemaURL: a.SchemaURL,
SortOrder: a.SortOrder, Enabled: a.Enabled, UpdatedAt: now,
}
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
return err
}
a.ID = row.ID
return nil
}
func (r *Repo) DeleteDeveloperApp(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Delete(&developerAppRow{}, id).Error
}
func mapDeveloperApp(row *developerAppRow) entity.DeveloperApp {
return entity.DeveloperApp{
ID: row.ID, SourceAppCode: row.SourceAppCode, TargetAppName: row.TargetAppName,
TargetPackageName: row.TargetPackageName, Subtitle: row.Subtitle, IconURL: row.IconURL,
StoreURL: row.StoreURL, FallbackURL: row.FallbackURL, SchemaURL: row.SchemaURL,
SortOrder: row.SortOrder, Enabled: row.Enabled,
}
}