前言
上篇《.net core实践系列之短信服务-架构设计》介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念。本篇继续讲解Api服务的实现过程。
源码地址:https://github.com/SkyChenSky/Sikiro.SMS
此服务会使用.NET Core WebApi进行搭建,.NET Core WebApi基础原型就是RESTful风格,然而什么叫RESTful呢。
REST API简介
REST
Representational State Transfer的缩写,翻译为“表现层状态转化”,是由Roy Thomas Fieding在他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一种架构思想。
而他的论文中提出了一个RESTful应用应该具备的几点约束。
- 每个资源都应该有一个唯一的标识
- 每一个对象或资源都可以通过一个唯一的URI进行寻址,URI的结构应该是简单的。
- 使用标准的方法来更改资源的状态
- GET、POST、PUT、PATCH、DELETE
- Request和Response的自描述
- 资源多重表述
- URI所访问的每个资源都可以使用不同的形式加以表示(XML或JSON)
- 无状态的服务
- 不需要保存会话状态(SESSION),资源本身就是天然的状态,是需要被保存的。
RESTful
当某Web服务遵守了REST这些约束条件和原则,那么我们可以称它设计风格就是 RESTful。
三特点
REST有三大特点:
- 资源(名词)
- 动作(动词)
- 表述(超文本)
资源
抽象的说他可以是音频、也可以是视频,更可以是订单。更俗讲其实就是实体,更接*我们*常说的“类(class)”。另外REST强调资源有唯一的URI。下面有对比
动作
主要动作:
- GET:检索单个资源;
- POST:主要是创建资源,但是GET的参数长度受限,因此也可以用在复杂参数的检索资源场景;
- PUT:更新资源所有属性,也可以称为替换资源;
- PATCH:更新资源部分属性;
- DELETE:删除资源;
表述
对于Request与Response的自描述,而表述方式有多种:XML、JSON等,强调HTTP通信的语义可见性。
对比
RPC
SMSApi.com/api/GetSMS
SMSApi.com/api/CreateSMS
传统的接口设计面向过程的,每个动作有特定的URI。
REST
SMSApi.com/api/SMS GET
SMSApi.com/api/SMS POST
REST API每个资源只有唯一的URI,而资源可以有不同的动作执行相应的接口
RPC的更加倾向于面向过程,而RESTful则以面向对象的思想进行设计。
接口定义
回到我们的短信服务,以上面的三特点进行出发,SMS不需要由外部服务进行删除、修改资源因此:
资源:SMS
动作:GET、POST
表述方式:我们约定Request、Response为JSON格式
/// <summary> /// 短信接口 /// </summary> [Route("api/[controller]")] [ApiController] public class SmsController : ControllerBase { private readonly SmsService _smsService; private readonly IBus _bus; public SmsController(SmsService smsService, IBus bus) { _smsService = smsService; _bus = bus; } /// <summary> /// 获取一条短信记录 /// </summary> /// <param name="id">主键</param> /// <returns></returns> [HttpGet("{id}")] public ActionResult<SmsModel> Get(string id) { if (string.IsNullOrEmpty(id)) return NotFound(); var smsService = _smsService.Get(id); return smsService.Sms; } /// <summary> /// 发送短信 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] public ActionResult Post([FromBody] List<PostModel> model) { _smsService.Add(model.MapTo<List<PostModel>, List<AddSmsModel>>()); _smsService.SmsList.Where(a => a.TimeSendDateTime == null) .ToList().MapTo<List<SmsModel>, List<SmsQueueModel>>().ForEach(item => { _bus.Publish(item); }); return Ok(); } /// <summary> /// 查询短信记录 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost("_search")] public ActionResult<List<SmsModel>> Post([FromBody] SearchModel model) { _smsService.Search(model.MapTo<SearchModel, SearchSmsModel>()); return _smsService.SmsList; } }
功能描述
由上可见一共定义了三个接口
- GET http://localhost:port/api/sms/id 获取一条短信记录
- POST http://localhost:port/api/sms 发送短信
- POST http://localhost:port/api/sms/_search 查询短信记录
获取一条短信记录就不多解析了
查询短信记录
动作我使用了POST,有人会问检索资源不是用GET么?对,但是GET的参数在URL里是受限的,因此在复杂参数的场景下应该选择POST,然而我是模仿elasticsearch的复杂查询时定义,添加多一个节点/_search申明此URI是做查询的。
发送短信
此接口的实现逻辑主要两件事,持久化到MongoDB,过滤出及时发送的短信记录发送到RabbitMQ。
在持久化之前我做了一个分页的动作,我们提供出去的接口,同一条短信内容支持N个手机号,但是不同的短信运营商的所支持一次性发送的手机数量是有限的。
开始实现时,我把分页发送写到队列消费服务的发送短信逻辑里,但是这里有个问题,如果分页后部分发送成功,部分发送失败,那么这个聚合究竟以失败还是成功的状态标示呢?换句话来说我们无法保证聚合内的数据一致性。
因此我的做法就是优先在分页成多个文档存储,那么就可以避免从数据库取出后分页导致部分成功、失败。
public void Add(List<AddSmsModel> smsModels) { DateTime now = DateTime.Now; var smsModel = new List<SmsModel>(); foreach (var sms in smsModels) { var maxCount = _smsFactory.Create(sms.Type).MaxCount; sms.Mobiles = sms.Mobiles.Distinct().ToList(); var page = GetPageCount(sms.Mobiles.Count, maxCount); var index = 0; do { var toBeSendPhones = sms.Mobiles.Skip(index * maxCount).Take(maxCount).ToList(); smsModel.Add(new SmsModel { Content = sms.Content, CreateDateTime = now, Mobiles = toBeSendPhones, TimeSendDateTime = sms.TimeSendDateTime, Type = sms.Type }); index++; } while (index < page); } SmsList = smsModel; _mongoProxy.BatchAddAsync(SmsList); }