388 lines
12 KiB
Go
388 lines
12 KiB
Go
package upstream
|
|
|
|
import (
|
|
"cmpp-platform/gateway/internal/queue"
|
|
"context"
|
|
"fmt"
|
|
cmpp "github.com/bigwhite/gocmpp"
|
|
cmpputils "github.com/bigwhite/gocmpp/utils"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Segment callbacks are emitted in submission order. The aggregate result keeps
|
|
// the first sequence and Msg_Id while retaining every segment result for billing.
|
|
|
|
func (m *Manager) Submit(ctx context.Context, cmd queue.SubmitCommand) (queue.SubmitResult, error) {
|
|
if err := validateSubmitCommand(cmd); err != nil {
|
|
result := submitResult(cmd, 0, "", "rejected", "INVALID_COMMAND", err.Error())
|
|
if postErr := m.post(ctx, "/gateway/events/submit-result", result); postErr != nil {
|
|
return result, postErr
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
pool, err := m.connectionFor(cmd)
|
|
if err != nil {
|
|
result := submitResult(cmd, 0, "", "rejected", "CONNECT_FAILED", err.Error())
|
|
if postErr := m.post(ctx, "/gateway/events/submit-result", result); postErr != nil {
|
|
return result, postErr
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
result, err := pool.submit(ctx, cmd, func(segment queue.SubmitSegmentResult) {
|
|
payload := struct {
|
|
queue.Envelope
|
|
SubmitID string `json:"submitId,omitempty"`
|
|
queue.SubmitSegmentResult
|
|
}{
|
|
Envelope: cmd.Envelope,
|
|
SubmitID: cmd.SubmitID,
|
|
SubmitSegmentResult: segment,
|
|
}
|
|
callbackCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
postErr := m.post(callbackCtx, "/gateway/events/submit-segment-result", payload)
|
|
cancel()
|
|
if postErr != nil {
|
|
log.Printf(
|
|
"protocol_event protocol=cmpp direction=gateway_to_api event=submit_segment_result status=forward_failed channel_id=%s message_id=%s segment=%d/%d error=%q",
|
|
cmd.ChannelID, cmd.MessageID, segment.SegmentIndex, segment.SegmentTotal, postErr,
|
|
)
|
|
}
|
|
})
|
|
if err != nil {
|
|
if postErr := m.post(ctx, "/gateway/events/submit-result", result); postErr != nil {
|
|
return result, postErr
|
|
}
|
|
return result, err
|
|
}
|
|
if err := m.post(ctx, "/gateway/events/submit-result", result); err != nil {
|
|
return result, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (p *connectionPool) submit(
|
|
ctx context.Context,
|
|
cmd queue.SubmitCommand,
|
|
onSegment func(queue.SubmitSegmentResult),
|
|
) (queue.SubmitResult, error) {
|
|
parts, err := splitSubmitContent(cmd.CMPP.MsgFmt, cmd.Content)
|
|
if err != nil {
|
|
result := submitResult(cmd, 0, "", "rejected", "ENCODE_FAILED", err.Error())
|
|
return result, err
|
|
}
|
|
|
|
var firstSequence uint32
|
|
var firstGatewayMessageID string
|
|
segments := make([]queue.SubmitSegmentResult, 0, len(parts))
|
|
for _, part := range parts {
|
|
conn, release, err := p.acquireConnection(ctx)
|
|
if err != nil {
|
|
result := submitResult(cmd, 0, "", "timeout", "WINDOW_TIMEOUT", err.Error())
|
|
result.Segments = segments
|
|
return result, err
|
|
}
|
|
seq, gatewayMessageID, result, err := conn.submitPart(ctx, cmd, part)
|
|
release()
|
|
segment := submitSegmentResult(part, seq, gatewayMessageID, result)
|
|
segments = append(segments, segment)
|
|
if onSegment != nil {
|
|
onSegment(segment)
|
|
}
|
|
if firstSequence == 0 {
|
|
firstSequence = seq
|
|
}
|
|
if firstGatewayMessageID == "" {
|
|
firstGatewayMessageID = gatewayMessageID
|
|
}
|
|
if err != nil {
|
|
result.Segments = segments
|
|
return result, err
|
|
}
|
|
if result.SubmitStatus != "accepted" {
|
|
result.Segments = segments
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
result := submitResult(cmd, firstSequence, firstGatewayMessageID, "accepted", "", "")
|
|
result.Segments = segments
|
|
return result, nil
|
|
}
|
|
|
|
func (c *connection) submitPart(ctx context.Context, cmd queue.SubmitCommand, part submitPart) (uint32, string, queue.SubmitResult, error) {
|
|
rspCh := make(chan submitPartResponse, 1)
|
|
pkt := c.submitRequestPacket(cmd, part)
|
|
|
|
c.mu.Lock()
|
|
client := c.client
|
|
closed := c.closed
|
|
c.mu.Unlock()
|
|
if closed || client == nil {
|
|
err := fmt.Errorf("supplier connection is not available")
|
|
result := submitResult(cmd, 0, "", "timeout", "CONNECTION_LOST", err.Error())
|
|
return 0, "", result, err
|
|
}
|
|
c.sendMu.Lock()
|
|
seq, err := client.SendReqPkt(pkt)
|
|
c.sendMu.Unlock()
|
|
if err != nil {
|
|
c.emitProtocolLog(protocolLogEvent{
|
|
Protocol: "cmpp",
|
|
Direction: "platform_to_channel",
|
|
EventType: "submit",
|
|
Status: "failed",
|
|
TenantID: cmd.TenantID,
|
|
ApplicationID: cmd.ApplicationID,
|
|
ChannelID: cmd.ChannelID,
|
|
Account: c.config.Account,
|
|
MessageID: cmd.MessageID,
|
|
Phone: cmd.PhoneNumber,
|
|
ResultCode: "SEND_FAILED",
|
|
PayloadBytes: len(part.MsgContent),
|
|
Detail: map[string]any{
|
|
"segmentTotal": part.PkTotal,
|
|
"segmentIndex": part.PkNumber,
|
|
},
|
|
})
|
|
c.close()
|
|
result := submitResult(cmd, 0, "", "timeout", "SEND_FAILED", err.Error())
|
|
return 0, "", result, err
|
|
}
|
|
c.emitProtocolLog(protocolLogEvent{
|
|
Protocol: "cmpp",
|
|
Direction: "platform_to_channel",
|
|
EventType: "submit",
|
|
Status: "success",
|
|
TenantID: cmd.TenantID,
|
|
ApplicationID: cmd.ApplicationID,
|
|
ChannelID: cmd.ChannelID,
|
|
Account: c.config.Account,
|
|
MessageID: cmd.MessageID,
|
|
Phone: cmd.PhoneNumber,
|
|
PayloadBytes: len(part.MsgContent),
|
|
Detail: map[string]any{
|
|
"sequenceId": seq,
|
|
"segmentTotal": part.PkTotal,
|
|
"segmentIndex": part.PkNumber,
|
|
},
|
|
})
|
|
|
|
c.mu.Lock()
|
|
c.pending[seq] = rspCh
|
|
c.mu.Unlock()
|
|
defer func() {
|
|
c.mu.Lock()
|
|
delete(c.pending, seq)
|
|
c.mu.Unlock()
|
|
}()
|
|
|
|
waitCtx, cancel := context.WithTimeout(ctx, defaultSubmitTimeout)
|
|
defer cancel()
|
|
select {
|
|
case <-waitCtx.Done():
|
|
result := submitResult(cmd, seq, "", "timeout", "SUBMIT_TIMEOUT", waitCtx.Err().Error())
|
|
return seq, "", result, waitCtx.Err()
|
|
case rsp := <-rspCh:
|
|
if rsp.err != nil {
|
|
result := submitResult(cmd, seq, "", "timeout", "CONNECTION_LOST", rsp.err.Error())
|
|
return seq, "", result, rsp.err
|
|
}
|
|
gatewayMessageID := fmt.Sprint(rsp.msgID)
|
|
status := "accepted"
|
|
errorCode := ""
|
|
errorMessage := ""
|
|
if rsp.result != 0 {
|
|
status = "rejected"
|
|
errorCode = fmt.Sprint(rsp.result)
|
|
errorMessage = fmt.Sprintf("upstream submit rejected with result %d", rsp.result)
|
|
}
|
|
c.emitProtocolLog(protocolLogEvent{
|
|
Protocol: "cmpp",
|
|
Direction: "channel_to_platform",
|
|
EventType: "submit_resp",
|
|
Status: "success",
|
|
TenantID: cmd.TenantID,
|
|
ApplicationID: cmd.ApplicationID,
|
|
ChannelID: cmd.ChannelID,
|
|
Account: c.config.Account,
|
|
MessageID: cmd.MessageID,
|
|
GatewayMessageID: gatewayMessageID,
|
|
Phone: cmd.PhoneNumber,
|
|
ResultCode: fmt.Sprint(rsp.result),
|
|
Detail: map[string]any{
|
|
"sequenceId": rsp.seqID,
|
|
"segmentTotal": part.PkTotal,
|
|
"segmentIndex": part.PkNumber,
|
|
},
|
|
})
|
|
if rsp.result == 0 {
|
|
c.mu.Lock()
|
|
c.tracker[rsp.msgID] = cmd
|
|
c.mu.Unlock()
|
|
}
|
|
return seq, gatewayMessageID, submitResult(cmd, seq, gatewayMessageID, status, errorCode, errorMessage), nil
|
|
}
|
|
}
|
|
|
|
func (c *connection) submitRequestPacket(cmd queue.SubmitCommand, part submitPart) cmpp.Packer {
|
|
base := submitRequestFields{
|
|
PkTotal: part.PkTotal,
|
|
PkNumber: part.PkNumber,
|
|
TpUdhi: part.TpUdhi,
|
|
RegisteredDelivery: uint8(cmd.CMPP.RegisteredDelivery),
|
|
MsgLevel: 1,
|
|
ServiceId: cmd.CMPP.ServiceID,
|
|
FeeUserType: uint8(defaultInt(cmd.CMPP.FeeUserType, 2)),
|
|
FeeTerminalId: cmd.PhoneNumber,
|
|
MsgFmt: uint8(cmd.CMPP.MsgFmt),
|
|
MsgSrc: c.config.Account,
|
|
FeeType: defaultString(cmd.CMPP.FeeType, "02"),
|
|
FeeCode: defaultString(cmd.CMPP.FeeCode, "0"),
|
|
SrcId: cmd.CMPP.SrcID,
|
|
DestUsrTl: 1,
|
|
DestTerminalId: []string{cmd.PhoneNumber},
|
|
MsgLength: uint8(len(part.MsgContent)),
|
|
MsgContent: part.MsgContent,
|
|
}
|
|
if protocolVersion(c.config.CMPPVersion) == cmpp.V20 {
|
|
return &cmpp.Cmpp2SubmitReqPkt{
|
|
PkTotal: base.PkTotal,
|
|
PkNumber: base.PkNumber,
|
|
RegisteredDelivery: base.RegisteredDelivery,
|
|
MsgLevel: base.MsgLevel,
|
|
ServiceId: base.ServiceId,
|
|
FeeUserType: base.FeeUserType,
|
|
FeeTerminalId: base.FeeTerminalId,
|
|
TpUdhi: base.TpUdhi,
|
|
MsgFmt: base.MsgFmt,
|
|
MsgSrc: base.MsgSrc,
|
|
FeeType: base.FeeType,
|
|
FeeCode: base.FeeCode,
|
|
SrcId: base.SrcId,
|
|
DestUsrTl: base.DestUsrTl,
|
|
DestTerminalId: base.DestTerminalId,
|
|
MsgLength: base.MsgLength,
|
|
MsgContent: base.MsgContent,
|
|
}
|
|
}
|
|
return &cmpp.Cmpp3SubmitReqPkt{
|
|
PkTotal: base.PkTotal,
|
|
PkNumber: base.PkNumber,
|
|
RegisteredDelivery: base.RegisteredDelivery,
|
|
MsgLevel: base.MsgLevel,
|
|
ServiceId: base.ServiceId,
|
|
FeeUserType: base.FeeUserType,
|
|
FeeTerminalId: base.FeeTerminalId,
|
|
TpUdhi: base.TpUdhi,
|
|
MsgFmt: base.MsgFmt,
|
|
MsgSrc: base.MsgSrc,
|
|
FeeType: base.FeeType,
|
|
FeeCode: base.FeeCode,
|
|
SrcId: base.SrcId,
|
|
DestUsrTl: base.DestUsrTl,
|
|
DestTerminalId: base.DestTerminalId,
|
|
MsgLength: base.MsgLength,
|
|
MsgContent: base.MsgContent,
|
|
}
|
|
}
|
|
|
|
type submitRequestFields struct {
|
|
PkTotal uint8
|
|
PkNumber uint8
|
|
RegisteredDelivery uint8
|
|
MsgLevel uint8
|
|
ServiceId string
|
|
FeeUserType uint8
|
|
FeeTerminalId string
|
|
TpUdhi uint8
|
|
MsgFmt uint8
|
|
MsgSrc string
|
|
FeeType string
|
|
FeeCode string
|
|
SrcId string
|
|
DestUsrTl uint8
|
|
DestTerminalId []string
|
|
MsgLength uint8
|
|
MsgContent string
|
|
}
|
|
|
|
func submitResult(cmd queue.SubmitCommand, sequenceID uint32, gatewayMessageID string, status string, code string, message string) queue.SubmitResult {
|
|
if gatewayMessageID == "" {
|
|
gatewayMessageID = fmt.Sprintf("GW-%s-%d", cmd.SubmitID, time.Now().UnixNano())
|
|
}
|
|
return queue.SubmitResult{
|
|
Envelope: queue.Envelope{
|
|
SchemaVersion: queue.SchemaVersion,
|
|
MessageType: queue.MessageTypeSubmitResult,
|
|
TraceID: cmd.TraceID,
|
|
MessageID: cmd.MessageID,
|
|
ChannelID: cmd.ChannelID,
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
SubmitID: cmd.SubmitID,
|
|
SequenceID: sequenceID,
|
|
GatewayMessageID: gatewayMessageID,
|
|
SubmitStatus: status,
|
|
ErrorCode: code,
|
|
ErrorMessage: message,
|
|
SubmittedAt: time.Now().UTC(),
|
|
}
|
|
}
|
|
|
|
func submitSegmentResult(part submitPart, sequenceID uint32, gatewayMessageID string, result queue.SubmitResult) queue.SubmitSegmentResult {
|
|
if gatewayMessageID == "" {
|
|
gatewayMessageID = result.GatewayMessageID
|
|
}
|
|
return queue.SubmitSegmentResult{
|
|
SegmentTotal: int(part.PkTotal),
|
|
SegmentIndex: int(part.PkNumber),
|
|
SequenceID: sequenceID,
|
|
GatewayMessageID: gatewayMessageID,
|
|
SubmitStatus: result.SubmitStatus,
|
|
ErrorCode: result.ErrorCode,
|
|
ErrorMessage: result.ErrorMessage,
|
|
SubmittedAt: result.SubmittedAt,
|
|
}
|
|
}
|
|
|
|
func validateSubmitCommand(cmd queue.SubmitCommand) error {
|
|
if cmd.MessageType != queue.MessageTypeSubmitCommand {
|
|
return fmt.Errorf("unsupported messageType %q", cmd.MessageType)
|
|
}
|
|
if cmd.MessageID == "" || cmd.ChannelID == "" || cmd.SubmitID == "" {
|
|
return fmt.Errorf("messageId, channelId and submitId are required")
|
|
}
|
|
if cmd.Upstream.GatewayHost == "" || cmd.Upstream.GatewayPort <= 0 {
|
|
return fmt.Errorf("upstream gatewayHost and gatewayPort are required")
|
|
}
|
|
if cmd.Upstream.Account == "" || cmd.Upstream.PasswordCipher == "" {
|
|
return fmt.Errorf("upstream account and passwordCipher are required")
|
|
}
|
|
if len(cmd.PhoneNumber) == 0 || len(cmd.Content) == 0 {
|
|
return fmt.Errorf("phoneNumber and content are required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func encodeContent(format int, content string) (string, error) {
|
|
switch format {
|
|
case 8:
|
|
return cmpputils.Utf8ToUcs2(content)
|
|
case 15:
|
|
return cmpputils.Utf8ToGB18030(content)
|
|
default:
|
|
return content, nil
|
|
}
|
|
}
|
|
|
|
func protocolVersion(version string) cmpp.Type {
|
|
if strings.HasPrefix(version, "2") {
|
|
return cmpp.V20
|
|
}
|
|
return cmpp.V30
|
|
}
|