文章目录
1.前言
1.1 sms4j是什么?
sms4j:让发送短信变得更简单,是一款开源、优雅、开箱即用、切换灵活。功能丰富等特性的发短信的开源好用开源好用的轮子,支持的短信提供商也比较的多,支持的短信提供厂商有:阿里云、容联云、天翼云、亿美软通、华为云短信、京东云短信、 网易云信、腾讯云短信、合一短信、云片短信、助通短信、鼎众短信、联麓短信、七牛云短信 、创蓝短信、极光短信、布丁云V2 、中国移动 云MAS 、百度云短信、螺丝帽短信、SUBMAIL短信、 单米短信;官网上也有详细的说明,本文以华为云短信为例。
1.2 缘由
由于最近项目需要一个发送短信的功能,所以我就选择想集成sms4j这个开源的轮子,其实有其他同事写好的接口可以调用,然后我一对比一看,还是集成sms4j实现比较优雅一点,毕竟不用去调用其他的服务接口,本项目中就直接实现了这个功能,避免了网路接口调用,做到了服务自己的高内聚低耦合,然后就看了下sms4j的官网,也看了sms4j的源码,华为云短信写的还是优雅的,就是不支持不带短信模版id的实现,所以我决定修改sms4j的源码来实现,sms4j的厂商yaml配置差异化的配置都是一个map注入的,具体的可以去看下它的源码,写的还是可以的,每一个短信厂商都有一个单列的工厂类,然后会给每个单列的工厂类生成一个SmsBlend 动态代理类(jdk的动态代理类,代理的对象是具体的短信厂商实现类,比如:HuaweiSmsImpl),然后在短信方法调用之前和之后会有一系列的处理器类,具体的可以去看源码。
1.3 官方地址
https://sms4j.com/
https://gitee.com/dromara/sms4j
https://github.com/dromara/SMS4J
sms4j官方提供了两个版本:2.x和3.x,具体可以参看官方文档,本文使用3.x的版本。
1.4 华为短信接口响应码文档地址
https://support.huaweicloud.com/api-msgsms/sms_05_0050.html#toTop
2.配置
2.1 依赖配置
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
版本可以看如下官方说明:
https://sms4j.com/title/log.html
2.2 yaml配置
nacos项目yaml配置如下:
sms:
# 标注从yml读取配置
config-type: yaml
blends:
# 自定义的标识,也就是configId这里可以是任意值(最好不要是中文)
hw1:
# 接入地址
url: https://smsapi.cn-north-4.myhuaweicloud.com:443
# 国内短信签名通道号
sender: xxxxx
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: huawei
# 您的accessKey
access-key-id: xxxxx
# 您的accessKeySecret
access-key-secret: xxxxxxx
# 您的短信签名
signature: xxxxxx
# 模板ID 非必须配置,如果使用sendMessage的快速发送需此配置
#template-id: xxxxxxxx
# 您的sdkAppId
sdk-app-id: hw1
# 自定义的标识,也就是configId这里可以是任意值(最好不要是中文)
各个厂商的差异配置都是可以配置成key-vule的map的一个元素,然后starter自动装配会自动注入到各自的config配置类中,比如本文的华为云短信就是HuaweiConfig类
3.重写
3.1项目中重写如下类

3.2 HuaweiSmsImpl类
package org.dromara.sms4j.huawei.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.huawei.config.HuaweiConfig;
import org.dromara.sms4j.huawei.utils.HuaweiBuilder;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import static org.dromara.sms4j.huawei.utils.HuaweiBuilder.listToString;
@Slf4j
public class HuaweiSmsImpl extends AbstractSmsBlend<HuaweiConfig> {
private int retry = 0;
public HuaweiSmsImpl(HuaweiConfig config, Executor pool, DelayedTime delayed) {
super(config, pool, delayed);
}
public HuaweiSmsImpl(HuaweiConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.HUAWEI;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
LinkedHashMap<String, String> mes = new LinkedHashMap<>();
mes.put(UUID.randomUUID().toString().replaceAll("-", ""), message);
return sendMessage(phone, getConfig().getTemplateId(), mes);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
if (Objects.isNull(messages)) {
messages = new LinkedHashMap<>();
}
return sendMessage(phone, getConfig().getTemplateId(), messages);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (Objects.isNull(messages)) {
messages = new LinkedHashMap<>();
}
String url = getConfig().getUrl() + Constant.HUAWEI_REQUEST_URL;
List<String> list = new ArrayList<>();
for (Map.Entry<String, String> entry : messages.entrySet()) {
list.add(entry.getValue());
}
String mess = "";
if (StringUtils.isNotEmpty(templateId)) {
mess = listToString(list);
} else {
if (list.size() == 1) {
mess = list.stream().collect(Collectors.joining(""));
} else {
mess = list.stream().collect(Collectors.joining(","));
}
}
if (StringUtils.isEmpty(mess)) {
throw new RuntimeException("华为发送短信消息不为空!");
}
String requestBody = HuaweiBuilder.buildRequestBody(getConfig().getSender(), phone, templateId, mess, getConfig().getStatusCallBack(), getConfig().getSignature());
Map<String, String> headers = MapUtil.newHashMap(3, true);
headers.put("Authorization", Constant.HUAWEI_AUTH_HEADER_VALUE);
headers.put("X-WSSE", HuaweiBuilder.buildWsseHeader(getConfig().getAccessKeyId(), getConfig().getAccessKeySecret()));
headers.put("Content-Type", Constant.FROM_URLENCODED);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(url, headers, requestBody));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, templateId, messages);
}
private SmsResponse requestRetry(String phone, String templateId, LinkedHashMap<String, String> messages) {
http.safeSleep(getConfig().getRetryInterval());
retry++;
log.warn("短信第 {} 次重新发送", retry);
return sendMessage(phone, templateId, messages);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return sendMessage(CollUtil.join(phones, ","), message);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (Objects.isNull(messages)) {
messages = new LinkedHashMap<>();
}
return sendMessage(CollUtil.join(phones, ","), templateId, messages);
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
}
}
3.3 HuaweiBuilder类
package org.dromara.sms4j.huawei.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class HuaweiBuilder {
private HuaweiBuilder() {
}
/**
* buildWsseHeader
* <p>构造X-WSSE参数值
*
* @author :Wind
*/
public static String buildWsseHeader(String appKey, String appSecret) {
if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) {
System.out.println("buildWsseHeader(): appKey or appSecret is null.");
return null;
}
String time = dateFormat(new Date());
// Nonce
String nonce = UUID.randomUUID().toString().replace("-", "");
MessageDigest md;
byte[] passwordDigest;
try {
md = MessageDigest.getInstance("SHA-256");
md.update((nonce + time + appSecret).getBytes());
passwordDigest = md.digest();
} catch (NoSuchAlgorithmException e) {
throw new SmsBlendException(e);
}
// PasswordDigest
String passwordDigestBase64Str = Base64.encode(passwordDigest);
//若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正
//passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", "");
return String.format(Constant.HUAWEI_WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time);
}
static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
/**
* buildRequestBody
* <p>构造请求Body体
*
* @param sender 国内短信签名通道号
* @param receiver 短信接收者
* @param templateId 短信模板id
* @param templateParas 模板参数
* @param statusCallBack 短信状态报告接收地
* @param signature | 签名名称,使用国内短信通用模板时填写
* @author :Wind
*/
public static String buildRequestBody(String sender, String receiver, String templateId, String templateParas,
String statusCallBack, String signature) {
if (null == sender || null == receiver || sender.isEmpty() || receiver.isEmpty()) {
System.out.println("buildRequestBody(): sender, receiver is null.");
return null;
}
Map<String, String> map = new HashMap<>();
map.put("from", sender);
map.put("to", receiver);
if (StringUtils.isNotEmpty(templateId)) {
map.put("templateId", templateId);
if (null != templateParas && !templateParas.isEmpty()) {
map.put("templateParas", templateParas);
}
}else {
map.put("body", templateParas);
}
if (null != statusCallBack && !statusCallBack.isEmpty()) {
map.put("statusCallback", statusCallBack);
}
if (null != signature && !signature.isEmpty()) {
map.put("signature", signature);
}
StringBuilder sb = new StringBuilder();
String temp;
for (String s : map.keySet()) {
try {
temp = URLEncoder.encode(map.get(s), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new SmsBlendException(e);
}
sb.append(s).append("=").append(temp).append("&");
}
return sb.deleteCharAt(sb.length() - 1).toString();
}
public static String listToString(List<String> list) {
if (null == list || list.isEmpty()) {
return null;
}
StringBuilder stringBuffer = new StringBuilder();
stringBuffer.append("[\"");
for (String s : list) {
stringBuffer.append(s);
stringBuffer.append("\"");
stringBuffer.append(",");
stringBuffer.append("\"");
}
stringBuffer.delete(stringBuffer.length() - 3, stringBuffer.length() - 1);
stringBuffer.append("]");
return stringBuffer.toString();
}
private static String dateFormat(Date date) {
return DateUtil.format(date, Constant.HUAWEI_JAVA_DATE);
}
}
3.4 SmsBlendsInitializer类
package org.dromara.sms4j.starter.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.aliyun.config.AlibabaFactory;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.cloopen.config.CloopenFactory;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.enumerate.ConfigType;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.core.datainterface.SmsReadConfig;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.sms4j.core.proxy.EnvirmentHolder;
import org.dromara.sms4j.core.proxy.SmsProxyFactory;
import org.dromara.sms4j.core.proxy.processor.BlackListProcessor;
import org.dromara.sms4j.core.proxy.processor.BlackListRecordingProcessor;
import org.dromara.sms4j.core.proxy.processor.CoreMethodParamValidateProcessor;
import org.dromara.sms4j.core.proxy.processor.RestrictedProcessor;
import org.dromara.sms4j.core.proxy.processor.SingleBlendRestrictedProcessor;
import org.dromara.sms4j.ctyun.config.CtyunFactory;
import org.dromara.sms4j.dingzhong.config.DingZhongFactory;
import org.dromara.sms4j.emay.config.EmayFactory;
import org.dromara.sms4j.huawei.config.HuaweiFactory;
import org.dromara.sms4j.jdcloud.config.JdCloudFactory;
import org.dromara.sms4j.lianlu.config.LianLuFactory;
import org.dromara.sms4j.netease.config.NeteaseFactory;
import org.dromara.sms4j.provider.config.SmsConfig;
import org.dromara.sms4j.provider.factory.BaseProviderFactory;
import org.dromara.sms4j.provider.factory.ProviderFactoryHolder;
import org.dromara.sms4j.qiniu.config.QiNiuFactory;
import org.dromara.sms4j.starter.adepter.ConfigCombineMapAdeptor;
import org.dromara.sms4j.tencent.config.TencentFactory;
import org.dromara.sms4j.unisms.config.UniFactory;
import org.dromara.sms4j.yunpian.config.YunPianFactory;
import org.dromara.sms4j.zhutong.config.ZhutongFactory;
import org.springframework.beans.factory.ObjectProvider;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
@Slf4j
public class SmsBlendsInitializer {
private final List<BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig>> factoryList;
private final SmsConfig smsConfig;
private final Map<String, Map<String, Object>> blends;
private final ObjectProvider<SmsReadConfig> extendsSmsConfigs;
public SmsBlendsInitializer(List<BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig>> factoryList,
SmsConfig smsConfig,
Map<String, Map<String, Object>> blends,
ObjectProvider<SmsReadConfig> extendsSmsConfigs) {
this.factoryList = factoryList;
this.smsConfig = smsConfig;
this.blends = blends;
this.extendsSmsConfigs = extendsSmsConfigs;
onApplicationEvent();
}
public void onApplicationEvent() {
this.registerDefaultFactory();
// 注册短信对象工厂
ProviderFactoryHolder.registerFactory(factoryList);
if (ConfigType.YAML.equals(this.smsConfig.getConfigType())) {
//持有初始化配置信息
Map<String, Map<String, Object>> blendsInclude = new ConfigCombineMapAdeptor<String, Map<String, Object>>();
blendsInclude.putAll(this.blends);
int num = 0;
for (SmsReadConfig smsReadConfig : extendsSmsConfigs) {
String key = SmsReadConfig.class.getSimpleName() + num;
Map<String, Object> insideMap = new HashMap<>();
insideMap.put(key, smsReadConfig);
blendsInclude.put(key, insideMap);
num++;
}
EnvirmentHolder.frozenEnvirmet(smsConfig, blendsInclude);
//注册执行器实现
SmsProxyFactory.addProcessor(new RestrictedProcessor());
SmsProxyFactory.addProcessor(new BlackListProcessor());
SmsProxyFactory.addProcessor(new BlackListRecordingProcessor());
SmsProxyFactory.addProcessor(new SingleBlendRestrictedProcessor());
//如果手机号校验器存在实现,则注册手机号校验器
ServiceLoader<PhoneVerify> loader = ServiceLoader.load(PhoneVerify.class);
if (loader.iterator().hasNext()) {
loader.forEach(f -> {
SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor(f));
});
} else {
//这里需要把处理器注释掉,否则smsBlend.massTexting(phones, msg)接口调用会抛异常:cant send message to null!,这里改造就不需要这些前置和后置的处理器了
//SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor(null));
}
// 解析供应商配置
for (String configId : blends.keySet()) {
Map<String, Object> configMap = blends.get(configId);
Object supplierObj = configMap.get(Constant.SUPPLIER_KEY);
String supplier = supplierObj == null ? "" : String.valueOf(supplierObj);
supplier = StrUtil.isEmpty(supplier) ? configId : supplier;
BaseProviderFactory<SmsBlend, SupplierConfig> providerFactory = (BaseProviderFactory<SmsBlend, org.dromara.sms4j.api.universal.SupplierConfig>) ProviderFactoryHolder.requireForSupplier(supplier);
if (providerFactory == null) {
log.warn("创建\"{}\"的短信服务失败,未找到供应商为\"{}\"的服务", configId, supplier);
continue;
}
configMap.put("config-id", configId);
SmsUtils.replaceKeysSeperator(configMap, "-", "_");
JSONObject configJson = new JSONObject(configMap);
org.dromara.sms4j.api.universal.SupplierConfig supplierConfig = JSONUtil.toBean(configJson, providerFactory.getConfigClass());
SmsFactory.createSmsBlend(supplierConfig);
}
}
}
/**
* 注册默认工厂实例
*/
private void registerDefaultFactory() {
ProviderFactoryHolder.registerFactory(AlibabaFactory.instance());
ProviderFactoryHolder.registerFactory(CloopenFactory.instance());
ProviderFactoryHolder.registerFactory(CtyunFactory.instance());
ProviderFactoryHolder.registerFactory(EmayFactory.instance());
ProviderFactoryHolder.registerFactory(HuaweiFactory.instance());
ProviderFactoryHolder.registerFactory(NeteaseFactory.instance());
ProviderFactoryHolder.registerFactory(TencentFactory.instance());
ProviderFactoryHolder.registerFactory(UniFactory.instance());
ProviderFactoryHolder.registerFactory(YunPianFactory.instance());
ProviderFactoryHolder.registerFactory(ZhutongFactory.instance());
ProviderFactoryHolder.registerFactory(LianLuFactory.instance());
ProviderFactoryHolder.registerFactory(DingZhongFactory.instance());
ProviderFactoryHolder.registerFactory(QiNiuFactory.instance());
if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
ProviderFactoryHolder.registerFactory(JdCloudFactory.instance());
}
log.debug("加载内置运营商完成!");
}
}
4.测试
4.1 HwSmsService类
package xxxxx.service.impl;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
public class HwSmsService {
public SmsBlend getSmsBlend() {
SmsBlend hw1 = SmsFactory.getSmsBlend("hw1");
return hw1;
}
public Boolean sendMsg(List<String> phones, String msg) {
SmsBlend smsBlend = this.getSmsBlend();
SmsResponse smsResponse = smsBlend.massTexting(phones, msg);
log.info("HwSmsService.smsResponse:{}", JSON.toJSONString(smsResponse));
if (smsResponse.isSuccess()) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
4.2 TestController测试类
package xxxxx.controller;
import com.alibaba.fastjson.JSON;
import com.xxxxx.impl.HwSmsService;
import com.xxl.job.core.biz.model.ReturnT;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private HwSmsService hwSmsService;
@GetMapping("/test3")
public SmsResponse test3() {
SmsBlend smsBlend = hwSmsService.getSmsBlend();
SmsResponse response = smsBlend.sendMessage("xxxxxxx手机号码", "【短信签名xxxx】 测试短信发送");
log.info("smsResponse:{}", JSON.toJSONString(response));
return response;
}
@GetMapping("/test8")
public SmsResponse test8() {
SmsBlend smsBlend = hwSmsService.getSmsBlend();
SmsResponse response = smsBlend.massTexting(Collections.singletonList("xxxxxxx手机号码"), "【短信签名xxxx】测试短信发送");
log.info("smsResponse:{}", JSON.toJSONString(response));
return response;
}
}
经过上面源码的修改,输入正确的手机号和正确的短信内容(注意这里需要注意的是短信内容里面需要带有短信签名信息【短信签名xxxx】+ 文本字符串(注意不要有-,如果有-会被转换替换为_,具体可以去看源码,不符合你的需求可以改源码实现)),经过上面的修改,这两个接口就可以正常调用了,这两个接口亲测是可以收到短信的,至于项目中使用这两个接口只需要准备手机号和构造文本短信,例如:文本短信模版是:【短信签名xxxx】您的订单:orderNo已经支付,然后使用String的replace(“orderNo”,202407300000018)替换就可构造好短信内容,具体的根据自己的业务来搞一个短信模版,然后替换参数就得到一个文本的字符串,不使用短信模版id也可以发送短信,只需要短信内容里面包含短信签名即可。
5.总结
经过以上对sms4j的源码改造,实现了华为云发送不带短信模版id的短信,可以去把sms4j的源码拉下来,趴一趴看一看,集成到项目中,然后debug打断点跟一波源码,其实也还是简单的,华为云发短信是这种玩的,其他厂商的也是同样的道理,举一反三,触类旁通,从开源的轮子中可以学习到优秀的设计思想和设计实现,而不是CRUD,就算是写CRUD也要写了优雅、干净、整洁、帅气、复用性-可拓展性-鲁棒性-可读性-可维护性都好一点,这种bug自然是没有啥bug的,养成良好的编码习惯和编码风格可以降低很多的bug,我的分享到此结束了,希望我的分享对你有所启发和帮助,创作不易,都是亲身实践经验总结,不吝分享,还请不要照抄过去就成了你自己的原创,转发请标注原文及作者出处,发现抄袭(不带思考的原模原样的抄袭,没有多大意义,全成为千篇一律的文章,写文章还是要有一点思考、实践的,否则就是坑文水文,没有一点深度,过于肤浅),则直接举报(或者可以联系本人来举报),请一键三连,么么么哒!
449

被折叠的 条评论
为什么被折叠?



