272 lines
8.7 KiB
Go
272 lines
8.7 KiB
Go
package repository
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
type AppVersionListOptions struct {
|
||
AppCode string
|
||
Platform string
|
||
Keyword string
|
||
}
|
||
|
||
type AppExploreTabListOptions struct {
|
||
AppCode string
|
||
Keyword string
|
||
Enabled *bool
|
||
}
|
||
|
||
func (s *Store) ListAppConfigs(group string) ([]model.AppConfig, error) {
|
||
var items []model.AppConfig
|
||
// 旧的通用配置继续使用 app_code='' 基线行;H5 配置必须走 scoped API,避免覆盖行被其他旧调用方误读。
|
||
err := s.db.Where("app_code = '' AND `group` = ?", strings.TrimSpace(group)).Order("`key` ASC").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
func (s *Store) GetAppConfig(group string, key string) (model.AppConfig, error) {
|
||
var item model.AppConfig
|
||
err := s.db.Where("app_code = '' AND `group` = ? AND `key` = ?", strings.TrimSpace(group), strings.TrimSpace(key)).First(&item).Error
|
||
return item, err
|
||
}
|
||
|
||
func (s *Store) CreateAppConfig(item *model.AppConfig) error {
|
||
return s.db.Create(item).Error
|
||
}
|
||
|
||
func (s *Store) UpdateAppConfig(item *model.AppConfig) error {
|
||
return s.db.Save(item).Error
|
||
}
|
||
|
||
func (s *Store) DeleteAppConfig(group string, key string) error {
|
||
return s.db.Where("app_code = '' AND `group` = ? AND `key` = ?", strings.TrimSpace(group), strings.TrimSpace(key)).Delete(&model.AppConfig{}).Error
|
||
}
|
||
|
||
func (s *Store) UpsertAppConfigs(items []model.AppConfig) error {
|
||
if len(items) == 0 {
|
||
return nil
|
||
}
|
||
|
||
nowMS := time.Now().UTC().UnixMilli()
|
||
for index := range items {
|
||
items[index].AppCode = ""
|
||
items[index].Group = strings.TrimSpace(items[index].Group)
|
||
items[index].Key = strings.TrimSpace(items[index].Key)
|
||
items[index].IsDeleted = false
|
||
items[index].UpdatedAtMS = nowMS
|
||
if items[index].CreatedAtMS == 0 {
|
||
items[index].CreatedAtMS = nowMS
|
||
}
|
||
}
|
||
|
||
return s.db.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{
|
||
{Name: "app_code"},
|
||
{Name: "group"},
|
||
{Name: "key"},
|
||
},
|
||
DoUpdates: clause.AssignmentColumns([]string{"value", "description", "is_deleted", "updated_at_ms"}),
|
||
}).Create(&items).Error
|
||
}
|
||
|
||
// ListScopedAppConfigs 返回当前 App 的有效配置视图:scoped 行覆盖 legacy 基线,墓碑显式屏蔽基线。
|
||
// legacy 基线保持只读,因此迁移后未知的新 App 也能继续获得原配置,同时任意 App 的写操作不会污染其他 App。
|
||
func (s *Store) ListScopedAppConfigs(appCode string, group string) ([]model.AppConfig, error) {
|
||
appCode = normalizeScopedAppCode(appCode)
|
||
var rows []model.AppConfig
|
||
err := s.db.
|
||
Where("`group` = ? AND app_code IN ?", strings.TrimSpace(group), []string{"", appCode}).
|
||
Order("`key` ASC, app_code ASC").
|
||
Find(&rows).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return mergeScopedAppConfigs(rows, appCode), nil
|
||
}
|
||
|
||
// GetScopedAppConfig 和列表使用同一覆盖规则,防止更新/删除误操作其他 App 或 legacy 基线行。
|
||
func (s *Store) GetScopedAppConfig(appCode string, group string, key string) (model.AppConfig, error) {
|
||
appCode = normalizeScopedAppCode(appCode)
|
||
var rows []model.AppConfig
|
||
err := s.db.
|
||
Where("`group` = ? AND `key` = ? AND app_code IN ?", strings.TrimSpace(group), strings.TrimSpace(key), []string{"", appCode}).
|
||
Order("app_code ASC").
|
||
Find(&rows).Error
|
||
if err != nil {
|
||
return model.AppConfig{}, err
|
||
}
|
||
items := mergeScopedAppConfigs(rows, appCode)
|
||
if len(items) == 0 {
|
||
return model.AppConfig{}, gorm.ErrRecordNotFound
|
||
}
|
||
return items[0], nil
|
||
}
|
||
|
||
// UpsertScopedAppConfigs 只写当前 app_code;调用方无法通过请求体覆盖租户作用域。
|
||
func (s *Store) UpsertScopedAppConfigs(appCode string, items []model.AppConfig) error {
|
||
if len(items) == 0 {
|
||
return nil
|
||
}
|
||
appCode = normalizeScopedAppCode(appCode)
|
||
nowMS := time.Now().UTC().UnixMilli()
|
||
for index := range items {
|
||
items[index].AppCode = appCode
|
||
items[index].Group = strings.TrimSpace(items[index].Group)
|
||
items[index].Key = strings.TrimSpace(items[index].Key)
|
||
items[index].IsDeleted = false
|
||
items[index].UpdatedAtMS = nowMS
|
||
if items[index].CreatedAtMS == 0 {
|
||
items[index].CreatedAtMS = nowMS
|
||
}
|
||
}
|
||
return s.db.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "app_code"}, {Name: "group"}, {Name: "key"}},
|
||
DoUpdates: clause.AssignmentColumns([]string{"value", "description", "is_deleted", "updated_at_ms"}),
|
||
}).Create(&items).Error
|
||
}
|
||
|
||
// TombstoneScopedAppConfig 不物理删除 scoped 行。若配置来自 legacy 基线,墓碑会阻止它在删除后重新出现。
|
||
func (s *Store) TombstoneScopedAppConfig(appCode string, group string, key string) error {
|
||
appCode = normalizeScopedAppCode(appCode)
|
||
nowMS := time.Now().UTC().UnixMilli()
|
||
item := model.AppConfig{
|
||
AppCode: appCode,
|
||
Group: strings.TrimSpace(group),
|
||
Key: strings.TrimSpace(key),
|
||
IsDeleted: true,
|
||
CreatedAtMS: nowMS,
|
||
UpdatedAtMS: nowMS,
|
||
}
|
||
return s.db.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "app_code"}, {Name: "group"}, {Name: "key"}},
|
||
DoUpdates: clause.Assignments(map[string]any{
|
||
"is_deleted": true,
|
||
"updated_at_ms": nowMS,
|
||
}),
|
||
}).Create(&item).Error
|
||
}
|
||
|
||
func normalizeScopedAppCode(value string) string {
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|
||
|
||
func mergeScopedAppConfigs(rows []model.AppConfig, appCode string) []model.AppConfig {
|
||
appCode = normalizeScopedAppCode(appCode)
|
||
baseline := make(map[string]model.AppConfig)
|
||
overrides := make(map[string]model.AppConfig)
|
||
for _, item := range rows {
|
||
item.AppCode = normalizeScopedAppCode(item.AppCode)
|
||
item.Group = strings.TrimSpace(item.Group)
|
||
item.Key = strings.TrimSpace(item.Key)
|
||
if item.Key == "" {
|
||
continue
|
||
}
|
||
switch item.AppCode {
|
||
case "":
|
||
baseline[item.Key] = item
|
||
case appCode:
|
||
overrides[item.Key] = item
|
||
}
|
||
}
|
||
|
||
keys := make(map[string]struct{}, len(baseline)+len(overrides))
|
||
for key := range baseline {
|
||
keys[key] = struct{}{}
|
||
}
|
||
for key := range overrides {
|
||
keys[key] = struct{}{}
|
||
}
|
||
sortedKeys := make([]string, 0, len(keys))
|
||
for key := range keys {
|
||
sortedKeys = append(sortedKeys, key)
|
||
}
|
||
sort.Strings(sortedKeys)
|
||
|
||
items := make([]model.AppConfig, 0, len(sortedKeys))
|
||
for _, key := range sortedKeys {
|
||
item, overridden := overrides[key]
|
||
if !overridden {
|
||
item = baseline[key]
|
||
}
|
||
if item.IsDeleted {
|
||
continue
|
||
}
|
||
// 对外有效视图始终归属于当前 App;baseline 的空作用域不应泄漏给响应或后续写操作。
|
||
item.AppCode = appCode
|
||
items = append(items, item)
|
||
}
|
||
return items
|
||
}
|
||
|
||
func (s *Store) ListAppVersions(options AppVersionListOptions) ([]model.AppVersion, error) {
|
||
db := s.db.Where("app_code = ?", strings.TrimSpace(options.AppCode))
|
||
if platform := strings.TrimSpace(options.Platform); platform != "" {
|
||
db = db.Where("platform = ?", platform)
|
||
}
|
||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||
like := "%" + keyword + "%"
|
||
db = db.Where("version LIKE ? OR download_url LIKE ? OR description LIKE ?", like, like, like)
|
||
}
|
||
|
||
var items []model.AppVersion
|
||
err := db.Order("platform ASC, build_number DESC, id DESC").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
func (s *Store) GetAppVersion(appCode string, id uint) (model.AppVersion, error) {
|
||
var item model.AppVersion
|
||
err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&item).Error
|
||
return item, err
|
||
}
|
||
|
||
func (s *Store) CreateAppVersion(item *model.AppVersion) error {
|
||
return s.db.Create(item).Error
|
||
}
|
||
|
||
func (s *Store) UpdateAppVersion(item *model.AppVersion) error {
|
||
return s.db.Save(item).Error
|
||
}
|
||
|
||
func (s *Store) DeleteAppVersion(appCode string, id uint) error {
|
||
return s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).Delete(&model.AppVersion{}).Error
|
||
}
|
||
|
||
func (s *Store) ListAppExploreTabs(options AppExploreTabListOptions) ([]model.AppExploreTab, error) {
|
||
db := s.db.Where("app_code = ?", strings.TrimSpace(options.AppCode))
|
||
if options.Enabled != nil {
|
||
db = db.Where("enabled = ?", *options.Enabled)
|
||
}
|
||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||
like := "%" + keyword + "%"
|
||
db = db.Where("tab LIKE ? OR h5_url LIKE ?", like, like)
|
||
}
|
||
|
||
var items []model.AppExploreTab
|
||
err := db.Order("sort_order ASC, id DESC").Find(&items).Error
|
||
return items, err
|
||
}
|
||
|
||
func (s *Store) GetAppExploreTab(appCode string, id uint) (model.AppExploreTab, error) {
|
||
var item model.AppExploreTab
|
||
err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&item).Error
|
||
return item, err
|
||
}
|
||
|
||
func (s *Store) CreateAppExploreTab(item *model.AppExploreTab) error {
|
||
return s.db.Create(item).Error
|
||
}
|
||
|
||
func (s *Store) UpdateAppExploreTab(item *model.AppExploreTab) error {
|
||
return s.db.Save(item).Error
|
||
}
|
||
|
||
func (s *Store) DeleteAppExploreTab(appCode string, id uint) error {
|
||
return s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).Delete(&model.AppExploreTab{}).Error
|
||
}
|