供稿人:刘少林
现在的越来越多的项目都有发送短信的需求,FWK 中内置有短信发送的功能模块,开发中需要根据短信提供商的接口或短信网关接口,实现相应的短信服务即可。
首先,在应用中新建一个新的 ShortMessageService 服务类,并继承 bropen.framework.plugins.message.ShortMessageService,模板如下:
import bropen.framework.plugins.message.ShortMessage
import bropen.framework.plugins.message.MessageStatus
/**
 * XXX 短信服务
 */
class ShortMessageService extends bropen.framework.plugins.message.ShortMessageService {
    static aliasOverridingOrder = bropen.framework.plugins.message.ShortMessageService.aliasOverridingOrder + 1
    static bootStrapInit() {
        // 一些初始化配置可以放到这里
    }
    
    @java.lang.Override
    protected MessageStatus send(ShortMessage sms) {
        // 调用短信接口,发送短信,并且返回状态
        ...
        return MessageStatus.SENT
    }
    
    @java.lang.Override
    protected ShortMessage bind(Map msg) {
        // 将一个 Map 转换为 ShortMessage 对象
    }
    
}
上面的模板中,bootStrapInit 方法用于初始化一些系统参数,如系统参数:
- bropen.framework.plugins.message.sms.enabled:是否启用短信,默认为否
- bropen.framework.plugins.message.sms.http.url:第三方短信服务的地址
- bropen.framework.plugins.message.sms.attempt.max:短信发送失败的最大重试次数,默认为 5
- bropen.framework.plugins.message.sms.attempt.interval:短信发送失败的重试间隔,默认为 5 分钟
此外,还可以根据第三方短信服务的接口,自定义其他系统参数,如下例所示:
static bootStrapInit() {
    if ( !settingService.exists("bropen.framework.plugins.message.sms.http.sn") ) {
        // 短信服务地址
        settingService.createOrUpdate("bropen.framework.plugins.message.sms.http.url",
            "http://www.jianzhou.sh.cn/JianzhouSMSWSServer/services/BusinessService", "string").save()
        // 短信服务帐号
        settingService.createOrUpdate("bropen.framework.plugins.message.sms.http.sn",
            "sdk_foobar", "string").save()
        // 短信服务密码
        settingService.createOrUpdate("bropen.framework.plugins.message.sms.http.pwd",
            "1234567890", "passwd").save()
        // 短信服务开关
        settingService.createOrUpdate("bropen.framework.plugins.message.sms.enabled",
            "false", "boolean").save()
        // 短信服务帐号
        settingService.createOrUpdate("bropen.framework.plugins.message.sms.http.sig",
            "【Foobar】", "string").save()
    }
}
接口方法 send 是最重要的部分,该方法中,需要调用实际的第三方接口,将短信发送出去,如下例所示:
/**
 * 接口方法:通过短信网关发送短信,并返回状态。如果短信发送被禁用,则应直接返回 null。
 */
@java.lang.Override
protected MessageStatus send(ShortMessage sms) {
    if ( !isEnabled() ) return null // 如果没有启用短信服务,则直接返回null
    
    String url = settingService.get("bropen.framework.plugins.message.sms.http.url")        // 服务地址
    String account = settingService.get("bropen.framework.plugins.message.sms.http.sn")     // 企业账户
    String pwd = settingService.get("bropen.framework.plugins.message.sms.http.pwd")        // 密码
    String sig = settingService.get("bropen.framework.plugins.message.sms.http.sig") ?: ""  // 短信后缀
    // 收件人
    List<String> phones = []
    for ( String to in sms.to ) {
        if ( to.indexOf("<") )
            phones += to.replaceAll("^[^<]*<", "").replaceAll(">.*\$", "")
    }
    // 拼消息实体:接收人、短信内容
    String phone = phones.join(";")
    String message = sms.text + (sms.signature ? ("\n"+ sms.signature) : "")
    message += sig
    
    // 发送 (开发和测试环境不发短信)
    String resp = null
    if ( grails.util.Environment.current == grails.util.Environment.PRODUCTION ) {
        Client c = new Client(url,account, pwd)
        resp = c.sendBatchMessage(phone, message)
        log.debug("sms.id=" + sms.id + ", resp=" + resp)
        resp = resp.replaceAll(/,.+/, "")
    } else {
        resp = '999999999'
        log.debug("sms.id=" + sms.id + ", resp=" + resp + ", phone=" + phone + ", message=" + message)
    }
    
    // 检查返回值(服务反馈的状态码),并设置 sms 对象的 statusCode、statusMessage,以便审计和查找发送失败原因
    sms.statusCode = resp
    switch ( sms.statusCode ) {
        case "-1" : sms.statusMessage="余额不足"; break
        case "-2" : sms.statusMessage="帐号或密码错误"; break
        ...
        case "-19" : sms.statusMessage="必须为POST提交"; break
        case "-20" : sms.statusMessage="超速提交(一般为每秒一次提交)"; break
        default: sms.statusMessage = sms.statusCode
    }
    // 返回状态
    return sms.statusCode.toLong() > 0 ? MessageStatus.SENT : MessageStatus.ERROR
}
/**
 * 从第三方接口的 demo 代码中拷贝出来的短信客户端
 */
public static class Client {
    ....
}
上面的例子中,注意:
- 第一行判断是否启用了短信,否则直接返回 null
- 调用短信客户端类发送完成后,可以设置 sms 对象的 statusCode、statusMessage 属性,以便审计和查找发送失败原因
此外,上面的例子中有一个内部类 Client,一般可以从短信服务商提供的 java 例子中找到,简单修改即可,如:
public static class Client {
    
    // webservice服务器定义
    private String serviceURL = null;        // 入口地址:http://www.jianzhou.sh.cn/JianzhouSMSWSServer/http/sendBatchMessage
    private String account    = null;        // 账户
    private String pwd        = null;        // 密码
    
    /*
     * 构造函数
     */
    public Client(String url, String account, String pwd) throws UnsupportedEncodingException {
        this.serviceURL = url;
        this.account = account;
        this.pwd = pwd;
    }
    
    /*
     * 方法名称:sendBatchMessage
     * 功    能:发送个性短信
     * 参    数:mobile,content(手机号,内容)
     * 返 回 值:唯一标识,如果不填写rrid将返回系统生成的
     */
    public String sendBatchMessage(String mobile, String content) {
        ....
    }
    
}
接口方法 bind 用来将 Map 对象转换成一个 ShortMessage 对象,默认情况下,Map 中包含下列数据(其中 from、to、text 不能为空):
- from:发送人名称/员工或用户对象
- to:逗号或分号分隔的手机号、或手机号列表、员工列表
- text:消息正文
- signature:签名
- sentDateSche:计划发送时间,空则立即发送
如有其他需求,可以覆盖 bind 方法实现自己的转换逻辑。
最后,接口开发完成后,可以调用 ShortMessageService 的 sendMessage 方法发送短信,如:
ctx.shortMessageService.flush = true    // 如果在 console 中执行,必须加上这句;否则不需要
// 发送一条新的短信
ctx.shortMessageService.sendMessage( [from:"abc", to:"13312345678", text:"test text"] )
// 重试一条老的短信
def sms = bropen.framework.plugins.message.ShortMessage.get(xxxx)
ctx.shortMessageService.sendMessage( sms )
如上例所示,sendMessage 方法有两个版本,一个版本的参数是 Map、一个版本的参数是 ShortMessage 对象,通常情况下我们使用 Map 来传递参数(新建一条短信并发送),而 Map 中的数据规范参见上文中 bind 方法的默认情况。
此外,启用短信配置、并实现短信接口后,用管理员登录到后台,可以查看短信发送情况,并进行重发、或直接发送短信。

