zoukankan      html  css  js  c++  java
  • .net core实践系列之短信服务-Sikiro.SMS.Api服务的实现

    前言

    上篇《.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);
            }

    项目相关开源框架

    EasyNetQ

    EasyNetQ.DI.Microsoft

    Sikiro.Nosql.Mongo

    log4net

    Mapster

    EasyNetQ

    这个开源框架是针对RabbitMQ.Client的封装,隐藏了很多实现细节,简化使用方式。并提供了多种IOC注入方式

    源码地址:https://github.com/EasyNetQ/EasyNetQ

    Sikiro.Nosql.Mongo

    这个是我自己针对mongo驱动的常用的基础操作的封装库

    源码地址:https://github.com/SkyChenSky/Sikiro.Nosql.Mongo

    Mapster

    实体映射框架,看评测数据比AutoMapper等之类的效率要高,而且易用性也非常高。

    https://github.com/MapsterMapper/Mapster

    全局异常日志记录

    public class GolbalExceptionAttribute : ExceptionFilterAttribute
        {
            public override void OnException(ExceptionContext context)
            {
                if (!context.ExceptionHandled)
                {
                    context.Exception.WriteToFile();
                }
    
                base.OnException(context);
            }
        }
    
     public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc(option =>
                    {
                        option.Filters.Add<GolbalExceptionAttribute>();
                    })
                    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }

    LoggerHelper

    上面的WriteToFile是我对Exception的扩展方法,使用了Log4Net日志框架对异常进行记录,如果有需要也可以写到mongodb或者elasticsearch

    /// <summary>
        /// 日志帮助类
        /// </summary>
        public static class LoggerHelper
        {
            private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository");
            public static readonly ILog Log = LogManager.GetLogger(Repository.Name, typeof(LoggerHelper));
    
            static LoggerHelper()
            {
                XmlConfigurator.Configure(Repository, new FileInfo("log4net.config"));
            }
    
            #region 文本日志
    
            /// <summary>
            /// 文本日志
            /// </summary>
            /// <param name="message"></param>
            /// <param name="ex"></param>
            public static void WriteToFile(this Exception ex, string message = null)
            {
                if (string.IsNullOrEmpty(message))
                    message = ex.Message;
    
                Log.Error(message, ex);
            }
            #endregion
        }

    工具库的封装

    框架与工具库都是以库的形式提供我们使用,而且都是可复用,但是他们区别在于:工具库开箱即用,大多数以静态方法提供调用,只调用少量甚至一个方法则完成使用。

    而框架定义,为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品,而他具有约束性、可复用性、规范性。他是一个半成品,可重写。

    因此为了简化框架的使用,对常用设置、构建组合进行封装,以一个扩展类或者帮助类的形式提供,简化使用、增加可读性。

    Swagger的使用

    Http协议的好处是轻量、跨平台,如此良好的灵活性然而需要接口描述对外暴露。Swagger是一个很好的选择,不需要自己手写文档并提供后台管理界面,还可以测试,简化不少工作。

    我选择了NSwag.AspNetCore开源组件,他的使用非常简单。只需要两步:

    1.配置Swagger:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseSwaggerUiWithApiExplorer(settings =>
               {
                   settings.GeneratorSettings.DefaultPropertyNameHandling =
                       PropertyNameHandling.CamelCase;
                   settings.PostProcess = document =>
                   {
                       document.Info.Version = "v1";
                       document.Info.Title = "Sikiro.SMS.API";
                       document.Info.Description = "短信服务API";
                       document.Info.TermsOfService = "None";
                   };
               });
                app.UseMvc();
            }

    2.设置站点项目

    此设置为了把接口、参数注释显示到Swagger页面

    NSwag还有多个版本的UI选择:

    • UseSwaggerReDoc
    • UseSwaggerUi
    • UseSwaggerUi3

    访问http://localhost:port/swagger就可以见到API文档了

    部署

     因为我公司还是使用windows server 2008。因此部署前应准备环境安装包:

    .NET Core 2.1.3 windows-hosting

    安装完成后重启服务器,再把文件发布到服务器,编辑应用程序池为无托管代码。就可以访问了

    结尾

    本篇介绍Sikiro.SMS.Api的设计与实现,下篇会针对API调用进行封装SDK。如果有任何建议,请在下方评论反馈给我。

  • 相关阅读:
    react 调用webIm
    css样式问题解决
    学习animejs
    vue,在模块中动态添加dom节点,并监听
    vue 关于solt得用法
    vue-cli 安装过程出现错误
    处理参数中存在多个连续空格,只显示一个空格,复制后搜索不到得问题
    http StatusCode(状态码)
    修改表单小技巧
    关于swiper中包含表单元素的bug
  • 原文地址:https://www.cnblogs.com/skychen1218/p/9549950.html
Copyright © 2011-2022 走看看