ABP-Book Store Application中文讲解 - Part 10: Book to Author Relation
1. 汇总
ABP-Book Store Application中文讲解-汇总-CSDN博客
2. 前一章
ABP-Book Store Application中文讲解 - Part 9: Authors: User Interface-CSDN博客
项目之间的引用关系。
目录
3.2 利用VS的Package Manager Console (PMC)
7. Unit Tests-BookAppService_Tests.cs
1. 添加Author到Book的映射
在Acme.BookStore.Domain中打开Books/Book.cs,添加如下代码:
public Guid AuthorId { get; set; }
ABP遵守DDD(Domain Driven Design)的设计模式,因此我们不用在Book.cs中添加
public Author Author { get; set; }
2. 在OnModelCreating中设置外键约束
因为我们没有添加引用对象Author , 我们需要在OnModelCreating里面指定AuthorId作为外键,代码如下:
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
// 添加外键
b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
});
3. 新建DB Migration
按照之前介绍的,此处有两种方式,可以根据自己的需要选择任意一种即可。
1. 利用dotnet ef migrations。 需要全局安装ef
dotnet tool install --global dotnet-ef
2. 利用VS的Package Manager console
3.1 利用dotnet ef migrations
右击Acme.BookStore.EntityFrameworkCore,选择Open in Terminal,然后输入以下命令后敲回车:
dotnet ef migrations add Added_AuthorId_To_Book
3.2 利用VS的Package Manager Console (PMC)
Tools -> NuGet Package Manager --> Package Manager Console打开PMC, 输入以下命令后敲回车:
Add-Migration Added_AuthorId_To_Book
Tips: 输入add-migr直接敲Tab会自动补全Add-Migration 命令
4. 更改Seeder
展开Acme.BookStore.Domain,找到Data目录下的BookStoreDataSeederContributor.cs,打开后做如下更改:
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() > 0)
{
return;
}
// 添加Author初始化数据
var author = await _authorManager.CreateAsync("刘慈欣", new DateTime(1963, 6, 1), "刘慈欣被誉为中国科幻的领军人物,他的作品“三体三部曲”是中国科幻文学的里程碑之作,将中国科幻推上了世界的高度。");
await _authorRepository.InsertAsync(author);
var authorLxs = await _authorManager.CreateAsync("梁晓声", new DateTime(1949, 9, 22), "梁晓声,原名梁绍生,中国当代著名作家,中国作家协会会员,1949年9月22日出生于黑龙江省哈尔滨市,毕业于复旦大学 [3] [50],祖籍山东威海市泊于镇温泉寨。他曾创作出版过大量有影响的小说、散文、随笔及影视作品,为中国现当代以知青文学成名的代表作家之一。现居北京,任教于北京语言大学人文学院汉语言文学专业。");
await _authorRepository.InsertAsync(authorLxs);
// 添加Book初始化数据
await _bookRepository.InsertAsync(
new Book()
{
Name = "三体",
Type = BookType.Fantastic,
PublishDate = new DateTime(2006, 5, 20),
Price = 100,
AuthorId = author.Id
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book()
{
Name = "三体2:黑暗森林",
Type = BookType.Fantastic,
PublishDate = new DateTime(2008, 5, 21),
Price = 100,
AuthorId = author.Id
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book()
{
Name = "三体3:死神永生",
Type = BookType.Fantastic,
PublishDate = new DateTime(2010, 11, 11),
Price = 100,
AuthorId = author.Id
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book()
{
Name = "人世间",
Type = BookType.Dystopia,
PublishDate = new DateTime(2017, 12, 11),
Price = 100,
AuthorId = authorLxs.Id
},
autoSave: true
);
}
5. DB Update
删除原来的数据或者直接删除原来的数据库,然后重新运行Acme.BookStore.DbMigrator去重新更新数据或者重新创建数据库。
6. 更改BookAppService
6.1 更改Dtos
Acme.BookStore.Application.Contracts项目的Books目录中:
BookDto,新增如下两个fields
public Guid AuthorId { get; set; }
public string AuthorName { get; set; } = string.Empty;
CreateUpdateBookDto, 新增一个字段
public Guid AuthorId { get; set; }
在Books目录中新增一个新类AuthorLookupDto.cs
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books.Dtos
{
public class AuthorLookupDto : EntityDto<Guid>
{
public string Name { get; set; } = string.Empty;
}
}
6.2 IBookAppService新增方法
在Acme.BookStore.Application.Contracts项目的Books目录中IBookAppService.cs中
新增GetAuthorLookupAsync
/// <summary>
/// 获取Authors的Lookup列表,在新增或者编辑Book时使用
/// </summary>
/// <returns></returns>
Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
6.3 BookAppService新增和更改方法
首先需要打开Acme.BookStore.Application中的BookStoreApplicationAutoMapperProfile.cs添加Author到AuthorLookupDto的映射。
CreateMap<Author, AuthorLookupDto>();
然后开始打开Acme.BookStore.Application中的Books目录下的BookAppService.cs,进行如下方法的更改:
因为需要override现有的方法和新增新的方法,因此原来的权限验证会失效,需要主动添加Attribute ([Authorize(BookStorePermissions.Books.Default)])在BookAppService中。
6.3 1. 构造函数中引入IAuthorRepository authorRepository
private readonly IAuthorRepository _authorRepository;
public BookAppService(IRepository<Book, Guid> repository, IAuthorRepository authorRepository) : base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;// Default
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
_authorRepository = authorRepository;
}
6.3 2. 更改GetAsync
此处有两种方式:因为只有一条记录,并且都是根据主键进行查询,所以两种方式都行。
1. 分开查询,这个方式会访问数据库两次,但是ABP默认启动的UnitOfWork,即一个function或者API用的同一个transaction
public override async Task<BookDto> GetAsync(Guid id)
{
// Get the IQueryable<Book> from the repository
var book = await base.Repository.GetAsync(id);
var author = await _authorRepository.GetAsync(book.AuthorId);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
bookDto.AuthorName = author.Name;
return bookDto;
}
2. 利用Linq的join查询,只会访问数据库一次,即 query.FirstOrDefault()才会访问数据库
public override async Task<BookDto> GetAsync(Guid id)
{
// Get the IQueryable<Book> from the repository
var books = await Repository.GetQueryableAsync();
var authors = await _authorRepository.GetQueryableAsync();
// Ensure LINQ methods like Join are available by converting to Enumerable
var query = from book in books.AsEnumerable()
join author in authors.AsEnumerable() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
// Execute the query and get the book with author
var queryResult = query.FirstOrDefault();
if (queryResult == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
bookDto.AuthorName = queryResult.author.Name;
return bookDto;
}
6.3 3. 更改GetListAsync
不确定为啥ABP官网的代码无法在我本地编译,因此做了如下更改。
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
select new { book, author };
//Paging
var sorting = NormalizeSorting(input.Sorting);
query = query
.OrderBy(x => sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Execute the query and get a list
var queryResult = await AsyncExecuter.ToListAsync(query);
//Convert the query result to a list of BookDto objects
var bookDtos = queryResult.Select(x =>
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Get the total count with another query
var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>(
totalCount,
bookDtos
);
}
private string NormalizeSorting(string sorting)
{
if (sorting.IsNullOrEmpty())
{
return $"book.{nameof(Book.Name)}";
}
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{
return sorting.Replace(
"authorName",
"author.Name",
StringComparison.OrdinalIgnoreCase
);
}
return $"book.{sorting}";
}
6.3 4. 新增方法GetAuthorLookupAsync
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{
var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
);
}
完整代码:
我更改后的版本:
using Acme.BookStore.Authors;
using Acme.BookStore.Books.Dtos;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.ObjectMapping;
namespace Acme.BookStore.Books
{
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books in UI
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
private readonly IAuthorRepository _authorRepository;
public BookAppService(IRepository<Book, Guid> repository, IAuthorRepository authorRepository) : base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;// Default
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
_authorRepository = authorRepository;
}
public override async Task<BookDto> GetAsync(Guid id)
{
// Get the IQueryable<Book> from the repository
var books = await Repository.GetQueryableAsync();
var authors = await _authorRepository.GetQueryableAsync();
// Ensure LINQ methods like Join are available by converting to Enumerable
var query = from book in books.AsEnumerable()
join author in authors.AsEnumerable() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
// Execute the query and get the book with author
var queryResult = query.FirstOrDefault();
if (queryResult == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
bookDto.AuthorName = queryResult.author.Name;
return bookDto;
}
//public override async Task<BookDto> GetAsync(Guid id)
//{
// // Get the IQueryable<Book> from the repository
// var book = await base.Repository.GetAsync(id);
// var author = await _authorRepository.GetAsync(book.AuthorId);
// var bookDto = ObjectMapper.Map<Book, BookDto>(book);
// bookDto.AuthorName = author.Name;
// return bookDto;
//}
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{
var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
);
}
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
select new { book, author };
//Paging
var sorting = NormalizeSorting(input.Sorting);
query = query
.OrderBy(x => sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Execute the query and get a list
var queryResult = await AsyncExecuter.ToListAsync(query);
//Convert the query result to a list of BookDto objects
var bookDtos = queryResult.Select(x =>
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Get the total count with another query
var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>(
totalCount,
bookDtos
);
}
private string NormalizeSorting(string sorting)
{
if (sorting.IsNullOrEmpty())
{
return $"book.{nameof(Book.Name)}";
}
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{
return sorting.Replace(
"authorName",
"author.Name",
StringComparison.OrdinalIgnoreCase
);
}
return $"book.{sorting}";
}
}
}
ABP官网的版本:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books;
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
private readonly IAuthorRepository _authorRepository;
public BookAppService(
IRepository<Book, Guid> repository,
IAuthorRepository authorRepository)
: base(repository)
{
_authorRepository = authorRepository;
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
//Execute the query and get the book with author
var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
if (queryResult == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
bookDto.AuthorName = queryResult.author.Name;
return bookDto;
}
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
select new {book, author};
//Paging
query = query
.OrderBy(NormalizeSorting(input.Sorting))
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Execute the query and get a list
var queryResult = await AsyncExecuter.ToListAsync(query);
//Convert the query result to a list of BookDto objects
var bookDtos = queryResult.Select(x =>
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Get the total count with another query
var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>(
totalCount,
bookDtos
);
}
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{
var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
);
}
private static string NormalizeSorting(string sorting)
{
if (sorting.IsNullOrEmpty())
{
return $"book.{nameof(Book.Name)}";
}
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{
return sorting.Replace(
"authorName",
"author.Name",
StringComparison.OrdinalIgnoreCase
);
}
return $"book.{sorting}";
}
}
7. Unit Tests-BookAppService_Tests.cs
在Acme.BookStore.Application.Tests项目的Books目录中找到BookAppService_Tests.cs,做如下更改:
using Acme.BookStore.Authors.Dtos;
using Acme.BookStore.Books.Dtos;
using Shouldly;
using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Modularity;
using Volo.Abp.Validation;
using Xunit;
namespace Acme.BookStore.Books
{
public abstract class BookAppService_Tests<TStartupModule> : BookStoreApplicationTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IBookAppService _bookAppService;
protected BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "三体" && b.AuthorName == "刘慈欣");
}
[Fact]
public async Task Should_Create_A_Valid_Book()
{
var authors = await _bookAppService.GetAuthorLookupAsync();
var firstAuthor = authors.Items.First();
var newBookName = "Evan test new book";// 按照需要更改成你自己的书名
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = newBookName,
Price = 100,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction,
AuthorId = firstAuthor.Id // 使用第一个作者的ID
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe(newBookName);
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
}
}
8. UI端的更改
8.1 启动API,生成proxy
在angular根目录,通过ctrl+`打开Terminal console,然后输入以下命令,敲回车去更新proxy。
abp generate-proxy -t ng
8.2 Book List更新
如果你是单独生成的add-edit-book,那么此处只需要在book.component.html页面增加一行代码即可。
<!-- add the Author column-->
<ngx-datatable-column name="{{'::Author' | abpLocalization}}" prop="authorName"></ngx-datatable-column>
8.3 新增、编辑页面更新
add-edit-book.component.html增加以下代码
<div class="formGroup mt-2">
<label for="book-author">{{'::Author' | abpLocalization}}</label><span> * </span>
<select class="form-control" id="book-author" formControlName="authorId">
<option [ngValue]="null">{{'::SelectAuthor' | abpLocalization}}</option>
<option [ngValue]="author.id" *ngFor="let author of authors"> {{ author.name }}</option>
</select>
</div>
add-edit-book.component.ts中需要更改以下几处代码:
导入AuthorLookupDto
注意此处如果重新生成proxy导致BookDto报错的话,需要将路径更改为@proxy/books/dtos即可。
import { BookDto, AuthorLookupDto } from '@proxy/books/dtos';
定义authors变量
authors: AuthorLookupDto[] = [];
ngOnInit增加加载Authors的方法
ngOnInit(): void {
this.bookService.getAuthorLookup().subscribe((result) => {
this.authors = result.items;
this.buildForm();
});
}
buildForm增加authorId
buildForm() {
this.form = this.fb.group({
name: [this.book.name, Validators.required],
type: [this.book.type, Validators.required],
publishDate: [this.book.publishDate ? new Date(this.book.publishDate) : null, Validators.required],
price: [this.book.price, Validators.required],
authorId: [this.book.authorId, Validators.required],
});
}
9. 补充下本地化资源
en.json
{
"Culture": "en",
"Texts": {
"Menu:Home": "Home",
"Welcome": "Welcome",
"LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP. For more information, visit abp.io.",
// 以下内容是新增内容
"Menu:BookStore": "Book Store",
"Menu:Books": "Books",
"Actions": "Actions",
"Close": "Close",
"Delete": "Delete",
"Edit": "Edit",
"Save": "Save",
"Cancel": "Cancel",
"Update": "Update",
"PublishDate": "Publish date",
"NewBook": "New book",
"EditBook": "Edit book",
"Name": "Name",
"Type": "Type",
"Price": "Price",
"CreationTime": "Creation time",
"Author": "Author",
"SelectAuthor": "Select an author",
"SelectType": "Select a book type",
"AreYouSure": "Are you sure?",
"AreYouSureToDelete": "Are you sure you want to delete this item?",
"Enum:BookType.0": "Undefined",
"Enum:BookType.1": "Adventure",
"Enum:BookType.2": "Biography",
"Enum:BookType.3": "Dystopia",
"Enum:BookType.4": "Fantastic",
"Enum:BookType.5": "Horror",
"Enum:BookType.6": "Science",
"Enum:BookType.7": "Science fiction",
"Enum:BookType.8": "Poetry",
"Permission:BookStore": "Book Store",
"Permission:Books": "Book Management",
"Permission:Books.Create": "Creating new books",
"Permission:Books.Edit": "Editing the books",
"Permission:Books.Delete": "Deleting the books",
"BookStore:00001": "There is already an author with the same name: {name}",
"Permission:Authors": "Author Management",
"Permission:Authors.Create": "Creating new authors",
"Permission:Authors.Edit": "Editing the authors",
"Permission:Authors.Delete": "Deleting the authors",
"Menu:Authors": "Authors",
"Authors": "Authors",
"AuthorDeletionConfirmationMessage": "Are you sure you want to delete this item?",
"BirthDate": "Birth date",
"ShortBio": "Short Bio",
"NewAuthor": "New author",
"EditAuthor": "Edit author"
}
}
zh-Hans.json
{
"culture": "zh-Hans",
"texts": {
"AppName": "BookStore",
"Menu:Home": "首页",
"Welcome": "欢迎",
"LongWelcomeMessage": "欢迎使用本应用程序。这是一个基于 ABP 框架的启动项目。更多信息,请访问 abp.io。",
// 以下内容是新增内容
"Menu:BookStore": "书店",
"Menu:Books": "书籍管理",
"Actions": "操作",
"Close": "关闭",
"Delete": "删除",
"Edit": "编辑",
"Save": "保存",
"Cancel": "取消",
"Update": "更新",
"PublishDate": "发布日期",
"NewBook": "新增书",
"EditBook": "编辑书",
"Name": "名字",
"Type": "类型",
"Price": "价格",
"CreationTime": "新建日期",
"Author": "作者",
"SelectAuthor": "选择作者",
"SelectType": "选择类型",
"AreYouSure": "你确定吗?",
"AreYouSureToDelete": "你确定你要删除此条目吗?",
"Enum:BookType.0": "未定义",
"Enum:BookType.1": "冒险",
"Enum:BookType.2": "传记",
"Enum:BookType.3": "反乌托邦",
"Enum:BookType.4": "奇幻",
"Enum:BookType.5": "恐怖",
"Enum:BookType.6": "科学",
"Enum:BookType.7": "科幻",
"Enum:BookType.8": "诗歌",
"Permission:BookStore": "书店",
"Permission:Books": "书籍管理",
"Permission:Books.Create": "新建书籍",
"Permission:Books.Edit": "编辑书籍",
"Permission:Books.Delete": "删除书籍",
"BookStore:00001": "已经存在相同名字的作者: {name}",
"Permission:Authors": "作者管理",
"Permission:Authors.Create": "新建作者",
"Permission:Authors.Edit": "编辑作者",
"Permission:Authors.Delete": "删除作者",
"Menu:Authors": "作者管理",
"Authors": "作者",
"AuthorDeletionConfirmationMessage": "你确定要删除此条目吗?",
"BirthDate": "生日",
"ShortBio": "简介",
"NewAuthor": "新建作者",
"EditAuthor": "编辑作者"
}
}
10. 感谢
历时一个多月,总算完成本系列的最后一篇了,感谢各位的收藏、点赞支持。
后序我会开始完成ABP User Interface-Angular UI中文详解-CSDN博客系列的中文版本以及ABP核心模块的中文版本,感兴趣的朋友可以先收藏下。
谢谢。
Clean版本的代码:acme-book-store-default
如果需要完整版的可以私聊我微信:18901599114