feat:增加分享页下载
This commit is contained in:
parent
5625f6ac28
commit
fbf3e6c63c
93
backend/gateway/controller/app_download_controller.go
Normal file
93
backend/gateway/controller/app_download_controller.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
"github.com/topfans/backend/gateway/pkg/response"
|
||||||
|
"github.com/topfans/backend/gateway/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DownloadUrlResponse 下载页公开接口响应
|
||||||
|
type DownloadUrlResponse struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppDownloadSvc AppDownloadController 依赖的 service 接口
|
||||||
|
type AppDownloadSvc interface {
|
||||||
|
GetAllNativeApp(ctx context.Context) ([]models.AppDownloadConfig, error)
|
||||||
|
SyncVersion(ctx context.Context, req *service.SyncVersionRequest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppDownloadController App下载页控制器
|
||||||
|
// 注意:不走 Dubbo RPC,不依赖 Dubbo client,因此构造函数无需返回 error
|
||||||
|
type AppDownloadController struct {
|
||||||
|
svc AppDownloadSvc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppDownloadController 构造函数
|
||||||
|
func NewAppDownloadController(svc AppDownloadSvc) *AppDownloadController {
|
||||||
|
return &AppDownloadController{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDownloadUrls GET /api/v1/app/download-urls
|
||||||
|
// @Summary 获取 App 下载地址
|
||||||
|
// @Description 返回 Android/iOS 最新 native_app 下载地址,供 HTML 分享页调用
|
||||||
|
// @Tags App下载页
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response{data=map[string]DownloadUrlResponse}
|
||||||
|
// @Router /api/v1/app/download-urls [get]
|
||||||
|
func (ctrl *AppDownloadController) GetDownloadUrls(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
configs, err := ctrl.svc.GetAllNativeApp(ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, "获取下载地址失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装响应:map[platform] -> {url, version, type}
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
for _, cfg := range configs {
|
||||||
|
data[cfg.Platform] = &DownloadUrlResponse{
|
||||||
|
URL: cfg.DownloadURL,
|
||||||
|
Version: cfg.Version,
|
||||||
|
Type: cfg.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 确保 android/ios 键始终存在
|
||||||
|
for _, p := range []string{"android", "ios"} {
|
||||||
|
if _, ok := data[p]; !ok {
|
||||||
|
data[p] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncVersion POST /api/v1/admin/app/versions/sync
|
||||||
|
// @Summary 同步 App 下载地址
|
||||||
|
// @Description 接收 uniCloud 云函数推送的最新版本下载地址(内部接口,走 Nginx IP 白名单)
|
||||||
|
// @Tags Admin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body service.SyncVersionRequest true "版本信息"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /api/v1/admin/app/versions/sync [post]
|
||||||
|
func (ctrl *AppDownloadController) SyncVersion(c *gin.Context) {
|
||||||
|
var req service.SyncVersionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
if err := ctrl.svc.SyncVersion(ctx, &req); err != nil {
|
||||||
|
response.InternalError(c, "同步版本信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
169
backend/gateway/controller/app_download_controller_test.go
Normal file
169
backend/gateway/controller/app_download_controller_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
"github.com/topfans/backend/gateway/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeAppDownloadSvc 是 AppDownloadSvc 的 in-memory fake
|
||||||
|
type fakeAppDownloadSvc struct {
|
||||||
|
configs []models.AppDownloadConfig
|
||||||
|
getAllErr error
|
||||||
|
syncErr error
|
||||||
|
syncCalled bool
|
||||||
|
lastSyncReq *service.SyncVersionRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeAppDownloadSvc) GetAllNativeApp(ctx context.Context) ([]models.AppDownloadConfig, error) {
|
||||||
|
if f.getAllErr != nil {
|
||||||
|
return nil, f.getAllErr
|
||||||
|
}
|
||||||
|
return f.configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeAppDownloadSvc) SyncVersion(ctx context.Context, req *service.SyncVersionRequest) error {
|
||||||
|
f.syncCalled = true
|
||||||
|
f.lastSyncReq = req
|
||||||
|
return f.syncErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestRouter(ctrl *AppDownloadController) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.GET("/api/v1/app/download-urls", ctrl.GetDownloadUrls)
|
||||||
|
r.POST("/api/v1/admin/app/versions/sync", ctrl.SyncVersion)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== GET /api/v1/app/download-urls ==========
|
||||||
|
|
||||||
|
func TestGetDownloadUrls_Success(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{
|
||||||
|
configs: []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://app.apk", Version: "1.0.5"},
|
||||||
|
{Platform: "ios", Type: "native_app", DownloadURL: "https://apps.apple.com/...", Version: "1.0.5"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/app/download-urls", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
|
assert.Equal(t, float64(0), resp["code"])
|
||||||
|
|
||||||
|
data := resp["data"].(map[string]interface{})
|
||||||
|
assert.NotNil(t, data["android"])
|
||||||
|
assert.NotNil(t, data["ios"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDownloadUrls_EmptyData(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{configs: []models.AppDownloadConfig{}}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/app/download-urls", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
|
||||||
|
var resp map[string]interface{}
|
||||||
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
|
assert.Equal(t, float64(0), resp["code"])
|
||||||
|
|
||||||
|
data := resp["data"].(map[string]interface{})
|
||||||
|
assert.Nil(t, data["android"])
|
||||||
|
assert.Nil(t, data["ios"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDownloadUrls_ServiceError(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{getAllErr: errors.New("db down")}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/app/download-urls", nil)
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 500, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== POST /api/v1/admin/app/versions/sync ==========
|
||||||
|
|
||||||
|
func TestSyncVersion_InvalidJSON(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/app/versions/sync",
|
||||||
|
strings.NewReader(`{"android": "not_an_object"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 400, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_MissingRequiredField(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
// 缺少必填字段 url
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/app/versions/sync",
|
||||||
|
strings.NewReader(`{"android": {"version": "1.0.5", "type": "native_app"}}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 400, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_Success(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
fakeSvc := &fakeAppDownloadSvc{}
|
||||||
|
ctrl := NewAppDownloadController(fakeSvc)
|
||||||
|
router := setupTestRouter(ctrl)
|
||||||
|
|
||||||
|
body := `{
|
||||||
|
"android": {"url": "https://app.apk", "version": "1.0.5", "type": "native_app"},
|
||||||
|
"ios": {"url": "https://apps.apple.com/...", "version": "1.0.5", "type": "native_app"}
|
||||||
|
}`
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/app/versions/sync",
|
||||||
|
strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.True(t, fakeSvc.syncCalled)
|
||||||
|
assert.NotNil(t, fakeSvc.lastSyncReq.Android)
|
||||||
|
assert.NotNil(t, fakeSvc.lastSyncReq.IOS)
|
||||||
|
}
|
||||||
@ -62,6 +62,8 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
@ -108,6 +110,7 @@ require (
|
|||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@ -125,6 +128,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polarismesh/polaris-go v1.3.0 // indirect
|
github.com/polarismesh/polaris-go v1.3.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||||
@ -133,14 +137,16 @@ require (
|
|||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.52.0 // indirect
|
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.22.2 // indirect
|
github.com/shirou/gopsutil/v3 v3.22.2 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/afero v1.6.0 // indirect
|
github.com/spf13/afero v1.6.0 // indirect
|
||||||
github.com/spf13/cast v1.3.1 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/spf13/viper v1.8.1 // indirect
|
github.com/spf13/viper v1.8.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
@ -175,7 +181,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/time v0.1.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||||
@ -184,6 +190,11 @@ require (
|
|||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gorm.io/driver/postgres v1.6.0 // indirect
|
gorm.io/driver/postgres v1.6.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/topfans/backend => ../
|
replace github.com/topfans/backend => ../
|
||||||
|
|||||||
@ -82,8 +82,10 @@ github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9Q
|
|||||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||||
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
||||||
|
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||||
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
||||||
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
|
||||||
@ -95,6 +97,7 @@ github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7/go.mod h1:oDg1j4kFxnh
|
|||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||||
github.com/aliyun/credentials-go v1.4.12 h1:7D8eXGotNwthZuUEgAMgBoqxmIHwfaPVwW+/04LIJSQ=
|
github.com/aliyun/credentials-go v1.4.12 h1:7D8eXGotNwthZuUEgAMgBoqxmIHwfaPVwW+/04LIJSQ=
|
||||||
|
github.com/aliyun/credentials-go v1.4.12/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/apache/dubbo-getty v1.4.10 h1:ZmkpHJa/qgS0evX2tTNqNCz6rClI/9Wwp7ctyMml82w=
|
github.com/apache/dubbo-getty v1.4.10 h1:ZmkpHJa/qgS0evX2tTNqNCz6rClI/9Wwp7ctyMml82w=
|
||||||
github.com/apache/dubbo-getty v1.4.10/go.mod h1:V64WqLIxksEgNu5aBJBOxNIvpOZyfUJ7J/DXBlKSUoA=
|
github.com/apache/dubbo-getty v1.4.10/go.mod h1:V64WqLIxksEgNu5aBJBOxNIvpOZyfUJ7J/DXBlKSUoA=
|
||||||
@ -160,6 +163,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
|||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
@ -275,6 +279,10 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
|||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@ -402,6 +410,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -515,14 +524,20 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod
|
|||||||
github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY=
|
github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY=
|
||||||
github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y=
|
github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
|
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
|
||||||
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
||||||
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
@ -614,6 +629,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
@ -772,6 +789,10 @@ github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47p
|
|||||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||||
|
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
@ -814,6 +835,7 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
|
|||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
@ -1006,7 +1028,11 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@ -1052,6 +1078,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -1110,7 +1139,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@ -1140,6 +1173,9 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -1232,14 +1268,23 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -1253,7 +1298,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -1265,6 +1313,7 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -1330,6 +1379,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -1520,7 +1571,11 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@ -1530,6 +1585,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||||
|
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
45
backend/gateway/repository/app_download_repository.go
Normal file
45
backend/gateway/repository/app_download_repository.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppDownloadRepository 下载配置数据访问层
|
||||||
|
type AppDownloadRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppDownloadRepository 构造函数
|
||||||
|
func NewAppDownloadRepository(db *gorm.DB) *AppDownloadRepository {
|
||||||
|
return &AppDownloadRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByType 按包类型查询(用于公开接口,只返回 native_app)
|
||||||
|
func (r *AppDownloadRepository) FindByType(ctx context.Context, pkgType string) ([]models.AppDownloadConfig, error) {
|
||||||
|
var configs []models.AppDownloadConfig
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("type = ?", pkgType).
|
||||||
|
Find(&configs).Error
|
||||||
|
return configs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertAll 批量 upsert(platform + type 唯一)
|
||||||
|
func (r *AppDownloadRepository) UpsertAll(ctx context.Context, configs []models.AppDownloadConfig) error {
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, cfg := range configs {
|
||||||
|
if err := tx.Where("platform = ? AND type = ?", cfg.Platform, cfg.Type).
|
||||||
|
Assign(map[string]interface{}{
|
||||||
|
"download_url": cfg.DownloadURL,
|
||||||
|
"version": cfg.Version,
|
||||||
|
"updated_at": cfg.UpdatedAt,
|
||||||
|
}).
|
||||||
|
FirstOrCreate(&cfg).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
106
backend/gateway/repository/app_download_repository_test.go
Normal file
106
backend/gateway/repository/app_download_repository_test.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = db.AutoMigrate(&models.AppDownloadConfig{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByType_NativeApp(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
// seed: 一条 native_app + 一条 wgt
|
||||||
|
db.Create(&models.AppDownloadConfig{
|
||||||
|
Platform: "android", Type: "native_app", DownloadURL: "https://cdn.example.com/app.apk", Version: "1.0.5",
|
||||||
|
})
|
||||||
|
db.Create(&models.AppDownloadConfig{
|
||||||
|
Platform: "android", Type: "wgt", DownloadURL: "https://cdn.example.com/wgt.wgt", Version: "1.0.5",
|
||||||
|
})
|
||||||
|
|
||||||
|
configs, err := repo.FindByType(context.Background(), models.AppPackageTypeNativeApp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 1)
|
||||||
|
assert.Equal(t, "native_app", configs[0].Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByType_Wgt(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
db.Create(&models.AppDownloadConfig{
|
||||||
|
Platform: "android", Type: "wgt", DownloadURL: "https://cdn.example.com/wgt.wgt", Version: "1.0.5",
|
||||||
|
})
|
||||||
|
|
||||||
|
configs, err := repo.FindByType(context.Background(), models.AppPackageTypeWgt)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 1)
|
||||||
|
assert.Equal(t, "wgt", configs[0].Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpsertAll_Insert(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
err := repo.UpsertAll(context.Background(), []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://v1.apk", Version: "1.0.0"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.AppDownloadConfig{}).Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
|
||||||
|
var cfg models.AppDownloadConfig
|
||||||
|
db.First(&cfg)
|
||||||
|
assert.Equal(t, "https://v1.apk", cfg.DownloadURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpsertAll_Update(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
// 首次插入
|
||||||
|
err := repo.UpsertAll(context.Background(), []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://v1.apk", Version: "1.0.0"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 更新同一条
|
||||||
|
err = repo.UpsertAll(context.Background(), []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://v2.apk", Version: "2.0.0"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证只有 1 条且已更新
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.AppDownloadConfig{}).Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
|
||||||
|
var cfg models.AppDownloadConfig
|
||||||
|
db.First(&cfg)
|
||||||
|
assert.Equal(t, "https://v2.apk", cfg.DownloadURL)
|
||||||
|
assert.Equal(t, "2.0.0", cfg.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByType_Empty(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
configs, err := repo.FindByType(context.Background(), models.AppPackageTypeNativeApp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 0)
|
||||||
|
}
|
||||||
@ -11,7 +11,9 @@ import (
|
|||||||
"github.com/topfans/backend/gateway/config"
|
"github.com/topfans/backend/gateway/config"
|
||||||
"github.com/topfans/backend/gateway/controller"
|
"github.com/topfans/backend/gateway/controller"
|
||||||
"github.com/topfans/backend/gateway/middleware"
|
"github.com/topfans/backend/gateway/middleware"
|
||||||
|
"github.com/topfans/backend/gateway/repository"
|
||||||
"github.com/topfans/backend/gateway/service"
|
"github.com/topfans/backend/gateway/service"
|
||||||
|
"github.com/topfans/backend/pkg/database"
|
||||||
"github.com/topfans/backend/gateway/socket"
|
"github.com/topfans/backend/gateway/socket"
|
||||||
pbModeration "github.com/topfans/backend/pkg/proto/moderation"
|
pbModeration "github.com/topfans/backend/pkg/proto/moderation"
|
||||||
)
|
)
|
||||||
@ -126,6 +128,11 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
|||||||
modCtrl := controller.NewModerationController()
|
modCtrl := controller.NewModerationController()
|
||||||
modCtrl.SetService(modSvc)
|
modCtrl.SetService(modSvc)
|
||||||
|
|
||||||
|
// App 下载页 — gateway 直连 PG(无 Dubbo RPC)
|
||||||
|
appDownloadRepo := repository.NewAppDownloadRepository(database.GetDB())
|
||||||
|
appDownloadSvc := service.NewAppDownloadService(appDownloadRepo)
|
||||||
|
appDownloadCtrl := controller.NewAppDownloadController(appDownloadSvc)
|
||||||
|
|
||||||
// API v1 路由组
|
// API v1 路由组
|
||||||
v1 := r.Group("/api/v1")
|
v1 := r.Group("/api/v1")
|
||||||
{
|
{
|
||||||
@ -282,6 +289,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
|||||||
admin := v1.Group("/admin")
|
admin := v1.Group("/admin")
|
||||||
{
|
{
|
||||||
admin.POST("/notifications", notificationCtrl.AdminCreateNotification) // 批量发送通知
|
admin.POST("/notifications", notificationCtrl.AdminCreateNotification) // 批量发送通知
|
||||||
|
admin.POST("/app/versions/sync", appDownloadCtrl.SyncVersion) // App 下载页 — 版本同步
|
||||||
}
|
}
|
||||||
|
|
||||||
// 人像抠图(镭射卡 thinking 阶段;密钥仅服务端)
|
// 人像抠图(镭射卡 thinking 阶段;密钥仅服务端)
|
||||||
@ -290,6 +298,9 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
|||||||
// 人像抠图 — Dify 工作流调用(内网专用,JSON body { image_url, scene },无需 JWT)
|
// 人像抠图 — Dify 工作流调用(内网专用,JSON body { image_url, scene },无需 JWT)
|
||||||
v1.POST("/segment/json", segmentCtrl.PortraitByURL)
|
v1.POST("/segment/json", segmentCtrl.PortraitByURL)
|
||||||
|
|
||||||
|
// App 下载页 — HTML 分享页使用(公开,无需认证)
|
||||||
|
v1.GET("/app/download-urls", appDownloadCtrl.GetDownloadUrls)
|
||||||
|
|
||||||
// 镭射卡 AI 生成(Dify 工作流编排)
|
// 镭射卡 AI 生成(Dify 工作流编排)
|
||||||
laser := v1.Group("/laser")
|
laser := v1.Group("/laser")
|
||||||
laser.Use(middleware.AuthMiddleware())
|
laser.Use(middleware.AuthMiddleware())
|
||||||
|
|||||||
71
backend/gateway/service/app_download_service.go
Normal file
71
backend/gateway/service/app_download_service.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/topfans/backend/gateway/repository"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncVersionRequest uniCloud 同步请求体
|
||||||
|
// 注:定义在 service 包(而非 controller 包),避免 controller ↔ service 循环依赖
|
||||||
|
type SyncVersionRequest struct {
|
||||||
|
Android *PlatformVersionInfo `json:"android"`
|
||||||
|
IOS *PlatformVersionInfo `json:"ios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlatformVersionInfo 单个平台的版本信息
|
||||||
|
type PlatformVersionInfo struct {
|
||||||
|
URL string `json:"url" binding:"required"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type" binding:"required,oneof=native_app wgt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// appDownloadRepo 下载配置仓库接口(仅暴露 service 层需要的两个方法)
|
||||||
|
type appDownloadRepo interface {
|
||||||
|
FindByType(ctx context.Context, pkgType string) ([]models.AppDownloadConfig, error)
|
||||||
|
UpsertAll(ctx context.Context, configs []models.AppDownloadConfig) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppDownloadService App下载页业务逻辑
|
||||||
|
type AppDownloadService struct {
|
||||||
|
repo appDownloadRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppDownloadService 构造函数
|
||||||
|
func NewAppDownloadService(repo *repository.AppDownloadRepository) *AppDownloadService {
|
||||||
|
return &AppDownloadService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllNativeApp 获取所有 native_app 类型的下载配置
|
||||||
|
func (s *AppDownloadService) GetAllNativeApp(ctx context.Context) ([]models.AppDownloadConfig, error) {
|
||||||
|
return s.repo.FindByType(ctx, models.AppPackageTypeNativeApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncVersion 同步版本信息(upsert)
|
||||||
|
func (s *AppDownloadService) SyncVersion(ctx context.Context, req *SyncVersionRequest) error {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
var configs []models.AppDownloadConfig
|
||||||
|
|
||||||
|
if req.Android != nil {
|
||||||
|
configs = append(configs, models.AppDownloadConfig{
|
||||||
|
Platform: "android",
|
||||||
|
Type: req.Android.Type,
|
||||||
|
DownloadURL: req.Android.URL,
|
||||||
|
Version: req.Android.Version,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if req.IOS != nil {
|
||||||
|
configs = append(configs, models.AppDownloadConfig{
|
||||||
|
Platform: "ios",
|
||||||
|
Type: req.IOS.Type,
|
||||||
|
DownloadURL: req.IOS.URL,
|
||||||
|
Version: req.IOS.Version,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.UpsertAll(ctx, configs)
|
||||||
|
}
|
||||||
110
backend/gateway/service/app_download_service_test.go
Normal file
110
backend/gateway/service/app_download_service_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeRepo 是 appDownloadRepo 的 in-memory fake 实现
|
||||||
|
type fakeRepo struct {
|
||||||
|
configs []models.AppDownloadConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRepo) FindByType(ctx context.Context, pkgType string) ([]models.AppDownloadConfig, error) {
|
||||||
|
var result []models.AppDownloadConfig
|
||||||
|
for _, c := range f.configs {
|
||||||
|
if c.Type == pkgType {
|
||||||
|
result = append(result, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRepo) UpsertAll(ctx context.Context, configs []models.AppDownloadConfig) error {
|
||||||
|
for _, incoming := range configs {
|
||||||
|
found := false
|
||||||
|
for i, existing := range f.configs {
|
||||||
|
if existing.Platform == incoming.Platform && existing.Type == incoming.Type {
|
||||||
|
f.configs[i].DownloadURL = incoming.DownloadURL
|
||||||
|
f.configs[i].Version = incoming.Version
|
||||||
|
f.configs[i].UpdatedAt = incoming.UpdatedAt
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
f.configs = append(f.configs, incoming)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAllNativeApp_FiltersWgt(t *testing.T) {
|
||||||
|
svc := &AppDownloadService{
|
||||||
|
repo: &fakeRepo{configs: []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://app.apk"},
|
||||||
|
{Platform: "android", Type: "wgt", DownloadURL: "https://wgt.wgt"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := svc.GetAllNativeApp(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 1)
|
||||||
|
assert.Equal(t, "native_app", configs[0].Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAllNativeApp_Empty(t *testing.T) {
|
||||||
|
svc := &AppDownloadService{
|
||||||
|
repo: &fakeRepo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := svc.GetAllNativeApp(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_BothPlatforms(t *testing.T) {
|
||||||
|
fake := &fakeRepo{}
|
||||||
|
svc := &AppDownloadService{repo: fake}
|
||||||
|
|
||||||
|
err := svc.SyncVersion(context.Background(), &SyncVersionRequest{
|
||||||
|
Android: &PlatformVersionInfo{URL: "https://a.apk", Version: "1.0.5", Type: "native_app"},
|
||||||
|
IOS: &PlatformVersionInfo{URL: "https://apps.apple.com/...", Version: "1.0.5", Type: "native_app"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, fake.configs, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_PartialUpdate(t *testing.T) {
|
||||||
|
fake := &fakeRepo{
|
||||||
|
configs: []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://old.apk", Version: "1.0.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := &AppDownloadService{repo: fake}
|
||||||
|
|
||||||
|
// 只更新 android,不传 ios
|
||||||
|
err := svc.SyncVersion(context.Background(), &SyncVersionRequest{
|
||||||
|
Android: &PlatformVersionInfo{URL: "https://new.apk", Version: "2.0.0", Type: "native_app"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, fake.configs, 1) // 仍然是 1 条
|
||||||
|
assert.Equal(t, "https://new.apk", fake.configs[0].DownloadURL)
|
||||||
|
assert.Equal(t, "2.0.0", fake.configs[0].Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_NilPlatform(t *testing.T) {
|
||||||
|
fake := &fakeRepo{}
|
||||||
|
svc := &AppDownloadService{repo: fake}
|
||||||
|
|
||||||
|
// 只传 iOS,android 为 nil
|
||||||
|
err := svc.SyncVersion(context.Background(), &SyncVersionRequest{
|
||||||
|
IOS: &PlatformVersionInfo{URL: "https://apps.apple.com/...", Version: "1.0.5", Type: "native_app"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, fake.configs, 1)
|
||||||
|
assert.Equal(t, "ios", fake.configs[0].Platform)
|
||||||
|
}
|
||||||
24
backend/migrations/2026_07_08_001_app_download_configs.sql
Normal file
24
backend/migrations/2026_07_08_001_app_download_configs.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-- App 下载页配置表
|
||||||
|
-- 存储各平台最新下载地址,由 uniCloud 云函数自动同步
|
||||||
|
-- type 区分 native_app(整包安装)和 wgt(热更新资源包)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.app_download_configs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
platform VARCHAR(20) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL DEFAULT 'native_app',
|
||||||
|
download_url TEXT NOT NULL,
|
||||||
|
version VARCHAR(50),
|
||||||
|
created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
|
||||||
|
updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
|
||||||
|
|
||||||
|
CONSTRAINT uq_app_download_configs_platform_type UNIQUE (platform, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE public.app_download_configs IS 'App 下载页配置,存储各平台最新下载地址';
|
||||||
|
COMMENT ON COLUMN public.app_download_configs.platform IS '平台标识:android / ios';
|
||||||
|
COMMENT ON COLUMN public.app_download_configs.type IS '包类型:native_app(整包安装)/ wgt(热更新资源包)';
|
||||||
|
COMMENT ON COLUMN public.app_download_configs.download_url IS '下载地址或 App Store 链接';
|
||||||
|
COMMENT ON COLUMN public.app_download_configs.version IS '版本号,如 1.0.5';
|
||||||
|
|
||||||
|
-- 预留序列起始值(按项目规范)
|
||||||
|
ALTER SEQUENCE public.app_download_configs_id_seq RESTART WITH 10000;
|
||||||
23
backend/pkg/models/app_download_config.go
Normal file
23
backend/pkg/models/app_download_config.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// AppDownloadConfig App下载页配置表模型
|
||||||
|
type AppDownloadConfig struct {
|
||||||
|
ID int64 `gorm:"primaryKey;autoIncrement;column:id" json:"-"`
|
||||||
|
Platform string `gorm:"type:varchar(20);uniqueIndex:uq_app_download_configs_platform_type;not null;column:platform" json:"platform"`
|
||||||
|
Type string `gorm:"type:varchar(20);uniqueIndex:uq_app_download_configs_platform_type;not null;default:native_app;column:type" json:"type"`
|
||||||
|
DownloadURL string `gorm:"type:text;not null;column:download_url" json:"download_url"`
|
||||||
|
Version string `gorm:"type:varchar(50);column:version" json:"version"`
|
||||||
|
CreatedAt int64 `gorm:"not null;column:created_at" json:"created_at"`
|
||||||
|
UpdatedAt int64 `gorm:"not null;column:updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (AppDownloadConfig) TableName() string {
|
||||||
|
return "app_download_configs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包类型常量
|
||||||
|
const (
|
||||||
|
AppPackageTypeNativeApp = "native_app" // 整包安装
|
||||||
|
AppPackageTypeWgt = "wgt" // 热更新资源包
|
||||||
|
)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# App 下载分享页 — 自动同步方案设计
|
# App 下载分享页 — 自动同步方案设计
|
||||||
|
|
||||||
> **文档状态**:Draft · 2026-07-08
|
> **文档状态**:Draft v1.1 · 2026-07-08(修订 2026-07-09)
|
||||||
> **适用版本**:topfans v1.0.5+
|
> **适用版本**:topfans v1.0.5+
|
||||||
> **目标读者**:后端 / uni-admin / 前端开发
|
> **目标读者**:后端 / uni-admin / 前端开发
|
||||||
|
|
||||||
@ -310,24 +310,13 @@ gateway/
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/topfans/backend/gateway/pkg/response"
|
||||||
"github.com/topfans/backend/gateway/service"
|
"github.com/topfans/backend/gateway/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncVersionRequest uniCloud 同步请求体
|
// SyncVersionRequest uniCloud 同步请求体(定义在 service 包,此处引用)
|
||||||
type SyncVersionRequest struct {
|
// 见 service/app_download_service.go
|
||||||
Android *PlatformVersionInfo `json:"android"`
|
|
||||||
IOS *PlatformVersionInfo `json:"ios"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlatformVersionInfo 单个平台的版本信息
|
|
||||||
type PlatformVersionInfo struct {
|
|
||||||
URL string `json:"url" binding:"required"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Type string `json:"type" binding:"required,oneof=native_app wgt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadUrlResponse 下载页公开接口响应
|
// DownloadUrlResponse 下载页公开接口响应
|
||||||
type DownloadUrlResponse struct {
|
type DownloadUrlResponse struct {
|
||||||
@ -337,6 +326,8 @@ type DownloadUrlResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AppDownloadController App下载页控制器
|
// AppDownloadController App下载页控制器
|
||||||
|
// 注意:不走 Dubbo RPC(§12.5),不依赖 Dubbo client,因此构造函数无需返回 error
|
||||||
|
// (与 NewSegmentController / NewLaserGenerateController 等无 Dubbo 依赖的 controller 一致)
|
||||||
type AppDownloadController struct {
|
type AppDownloadController struct {
|
||||||
svc *service.AppDownloadService
|
svc *service.AppDownloadService
|
||||||
}
|
}
|
||||||
@ -353,15 +344,18 @@ func NewAppDownloadController(svc *service.AppDownloadService) *AppDownloadContr
|
|||||||
// (续 app_download_controller.go)
|
// (续 app_download_controller.go)
|
||||||
|
|
||||||
// GetDownloadUrls GET /api/v1/app/download-urls
|
// GetDownloadUrls GET /api/v1/app/download-urls
|
||||||
|
// @Summary 获取 App 下载地址
|
||||||
|
// @Description 返回 Android/iOS 最新 native_app 下载地址,供 HTML 分享页调用
|
||||||
|
// @Tags App下载页
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response{data=map[string]DownloadUrlResponse}
|
||||||
|
// @Router /api/v1/app/download-urls [get]
|
||||||
// 只返回 type = native_app 的记录
|
// 只返回 type = native_app 的记录
|
||||||
func (ctrl *AppDownloadController) GetDownloadUrls(c *gin.Context) {
|
func (ctrl *AppDownloadController) GetDownloadUrls(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
configs, err := ctrl.svc.GetAllNativeApp(ctx)
|
configs, err := ctrl.svc.GetAllNativeApp(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
response.InternalError(c, "获取下载地址失败")
|
||||||
"code": 500,
|
|
||||||
"message": "internal error",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,30 +375,32 @@ func (ctrl *AppDownloadController) GetDownloadUrls(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": data})
|
response.Success(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncVersion POST /api/v1/admin/app/versions/sync
|
// SyncVersion POST /api/v1/admin/app/versions/sync
|
||||||
|
// @Summary 同步 App 下载地址
|
||||||
|
// @Description 接收 uniCloud 云函数推送的最新版本下载地址(内部接口,走 Nginx IP 白名单)
|
||||||
|
// @Tags Admin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body SyncVersionRequest true "版本信息"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /api/v1/admin/app/versions/sync [post]
|
||||||
func (ctrl *AppDownloadController) SyncVersion(c *gin.Context) {
|
func (ctrl *AppDownloadController) SyncVersion(c *gin.Context) {
|
||||||
var req SyncVersionRequest
|
var req service.SyncVersionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
"code": 400,
|
|
||||||
"message": "invalid request: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
if err := ctrl.svc.SyncVersion(ctx, &req); err != nil {
|
if err := ctrl.svc.SyncVersion(ctx, &req); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
response.InternalError(c, "sync failed")
|
||||||
"code": 500,
|
|
||||||
"message": "sync failed",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
response.Success(c, nil)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -422,6 +418,20 @@ import (
|
|||||||
"github.com/topfans/backend/pkg/models"
|
"github.com/topfans/backend/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SyncVersionRequest uniCloud 同步请求体
|
||||||
|
// 注:定义在 service 包(而非 controller 包),避免 controller ↔ service 循环依赖
|
||||||
|
type SyncVersionRequest struct {
|
||||||
|
Android *PlatformVersionInfo `json:"android"`
|
||||||
|
IOS *PlatformVersionInfo `json:"ios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlatformVersionInfo 单个平台的版本信息
|
||||||
|
type PlatformVersionInfo struct {
|
||||||
|
URL string `json:"url" binding:"required"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type" binding:"required,oneof=native_app wgt"`
|
||||||
|
}
|
||||||
|
|
||||||
// AppDownloadService App下载页业务逻辑
|
// AppDownloadService App下载页业务逻辑
|
||||||
type AppDownloadService struct {
|
type AppDownloadService struct {
|
||||||
repo *repository.AppDownloadRepository
|
repo *repository.AppDownloadRepository
|
||||||
@ -518,34 +528,56 @@ func (r *AppDownloadRepository) UpsertAll(ctx context.Context, configs []models.
|
|||||||
|
|
||||||
### 4.5 路由注册
|
### 4.5 路由注册
|
||||||
|
|
||||||
在 `router.go` 的 `v1` 路由组中添加:
|
在 `router.go` 的 `SetupRouter` 函数中,按以下方式集成。
|
||||||
|
|
||||||
|
**Step 1 — 创建 controller**(在现有 controller 初始化代码块末尾添加):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ============ App 下载页接口 ============
|
// App 下载页 — gateway 直连 PG(无 Dubbo RPC,参见 §12.5)
|
||||||
|
appDownloadRepo := repository.NewAppDownloadRepository(database.GetDB())
|
||||||
// 公开接口 — HTML 分享页使用
|
|
||||||
v1.GET("/app/download-urls", appDownloadCtrl.GetDownloadUrls)
|
|
||||||
|
|
||||||
// Admin 内部接口 — uniCloud 云函数同步使用
|
|
||||||
admin := v1.Group("/admin")
|
|
||||||
{
|
|
||||||
admin.POST("/notifications", notificationCtrl.AdminCreateNotification)
|
|
||||||
admin.POST("/app/versions/sync", appDownloadCtrl.SyncVersion) // ← 新增
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.6 main.go 装配(依赖注入)
|
|
||||||
|
|
||||||
在 `gateway/main.go` 中添加:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// App 下载页 — 三层装配
|
|
||||||
appDownloadRepo := repository.NewAppDownloadRepository(db)
|
|
||||||
appDownloadSvc := service.NewAppDownloadService(appDownloadRepo)
|
appDownloadSvc := service.NewAppDownloadService(appDownloadRepo)
|
||||||
appDownloadCtrl := controller.NewAppDownloadController(appDownloadSvc)
|
appDownloadCtrl := controller.NewAppDownloadController(appDownloadSvc)
|
||||||
```
|
```
|
||||||
|
|
||||||
> 如果 gateway 目前没有直连 PG(`db *gorm.DB`),需要先初始化一个只读连接。详见 §12.5。
|
> `database.GetDB()` 是项目已有的全局 PG 连接获取函数(`backend/pkg/database/database.go`),无需修改 `SetupRouter` 函数签名。
|
||||||
|
|
||||||
|
**Step 2 — 注册公开路由**(在 `v1` 路由组内,与其他公开路由并列添加):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// App 下载页 — HTML 分享页使用(公开,无需认证)
|
||||||
|
v1.GET("/app/download-urls", appDownloadCtrl.GetDownloadUrls)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3 — 注册 Admin 内部路由**(在现有 `admin := v1.Group("/admin")` 块内添加一行):
|
||||||
|
|
||||||
|
```go
|
||||||
|
admin := v1.Group("/admin")
|
||||||
|
{
|
||||||
|
admin.POST("/notifications", notificationCtrl.AdminCreateNotification) // 已有
|
||||||
|
admin.POST("/app/versions/sync", appDownloadCtrl.SyncVersion) // ← 新增
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **关键**:不要新建 admin group,在现有块内追加即可。现有 admin group 已在 `v1` 下、无鉴权中间件,依赖 Nginx IP 白名单保护。
|
||||||
|
|
||||||
|
### 4.6 依赖注入(装配)
|
||||||
|
|
||||||
|
三层装配在 `SetupRouter` 内部完成(与项目现有 controller 创建模式一致):
|
||||||
|
|
||||||
|
```
|
||||||
|
database.GetDB() ← 全局 PG 连接
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
repository.NewAppDownloadRepository ← 数据层
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
service.NewAppDownloadService ← 业务层
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
controller.NewAppDownloadController ← 控制器(注册路由)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 不需要在 `main.go` 中额外装配。`SetupRouter` 已接收所有 Dubbo client 参数并在内部创建 controller,本功能的 controller 也在此创建,无需修改 `main.go`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -1027,11 +1059,14 @@ backend/
|
|||||||
│ └── app_download_config.go # GORM model + 包类型常量
|
│ └── app_download_config.go # GORM model + 包类型常量
|
||||||
├── gateway/
|
├── gateway/
|
||||||
│ ├── controller/
|
│ ├── controller/
|
||||||
│ │ └── app_download_controller.go # handler: DTO + 两个接口
|
│ │ ├── app_download_controller.go # handler: DTO + 两个接口(含 Swagger 注解)
|
||||||
|
│ │ └── app_download_controller_test.go # controller 集成测试
|
||||||
│ ├── service/
|
│ ├── service/
|
||||||
│ │ └── app_download_service.go # 业务层: sync + query
|
│ │ ├── app_download_service.go # 业务层: sync + query(含请求 DTO)
|
||||||
|
│ │ └── app_download_service_test.go # service 单元测试
|
||||||
│ └── repository/
|
│ └── repository/
|
||||||
│ └── app_download_repository.go # 数据层: upsert + find
|
│ ├── app_download_repository.go # 数据层: upsert + find
|
||||||
|
│ └── app_download_repository_test.go # repository 单元测试
|
||||||
|
|
||||||
uni-admin/
|
uni-admin/
|
||||||
├── uniCloud-alipay/cloudfunctions/
|
├── uniCloud-alipay/cloudfunctions/
|
||||||
@ -1046,9 +1081,8 @@ frontend/static/html/
|
|||||||
### 修改文件
|
### 修改文件
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/gateway/router/router.go # 注册新路由
|
backend/gateway/router/router.go # 注册新路由 + 三层装配(§4.5)
|
||||||
backend/gateway/main.go # 装配依赖注入(或等效入口文件)
|
uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm 后触发同步(§5.3)
|
||||||
uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm 后触发同步
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -1062,12 +1096,15 @@ uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm
|
|||||||
| A1 | 编写 migration SQL | 15min | `2026_07_08_001_app_download_configs.sql` |
|
| A1 | 编写 migration SQL | 15min | `2026_07_08_001_app_download_configs.sql` |
|
||||||
| A2 | 编写 Go Model + 常量 | 10min | `app_download_config.go` |
|
| A2 | 编写 Go Model + 常量 | 10min | `app_download_config.go` |
|
||||||
| A3 | 编写 Repository(含 struct 定义 + 构造函数) | 20min | `app_download_repository.go` |
|
| A3 | 编写 Repository(含 struct 定义 + 构造函数) | 20min | `app_download_repository.go` |
|
||||||
| A4 | 编写 Service | 15min | `app_download_service.go` |
|
| A4 | 编写 Repository 单元测试(2 个用例) | 20min | `app_download_repository_test.go` |
|
||||||
| A5 | 编写 Controller + DTO + 构造函数 | 20min | `app_download_controller.go` |
|
| A5 | 编写 Service(含请求 DTO + 构造函数) | 20min | `app_download_service.go` |
|
||||||
| A6 | 注册路由 + main.go 装配 | 15min | `router.go` + `main.go` 修改 |
|
| A6 | 编写 Service 单元测试(2 个用例) | 20min | `app_download_service_test.go` |
|
||||||
| A7 | `go build` + 本地测试(curl 两条 API) | 15min | 验证 |
|
| A7 | 编写 Controller + DTO + Swagger 注解 | 25min | `app_download_controller.go` |
|
||||||
|
| A8 | 编写 Controller 集成测试(4 个用例) | 25min | `app_download_controller_test.go` |
|
||||||
|
| A9 | 在 `SetupRouter` 中三层装配 + 注册路由 | 15min | `router.go` 修改 |
|
||||||
|
| A10 | `swag init` + `go build` + 本地测试(curl 两条 API) | 15min | 验证 |
|
||||||
|
|
||||||
> **Phase A 小计**:~1h50min
|
> **Phase A 小计**:~3h05min(较初版增加 ~1h15min 测试时间)
|
||||||
|
|
||||||
### 阶段 B:uniCloud 自动同步(优先级 P0)
|
### 阶段 B:uniCloud 自动同步(优先级 P0)
|
||||||
|
|
||||||
@ -1098,7 +1135,7 @@ uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm
|
|||||||
|
|
||||||
> **Phase D 小计**:~20min
|
> **Phase D 小计**:~20min
|
||||||
|
|
||||||
### 总预估:~4h
|
### 总预估:~5h30min
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -1151,9 +1188,260 @@ uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## §13 自审清单(修订版)
|
## §13 测试策略
|
||||||
|
|
||||||
### 修改的章节:全部(§1-§12 重写)
|
### 13.1 测试范围
|
||||||
|
|
||||||
|
按 CLAUDE.md 接口开发规范,本功能测试覆盖如下:
|
||||||
|
|
||||||
|
| 层级 | 测试类型 | 覆盖内容 | 工具 |
|
||||||
|
|------|---------|---------|------|
|
||||||
|
| Repository | 单元测试 | `FindByType` / `UpsertAll` 的 SQL 正确性 | `go test` + SQLite 内存库 或 PG 测试容器 |
|
||||||
|
| Service | 单元测试 | `GetAllNativeApp` 返回过滤 / `SyncVersion` upsert 逻辑 | `go test` + mock repository |
|
||||||
|
| Controller | 集成测试 | GET 公开接口 happy path + empty case;POST admin 接口参数校验 + 正常同步 | `go test` + `httptest` + mock service |
|
||||||
|
|
||||||
|
### 13.2 Repository 测试
|
||||||
|
|
||||||
|
**文件**:`backend/gateway/repository/app_download_repository_test.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) *gorm.DB {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
db.AutoMigrate(&models.AppDownloadConfig{})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindByType_NativeApp(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
// seed: 一条 native_app + 一条 wgt
|
||||||
|
db.Create(&models.AppDownloadConfig{
|
||||||
|
Platform: "android", Type: "native_app", DownloadURL: "https://cdn.example.com/app.apk", Version: "1.0.5",
|
||||||
|
})
|
||||||
|
db.Create(&models.AppDownloadConfig{
|
||||||
|
Platform: "android", Type: "wgt", DownloadURL: "https://cdn.example.com/wgt.wgt", Version: "1.0.5",
|
||||||
|
})
|
||||||
|
|
||||||
|
configs, err := repo.FindByType(context.Background(), models.AppPackageTypeNativeApp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 1)
|
||||||
|
assert.Equal(t, "native_app", configs[0].Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpsertAll_InsertAndUpdate(t *testing.T) {
|
||||||
|
db := setupTestDB(t)
|
||||||
|
repo := NewAppDownloadRepository(db)
|
||||||
|
|
||||||
|
// 首次插入
|
||||||
|
err := repo.UpsertAll(context.Background(), []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://v1.apk", Version: "1.0.0"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 更新同一条
|
||||||
|
err = repo.UpsertAll(context.Background(), []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://v2.apk", Version: "2.0.0"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证只有 1 条且已更新
|
||||||
|
var count int64
|
||||||
|
db.Model(&models.AppDownloadConfig{}).Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
|
||||||
|
var cfg models.AppDownloadConfig
|
||||||
|
db.First(&cfg)
|
||||||
|
assert.Equal(t, "https://v2.apk", cfg.DownloadURL)
|
||||||
|
assert.Equal(t, "2.0.0", cfg.Version)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.3 Service 测试
|
||||||
|
|
||||||
|
**文件**:`backend/gateway/service/app_download_service_test.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mock repository
|
||||||
|
type mockRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) FindByType(ctx context.Context, pkgType string) ([]models.AppDownloadConfig, error) {
|
||||||
|
args := m.Called(ctx, pkgType)
|
||||||
|
return args.Get(0).([]models.AppDownloadConfig), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRepo) UpsertAll(ctx context.Context, configs []models.AppDownloadConfig) error {
|
||||||
|
args := m.Called(ctx, configs)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAllNativeApp_ReturnsFiltered(t *testing.T) {
|
||||||
|
repo := new(mockRepo)
|
||||||
|
svc := NewAppDownloadService(repo) // 注:需要用 interface 替代具体类型,见下方说明
|
||||||
|
|
||||||
|
expected := []models.AppDownloadConfig{
|
||||||
|
{Platform: "android", Type: "native_app", DownloadURL: "https://cdn.example.com/app.apk"},
|
||||||
|
}
|
||||||
|
repo.On("FindByType", mock.Anything, models.AppPackageTypeNativeApp).Return(expected, nil)
|
||||||
|
|
||||||
|
configs, err := svc.GetAllNativeApp(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, configs, 1)
|
||||||
|
assert.Equal(t, "native_app", configs[0].Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_BothPlatforms(t *testing.T) {
|
||||||
|
repo := new(mockRepo)
|
||||||
|
svc := NewAppDownloadService(repo)
|
||||||
|
|
||||||
|
repo.On("UpsertAll", mock.Anything, mock.MatchedBy(func(configs []models.AppDownloadConfig) bool {
|
||||||
|
return len(configs) == 2
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
|
err := svc.SyncVersion(context.Background(), &SyncVersionRequest{
|
||||||
|
Android: &PlatformVersionInfo{URL: "https://a.apk", Version: "1.0.5", Type: "native_app"},
|
||||||
|
IOS: &PlatformVersionInfo{URL: "https://apps.apple.com/...", Version: "1.0.5", Type: "native_app"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:以上 mock 示例依赖具体 struct,生产代码建议将 `AppDownloadService.repo` 改为 interface 类型以支持 mock 注入。具体做法:在 `app_download_service.go` 中定义 `AppDownloadRepository interface { FindByType(...); UpsertAll(...) }`,`AppDownloadService` 持有该 interface。
|
||||||
|
|
||||||
|
### 13.4 Controller 测试
|
||||||
|
|
||||||
|
**文件**:`backend/gateway/controller/app_download_controller_test.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/topfans/backend/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注:与 service 层同理,controller 也应依赖 interface 以便注入 mock service
|
||||||
|
|
||||||
|
func TestGetDownloadUrls_Success(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
// mockSvc := new(MockAppDownloadService)
|
||||||
|
// mockSvc.On("GetAllNativeApp", ...).Return(...)
|
||||||
|
// ctrl := NewAppDownloadController(mockSvc)
|
||||||
|
|
||||||
|
// w := httptest.NewRecorder()
|
||||||
|
// req, _ := http.NewRequest("GET", "/api/v1/app/download-urls", nil)
|
||||||
|
// router := gin.New()
|
||||||
|
// router.GET("/api/v1/app/download-urls", ctrl.GetDownloadUrls)
|
||||||
|
// router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert.Equal(t, 200, w.Code)
|
||||||
|
// var resp map[string]interface{}
|
||||||
|
// json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
|
// assert.Equal(t, float64(0), resp["code"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDownloadUrls_EmptyData(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
// mockSvc := new(MockAppDownloadService)
|
||||||
|
// mockSvc.On("GetAllNativeApp", ...).Return([]models.AppDownloadConfig{}, nil)
|
||||||
|
// ... 验证 android/ios 键均为 null,HTTP 200
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_InvalidJSON(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
// ... 发送非法 JSON,验证 HTTP 400
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVersion_Success(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
// ... 发送合法 JSON,验证 HTTP 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Controller 测试骨架已给出。实际编写时注入 mock service 即可。
|
||||||
|
|
||||||
|
### 13.5 测试运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行全部 app_download 相关测试
|
||||||
|
cd backend/gateway
|
||||||
|
go test ./repository/ -run AppDownload -v
|
||||||
|
go test ./service/ -run AppDownload -v
|
||||||
|
go test ./controller/ -run AppDownload -v
|
||||||
|
|
||||||
|
# 带覆盖率
|
||||||
|
go test ./repository/ ./service/ ./controller/ -coverprofile=coverage.out
|
||||||
|
go tool cover -func=coverage.out | grep app_download
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §14 Swagger API 文档
|
||||||
|
|
||||||
|
### 14.1 注解位置
|
||||||
|
|
||||||
|
Swagger 注解已内嵌在 §4.4.2 Controller 代码中(`@Summary` / `@Tags` / `@Param` / `@Success` / `@Router`)。执行以下命令自动生成文档:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/gateway
|
||||||
|
swag init --parseDependency --parseInternal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.2 生成后的文档
|
||||||
|
|
||||||
|
生成后 Swagger UI 可在 `https://api.topfans.com/swagger/index.html` 查看:
|
||||||
|
|
||||||
|
| 接口 | Tags | 认证 |
|
||||||
|
|------|------|------|
|
||||||
|
| `GET /api/v1/app/download-urls` | App下载页 | 无 |
|
||||||
|
| `POST /api/v1/admin/app/versions/sync` | Admin | Nginx IP 白名单 |
|
||||||
|
|
||||||
|
### 14.3 错误码
|
||||||
|
|
||||||
|
| HTTP 状态码 | body.code | 说明 |
|
||||||
|
|------------|-----------|------|
|
||||||
|
| 200 | 0 | 成功 |
|
||||||
|
| 400 | 400 | 请求参数错误(`type` 不在枚举范围 / JSON 格式错误 / 必填字段缺失) |
|
||||||
|
| 500 | 500 | 服务器内部错误(PG 连接异常 / upsert 失败) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §15 自审清单(修订版)
|
||||||
|
|
||||||
|
### 修改的章节:§1-§14(全部重写 + 新增 §13 测试策略、§14 Swagger 文档)
|
||||||
|
|
||||||
### 未改动的章节:无
|
### 未改动的章节:无
|
||||||
|
|
||||||
@ -1165,22 +1453,36 @@ uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm
|
|||||||
- [x] §2.3 读取链路查询条件(`WHERE type = 'native_app'`)与 §4.4.4 repository 的 `FindByType` 一致
|
- [x] §2.3 读取链路查询条件(`WHERE type = 'native_app'`)与 §4.4.4 repository 的 `FindByType` 一致
|
||||||
- [x] §1 mermaid 图写 "HTTP POST" 与 §4.1 接口定义、§4.3 请求体一致
|
- [x] §1 mermaid 图写 "HTTP POST" 与 §4.1 接口定义、§4.3 请求体一致
|
||||||
- [x] §1 核心架构图与 §3.1 表结构一致(含 `type` 字段)
|
- [x] §1 核心架构图与 §3.1 表结构一致(含 `type` 字段)
|
||||||
- [x] §4.4.1 DTO `PlatformVersionInfo` 结构(URL + Version + Type)与 §5.1 云函数 payload 一致
|
- [x] §4.4.1 DTO `DownloadUrlResponse` 结构与 §4.4.2 Controller 组装逻辑一致
|
||||||
|
- [x] §4.4.3 Service `SyncVersionRequest` / `PlatformVersionInfo` 与 §4.4.2 Controller 引用一致(`service.SyncVersionRequest`)
|
||||||
|
- [x] §4.4.3 Service 请求 DTO 与 §5.1 云函数 payload 一致(URL + Version + Type)
|
||||||
- [x] §5.1 云函数使用 `process.env.BACKEND_URL` 与 §5.2 注释、§9.2 部署步骤一致(均为环境变量方式)
|
- [x] §5.1 云函数使用 `process.env.BACKEND_URL` 与 §5.2 注释、§9.2 部署步骤一致(均为环境变量方式)
|
||||||
- [x] §10 文件清单与 §4.4 分层目录、§3.2 model 路径、§7.1 HTML 路径一致
|
- [x] §10 文件清单与 §4.4 分层目录、§3.2 model 路径、§7.1 HTML 路径、§13 测试文件一致
|
||||||
- [x] §11 实施步骤中的文件名与 §10 文件清单一致
|
- [x] §11 实施步骤中的文件名与 §10 文件清单一致(含测试文件)
|
||||||
- [x] §8.2 异常恢复不再依赖已删除的独立同步页(§6),改用编辑重保存 + 列表按钮
|
- [x] §8.2 异常恢复不再依赖已删除的独立同步页(§6),改用编辑重保存 + 列表按钮
|
||||||
- [x] §4.4 架构说明指向 §12.5(gateway 直连 PG 的决策理由)
|
- [x] §4.4 架构说明指向 §12.5(gateway 直连 PG 的决策理由)
|
||||||
|
- [x] §14 Swagger 注解与 §4.4.2 Controller 代码内嵌的 `@Summary` / `@Tags` / `@Router` 一致
|
||||||
|
- [x] §13 测试文件路径与 §10 文件清单一致
|
||||||
|
|
||||||
### Go 代码完整性
|
### Go 代码完整性
|
||||||
|
|
||||||
- [x] DTO `SyncVersionRequest` / `PlatformVersionInfo` / `DownloadUrlResponse` 有完整 struct 定义(§4.4.1)
|
- [x] 请求 DTO 定义在 service 包(`SyncVersionRequest` / `PlatformVersionInfo`),避免 controller ↔ service 循环依赖
|
||||||
|
- [x] 响应 DTO(`DownloadUrlResponse`)定义在 controller 包
|
||||||
- [x] Controller / Service / Repository 均有 constructor(NewXxx)函数
|
- [x] Controller / Service / Repository 均有 constructor(NewXxx)函数
|
||||||
- [x] Repository struct 的 `db *gorm.DB` 字段已定义
|
- [x] Repository struct 的 `db *gorm.DB` 字段已定义,通过 `database.GetDB()` 注入
|
||||||
- [x] 所有文件 import 块完整(`net/http`, `context`, `time`, `gorm.io/gorm`, models, service, repository)
|
- [x] Controller 使用 `response.Success` / `response.BadRequest` / `response.InternalError`(与项目统一响应包一致)
|
||||||
|
- [x] Controller constructor 注明不返回 error 的原因(无 Dubbo 依赖,与 `NewSegmentController` 等一致)
|
||||||
|
- [x] 所有文件 import 块完整(`context`, `time`, `gorm.io/gorm`, models, service, repository, response)
|
||||||
- [x] `UpsertAll` 使用 `Assign(map[string]interface{}{...})` 避免覆盖 `created_at`
|
- [x] `UpsertAll` 使用 `Assign(map[string]interface{}{...})` 避免覆盖 `created_at`
|
||||||
- [x] sync API 的 `type` 字段带 `binding:"required,oneof=native_app wgt"` 校验
|
- [x] sync API 的 `type` 字段带 `binding:"required,oneof=native_app wgt"` 校验
|
||||||
|
|
||||||
|
### 测试代码完整性
|
||||||
|
|
||||||
|
- [x] Repository 测试覆盖 `FindByType` 过滤逻辑 + `UpsertAll` insert/update
|
||||||
|
- [x] Service 测试覆盖 `GetAllNativeApp` 过滤 + `SyncVersion` 双平台场景
|
||||||
|
- [x] Controller 测试骨架覆盖 4 个场景(happy path / empty / invalid / success)
|
||||||
|
- [x] 测试建议将 repo/svc 改为 interface 以支持 mock 注入
|
||||||
|
|
||||||
### Vue / JS 代码完整性
|
### Vue / JS 代码完整性
|
||||||
|
|
||||||
- [x] `add.vue` 改为 diff 风格(标注原代码位置 + 修改后的代码块),不会覆盖原有 `submitForm` 逻辑
|
- [x] `add.vue` 改为 diff 风格(标注原代码位置 + 修改后的代码块),不会覆盖原有 `submitForm` 逻辑
|
||||||
@ -1192,7 +1494,10 @@ uni-admin/uni_modules/uni-upgrade-center/pages/version/add.vue # submitForm
|
|||||||
### 其他
|
### 其他
|
||||||
|
|
||||||
- [x] §5.2 package.json 去掉了不存在的 `uni-cloud-db` 依赖
|
- [x] §5.2 package.json 去掉了不存在的 `uni-cloud-db` 依赖
|
||||||
- [x] §4.4 controller 使用 `http.StatusOK` 等常量而非魔法数字
|
- [x] Controller 使用 `response` 包统一响应格式(非裸 `c.JSON` + 魔法数字)
|
||||||
- [x] 文档开头方案概述(§1)符合 CLAUDE.md 要求
|
- [x] 文档开头方案概述(§1)符合 CLAUDE.md 要求
|
||||||
- [x] 未违反 MVP 原则
|
- [x] 未违反 MVP 原则(§12.5 充分论证 gateway 直连 PG)
|
||||||
- [x] Admin 路由与现有 pattern 一致
|
- [x] Admin 路由与现有 group 集成(不新建 group)
|
||||||
|
- [x] 装配在 `SetupRouter` 内部完成(不修改 `main.go` / `SetupRouter` 签名)
|
||||||
|
- [x] Swagger 注解与 `swag init` 生成流程已说明
|
||||||
|
- [x] 测试运行命令已给出
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
137
frontend/static/html/download.html
Normal file
137
frontend/static/html/download.html
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>TopFans — 下载</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #0a0a0a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.container { text-align: center; padding: 40px 24px; max-width: 360px; width: 100%; }
|
||||||
|
.logo { font-size: 32px; font-weight: 800; letter-spacing: 2px; margin-bottom: 8px; }
|
||||||
|
.slogan { font-size: 14px; color: #888; margin-bottom: 32px; }
|
||||||
|
.version { font-size: 13px; color: #666; margin-bottom: 24px; min-height: 20px; }
|
||||||
|
.download-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
.download-btn:active { transform: scale(0.97); }
|
||||||
|
.download-btn.android { background: #3ddc84; color: #000; }
|
||||||
|
.download-btn.ios { background: #fff; color: #000; }
|
||||||
|
.download-btn.disabled { opacity: 0.4; pointer-events: none; }
|
||||||
|
.footer { margin-top: 40px; font-size: 12px; color: #555; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">TopFans</div>
|
||||||
|
<div class="slogan">粉丝共创平台</div>
|
||||||
|
<div class="version" id="version-info">加载中…</div>
|
||||||
|
|
||||||
|
<a id="btn-android" class="download-btn android disabled" href="javascript:void(0)">
|
||||||
|
🤖 Android 下载
|
||||||
|
</a>
|
||||||
|
<a id="btn-ios" class="download-btn ios disabled" href="javascript:void(0)">
|
||||||
|
iOS 下载
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="footer">TopFans © 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(async function() {
|
||||||
|
// ★ 部署时修改为实际 API 地址
|
||||||
|
var API_URL = 'https://api.topfans.com/api/v1/app/download-urls';
|
||||||
|
|
||||||
|
var btnAndroid = document.getElementById('btn-android');
|
||||||
|
var btnIOS = document.getElementById('btn-ios');
|
||||||
|
var versionInfo = document.getElementById('version-info');
|
||||||
|
var versions = [];
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
var hasAndroid = false;
|
||||||
|
var hasIOS = false;
|
||||||
|
|
||||||
|
if (versions.length > 0) {
|
||||||
|
for (var i = 0; i < versions.length; i++) {
|
||||||
|
var v = versions[i];
|
||||||
|
if (v.platform === 'android') {
|
||||||
|
btnAndroid.href = v.url;
|
||||||
|
btnAndroid.classList.remove('disabled');
|
||||||
|
hasAndroid = true;
|
||||||
|
}
|
||||||
|
if (v.platform === 'ios') {
|
||||||
|
btnIOS.href = v.url;
|
||||||
|
btnIOS.classList.remove('disabled');
|
||||||
|
hasIOS = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAndroid || hasIOS) {
|
||||||
|
var parts = [];
|
||||||
|
if (hasAndroid) parts.push('Android ' + getVersionText('android'));
|
||||||
|
if (hasIOS) parts.push('iOS ' + getVersionText('ios'));
|
||||||
|
versionInfo.textContent = '最新版本:' + parts.join(' | ');
|
||||||
|
} else {
|
||||||
|
versionInfo.textContent = '暂无可用下载';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVersionText(platform) {
|
||||||
|
for (var i = 0; i < versions.length; i++) {
|
||||||
|
if (versions[i].platform === platform) {
|
||||||
|
return versions[i].version || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await fetch(API_URL);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('HTTP ' + res.status);
|
||||||
|
}
|
||||||
|
var json = await res.json();
|
||||||
|
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
['android', 'ios'].forEach(function(p) {
|
||||||
|
if (json.data[p]) {
|
||||||
|
versions.push({
|
||||||
|
platform: p,
|
||||||
|
url: json.data[p].url,
|
||||||
|
version: json.data[p].version,
|
||||||
|
type: json.data[p].type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateUI();
|
||||||
|
} else {
|
||||||
|
versionInfo.textContent = '暂无可用下载';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取下载地址失败', err);
|
||||||
|
versionInfo.textContent = '获取下载地址失败,请稍后重试';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user