feat: harden CMPP delivery and platform workflows

This commit is contained in:
hectorzhao
2026-07-20 18:07:29 +08:00
parent 80fb5a8f53
commit f02c33cbb7
61 changed files with 1834 additions and 281 deletions
+75 -52
View File
@@ -44,18 +44,25 @@ type authRequest struct {
}
type submitRequest struct {
Account string `json:"account"`
Account string `json:"account"`
PhoneNumber string `json:"phoneNumber,omitempty"`
PhoneNumbers []string `json:"phoneNumbers,omitempty"`
Content string `json:"content"`
SrcID string `json:"srcId,omitempty"`
DestID string `json:"destId,omitempty"`
SequenceID uint32 `json:"sequenceId,omitempty"`
RemoteIP string `json:"remoteIp,omitempty"`
}
type submitResponseMessage struct {
PhoneNumber string `json:"phoneNumber"`
Content string `json:"content"`
SrcID string `json:"srcId,omitempty"`
DestID string `json:"destId,omitempty"`
SequenceID uint32 `json:"sequenceId,omitempty"`
RemoteIP string `json:"remoteIp,omitempty"`
MessageID string `json:"messageId"`
}
type submitResponse struct {
Accepted bool `json:"accepted"`
MessageID string `json:"messageId"`
Accepted bool `json:"accepted"`
MessageID string `json:"messageId"`
Messages []submitResponseMessage `json:"messages,omitempty"`
}
type authResponse struct {
@@ -68,17 +75,18 @@ type authResponse struct {
}
type DownstreamReceipt struct {
DeliveryID string `json:"deliveryId,omitempty"`
Account string `json:"account,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
MessageID string `json:"messageId"`
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
ReceiptStatus string `json:"receiptStatus"`
RawStatus string `json:"rawStatus,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
SubmitSequenceID uint32 `json:"submitSequenceId,omitempty"`
DeliveredAt string `json:"deliveredAt,omitempty"`
DeliveryID string `json:"deliveryId,omitempty"`
Account string `json:"account,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
MessageID string `json:"messageId"`
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
ReceiptStatus string `json:"receiptStatus"`
RawStatus string `json:"rawStatus,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
SubmitSequenceID uint32 `json:"submitSequenceId,omitempty"`
SubmitGroupMessageID string `json:"submitGroupMessageId,omitempty"`
DeliveredAt string `json:"deliveredAt,omitempty"`
}
type DownstreamUplink struct {
@@ -267,9 +275,13 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
setInboundSubmitResponse(response.Packer, 0, 9)
return false, nil
}
phones := make([]string, len(req.destTerminalIDs))
for index, destination := range req.destTerminalIDs {
phones[index] = strings.TrimSpace(strings.TrimRight(destination, "\x00"))
}
phone := ""
if len(req.destTerminalIDs) > 0 {
phone = strings.TrimRight(req.destTerminalIDs[0], "\x00")
if len(phones) > 0 {
phone = phones[0]
}
remote := packet.Conn.Conn.RemoteAddr()
clientProtocol := defaultString(session.protocol, req.protocol)
@@ -290,13 +302,14 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
contentHash := fmt.Sprintf("%x", md5.Sum([]byte(content)))
startedAt := time.Now()
result, err := s.submit(remote, submitRequest{
Account: account,
PhoneNumber: phone,
Content: content,
SrcID: req.srcID,
DestID: phone,
SequenceID: req.sequenceID,
RemoteIP: remoteIP(remote),
Account: account,
PhoneNumber: phone,
PhoneNumbers: phones,
Content: content,
SrcID: req.srcID,
DestID: phone,
SequenceID: req.sequenceID,
RemoteIP: remoteIP(remote),
})
if err != nil || !result.Accepted {
reason := "api returned accepted=false"
@@ -312,24 +325,34 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
}
gatewayMsgID := messageIDFrom(result.MessageID, req.sequenceID)
setInboundSubmitResponse(response.Packer, gatewayMsgID, 0)
rememberDownstream(downstreamSession{
messageID: result.MessageID,
account: account,
enterpriseCode: session.enterpriseCode,
protocol: clientProtocol,
srcID: strings.TrimSpace(req.srcID),
phoneNumber: phone,
gatewayMsgID: gatewayMsgID,
remoteIP: remoteIP(remote),
connectedAt: time.Now().UTC(),
connectionID: session.connectionID,
conn: packet.Conn,
mu: &sync.Mutex{},
presence: s.PresenceStore,
instanceID: s.gatewayInstanceID(),
report: session.report,
deliveryReport: session.deliveryReport,
})
responseMessages := result.Messages
if len(responseMessages) == 0 {
responseMessages = []submitResponseMessage{{PhoneNumber: phone, MessageID: result.MessageID}}
}
for index, acceptedMessage := range responseMessages {
acceptedPhone := strings.TrimSpace(acceptedMessage.PhoneNumber)
if acceptedPhone == "" && index < len(phones) {
acceptedPhone = phones[index]
}
rememberDownstream(downstreamSession{
messageID: acceptedMessage.MessageID,
account: account,
enterpriseCode: session.enterpriseCode,
protocol: clientProtocol,
srcID: strings.TrimSpace(req.srcID),
phoneNumber: acceptedPhone,
gatewayMsgID: gatewayMsgID,
remoteIP: remoteIP(remote),
connectedAt: time.Now().UTC(),
connectionID: session.connectionID,
conn: packet.Conn,
mu: &sync.Mutex{},
presence: s.PresenceStore,
instanceID: s.gatewayInstanceID(),
report: session.report,
deliveryReport: session.deliveryReport,
})
}
if current := findSessionByConn(packet.Conn); current != nil && current.report != nil {
go current.report(current, "submit", "")
}
@@ -344,8 +367,8 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
}()
}
logger.Printf(
"cmpp inbound event=submit_accepted protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s result=0 message_id=%s gateway_message_id=%d duration_ms=%d content_chars=%d content_hash=%s",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, result.MessageID, gatewayMsgID, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash,
"cmpp inbound event=submit_accepted protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s dest_count=%d accepted_count=%d result=0 message_id=%s gateway_message_id=%d duration_ms=%d content_chars=%d content_hash=%s",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, len(phones), len(responseMessages), result.MessageID, gatewayMsgID, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash,
)
return false, nil
}
@@ -742,9 +765,9 @@ func forgetDownstream(session *downstreamSession) {
return
}
downstreamRegistry.Lock()
if session.messageID != "" {
if current := downstreamRegistry.byMessageID[session.messageID]; current == session {
delete(downstreamRegistry.byMessageID, session.messageID)
for messageID, current := range downstreamRegistry.byMessageID {
if current != nil && current.conn == session.conn {
delete(downstreamRegistry.byMessageID, messageID)
}
}
if session.account != "" {
@@ -1003,7 +1026,7 @@ func recoverReceiptSession(event DownstreamReceipt) *downstreamSession {
}
recovered := *accountSession
recovered.messageID = event.MessageID
recovered.gatewayMsgID = messageIDFrom(event.MessageID, event.SubmitSequenceID)
recovered.gatewayMsgID = messageIDFrom(defaultString(event.SubmitGroupMessageID, event.MessageID), event.SubmitSequenceID)
return &recovered
}
+24 -7
View File
@@ -9,6 +9,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"sync"
"testing"
@@ -112,7 +113,13 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&gotSubmit); err != nil {
t.Fatalf("decode submit: %v", err)
}
_ = json.NewEncoder(w).Encode(submitResponse{Accepted: true, MessageID: "MSG-1"})
_ = json.NewEncoder(w).Encode(submitResponse{
Accepted: true, MessageID: "MSG-1",
Messages: []submitResponseMessage{
{PhoneNumber: "13500002696", MessageID: "MSG-1"},
{PhoneNumber: "13600002696", MessageID: "MSG-2"},
},
})
case "/api/gateway/events/downstream/pending":
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
case "/api/gateway/events/inbound/connection":
@@ -174,8 +181,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
FeeType: "02",
FeeCode: "0",
SrcId: "10690000",
DestUsrTl: 1,
DestTerminalId: []string{"13500002696"},
DestUsrTl: 2,
DestTerminalId: []string{"13500002696", "13600002696"},
MsgLength: uint8(len(content)),
MsgContent: content,
})
@@ -188,8 +195,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
}
sendResult, err := PushReceiptWithResult(DownstreamReceipt{
DeliveryID: "delivery-1",
MessageID: "MSG-1",
PhoneNumber: "13500002696",
MessageID: "MSG-2",
PhoneNumber: "13600002696",
ReceiptStatus: "delivered",
DeliveredAt: time.Now().UTC().Format(time.RFC3339Nano),
})
@@ -204,7 +211,7 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
if err := receipt.Unpack([]byte(deliver.MsgContent)); err != nil {
t.Fatalf("unpack pushed receipt: %v", err)
}
if receipt.Stat != "DELIVRD" || receipt.DestTerminalId != "13500002696" {
if receipt.Stat != "DELIVRD" || receipt.DestTerminalId != "13600002696" || receipt.MsgId != rsp.MsgId {
t.Fatalf("unexpected pushed receipt: %+v", receipt)
}
if err := client.SendRspPkt(&cmpp.Cmpp3DeliverRspPkt{MsgId: deliver.MsgId, Result: 0}, deliver.SeqId); err != nil {
@@ -221,7 +228,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
if gotAuth.Account != account || gotAuth.AuthSource == "" || gotAuth.RemoteIP == "" {
t.Fatalf("unexpected auth payload: %+v", gotAuth)
}
if gotSubmit.Account != account || gotSubmit.PhoneNumber != "13500002696" || gotSubmit.Content != "测试入站" {
if gotSubmit.Account != account || gotSubmit.PhoneNumber != "13500002696" || gotSubmit.Content != "测试入站" ||
!reflect.DeepEqual(gotSubmit.PhoneNumbers, []string{"13500002696", "13600002696"}) {
t.Fatalf("unexpected submit payload: %+v", gotSubmit)
}
}
@@ -742,6 +750,15 @@ func TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists(t *testi
if recovered.gatewayMsgID != messageIDFrom("MSG-NOT-REMEMBERED", 1216579149) || recovered.gatewayMsgID == 0 {
t.Fatalf("unexpected recovered Msg_Id: %d", recovered.gatewayMsgID)
}
first := recoverReceiptSession(DownstreamReceipt{
MessageID: "MSG-FIRST", SubmitGroupMessageID: "MSG-GROUP", Account: "100001", SubmitSequenceID: 77,
})
second := recoverReceiptSession(DownstreamReceipt{
MessageID: "MSG-SECOND", SubmitGroupMessageID: "MSG-GROUP", Account: "100001", SubmitSequenceID: 77,
})
if first == nil || second == nil || first.gatewayMsgID != second.gatewayMsgID || first.gatewayMsgID != messageIDFrom("MSG-GROUP", 77) {
t.Fatalf("multi-destination recovery did not preserve the original Msg_Id: first=%+v second=%+v", first, second)
}
}
func TestSendDownstreamRejectsZeroMessageID(t *testing.T) {