415 lines
12 KiB
Go
415 lines
12 KiB
Go
package appconfig
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
"hyapp-admin-server/internal/model"
|
|
"hyapp-admin-server/internal/repository"
|
|
)
|
|
|
|
const h5LinkGroup = "h5-links"
|
|
|
|
var h5LinkDefinitions = []H5LinkDefinition{
|
|
{Key: "host-center", Label: "Host Center"},
|
|
{Key: "bd-center", Label: "BD Center"},
|
|
{Key: "bd-leader-center", Label: "BD Leader Center"},
|
|
{Key: "agency-center", Label: "Agency Center"},
|
|
{Key: "invite-user", Label: "Invite User"},
|
|
}
|
|
|
|
type AppConfigService struct {
|
|
store *repository.Store
|
|
}
|
|
|
|
type H5LinkDefinition struct {
|
|
Key string `json:"key"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
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"`
|
|
BannerType string `json:"bannerType"`
|
|
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"`
|
|
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"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
configByKey := make(map[string]model.AppConfig, len(configs))
|
|
for _, config := range configs {
|
|
configByKey[config.Key] = config
|
|
}
|
|
|
|
items := make([]H5Link, 0, len(h5LinkDefinitions))
|
|
for _, definition := range h5LinkDefinitions {
|
|
item := H5Link{
|
|
Key: definition.Key,
|
|
Label: definition.Label,
|
|
}
|
|
if config, ok := configByKey[definition.Key]; ok {
|
|
item.URL = config.Value
|
|
item.UpdatedAtMs = config.UpdatedAtMS
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *AppConfigService) UpdateH5Links(request updateH5LinksRequest) ([]H5Link, error) {
|
|
if len(request.Items) == 0 {
|
|
return nil, errors.New("items are required")
|
|
}
|
|
|
|
allowed := h5LinkDefinitionSet()
|
|
items := make([]model.AppConfig, 0, len(request.Items))
|
|
seen := make(map[string]struct{}, len(request.Items))
|
|
for _, item := range request.Items {
|
|
key := strings.TrimSpace(item.Key)
|
|
if _, ok := allowed[key]; !ok {
|
|
return nil, fmt.Errorf("invalid h5 link key: %s", key)
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
return nil, fmt.Errorf("duplicate h5 link key: %s", key)
|
|
}
|
|
seen[key] = struct{}{}
|
|
|
|
link := strings.TrimSpace(item.URL)
|
|
if err := validateH5URL(link); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, model.AppConfig{
|
|
Group: h5LinkGroup,
|
|
Key: key,
|
|
Value: link,
|
|
Description: allowed[key],
|
|
})
|
|
}
|
|
|
|
if err := s.store.UpsertAppConfigs(items); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.ListH5Links()
|
|
}
|
|
|
|
func (s *AppConfigService) ListBanners(appCode string, options repository.AppBannerListOptions) ([]AppBanner, error) {
|
|
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)
|
|
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.BannerType = updated.BannerType
|
|
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
|
|
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) 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 h5LinkDefinitionSet() map[string]string {
|
|
out := make(map[string]string, len(h5LinkDefinitions))
|
|
for _, definition := range h5LinkDefinitions {
|
|
out[definition.Key] = definition.Label
|
|
}
|
|
return out
|
|
}
|
|
|
|
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 bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner, error) {
|
|
item := model.AppBanner{
|
|
AppCode: appctx.Normalize(appCode),
|
|
CoverURL: strings.TrimSpace(req.CoverURL),
|
|
BannerType: normalizeBannerType(req.BannerType),
|
|
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),
|
|
}
|
|
if item.CoverURL == "" || len(item.CoverURL) > 1024 {
|
|
return model.AppBanner{}, errors.New("banner cover is invalid")
|
|
}
|
|
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 != "active" && item.Status != "disabled" {
|
|
return model.AppBanner{}, errors.New("banner status is invalid")
|
|
}
|
|
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,
|
|
BannerType: item.BannerType,
|
|
Param: item.Param,
|
|
Status: item.Status,
|
|
Platform: item.Platform,
|
|
SortOrder: item.SortOrder,
|
|
RegionID: item.RegionID,
|
|
CountryCode: item.CountryCode,
|
|
Description: item.Description,
|
|
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 normalizeBannerType(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func normalizeBannerStatus(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return "active"
|
|
}
|
|
return value
|
|
}
|
|
|
|
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 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
|
|
}
|