// Package appconfig 读取后台管理维护的 App 运行时配置。 package appconfig import ( "context" "database/sql" "errors" "strings" _ "github.com/go-sql-driver/mysql" ) const h5LinkGroup = "h5-links" const listH5LinksSQL = "SELECT `key`, COALESCE(value, ''), updated_at_ms FROM admin_app_configs WHERE `group` = ?" const listAppBannersSQL = ` SELECT id, app_code, cover_url, banner_type, param, platform, sort_order, region_id, country_code, description, updated_at_ms FROM admin_app_banners WHERE app_code = ? AND status = 'active' AND (? = '' OR platform = ?) AND (region_id = 0 OR region_id = ?) AND (country_code = '' OR country_code = ?) ORDER BY sort_order ASC, id DESC` // H5Link 是 gateway 下发给 App 的 H5 入口配置。 type H5Link struct { Key string `json:"key"` Label string `json:"label"` URL string `json:"url"` UpdatedAtMs int64 `json:"updated_at_ms"` } // BannerQuery 是 App 首页 banner 的公开筛选条件。 type BannerQuery struct { AppCode string Platform string RegionID int64 Country string } // Banner 是 gateway 下发给 App 首页的 banner 配置。 type Banner struct { ID uint `json:"id"` CoverURL string `json:"cover_url"` BannerType string `json:"type"` Param string `json:"param"` Platform string `json:"platform"` SortOrder int `json:"sort_order"` RegionID int64 `json:"region_id,omitempty"` CountryCode string `json:"country_code,omitempty"` Description string `json:"description,omitempty"` UpdatedAtMs int64 `json:"updated_at_ms"` } // Reader 是 HTTP 层读取 H5 配置的最小依赖。 type Reader interface { ListH5Links(ctx context.Context) ([]H5Link, error) ListBanners(ctx context.Context, query BannerQuery) ([]Banner, error) } type h5LinkDefinition struct { Key string Label string } 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"}, } // MySQLReader 从 hyapp_admin.admin_app_configs 读取后台 H5 配置。 type MySQLReader struct { db *sql.DB } // OpenMySQLReader 创建后台配置只读连接池。 func OpenMySQLReader(dsn string) (*MySQLReader, error) { db, err := sql.Open("mysql", strings.TrimSpace(dsn)) if err != nil { return nil, err } return &MySQLReader{db: db}, nil } // Close 关闭后台配置连接池。 func (r *MySQLReader) Close() error { if r == nil || r.db == nil { return nil } return r.db.Close() } // ListH5Links 返回固定 H5 入口集合;未配置的入口保留空 URL,方便客户端按 key 稳定渲染。 func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) { if r == nil || r.db == nil { return nil, errors.New("app config reader is not configured") } rows, err := r.db.QueryContext(ctx, listH5LinksSQL, h5LinkGroup) if err != nil { return nil, err } defer rows.Close() configs := make(map[string]H5Link, len(h5LinkDefinitions)) for rows.Next() { var key string var url string var updatedAtMS int64 if err := rows.Scan(&key, &url, &updatedAtMS); err != nil { return nil, err } key = strings.TrimSpace(key) link := H5Link{ Key: key, URL: strings.TrimSpace(url), } link.UpdatedAtMs = updatedAtMS configs[key] = link } if err := rows.Err(); err != nil { return nil, err } items := make([]H5Link, 0, len(h5LinkDefinitions)) for _, definition := range h5LinkDefinitions { item := H5Link{ Key: definition.Key, Label: definition.Label, } if config, ok := configs[definition.Key]; ok { item.URL = config.URL item.UpdatedAtMs = config.UpdatedAtMs } items = append(items, item) } return items, nil } // ListBanners 返回当前 App、平台和地域可见的首页 banner。 func (r *MySQLReader) ListBanners(ctx context.Context, query BannerQuery) ([]Banner, error) { if r == nil || r.db == nil { return nil, errors.New("app config reader is not configured") } appCode := normalizeAppCode(query.AppCode) platform := normalizePlatform(query.Platform) country := normalizeCountry(query.Country) rows, err := r.db.QueryContext(ctx, listAppBannersSQL, appCode, platform, platform, query.RegionID, country) if err != nil { return nil, err } defer rows.Close() items := make([]Banner, 0) for rows.Next() { var item Banner var appCode string var updatedAtMS int64 if err := rows.Scan( &item.ID, &appCode, &item.CoverURL, &item.BannerType, &item.Param, &item.Platform, &item.SortOrder, &item.RegionID, &item.CountryCode, &item.Description, &updatedAtMS, ); err != nil { return nil, err } item.UpdatedAtMs = updatedAtMS items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } return items, nil } // StaticReader 给测试和临时环境提供内存版 H5 配置读取。 type StaticReader struct { Links []H5Link Banners []Banner Err error } // ListH5Links 返回预置 H5 配置。 func (r StaticReader) ListH5Links(context.Context) ([]H5Link, error) { if r.Err != nil { return nil, r.Err } out := make([]H5Link, len(r.Links)) copy(out, r.Links) return out, nil } // ListBanners 返回预置 App banner 配置。 func (r StaticReader) ListBanners(context.Context, BannerQuery) ([]Banner, error) { if r.Err != nil { return nil, r.Err } out := make([]Banner, len(r.Banners)) copy(out, r.Banners) return out, nil } func normalizeAppCode(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "lalu" } return value } func normalizePlatform(value string) string { return strings.ToLower(strings.TrimSpace(value)) } func normalizeCountry(value string) string { return strings.ToUpper(strings.TrimSpace(value)) }