This commit is contained in:
@@ -20,13 +20,17 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
"github.com/perfect-panel/server/pkg/uuidx"
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
pkgerrors "github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
const genericRecoveryError = "order recovery information is invalid"
|
const (
|
||||||
|
recoverySuccessMessage = "恢复完成,请使用该邮箱验证码登录查看订阅。"
|
||||||
|
recoveryAlreadyDoneMessage = "该订单已恢复,请使用恢复时绑定的邮箱登录查看订阅。"
|
||||||
|
recoveryInvalidOrderMessage = "订单信息无效,请确认订单号是否正确。"
|
||||||
|
recoveryInvalidCodeMessage = "邮箱验证码错误或已过期,请重新获取验证码。"
|
||||||
|
recoverySystemErrorMessage = "恢复暂时失败,请稍后再试或联系客服处理。"
|
||||||
|
)
|
||||||
|
|
||||||
type RecoverOrderLogic struct {
|
type RecoverOrderLogic struct {
|
||||||
logger.Logger
|
logger.Logger
|
||||||
@@ -48,45 +52,51 @@ func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types
|
|||||||
code := strings.TrimSpace(req.Code)
|
code := strings.TrimSpace(req.Code)
|
||||||
|
|
||||||
if orderNo == "" || email == "" || code == "" {
|
if orderNo == "" || email == "" || code == "" {
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := l.verifyEmailCode(email, code); err != nil {
|
if err := l.verifyEmailCode(email, code); err != nil {
|
||||||
return nil, err
|
return recoveryFailed(recoveryInvalidCodeMessage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
item, ok, err := recoveryconfig.FindOrder(orderNo)
|
item, ok, err := recoveryconfig.FindOrder(orderNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[RecoverOrder] load recovery config failed", logger.Field("error", err.Error()))
|
l.Errorw("[RecoverOrder] load recovery config failed", logger.Field("error", err.Error()))
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.ERROR), genericRecoveryError)
|
return recoveryFailed(recoverySystemErrorMessage), nil
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.OrderStatus != "" && item.OrderStatus != "success" {
|
if item.OrderStatus != "" && item.OrderStatus != "success" {
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
||||||
}
|
}
|
||||||
if item.Quantity <= 0 || item.OrderMoneyCents <= 0 {
|
if item.Quantity <= 0 || item.OrderMoneyCents <= 0 {
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return recoveryFailed(recoveryInvalidOrderMessage), nil
|
||||||
|
}
|
||||||
|
claimOrderNo := item.OutOrderNo
|
||||||
|
if claimOrderNo == "" {
|
||||||
|
claimOrderNo = item.OrderNo
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, item.SubscribeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[RecoverOrder] subscribe not found", logger.Field("subscribe_id", item.SubscribeId), logger.Field("error", err.Error()))
|
l.Errorw("[RecoverOrder] subscribe not found", logger.Field("subscribe_id", item.SubscribeId), logger.Field("error", err.Error()))
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return recoveryFailed(recoverySystemErrorMessage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
claim recoverymodel.OrderRecoveryClaim
|
claim recoverymodel.OrderRecoveryClaim
|
||||||
order ordermodel.Order
|
order ordermodel.Order
|
||||||
userSub usermodel.Subscribe
|
userSub usermodel.Subscribe
|
||||||
|
alreadyRecovered bool
|
||||||
)
|
)
|
||||||
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
Model(&recoverymodel.OrderRecoveryClaim{}).
|
Model(&recoverymodel.OrderRecoveryClaim{}).
|
||||||
Where("order_no = ?", orderNo).
|
Where("order_no = ?", claimOrderNo).
|
||||||
First(&claim).Error; err == nil {
|
First(&claim).Error; err == nil {
|
||||||
|
alreadyRecovered = true
|
||||||
return nil
|
return nil
|
||||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
@@ -98,7 +108,7 @@ func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types
|
|||||||
}
|
}
|
||||||
|
|
||||||
claim = recoverymodel.OrderRecoveryClaim{
|
claim = recoverymodel.OrderRecoveryClaim{
|
||||||
OrderNo: orderNo,
|
OrderNo: claimOrderNo,
|
||||||
Email: email,
|
Email: email,
|
||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
SubscribeId: item.SubscribeId,
|
SubscribeId: item.SubscribeId,
|
||||||
@@ -128,7 +138,14 @@ func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[RecoverOrder] recover failed", logger.Field("order_no", orderNo), logger.Field("error", err.Error()))
|
l.Errorw("[RecoverOrder] recover failed", logger.Field("order_no", orderNo), logger.Field("error", err.Error()))
|
||||||
return nil, pkgerrors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), genericRecoveryError)
|
return recoveryFailed(recoverySystemErrorMessage), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if alreadyRecovered {
|
||||||
|
return &types.RecoverOrderResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: recoveryAlreadyDoneMessage,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
l.clearCaches(&userSub, item.SubscribeId, email)
|
l.clearCaches(&userSub, item.SubscribeId, email)
|
||||||
@@ -137,10 +154,17 @@ func (l *RecoverOrderLogic) RecoverOrder(req *types.RecoverOrderRequest) (*types
|
|||||||
|
|
||||||
return &types.RecoverOrderResponse{
|
return &types.RecoverOrderResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "recovery completed, please login with your email verification code",
|
Message: recoverySuccessMessage,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recoveryFailed(message string) *types.RecoverOrderResponse {
|
||||||
|
return &types.RecoverOrderResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *RecoverOrderLogic) createRecoveredOrder(tx *gorm.DB, userId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (ordermodel.Order, error) {
|
func (l *RecoverOrderLogic) createRecoveredOrder(tx *gorm.DB, userId int64, item recoveryconfig.Order, sub *subscribemodel.Subscribe) (ordermodel.Order, error) {
|
||||||
var existing ordermodel.Order
|
var existing ordermodel.Order
|
||||||
orderNo := item.OutOrderNo
|
orderNo := item.OutOrderNo
|
||||||
@@ -219,10 +243,6 @@ func (l *RecoverOrderLogic) createOrExtendSubscribe(tx *gorm.DB, userId int64, o
|
|||||||
existing.ExpireTime = base.Add(time.Duration(item.Quantity) * 24 * time.Hour)
|
existing.ExpireTime = base.Add(time.Duration(item.Quantity) * 24 * time.Hour)
|
||||||
existing.Status = 1
|
existing.Status = 1
|
||||||
existing.FinishedAt = nil
|
existing.FinishedAt = nil
|
||||||
existing.NodeGroupId = sub.NodeGroupId
|
|
||||||
existing.Traffic = sub.Traffic
|
|
||||||
existing.Download = 0
|
|
||||||
existing.Upload = 0
|
|
||||||
if strings.TrimSpace(item.Note) != "" {
|
if strings.TrimSpace(item.Note) != "" {
|
||||||
existing.Note = strings.TrimSpace(item.Note)
|
existing.Note = strings.TrimSpace(item.Note)
|
||||||
}
|
}
|
||||||
@@ -275,7 +295,7 @@ func (l *RecoverOrderLogic) verifyEmailCode(email string, code string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resp == nil || !resp.Status {
|
if resp == nil || !resp.Status {
|
||||||
return pkgerrors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), genericRecoveryError)
|
return errors.New("invalid verification code")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ func loadOrders() (map[string]Order, error) {
|
|||||||
return nil, fmt.Errorf("duplicate recovery out_order_no %s", item.OutOrderNo)
|
return nil, fmt.Errorf("duplicate recovery out_order_no %s", item.OutOrderNo)
|
||||||
}
|
}
|
||||||
result[item.OutOrderNo] = item
|
result[item.OutOrderNo] = item
|
||||||
|
if item.OrderNo != "" && item.OrderNo != item.OutOrderNo {
|
||||||
|
if _, exists := result[item.OrderNo]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate recovery order_no %s", item.OrderNo)
|
||||||
|
}
|
||||||
|
result[item.OrderNo] = item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,9 +277,22 @@
|
|||||||
const emailInput = document.getElementById("email");
|
const emailInput = document.getElementById("email");
|
||||||
const orderInput = document.getElementById("order-no");
|
const orderInput = document.getElementById("order-no");
|
||||||
const codeInput = document.getElementById("code");
|
const codeInput = document.getElementById("code");
|
||||||
|
const API_BASE = "https://tapi.hifast.biz";
|
||||||
|
|
||||||
let countdownTimer = null;
|
let countdownTimer = null;
|
||||||
|
|
||||||
|
const ERROR_MESSAGE_MAP = {
|
||||||
|
"Param Error": "请检查订单号、邮箱和验证码是否填写正确。",
|
||||||
|
"Verify code error": "邮箱验证码错误或已过期,请重新获取验证码。",
|
||||||
|
"Too Many Requests": "验证码发送太频繁,请稍后再试。",
|
||||||
|
"This account has reached the limit of sending times today":
|
||||||
|
"该邮箱今天获取验证码次数已达上限,请明天再试。"
|
||||||
|
};
|
||||||
|
|
||||||
|
function friendlyMessage(message, fallback) {
|
||||||
|
return ERROR_MESSAGE_MAP[message] || message || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function showStatus(type, message) {
|
function showStatus(type, message) {
|
||||||
statusBox.className = `status show ${type}`;
|
statusBox.className = `status show ${type}`;
|
||||||
statusBox.textContent = message;
|
statusBox.textContent = message;
|
||||||
@@ -296,7 +309,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function postJson(path, body) {
|
async function postJson(path, body) {
|
||||||
const response = await fetch(path, {
|
const response = await fetch(`${API_BASE}${path}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -305,7 +318,9 @@
|
|||||||
});
|
});
|
||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
if (!response.ok || payload.code !== 200) {
|
if (!response.ok || payload.code !== 200) {
|
||||||
throw new Error(payload.msg || "请求失败,请稍后再试");
|
throw new Error(
|
||||||
|
friendlyMessage(payload.msg, "请求失败,请稍后再试。")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return payload.data || {};
|
return payload.data || {};
|
||||||
}
|
}
|
||||||
@@ -361,13 +376,19 @@
|
|||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
showStatus(
|
const message =
|
||||||
"success",
|
result.message || "恢复完成,请使用该邮箱验证码登录查看订阅。";
|
||||||
result.message || "恢复完成,请使用该邮箱验证码登录查看订阅。"
|
if (result.success === false) {
|
||||||
);
|
showStatus("error", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showStatus("success", message);
|
||||||
form.reset();
|
form.reset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showStatus("error", "订单或邮箱验证码无效,请确认后重试。");
|
showStatus(
|
||||||
|
"error",
|
||||||
|
error.message || "恢复失败,请检查信息后重试。"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.textContent = "恢复订阅";
|
submitButton.textContent = "恢复订阅";
|
||||||
|
|||||||
Reference in New Issue
Block a user