后端基础入门 #
准备 #
创建Web项目 #
环境要求
使用Mall3s之前先确保安装了最新的.net 5 SDK并升级Visual Studio 2019至最新版。
创建ASP.NET Core Web 应用程序 #
打开visual Studio 2019并创建Web项目
配置项目名称
选择webAPi项目
特别注意
Mall3s已经内置了Swagger规范化库,所以创建时无需勾选启动openAPi支持选项。否则提示版本不一致产生冲突。
脚手架 #
前端 #
共用前端,不需要脚手架,前端代码仓库地址:http://code.mall3s.com/erp/mall3s.web.git
后端 #
脚手架地址:http://code.mall3s.com/erp/mall3s.basedata/-/tree/new_version_220311
脚手架说明请查看项目README.md文档
注:如对脚手架改名,需把obj和bin文件夹删除(如有)。README.md文档一定要看!!!
初始化项目工程结构
Mall3s.Library
Mall3s.Entities实体层
Mall3s.Interface接口层
Mall3s.Serviceweb服务层
Mall3s.WebApi接口层
项目初始化**-Mall3s.API** #
Nacos环境配置 #
nacos环境地址:nacos.mall3s.com/nacos
- 新增业务数据库链接
nacos配置好数据库连接,在netcore-datasource.json****上新增配置
{
"SkumsConnectionStrings": {
"ConfigId": "mall3s_demo",//租户ID,通常使用数据库名不带环境,如mall3s_oms_test/mall3s_oms_pro,租户ID使用mall3s_oms
"DBName": "mall3s_demo",//数据库名
"DBType": "MySql", //数据库类型
"DefaultConnection": "server=sh-cdb-s261yedo.sql.tencentcdb.com;port=59347;uid=erpdba;pwd=erp123!@#;database={0}"//数据库链接
}
}
备注:ConfigId为关键点,为了避免冲突,请使用数据库名不带环境,如mall3s_oms_test/mall3s_oms_pro,租户ID使用mall3s_oms,同一次启动服务,切勿同时注入正式和测试环境。
- 新建业务项目公共nacos配置,netcore-项目名-common.json(如netcore-skums-common.json)
{
"ExtraDB":["SkumsConnectionStrings"], //数据库配置
"SkyWalking": {
"ServiceName": "mall3s-skums-api",//这里改为skywalking的服务名称
"Namespace": "DEFAULT_GROUP"
}
}
备注:修改数据库链接字符串以及skywalking的服务名
本地环境配置 #
- launchSettings.json环境变量添加skywalking配置:
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore",
"SKYWALKING__SERVICENAME": "mall3s-skums-api"
}
备注:SKYWALKING__SERVICENAME改为本系统的服务名
- appsettings添加appsettings.json配置
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"NacosConfig": { //nacos配置中心,所有配置基于NacosConfig:GroupName命名空间
"Listeners": [ //监听下面的配置(设置了的数据会自动加载到Configuration中),需要加载的配置文件请在此增加
{
"Optional": false, //代表是否自动刷新配置
"DataId": "netcore-datasource.json", //代表资源ID
"Group": "DEFAULT_GROUP" //资源的分组
},
{
"Optional": false,
"DataId": "netcore-common.json",
"Group": "DEFAULT_GROUP"
},
{
"Optional": false,
"DataId": "netcore-app.json",
"Group": "DEFAULT_GROUP"
},
{
"Optional": false,
"DataId": "netcore-skums-common.json", //改为各自配置地址
"Group": "DEFAULT_GROUP"
}
],
"UserName": "nacos",
"Password": "erp1276",
"DefaultTimeOut": 5000,
"ListenInterval": 1000,
"ServiceName": "mall3s-webapi-skums", //改为各自服务名
"ClusterName": "DEFAULT",
//"PreferredNetworks": "",
"Weight": 100,
"RegisterEnabled": true,
"InstanceEnabled": true,
"Ephemeral": true,
"Secure": false,
//"AccessKey": "",
// "SecretKey": "",
"ConfigUseRpc": false,
"NamingUseRpc": false,
"NamingLoadCacheAtStart": ""
},
//过滤api文档
"AppSettings": {
"HideApiDocList": ""
},
"xxlJob": {
"appName": "xxl-job-executor-dotnet", //执行器(不需要改)
"specialBindAddress": "",
"port": 5000, //当前执行器
"autoRegistry": true,
"accessToken": "",
"logRetentionDays": 30
}
}
备注:注意新增的netcore-skums-common.json配置节点,以及修改ServiceName改为服务名mall3s-webapi-skums。
- 添加开发环境配置 appsettings.Development.json
{
"NacosConfig": { //nacos配置中心
"ServerAddresses": [ "http://nacos.mall3s.com" ],
"GroupName": "DEFAULT_GROUP",
"Namespace": "69c4eecb-05bd-4041-81fe-1473f95f578c" //dev开发环境
//"Ip": "localhost", //改成自己的服务ip,不填默认读取当前本机ip
//"Port": 31201 //改成环境端口
},
"xxlJob": {
"adminAddresses": "http://job.mall3s.com/xxl-job-admin" //正式环境改为线上:"adminAddresses": "http://xxl-job-admin.base:8080/xxl-job-admin",
}
}
- 添加生产环境配置 appsettings.Product.json
{
"NacosConfig": { //nacos
"ServerAddresses": [ "http://nacos.base:30099" ],
"GroupName": "DEFAULT_GROUP",
"Namespace": "3baec428-9669-486c-b359-a76f7a1f1ac7" //product
},
"xxlJob": {
"adminAddresses": "http://xxl-job-admin.base:8080/xxl-job-admin" //正式环境改为线上:"adminAddresses": "http://job.mall3s.com/xxl-job-admin",
}
}
- 添加测试环境配置 appsettings.Test.json
{
"NacosConfig": { //nacos
"ServerAddresses": [ "http://nacos.base:30099" ],
"GroupName": "DEFAULT_GROUP",
"Namespace": "1e017954-eb52-4d21-a843-0286d9013cf3" //test
},
"xxlJob": {
"adminAddresses": "http://xxl-job-admin.base:8080/xxl-job-admin" //正式环境改为线上:"adminAddresses": "http://job.mall3s.com/xxl-job-admin",
}
}
- startup加入2个微服务库
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
//添加微服务
services.AddMicroService(Configuration);
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
//注入mall3s微服务
app.UseMicroService(Configuration);
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
- program需要引用微服务
webBuilder.Inject().UseMicroService().UseStartup<Startup>();
- Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//除了默认DB和租户DB外的DB
#if DEBUG
//添加微服务(debug不验证token)
services.AddMicroService(_configuration, false);
#else
//添加微服务
services.AddMicroService(_configuration, true);
#endif
//注册IHttp服务请求,微服务调用
//services.AddNacosDiscoveryTypedClient<ITestHttpApi>("mall3s-system", "DEFAULT");
services.AddRazorPages();
//翻译
//services.AddTranslate(_configuration);
//七牛云
//services.AddQiNiu(_configuration);
//任务管理
services.AddXxlJobAuto(_configuration);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
//注入mall3s微服务
app.UseMicroService(_configuration);
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
//xxljob
app.UseXxlJobExecutor();
}
备注:关键代码:services.AddMicroService(_configuration, false);
添加微服务后即可自动加入数据库租户信息
微服务有四个实现,下面两个为自定义扩展库,可灵活选择:
//实现一:nacos配置好DB链接信息,直接告知微服务需要额外加入的DB的key
public static IServiceCollection AddMicroService(this IServiceCollection services, IConfiguration configuration, bool enableGlobalAuthorize = false, List<string> connKeys = null)
//实现二:自行配置DB配置后赋值
public static IServiceCollection AddMicroService(this IServiceCollection services, IConfiguration configuration,bool enableGlobalAuthorize= false, List<ConnectionConfig> connectionConfigs = null)
创建新增删改查接口 #
Mall3s微服务不需要单独创建Controller,一个标准化的增删改查只需要创建三个工程:
- Mall3s.Service层
- Mall3s.Entities层
- Mall3s.Interfaces层
Mall3s.Entities实体层 #
Entity层主要分为5类:
- Dto实体
用于与UI层交互的模型层。
命名规则主要是xxxInput,xxxxOutput分别代表输入和输出返回,并需要设置[SuppressSniffer]不被服务注册扫描到该实体。
- Entity实体
用于数据库交互的实体模型。
- 统一继承Mall3sEntityBase接口,其中Mall3sEntityBase包含基础字段。
- 需要设置SugarTable标签,代表映射数据库哪个表[SugarTable("skums_category")]
- 需要设置租户标签**[Tenant("mall3s_skums")]**
/// <summary>
/// 类目管理
/// </summary>
[SugarTable("skums_category")]
[Tenant("mall3s_skums")]
public class SkumsCategoryEntity:Mall3sEntityBase
{
/// <summary>
/// 类目中文名
/// </summary>
[SugarColumn(ColumnName = "category_cn_name")]
public string CategoryCnName { get; set; }
/// <summary>
/// 类目英文名
/// </summary>
[SugarColumn(ColumnName = "category_en_name")]
public string CategoryEnName { get; set; }
/// <summary>
/// 完整类目路径
/// </summary>
[SugarColumn(ColumnName = "category_cn_path")]
public string CategoryCnPath { get; set; }
/// <summary>
/// 上级分类Id
/// </summary>
[SugarColumn(ColumnName = "parent_id")]
public string ParentId { get; set; }
/// <summary>
/// 上级分类
/// </summary>
[SugarColumn(ColumnName = "parent_category_cn_name")]
public string ParentCategoryCnName { get; set; }
/// <summary>
/// 海关编码
/// </summary>
[SugarColumn(ColumnName = "customs_code")]
public string CustomsCode { get; set; }
/// <summary>
/// 申报品名
/// </summary>
[SugarColumn(ColumnName = "declared_product_name")]
public string DeclaredProductName { get; set; }
/// <summary>
/// 产品性质
/// </summary>
[SugarColumn(ColumnName = "product_nature")]
public string ProductNature { get; set; }
/// <summary>
/// 最低申报价(美元)
/// </summary>
[SugarColumn(ColumnName = "minimum_declare_price")]
public decimal MinimumDeclaraPrice { get; set; }
}
- Model实体
各个层交互的模型实体。
- Mapper
用于定义Dto或者Model与Entity数据库映射的模型。
- 需继承IRegister接口
- 需要定义Register方法,用于设置Mapping规则。
public class Mapper : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.ForType<IMContentEntity, IMUnreadNumModel>()
.Map(dest => dest.unreadNum, src => src.State);
config.ForType<UserOnlineModel, OnlineUserListOutput>()
.Map(dest => dest.userId, src => src.userId)
.Map(dest => dest.userAccount, src => src.account)
.Map(dest => dest.userName, src => src.userName)
.Map(dest => dest.loginTime, src => src.lastTime)
.Map(dest => dest.loginIPAddress, src => src.lastLoginIp)
.Map(dest => dest.loginPlatForm, src => src.lastLoginPlatForm);
}
}
Mall3s.Service层 #
新建Service继承IDynamicApiController和ITransient,将会动态注入API服务并完成自动注册。
通过ApiDescrtionsettings设置接口文档参数信息。
通过Route设置API路由信息
/// <summary>
/// 系统消息
/// 版 本:V3.2
/// 版 权:mall3s开发
/// 作 者:Mall3s开发平台组
/// 日 期:2021-06-01
/// </summary>
[ApiDescriptionSettings(Tag = "Message", Name = "message", Order = 240)]
[Route("api/[controller]")]
public class MessageService : IMessageService, IDynamicApiController, ITransient
{
}
}
标准化RESTFULL风格的接口,请遵循标准。
列表分页采用Get请求,更新采用PUT,删除采用DELETE,新建采用POST。
返回值尽量采用Dynamic返回。
不对外提供API请设置 [NonAction]标签。
/// <summary>
/// 获取类目管理
/// </summary>
/// <param name="id">参数</param>
/// <returns></returns>
[HttpGet("{id}")]
public async Task<dynamic> GetInfo(string id)
{
var entity = await _db.Queryable<SkumsCategoryEntity>().FirstAsync(p => p.Id == id);
var output = entity.Adapt<SkumsCategoryInfoOutput>();
return output;
}
/// <summary>
/// 获取类目管理列表
/// </summary>
/// <param name="input">请求参数</param>
/// <returns></returns>
[HttpGet("")]
public async Task<dynamic> GetList([FromQuery] SkumsCategoryListQueryInput input)
{
var sidx = input.sidx == null ? "id" : input.sidx;
var data = await _db.Queryable<SkumsCategoryEntity>()
.WhereIF(!string.IsNullOrEmpty(input.categoryCnName), p => p.CategoryCnName.Contains(input.categoryCnName))
.WhereIF(!string.IsNullOrEmpty(input.categoryEnName), p => p.CategoryEnName.Contains(input.categoryEnName))
.WhereIF(!string.IsNullOrEmpty(input.parentCategoryCnName), p => p.ParentCategoryCnName.Contains(input.parentCategoryCnName))
.Select(it=> new SkumsCategoryListOutput
{
id = it.Id,
categoryCnName=it.CategoryCnName,
categoryEnName=it.CategoryEnName,
parentCategoryCnName=it.ParentCategoryCnName,
customsCode=it.CustomsCode,
declaredProductName=it.DeclaredProductName,
productNature=it.ProductNature,
minimumDeclaraPrice=it.MinimumDeclaraPrice,
}).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
return PageResult<SkumsCategoryListOutput>.SqlSugarPageResult(data);
}
/// <summary>
/// 新建类目管理
/// </summary>
/// <param name="input">参数</param>
/// <returns></returns>
[HttpPost("")]
public async Task Create([FromBody] SkumsCategoryCrInput input)
{
var userInfo = await _userManager.GetUserInfo();
var entity = input.Adapt<SkumsCategoryEntity>();
entity.Id = YitIdHelper.NextId().ToString();
var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync();
if (!(isOk > 0)) throw Mall3sException.Oh(ErrorCode.COM1000);
}
/// <summary>
/// 获取类目管理无分页列表
/// </summary>
/// <param name="input">请求参数</param>
/// <returns></returns>
[NonAction]
public async Task<dynamic> GetNoPagingList([FromQuery] SkumsCategoryListQueryInput input)
{
var sidx = input.sidx == null ? "id" : input.sidx;
var data = await _db.Queryable<SkumsCategoryEntity>()
.WhereIF(!string.IsNullOrEmpty(input.categoryCnName), p => p.CategoryCnName.Contains(input.categoryCnName))
.WhereIF(!string.IsNullOrEmpty(input.categoryEnName), p => p.CategoryEnName.Contains(input.categoryEnName))
.WhereIF(!string.IsNullOrEmpty(input.parentCategoryCnName), p => p.ParentCategoryCnName.Contains(input.parentCategoryCnName))
.Select(it => new SkumsCategoryListOutput
{
id = it.Id,
categoryCnName = it.CategoryCnName,
categoryEnName = it.CategoryEnName,
parentCategoryCnName = it.ParentCategoryCnName,
customsCode = it.CustomsCode,
declaredProductName = it.DeclaredProductName,
productNature = it.ProductNature,
minimumDeclaraPrice = it.MinimumDeclaraPrice,
}).MergeTable().OrderBy(sidx + " " + input.sort).ToListAsync();
return data;
}
/// <summary>
/// 导出类目管理
/// </summary>
/// <param name="input">请求参数</param>
/// <returns></returns>
[HttpGet("Actions/Export")]
public async Task<dynamic> Export([FromQuery] SkumsCategoryListQueryInput input)
{
var userInfo = await _userManager.GetUserInfo();
var exportData = new List<SkumsCategoryListOutput>();
if (input.dataType == 0)
{
var data = Clay.Object(await this.GetList(input));
exportData = data.Solidify<PageResult<SkumsCategoryListOutput>>().list;
}
else
{
exportData = await this.GetNoPagingList(input);
}
List<ParamsModel> paramList = "[{\"value\":\"类目中文名\",\"field\":\"categoryCnName\"},{\"value\":\"类目英文名\",\"field\":\"categoryEnName\"},{\"value\":\"上级分类\",\"field\":\"parentCategoryCnName\"},{\"value\":\"海关编码\",\"field\":\"customsCode\"},{\"value\":\"申报品名\",\"field\":\"declaredProductName\"},{\"value\":\"产品性质\",\"field\":\"productNature\"},{\"value\":\"最低申报价(美元)\",\"field\":\"minimumDeclaraPrice\"},]".ToList<ParamsModel>();
ExcelConfig excelconfig = new ExcelConfig();
excelconfig.FileName = DateTime.Now.ToString("yyyyMMdd") + "_" + "类目管理.xls";
excelconfig.HeadFont = "微软雅黑";
excelconfig.HeadPoint = 10;
excelconfig.IsAllSizeColumn = true;
excelconfig.ColumnModel = new List<ExcelColumnModel>();
List<string> selectKeyList = input.selectKey.Split(',').ToList();
foreach (var item in selectKeyList)
{
var isExist = paramList.Find(p => p.field == item);
if (isExist != null)
{
excelconfig.ColumnModel.Add(new ExcelColumnModel() { Column = isExist.field, ExcelColumn = isExist.value });
}
}
if (!Directory.Exists(FileVariable.TemporaryFilePath))
Directory.CreateDirectory(FileVariable.TemporaryFilePath);
var addPath = FileVariable.TemporaryFilePath + excelconfig.FileName;
ExcelExportHelper<SkumsCategoryListOutput>.Export(exportData, excelconfig, addPath);
FileInfo file = new FileInfo(addPath);
FileStream fs = file.OpenRead();
BinaryReader br = new BinaryReader(fs);
byte[] bytes = br.ReadBytes((int)fs.Length);
Stream stream = new MemoryStream(bytes);
fs.Close();
var _fileName = DateTime.Now.ToString("yyyyMMdd") + "_" + YitIdHelper.NextId().ToString() + Path.GetExtension(file.Name);
var bucketName = KeyVariable.BucketName;
var _filePath = "storage/TemporaryFile/";
var uploadPath = Path.Combine(_filePath, _fileName);
var result = await _oSSServiceFactory.Create().PutObjectAsync(bucketName, uploadPath, stream);
var qiniuUrl = App.Configuration["QiNiu:CdnUrl"] + "/" + uploadPath;
if (File.Exists(addPath))
{
File.Delete(addPath);
}
var output = new
{
name = excelconfig.FileName,
url = qiniuUrl
};
return output;
}
/// <summary>
/// 更新类目管理
/// </summary>
/// <param name="id">主键</param>
/// <param name="input">参数</param>
/// <returns></returns>
[HttpPut("{id}")]
public async Task Update(string id, [FromBody] SkumsCategoryUpInput input)
{
var entity = input.Adapt<SkumsCategoryEntity>();
var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
if (!(isOk > 0)) throw Mall3sException.Oh(ErrorCode.COM1001);
}
/// <summary>
/// 删除类目管理
/// </summary>
/// <returns></returns>
[HttpDelete("{id}")]
public async Task Delete(string id)
{
var entity = await _db.Queryable<SkumsCategoryEntity>().FirstAsync(p => p.Id == id);
_ = entity ?? throw Mall3sException.Oh(ErrorCode.COM1005);
var isOk = await _db.Deleteable<SkumsCategoryEntity>().Where(d => d.Id == id).ExecuteCommandAsync();
if (!(isOk > 0)) throw Mall3sException.Oh(ErrorCode.COM1002);
}
/// <summary>
/// 列表
/// </summary>
/// <returns></returns>
[HttpGet("Selector")]
public async Task<dynamic> GetSelector()
{
var data = await GetList();
var output = data.Adapt<List<CategoryTypeSelectorOutput>>();
return new { list = output.ToTree("-1") };
}
#region PublicMethod
/// <summary>
/// 列表
/// </summary>
/// <returns></returns>
[NonAction]
public async Task<List<SkumsCategoryEntity>> GetList()
{
return await _skumsCategoryRepository.Entities.OrderBy(x => x.CreatorTime, OrderByType.Desc).ToListAsync();
}
#endregion
Mall3s.Interfaces接口层 #
接口层主要是为Service层提供接口调用。
namespace Mall3s.SubDev.Interfaces.SkumsCategory
{
public interface ISkumsCategoryService
{
}
}
第一个增删改查案例 #
业务需求:完成一个简单的增删改查模块教学。
主要流程如下:
数据库建模。设计数据库结构。
使用低代码功能设计界面。使用代码生成器设计界面。
下载源码并放到项目中。生成代码后,下载源码并将前后端分别到对应项目工程中。
运行。启动前后端项目。
配置菜单并上线。配置菜单,功能上线。
一、建立数据库模型 #
数据库设计建模 #
可使用powerdesigner或者pdMan做数据库结构设计。
Mall3s数据管理创建数据表。 #
配置数据库链接。添加自己的数据库。
数据建模。 #
新建数据库表以及字段并保存。
二、使用低代码功能设计界面 #
代码生成器生成代码 #
打开http://test.mall3s.com/generator/webForm,选择功能表单-》新建模板,根据业务场景,选择表单,列表,流程多个模式生成代码。
根据教程配置模块。
三、下载源码并放到项目中 #
下载源代码。涉及前端的页面主要包括三个部分:
- index.vue页面
- ExportBox.vue页面
- Form.vue页面
复制代码到/src/views/xxx/yyyy目录下(xxx代表子系统的名称,yyyy代表模块名称)
涉及后端的代码,分别放到您项目模块中以下目录:
Mall3s.Entities实体层
Mall3s.Interface接口层
Mall3s.Service服务层
Mall3s.WebApi接口层
四、运行 #
- 前端执行npm run dev
- 后端用vs 2019启动项目即可
五、配置菜单功能上线 #
打开菜单 http://web.mall3s.com/system/menu
选择类型:页面,设置地址即可。
预览效果: