|
|
@@ -0,0 +1,223 @@
|
|
|
+import axios from 'axios'
|
|
|
+import { FastifyInstance } from 'fastify'
|
|
|
+import { GetSmsService } from './get-sms-service'
|
|
|
+import { XMLParser } from 'fast-xml-parser'
|
|
|
+import { SendSmsResult, PhoneStatus, GetReportResult } from './sms.types'
|
|
|
+
|
|
|
+const axiosInstance = axios.create({
|
|
|
+ baseURL: 'http://114.199.71.130:8138/'
|
|
|
+})
|
|
|
+
|
|
|
+const MKT_USER_NAME = 'JIAXINMKT'
|
|
|
+const MKT_PASSWORD = 'RB7EAo3d'
|
|
|
+
|
|
|
+const OTP_USER_NAME = 'JIAXINOTP'
|
|
|
+const OTP_PASSWORD = 'OgXb8X4q'
|
|
|
+
|
|
|
+const DEFAULT_CALLER = 10123456789
|
|
|
+
|
|
|
+export class xinsService extends GetSmsService {
|
|
|
+ private xmlParser: XMLParser
|
|
|
+ private app: FastifyInstance
|
|
|
+
|
|
|
+ constructor(app: FastifyInstance) {
|
|
|
+ super()
|
|
|
+ this.app = app
|
|
|
+ this.xmlParser = new XMLParser({
|
|
|
+ ignoreAttributes: false,
|
|
|
+ attributeNamePrefix: '@_'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 XML 字符串
|
|
|
+ */
|
|
|
+ private parseXml(xml: string): any {
|
|
|
+ return this.xmlParser.parse(xml)
|
|
|
+ }
|
|
|
+
|
|
|
+ async sendSms(numbers: string[], message: string): Promise<SendSmsResult> {
|
|
|
+ if (numbers.length > 100) {
|
|
|
+ throw new Error('Maximum number of numbers is 100')
|
|
|
+ }
|
|
|
+ const Callee = numbers.join(',')
|
|
|
+ const Text = encodeURIComponent(message)
|
|
|
+ const response = await axiosInstance.get('14.dox', {
|
|
|
+ params: {
|
|
|
+ UserName: MKT_USER_NAME,
|
|
|
+ PassWord: MKT_PASSWORD,
|
|
|
+ Caller: DEFAULT_CALLER,
|
|
|
+ Callee,
|
|
|
+ CallerAddrTon: 1,
|
|
|
+ CallerAddrNpi: 1,
|
|
|
+ CalleeAddrTon: 1,
|
|
|
+ CalleeAddrNpi: 1,
|
|
|
+ CharSet: 1,
|
|
|
+ DCS: 8,
|
|
|
+ Text
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理返回结果
|
|
|
+ const xmlData = response.data
|
|
|
+ const parsed = this.parseXml(xmlData)
|
|
|
+
|
|
|
+ const status = parsed?.Message?.Head?.Status
|
|
|
+ if (status !== '0') {
|
|
|
+ const errorDesc = this.getAckErrorDescription(status)
|
|
|
+ throw new Error(`发送短信失败 [Status:${status}]: ${errorDesc}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info(`发送短信成功 : ${xmlData}`)
|
|
|
+
|
|
|
+ const msgid = parsed?.Message?.Body?.MsgID
|
|
|
+ if (!msgid) {
|
|
|
+ throw new Error('MsgID is missing in response')
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ msgid,
|
|
|
+ success: true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getReport(params: any): Promise<GetReportResult> {
|
|
|
+ const response = await axiosInstance.get('5.dox', {
|
|
|
+ params: {
|
|
|
+ UserName: MKT_USER_NAME,
|
|
|
+ PassWord: MKT_PASSWORD
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const xmlData = response.data
|
|
|
+ const parsed = this.parseXml(xmlData)
|
|
|
+
|
|
|
+ const status = parsed?.Message?.Head?.Status
|
|
|
+ if (status !== '0') {
|
|
|
+ const errorDesc = this.getAckErrorDescription(status)
|
|
|
+ throw new Error(`获取报告失败 [Status:${status}]: ${errorDesc}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.app.log.info('获取报告成功')
|
|
|
+
|
|
|
+ // Report
|
|
|
+ const body = parsed?.Message?.Body
|
|
|
+ let reports: any[] = []
|
|
|
+
|
|
|
+ if (body?.Report) {
|
|
|
+ if (Array.isArray(body.Report)) {
|
|
|
+ reports = body.Report
|
|
|
+ } else {
|
|
|
+ reports = [body.Report]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // msgid过滤
|
|
|
+ if (params?.msgid) {
|
|
|
+ reports = reports.filter(report => report.MsgID === params.msgid)
|
|
|
+ }
|
|
|
+
|
|
|
+ const phoneStatusList: PhoneStatus[] = reports.map(report => {
|
|
|
+ // 000 表示成功
|
|
|
+ const state = report.State || ''
|
|
|
+ const error = report.Error || ''
|
|
|
+
|
|
|
+ const status = this.parseReportStatus(state, error)
|
|
|
+
|
|
|
+ return {
|
|
|
+ number: `+${report.Callee}`,
|
|
|
+ status
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ phoneStatusList
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseReportStatus(state: string, error: string): 'success' | 'waiting' | 'fail' {
|
|
|
+ if (error === '000' && state === 'DELIVRD') {
|
|
|
+ return 'success'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error !== '000' && error !== '') {
|
|
|
+ return 'fail'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state) {
|
|
|
+ if (state.includes(':')) {
|
|
|
+ const [statusType] = state.split(':')
|
|
|
+ switch (statusType) {
|
|
|
+ case 'REJECTD':
|
|
|
+ case 'UNDELIV':
|
|
|
+ case 'DELETED':
|
|
|
+ case 'EXPIRED':
|
|
|
+ return 'fail'
|
|
|
+ default:
|
|
|
+ return 'waiting'
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ switch (state) {
|
|
|
+ case 'DELIVRD':
|
|
|
+ return 'waiting'
|
|
|
+ case 'UNKNOWN':
|
|
|
+ return 'waiting'
|
|
|
+ default:
|
|
|
+ return 'waiting'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'waiting'
|
|
|
+ }
|
|
|
+
|
|
|
+ private getAckErrorDescription(status: string): string {
|
|
|
+ const errorMap: Record<string, string> = {
|
|
|
+ '0': '成功',
|
|
|
+ '1': '登陆错误:用户名、密码鉴权错误',
|
|
|
+ '2': '登陆错误:IP地址错误',
|
|
|
+ '3': '登陆错误:账号被禁用',
|
|
|
+ '4': '登陆错误:登陆接口数量过多',
|
|
|
+ '9': '系统内部错误:客户需要重新提交,直到提交成功为止',
|
|
|
+ '12': '短信内容URL解码错误',
|
|
|
+ '13': 'UCS2转换GBK错误',
|
|
|
+ '14': '字符集错误',
|
|
|
+ '15': '短信内容UTF8解码错误',
|
|
|
+ '16': 'GBK转换UCS2错误',
|
|
|
+ '17': '短信内容长度过长或为空',
|
|
|
+ '20': '被叫匹配MCC/MNC失败',
|
|
|
+ '21': '被叫没有对匹配到的费率',
|
|
|
+ '22': '相同内容重复提交',
|
|
|
+ '23': '目的账号没有配置',
|
|
|
+ '24': '签名有前后各1个',
|
|
|
+ '25': '高频自动拒绝',
|
|
|
+ '26': '被叫长度错误',
|
|
|
+ '27': '被叫是黑名单',
|
|
|
+ '28': '短信内容鉴权失败:内容没有匹配到模板',
|
|
|
+ '29': '短信内容鉴权失败:内容出现违禁词',
|
|
|
+ '30': 'DCS错误',
|
|
|
+ '31': '内容和DCS不一致',
|
|
|
+ '44': '路由错误',
|
|
|
+ '45': '短信内容长度超长',
|
|
|
+ '46': '落地网关没有设置费率',
|
|
|
+ '48': '客户自定义签名错误',
|
|
|
+ '49': '系统没有匹配出签名',
|
|
|
+ '50': '客户签名和主叫绑定失败',
|
|
|
+ '60': '长消息过长或子消息数量过多',
|
|
|
+ '61': '长消息的子消息DCS不一致',
|
|
|
+ '62': '长消息组合超时',
|
|
|
+ '63': '长消息中的子消息序号越界',
|
|
|
+ '64': '长消息:子消息已经存在',
|
|
|
+ '77': 'DR超时的错误码',
|
|
|
+ '78': '定制DR的错误码',
|
|
|
+ '88': '客户余额不足',
|
|
|
+ '98': '落地原因导致消息不可到达',
|
|
|
+ '99': '未知状态',
|
|
|
+ '240': 'DCS错误',
|
|
|
+ '241': '主叫号码异常:Caller字段值过长',
|
|
|
+ '242': '被叫号码异常:被叫号码字段不存在或字段值过长',
|
|
|
+ '253': '被叫号码数量过多'
|
|
|
+ }
|
|
|
+ return errorMap[status] || `未知错误码: ${status}`
|
|
|
+ }
|
|
|
+}
|