fix gitea workflow path and runner label
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m54s
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m54s
This commit is contained in:
parent
f773fe1d6d
commit
8bdc8afea3
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -14,6 +13,8 @@ import (
|
|||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeleteAccountHandler 注销账号处理器
|
// DeleteAccountHandler 注销账号处理器
|
||||||
@ -28,7 +29,7 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var req deleteAccountReq
|
var req deleteAccountReq
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()})
|
result.ParamErrorResult(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,18 +38,13 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
|||||||
|
|
||||||
// 校验邮箱验证码
|
// 校验邮箱验证码
|
||||||
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
|
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
result.HttpResult(c, nil, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l := user.NewDeleteAccountLogic(c.Request.Context(), serverCtx)
|
l := user.NewDeleteAccountLogic(c.Request.Context(), serverCtx)
|
||||||
resp, err := l.DeleteAccountAll()
|
resp, err := l.DeleteAccountAll()
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.HttpResult(c, resp, err)
|
result.HttpResult(c, resp, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +81,7 @@ func verifyEmailCode(ctx context.Context, serverCtx *svc.ServiceContext, email s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !verified {
|
if !verified {
|
||||||
return fmt.Errorf("verification code error or expired")
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verification code error or expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证成功后删除缓存
|
// 验证成功后删除缓存
|
||||||
|
|||||||
102
internal/handler/public/user/deleteAccountHandler_test.go
Normal file
102
internal/handler/public/user/deleteAccountHandler_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/internal/config"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handlerResponse struct {
|
||||||
|
Code uint32 `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeleteAccountTestRouter(serverCtx *svc.ServiceContext) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/v1/public/user/delete_account", DeleteAccountHandler(serverCtx))
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteAccountHandlerInvalidParamsUsesUnifiedResponse(t *testing.T) {
|
||||||
|
router := newDeleteAccountTestRouter(&svc.ServiceContext{})
|
||||||
|
|
||||||
|
reqBody := bytes.NewBufferString(`{"email":"invalid-email"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d", recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp handlerResponse
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 400 {
|
||||||
|
t.Fatalf("expected business code 400, got %d, body=%s", resp.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &raw); err != nil {
|
||||||
|
t.Fatalf("failed to decode raw response: %v", err)
|
||||||
|
}
|
||||||
|
if _, exists := raw["error"]; exists {
|
||||||
|
t.Fatalf("unexpected raw error field in response: %s", recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteAccountHandlerVerifyCodeErrorUsesUnifiedResponse(t *testing.T) {
|
||||||
|
redisClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: "invalid:6379",
|
||||||
|
Dialer: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return nil, errors.New("dial disabled in test")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer redisClient.Close()
|
||||||
|
|
||||||
|
serverCtx := &svc.ServiceContext{
|
||||||
|
Redis: redisClient,
|
||||||
|
Config: config.Config{
|
||||||
|
VerifyCode: config.VerifyCode{
|
||||||
|
ExpireTime: 900,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
router := newDeleteAccountTestRouter(serverCtx)
|
||||||
|
|
||||||
|
reqBody := bytes.NewBufferString(`{"email":"user@example.com","code":"123456"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/public/user/delete_account", reqBody)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d", recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp handlerResponse
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 70001 {
|
||||||
|
t.Fatalf("expected business code 70001, got %d, body=%s", resp.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
@ -146,9 +147,36 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
|||||||
|
|
||||||
// 获取当前调用设备 ID
|
// 获取当前调用设备 ID
|
||||||
currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64)
|
currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64)
|
||||||
|
currentIdentifier, _ := l.ctx.Value(constant.CtxKeyIdentifier).(string)
|
||||||
|
currentIdentifier = strings.TrimSpace(currentIdentifier)
|
||||||
|
|
||||||
|
if currentDeviceId == 0 && currentIdentifier != "" {
|
||||||
|
deviceInfo, deviceErr := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, currentIdentifier)
|
||||||
|
switch {
|
||||||
|
case deviceErr == nil:
|
||||||
|
if deviceInfo.UserId == currentUser.Id {
|
||||||
|
currentDeviceId = deviceInfo.Id
|
||||||
|
} else {
|
||||||
|
l.Infow("当前标识符设备归属与用户不匹配",
|
||||||
|
logger.Field("user_id", currentUser.Id),
|
||||||
|
logger.Field("device_id", deviceInfo.Id),
|
||||||
|
logger.Field("identifier", currentIdentifier),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case errors.Is(deviceErr, gorm.ErrRecordNotFound):
|
||||||
|
l.Infow("未通过标识符找到当前设备", logger.Field("user_id", currentUser.Id), logger.Field("identifier", currentIdentifier))
|
||||||
|
default:
|
||||||
|
l.Errorw("通过标识符查找当前设备失败",
|
||||||
|
logger.Field("user_id", currentUser.Id),
|
||||||
|
logger.Field("identifier", currentIdentifier),
|
||||||
|
logger.Field("error", deviceErr.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp = &types.DeleteAccountResponse{}
|
resp = &types.DeleteAccountResponse{}
|
||||||
var newUserId int64
|
var newUserId int64
|
||||||
|
var firstMigratedUserId int64
|
||||||
|
|
||||||
// 开始数据库事务
|
// 开始数据库事务
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||||
@ -179,6 +207,9 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
|||||||
l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
|
l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if firstMigratedUserId == 0 {
|
||||||
|
firstMigratedUserId = newUser.Id
|
||||||
|
}
|
||||||
|
|
||||||
// B. 迁移设备记录 (Update user_id)
|
// B. 迁移设备记录 (Update user_id)
|
||||||
if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil {
|
if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil {
|
||||||
@ -195,7 +226,11 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是当前请求的设备,记录其新 UserID 返回给前端
|
// 如果是当前请求的设备,记录其新 UserID 返回给前端
|
||||||
if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) {
|
isCurrentRequestDevice := currentDeviceId > 0 && dev.Id == currentDeviceId
|
||||||
|
if !isCurrentRequestDevice && currentIdentifier != "" {
|
||||||
|
isCurrentRequestDevice = dev.Identifier == currentIdentifier
|
||||||
|
}
|
||||||
|
if isCurrentRequestDevice {
|
||||||
newUserId = newUser.Id
|
newUserId = newUser.Id
|
||||||
}
|
}
|
||||||
l.Infow("旧设备已迁移至新匿名账号",
|
l.Infow("旧设备已迁移至新匿名账号",
|
||||||
@ -236,6 +271,9 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
|||||||
|
|
||||||
// 最终清理所有 Session (踢掉所有设备)
|
// 最终清理所有 Session (踢掉所有设备)
|
||||||
l.clearAllSessions(currentUser.Id)
|
l.clearAllSessions(currentUser.Id)
|
||||||
|
if newUserId == 0 {
|
||||||
|
newUserId = firstMigratedUserId
|
||||||
|
}
|
||||||
|
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
resp.Message = "注销成功"
|
resp.Message = "注销成功"
|
||||||
|
|||||||
37
internal/types/deleteAccountResponse_test.go
Normal file
37
internal/types/deleteAccountResponse_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeleteAccountResponseAlwaysContainsIntFields(t *testing.T) {
|
||||||
|
data, err := json.Marshal(DeleteAccountResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "ok",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded map[string]interface{}
|
||||||
|
if err = json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, hasUserID := decoded["user_id"]
|
||||||
|
if !hasUserID {
|
||||||
|
t.Fatalf("expected user_id in JSON, got %s", string(data))
|
||||||
|
}
|
||||||
|
if userID != float64(0) {
|
||||||
|
t.Fatalf("expected user_id=0, got %v", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, hasCode := decoded["code"]
|
||||||
|
if !hasCode {
|
||||||
|
t.Fatalf("expected code in JSON, got %s", string(data))
|
||||||
|
}
|
||||||
|
if code != float64(0) {
|
||||||
|
t.Fatalf("expected code=0, got %v", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2968,8 +2968,8 @@ type GetSubscribeStatusRequest struct {
|
|||||||
type DeleteAccountResponse struct {
|
type DeleteAccountResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
UserId int64 `json:"user_id,omitempty"`
|
UserId int64 `json:"user_id"`
|
||||||
Code int `json:"code,omitempty"`
|
Code int `json:"code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDownloadLinkRequest struct {
|
type GetDownloadLinkRequest struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user