补单逻辑
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
2026-05-06 05:42:48 -07:00
parent 54daa923da
commit 8bc8e81e95
3 changed files with 76 additions and 29 deletions
@@ -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
} }
+6
View File
@@ -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
} }
+28 -7
View File
@@ -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 = "恢复订阅";