zoukankan      html  css  js  c++  java
  • 尝试从零开始构建我的商城 (一) :使用Abp vNext快速一个简单的商城项目

    尝试从零开始构建我的商城 (一) :使用Abp vNext快速搭建一个简单的项目

    前言

    GitHub地址

    https://github.com/yingpanwang/MyShop

    此文目的

    本文将尝试使用Abp vNext 搭建一个商城系统并尽可能从业务,架构相关方向优化升级项目结构,并会将每次升级所涉及的模块及特性以blog的方式展示出来。

    代码实践

    Abp vNext 使用 DDD 领域驱动设计 是非常方便的,但是由于本人认为自身没有足够的功力玩转DDD,所以开发中使用的是基于贫血模型的设计的开发,而不是遵照DDD的方式

    Domain 领域层

    1.添加引用

    通过 Nuget 安装 Volo.Abp.Ddd.Domain

    2.定义模块

    创建 MyShopDomainModule.cs 作为Domain的模块,Domain不依赖任何外部模块,本体也没有什么相关配置,所以只需要继承AbpModule即可

    namespace MyShop.Domain
    {
        public class MyShopDomainModule:AbpModule
        {
        }
    }
    

    3.定义实体

    BaseEntity 实体基类

    定义BaseEntity并继承由Volo.Abp.Ddd.Domain提供的Entity并添加CreationTime属性

        public class BaseEntity :Entity<long>
        {
            public DateTime CreationTime { get; set; } = DateTime.Now;
        }
    
    Product 商品

    继承BaseEntity类 添加相关属性

       /// <summary>
        /// 商品信息
        /// </summary>
        public class Product :BaseEntity
        {
            /// <summary>
            /// 名称
            /// </summary>
            public string Name { get; set; }
    
            /// <summary>
            /// 分类id
            /// </summary>
            public long CategoryId { get; set; }
    
            /// <summary>
            /// 价格
            /// </summary>
            public decimal? Price { get; set; }
            
            /// <summary>
            /// 描述
            /// </summary>
            public string Description { get; set; }
    
            /// <summary>
            /// 库存
            /// </summary>
            public decimal Stock { get; set; }
        }
    
    Order 订单

    继承BaseEntity类 添加相关属性

        /// <summary>
        /// 订单
        /// </summary>
        public class Order:BaseEntity
        {
    
            /// <summary>
            /// 订单号
            /// </summary>
            public Guid OrderNo { get; set; } = Guid.NewGuid();
            
            /// <summary>
            /// 用户
            /// </summary>
            public long UserId { get; set; }
    
            /// <summary>
            /// 订单状态
            /// </summary>
            public OrderStatus OrderStatus { get; set; }
    
            /// <summary>
            /// 总金额
            /// </summary>
            public decimal Total { get; set; }
    
            /// <summary>
            /// 地址
            /// </summary>
            public string Address { get; set; }
    
        }
    
        public enum OrderStatus 
        {
            Created,
            Cancel,
            Paid,
        }
    

    Application 应用层实现

    1.添加引用

    通过 Nuget 安装 Volo.Abp.Application
    通过 Nuget 安装 Volo.Abp.AutoMapper

    2.定义模块

    创建 MyShopApplicationModule.cs ,继承自AbpModule,并且依赖Domain以及ApplicationContract层和后续使用的AutoMapper,所以对应DependsOn中需要添加对应依赖

        /// <summary>
        /// 项目模块依赖
        /// </summary>
        [DependsOn(typeof(MyShopApplicationContractModule),
            typeof(MyShopDomainModule))]
    
        /// 组件依赖
        [DependsOn(typeof(AbpAutoMapperModule))]
        public class MyShopApplicationModule:AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
    
                //添加ObjectMapper注入
                context.Services.AddAutoMapperObjectMapper<MyShopApplicationModule>();
    
                // Abp AutoMapper设置
                Configure<AbpAutoMapperOptions>(config => 
                {
                    // 添加对应依赖关系Profile
                    config.AddMaps<MyShopApplicationProfile>();
                });
            }
        }
    

    3.实现相关服务

    这里由于我们使用AutoMapper所以需要创建Profile并添加相关映射关系

    Profile定义
    
    namespace MyShop.Application.AutoMapper.Profiles
    {
        public class MyShopApplicationProfile:Profile
        {
            public MyShopApplicationProfile() 
            {
                CreateMap<Product, ProductItemDto>().ReverseMap();
    
                CreateMap<Order, OrderInfoDto>().ReverseMap();
            }
        }
    }
    
    
    服务实现

    Abp 可以很方便的将我们的服务向外暴露,通过简单配置可以自动生成对应接口,只需要需要暴露的服务
    继承ApplicationService。

    Abp在确定服务方法的HTTP Method时使用命名约定:
    Get: 如果方法名称以GetList,GetAllGet开头.
    Put: 如果方法名称以PutUpdate开头.
    Delete: 如果方法名称以DeleteRemove开头.
    Post: 如果方法名称以Create,Add,InsertPost开头.
    Patch: 如果方法名称以Patch开头.
    其他情况, Post 为 默认方式.

    OrderApplicationService
    public class OrderApplicationService : ApplicationService, IOrderApplicationService
        {
    
            private readonly IRepository<Order> _orderRepository;
    
            public OrderApplicationService(IRepository<Order> orderRepository) 
            {
                _orderRepository = orderRepository;
            }
    
            public async Task<OrderInfoDto> GetAsync(long id)
            {
                var order = await _orderRepository.GetAsync(g => g.Id == id);
    
                return ObjectMapper.Map<Order, OrderInfoDto>(order);
            }
    
            public async Task<List<OrderInfoDto>> GetListAsync()
            {
                var orders = await _orderRepository.GetListAsync();
    
                return ObjectMapper.Map<List<Order>, List<OrderInfoDto>>(orders);
            }
        }
    
    ProductApplicationService
     /// <summary>
        /// 商品服务
        /// </summary>
        public class ProductApplicationService : ApplicationService, IProductApplicationService
        {
            /// <summary>
            /// 自定义商品仓储
            /// </summary>
            private readonly IProductRepository _productRepository;
    
            /// <summary>
            /// 商品类别仓储
            /// </summary>
            private readonly IRepository<Category> _categoryRepository;
    
            /// <summary>
            /// 构造
            /// </summary>
            /// <param name="productRepository">自定义商品仓储</param>
            /// <param name="categoryRepository">商品类别仓储</param>
            public ProductApplicationService(IProductRepository productRepository,
                IRepository<Category> categoryRepository)
            {
                _productRepository = productRepository;
                _categoryRepository = categoryRepository;
            }
    
            /// <summary>
            /// 获取商品信息
            /// </summary>
            /// <param name="id">商品id</param>
            /// <returns></returns>
            public async Task<ProductItemDto> GetAsync(long id)
            {
                return await Task.FromResult(Query(p => p.Id == id).FirstOrDefault(p =>p.Id == id));
            }
    
            /// <summary>
            /// 获取分页商品列表
            /// </summary>
            /// <param name="page">分页信息</param>
            /// <returns></returns>
            public IPagedResult<ProductItemDto> GetPage(BasePageInput page)
            {
                var query = Query()
                    .WhereIf(!string.IsNullOrEmpty(page.Keyword), w => w.Name.StartsWith(page.Keyword));
                   
                var count =  query.Count();
    
                var products = query.PageBy(page).ToList();
    
                return new PagedResultDto<ProductItemDto>(count,products);
            }
    
            /// <summary>
            /// 获取商品列表
            /// </summary>
            /// <returns></returns>
            public async Task<List<ProductItemDto>> GetListAsync()
            {
                var products = await _productRepository.GetListAsync();
                return ObjectMapper.Map<List<Product>, List<ProductItemDto>>(products);
            }
    
    
            private IQueryable<ProductItemDto> Query(Expression<Func<Product,bool>> expression = null) 
            {
                var products = _productRepository.WhereIf(expression != null, expression);
    
                var categories = _categoryRepository;
    
                var data = from product in products
                           join category in categories on product.CategoryId equals category.Id into temp
                           from result in temp.DefaultIfEmpty()
                           select new ProductItemDto
                           {
                               Id = product.Id,
                               Description = product.Description,
                               Name = product.Name,
                               Price = product.Price,
                               Stock = product.Stock,
                               CategoryName = result.CategoryName,
                           };
    
                return data;
            }
        }
    

    Application.Contract 应用层抽象

    1.添加引用

    通过 Nuget 安装 Volo.Abp.Application.Contract

    2.定义模块

    创建 MyShopApplicationContractModule.cs ,继承自AbpModule,并且没有对外依赖

       public class MyShopApplicationContractModule:AbpModule
        {
    
        }
    

    3.定义Contract

    IOrderApplicationService
        public interface IOrderApplicationService:IApplicationService
        {
            Task<OrderInfoDto> GetAsync(long id);
            Task<List<OrderInfoDto>> GetListAsync();
        }
    
    IProductApplicationService
        public interface IProductApplicationService :IApplicationService
        {
            Task<List<ProductItemDto>> GetListAsync();
            IPagedResult<ProductItemDto> GetPage(BasePageInput page);
            Task<ProductItemDto> GetAsync(long id);
        }
    

    Admin.Application 管理后台应用层

    这里和Application层差不多,但是为了方便编写代码所以Admin.Contract和Admin.Application 放在了一个项目中,这里我定义了IBaseAdminCRUDApplicationService接口并且实现了一个BaseAdminCRUDApplicationService实现了一些简单的CRUD的方法暂时作为后台数据管理的方法,并且为了区分后台管理接口和平台接口在自动注册为api时加上admin前缀

    
                service.Configure((AbpAspNetCoreMvcOptions options) =>
                {
                    // 平台接口和后台管理接口暂时放在一个站点下
                    options.ConventionalControllers.Create(typeof(Application.ProductApplicationService).Assembly);
                    options.ConventionalControllers.Create(typeof(Application.OrderApplicationService).Assembly);
    
                    // 区分接口 请求路径加上admin
                    options.ConventionalControllers.Create(typeof(Admin.Application.Services.ProductApplicationService).Assembly, options =>
                     {
                         options.RootPath = "admin";
                     });
                });
    
    

    1.添加引用

    通过 Nuget 安装 Volo.Abp.Application
    通过 Nuget 安装 Volo.Abp.AutoMapper

    2.定义模块

    创建 MyShopAdminApplicationModule.cs ,继承自AbpModule,并且依赖Domain以及ApplicationContract层和后续使用的AutoMapper,所以对应DependsOn中需要添加对应依赖

        [DependsOn(typeof(MyShopDomainModule))]
        [DependsOn(typeof(AbpAutoMapperModule))]
        public class MyShopAdminApplicationModule:AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddAutoMapperObjectMapper<MyShopAdminApplicationModule>();
    
                Configure<AbpAutoMapperOptions>(config =>
                {
                    config.AddMaps<MyShopAdminApplicationProfile>();
                });
            }
        }
    

    EntityFreaworkCore数据访问层中实现Repository

    通过Nuget添加 Volo.Abp.EntityFrameworkCore.MySQL

    定义DbContext
    
        [ConnectionStringName("Default")]
        public class MyShopDbContext:AbpDbContext<MyShopDbContext>
        {
            public MyShopDbContext(DbContextOptions<MyShopDbContext> options) : base(options) 
            {
                
            }
    
            protected override void OnModelCreating(ModelBuilder builder)
            {
                builder.ConfigureProductStore();
                builder.ConfigureOrderStore();
                builder.ConfigureOrderItemStore();
                builder.ConfigureCategoryStore();
            }
    
            public DbSet<Product> Products { get; set; }
            public DbSet<Order> Orders { get; set; }
            public DbSet<OrderItem> OrderItems { get; set; }
            public DbSet<Category> Categories { get; set; }
        }
    
    

    这里通过扩展方法分开了实体的一些定义

    ConfigureProductStore
        public static class ProductCreatingExtension
        {
            public static void ConfigureProductStore(this ModelBuilder builder)
            {
                Check.NotNull(builder, nameof(builder));
                builder.Entity<Product>(option =>
                {
                    option.ToTable("Product");
                    option.ConfigureByConvention();
                });
            }
        }
    
    ConfigureOrderStore
        public static class OrderCreatingExtension
        {
            public static void ConfigureOrderStore(this ModelBuilder builder)
            {
                Check.NotNull(builder, nameof(builder));
                builder.Entity<Order>(option =>
                {
                    option.ToTable("Order");
                    option.ConfigureByConvention();
                });
            }
        }
    
    ConfigureOrderItemStore
        public static class OrderItemsCreatingExtension
        {
            public static void ConfigureOrderItemStore(this ModelBuilder builder)
            {
                Check.NotNull(builder, nameof(builder));
                builder.Entity<OrderItem>(option =>
                {
                    option.ToTable("OrderItem");
                    option.ConfigureByConvention();
                });
            }
        }
    
    ConfigureCategoryStore
        public static class CategoryCreatingExtension
        {
    
            public static void ConfigureCategoryStore(this ModelBuilder builder) 
            {
                Check.NotNull(builder, nameof(builder));
                builder.Entity<Category>(option =>
                {
                    option.ToTable("Category");
                    option.ConfigureByConvention();
                });
            }
        }
    

    由于目前Abp vNext提供的 IRepository<TEntity,TKey> 接口已经满足我们当前的使用需要,也提供了IQuaryable 接口,所以灵活组装的能力也有,所以不另外实现自定义仓储,不过这里还是预先准备一个自定义的ProductRepsitory

    在Domain层中定义IProductRepository
    
        public interface IProductRepository : IRepository<Product, long>
        {
            IQueryable<Product> GetProducts(long id);
        }
    
    
    EntityFrameworkCore数据访问层中实现IProductRepository

    继承EfCoreRepository以方便实现 IRepository接口中提供的基本方法和 DbContext的访问,并实现IProductRepository

    
       public class ProductRepository : EfCoreRepository<MyShopDbContext, Product, long>, IProductRepository
       {
           public ProductRepository(IDbContextProvider<MyShopDbContext> dbContextProvider) : base(dbContextProvider)
           {
           }
    
           public IQueryable<Product> GetProducts(long id) 
           {
               return DbContext.Products.Where(p => p.Id == id);
           }
       }
    
    

    定义模块,注入默认仓储

    
        [DependsOn(typeof(AbpEntityFrameworkCoreMySQLModule))]
        public class MyShopEntityFrameworkCoreModule :AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddAbpDbContext<MyShopDbContext>(option=> 
                {
    
                    // 如果只是用了 entity, 请将 includeAllEntities 设置为 true 否则会提示异常,导致所属仓储无法注入
                    option.AddDefaultRepositories(true);
                });
    
                Configure<AbpDbContextOptions>(option=> 
                {
                    option.UseMySQL();
                });
                
            }
        }
    
    

    Migration 迁移

    1.添加引用

    通过Nuget安装 Microsoft.EntityFrameworkCore.Tools

    添加Domain,EntiyFrameworkCore数据访问层的引用
     <ItemGroup>
        <ProjectReference Include="..MyShop.EntityFrameworkCoreMyShop.EntityFrameworkCore.csproj" />
        <ProjectReference Include="..MyShop.EntityMyShop.Domain.csproj" />
      </ItemGroup>
    

    2.定义DbContext

    这里我们使用了之前在EntifyFrameworkCore中定义的一些实体配置

    
        [ConnectionStringName("Default")]
        public class DbMigrationsContext : AbpDbContext<DbMigrationsContext>
        {
            public DbMigrationsContext(DbContextOptions<DbMigrationsContext> options) : base(options)
            {
            }
    
            protected override void OnModelCreating(ModelBuilder builder)
            {
                base.OnModelCreating(builder);
                builder.ConfigureProductStore();
                builder.ConfigureOrderStore();
                builder.ConfigureOrderItemStore();
                builder.ConfigureCategoryStore();
            }
    
        }
    
    

    3.定义模块

    
        [DependsOn(typeof(MyShopEntityFrameworkCoreModule))]
        public class MyShopEntityFrameworkCoreMigrationModule:AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                context.Services.AddAbpDbContext<DbMigrationsContext>();
            }
        }
    
    

    4.执行迁移

    打开程序包管理器控制台,由于我们的连接字符串定义在Api层,我已我们需要将Api设置为启动项目,并将程序包管理器控制台默认程序切换为我们的迁移层Migration并执行一下指令

    1.添加迁移文件

    Add-Migration "Init"

    执行完后我们会在Migrations文件夹目录下得到相关的迁移文件

    2.执行迁移

    Update-Database

    执行完成后就可以打开数据库查看我们生成的表了

    Api 接口层

    1.建立项目

    接下来建立AspNetCore WebAPI 项目 并命名为MyShop.Api作为我们对外暴露的API,并添加Abp Vnext AspNetCore.MVC包

    通过Nuget 安装 Volo.Abp.AspNetCore.Mvc

    StartUp
    
    namespace MyShop.Api
    {
        public class Startup
        {
            // This method gets called by the runtime. Use this method to add services to the container.
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
            public void ConfigureServices(IServiceCollection services)
            {
                // 注册模块
                services.AddApplication<MyShopApiModule>();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                // 初始化
                app.InitializeApplication();
            }
        }
    }
    
    
    Program

    由于我们使用了 Abp vNext 中的Autofac 所以需要添加 Abp vNext Autofac 模块 并注册Autofac

    通过Nuget 安装 Volo.Abp.Autofac

    
    namespace MyShop.Api
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    })
                    .UseAutofac();
        }
    }
    
    
    MyShopApiModule

    这里我们使用了Swagger 作为Api文档自动生成,所以需要添加相关Nuget包,并且依赖了MyShopEntityFrameworkCoreModule和 MyShopApplicationModule,MyShopAdminApplicationModule 所以需要添加相关依赖及项目引用,
    并且使用了Api作为迁移连接字符串所在项目,所以还需要添加EF的Design包

    Nuget

    通过Nuget 安装 Swashbuckle.AspNetCore
    通过Nuget 安装 Microsoft.EntityFrameworkCore.Design

    项目引用

    MyShop.AdminApplication
    MyShop.Application
    MyShop.EntitFrameworkCore
    MyShop.EntitFrameworkCore.DbMigration

    
    namespace MyShop.Api
    {
    
        // 注意是依赖于AspNetCoreMvc 而不是 AspNetCore
        [DependsOn(typeof(AbpAspNetCoreMvcModule),typeof(AbpAutofacModule))]
        [DependsOn(typeof(MyShopApplicationModule),typeof(MyShopEntityFrameworkCoreModule),typeof(MyShopAdminApplicationModule))]
        public class MyShopApiModule :AbpModule
        {
            public override void ConfigureServices(ServiceConfigurationContext context)
            {
                var service = context.Services;
    
                // 跨域
                context.Services.AddCors(options =>
                {
                    options.AddPolicy("AllowAll", builder =>
                    {
                        builder.AllowAnyOrigin()
                            .SetIsOriginAllowedToAllowWildcardSubdomains()
                            .AllowAnyHeader()
                            .AllowAnyMethod();
                    });
                });
    
                // 自动生成控制器
                service.Configure((AbpAspNetCoreMvcOptions options) =>
                {
                    options.ConventionalControllers.Create(typeof(Application.ProductApplicationService).Assembly);
                    options.ConventionalControllers.Create(typeof(Application.OrderApplicationService).Assembly);
                    options.ConventionalControllers.Create(typeof(Admin.Application.Services.ProductApplicationService).Assembly, options =>
                     {
                         options.RootPath = "admin";
                     });
                });
    
                // swagger
                service.AddSwaggerGen(options =>
                {
                    options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo()
                    {
                        Title = "MyShopApi",
                        Version = "v0.1"
                    });
                    options.DocInclusionPredicate((docName, predicate) => true);
                    options.CustomSchemaIds(type => type.FullName);
                });
    
            }
    
            public override void OnApplicationInitialization(ApplicationInitializationContext context)
            {
                var env = context.GetEnvironment();
                var app = context.GetApplicationBuilder();
    
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseCors("AllowAll");
                app.UseSwagger();
                app.UseSwaggerUI(options =>
                {
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "MyShopApi");
                });
                app.UseRouting();
                app.UseConfiguredEndpoints();
            }
        }
    }
    
    

    至此我们的简单的项目就搭建好了 点击运行 查看 https://localhost:5001/swagger/index.html 就可以查看我们Application和Admin.Application中定义的接口了.

  • 相关阅读:
    ETCD 添加节点报错 tocommit(2314438) is out of range [lastIndex(0)]. Was the raft log corrupted, truncated, or lost?
    如何删除docker镜像中已配置的volume
    kubeadm初始化集群
    kubeadm安装依赖镜像
    standard_init_linux.go:178: exec user process caused "no such file or directory"
    kubernetes支持local volume
    git http方式时保存密码
    分布式核心技术
    docker使用
    Python实用日期时间处理方法汇总
  • 原文地址:https://www.cnblogs.com/ablewang/p/13856653.html
Copyright © 2011-2022 走看看