diff --git a/scripts/mysql/066_app_logo_url.sql b/scripts/mysql/066_app_logo_url.sql new file mode 100644 index 00000000..5d10a3e5 --- /dev/null +++ b/scripts/mysql/066_app_logo_url.sql @@ -0,0 +1,43 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_user; + +-- 只新增无索引的展示列,并强制 INSTANT 算法;不支持元数据即时变更时直接失败,避免意外触发全表重建。 +SET @apps_logo_column_exists = ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'apps' + AND COLUMN_NAME = 'logo_url' +); +SET @apps_logo_ddl = IF( + @apps_logo_column_exists = 0, + 'ALTER TABLE apps ADD COLUMN logo_url VARCHAR(1024) NOT NULL DEFAULT '''' COMMENT ''应用 Logo 的公网访问地址'', ALGORITHM=INSTANT', + 'SELECT 1' +); +PREPARE apps_logo_stmt FROM @apps_logo_ddl; +EXECUTE apps_logo_stmt; +DEALLOCATE PREPARE apps_logo_stmt; + +SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); + +-- app_code 有唯一索引,五个常量键只会做定点查找;不在 apps 表的 legacy App 由 admin-server 财务源配置提供 Logo。 +UPDATE apps +SET logo_url = CASE app_code + WHEN 'lalu' THEN 'https://media.haiyihy.com/admin/apps/logos/lalu.png' + WHEN 'huwaa' THEN 'https://media.haiyihy.com/admin/apps/logos/huwaa.png' + WHEN 'fami' THEN 'https://media.haiyihy.com/admin/apps/logos/fami.png' + WHEN 'aslan' THEN 'https://media.haiyihy.com/admin/apps/logos/aslan.png' + WHEN 'yumi' THEN 'https://media.haiyihy.com/admin/apps/logos/yumi.png' + ELSE logo_url + END, + updated_at_ms = @now_ms +WHERE app_code IN ('lalu', 'huwaa', 'fami', 'aslan', 'yumi') + AND logo_url <> CASE app_code + WHEN 'lalu' THEN 'https://media.haiyihy.com/admin/apps/logos/lalu.png' + WHEN 'huwaa' THEN 'https://media.haiyihy.com/admin/apps/logos/huwaa.png' + WHEN 'fami' THEN 'https://media.haiyihy.com/admin/apps/logos/fami.png' + WHEN 'aslan' THEN 'https://media.haiyihy.com/admin/apps/logos/aslan.png' + WHEN 'yumi' THEN 'https://media.haiyihy.com/admin/apps/logos/yumi.png' + ELSE logo_url + END; diff --git a/server/admin/configs/config.tencent.example.yaml b/server/admin/configs/config.tencent.example.yaml index 11271075..9a5d5421 100644 --- a/server/admin/configs/config.tencent.example.yaml +++ b/server/admin/configs/config.tencent.example.yaml @@ -18,6 +18,7 @@ money_region_sources: - enabled: true app_code: "yumi" app_name: "Yumi" + logo_url: "https://media.haiyihy.com/admin/apps/logos/yumi.png" sys_origin: "LIKEI" mongo_uri: "mongodb://root:123456@10.2.21.9:27017/tarab_all?authSource=admin&readPreference=secondaryPreferred&retryWrites=false&maxPoolSize=50&minPoolSize=5&maxIdleTimeMS=6000" mongo_database: "tarab_all" @@ -26,6 +27,7 @@ money_region_sources: - enabled: true app_code: "aslan" app_name: "Aslan" + logo_url: "https://media.haiyihy.com/admin/apps/logos/aslan.png" sys_origin: "ATYOU" mongo_uri: "mongodb://aslan_data:aslan_data@lb-le274w4o-wjgmcdkhye3v0ors.clb.sg-tencentclb.com:27017,lb-le274w4o-wjgmcdkhye3v0ors.clb.sg-tencentclb.com:27018/test?replicaSet=cmgo-6cztxjgr_0&authSource=admin&readPreference=secondaryPreferred" mongo_database: "atyou" @@ -36,6 +38,7 @@ finance_bill_sources: - enabled: true app_code: "yumi" app_name: "Yumi" + logo_url: "https://media.haiyihy.com/admin/apps/logos/yumi.png" sys_origin: "LIKEI" mongo_uri: "mongodb://root:123456@10.2.21.9:27017/tarab_all?authSource=admin&readPreference=secondaryPreferred&retryWrites=false&maxPoolSize=50&minPoolSize=5&maxIdleTimeMS=6000" mongo_database: "tarab_all" @@ -47,6 +50,7 @@ finance_bill_sources: - enabled: true app_code: "aslan" app_name: "Aslan" + logo_url: "https://media.haiyihy.com/admin/apps/logos/aslan.png" sys_origin: "ATYOU" mongo_uri: "mongodb://aslan_data:aslan_data@lb-le274w4o-wjgmcdkhye3v0ors.clb.sg-tencentclb.com:27017,lb-le274w4o-wjgmcdkhye3v0ors.clb.sg-tencentclb.com:27018/test?replicaSet=cmgo-6cztxjgr_0&authSource=admin&readPreference=secondaryPreferred" mongo_database: "atyou" diff --git a/server/admin/configs/config.yaml b/server/admin/configs/config.yaml index 5beed748..e0b5310f 100644 --- a/server/admin/configs/config.yaml +++ b/server/admin/configs/config.yaml @@ -18,6 +18,7 @@ money_region_sources: - enabled: false app_code: "yumi" app_name: "Yumi" + logo_url: "https://media.haiyihy.com/admin/apps/logos/yumi.png" sys_origin: "LIKEI" mongo_uri: "" mongo_database: "tarab_all" @@ -26,6 +27,7 @@ money_region_sources: - enabled: false app_code: "aslan" app_name: "Aslan" + logo_url: "https://media.haiyihy.com/admin/apps/logos/aslan.png" sys_origin: "ATYOU" mongo_uri: "" mongo_database: "atyou" @@ -36,6 +38,7 @@ finance_bill_sources: - enabled: false app_code: "yumi" app_name: "Yumi" + logo_url: "https://media.haiyihy.com/admin/apps/logos/yumi.png" sys_origin: "LIKEI" mongo_uri: "" mongo_database: "tarab_all" @@ -47,6 +50,7 @@ finance_bill_sources: - enabled: false app_code: "aslan" app_name: "Aslan" + logo_url: "https://media.haiyihy.com/admin/apps/logos/aslan.png" sys_origin: "ATYOU" mongo_uri: "" mongo_database: "atyou" diff --git a/server/admin/internal/config/config.go b/server/admin/internal/config/config.go index aa23d4a0..5d5f2877 100644 --- a/server/admin/internal/config/config.go +++ b/server/admin/internal/config/config.go @@ -149,6 +149,7 @@ type MoneyRegionSourceConfig struct { Enabled bool `yaml:"enabled"` AppCode string `yaml:"app_code"` AppName string `yaml:"app_name"` + LogoURL string `yaml:"logo_url"` SysOrigin string `yaml:"sys_origin"` MongoURI string `yaml:"mongo_uri"` MongoDatabase string `yaml:"mongo_database"` @@ -162,6 +163,7 @@ type FinanceBillSourceConfig struct { Enabled bool `yaml:"enabled"` AppCode string `yaml:"app_code"` AppName string `yaml:"app_name"` + LogoURL string `yaml:"logo_url"` SysOrigin string `yaml:"sys_origin"` MongoURI string `yaml:"mongo_uri"` MongoDatabase string `yaml:"mongo_database"` @@ -237,6 +239,7 @@ func Default() Config { Enabled: false, AppCode: "yumi", AppName: "Yumi", + LogoURL: "https://media.haiyihy.com/admin/apps/logos/yumi.png", SysOrigin: "LIKEI", MongoDatabase: "tarab_all", MongoCollection: "sys_region_config", @@ -246,6 +249,7 @@ func Default() Config { Enabled: false, AppCode: "aslan", AppName: "Aslan", + LogoURL: "https://media.haiyihy.com/admin/apps/logos/aslan.png", SysOrigin: "ATYOU", MongoDatabase: "tarab_all", MongoCollection: "sys_region_config", @@ -257,6 +261,7 @@ func Default() Config { Enabled: false, AppCode: "yumi", AppName: "Yumi", + LogoURL: "https://media.haiyihy.com/admin/apps/logos/yumi.png", SysOrigin: "LIKEI", MongoDatabase: "tarab_all", MongoCollection: "in_app_purchase_details", @@ -266,6 +271,7 @@ func Default() Config { Enabled: false, AppCode: "aslan", AppName: "Aslan", + LogoURL: "https://media.haiyihy.com/admin/apps/logos/aslan.png", // Aslan(likei-services 项目)与 Yumi 同一套平台代码,但独立 Mongo 集群;业务库是 atyou。 SysOrigin: "ATYOU", MongoDatabase: "atyou", @@ -585,6 +591,7 @@ func (cfg *Config) Normalize() { // legacy 区域来源只服务财务范围目录;这里先规整配置,handler 层只处理业务过滤,不重复处理大小写和默认集合名。 source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode)) source.AppName = strings.TrimSpace(source.AppName) + source.LogoURL = strings.TrimSpace(source.LogoURL) source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin)) source.MongoURI = strings.TrimSpace(source.MongoURI) source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/") @@ -605,6 +612,7 @@ func (cfg *Config) Normalize() { // legacy 账单源只服务财务充值明细;配置层统一大小写和默认集合,handler 只按 app_code 命中。 source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode)) source.AppName = strings.TrimSpace(source.AppName) + source.LogoURL = strings.TrimSpace(source.LogoURL) source.SysOrigin = strings.ToUpper(strings.TrimSpace(source.SysOrigin)) source.MongoURI = strings.TrimSpace(source.MongoURI) source.MongoDatabase = strings.Trim(strings.TrimSpace(source.MongoDatabase), "/") diff --git a/server/admin/internal/modules/appregistry/service.go b/server/admin/internal/modules/appregistry/service.go index 0153e259..ff6aed6b 100644 --- a/server/admin/internal/modules/appregistry/service.go +++ b/server/admin/internal/modules/appregistry/service.go @@ -14,6 +14,7 @@ type App struct { AppID int64 `json:"appId"` AppCode string `json:"appCode"` AppName string `json:"appName"` + LogoURL string `json:"logoUrl"` PackageName string `json:"packageName"` Platform string `json:"platform"` Status string `json:"status"` @@ -30,7 +31,7 @@ func (s *Service) ListApps(ctx context.Context) ([]App, error) { return nil, fmt.Errorf("user mysql is not configured") } rows, err := s.userDB.QueryContext(ctx, ` - SELECT app_id, app_code, app_name, package_name, platform, status, created_at_ms, updated_at_ms + SELECT app_id, app_code, app_name, logo_url, package_name, platform, status, created_at_ms, updated_at_ms FROM apps WHERE status = 'active' ORDER BY app_name ASC, app_code ASC @@ -43,7 +44,7 @@ func (s *Service) ListApps(ctx context.Context) ([]App, error) { items := []App{} for rows.Next() { var item App - if err := rows.Scan(&item.AppID, &item.AppCode, &item.AppName, &item.PackageName, &item.Platform, &item.Status, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { + if err := rows.Scan(&item.AppID, &item.AppCode, &item.AppName, &item.LogoURL, &item.PackageName, &item.Platform, &item.Status, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { return nil, err } items = append(items, item) diff --git a/server/admin/internal/modules/appregistry/service_test.go b/server/admin/internal/modules/appregistry/service_test.go new file mode 100644 index 00000000..4613150c --- /dev/null +++ b/server/admin/internal/modules/appregistry/service_test.go @@ -0,0 +1,31 @@ +package appregistry + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestListAppsReturnsLogoURL(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("create sqlmock: %v", err) + } + defer db.Close() + + mock.ExpectQuery(`SELECT app_id, app_code, app_name, logo_url, package_name, platform, status, created_at_ms, updated_at_ms`). + WillReturnRows(sqlmock.NewRows([]string{"app_id", "app_code", "app_name", "logo_url", "package_name", "platform", "status", "created_at_ms", "updated_at_ms"}). + AddRow(1, "lalu", "Lalu", "https://media.haiyihy.com/admin/apps/logos/lalu.png", "com.org.laluparty", "", "active", 10, 20)) + + items, err := NewService(db).ListApps(context.Background()) + if err != nil { + t.Fatalf("list apps: %v", err) + } + if len(items) != 1 || items[0].LogoURL != "https://media.haiyihy.com/admin/apps/logos/lalu.png" { + t.Fatalf("unexpected app items: %+v", items) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} diff --git a/server/admin/internal/modules/payment/legacy_bill_source.go b/server/admin/internal/modules/payment/legacy_bill_source.go index 1fa20c4e..41e2e979 100644 --- a/server/admin/internal/modules/payment/legacy_bill_source.go +++ b/server/admin/internal/modules/payment/legacy_bill_source.go @@ -240,6 +240,11 @@ func (s *MongoRechargeBillSource) AppName() string { return s.config.AppCode } +// LogoURL 与 legacy 账单源同配置生命周期;这类 App 不是 HyApp gateway 租户,不为了展示 Logo 向 apps 注册表塞入虚假 active 记录。 +func (s *MongoRechargeBillSource) LogoURL() string { + return strings.TrimSpace(s.config.LogoURL) +} + type legacyCoinSellerRechargeStats struct { TotalUSDMinor int64 DailyUSDMinor map[string]int64 diff --git a/server/admin/internal/modules/payment/legacy_bill_source_test.go b/server/admin/internal/modules/payment/legacy_bill_source_test.go index 374cad77..9efc32c8 100644 --- a/server/admin/internal/modules/payment/legacy_bill_source_test.go +++ b/server/admin/internal/modules/payment/legacy_bill_source_test.go @@ -16,6 +16,7 @@ func yumiBillSourceConfig() config.FinanceBillSourceConfig { Enabled: true, AppCode: "yumi", AppName: "Yumi", + LogoURL: "https://media.haiyihy.com/admin/apps/logos/yumi.png", SysOrigin: "LIKEI", MongoDatabase: "test", MongoCollection: "in_app_purchase_details", @@ -23,6 +24,13 @@ func yumiBillSourceConfig() config.FinanceBillSourceConfig { } } +func TestLegacyRechargeBillSourceExposesConfiguredLogoURL(t *testing.T) { + source := &MongoRechargeBillSource{config: yumiBillSourceConfig()} + if source.LogoURL() != "https://media.haiyihy.com/admin/apps/logos/yumi.png" { + t.Fatalf("logo url=%q", source.LogoURL()) + } +} + func aslanBillSourceConfig() config.FinanceBillSourceConfig { cfg := yumiBillSourceConfig() cfg.AppCode = "aslan" diff --git a/server/admin/internal/modules/payment/money.go b/server/admin/internal/modules/payment/money.go index f27f27b7..899a58c7 100644 --- a/server/admin/internal/modules/payment/money.go +++ b/server/admin/internal/modules/payment/money.go @@ -46,6 +46,7 @@ type moneyAppDTO struct { AppID int64 `json:"appId"` AppCode string `json:"appCode"` AppName string `json:"appName"` + LogoURL string `json:"logoUrl"` PackageName string `json:"packageName"` Platform string `json:"platform"` Status string `json:"status"` @@ -456,7 +457,7 @@ func (h *Handler) loadMoneyMasterData(ctx context.Context, access repository.Mon func (h *Handler) listMoneyApps(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, error) { where, args := scopedAppWhere(access) rows, err := h.userDB.QueryContext(ctx, ` - SELECT app_id, app_code, app_name, package_name, platform, status + SELECT app_id, app_code, app_name, logo_url, package_name, platform, status FROM apps WHERE status = 'active'`+where+` ORDER BY app_name ASC, app_code ASC`, args...) @@ -467,7 +468,7 @@ func (h *Handler) listMoneyApps(ctx context.Context, access repository.MoneyAcce items := []moneyAppDTO{} for rows.Next() { var item moneyAppDTO - if err := rows.Scan(&item.AppID, &item.AppCode, &item.AppName, &item.PackageName, &item.Platform, &item.Status); err != nil { + if err := rows.Scan(&item.AppID, &item.AppCode, &item.AppName, &item.LogoURL, &item.PackageName, &item.Platform, &item.Status); err != nil { return nil, err } items = append(items, item) diff --git a/server/admin/internal/modules/payment/money_region_source.go b/server/admin/internal/modules/payment/money_region_source.go index 99362d46..39d04e74 100644 --- a/server/admin/internal/modules/payment/money_region_source.go +++ b/server/admin/internal/modules/payment/money_region_source.go @@ -253,8 +253,10 @@ func legacyRegionDocumentsToMoneyData(sourceConfig config.MoneyRegionSourceConfi } } return []moneyAppDTO{{ - AppCode: appCode, - AppName: appName, + AppCode: appCode, + AppName: appName, + // Legacy App 没有 apps 行;其 Logo 与区域源一起配置,避免为了 UI 展示伪造活跃租户。 + LogoURL: strings.TrimSpace(sourceConfig.LogoURL), Platform: "legacy", Status: "active", }}, regions, countries, nil diff --git a/server/admin/internal/modules/payment/money_region_source_test.go b/server/admin/internal/modules/payment/money_region_source_test.go index 3b3fce80..729d1457 100644 --- a/server/admin/internal/modules/payment/money_region_source_test.go +++ b/server/admin/internal/modules/payment/money_region_source_test.go @@ -24,6 +24,7 @@ func TestLegacyRegionDocumentsToMoneyDataFiltersYumiRegions(t *testing.T) { cfg := config.MoneyRegionSourceConfig{ AppCode: "yumi", AppName: "Yumi", + LogoURL: "https://media.example.com/yumi.png", SysOrigin: "LIKEI", } apps, regions, countries, err := legacyRegionDocumentsToMoneyData(cfg, []legacyRegionDocument{ @@ -35,7 +36,7 @@ func TestLegacyRegionDocumentsToMoneyDataFiltersYumiRegions(t *testing.T) { if err != nil { t.Fatalf("legacy money data failed: %v", err) } - if len(apps) != 1 || apps[0].AppCode != "yumi" || apps[0].AppName != "Yumi" || apps[0].Platform != "legacy" { + if len(apps) != 1 || apps[0].AppCode != "yumi" || apps[0].AppName != "Yumi" || apps[0].LogoURL != cfg.LogoURL || apps[0].Platform != "legacy" { t.Fatalf("apps mismatch: %+v", apps) } if len(regions) != 2 || regions[0].RegionID < legacySyntheticRegionIDBase || regions[0].RegionCode != "OTHER" || regions[1].RegionID != 1001 || regions[1].RegionCode != "MIDDLE_EAST" { @@ -159,7 +160,7 @@ func newMoneyRegionSourceUserSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, fu } func expectEmptyMoneyApps(mock sqlmock.Sqlmock, appCodes ...string) { - query := mock.ExpectQuery(`(?s)SELECT app_id, app_code, app_name, package_name, platform, status\s+FROM apps\s+WHERE status = 'active'`) + query := mock.ExpectQuery(`(?s)SELECT app_id, app_code, app_name, logo_url, package_name, platform, status\s+FROM apps\s+WHERE status = 'active'`) if len(appCodes) > 0 { args := make([]driver.Value, 0, len(appCodes)) for _, appCode := range appCodes { @@ -167,12 +168,12 @@ func expectEmptyMoneyApps(mock sqlmock.Sqlmock, appCodes ...string) { } query.WithArgs(args...) } - query.WillReturnRows(sqlmock.NewRows([]string{"app_id", "app_code", "app_name", "package_name", "platform", "status"})) + query.WillReturnRows(sqlmock.NewRows([]string{"app_id", "app_code", "app_name", "logo_url", "package_name", "platform", "status"})) } func TestMoneyRegionSourceConfigDefaults(t *testing.T) { cfg := config.Default() - if len(cfg.MoneyRegionSources) != 2 || cfg.MoneyRegionSources[0].RequestTimeout != 5*time.Second || cfg.MoneyRegionSources[1].AppCode != "aslan" { + if len(cfg.MoneyRegionSources) != 2 || cfg.MoneyRegionSources[0].RequestTimeout != 5*time.Second || cfg.MoneyRegionSources[0].LogoURL == "" || cfg.MoneyRegionSources[1].AppCode != "aslan" { t.Fatalf("default money region source mismatch: %+v", cfg.MoneyRegionSources) } } diff --git a/server/admin/internal/modules/payment/recharge_bill_apps_test.go b/server/admin/internal/modules/payment/recharge_bill_apps_test.go new file mode 100644 index 00000000..e465c664 --- /dev/null +++ b/server/admin/internal/modules/payment/recharge_bill_apps_test.go @@ -0,0 +1,47 @@ +package payment + +import ( + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" +) + +func TestListRechargeBillAppsReturnsStoredLogoURL(t *testing.T) { + gin.SetMode(gin.TestMode) + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("create sqlmock: %v", err) + } + defer db.Close() + + mock.ExpectQuery(`SELECT app_code, app_name, logo_url`). + WillReturnRows(sqlmock.NewRows([]string{"app_code", "app_name", "logo_url"}). + AddRow("lalu", "Lalu", "https://media.haiyihy.com/admin/apps/logos/lalu.png")) + + recorder := httptest.NewRecorder() + // 直接使用 recorder 承载 Gin 响应,测试只关注 App 目录映射,不绕过真实 handler 序列化。 + context, _ := gin.CreateTestContext(recorder) + context.Request = httptest.NewRequest("GET", "/api/v1/admin/payment/recharge-apps", nil) + (&Handler{userDB: db, billSources: map[string]RechargeBillSource{}}).ListRechargeBillApps(context) + + if recorder.Code != 200 { + t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) + } + var payload struct { + Data struct { + Items []rechargeBillAppDTO `json:"items"` + } `json:"data"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil { + t.Fatalf("decode response: %v", err) + } + if len(payload.Data.Items) != 1 || payload.Data.Items[0].LogoURL != "https://media.haiyihy.com/admin/apps/logos/lalu.png" { + t.Fatalf("unexpected apps: %+v", payload.Data.Items) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} diff --git a/server/admin/internal/modules/payment/recharge_bill_stats.go b/server/admin/internal/modules/payment/recharge_bill_stats.go index 8993ce35..5a6ebfe7 100644 --- a/server/admin/internal/modules/payment/recharge_bill_stats.go +++ b/server/admin/internal/modules/payment/recharge_bill_stats.go @@ -279,6 +279,12 @@ func (h *Handler) RefreshGoogleRechargePaidDetails(c *gin.Context) { type rechargeBillAppDTO struct { AppCode string `json:"appCode"` AppName string `json:"appName"` + LogoURL string `json:"logoUrl"` +} + +// rechargeBillSourceLogo 是兼容扩展:历史或测试账单源不实现时仍可列出,新的 Mongo 源则补充 Logo。 +type rechargeBillSourceLogo interface { + LogoURL() string } // ListRechargeBillApps 返回充值详情可选的 App 目录:hyapp 在册应用 + 配置的 legacy 账单源(Yumi 等)。 @@ -287,7 +293,7 @@ func (h *Handler) ListRechargeBillApps(c *gin.Context) { seen := map[string]struct{}{} if h.userDB != nil { rows, err := h.userDB.QueryContext(c.Request.Context(), ` - SELECT app_code, app_name + SELECT app_code, app_name, logo_url FROM apps WHERE status = 'active' ORDER BY app_name ASC, app_code ASC`) @@ -298,7 +304,7 @@ func (h *Handler) ListRechargeBillApps(c *gin.Context) { defer rows.Close() for rows.Next() { var item rechargeBillAppDTO - if err := rows.Scan(&item.AppCode, &item.AppName); err != nil { + if err := rows.Scan(&item.AppCode, &item.AppName, &item.LogoURL); err != nil { response.ServerError(c, "获取 App 列表失败") return } @@ -317,7 +323,11 @@ func (h *Handler) ListRechargeBillApps(c *gin.Context) { if _, ok := seen[appCode]; ok { continue } - legacyApps = append(legacyApps, rechargeBillAppDTO{AppCode: appCode, AppName: source.AppName()}) + logoURL := "" + if logoSource, ok := source.(rechargeBillSourceLogo); ok { + logoURL = strings.TrimSpace(logoSource.LogoURL()) + } + legacyApps = append(legacyApps, rechargeBillAppDTO{AppCode: appCode, AppName: source.AppName(), LogoURL: logoURL}) } sort.Slice(legacyApps, func(i, j int) bool { return legacyApps[i].AppName < legacyApps[j].AppName }) items = append(items, legacyApps...) diff --git a/services/user-service/deploy/mysql/initdb/001_user_service.sql b/services/user-service/deploy/mysql/initdb/001_user_service.sql index f3664040..7e99825c 100644 --- a/services/user-service/deploy/mysql/initdb/001_user_service.sql +++ b/services/user-service/deploy/mysql/initdb/001_user_service.sql @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS apps ( app_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '应用 ID', app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', app_name VARCHAR(128) NOT NULL COMMENT '应用名称', + logo_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '应用 Logo 的公网访问地址', package_name VARCHAR(191) NOT NULL COMMENT '包名称', platform VARCHAR(32) NOT NULL DEFAULT '' COMMENT '平台', status VARCHAR(32) NOT NULL COMMENT '业务状态', @@ -20,14 +21,15 @@ CREATE TABLE IF NOT EXISTS apps ( KEY idx_apps_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应用注册表'; -INSERT INTO apps (app_code, app_name, package_name, platform, status, created_at_ms, updated_at_ms) +INSERT INTO apps (app_code, app_name, logo_url, package_name, platform, status, created_at_ms, updated_at_ms) VALUES - ('lalu', 'Lalu', 'com.org.laluparty', '', 'active', 0, 0), - ('huwaa', 'Huwaa', 'com.app.huwaa', '', 'active', 0, 0), - ('fami', 'Fami', 'com.chat.fami', '', 'active', 0, 0), - ('yumi', 'Yumi', 'com.org.yumi', '', 'active', 0, 0) + ('lalu', 'Lalu', 'https://media.haiyihy.com/admin/apps/logos/lalu.png', 'com.org.laluparty', '', 'active', 0, 0), + ('huwaa', 'Huwaa', 'https://media.haiyihy.com/admin/apps/logos/huwaa.png', 'com.app.huwaa', '', 'active', 0, 0), + ('fami', 'Fami', 'https://media.haiyihy.com/admin/apps/logos/fami.png', 'com.chat.fami', '', 'active', 0, 0), + ('yumi', 'Yumi', 'https://media.haiyihy.com/admin/apps/logos/yumi.png', 'com.org.yumi', '', 'active', 0, 0) ON DUPLICATE KEY UPDATE app_name = VALUES(app_name), + logo_url = VALUES(logo_url), package_name = VALUES(package_name), platform = VALUES(platform), status = VALUES(status), diff --git a/services/user-service/deploy/mysql/migrations/016_app_logo_url.sql b/services/user-service/deploy/mysql/migrations/016_app_logo_url.sql new file mode 100644 index 00000000..7975346a --- /dev/null +++ b/services/user-service/deploy/mysql/migrations/016_app_logo_url.sql @@ -0,0 +1,43 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_user; + +-- apps 是 App 目录于后台与 gateway 共用的事实源;Logo 只是稳定展示元数据,不参与租户解析或鉴权。 +SET @apps_logo_column_exists = ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'apps' + AND COLUMN_NAME = 'logo_url' +); +SET @apps_logo_ddl = IF( + @apps_logo_column_exists = 0, + 'ALTER TABLE apps ADD COLUMN logo_url VARCHAR(1024) NOT NULL DEFAULT '''' COMMENT ''应用 Logo 的公网访问地址'', ALGORITHM=INSTANT', + 'SELECT 1' +); +PREPARE apps_logo_stmt FROM @apps_logo_ddl; +EXECUTE apps_logo_stmt; +DEALLOCATE PREPARE apps_logo_stmt; + +SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); + +-- 用 app_code 唯一索引定点更新;Aslan/Yumi 若仅作为 legacy 财务源不在表内,本语句不会为其创建可被 gateway 解析的租户记录。 +UPDATE apps +SET logo_url = CASE app_code + WHEN 'lalu' THEN 'https://media.haiyihy.com/admin/apps/logos/lalu.png' + WHEN 'huwaa' THEN 'https://media.haiyihy.com/admin/apps/logos/huwaa.png' + WHEN 'fami' THEN 'https://media.haiyihy.com/admin/apps/logos/fami.png' + WHEN 'aslan' THEN 'https://media.haiyihy.com/admin/apps/logos/aslan.png' + WHEN 'yumi' THEN 'https://media.haiyihy.com/admin/apps/logos/yumi.png' + ELSE logo_url + END, + updated_at_ms = @now_ms +WHERE app_code IN ('lalu', 'huwaa', 'fami', 'aslan', 'yumi') + AND logo_url <> CASE app_code + WHEN 'lalu' THEN 'https://media.haiyihy.com/admin/apps/logos/lalu.png' + WHEN 'huwaa' THEN 'https://media.haiyihy.com/admin/apps/logos/huwaa.png' + WHEN 'fami' THEN 'https://media.haiyihy.com/admin/apps/logos/fami.png' + WHEN 'aslan' THEN 'https://media.haiyihy.com/admin/apps/logos/aslan.png' + WHEN 'yumi' THEN 'https://media.haiyihy.com/admin/apps/logos/yumi.png' + ELSE logo_url + END;