305 lines
8.0 KiB
Go
305 lines
8.0 KiB
Go
// 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`
|
||
const latestAppVersionSQL = `
|
||
SELECT id, app_code, platform, version, build_number, force_update, download_url, COALESCE(description, ''), updated_at_ms
|
||
FROM admin_app_versions
|
||
WHERE app_code = ? AND platform = ?
|
||
ORDER BY build_number DESC, id DESC
|
||
LIMIT 1`
|
||
|
||
// 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"`
|
||
}
|
||
|
||
// VersionQuery 是 App 检查更新的公开筛选条件。
|
||
type VersionQuery struct {
|
||
AppCode string
|
||
Platform string
|
||
CurrentBuildNumber int64
|
||
}
|
||
|
||
// Version 是后台 APP配置/版本管理 中当前平台的最新 App 版本。
|
||
type Version struct {
|
||
ID uint `json:"id"`
|
||
AppCode string `json:"app_code"`
|
||
Platform string `json:"platform"`
|
||
Version string `json:"version"`
|
||
BuildNumber int64 `json:"build_number"`
|
||
ForceUpdate bool `json:"force_update"`
|
||
DownloadURL string `json:"download_url"`
|
||
Description string `json:"description"`
|
||
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)
|
||
LatestVersion(ctx context.Context, query VersionQuery) (Version, 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
|
||
}
|
||
|
||
// LatestVersion 返回指定 App 和平台的最高 build_number 版本;未配置时返回空版本,方便 App 端按无更新处理。
|
||
func (r *MySQLReader) LatestVersion(ctx context.Context, query VersionQuery) (Version, error) {
|
||
if r == nil || r.db == nil {
|
||
return Version{}, errors.New("app config reader is not configured")
|
||
}
|
||
|
||
var item Version
|
||
err := r.db.QueryRowContext(ctx, latestAppVersionSQL, normalizeAppCode(query.AppCode), normalizePlatform(query.Platform)).Scan(
|
||
&item.ID,
|
||
&item.AppCode,
|
||
&item.Platform,
|
||
&item.Version,
|
||
&item.BuildNumber,
|
||
&item.ForceUpdate,
|
||
&item.DownloadURL,
|
||
&item.Description,
|
||
&item.UpdatedAtMs,
|
||
)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return Version{AppCode: normalizeAppCode(query.AppCode), Platform: normalizePlatform(query.Platform)}, nil
|
||
}
|
||
if err != nil {
|
||
return Version{}, err
|
||
}
|
||
return item, nil
|
||
}
|
||
|
||
// StaticReader 给测试和临时环境提供内存版 H5 配置读取。
|
||
type StaticReader struct {
|
||
Links []H5Link
|
||
Banners []Banner
|
||
Version Version
|
||
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
|
||
}
|
||
|
||
// LatestVersion 返回预置版本配置。
|
||
func (r StaticReader) LatestVersion(_ context.Context, query VersionQuery) (Version, error) {
|
||
if r.Err != nil {
|
||
return Version{}, r.Err
|
||
}
|
||
item := r.Version
|
||
if item.AppCode == "" {
|
||
item.AppCode = normalizeAppCode(query.AppCode)
|
||
}
|
||
if item.Platform == "" {
|
||
item.Platform = normalizePlatform(query.Platform)
|
||
}
|
||
return item, 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))
|
||
}
|