fix: 排队激活未评估停复机、孤儿套餐重复激活与微信查单订单不存在反复重查
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s
- 排队激活与首次实名激活任务的处理器注入停复机服务:原来传 nil,激活后不重新评估停复机,付款或实名后要等下一轮轮询才复机(生产同样存在) - 孤儿套餐扫描跳过载体未实名的"实名即生效"套餐:激活服务会直接跳过,原来每 10 秒重复提交一次激活任务 - 微信 v2 查单返回 ORDERNOTEXIST 时按"渠道无此订单"处理:代理充值支付单过期后关闭,不再当作结果未知每几分钟重查 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,8 @@ const (
|
||||
OnlinePaymentStateClosed = "closed"
|
||||
// OnlinePaymentStateUnknown 表示渠道状态暂时无法确定。
|
||||
OnlinePaymentStateUnknown = "unknown"
|
||||
// OnlinePaymentStateNotFound 表示渠道明确没有这笔订单;恢复任务在支付单过期后按关闭处理。
|
||||
OnlinePaymentStateNotFound = "not_found"
|
||||
)
|
||||
|
||||
// OnlinePaymentRequest 描述生成支付链接与主动查单所需的最小事实。
|
||||
|
||||
@@ -139,6 +139,12 @@ func (s *RecoverOnlinePaymentService) queryPayment(ctx context.Context, adapter
|
||||
return err
|
||||
case OnlinePaymentStateClosed:
|
||||
return s.closePending(ctx, payment, recharge)
|
||||
case OnlinePaymentStateNotFound:
|
||||
// 渠道没有这笔订单:过期后已不可能再支付,按关闭处理;未过期时可能尚未同步到渠道,继续等待。
|
||||
if payment.ExpireAt != nil && time.Now().After(*payment.ExpireAt) {
|
||||
return s.closePending(ctx, payment, recharge)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -66,6 +67,16 @@ func (a *WechatWebAdapter) Query(ctx context.Context, request agentrecharge.Onli
|
||||
}
|
||||
startedAt := time.Now()
|
||||
info, callErr := a.queryOrder(ctx, request.Config, request.PaymentNo)
|
||||
if stderrors.Is(callErr, wechatpay.ErrOrderNotExist) {
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: "ORDERNOTEXIST",
|
||||
ResponseSummary: map[string]any{"state": agentrecharge.OnlinePaymentStateNotFound},
|
||||
DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
return agentrecharge.OnlinePaymentQueryResult{State: agentrecharge.OnlinePaymentStateNotFound}, nil
|
||||
}
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
|
||||
@@ -200,11 +200,27 @@ func (h *PackageActivationHandler) findAndActivateOrphanPackages(ctx context.Con
|
||||
FROM pending_queue AS candidate
|
||||
JOIN tb_package_usage AS usage ON usage.id = candidate.id
|
||||
WHERE candidate.queue_position = 1
|
||||
-- 实名即生效的套餐在载体实名前不能激活,激活服务会直接跳过;不排除会每轮重复提交激活任务。
|
||||
-- 实名后由实名激活入口激活。口径与 ActivationService.canActivatePendingUsage 一致。
|
||||
AND (
|
||||
NOT usage.pending_realname_activation
|
||||
OR (COALESCE(usage.iot_card_id, 0) > 0 AND EXISTS (
|
||||
SELECT 1 FROM tb_iot_card AS card
|
||||
WHERE card.id = usage.iot_card_id AND card.real_name_status = ?))
|
||||
OR (COALESCE(usage.iot_card_id, 0) = 0 AND usage.device_id > 0 AND EXISTS (
|
||||
SELECT 1 FROM tb_device_sim_binding AS binding
|
||||
JOIN tb_iot_card AS card ON card.id = binding.iot_card_id
|
||||
WHERE binding.device_id = usage.device_id AND binding.bind_status = ?
|
||||
AND binding.deleted_at IS NULL AND card.real_name_status = ?))
|
||||
)
|
||||
ORDER BY candidate.priority ASC, candidate.created_at ASC, candidate.id ASC
|
||||
LIMIT ?`,
|
||||
constants.PackageUsageStatusPending,
|
||||
constants.PackageUsageStatusActive,
|
||||
constants.PackageUsageStatusDepleted,
|
||||
constants.RealNameStatusVerified,
|
||||
constants.BindStatusBound,
|
||||
constants.RealNameStatusVerified,
|
||||
orphanPackageScanLimit,
|
||||
).Scan(&orphanUsages).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -309,7 +309,8 @@ func (h *Handler) registerPackageActivationHandlers() {
|
||||
h.redis,
|
||||
h.asynqClient,
|
||||
h.workerResult.Services.ActivationService,
|
||||
nil,
|
||||
// 激活成功后须重新评估停复机(客户付款或实名后及时复机);与轮询调度中的激活处理器注入同一服务。
|
||||
h.workerResult.Services.StopResumeService,
|
||||
h.logger,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/md5"
|
||||
"crypto/subtle"
|
||||
"encoding/xml"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -41,6 +42,10 @@ type PaymentV2Service struct {
|
||||
}
|
||||
|
||||
// NewPaymentV2Service 创建微信支付 v2 服务
|
||||
|
||||
// ErrOrderNotExist 表示微信明确返回订单不存在(v2 err_code=ORDERNOTEXIST)。
|
||||
var ErrOrderNotExist = stderrors.New("微信订单不存在")
|
||||
|
||||
func NewPaymentV2Service(appID, mchID, apiKey, notifyURL string, logger *zap.Logger) *PaymentV2Service {
|
||||
return &PaymentV2Service{
|
||||
appID: appID,
|
||||
@@ -297,6 +302,11 @@ func (s *PaymentV2Service) QueryOrder(ctx context.Context, orderNo string) (*Ord
|
||||
s.logger.Error("解析微信 v2 查单响应失败", zap.String("order_no", orderNo), zap.Error(err))
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "查询微信支付订单失败")
|
||||
}
|
||||
if resp.ReturnCode == "SUCCESS" && resp.ResultCode != "SUCCESS" && resp.ErrCode == "ORDERNOTEXIST" {
|
||||
// 微信明确表示没有这笔订单:返回专门的错误,由调用方按"渠道无此订单"处理,不当作结果未知反复重查。
|
||||
s.logger.Warn("微信 v2 查单:订单不存在", zap.String("order_no", orderNo))
|
||||
return nil, ErrOrderNotExist
|
||||
}
|
||||
if resp.ReturnCode != "SUCCESS" || resp.ResultCode != "SUCCESS" {
|
||||
s.logger.Error("微信 v2 查单失败", zap.String("order_no", orderNo), zap.String("return_code", resp.ReturnCode),
|
||||
zap.String("return_msg", resp.ReturnMsg), zap.String("result_code", resp.ResultCode), zap.String("err_code", resp.ErrCode), zap.String("err_code_des", resp.ErrCodeDes))
|
||||
|
||||
Reference in New Issue
Block a user