feat(client): 接入同开发者应用墙 API 与管理端 CRUD
合并此前 stash 的 developer-apps:免登录列表、popadmin 管理与 0027 迁移。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2e7735f103
commit
c58b60419e
@ -2895,7 +2895,7 @@ const pages = {
|
||||
|
||||
$('content').innerHTML = `
|
||||
|
||||
<div class="note">同开发者应用(火花)· 客户端 <code>GET /api/client/pop/developer-apps</code></div>
|
||||
<div class="note">同开发者应用(火花)· 客户端 <code>GET /v1/developer-apps</code>(免登录)· 别名 <code>GET /api/client/pop/developer-apps</code></div>
|
||||
|
||||
<div class="toolbar" style="flex-wrap:wrap">
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
159
internal/module/client/logic/developer_apps.go
Normal file
159
internal/module/client/logic/developer_apps.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
51
migrations/0027_developer_apps.sql
Normal file
51
migrations/0027_developer_apps.sql
Normal file
@ -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);
|
||||
Loading…
Reference in New Issue
Block a user