377 lines
12 KiB
Go
377 lines
12 KiB
Go
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"`
|
||
Platform string `gorm:"column:platform;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, Platform: row.Platform,
|
||
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 i := range items {
|
||
if strings.TrimSpace(items[i].PackageName) == "" {
|
||
out = append(out, items[i])
|
||
}
|
||
}
|
||
if len(out) > 0 {
|
||
return out
|
||
}
|
||
return items
|
||
}
|
||
want := strings.ToLower(entity.CanonicalHealthPackage(packageName))
|
||
exact := make([]entity.SubscriptionPlan, 0)
|
||
fallback := make([]entity.SubscriptionPlan, 0)
|
||
for i := range items {
|
||
p := items[i]
|
||
pkg := entity.CanonicalHealthPackage(p.PackageName)
|
||
if pkg != "" && strings.EqualFold(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), Platform: p.Platform,
|
||
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,
|
||
}
|
||
}
|