什么是OSharp

OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架。这个框架使用最新稳定版的.NetCore SDK(当前是.NET Core 2.2),对 AspNetCore 的配置、依赖注入、日志、缓存、实体框架、Mvc(WebApi)、身份认证、权限授权等模块进行更高一级的自动化封装,并规范了一套业务实现的代码结构与操作流程,使 .Net Core 框架更易于应用到实际项目开发中。

概述

一个模块的服务层,主要负责如下几个方面的工作:

  • 向 API层 提供各个实体的数据查询的 IQueryable<T> 类型的数据源
  • 接收 API层 传入的 IInputDto 参数,完成实体的 新增更新删除 等业务操作
  • 接收 API层 传入的参数,处理各种 模块级别 的综合业务
  • 处理完业务之后,将数据通过 数据仓储IRepository 更新到数据库
  • 事件总线 模块发布业务处理事件,触发订阅的业务事件
  • 向 API 层返回业务操作结果

整个过程如下图所示:

服务层代码布局

服务层代码布局分析

一个业务模块,是负责完成一系列功能的,这些功能相互之间具有密切的关联性,所以对于一个模块来说,业务服务是一个整体,不应把他们再按单个实体拆分开来。

OSharp 的业务模块代码结构设计,也是根据这一原则来设计的。设计规则如下:

  • 服务接口IBlogsContract:一个模块的业务服务共享一个服务接口,接口中包含模块的综合业务服务,也包含模块的各个实体的查询数据集、新增、更新、删除等自有业务服务。

  • 服务实现BlogsService:服务实现使用 分部类partial 设计,例如本例中的博客模块业务,文件拆分如下:

    • BlogsService.cs:博客模块服务实现类的主文件,负责各实体的仓储服务注入,辅助服务注入,模块综合业务实现
    • BlogsService.Blog.cs:博客模块服务的博客实体服务实现类,负责博客实体的 查询数据集、增改删 业务实现
    • BlogsService.Post.cs:博客模块服务的文章实体服务实现类,负责文章实体的 查询数据集、增改删 业务实现
  • 模块入口BlogsPack:定义模块的级别、启动顺序、执行服务添加、模块初始化等功能

综上,服务层代码布局如下所示:

  1. src # 源代码文件夹
  2. └─Liuliu.Blogs.Core # 项目核心工程
  3. └─Blogs # 博客模块文件夹
  4. ├─Events # 业务事件文件夹
  5. ├─VerifyBlogEventData.cs # 审核博客事件数据
  6. └─VerifyBlogEventHandler.cs # 审核博客事件处理器
  7. ├─BlogsPack.cs # 博客模块入口类
  8. ├─BlogsService.cs # 博客服务类
  9. ├─BlogsService.Blog.cs # 博客模块-博客服务类
  10. ├─BlogsService.Post.cs # 博客模块-文章服务类
  11. └─IBlogsContract.cs # 博客模块服务接口

服务接口 IBlogsContract

接口定义分析

数据查询

对于数据查询,业务层只向 API 层开放一个 IQueryable<TEntity> 的查询数据集。原则上,服务层不实现 纯数据查询(例如 用于列表分页数据、下拉菜单选项 等数据,不涉及数据变更的查询操作) 的服务,所有的 纯数据查询 都在 API层 按需要进行查询。具体分析请看 >>数据查询应该在哪做>>

额外的,根据一定条件判断一个数据是否存在 这种需求经常会用到(例如在新增或修改一个要求唯一的字符串时,需要异步检查输入的字符串是否已存在),因此设计一个 检查实体是否存在CheckEntityExists 的服务很有必要。

!!!node

对于新增、更新、删除操作,除非很确定一次只操作一条记录除外,为了支持可能的批量操作,设计上都应把服务层的 增改删 操作设计为数组型参数的批量操作,同时使用 params 关键字使操作支持单个数据操作。

数据变更

对于每一个实体,服务层按 业务需求分析 的要求定义必要的 新增、更新、删除 等操作,OSharp框架定义了一个 业务操作结果信息类 OperationResult 来封装业务操作结果,这个结果可以返回 操作结果类型(成功/错误/未变化/不存在/验证失败)、返回消息、返回附加数据 等丰富的信息,API层 接受操作结果后可进行相应的处理。

博客模块的接口定义

回到我们的 Liuliu.Blogs 项目,根据 <业务模块设计#服务层> 的需求分析,我们需要给 博客Blog 实体定义 申请开通、开通审核、更新、删除 服务,给 文章Post 实体类定义 新增、更新、删除 服务。

接口定义如下:


  1. /// <summary>
  2. /// 业务契约接口:博客模块
  3. /// </summary>
  4. public interface IBlogsContract
  5. {
  6. #region 博客信息业务
  7. /// <summary>
  8. /// 获取 博客信息查询数据集
  9. /// </summary>
  10. IQueryable<Blog> Blogs { get; }
  11. /// <summary>
  12. /// 检查博客信息是否存在
  13. /// </summary>
  14. /// <param name="predicate">检查谓语表达式</param>
  15. /// <param name="id">更新的博客信息编号</param>
  16. /// <returns>博客信息是否存在</returns>
  17. Task<bool> CheckBlogExists(Expression<Func<Blog, bool>> predicate, int id = 0);
  18. /// <summary>
  19. /// 申请博客信息
  20. /// </summary>
  21. /// <param name="dto">申请博客信息DTO信息</param>
  22. /// <returns>业务操作结果</returns>
  23. Task<OperationResult> ApplyForBlog(BlogInputDto dto);
  24. /// <summary>
  25. /// 审核博客信息
  26. /// </summary>
  27. /// <param name="id">博客编号</param>
  28. /// <param name="isEnabled">是否通过</param>
  29. /// <returns>业务操作结果</returns>
  30. Task<OperationResult> VerifyBlog(int id, bool isEnabled);
  31. /// <summary>
  32. /// 更新博客信息
  33. /// </summary>
  34. /// <param name="dtos">包含更新信息的博客信息DTO信息</param>
  35. /// <returns>业务操作结果</returns>
  36. Task<OperationResult> UpdateBlogs(params BlogInputDto[] dtos);
  37. /// <summary>
  38. /// 删除博客信息
  39. /// </summary>
  40. /// <param name="ids">要删除的博客信息编号</param>
  41. /// <returns>业务操作结果</returns>
  42. Task<OperationResult> DeleteBlogs(params int[] ids);
  43. #endregion
  44. #region 文章信息业务
  45. /// <summary>
  46. /// 获取 文章信息查询数据集
  47. /// </summary>
  48. IQueryable<Post> Posts { get; }
  49. /// <summary>
  50. /// 检查文章信息是否存在
  51. /// </summary>
  52. /// <param name="predicate">检查谓语表达式</param>
  53. /// <param name="id">更新的文章信息编号</param>
  54. /// <returns>文章信息是否存在</returns>
  55. Task<bool> CheckPostExists(Expression<Func<Post, bool>> predicate, int id = 0);
  56. /// <summary>
  57. /// 添加文章信息
  58. /// </summary>
  59. /// <param name="dtos">要添加的文章信息DTO信息</param>
  60. /// <returns>业务操作结果</returns>
  61. Task<OperationResult> CreatePosts(params PostInputDto[] dtos);
  62. /// <summary>
  63. /// 更新文章信息
  64. /// </summary>
  65. /// <param name="dtos">包含更新信息的文章信息DTO信息</param>
  66. /// <returns>业务操作结果</returns>
  67. Task<OperationResult> UpdatePosts(params PostInputDto[] dtos);
  68. /// <summary>
  69. /// 删除文章信息
  70. /// </summary>
  71. /// <param name="ids">要删除的文章信息编号</param>
  72. /// <returns>业务操作结果</returns>
  73. Task<OperationResult> DeletePosts(params int[] ids);
  74. #endregion
  75. }

服务实现 BlogsService

依赖服务注入方式分析

服务层的业务实现,通过向服务实现类注入 数据仓储IRepository<TEntity, TKey> 对象来获得向数据库存取数据的能力。根据 .NetCore 的依赖注入使用原则,常规的做法是在服务实现类的 构造函数 进行依赖服务的注入。形如:


  1. /// <summary>
  2. /// 业务服务实现:博客模块
  3. /// </summary>
  4. public class BlogsService : IBlogsContract
  5. {
  6. private readonly IRepository<Blog, int> _blogRepository;
  7. private readonly IRepository<Post, int> _postRepository;
  8. private readonly IRepository<User, int> _userRepository;
  9. private readonly IRepository<Role, int> _roleRepository;
  10. private readonly IRepository<UserRole, Guid> _userRoleRepository;
  11. private readonly IEventBus _eventBus;
  12. /// <summary>
  13. /// 初始化一个<see cref="BlogsService"/>类型的新实例
  14. /// </summary>
  15. public BlogsService(IRepository<Blog, int> blogRepository,
  16. IRepository<Post, int> postRepository,
  17. IRepository<User, int> userRepository,
  18. IRepository<Role, int> roleRepository,
  19. IRepository<UserRole, Guid> userRoleRepository,
  20. IEventBus eventBus)
  21. {
  22. _blogRepository = blogRepository;
  23. _postRepository = postRepository;
  24. _userRepository = userRepository;
  25. _roleRepository = roleRepository;
  26. _userRoleRepository = userRoleRepository;
  27. _eventBus = eventBus;
  28. }
  29. }

构造函数注入带来的性能影响

每个仓储都使用构造函数注入的话,如果模块的业务比较复杂,涉及的实体比较多(比如十几个实体是很经常的事),就会造成每次实例化 BlogsService 类的实例的时候,都需要去实例化很多个依赖服务,而实际上 一次业务执行只执行服务中的某个方法,可能也就用到其中的一两个依赖服务,这就造成了很多不必要的额外工作,也就是性能损耗。

依赖服务注入的性能优化

如果 不考虑业务服务的可测试性(单元测试通常需要Mock依赖服务)的话,在构造函数中只注入 IServiceProvider 实例,然后在业务代码中使用 serviceProvider.GetService<T>() 的方式来 按需获取 依赖服务的实例,是比较经济的方式。则服务实现变为如下所示:

  1. /// <summary>
  2. /// 业务服务实现:博客模块
  3. /// </summary>
  4. public class BlogsService : IBlogsContract
  5. {
  6. private readonly IServiceProvider _serviceProvider;
  7. /// <summary>
  8. /// 初始化一个<see cref="BlogsService"/>类型的新实例
  9. /// </summary>
  10. public BlogsService(IServiceProvider serviceProvider)
  11. {
  12. _serviceProvider = serviceProvider;
  13. }
  14. /// <summary>
  15. /// 获取 博客仓储对象
  16. /// </summary>
  17. protected IRepository<Blog, int> BlogRepository => _serviceProvider.GetService<IRepository<Blog, int>>();
  18. /// <summary>
  19. /// 获取 文章仓储对象
  20. /// </summary>
  21. protected IRepository<Post, int> PostRepository => _serviceProvider.GetService<IRepository<Post, int>>();
  22. /// <summary>
  23. /// 获取 用户仓储对象
  24. /// </summary>
  25. protected IRepository<User, int> UserRepository => _serviceProvider.GetService<IRepository<User, int>>();
  26. /// <summary>
  27. /// 获取 角色仓储对象
  28. /// </summary>
  29. protected IRepository<Role, int> RoleRepository => _serviceProvider.GetService<IRepository<Role, int>>();
  30. /// <summary>
  31. /// 获取 角色仓储对象
  32. /// </summary>
  33. protected IRepository<UserRole, Guid> UserRoleRepository => _serviceProvider.GetService<IRepository<UserRole, Guid>>();
  34. /// <summary>
  35. /// 获取 事件总线对象
  36. /// </summary>
  37. protected IEventBus EventBus => _serviceProvider.GetService<IEventBus>();
  38. }

各个依赖服务改为属性的存在方式,并且可访问性为 protected,这就保证了依赖服务的安全性。依赖服务使用 serviceProvider.GetService<T>() 的方式创建实例,可以做到 按需创建,达到性能优化的目的。

增改删操作的简化

常规批量操作的弊端

直接通过 数据仓储IRepository<TEntity, TKey> 实现数据的增改删的批量操作,总免不了要使用循环来遍历传进来的多个InputDto,例如文章的更新操作,以下几个步骤是免不了的:

  1. dto.Id 查找出相应的文章实体 entity,如果不存在,中止操作并返回
  2. 进行更新前的数据检查
    1. 检查 dto 的合法性,比如文章标题要求唯一,dto.Title 就要验证唯一性,中止操作并返回
    2. 检查 entity 的合法性,比如文章已锁定,就不允许编辑,要进行拦截,检查不通过,中止操作并返回
  3. 使用 AutoMapperdto 的值更新到 entity
  4. 进行其他关联实体的更新
    1. 比如添加文章的编辑记录
    2. 比如给当前操作人加积分
  5. entity 的更新提交到数据库

整个过程实现代码如下:

  1. /// <summary>
  2. /// 更新文章信息
  3. /// </summary>
  4. /// <param name="dtos">包含更新信息的文章信息DTO信息</param>
  5. /// <returns>业务操作结果</returns>
  6. public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
  7. {
  8. Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
  9. int count = 0;
  10. foreach (PostInputDto dto in dtos)
  11. {
  12. Post entity = await PostRepository.GetAsync(dto.Id);
  13. if (entity == null)
  14. {
  15. return new OperationResult(OperationResultType.QueryNull, $"编号为{dto.Id}的文章信息无法找到");
  16. }
  17. // todo:
  18. // 在这里要检查 dto 的合法性,比如文章标题要求唯一,dto.Title 就要验证唯一性
  19. // 在这里要检查 entity 的合法性,比如文章已锁定,就不允许编辑,要进行拦截
  20. entity = dto.MapTo(entity);
  21. // todo:
  22. // 在这里要进行其他实体的关联更新,比如添加文章的编辑记录
  23. count += await PostRepository.UpdateAsync(entity);
  24. }
  25. if (count > 0)
  26. {
  27. return new OperationResult(OperationResultType.Success, $"{dtos.Length}个文章信息更新成功");
  28. }
  29. return OperationResult.NoChanged;
  30. }

批量操作改进

这是个重复性很大的繁琐工作,整个流程中只有第2步和第4步是变化的,其余步骤都相对固定。为了简化这类操作,我们可以将第2、4步骤变化点封装起来,使用 委托 将操作内容作为参数传进来。

OSharp在 数据仓储IRepository<TEntity, TKey> 中定义了关于这类 IInputDto 类型参数的实体批量操作API。

例如批量更新,实现如下:

  1. /// <summary>
  2. /// 异步以DTO为载体批量更新实体
  3. /// </summary>
  4. /// <typeparam name="TEditDto">更新DTO类型</typeparam>
  5. /// <param name="dtos">更新DTO信息集合</param>
  6. /// <param name="checkAction">更新信息合法性检查委托</param>
  7. /// <param name="updateFunc">由DTO到实体的转换委托</param>
  8. /// <returns>业务操作结果</returns>
  9. public virtual async Task<OperationResult> UpdateAsync<TEditDto>(ICollection<TEditDto> dtos,
  10. Func<TEditDto, TEntity, Task> checkAction = null,
  11. Func<TEditDto, TEntity, Task<TEntity>> updateFunc = null) where TEditDto : IInputDto<TKey>
  12. {
  13. List<string> names = new List<string>();
  14. foreach (TEditDto dto in dtos)
  15. {
  16. try
  17. {
  18. TEntity entity = await _dbSet.FindAsync(dto.Id);
  19. if (entity == null)
  20. {
  21. return new OperationResult(OperationResultType.QueryNull);
  22. }
  23. if (checkAction != null)
  24. {
  25. await checkAction(dto, entity);
  26. }
  27. entity = dto.MapTo(entity);
  28. if (updateFunc != null)
  29. {
  30. entity = await updateFunc(dto, entity);
  31. }
  32. entity = CheckUpdate(entity)[0];
  33. _dbContext.Update<TEntity, TKey>(entity);
  34. }
  35. catch (OsharpException e)
  36. {
  37. return new OperationResult(OperationResultType.Error, e.Message);
  38. }
  39. catch (Exception e)
  40. {
  41. _logger.LogError(e, e.Message);
  42. return new OperationResult(OperationResultType.Error, e.Message);
  43. }
  44. names.AddIfNotNull(GetNameValue(dto));
  45. }
  46. int count = await _dbContext.SaveChangesAsync(_cancellationTokenProvider.Token);
  47. return count > 0
  48. ? new OperationResult(OperationResultType.Success,
  49. names.Count > 0
  50. ? "信息“{0}”更新成功".FormatWith(names.ExpandAndToString())
  51. : "{0}个信息更新成功".FormatWith(dtos.Count))
  52. : new OperationResult(OperationResultType.NoChanged);
  53. }

如上高亮代码,此方法定义了 Func<TEditDto, TEntity, Task> checkActionFunc<TEditDto, TEntity, Task<TEntity>> updateFunc 两个委托参数作为 更新前参数检查更新后关联更新 的操作传入方式,方法中是以 OsharpException 类型异常来作为中止信号的,如果需要在委托中中止操作,直接抛 OsharpException 异常即可。在调用时,即可极大简化批量更新的操作,如上的更新代码,简化如下:

  1. /// <summary>
  2. /// 更新文章信息
  3. /// </summary>
  4. /// <param name="dtos">包含更新信息的文章信息DTO信息</param>
  5. /// <returns>业务操作结果</returns>
  6. public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
  7. {
  8. Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
  9. return await PostRepository.UpdateAsync(dtos, async (dto, entity) =>
  10. {
  11. // todo:
  12. // 在这里要检查 dto 的合法性,比如文章标题要求唯一,dto.Title 就要验证唯一性
  13. // 在这里要检查 entity 的合法性,比如文章已锁定,就不允许编辑,要进行拦截
  14. },
  15. async (dto, entity) =>
  16. {
  17. // todo:
  18. // 在这里要进行其他实体的关联更新,比如添加文章的编辑记录
  19. return entity;
  20. });
  21. }

如果没有必要做额外的 更新前检查 和更新后的 关联更新,上面的批量更新可以简化到极致:

  1. /// <summary>
  2. /// 更新文章信息
  3. /// </summary>
  4. /// <param name="dtos">包含更新信息的文章信息DTO信息</param>
  5. /// <returns>业务操作结果</returns>
  6. public virtual async Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
  7. {
  8. Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
  9. return await PostRepository.UpdateAsync(dtos);
  10. }

服务层的事务管理

事务开启与重用

OSharp的数据层在一次业务处理请求中遇到数据的 新增、更新、删除 操作并第一次执行 SaveChanges 操作时,会自动开启手动事务,以后再次执行 SaveChanges 操作时,会直接使用 同一连接对象 的现有事务,以保证一次业务请求的操作都自在一个事务内。

  1. public override int SaveChanges()
  2. {
  3. // ...
  4. //开启或使用现有事务
  5. BeginOrUseTransaction();
  6. int count = base.SaveChanges();
  7. // ...
  8. return count;
  9. }

事务提交

为了方便事务管理和不同的服务层之间的事务同步,OSharp框架默认的事务提交是在 API 层通过 MVC 的 UnitOfWorkAttribute 特性来提交的。

  1. /// <summary>
  2. /// 新用户注册
  3. /// </summary>
  4. /// <param name="dto">注册信息</param>
  5. /// <returns>JSON操作结果</returns>
  6. [HttpPost]
  7. [ServiceFilter(typeof(UnitOfWorkAttribute))]
  8. [ModuleInfo]
  9. [Description("用户注册")]
  10. public async Task<AjaxResult> Register(RegisterDto dto)
  11. {
  12. // ...
  13. }

当然,你也可以不在 API 层标注 [UnitOfWorkAttribute],而是在需要的时候通过 IUnitOfWork.Commit() 手动提交事务

  1. IUnitOfWork unitOfWork = HttpContext.RequestServices.GetUnitOfWork<User, int>();
  2. unitOfWork.Commit();

业务服务事件订阅与发布

业务服务事件,是通过 事件总线EventBus 来实现的,OSharp构建了一个简单的事件总线基础建设,可以很方便地订阅和发布业务事件。

订阅事件

订阅事件很简单,只需要定义一组配套的 事件数据EventData 和相应的 事件处理器EventHandler,即可完成事件订阅的工作。

IEventData

事件数据EventData 是业务服务发布事件时向事件总线传递的数据,每一种业务,都有特定的事件数据,一个事件数据可触发多个事件处理器

定义一个事件数据,需要实现 IEventData 接口

  1. /// <summary>
  2. /// 定义事件数据,所有事件都要实现该接口
  3. /// </summary>
  4. public interface IEventData
  5. {
  6. /// <summary>
  7. /// 获取 事件编号
  8. /// </summary>
  9. Guid Id { get; }
  10. /// <summary>
  11. /// 获取 事件发生的时间
  12. /// </summary>
  13. DateTime EventTime { get; }
  14. /// <summary>
  15. /// 获取或设置 事件源,触发事件的对象
  16. /// </summary>
  17. object EventSource { get; set; }
  18. }

EventDataBase

为了方便 事件数据 的定义,OSharp定义了一个通用事件数据基类EventDataBase,继承此基类,只需要添加事件触发需要的业务数据即可

  1. /// <summary>
  2. /// 事件源数据信息基类
  3. /// </summary>
  4. public abstract class EventDataBase : IEventData
  5. {
  6. /// <summary>
  7. /// 初始化一个<see cref="EventDataBase"/>类型的新实例
  8. /// </summary>
  9. protected EventDataBase()
  10. {
  11. Id = Guid.NewGuid();
  12. EventTime = DateTime.Now;
  13. }
  14. /// <summary>
  15. /// 获取 事件编号
  16. /// </summary>
  17. public Guid Id { get; }
  18. /// <summary>
  19. /// 获取 事件发生时间
  20. /// </summary>
  21. public DateTime EventTime { get; }
  22. /// <summary>
  23. /// 获取或设置 触发事件的对象
  24. /// </summary>
  25. public object EventSource { get; set; }
  26. }

IEventHandler

业务事件的处理逻辑,是通过 事件处理器 EventHandler 来实现的,事件处理器应遵从 单一职责 原则,一个处理器只做一件事,业务服务层发布一项 事件数据,可触发多个 事件处理器

  1. /// <summary>
  2. /// 定义事件处理器,所有事件处理都要实现该接口
  3. /// EventBus中,Handler的调用是同步执行的,如果需要触发就不管的异步执行,可以在实现EventHandler的Handle逻辑时使用Task.Run
  4. /// </summary>
  5. [IgnoreDependency]
  6. public interface IEventHandler
  7. {
  8. /// <summary>
  9. /// 是否可处理指定事件
  10. /// </summary>
  11. /// <param name="eventData">事件源数据</param>
  12. /// <returns>是否可处理</returns>
  13. bool CanHandle(IEventData eventData);
  14. /// <summary>
  15. /// 事件处理
  16. /// </summary>
  17. /// <param name="eventData">事件源数据</param>
  18. void Handle(IEventData eventData);
  19. /// <summary>
  20. /// 异步事件处理
  21. /// </summary>
  22. /// <param name="eventData">事件源数据</param>
  23. /// <param name="cancelToken">异步取消标识</param>
  24. /// <returns></returns>
  25. Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken));
  26. }

泛型事件处理器

  1. /// <summary>
  2. /// 定义泛型事件处理器
  3. /// EventBus中,Handler的调用是同步执行的,如果需要触发就不管的异步执行,可以在实现EventHandler的Handle逻辑时使用Task.Run
  4. /// </summary>
  5. /// <typeparam name="TEventData">事件源数据</typeparam>
  6. [IgnoreDependency]
  7. public interface IEventHandler<in TEventData> : IEventHandler where TEventData : IEventData
  8. {
  9. /// <summary>
  10. /// 事件处理
  11. /// </summary>
  12. /// <param name="eventData">事件源数据</param>
  13. void Handle(TEventData eventData);
  14. /// <summary>
  15. /// 异步事件处理
  16. /// </summary>
  17. /// <param name="eventData">事件源数据</param>
  18. /// <param name="cancelToken">异步取消标识</param>
  19. Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken));
  20. }

EventHandlerBase

同样的,为了方便 事件处理器 的定义,OSharp定义了一个通用的事件处理器基类EventHandlerBase<TEventData>,继承此基类,只需要实现核心的事件处理逻辑即可

  1. /// <summary>
  2. /// 事件处理器基类
  3. /// </summary>
  4. public abstract class EventHandlerBase<TEventData> : IEventHandler<TEventData> where TEventData : IEventData
  5. {
  6. /// <summary>
  7. /// 是否可处理指定事件
  8. /// </summary>
  9. /// <param name="eventData">事件源数据</param>
  10. /// <returns>是否可处理</returns>
  11. public virtual bool CanHandle(IEventData eventData)
  12. {
  13. return eventData.GetType() == typeof(TEventData);
  14. }
  15. /// <summary>
  16. /// 事件处理
  17. /// </summary>
  18. /// <param name="eventData">事件源数据</param>
  19. public virtual void Handle(IEventData eventData)
  20. {
  21. if (!CanHandle(eventData))
  22. {
  23. return;
  24. }
  25. Handle((TEventData)eventData);
  26. }
  27. /// <summary>
  28. /// 异步事件处理
  29. /// </summary>
  30. /// <param name="eventData">事件源数据</param>
  31. /// <param name="cancelToken">异步取消标识</param>
  32. /// <returns></returns>
  33. public virtual Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken))
  34. {
  35. if (!CanHandle(eventData))
  36. {
  37. return Task.FromResult(0);
  38. }
  39. return HandleAsync((TEventData)eventData, cancelToken);
  40. }
  41. /// <summary>
  42. /// 事件处理
  43. /// </summary>
  44. /// <param name="eventData">事件源数据</param>
  45. public abstract void Handle(TEventData eventData);
  46. /// <summary>
  47. /// 异步事件处理
  48. /// </summary>
  49. /// <param name="eventData">事件源数据</param>
  50. /// <param name="cancelToken">异步取消标识</param>
  51. /// <returns>是否成功</returns>
  52. public virtual Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken))
  53. {
  54. return Task.Run(() => Handle(eventData), cancelToken);
  55. }
  56. }

发布事件

事件的发布,就相当简单了,只需要实例化一个事件数据EventData的实例,然后通过IEventBus.Publish(eventData)即可发布事件,触发该EventData的所有订阅处理器

  1. XXXEventData eventData = new XXXEventData()
  2. {
  3. // ...
  4. };
  5. EventBus.Publish(eventData);

博客模块的业务事件实现

回到我们的 Liuliu.Blogs 项目,根据 <业务模块设计#博客业务需求分析> 的需求分析的第二条,审核博客之后需要发邮件通知用户,发邮件属于审核博客业务计划外的需求,使用 业务事件 来实现正当其时。

  • 审核博客业务事件数据
  1. /// <summary>
  2. /// 审核博客事件数据
  3. /// </summary>
  4. public class VerifyBlogEventData : EventDataBase
  5. {
  6. /// <summary>
  7. /// 获取或设置 博客名称
  8. /// </summary>
  9. public string BlogName { get; set; }
  10. /// <summary>
  11. /// 获取或设置 用户名
  12. /// </summary>
  13. public string UserName { get; set; }
  14. /// <summary>
  15. /// 获取或设置 审核是否通过
  16. /// </summary>
  17. public bool IsEnabled { get; set; }
  18. }
  • 审核博客业务事件处理器
  1. /// <summary>
  2. /// 审核博客事件处理器
  3. /// </summary>
  4. public class VerifyBlogEventHandler : EventHandlerBase<VerifyBlogEventData>
  5. {
  6. private readonly ILogger _logger;
  7. /// <summary>
  8. /// 初始化一个<see cref="VerifyBlogEventHandler"/>类型的新实例
  9. /// </summary>
  10. public VerifyBlogEventHandler(IServiceProvider serviceProvider)
  11. {
  12. _logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger<VerifyBlogEventHandler>();
  13. }
  14. /// <summary>事件处理</summary>
  15. /// <param name="eventData">事件源数据</param>
  16. public override void Handle(VerifyBlogEventData eventData)
  17. {
  18. _logger.LogInformation(
  19. $"触发 审核博客事件处理器,用户“{eventData.UserName}”的博客“{eventData.BlogName}”审核结果:{(eventData.IsEnabled ? "通过" : "未通过")}");
  20. }
  21. }

博客模块的服务实现

回到我们的 Liuliu.Blogs 项目,根据 <业务模块设计#服务层> 的需求分析,综合使用OSharp框架提供的基础建设,博客模块的业务服务实现如下:

BlogsService.cs

  1. /// <summary>
  2. /// 业务服务实现:博客模块
  3. /// </summary>
  4. public partial class BlogsService : IBlogsContract
  5. {
  6. private readonly IServiceProvider _serviceProvider;
  7. /// <summary>
  8. /// 初始化一个<see cref="BlogsService"/>类型的新实例
  9. /// </summary>
  10. public BlogsService(IServiceProvider serviceProvider)
  11. {
  12. _serviceProvider = serviceProvider;
  13. }
  14. /// <summary>
  15. /// 获取 博客仓储对象
  16. /// </summary>
  17. protected IRepository<Blog, int> BlogRepository => _serviceProvider.GetService<IRepository<Blog, int>>();
  18. /// <summary>
  19. /// 获取 文章仓储对象
  20. /// </summary>
  21. protected IRepository<Post, int> PostRepository => _serviceProvider.GetService<IRepository<Post, int>>();
  22. /// <summary>
  23. /// 获取 用户仓储对象
  24. /// </summary>
  25. protected IRepository<User, int> UserRepository => _serviceProvider.GetService<IRepository<User, int>>();
  26. }

BlogsService.Blog.cs

  1. public partial class BlogsService
  2. {
  3. /// <summary>
  4. /// 获取 博客信息查询数据集
  5. /// </summary>
  6. public virtual IQueryable<Blog> Blogs => BlogRepository.Query();
  7. /// <summary>
  8. /// 检查博客信息是否存在
  9. /// </summary>
  10. /// <param name="predicate">检查谓语表达式</param>
  11. /// <param name="id">更新的博客信息编号</param>
  12. /// <returns>博客信息是否存在</returns>
  13. public virtual Task<bool> CheckBlogExists(Expression<Func<Blog, bool>> predicate, int id = 0)
  14. {
  15. Check.NotNull(predicate, nameof(predicate));
  16. return BlogRepository.CheckExistsAsync(predicate, id);
  17. }
  18. /// <summary>
  19. /// 申请博客信息
  20. /// </summary>
  21. /// <param name="dto">申请博客信息DTO信息</param>
  22. /// <returns>业务操作结果</returns>
  23. public virtual async Task<OperationResult> ApplyForBlog(BlogInputDto dto)
  24. {
  25. Check.Validate(dto, nameof(dto));
  26. // 博客是以当前用户的身份来申请的
  27. ClaimsPrincipal principal = _serviceProvider.GetCurrentUser();
  28. if (principal == null || !principal.Identity.IsAuthenticated)
  29. {
  30. return new OperationResult(OperationResultType.Error, "用户未登录或登录已失效");
  31. }
  32. int userId = principal.Identity.GetUserId<int>();
  33. User user = await UserRepository.GetAsync(userId);
  34. if (user == null)
  35. {
  36. return new OperationResult(OperationResultType.QueryNull, $"编号为“{userId}”的用户信息不存在");
  37. }
  38. Blog blog = BlogRepository.TrackQuery(m => m.UserId == userId).FirstOrDefault();
  39. if (blog != null)
  40. {
  41. return new OperationResult(OperationResultType.Error, "当前用户已开通博客,不能重复申请");
  42. }
  43. if (await CheckBlogExists(m => m.Url == dto.Url))
  44. {
  45. return new OperationResult(OperationResultType.Error, $"Url 为“{dto.Url}”的博客已存在,不能重复添加");
  46. }
  47. blog = dto.MapTo<Blog>();
  48. blog.UserId = userId;
  49. int count = await BlogRepository.InsertAsync(blog);
  50. return count > 0
  51. ? new OperationResult(OperationResultType.Success, "博客申请成功")
  52. : OperationResult.NoChanged;
  53. }
  54. /// <summary>
  55. /// 审核博客信息
  56. /// </summary>
  57. /// <param name="id">博客编号</param>
  58. /// <param name="isEnabled">是否通过</param>
  59. /// <returns>业务操作结果</returns>
  60. public virtual async Task<OperationResult> VerifyBlog(int id, bool isEnabled)
  61. {
  62. Blog blog = await BlogRepository.GetAsync(id);
  63. if (blog == null)
  64. {
  65. return new OperationResult(OperationResultType.QueryNull, $"编号为“{id}”的博客信息不存在");
  66. }
  67. // 更新博客
  68. blog.IsEnabled = isEnabled;
  69. int count = await BlogRepository.UpdateAsync(blog);
  70. User user = await UserRepository.GetAsync(blog.UserId);
  71. if (user == null)
  72. {
  73. return new OperationResult(OperationResultType.QueryNull, $"编号为“{blog.UserId}”的用户信息不存在");
  74. }
  75. // 如果开通博客,给用户开通博主身份
  76. if (isEnabled)
  77. {
  78. // 查找博客主的角色,博主角色名可由配置系统获得
  79. const string roleName = "博主";
  80. // 用于CUD操作的实体,要用 TrackQuery 方法来查询出需要的数据,不能用 Query,因为 Query 会使用 AsNoTracking
  81. Role role = RoleRepository.TrackQuery(m => m.Name == roleName).FirstOrDefault();
  82. if (role == null)
  83. {
  84. return new OperationResult(OperationResultType.QueryNull, $"名称为“{roleName}”的角色信息不存在");
  85. }
  86. UserRole userRole = UserRoleRepository.TrackQuery(m => m.UserId == user.Id && m.RoleId == role.Id)
  87. .FirstOrDefault();
  88. if (userRole == null)
  89. {
  90. userRole = new UserRole() { UserId = user.Id, RoleId = role.Id, IsLocked = false };
  91. count += await UserRoleRepository.InsertAsync(userRole);
  92. }
  93. }
  94. OperationResult result = count > 0
  95. ? new OperationResult(OperationResultType.Success, $"博客“{blog.Display}”审核 {(isEnabled ? "通过" : "未通过")}")
  96. : OperationResult.NoChanged;
  97. if (result.Succeeded)
  98. {
  99. VerifyBlogEventData eventData = new VerifyBlogEventData()
  100. {
  101. BlogName = blog.Display,
  102. UserName = user.NickName,
  103. IsEnabled = isEnabled
  104. };
  105. EventBus.Publish(eventData);
  106. }
  107. return result;
  108. }
  109. /// <summary>
  110. /// 更新博客信息
  111. /// </summary>
  112. /// <param name="dtos">包含更新信息的博客信息DTO信息</param>
  113. /// <returns>业务操作结果</returns>
  114. public virtual Task<OperationResult> UpdateBlogs(params BlogInputDto[] dtos)
  115. {
  116. return BlogRepository.UpdateAsync(dtos, async (dto, entity) =>
  117. {
  118. if (await BlogRepository.CheckExistsAsync(m => m.Url == dto.Url, dto.Id))
  119. {
  120. throw new OsharpException($"Url为“{dto.Url}”的博客已存在,不能重复");
  121. }
  122. });
  123. }
  124. /// <summary>
  125. /// 删除博客信息
  126. /// </summary>
  127. /// <param name="ids">要删除的博客信息编号</param>
  128. /// <returns>业务操作结果</returns>
  129. public virtual Task<OperationResult> DeleteBlogs(params int[] ids)
  130. {
  131. return BlogRepository.DeleteAsync(ids, entity =>
  132. {
  133. if (PostRepository.Query(m => m.BlogId == entity.Id).Any())
  134. {
  135. throw new OsharpException($"博客“{entity.Display}”中还有文章未删除,请先删除所有文章,再删除博客");
  136. }
  137. return Task.FromResult(0);
  138. });
  139. }
  140. }

BlogsService.Post.cs

  1. public partial class BlogsService
  2. {
  3. /// <summary>
  4. /// 获取 文章信息查询数据集
  5. /// </summary>
  6. public virtual IQueryable<Post> Posts => PostRepository.Query();
  7. /// <summary>
  8. /// 检查文章信息是否存在
  9. /// </summary>
  10. /// <param name="predicate">检查谓语表达式</param>
  11. /// <param name="id">更新的文章信息编号</param>
  12. /// <returns>文章信息是否存在</returns>
  13. public virtual Task<bool> CheckPostExists(Expression<Func<Post, bool>> predicate, int id = 0)
  14. {
  15. Check.NotNull(predicate, nameof(predicate));
  16. return PostRepository.CheckExistsAsync(predicate, id);
  17. }
  18. /// <summary>
  19. /// 添加文章信息
  20. /// </summary>
  21. /// <param name="dtos">要添加的文章信息DTO信息</param>
  22. /// <returns>业务操作结果</returns>
  23. public virtual async Task<OperationResult> CreatePosts(params PostInputDto[] dtos)
  24. {
  25. Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
  26. if (dtos.Length == 0)
  27. {
  28. return OperationResult.NoChanged;
  29. }
  30. // 文章是以当前用户身份来添加的
  31. ClaimsPrincipal principal = _serviceProvider.GetCurrentUser();
  32. if (principal == null || !principal.Identity.IsAuthenticated)
  33. {
  34. throw new OsharpException("用户未登录或登录已失效");
  35. }
  36. // 检查当前用户的博客状态
  37. int userId = principal.Identity.GetUserId<int>();
  38. Blog blog = BlogRepository.TrackQuery(m => m.UserId == userId).FirstOrDefault();
  39. if (blog == null || !blog.IsEnabled)
  40. {
  41. throw new OsharpException("当前用户的博客未开通,无法添加文章");
  42. }
  43. // 没有前置检查,checkAction为null
  44. return await PostRepository.InsertAsync(dtos, null, (dto, entity) =>
  45. {
  46. // 给新建的文章关联博客和作者
  47. entity.BlogId = blog.Id;
  48. entity.UserId = userId;
  49. return Task.FromResult(entity);
  50. });
  51. }
  52. /// <summary>
  53. /// 更新文章信息
  54. /// </summary>
  55. /// <param name="dtos">包含更新信息的文章信息DTO信息</param>
  56. /// <returns>业务操作结果</returns>
  57. public virtual Task<OperationResult> UpdatePosts(params PostInputDto[] dtos)
  58. {
  59. Check.Validate<PostInputDto, int>(dtos, nameof(dtos));
  60. return PostRepository.UpdateAsync(dtos);
  61. }
  62. /// <summary>
  63. /// 删除文章信息
  64. /// </summary>
  65. /// <param name="ids">要删除的文章信息编号</param>
  66. /// <returns>业务操作结果</returns>
  67. public virtual Task<OperationResult> DeletePosts(params int[] ids)
  68. {
  69. Check.NotNull(ids, nameof(ids));
  70. return PostRepository.DeleteAsync(ids);
  71. }
  72. }

模块入口 BlogsPack

模块入口基类

非AspNetCore模块基类 OsharpPack

前面多次提到,每个Pack模块都是继承自一个 模块基类OsharpPack,这个基类用于定义 模块初始化UsePack 过程中未涉及 AspNetCore 环境的模块。

  1. /// <summary>
  2. /// OSharp模块基类
  3. /// </summary>
  4. public abstract class OsharpPack
  5. {
  6. /// <summary>
  7. /// 获取 模块级别,级别越小越先启动
  8. /// </summary>
  9. public virtual PackLevel Level => PackLevel.Business;
  10. /// <summary>
  11. /// 获取 模块启动顺序,模块启动的顺序先按级别启动,同一级别内部再按此顺序启动,
  12. /// 级别默认为0,表示无依赖,需要在同级别有依赖顺序的时候,再重写为>0的顺序值
  13. /// </summary>
  14. public virtual int Order => 0;
  15. /// <summary>
  16. /// 获取 是否已可用
  17. /// </summary>
  18. public bool IsEnabled { get; protected set; }
  19. /// <summary>
  20. /// 将模块服务添加到依赖注入服务容器中
  21. /// </summary>
  22. /// <param name="services">依赖注入服务容器</param>
  23. /// <returns></returns>
  24. public virtual IServiceCollection AddServices(IServiceCollection services)
  25. {
  26. return services;
  27. }
  28. /// <summary>
  29. /// 应用模块服务
  30. /// </summary>
  31. /// <param name="provider">服务提供者</param>
  32. public virtual void UsePack(IServiceProvider provider)
  33. {
  34. IsEnabled = true;
  35. }
  36. /// <summary>
  37. /// 获取当前模块的依赖模块类型
  38. /// </summary>
  39. /// <returns></returns>
  40. internal Type[] GetDependPackTypes(Type packType = null)
  41. {
  42. // ...
  43. }
  44. }

模块基类OsharpPack 定义了两个可重写属性:

  • PackLevel:模块级别,级别越小越先启动

    模块级别按 模块 在框架中不同的功能层次,定义了如下几个级别:
  1. /// <summary>
  2. /// 模块级别,级别越核心,优先启动
  3. /// </summary>
  4. public enum PackLevel
  5. {
  6. /// <summary>
  7. /// 核心级别,表示系统的核心模块,
  8. /// 这些模块不涉及第三方组件,在系统运行中是不可替换的,核心模块将始终加载
  9. /// </summary>
  10. Core = 1,
  11. /// <summary>
  12. /// 框架级别,表示涉及第三方组件的基础模块
  13. /// </summary>
  14. Framework = 10,
  15. /// <summary>
  16. /// 应用级别,表示涉及应用数据的基础模块
  17. /// </summary>
  18. Application = 20,
  19. /// <summary>
  20. /// 业务级别,表示涉及真实业务处理的模块
  21. /// </summary>
  22. Business = 30
  23. }
  • Order:级别内模块启动顺序,模块启动的顺序先按级别启动,同一级别内部再按此顺序启动,级别默认为 0,表示无依赖,需要在同级别有依赖顺序的时候,再重写为 >0 的顺序值

同时,模块基类 还定义了两个方法:

  • AddServices:用于将模块内定义的服务注入到 依赖注入服务容器 中。
  • UsePack:用于使用服务对当前模块进行初始化。

AspNetCore模块基类 AspOsharpPack

AspOsharpPack 基类继承了 OsharpPack,添加了一个对 IApplicationBuilder 支持的 UsePack 方法,用于实现与 AspNetCore 关联的模块初始化工作,例如 Mvc模块 初始化的时候需要应用中间件:app.UseMvcWithAreaRoute();

  1. /// <summary>
  2. /// 基于AspNetCore环境的Pack模块基类
  3. /// </summary>
  4. public abstract class AspOsharpPack : OsharpPack
  5. {
  6. /// <summary>
  7. /// 应用AspNetCore的服务业务
  8. /// </summary>
  9. /// <param name="app">Asp应用程序构建器</param>
  10. public virtual void UsePack(IApplicationBuilder app)
  11. {
  12. base.UsePack(app.ApplicationServices);
  13. }
  14. }

博客模块的模块入口实现

回到我们的 Liuliu.Blogs 项目,我们来实现投票模块的模块入口类 BlogsPack

  • 博客模块属于业务模块,因此 PackLevel 设置为 Business
  • 博客模块的启动顺序无需重写,保持 0 即可
  • 将 博客业务服务 注册到 服务容器中
  • 无甚初始化业务

实现代码如下:

  1. /// <summary>
  2. /// 博客模块
  3. /// </summary>
  4. public class BlogsPack : OsharpPack
  5. {
  6. /// <summary>
  7. /// 获取 模块级别,级别越小越先启动
  8. /// </summary>
  9. public override PackLevel Level { get; } = PackLevel.Business;
  10. /// <summary>将模块服务添加到依赖注入服务容器中</summary>
  11. /// <param name="services">依赖注入服务容器</param>
  12. /// <returns></returns>
  13. public override IServiceCollection AddServices(IServiceCollection services)
  14. {
  15. services.TryAddScoped<IBlogsContract, BlogsService>();
  16. return services;
  17. }
  18. }

至此,博客模块的服务层实现完毕。

[开源]OSharpNS 步步为营系列 - 3. 添加业务服务层的更多相关文章

  1. [开源]OSharpNS 步步为营系列 - 4. 添加业务对外API

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  2. [开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  3. [开源]OSharpNS 步步为营系列 - 5. 添加前端Angular模块[完结]

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  4. [开源]OSharpNS 步步为营系列 - 1. 业务模块设计

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  5. Map工具系列-05-添加业务参数工具

    所有cs端工具集成了一个工具面板 -打开(IE) Map工具系列-01-Map代码生成工具说明 Map工具系列-02-数据迁移工具使用说明 Map工具系列-03-代码生成BySQl工具使用说明 Map ...

  6. 开源分布式Job系统,调度与业务分离-如何创建一个计划HttpJob任务

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  7. 开源分布式Job系统,调度与业务分离-如何创建周期性的HttpJob任务

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  8. 开源分布式Job系统,调度与业务分离-HttpJob.Agent组件介绍以及如何使用

    项目介绍: Hangfire:是一个开源的job调度系统,支持分布式JOB!! Hangfire.HttpJob 是我针对Hangfire开发的一个组件,该组件和Hangfire本身是独立的.可以独立 ...

  9. Quartz.NET开源作业调度框架系列

    Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...

随机推荐

  1. 所有语言的Awesome(2)

    Curated list of awesome lists https://awesomeweekly.co https://github.com/sindresorhus/awesome ✨ Pre ...

  2. vagrant up 无法加载映像目录

    错误代码显示: ==> default: Attempting graceful shutdown of VM... ==> default: Clearing any previousl ...

  3. 使用ServiceStack.Redis实现Redis数据读写

    原文:使用ServiceStack.Redis实现Redis数据读写 User.cs实体类 public class User { public string Name { get; set; } p ...

  4. Android零基础入门第53节:拖动条SeekBar和星级评分条RatingBar

    原文:Android零基础入门第53节:拖动条SeekBar和星级评分条RatingBar 前面两期都在学习ProgressBar的使用,关于自定义ProgressBar的内容后期会继续学习的,本期先 ...

  5. Android零基础入门第70节:ViewPager轻松完成TabHost效果

    上一期学习了ViewPager的简单使用,本期一起来学习ViewPager的更多用法. 相信很多同学都使用过今日头条APP吧,一打开主界面就可以看到顶部有很多Tab,然后通过左右滑动来切换,就可以通过 ...

  6. PC-lint 简明教程(C/C++静态代码检查工具)

    前言 PC-lint是一款小而强大的C/C++静态代码检查工具,它可以检查未初始化变量,数组越界,空指针等编译器很难发现的潜在错误.在很多专业的软件公司如Microsoft,PC-Lint检查无错误无 ...

  7. Windows服务(system权限)程序显示界面与用户交互,Session0通知Session1里弹出对话框(真的很牛) good

    源码资源下载:http://download.csdn.net/detail/stony1980/4512984   1.VC2008中编写“Windows服务”(Windows Service)程序 ...

  8. Geoserver发布Image Mossaic图层

    1数据准备:请事先在arcgis desktop软件中将栅格数据拼接完毕,并为每一幅影像生成一个prj文件,坐标系一定是要有的,不然Mossaic图层发布不了. 2."数据存储“->& ...

  9. 百度AI开放平台,语音识别,语音合成以及短文本相似度

    百度AI开放平台:https://ai.baidu.com/ 语音合成 from aip import AipSpeech APP_ID=" #'你的 App ID' API_KEY=&qu ...

  10. 3020配置_Java_win10

    1. 安装Java SE平台 1°  下载 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-213315 ...