2026-06-09 22:08:52 +08:00

875 lines
27 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 appconfig
import (
"errors"
"fmt"
"strings"
"time"
"unicode"
"unicode/utf8"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
)
const h5LinkGroup = "h5-links"
const (
bannerDisplayScopeHome = "home"
bannerDisplayScopeRoom = "room"
bannerDisplayScopeRecharge = "recharge"
bannerStatusActive = "active"
bannerStatusDisabled = "disabled"
bannerStatusExpired = "expired"
)
var bannerDisplayScopeOrder = []string{bannerDisplayScopeHome, bannerDisplayScopeRoom, bannerDisplayScopeRecharge}
type AppConfigService struct {
store *repository.Store
}
type H5Link struct {
Key string `json:"key"`
Label string `json:"label"`
URL string `json:"url"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppBanner struct {
ID uint `json:"id"`
AppCode string `json:"appCode"`
CoverURL string `json:"coverUrl"`
RoomSmallImageURL string `json:"roomSmallImageUrl"`
BannerType string `json:"bannerType"`
DisplayScope string `json:"displayScope"`
DisplayScopes []string `json:"displayScopes"`
Param string `json:"param"`
Status string `json:"status"`
Platform string `json:"platform"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
StartsAtMs int64 `json:"startsAtMs"`
EndsAtMs int64 `json:"endsAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppSplashScreen struct {
ID uint `json:"id"`
AppCode string `json:"appCode"`
CoverURL string `json:"coverUrl"`
SplashType string `json:"splashType"`
Param string `json:"param"`
Status string `json:"status"`
Platform string `json:"platform"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
StartsAtMs int64 `json:"startsAtMs"`
EndsAtMs int64 `json:"endsAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppVersion struct {
ID uint `json:"id"`
AppCode string `json:"appCode"`
Platform string `json:"platform"`
Version string `json:"version"`
BuildNumber int64 `json:"buildNumber"`
ForceUpdate bool `json:"forceUpdate"`
DownloadURL string `json:"downloadUrl"`
Description string `json:"description"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type ExploreTab struct {
ID uint `json:"id"`
AppCode string `json:"appCode"`
Tab string `json:"tab"`
H5URL string `json:"h5Url"`
Enabled bool `json:"enabled"`
SortOrder int `json:"sortOrder"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
func NewService(store *repository.Store) *AppConfigService {
return &AppConfigService{store: store}
}
func (s *AppConfigService) ListH5Links() ([]H5Link, error) {
configs, err := s.store.ListAppConfigs(h5LinkGroup)
if err != nil {
return nil, err
}
items := make([]H5Link, 0, len(configs))
for _, config := range configs {
items = append(items, h5LinkFromModel(config))
}
return items, nil
}
func (s *AppConfigService) UpdateH5Links(request updateH5LinksRequest) ([]H5Link, error) {
if len(request.Items) == 0 {
return nil, errors.New("items are required")
}
items := make([]model.AppConfig, 0, len(request.Items))
seen := make(map[string]struct{}, len(request.Items))
for _, item := range request.Items {
config, err := h5LinkModelFromPayload(item)
if err != nil {
return nil, err
}
key := config.Key
if _, ok := seen[key]; ok {
return nil, fmt.Errorf("duplicate h5 link key: %s", key)
}
seen[key] = struct{}{}
items = append(items, config)
}
if err := s.store.UpsertAppConfigs(items); err != nil {
return nil, err
}
return s.ListH5Links()
}
func (s *AppConfigService) CreateH5Link(req h5LinkPayload) (H5Link, error) {
item, err := h5LinkModelFromPayload(req)
if err != nil {
return H5Link{}, err
}
if err := s.store.CreateAppConfig(&item); err != nil {
return H5Link{}, err
}
return h5LinkFromModel(item), nil
}
func (s *AppConfigService) UpdateH5Link(key string, req h5LinkPayload) (H5Link, error) {
item, err := s.store.GetAppConfig(h5LinkGroup, key)
if err != nil {
return H5Link{}, err
}
req.Key = item.Key
updated, err := h5LinkModelFromPayload(req)
if err != nil {
return H5Link{}, err
}
item.Value = updated.Value
item.Description = updated.Description
if err := s.store.UpdateAppConfig(&item); err != nil {
return H5Link{}, err
}
return h5LinkFromModel(item), nil
}
func (s *AppConfigService) DeleteH5Link(key string) error {
return s.store.DeleteAppConfig(h5LinkGroup, key)
}
func (s *AppConfigService) ListBanners(appCode string, options repository.AppBannerListOptions) ([]AppBanner, error) {
options.AppCode = appctx.Normalize(appCode)
options.Status = normalizeBannerStatus(options.Status)
if strings.TrimSpace(options.DisplayScope) != "" {
options.DisplayScope = normalizeBannerDisplayScope(options.DisplayScope)
}
options.Platform = normalizeBannerPlatform(options.Platform)
options.Country = normalizeCountryCode(options.Country)
options.Keyword = strings.TrimSpace(options.Keyword)
if err := s.store.ExpireAppBanners(options.AppCode, time.Now().UTC().UnixMilli()); err != nil {
return nil, err
}
items, err := s.store.ListAppBanners(options)
if err != nil {
return nil, err
}
out := make([]AppBanner, 0, len(items))
for _, item := range items {
out = append(out, appBannerFromModel(item))
}
return out, nil
}
func (s *AppConfigService) CreateBanner(appCode string, req bannerRequest) (AppBanner, error) {
item, err := bannerModelFromRequest(appctx.Normalize(appCode), req)
if err != nil {
return AppBanner{}, err
}
if err := s.store.CreateAppBanner(&item); err != nil {
return AppBanner{}, err
}
return appBannerFromModel(item), nil
}
func (s *AppConfigService) UpdateBanner(appCode string, id uint, req bannerRequest) (AppBanner, error) {
item, err := s.store.GetAppBanner(appctx.Normalize(appCode), id)
if err != nil {
return AppBanner{}, err
}
updated, err := bannerModelFromRequest(item.AppCode, req)
if err != nil {
return AppBanner{}, err
}
item.CoverURL = updated.CoverURL
item.RoomSmallImageURL = updated.RoomSmallImageURL
item.BannerType = updated.BannerType
item.DisplayScope = updated.DisplayScope
item.Param = updated.Param
item.Status = updated.Status
item.Platform = updated.Platform
item.SortOrder = updated.SortOrder
item.RegionID = updated.RegionID
item.CountryCode = updated.CountryCode
item.Description = updated.Description
item.StartsAtMS = updated.StartsAtMS
item.EndsAtMS = updated.EndsAtMS
if err := s.store.UpdateAppBanner(&item); err != nil {
return AppBanner{}, err
}
return appBannerFromModel(item), nil
}
func (s *AppConfigService) DeleteBanner(appCode string, id uint) error {
return s.store.DeleteAppBanner(appctx.Normalize(appCode), id)
}
func (s *AppConfigService) ListSplashScreens(appCode string, options repository.AppSplashScreenListOptions) ([]AppSplashScreen, error) {
// 列表入口先把管理后台传入的筛选条件统一成数据库稳定值,再执行过期回收,避免已过期 active 数据继续展示。
options.AppCode = appctx.Normalize(appCode)
options.Status = normalizeBannerStatus(options.Status)
options.Platform = normalizeBannerPlatform(options.Platform)
options.Country = normalizeCountryCode(options.Country)
options.Keyword = strings.TrimSpace(options.Keyword)
if err := s.store.ExpireAppSplashScreens(options.AppCode, time.Now().UTC().UnixMilli()); err != nil {
return nil, err
}
items, err := s.store.ListAppSplashScreens(options)
if err != nil {
return nil, err
}
out := make([]AppSplashScreen, 0, len(items))
for _, item := range items {
out = append(out, appSplashScreenFromModel(item))
}
return out, nil
}
func (s *AppConfigService) CreateSplashScreen(appCode string, req splashScreenRequest) (AppSplashScreen, error) {
// 创建时只接受 service 归一化后的模型,保证 status、平台、国家码和时间窗口在入库前已经被校验。
item, err := splashScreenModelFromRequest(appctx.Normalize(appCode), req)
if err != nil {
return AppSplashScreen{}, err
}
if err := s.store.CreateAppSplashScreen(&item); err != nil {
return AppSplashScreen{}, err
}
return appSplashScreenFromModel(item), nil
}
func (s *AppConfigService) UpdateSplashScreen(appCode string, id uint, req splashScreenRequest) (AppSplashScreen, error) {
// 更新先按 app_code 和 id 锁定原记录,再只覆盖业务字段,避免请求体伪造 app_code 或创建时间。
item, err := s.store.GetAppSplashScreen(appctx.Normalize(appCode), id)
if err != nil {
return AppSplashScreen{}, err
}
updated, err := splashScreenModelFromRequest(item.AppCode, req)
if err != nil {
return AppSplashScreen{}, err
}
item.CoverURL = updated.CoverURL
item.SplashType = updated.SplashType
item.Param = updated.Param
item.Status = updated.Status
item.Platform = updated.Platform
item.SortOrder = updated.SortOrder
item.RegionID = updated.RegionID
item.CountryCode = updated.CountryCode
item.Description = updated.Description
item.StartsAtMS = updated.StartsAtMS
item.EndsAtMS = updated.EndsAtMS
if err := s.store.UpdateAppSplashScreen(&item); err != nil {
return AppSplashScreen{}, err
}
return appSplashScreenFromModel(item), nil
}
func (s *AppConfigService) DeleteSplashScreen(appCode string, id uint) error {
return s.store.DeleteAppSplashScreen(appctx.Normalize(appCode), id)
}
func (s *AppConfigService) ListAppVersions(appCode string, options repository.AppVersionListOptions) ([]AppVersion, error) {
options.AppCode = appctx.Normalize(appCode)
options.Platform = normalizeAppPlatform(options.Platform)
options.Keyword = strings.TrimSpace(options.Keyword)
items, err := s.store.ListAppVersions(options)
if err != nil {
return nil, err
}
out := make([]AppVersion, 0, len(items))
for _, item := range items {
out = append(out, appVersionFromModel(item))
}
return out, nil
}
func (s *AppConfigService) CreateAppVersion(appCode string, req appVersionRequest) (AppVersion, error) {
item, err := appVersionModelFromRequest(appctx.Normalize(appCode), req)
if err != nil {
return AppVersion{}, err
}
if err := s.store.CreateAppVersion(&item); err != nil {
return AppVersion{}, err
}
return appVersionFromModel(item), nil
}
func (s *AppConfigService) UpdateAppVersion(appCode string, id uint, req appVersionRequest) (AppVersion, error) {
item, err := s.store.GetAppVersion(appctx.Normalize(appCode), id)
if err != nil {
return AppVersion{}, err
}
updated, err := appVersionModelFromRequest(item.AppCode, req)
if err != nil {
return AppVersion{}, err
}
item.Platform = updated.Platform
item.Version = updated.Version
item.BuildNumber = updated.BuildNumber
item.ForceUpdate = updated.ForceUpdate
item.DownloadURL = updated.DownloadURL
item.Description = updated.Description
if err := s.store.UpdateAppVersion(&item); err != nil {
return AppVersion{}, err
}
return appVersionFromModel(item), nil
}
func (s *AppConfigService) DeleteAppVersion(appCode string, id uint) error {
return s.store.DeleteAppVersion(appctx.Normalize(appCode), id)
}
func (s *AppConfigService) ListExploreTabs(appCode string, options repository.AppExploreTabListOptions) ([]ExploreTab, error) {
options.AppCode = appctx.Normalize(appCode)
options.Keyword = strings.TrimSpace(options.Keyword)
items, err := s.store.ListAppExploreTabs(options)
if err != nil {
return nil, err
}
out := make([]ExploreTab, 0, len(items))
for _, item := range items {
out = append(out, exploreTabFromModel(item))
}
return out, nil
}
func (s *AppConfigService) CreateExploreTab(appCode string, req exploreTabRequest) (ExploreTab, error) {
item, err := exploreTabModelFromRequest(appctx.Normalize(appCode), req)
if err != nil {
return ExploreTab{}, err
}
if err := s.store.CreateAppExploreTab(&item); err != nil {
return ExploreTab{}, err
}
return exploreTabFromModel(item), nil
}
func (s *AppConfigService) UpdateExploreTab(appCode string, id uint, req exploreTabRequest) (ExploreTab, error) {
item, err := s.store.GetAppExploreTab(appctx.Normalize(appCode), id)
if err != nil {
return ExploreTab{}, err
}
updated, err := exploreTabModelFromRequest(item.AppCode, req)
if err != nil {
return ExploreTab{}, err
}
item.Tab = updated.Tab
item.H5URL = updated.H5URL
item.Enabled = updated.Enabled
item.SortOrder = updated.SortOrder
if err := s.store.UpdateAppExploreTab(&item); err != nil {
return ExploreTab{}, err
}
return exploreTabFromModel(item), nil
}
func (s *AppConfigService) DeleteExploreTab(appCode string, id uint) error {
return s.store.DeleteAppExploreTab(appctx.Normalize(appCode), id)
}
func validateH5URL(value string) error {
if value == "" {
return nil
}
if len(value) > 2048 {
return errors.New("h5 link is too long")
}
for _, char := range value {
if unicode.IsSpace(char) {
return errors.New("h5 link cannot contain whitespace")
}
}
return nil
}
func h5LinkModelFromPayload(req h5LinkPayload) (model.AppConfig, error) {
key := strings.TrimSpace(req.Key)
label := strings.TrimSpace(req.Label)
url := strings.TrimSpace(req.URL)
if err := validateH5Key(key); err != nil {
return model.AppConfig{}, err
}
if label == "" || utf8.RuneCountInString(label) > 80 {
return model.AppConfig{}, errors.New("h5 config label is invalid")
}
if url == "" {
return model.AppConfig{}, errors.New("h5 link is required")
}
if err := validateH5URL(url); err != nil {
return model.AppConfig{}, err
}
return model.AppConfig{
Group: h5LinkGroup,
Key: key,
Value: url,
Description: label,
}, nil
}
func validateH5Key(value string) error {
if value == "" || len(value) > 80 {
return errors.New("h5 config key is invalid")
}
for _, char := range value {
switch {
case char >= 'a' && char <= 'z':
case char >= 'A' && char <= 'Z':
case char >= '0' && char <= '9':
case char == '-' || char == '_' || char == '.' || char == ':':
default:
return errors.New("h5 config key is invalid")
}
}
return nil
}
func h5LinkFromModel(config model.AppConfig) H5Link {
label := strings.TrimSpace(config.Description)
if label == "" {
label = config.Key
}
return H5Link{
Key: config.Key,
Label: label,
URL: config.Value,
UpdatedAtMs: config.UpdatedAtMS,
}
}
func bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner, error) {
displayScopes := normalizeBannerDisplayScopes(req.DisplayScopes, req.DisplayScope)
item := model.AppBanner{
AppCode: appctx.Normalize(appCode),
CoverURL: strings.TrimSpace(req.CoverURL),
RoomSmallImageURL: strings.TrimSpace(req.RoomSmallImageURL),
BannerType: normalizeBannerType(req.BannerType),
DisplayScope: bannerDisplayScopeValue(displayScopes),
Param: strings.TrimSpace(req.Param),
Status: normalizeBannerStatus(req.Status),
Platform: normalizeBannerPlatform(req.Platform),
SortOrder: req.SortOrder,
RegionID: req.RegionID,
CountryCode: normalizeCountryCode(req.CountryCode),
Description: strings.TrimSpace(req.Description),
StartsAtMS: req.StartsAtMs,
EndsAtMS: req.EndsAtMs,
}
if item.CoverURL == "" || len(item.CoverURL) > 1024 {
return model.AppBanner{}, errors.New("banner cover is invalid")
}
if containsWhitespace(item.CoverURL) {
return model.AppBanner{}, errors.New("banner cover cannot contain whitespace")
}
if !validBannerDisplayScopes(displayScopes) {
return model.AppBanner{}, errors.New("banner display scope is invalid")
}
if bannerDisplayScopesContain(displayScopes, bannerDisplayScopeRoom) {
if item.RoomSmallImageURL == "" || len(item.RoomSmallImageURL) > 1024 {
return model.AppBanner{}, errors.New("room small image is required")
}
if containsWhitespace(item.RoomSmallImageURL) {
return model.AppBanner{}, errors.New("room small image cannot contain whitespace")
}
} else {
// 未投放到房间内时不保留小屏图,避免客户端误用房间专属素材。
item.RoomSmallImageURL = ""
}
if item.BannerType != "h5" && item.BannerType != "app" {
return model.AppBanner{}, errors.New("banner type is invalid")
}
if item.BannerType == "h5" {
if item.Param == "" {
return model.AppBanner{}, errors.New("h5 link is required")
}
if err := validateH5URL(item.Param); err != nil {
return model.AppBanner{}, err
}
}
if len(item.Param) > 2048 {
return model.AppBanner{}, errors.New("banner param is too long")
}
if item.Status != bannerStatusActive && item.Status != bannerStatusDisabled && item.Status != bannerStatusExpired {
return model.AppBanner{}, errors.New("banner status is invalid")
}
if item.StartsAtMS < 0 || item.EndsAtMS < 0 {
return model.AppBanner{}, errors.New("banner delivery time is invalid")
}
if item.StartsAtMS > 0 && item.EndsAtMS > 0 && item.StartsAtMS >= item.EndsAtMS {
return model.AppBanner{}, errors.New("banner delivery time range is invalid")
}
if item.Status == bannerStatusActive && item.EndsAtMS > 0 && item.EndsAtMS <= time.Now().UTC().UnixMilli() {
item.Status = bannerStatusExpired
}
if item.Platform != "android" && item.Platform != "ios" {
return model.AppBanner{}, errors.New("banner platform is invalid")
}
if item.RegionID < 0 {
return model.AppBanner{}, errors.New("banner region is invalid")
}
if item.CountryCode != "" && !validCountryCode(item.CountryCode) {
return model.AppBanner{}, errors.New("banner country is invalid")
}
if utf8.RuneCountInString(item.Description) > 255 {
return model.AppBanner{}, errors.New("banner description is too long")
}
return item, nil
}
func appBannerFromModel(item model.AppBanner) AppBanner {
return AppBanner{
ID: item.ID,
AppCode: item.AppCode,
CoverURL: item.CoverURL,
RoomSmallImageURL: item.RoomSmallImageURL,
BannerType: item.BannerType,
DisplayScope: item.DisplayScope,
DisplayScopes: bannerDisplayScopeList(item.DisplayScope),
Param: item.Param,
Status: item.Status,
Platform: item.Platform,
SortOrder: item.SortOrder,
RegionID: item.RegionID,
CountryCode: item.CountryCode,
Description: item.Description,
StartsAtMs: item.StartsAtMS,
EndsAtMs: item.EndsAtMS,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func splashScreenModelFromRequest(appCode string, req splashScreenRequest) (model.AppSplashScreen, error) {
item := model.AppSplashScreen{
AppCode: appctx.Normalize(appCode),
CoverURL: strings.TrimSpace(req.CoverURL),
SplashType: normalizeSplashType(req.SplashType),
Param: strings.TrimSpace(req.Param),
Status: normalizeBannerStatus(req.Status),
Platform: normalizeBannerPlatform(req.Platform),
SortOrder: req.SortOrder,
RegionID: req.RegionID,
CountryCode: normalizeCountryCode(req.CountryCode),
Description: strings.TrimSpace(req.Description),
StartsAtMS: req.StartsAtMs,
EndsAtMS: req.EndsAtMs,
}
if item.CoverURL == "" || len(item.CoverURL) > 1024 {
return model.AppSplashScreen{}, errors.New("splash cover is invalid")
}
if containsWhitespace(item.CoverURL) {
return model.AppSplashScreen{}, errors.New("splash cover cannot contain whitespace")
}
if item.SplashType != "h5" && item.SplashType != "app" {
return model.AppSplashScreen{}, errors.New("splash type is invalid")
}
if item.SplashType == "h5" {
// H5 开屏必须带可直接打开的链接APP 类型参数由客户端解释,所以只做长度限制。
if item.Param == "" {
return model.AppSplashScreen{}, errors.New("h5 link is required")
}
if err := validateH5URL(item.Param); err != nil {
return model.AppSplashScreen{}, err
}
}
if len(item.Param) > 2048 {
return model.AppSplashScreen{}, errors.New("splash param is too long")
}
if item.Status != bannerStatusActive && item.Status != bannerStatusDisabled && item.Status != bannerStatusExpired {
return model.AppSplashScreen{}, errors.New("splash status is invalid")
}
if item.StartsAtMS < 0 || item.EndsAtMS < 0 {
return model.AppSplashScreen{}, errors.New("splash delivery time is invalid")
}
if item.StartsAtMS > 0 && item.EndsAtMS > 0 && item.StartsAtMS >= item.EndsAtMS {
return model.AppSplashScreen{}, errors.New("splash delivery time range is invalid")
}
if item.Status == bannerStatusActive && item.EndsAtMS > 0 && item.EndsAtMS <= time.Now().UTC().UnixMilli() {
// 管理员保存已经结束的 active 配置时直接落为 expired列表和 App 侧都不会误判为可投放。
item.Status = bannerStatusExpired
}
if item.Platform != "android" && item.Platform != "ios" {
return model.AppSplashScreen{}, errors.New("splash platform is invalid")
}
if item.RegionID < 0 {
return model.AppSplashScreen{}, errors.New("splash region is invalid")
}
if item.CountryCode != "" && !validCountryCode(item.CountryCode) {
return model.AppSplashScreen{}, errors.New("splash country is invalid")
}
if utf8.RuneCountInString(item.Description) > 255 {
return model.AppSplashScreen{}, errors.New("splash description is too long")
}
return item, nil
}
func appSplashScreenFromModel(item model.AppSplashScreen) AppSplashScreen {
// DTO 只返回后台页面需要的字段不暴露数据库列名gateway 另有 snake_case 的 App 输出结构。
return AppSplashScreen{
ID: item.ID,
AppCode: item.AppCode,
CoverURL: item.CoverURL,
SplashType: item.SplashType,
Param: item.Param,
Status: item.Status,
Platform: item.Platform,
SortOrder: item.SortOrder,
RegionID: item.RegionID,
CountryCode: item.CountryCode,
Description: item.Description,
StartsAtMs: item.StartsAtMS,
EndsAtMs: item.EndsAtMS,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func appVersionModelFromRequest(appCode string, req appVersionRequest) (model.AppVersion, error) {
item := model.AppVersion{
AppCode: appctx.Normalize(appCode),
Platform: normalizeAppPlatform(req.Platform),
Version: strings.TrimSpace(req.Version),
BuildNumber: req.BuildNumber,
ForceUpdate: req.ForceUpdate,
DownloadURL: strings.TrimSpace(req.DownloadURL),
Description: strings.TrimSpace(req.Description),
}
if item.Platform != "android" && item.Platform != "ios" {
return model.AppVersion{}, errors.New("app version platform is invalid")
}
if item.Version == "" || utf8.RuneCountInString(item.Version) > 40 {
return model.AppVersion{}, errors.New("app version is invalid")
}
if item.BuildNumber <= 0 {
return model.AppVersion{}, errors.New("app build number is invalid")
}
if item.DownloadURL == "" || len(item.DownloadURL) > 2048 {
return model.AppVersion{}, errors.New("app download url is invalid")
}
for _, char := range item.DownloadURL {
if unicode.IsSpace(char) {
return model.AppVersion{}, errors.New("app download url cannot contain whitespace")
}
}
if utf8.RuneCountInString(item.Description) > 1000 {
return model.AppVersion{}, errors.New("app version description is too long")
}
return item, nil
}
func appVersionFromModel(item model.AppVersion) AppVersion {
return AppVersion{
ID: item.ID,
AppCode: item.AppCode,
Platform: item.Platform,
Version: item.Version,
BuildNumber: item.BuildNumber,
ForceUpdate: item.ForceUpdate,
DownloadURL: item.DownloadURL,
Description: item.Description,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func exploreTabModelFromRequest(appCode string, req exploreTabRequest) (model.AppExploreTab, error) {
item := model.AppExploreTab{
AppCode: appctx.Normalize(appCode),
Tab: strings.TrimSpace(req.Tab),
H5URL: strings.TrimSpace(req.H5URL),
Enabled: req.Enabled,
SortOrder: req.SortOrder,
}
if item.Tab == "" || utf8.RuneCountInString(item.Tab) > 40 {
return model.AppExploreTab{}, errors.New("explore tab is invalid")
}
if item.H5URL == "" || len(item.H5URL) > 2048 {
return model.AppExploreTab{}, errors.New("explore h5 url is invalid")
}
if err := validateH5URL(item.H5URL); err != nil {
return model.AppExploreTab{}, err
}
return item, nil
}
func exploreTabFromModel(item model.AppExploreTab) ExploreTab {
return ExploreTab{
ID: item.ID,
AppCode: item.AppCode,
Tab: item.Tab,
H5URL: item.H5URL,
Enabled: item.Enabled,
SortOrder: item.SortOrder,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func normalizeBannerType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeSplashType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeBannerStatus(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return bannerStatusActive
}
return value
}
func normalizeBannerDisplayScope(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "":
return bannerDisplayScopeHome
case bannerDisplayScopeHome, "首页":
return bannerDisplayScopeHome
case bannerDisplayScopeRoom, "房间内":
return bannerDisplayScopeRoom
case bannerDisplayScopeRecharge, "充值页":
return bannerDisplayScopeRecharge
default:
return value
}
}
func normalizeBannerDisplayScopes(values []string, fallback string) []string {
seen := make(map[string]struct{}, len(bannerDisplayScopeOrder))
for _, value := range values {
for _, part := range strings.Split(value, ",") {
scope := normalizeBannerDisplayScope(part)
if scope == "" {
continue
}
seen[scope] = struct{}{}
}
}
if len(seen) == 0 {
for _, part := range strings.Split(fallback, ",") {
scope := normalizeBannerDisplayScope(part)
if scope == "" {
continue
}
seen[scope] = struct{}{}
}
}
if len(seen) == 0 {
seen[bannerDisplayScopeHome] = struct{}{}
}
out := make([]string, 0, len(seen))
for _, scope := range bannerDisplayScopeOrder {
if _, ok := seen[scope]; ok {
out = append(out, scope)
delete(seen, scope)
}
}
for scope := range seen {
out = append(out, scope)
}
return out
}
func bannerDisplayScopeList(value string) []string {
return normalizeBannerDisplayScopes(nil, value)
}
func bannerDisplayScopeValue(scopes []string) string {
return strings.Join(scopes, ",")
}
func validBannerDisplayScopes(scopes []string) bool {
if len(scopes) == 0 {
return false
}
for _, scope := range scopes {
if scope != bannerDisplayScopeHome && scope != bannerDisplayScopeRoom && scope != bannerDisplayScopeRecharge {
return false
}
}
return true
}
func bannerDisplayScopesContain(scopes []string, target string) bool {
for _, scope := range scopes {
if scope == target {
return true
}
}
return false
}
func normalizeBannerPlatform(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeAppPlatform(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeCountryCode(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func containsWhitespace(value string) bool {
for _, char := range value {
if unicode.IsSpace(char) {
return true
}
}
return false
}
func validCountryCode(value string) bool {
if len(value) < 2 || len(value) > 3 {
return false
}
for _, char := range value {
if char < 'A' || char > 'Z' {
return false
}
}
return true
}