feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user