fix: harden dependencies and downstream delivery

This commit is contained in:
hectorzhao
2026-07-15 12:05:24 +08:00
parent 9bbfb72be6
commit a758672436
17 changed files with 418 additions and 57 deletions
+58 -4
View File
@@ -93,6 +93,9 @@ type DownstreamUplink struct {
type DownstreamSendResult struct {
Sent bool `json:"sent"`
Retryable bool `json:"retryable"`
ReasonCode string `json:"reasonCode,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
ConnectionID string `json:"connectionId,omitempty"`
SequenceID string `json:"sequenceId,omitempty"`
MessageID string `json:"messageId,omitempty"`
@@ -544,7 +547,20 @@ func (s Server) flushPending(account string, logger *log.Logger) (pendingFlushRe
result.DeliveredCount++
continue
}
result.WaitingCount++
errorMessage := defaultString(sendResult.ErrorMessage, "gateway did not complete downstream delivery")
failureType := "unrecoverable"
if sendResult.Retryable {
failureType = "send_failed"
result.WaitingCount++
} else {
result.FailedCount++
}
result.LastError = errorMessage
_ = s.post(context.Background(), "/gateway/events/downstream/failed", map[string]string{
"id": delivery.ID,
"errorMessage": errorMessageWithCode(errorMessage, sendResult.ReasonCode),
"failureType": failureType,
}, nil)
}
return result, nil
}
@@ -776,6 +792,10 @@ func (s Server) recoverPendingCandidates(logger *log.Logger) {
status.State = "partial"
status.LastError = result.LastError
status.FailureCategory = recoveryFailureCategory(status.State, status.LastError, status.LastSkipReason)
case result.FailedCount > 0:
status.State = "failed"
status.LastError = result.LastError
status.FailureCategory = recoveryFailureCategory(status.State, status.LastError, status.LastSkipReason)
default:
status.State = "success"
status.LastError = ""
@@ -861,7 +881,7 @@ func PushReceipt(event DownstreamReceipt) (bool, error) {
}
func PushReceiptWithResult(event DownstreamReceipt) (DownstreamSendResult, error) {
return pushReceiptWithResult(event, false)
return pushReceiptWithResult(event, true)
}
func pushReceiptWithResult(event DownstreamReceipt, allowRecovery bool) (DownstreamSendResult, error) {
@@ -870,7 +890,25 @@ func pushReceiptWithResult(event DownstreamReceipt, allowRecovery bool) (Downstr
session = recoverReceiptSession(event)
}
if session == nil {
return DownstreamSendResult{}, nil
if event.SubmitSequenceID == 0 {
return DownstreamSendResult{
Retryable: false,
ReasonCode: "MISSING_SUBMIT_SEQUENCE_ID",
ErrorMessage: "历史回执缺少原 Submit Sequence_Id,无法重建 Msg_Id,系统已终止重投",
}, nil
}
if strings.TrimSpace(event.MessageID) == "" || strings.TrimSpace(event.Account) == "" {
return DownstreamSendResult{
Retryable: false,
ReasonCode: "INVALID_RECEIPT_PAYLOAD",
ErrorMessage: "状态回执缺少平台消息 ID 或客户账号,系统已终止重投",
}, nil
}
return DownstreamSendResult{
Retryable: true,
ReasonCode: "CLIENT_DISCONNECTED",
ErrorMessage: "下游客户端当前未连接,等待自动重试",
}, nil
}
stat := strings.TrimSpace(event.RawStatus)
if stat == "" {
@@ -934,7 +972,11 @@ func PushUplink(event DownstreamUplink) (bool, error) {
func PushUplinkWithResult(event DownstreamUplink) (DownstreamSendResult, error) {
session := findSession(event.MessageID, event.Account)
if session == nil {
return DownstreamSendResult{}, nil
return DownstreamSendResult{
Retryable: true,
ReasonCode: "CLIENT_DISCONNECTED",
ErrorMessage: "下游客户端当前未连接,等待自动重试",
}, nil
}
content, err := cmpputils.Utf8ToUcs2(event.Content)
if err != nil {
@@ -952,6 +994,18 @@ func PushUplinkWithResult(event DownstreamUplink) (DownstreamSendResult, error)
return sendDownstream(session, deliver, event.DeliveryID)
}
func errorMessageWithCode(message string, code string) string {
message = strings.TrimSpace(message)
code = strings.TrimSpace(code)
if message == "" {
message = "gateway did not complete downstream delivery"
}
if code == "" {
return message
}
return fmt.Sprintf("%s (%s)", message, code)
}
func downstreamDeliverPacket(session *downstreamSession, messageID uint64, destID string, sourceTerminalID string, msgFmt uint8, registerDelivery uint8, content string) cmpp.Packer {
if session != nil && (session.protocol == "cmpp20" || session.protocol == "cmpp21") {
return &cmpp.Cmpp2DeliverReqPkt{
+40 -1
View File
@@ -302,6 +302,43 @@ func TestSubmitResponsePrecedesQueuedFailureReceipt(t *testing.T) {
}
}
func TestReceiptWithoutOriginalSequenceIsUnrecoverable(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
result, err := PushReceiptWithResult(DownstreamReceipt{
DeliveryID: "delivery-history",
Account: "100001",
MessageID: "MSG-HISTORY",
ReceiptStatus: "undelivered",
})
if err != nil {
t.Fatalf("push receipt: %v", err)
}
if result.Sent || result.Retryable || result.ReasonCode != "MISSING_SUBMIT_SEQUENCE_ID" {
t.Fatalf("unexpected result: %+v", result)
}
}
func TestRecoverableReceiptWaitsForClientConnection(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
result, err := PushReceiptWithResult(DownstreamReceipt{
DeliveryID: "delivery-retry",
Account: "100001",
MessageID: "MSG-RETRY",
SubmitSequenceID: 77,
ReceiptStatus: "delivered",
})
if err != nil {
t.Fatalf("push receipt: %v", err)
}
if result.Sent || !result.Retryable || result.ReasonCode != "CLIENT_DISCONNECTED" {
t.Fatalf("unexpected result: %+v", result)
}
}
func TestInboundServerNegotiatesCMPP2AndUsesAuthenticatedAccountForSubmit(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
@@ -571,8 +608,10 @@ func TestRecoverPendingCandidatesWritesWaitingConnectionStatus(t *testing.T) {
_ = json.NewEncoder(w).Encode([]pendingDelivery{{
ID: "delivery-1",
DeliveryType: "receipt",
Payload: json.RawMessage(`{"messageId":"MSG-404","phoneNumber":"13800000001","receiptStatus":"delivered"}`),
Payload: json.RawMessage(`{"account":"100010","messageId":"MSG-404","submitSequenceId":77,"phoneNumber":"13800000001","receiptStatus":"delivered"}`),
}})
case "/api/gateway/events/downstream/failed":
w.WriteHeader(http.StatusOK)
case "/api/gateway/events/downstream/recovery-status":
w.WriteHeader(http.StatusOK)
default: