fix: handle cmpp2 deliver receipts
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- 已执行:`npm --prefix api test -- channels.service.spec.ts --runInBand`、`npm --prefix api run build`、`npm run build`。待生产部署后用指定号码做一次真实发送验证,并回查 DB/短信记录。
|
- 已执行:`npm --prefix api test -- channels.service.spec.ts --runInBand`、`npm --prefix api run build`、`npm run build`。待生产部署后用指定号码做一次真实发送验证,并回查 DB/短信记录。
|
||||||
- 2026-07-09 追加:按产品边界收窄通道测试短信,`SmsMessageRecord/SmsSubmitRecord/SmsReceiptRecord/SmsMessageSegmentAudit` 支持 `tenantId/batchTaskId` 为空;通道测试只记录短信、提交结果和回执,不进入企业账务、发送任务进度、客户侧 Deliver 推送或业务补发。
|
- 2026-07-09 追加:按产品边界收窄通道测试短信,`SmsMessageRecord/SmsSubmitRecord/SmsReceiptRecord/SmsMessageSegmentAudit` 支持 `tenantId/batchTaskId` 为空;通道测试只记录短信、提交结果和回执,不进入企业账务、发送任务进度、客户侧 Deliver 推送或业务补发。
|
||||||
- 2026-07-09 追加:生产验证短信进入 Gateway 后出现 `SUBMIT_TIMEOUT/context deadline exceeded`,上游平台疑似内容乱码;排查确认 API、数据库和 Redis Stream 中中文内容正常,`SubmitCommand.cmpp.msgFmt=8`,根因为 Gateway 在 CMPP 2.0 通道上仍固定构造 `Cmpp3SubmitReqPkt`。已修复为按通道 `cmppVersion` 分别构造 `Cmpp2SubmitReqPkt`/`Cmpp3SubmitReqPkt`,并同时处理 CMPP 2.0/3.0 SubmitResp。新增 `submit_packet_test.go` 覆盖 CMPP 2.0 中文 UCS2 Submit 包类型和长度。
|
- 2026-07-09 追加:生产验证短信进入 Gateway 后出现 `SUBMIT_TIMEOUT/context deadline exceeded`,上游平台疑似内容乱码;排查确认 API、数据库和 Redis Stream 中中文内容正常,`SubmitCommand.cmpp.msgFmt=8`,根因为 Gateway 在 CMPP 2.0 通道上仍固定构造 `Cmpp3SubmitReqPkt`。已修复为按通道 `cmppVersion` 分别构造 `Cmpp2SubmitReqPkt`/`Cmpp3SubmitReqPkt`,并同时处理 CMPP 2.0/3.0 SubmitResp。新增 `submit_packet_test.go` 覆盖 CMPP 2.0 中文 UCS2 Submit 包类型和长度。
|
||||||
|
- 2026-07-09 追加:生产验证 Submit 已 accepted 后未见 `SmsReceiptRecord`,结合上游为 CMPP 2.0 排查 Gateway readLoop,确认只处理 `Cmpp3DeliverReqPkt`,CMPP 2.0 回执 Deliver 即使到达连接也不会 ACK 或进入 receipt 事件链路。已修复为同时处理 `Cmpp2DeliverReqPkt`/`Cmpp3DeliverReqPkt`,分别返回 `Cmpp2DeliverRspPkt`/`Cmpp3DeliverRspPkt`,并统一 Deliver 解码、回执和上行处理。新增 `deliver_test.go` 覆盖 CMPP 2.0 `DELIVRD` 回执写入事件链路。
|
||||||
|
|
||||||
## 2026-07-09 通道复制默认停用与真实连接池状态回写修复
|
## 2026-07-09 通道复制默认停用与真实连接池状态回写修复
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package upstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cmpp-platform/gateway/internal/queue"
|
||||||
|
|
||||||
|
cmpp "github.com/bigwhite/gocmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleCMPP2DeliverReceiptPostsReceiptEvent(t *testing.T) {
|
||||||
|
events := make(chan queue.ReceiptEvent, 1)
|
||||||
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/gateway/events/receipt" {
|
||||||
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
var event queue.ReceiptEvent
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
||||||
|
t.Fatalf("decode receipt event: %v", err)
|
||||||
|
}
|
||||||
|
events <- event
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
receipt := cmpp.CmppReceiptPkt{
|
||||||
|
MsgId: 8412634832294102675,
|
||||||
|
Stat: "DELIVRD",
|
||||||
|
SubmitTime: "2607091558",
|
||||||
|
DoneTime: "2607091559",
|
||||||
|
DestTerminalId: "18821203795",
|
||||||
|
SmscSequence: 1,
|
||||||
|
}
|
||||||
|
payload, err := receipt.Pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pack receipt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &connection{
|
||||||
|
channelID: "channel-1",
|
||||||
|
apiBaseURL: api.URL,
|
||||||
|
httpClient: api.Client(),
|
||||||
|
tracker: map[uint64]queue.SubmitCommand{
|
||||||
|
receipt.MsgId: {
|
||||||
|
Envelope: queue.Envelope{
|
||||||
|
SchemaVersion: queue.SchemaVersion,
|
||||||
|
MessageType: queue.MessageTypeSubmitCommand,
|
||||||
|
TraceID: "trace-1",
|
||||||
|
MessageID: "message-1",
|
||||||
|
ChannelID: "channel-1",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
},
|
||||||
|
SubmitID: "submit-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
conn.handleDeliver(deliverPacketFromCMPP2(&cmpp.Cmpp2DeliverReqPkt{
|
||||||
|
SeqId: 7,
|
||||||
|
MsgId: 999,
|
||||||
|
RegisterDelivery: 1,
|
||||||
|
MsgContent: string(payload),
|
||||||
|
}))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-events:
|
||||||
|
if event.MessageID != "message-1" || event.ChannelID != "channel-1" || event.TraceID != "trace-1" {
|
||||||
|
t.Fatalf("unexpected envelope: %+v", event.Envelope)
|
||||||
|
}
|
||||||
|
if event.SequenceID != 7 {
|
||||||
|
t.Fatalf("SequenceID = %d, want 7", event.SequenceID)
|
||||||
|
}
|
||||||
|
if event.GatewayMessageID != "8412634832294102675" {
|
||||||
|
t.Fatalf("GatewayMessageID = %q", event.GatewayMessageID)
|
||||||
|
}
|
||||||
|
if event.PhoneNumber != "18821203795" {
|
||||||
|
t.Fatalf("PhoneNumber = %q", event.PhoneNumber)
|
||||||
|
}
|
||||||
|
if event.ReceiptStatus != "delivered" || event.RawStatus != "DELIVRD" {
|
||||||
|
t.Fatalf("unexpected receipt status: %+v", event)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("timed out waiting for receipt event")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -605,25 +605,64 @@ func (c *connection) readLoop() {
|
|||||||
if ch != nil {
|
if ch != nil {
|
||||||
ch <- submitPartResponse{seqID: p.SeqId, msgID: p.MsgId, result: p.Result}
|
ch <- submitPartResponse{seqID: p.SeqId, msgID: p.MsgId, result: p.Result}
|
||||||
}
|
}
|
||||||
|
case *cmpp.Cmpp2DeliverReqPkt:
|
||||||
|
_ = c.client.SendRspPkt(&cmpp.Cmpp2DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
|
||||||
|
c.handleDeliver(deliverPacketFromCMPP2(p))
|
||||||
case *cmpp.Cmpp3DeliverReqPkt:
|
case *cmpp.Cmpp3DeliverReqPkt:
|
||||||
c.handleDeliver(p)
|
_ = c.client.SendRspPkt(&cmpp.Cmpp3DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
|
||||||
|
c.handleDeliver(deliverPacketFromCMPP3(p))
|
||||||
case *cmpp.CmppActiveTestReqPkt:
|
case *cmpp.CmppActiveTestReqPkt:
|
||||||
_ = c.client.SendRspPkt(&cmpp.CmppActiveTestRspPkt{}, p.SeqId)
|
_ = c.client.SendRspPkt(&cmpp.CmppActiveTestRspPkt{}, p.SeqId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connection) handleDeliver(pkt *cmpp.Cmpp3DeliverReqPkt) {
|
type deliverPacket struct {
|
||||||
_ = c.client.SendRspPkt(&cmpp.Cmpp3DeliverRspPkt{MsgId: pkt.MsgId, Result: 0}, pkt.SeqId)
|
seqID uint32
|
||||||
|
msgID uint64
|
||||||
|
destID string
|
||||||
|
tpUdhi uint8
|
||||||
|
msgFmt uint8
|
||||||
|
srcTerminalID string
|
||||||
|
registerDelivery uint8
|
||||||
|
msgContent string
|
||||||
|
}
|
||||||
|
|
||||||
if pkt.RegisterDelivery == 1 {
|
func deliverPacketFromCMPP2(pkt *cmpp.Cmpp2DeliverReqPkt) deliverPacket {
|
||||||
|
return deliverPacket{
|
||||||
|
seqID: pkt.SeqId,
|
||||||
|
msgID: pkt.MsgId,
|
||||||
|
destID: pkt.DestId,
|
||||||
|
tpUdhi: pkt.TpUdhi,
|
||||||
|
msgFmt: pkt.MsgFmt,
|
||||||
|
srcTerminalID: pkt.SrcTerminalId,
|
||||||
|
registerDelivery: pkt.RegisterDelivery,
|
||||||
|
msgContent: pkt.MsgContent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deliverPacketFromCMPP3(pkt *cmpp.Cmpp3DeliverReqPkt) deliverPacket {
|
||||||
|
return deliverPacket{
|
||||||
|
seqID: pkt.SeqId,
|
||||||
|
msgID: pkt.MsgId,
|
||||||
|
destID: pkt.DestId,
|
||||||
|
tpUdhi: pkt.TpUdhi,
|
||||||
|
msgFmt: pkt.MsgFmt,
|
||||||
|
srcTerminalID: pkt.SrcTerminalId,
|
||||||
|
registerDelivery: pkt.RegisterDelivery,
|
||||||
|
msgContent: pkt.MsgContent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connection) handleDeliver(pkt deliverPacket) {
|
||||||
|
if pkt.registerDelivery == 1 {
|
||||||
var receipt cmpp.CmppReceiptPkt
|
var receipt cmpp.CmppReceiptPkt
|
||||||
if err := receipt.Unpack([]byte(pkt.MsgContent)); err != nil {
|
if err := receipt.Unpack([]byte(pkt.msgContent)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cmd, ok := c.commandFor(receipt.MsgId)
|
cmd, ok := c.commandFor(receipt.MsgId)
|
||||||
if !ok {
|
if !ok {
|
||||||
cmd, ok = c.commandFor(pkt.MsgId)
|
cmd, ok = c.commandFor(pkt.msgID)
|
||||||
}
|
}
|
||||||
traceID := fmt.Sprintf("receipt-%d", receipt.MsgId)
|
traceID := fmt.Sprintf("receipt-%d", receipt.MsgId)
|
||||||
messageID := fmt.Sprintf("receipt-%d", receipt.MsgId)
|
messageID := fmt.Sprintf("receipt-%d", receipt.MsgId)
|
||||||
@@ -642,7 +681,7 @@ func (c *connection) handleDeliver(pkt *cmpp.Cmpp3DeliverReqPkt) {
|
|||||||
ChannelID: channelID,
|
ChannelID: channelID,
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
},
|
},
|
||||||
SequenceID: pkt.SeqId,
|
SequenceID: pkt.seqID,
|
||||||
GatewayMessageID: fmt.Sprint(receipt.MsgId),
|
GatewayMessageID: fmt.Sprint(receipt.MsgId),
|
||||||
PhoneNumber: strings.TrimSpace(receipt.DestTerminalId),
|
PhoneNumber: strings.TrimSpace(receipt.DestTerminalId),
|
||||||
ReceiptStatus: receiptStatus(receipt.Stat),
|
ReceiptStatus: receiptStatus(receipt.Stat),
|
||||||
@@ -660,7 +699,7 @@ func (c *connection) handleDeliver(pkt *cmpp.Cmpp3DeliverReqPkt) {
|
|||||||
if !complete {
|
if !complete {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cmd, _ := c.commandFor(pkt.MsgId)
|
cmd, _ := c.commandFor(pkt.msgID)
|
||||||
event := queue.UplinkEvent{
|
event := queue.UplinkEvent{
|
||||||
Envelope: queue.Envelope{
|
Envelope: queue.Envelope{
|
||||||
SchemaVersion: queue.SchemaVersion,
|
SchemaVersion: queue.SchemaVersion,
|
||||||
@@ -670,32 +709,32 @@ func (c *connection) handleDeliver(pkt *cmpp.Cmpp3DeliverReqPkt) {
|
|||||||
ChannelID: c.channelID,
|
ChannelID: c.channelID,
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
},
|
},
|
||||||
SequenceID: pkt.SeqId,
|
SequenceID: pkt.seqID,
|
||||||
PhoneNumber: strings.TrimSpace(pkt.SrcTerminalId),
|
PhoneNumber: strings.TrimSpace(pkt.srcTerminalID),
|
||||||
DestID: strings.TrimSpace(pkt.DestId),
|
DestID: strings.TrimSpace(pkt.destID),
|
||||||
Content: content,
|
Content: content,
|
||||||
ReceivedAt: time.Now().UTC(),
|
ReceivedAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
_ = postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/uplink", event)
|
_ = postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/uplink", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connection) decodeUplinkContent(pkt *cmpp.Cmpp3DeliverReqPkt) (string, bool, error) {
|
func (c *connection) decodeUplinkContent(pkt deliverPacket) (string, bool, error) {
|
||||||
if pkt.TpUdhi != 1 {
|
if pkt.tpUdhi != 1 {
|
||||||
content, err := decodeContent(pkt.MsgFmt, pkt.MsgContent)
|
content, err := decodeContent(pkt.msgFmt, pkt.msgContent)
|
||||||
return content, true, err
|
return content, true, err
|
||||||
}
|
}
|
||||||
ref, total, number, payload, ok := parseConcatSegment(pkt.MsgContent)
|
ref, total, number, payload, ok := parseConcatSegment(pkt.msgContent)
|
||||||
if !ok {
|
if !ok {
|
||||||
content, err := decodeContent(pkt.MsgFmt, pkt.MsgContent)
|
content, err := decodeContent(pkt.msgFmt, pkt.msgContent)
|
||||||
return content, true, err
|
return content, true, err
|
||||||
}
|
}
|
||||||
key := fmt.Sprintf("%s:%s:%s:%d:%d", c.channelID, strings.TrimSpace(pkt.SrcTerminalId), strings.TrimSpace(pkt.DestId), ref, total)
|
key := fmt.Sprintf("%s:%s:%s:%d:%d", c.channelID, strings.TrimSpace(pkt.srcTerminalID), strings.TrimSpace(pkt.destID), ref, total)
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.longUplink == nil {
|
if c.longUplink == nil {
|
||||||
c.longUplink = make(map[string]*longUplinkAssembly)
|
c.longUplink = make(map[string]*longUplinkAssembly)
|
||||||
}
|
}
|
||||||
pruneLongUplinkAssemblies(c.longUplink, time.Now(), 10*time.Minute)
|
pruneLongUplinkAssemblies(c.longUplink, time.Now(), 10*time.Minute)
|
||||||
content, complete, err := assembleLongUplink(c.longUplink, key, pkt.MsgFmt, total, number, payload)
|
content, complete, err := assembleLongUplink(c.longUplink, key, pkt.msgFmt, total, number, payload)
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return content, complete, err
|
return content, complete, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user