介绍

9月开篇讲,前面几章群里已经有几个小伙伴跟着做了一遍了,遇到的问题和疑惑也都在群里反馈和解决好了,9月咱们保持保持更新。争取10月份更新完基础篇。

另外番外篇属于 我在abp群里和日常开发的问题记录,如果各位在使用abp的过程中发现什么问题也可以及时反馈给我。

上一章已经把所有实体的迁移都做好了,这一章我们进入到文章聚合,文章聚合涉及接口比较多。

开工

先来看下需要定义那些应用层接口,Dto我也在下面定义好了,关于里面的BlogUserDto这个是作者目前打算采用ABP Identtiy中的User来做到时候通过权限控制,另外就是TagDto属于Posts领域的Dto.

  1. public interface IPostAppService : IApplicationService
  2. {
  3. Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid blogId, string tagName);
  4. Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedListAsync(Guid blogId);
  5. Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input);
  6. Task<PostWithDetailsDto> GetAsync(Guid id);
  7. Task DeleteAsync(Guid id);
  8. Task<PostWithDetailsDto> CreateAsync(CreatePostDto input);
  9. Task<PostWithDetailsDto> UpdateAsync(Guid id, UpdatePostDto input);
  10. }
  11. public class BlogUserDto : EntityDto<Guid>
  12. {
  13. public Guid? TenantId { get; set; }
  14. public string UserName { get; set; }
  15. public string Email { get; set; }
  16. public bool EmailConfirmed { get; set; }
  17. public string PhoneNumber { get; set; }
  18. public bool PhoneNumberConfirmed { get; set; }
  19. public Dictionary<string, object> ExtraProperties { get; set; }
  20. }
  21. public class CreatePostDto
  22. {
  23. public Guid BlogId { get; set; }
  24. [Required]
  25. [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxTitleLength))]
  26. public string Title { get; set; }
  27. [Required]
  28. public string CoverImage { get; set; }
  29. [Required]
  30. [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxUrlLength))]
  31. public string Url { get; set; }
  32. [Required]
  33. [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxContentLength))]
  34. public string Content { get; set; }
  35. public string Tags { get; set; }
  36. [DynamicStringLength(typeof(PostConsts), nameof(PostConsts.MaxDescriptionLength))]
  37. public string Description { get; set; }
  38. }
  39. public class GetPostInput
  40. {
  41. [Required]
  42. public string Url { get; set; }
  43. public Guid BlogId { get; set; }
  44. }
  45. public class UpdatePostDto
  46. {
  47. public Guid BlogId { get; set; }
  48. [Required]
  49. public string Title { get; set; }
  50. [Required]
  51. public string CoverImage { get; set; }
  52. [Required]
  53. public string Url { get; set; }
  54. [Required]
  55. public string Content { get; set; }
  56. public string Description { get; set; }
  57. public string Tags { get; set; }
  58. }
  59. public class PostWithDetailsDto : FullAuditedEntityDto<Guid>
  60. {
  61. public Guid BlogId { get; set; }
  62. public string Title { get; set; }
  63. public string CoverImage { get; set; }
  64. public string Url { get; set; }
  65. public string Content { get; set; }
  66. public string Description { get; set; }
  67. public int ReadCount { get; set; }
  68. public int CommentCount { get; set; }
  69. [CanBeNull]
  70. public BlogUserDto Writer { get; set; }
  71. public List<TagDto> Tags { get; set; }
  72. }
  73. public class TagDto : FullAuditedEntityDto<Guid>
  74. {
  75. public string Name { get; set; }
  76. public string Description { get; set; }
  77. public int UsageCount { get; set; }
  78. }

根据上上面的接口我想,就应该明白ABP自带的仓储无法满足我们业务需求,我们需要自定义仓储,在大多数场景下我们不会采用ABP提供的泛型仓储,除非业务足够简单泛型仓储完全满足(个人意见)。

另外我们重写了WithDetailsAsync通过扩展IncludeDetails方法实现Include包含⼦集合对象,其实这个也可以作为可选参数我们可以在使用ABP提供的泛型仓储GetAsync方法中看到他有一个可选参数includeDetails,来指明查询是否包含⼦集合对象。

  1. public interface IPostRepository : IBasicRepository<Post, Guid>
  2. {
  3. Task<List<Post>> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default);
  4. Task<bool> IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default);
  5. Task<Post> GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default);
  6. Task<List<Post>> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default);
  7. }
  8. public class EfCorePostRepository : EfCoreRepository<CoreDbContext, Post, Guid>, IPostRepository
  9. {
  10. public EfCorePostRepository(IDbContextProvider<CoreDbContext> dbContextProvider)
  11. : base(dbContextProvider)
  12. {
  13. }
  14. public async Task<List<Post>> GetPostsByBlogId(Guid id, CancellationToken cancellationToken = default)
  15. {
  16. return await (await GetDbSetAsync()).Where(p => p.BlogId == id).OrderByDescending(p => p.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
  17. }
  18. public async Task<bool> IsPostUrlInUseAsync(Guid blogId, string url, Guid? excludingPostId = null, CancellationToken cancellationToken = default)
  19. {
  20. var query = (await GetDbSetAsync()).Where(p => blogId == p.BlogId && p.Url == url);
  21. if (excludingPostId != null)
  22. {
  23. query = query.Where(p => excludingPostId != p.Id);
  24. }
  25. return await query.AnyAsync(GetCancellationToken(cancellationToken));
  26. }
  27. public async Task<Post> GetPostByUrl(Guid blogId, string url, CancellationToken cancellationToken = default)
  28. {
  29. var post = await (await GetDbSetAsync()).FirstOrDefaultAsync(p => p.BlogId == blogId && p.Url == url, GetCancellationToken(cancellationToken));
  30. if (post == null)
  31. {
  32. throw new EntityNotFoundException(typeof(Post), nameof(post));
  33. }
  34. return post;
  35. }
  36. public async Task<List<Post>> GetOrderedList(Guid blogId, bool descending = false, CancellationToken cancellationToken = default)
  37. {
  38. if (!descending)
  39. {
  40. return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderByDescending(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
  41. }
  42. else
  43. {
  44. return await (await GetDbSetAsync()).Where(x => x.BlogId == blogId).OrderBy(x => x.CreationTime).ToListAsync(GetCancellationToken(cancellationToken));
  45. }
  46. }
  47. public override async Task<IQueryable<Post>> WithDetailsAsync()
  48. {
  49. return (await GetQueryableAsync()).IncludeDetails();
  50. }
  51. }
  52. public static class CoreEntityFrameworkCoreQueryableExtensions
  53. {
  54. public static IQueryable<Post> IncludeDetails(this IQueryable<Post> queryable, bool include = true)
  55. {
  56. if (!include)
  57. {
  58. return queryable;
  59. }
  60. return queryable
  61. .Include(x => x.Tags);
  62. }
  63. }

应用层

新建PostAppService继承IPostAppService然后开始第一个方法GetListByBlogIdAndTagName该方法根据blogId 和 tagName 查询相关的文章数据。我们有IPostRepositoryGetPostsByBlogId方法可以根据blogId获取文章,那么如何在根据tagName筛选呢,这里就需要我们新增一个ITagRepository,先不着急先实现先把业务逻辑跑通。

  1. public interface ITagRepository : IBasicRepository<Tag, Guid>
  2. {
  3. Task<List<Tag>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default);
  4. Task<Tag> FindByNameAsync(Guid blogId, string name, CancellationToken cancellationToken = default);
  5. Task<List<Tag>> GetListAsync(IEnumerable<Guid> ids, CancellationToken cancellationToken = default);
  6. }

现在进行下一步,文章已经查询出来了,文章上的作者和Tag还没处理,下面代码我写了注释代码意思应该都能看明白,这里可能会比较疑问的事这样写代码for循环去跑数据库是不是不太合理,因为Tags这个本身就不会存在很多数据,这块如果要调整其实完全可以讲TagName存在Tasg值对象中。

  1. public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
  2. {
  3. // 根据blogId查询文章数据
  4. var posts = await _postRepository.GetPostsByBlogId(id);
  5. var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));
  6. // 根据tagName筛选tag
  7. var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName);
  8. // 给文章Tags赋值
  9. foreach (var postDto in postDtos)
  10. {
  11. postDto.Tags = await GetTagsOfPost(postDto.Id);
  12. }
  13. // 筛选掉不符合要求的文章
  14. if (tag != null)
  15. {
  16. postDtos = await FilterPostsByTag(postDtos, tag);
  17. }
  18. }
  19. private async Task<List<TagDto>> GetTagsOfPost(Guid id)
  20. {
  21. var tagIds = (await _postRepository.GetAsync(id)).Tags;
  22. var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId));
  23. return ObjectMapper.Map<List<Tag>, List<TagDto>>(tags);
  24. }
  25. private Task<List<PostWithDetailsDto>> FilterPostsByTag(IEnumerable<PostWithDetailsDto> allPostDtos, Tag tag)
  26. {
  27. var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList();
  28. return Task.FromResult(filteredPostDtos);
  29. }

继续向下就是赋值作者信息,对应上面Tasg最多十几个,但是系统有多少用户就不好说了所以这里使用userDictionary就是省掉重复查询数据。

  1. public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
  2. {
  3. // 前面的代码就不重复粘贴了
  4. var userDictionary = new Dictionary<Guid, BlogUserDto>();
  5. // 赋值作者信息
  6. foreach (var postDto in postDtos)
  7. {
  8. if (postDto.CreatorId.HasValue)
  9. {
  10. if (!userDictionary.ContainsKey(postDto.CreatorId.Value))
  11. {
  12. var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
  13. if (creatorUser != null)
  14. {
  15. userDictionary[creatorUser.Id] = ObjectMapper.Map<BlogUser, BlogUserDto>(creatorUser);
  16. }
  17. }
  18. if (userDictionary.ContainsKey(postDto.CreatorId.Value))
  19. {
  20. postDto.Writer = userDictionary[(Guid)postDto.CreatorId];
  21. }
  22. }
  23. }
  24. return new ListResultDto<PostWithDetailsDto>(postDtos);
  25. }

目前删除和修改接口做不了因为这里牵扯评论的部分操作,除去这两个,其他的接口直接看代码应该都没有什么问题,这一章的东西已经很多了剩下的我们下集。

  1. public async Task<ListResultDto<PostWithDetailsDto>> GetListByBlogIdAndTagName(Guid id, string tagName)
  2. {
  3. // 根据blogId查询文章数据
  4. var posts = await _postRepository.GetPostsByBlogId(id);
  5. // 根据tagName筛选tag
  6. var tag = tagName.IsNullOrWhiteSpace() ? null : await _tagRepository.FindByNameAsync(id, tagName);
  7. var userDictionary = new Dictionary<Guid, BlogUserDto>();
  8. var postDtos = new List<PostWithDetailsDto>(ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts));
  9. // 给文章Tags赋值
  10. foreach (var postDto in postDtos)
  11. {
  12. postDto.Tags = await GetTagsOfPost(postDto.Id);
  13. }
  14. // 筛选掉不符合要求的文章
  15. if (tag != null)
  16. {
  17. postDtos = await FilterPostsByTag(postDtos, tag);
  18. }
  19. // 赋值作者信息
  20. foreach (var postDto in postDtos)
  21. {
  22. if (postDto.CreatorId.HasValue)
  23. {
  24. if (!userDictionary.ContainsKey(postDto.CreatorId.Value))
  25. {
  26. var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
  27. if (creatorUser != null)
  28. {
  29. userDictionary[creatorUser.Id] = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
  30. }
  31. }
  32. if (userDictionary.ContainsKey(postDto.CreatorId.Value))
  33. {
  34. postDto.Writer = userDictionary[(Guid)postDto.CreatorId];
  35. }
  36. }
  37. }
  38. return new ListResultDto<PostWithDetailsDto>(postDtos);
  39. }
  40. public async Task<ListResultDto<PostWithDetailsDto>> GetTimeOrderedListAsync(Guid blogId)
  41. {
  42. var posts = await _postRepository.GetOrderedList(blogId);
  43. var postsWithDetails = ObjectMapper.Map<List<Post>, List<PostWithDetailsDto>>(posts);
  44. foreach (var post in postsWithDetails)
  45. {
  46. if (post.CreatorId.HasValue)
  47. {
  48. var creatorUser = await UserLookupService.FindByIdAsync(post.CreatorId.Value);
  49. if (creatorUser != null)
  50. {
  51. post.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
  52. }
  53. }
  54. }
  55. return new ListResultDto<PostWithDetailsDto>(postsWithDetails);
  56. }
  57. public async Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input)
  58. {
  59. var post = await _postRepository.GetPostByUrl(input.BlogId, input.Url);
  60. post.IncreaseReadCount();
  61. var postDto = ObjectMapper.Map<Post, PostWithDetailsDto>(post);
  62. postDto.Tags = await GetTagsOfPost(postDto.Id);
  63. if (postDto.CreatorId.HasValue)
  64. {
  65. var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
  66. postDto.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
  67. }
  68. return postDto;
  69. }
  70. public async Task<PostWithDetailsDto> GetAsync(Guid id)
  71. {
  72. var post = await _postRepository.GetAsync(id);
  73. var postDto = ObjectMapper.Map<Post, PostWithDetailsDto>(post);
  74. postDto.Tags = await GetTagsOfPost(postDto.Id);
  75. if (postDto.CreatorId.HasValue)
  76. {
  77. var creatorUser = await UserLookupService.FindByIdAsync(postDto.CreatorId.Value);
  78. postDto.Writer = ObjectMapper.Map<IdentityUser, BlogUserDto>(creatorUser);
  79. }
  80. return postDto;
  81. }
  82. public async Task<PostWithDetailsDto> CreateAsync(CreatePostDto input)
  83. {
  84. input.Url = await RenameUrlIfItAlreadyExistAsync(input.BlogId, input.Url);
  85. var post = new Post(
  86. id: GuidGenerator.Create(),
  87. blogId: input.BlogId,
  88. title: input.Title,
  89. coverImage: input.CoverImage,
  90. url: input.Url
  91. )
  92. {
  93. Content = input.Content,
  94. Description = input.Description
  95. };
  96. await _postRepository.InsertAsync(post);
  97. var tagList = SplitTags(input.Tags);
  98. await SaveTags(tagList, post);
  99. return ObjectMapper.Map<Post, PostWithDetailsDto>(post);
  100. }
  101. private async Task<string> RenameUrlIfItAlreadyExistAsync(Guid blogId, string url, Post existingPost = null)
  102. {
  103. if (await _postRepository.IsPostUrlInUseAsync(blogId, url, existingPost?.Id))
  104. {
  105. return url + "-" + Guid.NewGuid().ToString().Substring(0, 5);
  106. }
  107. return url;
  108. }
  109. private async Task SaveTags(ICollection<string> newTags, Post post)
  110. {
  111. await RemoveOldTags(newTags, post);
  112. await AddNewTags(newTags, post);
  113. }
  114. private async Task RemoveOldTags(ICollection<string> newTags, Post post)
  115. {
  116. foreach (var oldTag in post.Tags.ToList())
  117. {
  118. var tag = await _tagRepository.GetAsync(oldTag.TagId);
  119. var oldTagNameInNewTags = newTags.FirstOrDefault(t => t == tag.Name);
  120. if (oldTagNameInNewTags == null)
  121. {
  122. post.RemoveTag(oldTag.TagId);
  123. tag.DecreaseUsageCount();
  124. await _tagRepository.UpdateAsync(tag);
  125. }
  126. else
  127. {
  128. newTags.Remove(oldTagNameInNewTags);
  129. }
  130. }
  131. }
  132. private async Task AddNewTags(IEnumerable<string> newTags, Post post)
  133. {
  134. var tags = await _tagRepository.GetListAsync(post.BlogId);
  135. foreach (var newTag in newTags)
  136. {
  137. var tag = tags.FirstOrDefault(t => t.Name == newTag);
  138. if (tag == null)
  139. {
  140. tag = await _tagRepository.InsertAsync(new Tag(GuidGenerator.Create(), post.BlogId, newTag, 1));
  141. }
  142. else
  143. {
  144. tag.IncreaseUsageCount();
  145. tag = await _tagRepository.UpdateAsync(tag);
  146. }
  147. post.AddTag(tag.Id);
  148. }
  149. }
  150. private List<string> SplitTags(string tags)
  151. {
  152. if (tags.IsNullOrWhiteSpace())
  153. {
  154. return new List<string>();
  155. }
  156. return new List<string>(tags.Split(",").Select(t => t.Trim()));
  157. }
  158. private async Task<List<TagDto>> GetTagsOfPost(Guid id)
  159. {
  160. var tagIds = (await _postRepository.GetAsync(id)).Tags;
  161. var tags = await _tagRepository.GetListAsync(tagIds.Select(t => t.TagId));
  162. return ObjectMapper.Map<List<Tag>, List<TagDto>>(tags);
  163. }
  164. private Task<List<PostWithDetailsDto>> FilterPostsByTag(IEnumerable<PostWithDetailsDto> allPostDtos, Tag tag)
  165. {
  166. var filteredPostDtos = allPostDtos.Where(p => p.Tags?.Any(t => t.Id == tag.Id) ?? false).ToList();
  167. return Task.FromResult(filteredPostDtos);
  168. }

结语

本节知识点:

  • 1.我们梳理了一个聚合的开发过程

因为该聚合东西太多了我们就拆成2章来搞一章的话太长了

联系作者:加群:867095512 @MrChuJiu

六、Abp vNext 基础篇丨文章聚合功能上的更多相关文章

  1. 七、Abp vNext 基础篇丨文章聚合功能下

    介绍 不好意思这篇文章应该早点更新的,这几天在忙CICD的东西没顾得上,等后面整好了CICD我也发2篇文章讲讲,咱们进入正题,这一章来补全剩下的 2个接口和将文章聚合进行完善. 开工 上一章大部分业务 ...

  2. 八、Abp vNext 基础篇丨标签聚合功能

    介绍 本章节先来把上一章漏掉的上传文件处理下,然后实现Tag功能. 上传文件 上传文件其实不含在任何一个聚合中,它属于一个独立的辅助性功能,先把抽象接口定义一下,在Bcvp.Blog.Core.App ...

  3. 九、Abp vNext 基础篇丨评论聚合功能

    介绍 评论本来是要放到标签里面去讲的,但是因为上一章东西有点多了,我就没放进去,这一章单独拿出来,内容不多大家自己写写就可以,也算是对前面讲解的一个小练习吧. 相关注释我也加在代码上面了,大家看看代码 ...

  4. 十一、Abp vNext 基础篇丨测试

    前言 祝大家国庆快乐,本来想国庆之前更新完的,结果没写完,今天把剩下的代码补了一下总算ok了. 本章节也是我们后端日常开发中最重要的一步就是测试,我们经常听到的单元测试.集成测试.UI测试.系统测试, ...

  5. 五、Abp vNext 基础篇丨博客聚合功能

    介绍 业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动using不过问题不大. 开工 应用层 根据第三章分层架构里面讲到的现在我们模型 ...

  6. Abp vNext 基础篇丨分层架构

    介绍 本章节对 ABP 框架进行一个简单的介绍,摘自ABP官方,后面会在使用过程中对各个知识点进行细致的讲解. 领域驱动设计 领域驱动设计(简称:DDD)是一种针对复杂需求的软件开发方法.将软件实现与 ...

  7. Abp vNext 基础篇丨领域构建

    介绍 我们将通过例⼦介绍和解释⼀些显式规则.在实现领域驱动设计时,应该遵循这些规则并将其应⽤到解决⽅案中. 领域划分 首先我们先对比下Blog.Core和本次重构设计上的偏差,可以看到多了一个博客管理 ...

  8. 十、Abp vNext 基础篇丨权限

    介绍 本章节来把接口的权限加一下 权限配置和使用 官方地址:https://docs.abp.io/en/abp/latest/Authorization 下面这种代码可能我们日常开发都写过,ASP. ...

  9. [Abp vNext 源码分析] - 文章目录

    一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...

随机推荐

  1. POJ3179 Corral the Cows题解

    我就是个垃圾--一道水题能写这么长时间-- 首先看到题就想到了二维前缀和+二分边长,但地图边长10000,得离散化. 于是这个离散化就把我搞疯了,淦. 这反映出现在基础知识还是不牢固,相当不牢固. 复 ...

  2. odoo源码学习之任务中的阶段字段stage_id

    # 案例0004针对form表单 class Task(models.Model): _name = "project.task" _description = "对于项 ...

  3. 第十一篇 -- 如何实现MFC窗口的最大化以及控件随最大化

    这一篇介绍的是怎么实现MFC窗口的最大最小化,以及里面控件大小也随之改变 第一步:实现窗口最大最小化 首先右击窗口空白处,打开properties,将里面的MaximizeBox和MinimizeBo ...

  4. 货币兑换问题(动态规划法)——Python实现

      # 动态规划法求解货币兑换问题 # 货币系统有 n 种硬币,面值为 v1,v2,v3...vn,其中 v1=1,使用总值为money的钱与之兑换,求如何使硬币的数目最少,即 x1,x2,x3... ...

  5. 获取不到自定义的request的header属性

    java获取headers的代码如下: // 获取http-header里面对应的签名信息 Enumeration<?> headerNames = request.getHeaderNa ...

  6. Spring Data Commons 远程命令执行漏洞(CVE-2018-1273)

    影响版本 Spring Framework 5.0 to 5.0.4 Spring Framework 4.3 to 4.3.14 poc https://github.com/zhzyker/exp ...

  7. 使用ffmpeg给视频添加跑马灯效果(滚动字幕)

    直接上命令 从左往右滚 ffmpeg -i input.mp4 -vf "drawtext=text=string1 string2 string3 string4 string5 stri ...

  8. JMeter(1)-介绍+环境+安装+使用

    一.开发接口测试案例的整体方案: 分析出测试需求,并拿到开发提供的接口说明文档: 从接口说明文档中整理出接口测试案例(包括详细的入参和出参数据以及明确的格式和检查点). 和开发一起对评审接口测试案例 ...

  9. Java on Visual Studio Code的更新 – 2021年7月

    Nick zhu, Senior Program Manager, Developer Division at Microsoft 大家好,欢迎来到 7 月版的 Visual Studio Code ...

  10. 心酸!30岁深漂失业3个月,从巅峰跌落谷底,大龄Android开发必须要懂的事!

    2021年3月,我的前同事,在我们群里说他准备回老家了,问我们有没有人可以暂时收养他的猫. --他说,这周末就要离开深圳了. 他失业了.3个多月没收入,还要交着房租,过年来之后找了快一个月的工作也没有 ...