mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3881 from Wei-Shaw/feat/version-rollback
feat: 版本徽章新增近3个历史版本在线回退与手动回退指引
This commit is contained in:
@@ -26,6 +26,8 @@ type systemUpdateService interface {
|
||||
CheckUpdate(ctx context.Context, force bool) (*service.UpdateInfo, error)
|
||||
PerformUpdate(ctx context.Context) error
|
||||
Rollback() error
|
||||
ListRollbackVersions(ctx context.Context) ([]service.RollbackVersion, error)
|
||||
RollbackToVersion(ctx context.Context, version string) error
|
||||
}
|
||||
|
||||
// NewSystemHandler creates a new SystemHandler
|
||||
@@ -102,11 +104,42 @@ func (h *SystemHandler) PerformUpdate(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Rollback restores the previous version
|
||||
// GetRollbackVersions lists versions available for rollback
|
||||
// GET /api/v1/admin/system/rollback-versions
|
||||
func (h *SystemHandler) GetRollbackVersions(c *gin.Context) {
|
||||
versions, err := h.updateSvc.ListRollbackVersions(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"versions": versions,
|
||||
})
|
||||
}
|
||||
|
||||
// Rollback restores a previous version.
|
||||
// Without a body (or with an empty version) it restores the local .backup binary
|
||||
// left by the last in-place update. With {"version": "x.y.z"} it downloads and
|
||||
// installs that specific release (must be one of the recent rollback versions).
|
||||
// POST /api/v1/admin/system/rollback
|
||||
func (h *SystemHandler) Rollback(c *gin.Context) {
|
||||
operationID := buildSystemOperationID(c, "rollback")
|
||||
payload := gin.H{"operation_id": operationID}
|
||||
var req struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if c.Request.Body != nil && c.Request.ContentLength > 0 {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
}
|
||||
targetVersion := strings.TrimSpace(req.Version)
|
||||
|
||||
operation := "rollback"
|
||||
if targetVersion != "" {
|
||||
operation = "rollback:" + targetVersion
|
||||
}
|
||||
operationID := buildSystemOperationID(c, operation)
|
||||
payload := gin.H{"operation_id": operationID, "version": targetVersion}
|
||||
executeAdminIdempotentJSON(c, "admin.system.rollback", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||
lock, release, err := h.acquireSystemLock(ctx, operationID)
|
||||
if err != nil {
|
||||
@@ -118,7 +151,12 @@ func (h *SystemHandler) Rollback(c *gin.Context) {
|
||||
release(releaseReason, succeeded)
|
||||
}()
|
||||
|
||||
if err := h.updateSvc.Rollback(); err != nil {
|
||||
if targetVersion != "" {
|
||||
err = h.updateSvc.RollbackToVersion(ctx, targetVersion)
|
||||
} else {
|
||||
err = h.updateSvc.Rollback()
|
||||
}
|
||||
if err != nil {
|
||||
releaseReason = "SYSTEM_ROLLBACK_FAILED"
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,6 +165,7 @@ func (h *SystemHandler) Rollback(c *gin.Context) {
|
||||
return gin.H{
|
||||
"message": "Rollback completed. Please restart the service.",
|
||||
"need_restart": true,
|
||||
"version": targetVersion,
|
||||
"operation_id": lock.OperationID(),
|
||||
}, nil
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -17,11 +18,18 @@ import (
|
||||
)
|
||||
|
||||
type systemHandlerUpdateServiceStub struct {
|
||||
performErr error
|
||||
updateInfo *service.UpdateInfo
|
||||
checkErr error
|
||||
checkForces []bool
|
||||
performCall int
|
||||
performErr error
|
||||
updateInfo *service.UpdateInfo
|
||||
checkErr error
|
||||
checkForces []bool
|
||||
performCall int
|
||||
rollbackCall int
|
||||
rollbackToCall int
|
||||
rollbackToVersions []string
|
||||
rollbackToErr error
|
||||
rollbackVersions []service.RollbackVersion
|
||||
rollbackVersionsErr error
|
||||
rollbackVersionsCall int
|
||||
}
|
||||
|
||||
func (s *systemHandlerUpdateServiceStub) CheckUpdate(_ context.Context, force bool) (*service.UpdateInfo, error) {
|
||||
@@ -35,9 +43,21 @@ func (s *systemHandlerUpdateServiceStub) PerformUpdate(context.Context) error {
|
||||
}
|
||||
|
||||
func (s *systemHandlerUpdateServiceStub) Rollback() error {
|
||||
s.rollbackCall++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemHandlerUpdateServiceStub) ListRollbackVersions(context.Context) ([]service.RollbackVersion, error) {
|
||||
s.rollbackVersionsCall++
|
||||
return s.rollbackVersions, s.rollbackVersionsErr
|
||||
}
|
||||
|
||||
func (s *systemHandlerUpdateServiceStub) RollbackToVersion(_ context.Context, version string) error {
|
||||
s.rollbackToCall++
|
||||
s.rollbackToVersions = append(s.rollbackToVersions, version)
|
||||
return s.rollbackToErr
|
||||
}
|
||||
|
||||
type systemUpdateResponseEnvelope struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -71,6 +91,8 @@ func newSystemHandlerTestRouter(t *testing.T, updateSvc *systemHandlerUpdateServ
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/admin/system/update", handler.PerformUpdate)
|
||||
router.POST("/api/v1/admin/system/rollback", handler.Rollback)
|
||||
router.GET("/api/v1/admin/system/rollback-versions", handler.GetRollbackVersions)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -142,3 +164,104 @@ func TestSystemHandlerPerformUpdateFailureStillReturnsInternalError(t *testing.T
|
||||
require.Equal(t, http.StatusInternalServerError, body.Code)
|
||||
require.Equal(t, "internal error", body.Message)
|
||||
}
|
||||
|
||||
func TestSystemHandlerRollbackWithoutBodyUsesLegacyBackup(t *testing.T) {
|
||||
updateSvc := &systemHandlerUpdateServiceStub{}
|
||||
repo := newMemoryIdempotencyRepoStub()
|
||||
router := newSystemHandlerTestRouter(t, updateSvc, repo)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/rollback", nil)
|
||||
req.Header.Set("Idempotency-Key", "legacy-rollback")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, 1, updateSvc.rollbackCall)
|
||||
require.Equal(t, 0, updateSvc.rollbackToCall)
|
||||
requireSystemLockStatus(t, repo, service.IdempotencyStatusSucceeded)
|
||||
}
|
||||
|
||||
func TestSystemHandlerRollbackWithVersionCallsRollbackToVersion(t *testing.T) {
|
||||
updateSvc := &systemHandlerUpdateServiceStub{}
|
||||
repo := newMemoryIdempotencyRepoStub()
|
||||
router := newSystemHandlerTestRouter(t, updateSvc, repo)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/rollback",
|
||||
strings.NewReader(`{"version":"0.1.146"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Idempotency-Key", "rollback-to-146")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, 0, updateSvc.rollbackCall)
|
||||
require.Equal(t, 1, updateSvc.rollbackToCall)
|
||||
require.Equal(t, []string{"0.1.146"}, updateSvc.rollbackToVersions)
|
||||
requireSystemLockStatus(t, repo, service.IdempotencyStatusSucceeded)
|
||||
|
||||
var body systemUpdateResponseEnvelope
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
|
||||
require.Equal(t, 0, body.Code)
|
||||
require.Equal(t, "Rollback completed. Please restart the service.", body.Data.Message)
|
||||
}
|
||||
|
||||
func TestSystemHandlerRollbackWithDisallowedVersionReturnsBadRequest(t *testing.T) {
|
||||
updateSvc := &systemHandlerUpdateServiceStub{
|
||||
rollbackToErr: service.ErrRollbackVersionNotAllowed,
|
||||
}
|
||||
repo := newMemoryIdempotencyRepoStub()
|
||||
router := newSystemHandlerTestRouter(t, updateSvc, repo)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/rollback",
|
||||
strings.NewReader(`{"version":"9.9.9"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Idempotency-Key", "rollback-to-bad")
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Equal(t, 1, updateSvc.rollbackToCall)
|
||||
}
|
||||
|
||||
func TestSystemHandlerGetRollbackVersions(t *testing.T) {
|
||||
updateSvc := &systemHandlerUpdateServiceStub{
|
||||
rollbackVersions: []service.RollbackVersion{
|
||||
{Version: "0.1.146", PublishedAt: "2026-07-07T00:00:00Z", HTMLURL: "https://example.com/v0.1.146"},
|
||||
{Version: "0.1.145", PublishedAt: "2026-07-06T00:00:00Z", HTMLURL: "https://example.com/v0.1.145"},
|
||||
},
|
||||
}
|
||||
repo := newMemoryIdempotencyRepoStub()
|
||||
router := newSystemHandlerTestRouter(t, updateSvc, repo)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/system/rollback-versions", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, 1, updateSvc.rollbackVersionsCall)
|
||||
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Versions []service.RollbackVersion `json:"versions"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
|
||||
require.Equal(t, 0, body.Code)
|
||||
require.Len(t, body.Data.Versions, 2)
|
||||
require.Equal(t, "0.1.146", body.Data.Versions[0].Version)
|
||||
}
|
||||
|
||||
func TestSystemHandlerGetRollbackVersionsError(t *testing.T) {
|
||||
updateSvc := &systemHandlerUpdateServiceStub{
|
||||
rollbackVersionsErr: errors.New("github unavailable"),
|
||||
}
|
||||
repo := newMemoryIdempotencyRepoStub()
|
||||
router := newSystemHandlerTestRouter(t, updateSvc, repo)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/system/rollback-versions", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, rec.Code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user