diff --git a/deploy/popadmin/popadmin.js b/deploy/popadmin/popadmin.js index 2ad6123..31f9acc 100644 --- a/deploy/popadmin/popadmin.js +++ b/deploy/popadmin/popadmin.js @@ -2895,7 +2895,7 @@ const pages = { $('content').innerHTML = ` -
同开发者应用(火花)· 客户端 GET /api/client/pop/developer-apps
+
同开发者应用(火花)· 客户端 GET /v1/developer-apps(免登录)· 别名 GET /api/client/pop/developer-apps
diff --git a/docs/daily/2026-09-24-change-list.md b/docs/daily/2026-09-24-change-list.md index 22d8dbc..28d4d18 100644 --- a/docs/daily/2026-09-24-change-list.md +++ b/docs/daily/2026-09-24-change-list.md @@ -205,3 +205,36 @@ make install-git-hooks # 本机只需一次 ### 关联 - 用户需求:广场机器人 RSS + 15 天过期 + 后台机器人媒体压缩检测 + +## 14:30 — 同开发者应用墙(Discover) + +### 概要 + +- 新增免登录客户端接口 `GET /v1/developer-apps`(别名 `/api/client/pop|spark/developer-apps`) +- popadmin「同开发者应用」对接 `/api/admin/developer-apps` CRUD + 同步官网图标目录 +- 回落默认官网应用目录;migration 编号为 0027(避开已占用的 0024 feedback) + +### 变更范围 + +| 类型 | 说明 | +|------|------| +| 功能 | 同开发者应用列表 API | +| 管理端 | popadmin developer-apps 路径可落库 | +| 迁移 | `t_developer_apps` 种子数据(0027) | + +### 主要文件 + +- `migrations/0027_developer_apps.sql` — 表与种子 +- `internal/module/client/logic/developer_apps.go` — 业务 +- `internal/module/client/model/repo.go` — 仓储 +- `internal/transport/http/router*.go` / `auth.go` / handlers — 路由与免登录 +- `deploy/popadmin/popadmin.js` — 客户端路径说明 + +### 验证 + +- `make test`: skipped(本机 PATH 无 go) +- 需部署 spark-server 并跑 migrate 后线上接口才可用;无 DB 时 logic 回落官网默认目录 + +### 关联 + +- 任务:发现页顶部接入同开发者应用列表(stash 合并进 main) diff --git a/internal/module/client/entity/types.go b/internal/module/client/entity/types.go index 85bf2a6..b6e7768 100644 --- a/internal/module/client/entity/types.go +++ b/internal/module/client/entity/types.go @@ -65,3 +65,35 @@ type SubscriptionConfig struct { Subheadline string `json:"subheadline"` Plans []SubscriptionPlan `json:"plans"` } + +// DeveloperApp is one row in the same-developer app wall. +type DeveloperApp struct { + ID int64 `json:"id"` + SourceAppCode string `json:"sourceAppCode"` + TargetAppName string `json:"targetAppName"` + TargetPackageName string `json:"targetPackageName"` + Subtitle string `json:"subtitle"` + IconURL string `json:"iconUrl"` + StoreURL string `json:"storeUrl"` + FallbackURL string `json:"fallbackUrl"` + SchemaURL string `json:"schemaUrl"` + SortOrder int `json:"sortOrder"` + Enabled bool `json:"enabled"` +} + +// DeveloperAppItem is the client-facing card (Discover / welfare). +type DeveloperAppItem struct { + ID int64 `json:"id"` + Name string `json:"name"` + Subtitle string `json:"subtitle"` + IconURL string `json:"iconUrl"` + StoreURL string `json:"storeUrl"` + SchemaURL string `json:"schemaUrl"` + PackageName string `json:"packageName"` + SortOrder int `json:"sortOrder"` +} + +// DeveloperAppsPayload is GET /v1/developer-apps data. +type DeveloperAppsPayload struct { + Items []DeveloperAppItem `json:"items"` +} diff --git a/internal/module/client/logic/developer_apps.go b/internal/module/client/logic/developer_apps.go new file mode 100644 index 0000000..7d4f8e4 --- /dev/null +++ b/internal/module/client/logic/developer_apps.go @@ -0,0 +1,159 @@ +package logic + +import ( + "context" + "strings" + + "spark-server/internal/module/client/entity" + "spark-server/internal/pkg/errs" +) + +// DeveloperApps returns enabled apps for the given source app (client wall). +func (s *Service) DeveloperApps(ctx context.Context, sourceAppCode, excludePackage string) (*entity.DeveloperAppsPayload, error) { + sourceAppCode = normalizeAppCode(sourceAppCode) + excludePackage = strings.TrimSpace(excludePackage) + rows := s.loadDeveloperApps(ctx, sourceAppCode, true) + // SleepHeart 可回退 spark 共用墙 + if len(rows) == 0 && sourceAppCode == "sleepheart" { + rows = s.loadDeveloperApps(ctx, "spark", true) + } + items := make([]entity.DeveloperAppItem, 0, len(rows)) + for _, row := range rows { + if excludePackage != "" && strings.EqualFold(row.TargetPackageName, excludePackage) { + continue + } + items = append(items, toDeveloperAppItem(row)) + } + return &entity.DeveloperAppsPayload{Items: items}, nil +} + +// ListDeveloperAppsAdmin returns all rows for popadmin (including disabled). +func (s *Service) ListDeveloperAppsAdmin(ctx context.Context, sourceAppCode string) ([]entity.DeveloperApp, error) { + return s.loadDeveloperApps(ctx, normalizeAppCode(sourceAppCode), false), nil +} + +// CreateDeveloperAppAdmin inserts or updates one recommendation. +func (s *Service) CreateDeveloperAppAdmin(ctx context.Context, in entity.DeveloperApp) (*entity.DeveloperApp, error) { + if s.repo == nil { + return nil, errs.ErrAvailable + } + in.SourceAppCode = normalizeAppCode(in.SourceAppCode) + in.TargetAppName = strings.TrimSpace(in.TargetAppName) + in.TargetPackageName = strings.TrimSpace(in.TargetPackageName) + in.Subtitle = strings.TrimSpace(in.Subtitle) + in.IconURL = strings.TrimSpace(in.IconURL) + in.StoreURL = strings.TrimSpace(in.StoreURL) + in.FallbackURL = strings.TrimSpace(in.FallbackURL) + in.SchemaURL = strings.TrimSpace(in.SchemaURL) + if in.TargetAppName == "" { + return nil, errs.ErrInvalidArgument.WithMsg("targetAppName required") + } + if in.TargetPackageName == "" { + in.TargetPackageName = "pkg_" + strings.ToLower(strings.ReplaceAll(in.TargetAppName, " ", "_")) + } + if in.StoreURL == "" && in.FallbackURL != "" { + in.StoreURL = in.FallbackURL + } + in.Enabled = true + if err := s.repo.UpsertDeveloperApp(ctx, &in); err != nil { + return nil, err + } + return &in, nil +} + +// DeleteDeveloperAppAdmin removes one row by id. +func (s *Service) DeleteDeveloperAppAdmin(ctx context.Context, id int64) error { + if s.repo == nil { + return errs.ErrAvailable + } + if id <= 0 { + return errs.ErrInvalidArgument + } + return s.repo.DeleteDeveloperApp(ctx, id) +} + +// SyncWebsiteIconsAdmin upserts catalog apps + default icons from wyrs365.com. +func (s *Service) SyncWebsiteIconsAdmin(ctx context.Context) (int, error) { + if s.repo == nil { + return 0, errs.ErrAvailable + } + n := 0 + for _, src := range []string{"spark", "sleepheart"} { + for _, row := range websiteDeveloperCatalog(src) { + cur := row + if err := s.repo.UpsertDeveloperApp(ctx, &cur); err != nil { + return n, err + } + n++ + } + } + return n, nil +} + +func (s *Service) loadDeveloperApps(ctx context.Context, sourceAppCode string, enabledOnly bool) []entity.DeveloperApp { + if s.repo != nil { + rows, err := s.repo.ListDeveloperApps(ctx, sourceAppCode, enabledOnly) + if err == nil && len(rows) > 0 { + return rows + } + } + return defaultDeveloperApps(sourceAppCode) +} + +func toDeveloperAppItem(a entity.DeveloperApp) entity.DeveloperAppItem { + store := a.StoreURL + if store == "" { + store = a.FallbackURL + } + return entity.DeveloperAppItem{ + ID: a.ID, Name: a.TargetAppName, Subtitle: a.Subtitle, IconURL: a.IconURL, + StoreURL: store, SchemaURL: a.SchemaURL, PackageName: a.TargetPackageName, SortOrder: a.SortOrder, + } +} + +func normalizeAppCode(code string) string { + code = strings.ToLower(strings.TrimSpace(code)) + switch code { + case "", "pop": + return "spark" + case "health", "healthios", "sleep", "sleep-heart": + return "sleepheart" + default: + return code + } +} + +func defaultDeveloperApps(sourceAppCode string) []entity.DeveloperApp { + return websiteDeveloperCatalog(sourceAppCode) +} + +func websiteDeveloperCatalog(sourceAppCode string) []entity.DeveloperApp { + mk := func(name, pkg, sub, icon, page string, order int) entity.DeveloperApp { + if icon == "" { + icon = "https://www.wyrs365.com/assets/logo-piggy-account.png" + } + return entity.DeveloperApp{ + SourceAppCode: sourceAppCode, TargetAppName: name, TargetPackageName: pkg, + Subtitle: sub, IconURL: icon, StoreURL: page, FallbackURL: page, + SortOrder: order, Enabled: true, + } + } + return []entity.DeveloperApp{ + mk("Flipclock桌面翻页时钟", "com.wyrs.flipclock", "桌面翻页时钟小组件", + "https://www.wyrs365.com/assets/logo-flipclock.png", "https://www.wyrs365.com/flipclock", 10), + mk("手指测心率", "com.wyrs.heartrate", "指尖 PPG 测心率", + "https://www.wyrs365.com/assets/logo-heartrate.png", "https://www.wyrs365.com/heartrate", 20), + mk("猪咪记账记事本", "com.wyrs.piggy", "记账 · 倒数日 · 小组件", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/piggy-account", 30), + mk("鼾声分贝仪", "com.wyrs.snore", "鼾声检测与分贝测量", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/decibel", 40), + mk("微雨LedBanner", "com.wyrs.ledbanner", "LED 滚动灯牌", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/led-banner", 50), + mk("微雨PDF扫描转换器", "com.wyrs.wypdf", "PDF 扫描转换", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/pdf-scanner", 60), + mk("RunSteps", "com.wyrs.runsteps", "步数与运动", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/runsteps", 70), + mk("火花", "com.wyrs.spark", "即时通讯 · 火花", + "https://www.wyrs365.com/assets/logo-piggy-account.png", "https://www.wyrs365.com/", 80), + } +} diff --git a/internal/module/client/logic/service.go b/internal/module/client/logic/service.go index 7872416..63b2236 100644 --- a/internal/module/client/logic/service.go +++ b/internal/module/client/logic/service.go @@ -27,6 +27,9 @@ type Repository interface { UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error GetPasswordHash(ctx context.Context, userID int64) (string, error) SetPasswordHash(ctx context.Context, userID int64, hash string) error + ListDeveloperApps(ctx context.Context, sourceAppCode string, enabledOnly bool) ([]entity.DeveloperApp, error) + UpsertDeveloperApp(ctx context.Context, a *entity.DeveloperApp) error + DeleteDeveloperApp(ctx context.Context, id int64) error } type UserFinder interface { diff --git a/internal/module/client/logic/service_test.go b/internal/module/client/logic/service_test.go index 7b59c63..a094606 100644 --- a/internal/module/client/logic/service_test.go +++ b/internal/module/client/logic/service_test.go @@ -115,6 +115,21 @@ func (s *stubClientRepo) SetPasswordHash(_ context.Context, userID int64, hash s return nil } +func (s *stubClientRepo) ListDeveloperApps(_ context.Context, _ string, _ bool) ([]entity.DeveloperApp, error) { + return nil, nil +} + +func (s *stubClientRepo) UpsertDeveloperApp(_ context.Context, a *entity.DeveloperApp) error { + if a != nil && a.ID == 0 { + a.ID = 1 + } + return nil +} + +func (s *stubClientRepo) DeleteDeveloperApp(_ context.Context, _ int64) error { + return nil +} + type stubUserFinder struct { byEmail map[string]*userentity.User } @@ -167,6 +182,13 @@ func TestLegalPlansFeedbackFallback(t *testing.T) { if loginFB.AppVersion != "1.2.3" { t.Fatalf("app_version from header want 1.2.3 got %q", loginFB.AppVersion) } + apps, err := svc.DeveloperApps(t.Context(), "sleepheart", "") + if err != nil || apps == nil || len(apps.Items) == 0 { + t.Fatalf("developer apps %+v err=%v", apps, err) + } + if apps.Items[0].Name == "" || apps.Items[0].StoreURL == "" { + t.Fatalf("developer app item incomplete %+v", apps.Items[0]) + } } func TestClientServiceAdminAndRepo(t *testing.T) { diff --git a/internal/module/client/model/repo.go b/internal/module/client/model/repo.go index 225cc4f..171dd58 100644 --- a/internal/module/client/model/repo.go +++ b/internal/module/client/model/repo.go @@ -240,3 +240,95 @@ func (r *Repo) SetPasswordHash(ctx context.Context, userID int64, hash string) e row := passwordRow{UserID: userID, PasswordHash: hash, UpdatedAt: time.Now()} return r.db.WithContext(ctx).Save(&row).Error } + +type developerAppRow struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement"` + SourceAppCode string `gorm:"column:source_app_code"` + TargetAppName string `gorm:"column:target_app_name"` + TargetPackageName string `gorm:"column:target_package_name"` + Subtitle string `gorm:"column:subtitle"` + IconURL string `gorm:"column:icon_url"` + StoreURL string `gorm:"column:store_url"` + FallbackURL string `gorm:"column:fallback_url"` + SchemaURL string `gorm:"column:schema_url"` + SortOrder int `gorm:"column:sort_order"` + Enabled bool `gorm:"column:enabled"` + UpdatedAt time.Time `gorm:"column:updated_at"` +} + +func (developerAppRow) TableName() string { return "t_developer_apps" } + +func (r *Repo) ListDeveloperApps(ctx context.Context, sourceAppCode string, enabledOnly bool) ([]entity.DeveloperApp, error) { + q := r.db.WithContext(ctx).Where("source_app_code = ?", sourceAppCode) + if enabledOnly { + q = q.Where("enabled = 1") + } + var rows []developerAppRow + if err := q.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil { + return nil, err + } + out := make([]entity.DeveloperApp, 0, len(rows)) + for _, row := range rows { + out = append(out, mapDeveloperApp(row)) + } + return out, nil +} + +func (r *Repo) UpsertDeveloperApp(ctx context.Context, a *entity.DeveloperApp) error { + now := time.Now() + var existing developerAppRow + err := r.db.WithContext(ctx). + Where("source_app_code = ? AND target_package_name = ?", a.SourceAppCode, a.TargetPackageName). + Take(&existing).Error + if err == nil { + existing.TargetAppName = a.TargetAppName + existing.Subtitle = a.Subtitle + if a.IconURL != "" { + existing.IconURL = a.IconURL + } + if a.StoreURL != "" { + existing.StoreURL = a.StoreURL + } + if a.FallbackURL != "" { + existing.FallbackURL = a.FallbackURL + } + if a.SchemaURL != "" { + existing.SchemaURL = a.SchemaURL + } + existing.SortOrder = a.SortOrder + existing.Enabled = a.Enabled + existing.UpdatedAt = now + if err := r.db.WithContext(ctx).Save(&existing).Error; err != nil { + return err + } + a.ID = existing.ID + return nil + } + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + row := developerAppRow{ + SourceAppCode: a.SourceAppCode, TargetAppName: a.TargetAppName, + TargetPackageName: a.TargetPackageName, Subtitle: a.Subtitle, IconURL: a.IconURL, + StoreURL: a.StoreURL, FallbackURL: a.FallbackURL, SchemaURL: a.SchemaURL, + SortOrder: a.SortOrder, Enabled: a.Enabled, UpdatedAt: now, + } + if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { + return err + } + a.ID = row.ID + return nil +} + +func (r *Repo) DeleteDeveloperApp(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&developerAppRow{}, id).Error +} + +func mapDeveloperApp(row developerAppRow) entity.DeveloperApp { + return entity.DeveloperApp{ + ID: row.ID, SourceAppCode: row.SourceAppCode, TargetAppName: row.TargetAppName, + TargetPackageName: row.TargetPackageName, Subtitle: row.Subtitle, IconURL: row.IconURL, + StoreURL: row.StoreURL, FallbackURL: row.FallbackURL, SchemaURL: row.SchemaURL, + SortOrder: row.SortOrder, Enabled: row.Enabled, + } +} diff --git a/internal/transport/http/auth.go b/internal/transport/http/auth.go index 0b96ee7..65849ad 100644 --- a/internal/transport/http/auth.go +++ b/internal/transport/http/auth.go @@ -95,7 +95,9 @@ func publicPath(path string) bool { case "/v1/meta/enums", "/v1/auth/login", "/v1/auth/token/refresh", "/v1/sms/codes", "/v1/auth/password-login", "/v1/auth/password", "/v1/legal/agreements", "/v1/feedback", "/v1/subscriptions/plans", - "/v1/music/playlist", "/v1/music/categories", "/v1/music/settings": + "/v1/music/playlist", "/v1/music/categories", "/v1/music/settings", + "/v1/developer-apps", + "/api/client/pop/developer-apps", "/api/client/spark/developer-apps": return true default: // Admin routes use AdminAuth (Java popadmin JWT), not client Bearer. diff --git a/internal/transport/http/handler/admin/client.go b/internal/transport/http/handler/admin/client.go index 3621368..55b6005 100644 --- a/internal/transport/http/handler/admin/client.go +++ b/internal/transport/http/handler/admin/client.go @@ -11,7 +11,7 @@ import ( clientlogic "spark-server/internal/module/client/logic" ) -// ClientHandler serves popadmin legal / subscription / feedback ops. +// ClientHandler serves popadmin legal / subscription / feedback / developer-apps ops. type ClientHandler struct { svc *clientlogic.Service } @@ -122,3 +122,67 @@ func (h *ClientHandler) PatchFeedback(c *gin.Context) { } OKMsg(c, "已更新", gin.H{"id": id, "status": body.Status}) } + +func (h *ClientHandler) ListDeveloperApps(c *gin.Context) { + if !h.ready(c) { + return + } + appCode := c.Query("appCode") + if appCode == "" { + appCode = c.Query("sourceAppCode") + } + if appCode == "" { + appCode = "spark" + } + list, err := h.svc.ListDeveloperAppsAdmin(c.Request.Context(), appCode) + if err != nil { + Fail(c, http.StatusInternalServerError, err.Error()) + return + } + OK(c, list) +} + +func (h *ClientHandler) CreateDeveloperApp(c *gin.Context) { + if !h.ready(c) { + return + } + var body entity.DeveloperApp + if err := c.ShouldBindJSON(&body); err != nil { + Fail(c, http.StatusBadRequest, "无效 JSON") + return + } + out, err := h.svc.CreateDeveloperAppAdmin(c.Request.Context(), body) + if err != nil { + Fail(c, http.StatusBadRequest, err.Error()) + return + } + OKMsg(c, "已保存", out) +} + +func (h *ClientHandler) DeleteDeveloperApp(c *gin.Context) { + if !h.ready(c) { + return + } + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil || id <= 0 { + Fail(c, http.StatusBadRequest, "无效 id") + return + } + if err := h.svc.DeleteDeveloperAppAdmin(c.Request.Context(), id); err != nil { + Fail(c, http.StatusBadRequest, err.Error()) + return + } + OKMsg(c, "已删除", gin.H{"id": id}) +} + +func (h *ClientHandler) SyncDeveloperAppIcons(c *gin.Context) { + if !h.ready(c) { + return + } + n, err := h.svc.SyncWebsiteIconsAdmin(c.Request.Context()) + if err != nil { + Fail(c, http.StatusBadRequest, err.Error()) + return + } + OKMsg(c, "已同步", gin.H{"count": n}) +} diff --git a/internal/transport/http/handler/client/handler.go b/internal/transport/http/handler/client/handler.go index 1281c9b..db0dfb9 100644 --- a/internal/transport/http/handler/client/handler.go +++ b/internal/transport/http/handler/client/handler.go @@ -59,3 +59,19 @@ func (h *Handler) SetPassword(c *gin.Context) { } respond.JSON(c, gin.H{}) } + +func (h *Handler) DeveloperApps(c *gin.Context) { + source := c.Query("appCode") + if source == "" { + source = c.Query("sourceAppCode") + } + if source == "" { + source = "sleepheart" + } + out, err := h.svc.DeveloperApps(c.Request.Context(), source, c.Query("packageName")) + if err != nil { + respond.JSONError(c, err) + return + } + respond.JSON(c, out) +} diff --git a/internal/transport/http/router.go b/internal/transport/http/router.go index 65ccd15..66cb06c 100644 --- a/internal/transport/http/router.go +++ b/internal/transport/http/router.go @@ -30,6 +30,7 @@ func registerRoutes(r *gin.Engine, deps *EngineDeps) { registerDocs(r) registerHealth(r, deps.Health) registerAdminRoutes(r, deps) + registerClientPopAliases(r, deps) v1 := r.Group("/v1") registerMetaRoutes(v1) registerAuthRoutes(v1, deps) @@ -99,6 +100,18 @@ func registerClientRoutes(v1 *gin.RouterGroup, deps *EngineDeps) { v1.GET("/legal/agreements", h.LegalAgreements) v1.POST("/feedback", h.SubmitFeedback) v1.GET("/subscriptions/plans", h.SubscriptionPlans) + v1.GET("/developer-apps", h.DeveloperApps) +} + +func registerClientPopAliases(r *gin.Engine, deps *EngineDeps) { + if deps == nil || deps.Client == nil { + return + } + h := clienthandler.New(deps.Client) + for _, prefix := range []string{"/api/client/pop", "/api/client/spark"} { + g := r.Group(prefix) + g.GET("/developer-apps", h.DeveloperApps) + } } func registerSMSRoutes(v1 *gin.RouterGroup, deps *EngineDeps) { diff --git a/internal/transport/http/router_admin.go b/internal/transport/http/router_admin.go index 4a137e7..1bddf36 100644 --- a/internal/transport/http/router_admin.go +++ b/internal/transport/http/router_admin.go @@ -39,6 +39,8 @@ func registerAdminRoutes(r *gin.Engine, deps *EngineDeps) { } } + registerDeveloperAppsAdminRoot(r, auth, ch) + for _, prefix := range []string{"/api/admin/spark", "/api/admin/pop"} { g := r.Group(prefix) g.Use(auth) @@ -105,4 +107,21 @@ func registerClientAdmin(g *gin.RouterGroup, h *adminhandler.ClientHandler) { g.PUT("/subscriptions/plans", h.PutPlan) g.GET("/feedbacks", h.ListFeedback) g.PATCH("/feedbacks/:id", h.PatchFeedback) + g.GET("/developer-apps", h.ListDeveloperApps) + g.POST("/developer-apps", h.CreateDeveloperApp) + g.DELETE("/developer-apps/:id", h.DeleteDeveloperApp) + g.POST("/developer-apps/sync-website-icons", h.SyncDeveloperAppIcons) +} + +func registerDeveloperAppsAdminRoot(r *gin.Engine, auth gin.HandlerFunc, h *adminhandler.ClientHandler) { + if h == nil { + return + } + // popadmin.js uses /api/admin/developer-apps (no spark|pop prefix). + g := r.Group("/api/admin") + g.Use(auth) + g.GET("/developer-apps", h.ListDeveloperApps) + g.POST("/developer-apps", h.CreateDeveloperApp) + g.DELETE("/developer-apps/:id", h.DeleteDeveloperApp) + g.POST("/developer-apps/sync-website-icons", h.SyncDeveloperAppIcons) } diff --git a/migrations/0027_developer_apps.sql b/migrations/0027_developer_apps.sql new file mode 100644 index 0000000..daea2e9 --- /dev/null +++ b/migrations/0027_developer_apps.sql @@ -0,0 +1,51 @@ +-- 同开发者应用(应用墙):popadmin 可配,客户端 Discover / 福利页展示。 + +CREATE TABLE IF NOT EXISTS t_developer_apps ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '自增 id', + source_app_code VARCHAR(64) NOT NULL DEFAULT 'spark' COMMENT '来源 App 编码(spark|sleepheart|…)', + target_app_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '展示名称', + target_package_name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '包名 / Bundle ID', + subtitle VARCHAR(256) NOT NULL DEFAULT '' COMMENT '副标题', + icon_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '图标 URL', + store_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'App Store / 下载链接', + fallback_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '备用跳转链接', + schema_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'URL Scheme / 深链', + sort_order INT NOT NULL DEFAULT 0 COMMENT '越小越靠前', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=上架', + updated_at DATETIME(3) NOT NULL COMMENT '更新时间', + UNIQUE KEY uk_dev_apps_source_pkg (source_app_code, target_package_name), + KEY idx_dev_apps_source (source_app_code, enabled, sort_order) +) COMMENT='同开发者推荐应用列表'; + +INSERT INTO t_developer_apps +(source_app_code, target_app_name, target_package_name, subtitle, icon_url, store_url, fallback_url, schema_url, sort_order, enabled, updated_at) +VALUES +('spark', 'Flipclock桌面翻页时钟', 'com.wyrs.flipclock', '桌面翻页时钟小组件', 'https://www.wyrs365.com/assets/logo-flipclock.png', + 'https://www.wyrs365.com/flipclock', 'https://www.wyrs365.com/flipclock', '', 10, 1, CURRENT_TIMESTAMP(3)), +('spark', '手指测心率', 'com.wyrs.heartrate', '指尖 PPG 测心率', 'https://www.wyrs365.com/assets/logo-heartrate.png', + 'https://www.wyrs365.com/heartrate', 'https://www.wyrs365.com/heartrate', '', 20, 1, CURRENT_TIMESTAMP(3)), +('spark', '猪咪记账记事本', 'com.wyrs.piggy', '记账 · 倒数日 · 小组件', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/piggy-account', 'https://www.wyrs365.com/piggy-account', '', 30, 1, CURRENT_TIMESTAMP(3)), +('spark', '鼾声分贝仪', 'com.wyrs.snore', '鼾声检测与分贝测量', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/decibel', 'https://www.wyrs365.com/decibel', '', 40, 1, CURRENT_TIMESTAMP(3)), +('spark', '微雨LedBanner', 'com.wyrs.ledbanner', 'LED 滚动灯牌', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/led-banner', 'https://www.wyrs365.com/led-banner', '', 50, 1, CURRENT_TIMESTAMP(3)), +('sleepheart', 'Flipclock桌面翻页时钟', 'com.wyrs.flipclock', '桌面翻页时钟小组件', 'https://www.wyrs365.com/assets/logo-flipclock.png', + 'https://www.wyrs365.com/flipclock', 'https://www.wyrs365.com/flipclock', '', 10, 1, CURRENT_TIMESTAMP(3)), +('sleepheart', '手指测心率', 'com.wyrs.heartrate', '指尖 PPG 测心率', 'https://www.wyrs365.com/assets/logo-heartrate.png', + 'https://www.wyrs365.com/heartrate', 'https://www.wyrs365.com/heartrate', '', 20, 1, CURRENT_TIMESTAMP(3)), +('sleepheart', '猪咪记账记事本', 'com.wyrs.piggy', '记账 · 倒数日 · 小组件', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/piggy-account', 'https://www.wyrs365.com/piggy-account', '', 30, 1, CURRENT_TIMESTAMP(3)), +('sleepheart', '鼾声分贝仪', 'com.wyrs.snore', '鼾声检测与分贝测量', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/decibel', 'https://www.wyrs365.com/decibel', '', 40, 1, CURRENT_TIMESTAMP(3)), +('sleepheart', '微雨LedBanner', 'com.wyrs.ledbanner', 'LED 滚动灯牌', 'https://www.wyrs365.com/assets/logo-piggy-account.png', + 'https://www.wyrs365.com/led-banner', 'https://www.wyrs365.com/led-banner', '', 50, 1, CURRENT_TIMESTAMP(3)) +ON DUPLICATE KEY UPDATE + target_app_name = VALUES(target_app_name), + subtitle = VALUES(subtitle), + icon_url = IF(icon_url = '' OR icon_url IS NULL, VALUES(icon_url), icon_url), + store_url = IF(store_url = '' OR store_url IS NULL, VALUES(store_url), store_url), + fallback_url = IF(fallback_url = '' OR fallback_url IS NULL, VALUES(fallback_url), fallback_url), + sort_order = VALUES(sort_order), + enabled = VALUES(enabled), + updated_at = CURRENT_TIMESTAMP(3);