注意:目前v6.2.1版本已弃用,请使用基于.Net8的v8.1.1版本,最后更新于2024-07-07
本文档提供了Dakua快速开发框架的相关开发指南,如有问题请联系tonydai:6000666@qq.com
本框架亮点:
.Net 6的跨平台设计(商业应用已长期运行在CentOS及Windows平台下,以及Docker环境中)MySQL、SqlServer、Oracle、PostgreSql)EF、SqlSugarVue2 + Element及Vue3 + TS + ElementPlus的前端框架
MediatR)IAcModuleUseNacosConfiguration(x => x.AddSerilog(Log.Logger))UseCommFileConfiguration()NoRepeat实体字段标注:防重复,在新增或修改实体时进行字段的重复检查,会提示并记录重复的IdKeyWord实体字段标注:关键字,用于关键字搜索MemberGroup实体字段标注:对属性进行分组,用于分组更新实体指定字段IgnoreMap实体字段标注:映射忽略分组,用于实体映射时要忽略指定的字段DkEncode实体字段标注:数据库实体字段编码/加密,支持三种EncodeType:FuzzySearch、MyBase64、AESAutoWired服务层类属性标注:可自动注入非私有属性DbConnectionNameDbContext类标注:以获取对应的数据库连接 DynamicService接口层接口标:基于EMIT的动态编译,自动实现接口的本地实现或远程代理实现DisableRoute接口标注:禁止外部请求接口(仅限内部服务使用),不暴露到网络中DistributedCache接口标注:分布式接口缓存,减少数据库查询,建议应用于Query中PreventDuplicateSubmit接口标注:分布式防重复提交,建议应用于UseCase中Signature接口标注:对接口要求签名。可以使用SignatureHelper.SetSignature来为继承自ISignatureDto的类设置签名BasicAuth接口标注:简易身份认证系统,需要标注AllowAnonymous后使用,用于swagger文档等ITenantIDepartmentIRowVersionIHistoryRecordIHasExtraPropertiesIEntityValidateICreateAggregateRoot,IUpdateAggregateRoot,IFullAuditedAggregateRoot[AutoWired]Lazy<T>RSA非对称加密(DES加密(密码+验证码))传输ASP.NET 和 Web 开发,单个组件-.NET 6.0运行时项目采用DDD(领域驱动)理念设计,通过新建/复制项目模板,得到以下项目结构:
*.Application,业务层EntityFrameworkCore.Shared
用途:本领域的接口实现和服务
*.Domain,领域层Dakua.Common.EntityFrameworkCore.Shared
用途:本领域的Entity及Entity的验证规则,存储设施仓储接口
*.EntityFrameWorkCore,基础设施层Domain
用途:定义本领域的DbContext及仓储接口实现
*.HttpApi,API层.ApplicationDakua.Common.NacosExtensions 【可选】
用途:本领域的HttpApi,请注意参考Startup和Program的配置
*.Shared,其它领域分享层(DTO及接口)Dakua.Common.DynamicServiceDakua.Common.CoreApp
用途:本领域的DTO、接口、枚举、常量定义
框架在启动时,会按顺序执行以下操作:
IAcModule,并初始化模块ISingletonDependency,IScopedDependency,ITransientDependency[Table("kd_admin_user")]来映射AdminUser 或 [Column("kd_index_no")]来映射IndexNoIDbContextIDbContextRead[NotMapped],注意:NotMapped后无法Include
服务之间的调用,引入相应领域的Share项目即可实现基于Http的微服务调用接口实现,引入相应领域的Application项目即可直接接口实现
服务之间使用HTTP请求接口,接口基于AccessToken授权验证,可以应用于内网或外网访问。
对外restfull接口调用
Bearer {Token}或Query的URL中AccessToken={Token}Signature),详情参考相关不同语言的签名方法后端签名方法,使用SignatureHelper.SetSignature来为继承自ISignatureDto的类设置签名
后端JAVA签名方法
/**
* 签名生成算法
* @@param HashMap<string,string> params 请求参数集,所有参数必须已转换为字符串类型
* @@param String secret 签名密钥
* @@return 签名
* @@throws IOException
*/
public static String getSignature(HashMap<string,string>
params, String secret) throws IOException
{
// 先将参数以其参数名的字典序升序进行排序
Map<string, string>
sortedParams = new TreeMap<string, string>
(params);
Set<entry<String, String>> entrys = sortedParams.entrySet();
// 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
StringBuilder basestring = new StringBuilder();
for (Entry<String, String> param : entrys) {
basestring.append(param.getKey()).append("=").append(param.getValue());
}
basestring.Append(",").Append(secret);
// 使用MD5对待签名串求签
byte[] bytes = null;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
bytes = md5.digest(basestring.toString().getBytes("UTF-8"));
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}
// 将MD5输出的二进制结果转换为小写的十六进制
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex);
}
return sign.toString().toUpperCase();
}
前端JS签名方法
var getSignature = function (myParam, secret) {
// 对参数名进行字典排序
var array = new Array();
for (var key in myParam) {
array.push(key);
}
array.sort();
// 拼接有序的参数名-值串
var paramArray = new Array();
for (var index in array) {
var key = array[index];
paramArray.push(key + "=" + myParam[key]);
}
paramArray.push("," + secret);
// MD5签名,并转换成大写
var shaSource = paramArray.join("");
var sign = hex_md5(shaSource).toUpperCase();
return sign;
}
//调用方法:
// 定义申请获得的secret(signKey)
var secret = "pethome";
// 创建参数表
var param = {};
param["strWhere"] = "1=1";
param["pageSize"] = "20";
param["page"] = "1";
param["orderFld"] = "id";
param["isDesc"] = "true";
param["timestamp"] = (new Date().getTime() / 1000).toFixed(0);//Unix时间戳
var Signature = getSignature(param, secret);
// 字符串连接示例
// isDesc=trueorderFld=idpage=1pageSize=20strWhere=1=1timestamp=1450528907,mysecret
// 签名示例
// 41C3DEBCB735A4440DBFE2BF6E32BC61
开发技术栈
改写接口功能
namespace Dakua.Admin.Application;
/// <summary>
/// 系统后台工具查询改写
/// </summary>
public class SystemToolQueryRewrite : SystemToolQuery
{
/**
* 需要重新注入 ISystemToolQuery
* services.AddScoped<ISystemToolQuery, SystemToolQueryRewrite>();
*/
public SystemToolQueryRewrite(IServiceProvider sp) : base(sp) { }
public new async ValueTask<ResultEntity<OutUploadFileDto>> FileUpLoad(IFormFile file)
{
return await base.FileUpLoad(file);
}
}
数据库操作
通过EF操作
//注入DbContext,推荐使用池化方式,代码参考模板...
//------------------------
//Repositories
services.AddDefaultRepositories<AdminDbContext>(); //6.2.1版本后不需要此操作
//属性注入 + 延迟注入
protected IEfRepository<Temp_FullAuditTable> TempFullAuditTableRepository => LazyTempFullAuditTableRepository.Value;
[AutoWired] protected Lazy<IEfRepository<Temp_FullAuditTable>> LazyTempFullAuditTableRepository { get; set; }
public async ValueTask<ResultEntity<bool>> CreateEFAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await TempFullAuditTableRepository.AddAsync(entity, true);
return isOk.GetResultEntity();
}
public async ValueTask<ResultEntity<bool>> CreateEFUnitOfWorkAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await TempFullAuditTableRepository.AddAsync(entity);
return isOk.GetResultEntity();
}
通过SqlSugar操作
//注入DbContext,推荐使用池化方式,代码参考模板...
//------------------------
//SqlSugar
services.AddDefaultSqlSugarRepositories<AdminDbContext>(AdminDbContext.ConnectionStringName, SqlSugar.DbType.SqlServer); //6.2.1版本后不需要此操作
//属性注入 + 延迟注入
protected ISqlSugarRepository<AdminDbContext> SqlSugarTempFullAuditTableRepository => LazySqlSugarTempFullAuditTableRepository.Value;
[AutoWired] protected Lazy<ISqlSugarRepository<AdminDbContext>> LazySqlSugarTempFullAuditTableRepository { get; set; }
public async ValueTask<ResultEntity<bool>> CreateSqlSugarAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await SqlSugarTempFullAuditTableRepository.AddAsync(entity);
return isOk.GetResultEntity();
}
对象映射,默认为深度复制(如:DTO与实体Entity,支持class到Dictionary映射)
src.MapTo<Destination>(); //直接映射
src.MapTo(entity); //覆盖映射
src.MapTo(entity, new List<string>() { "Name" });//覆盖时忽略指定列
使用带时间顺序的GUID生成器
IGuidGenerate guidGenerator //注入IGuidGenerate 即可
获取HttpContextAccessor或HttpContext
IHttpContextAccessor httpContextAccessor //注入IHttpContextAccessor
httpContextAccessor.HttpContext //获取HttpContext
通过HTTP访问外部服务(如:上报数据)
IDakuaHttpClient dakuaHttpClient //注入IDakuaHttpClient
dakuaHttpClient.GetClient() //获取HttpClient
dakuaHttpClient.PostData<T>(...) //post json
dakuaHttpClient.GetData<T>(...) //get url
dakuaHttpClient.PostFormData<T>(...) //post form
dakuaHttpClient.RequestData<T>(...) //底层方法
DTO输出时的加密处理,使用[JsonConverter(typeof(JsonEncryptConverter))]标注
/// <summary>
/// 输出加密用户信息
/// </summary>
[JsonConverter(typeof(JsonEncryptConverter))]
public string UserInfo { get; set; }
DTO输出时的隐藏机密信息,使用[JsonConverter(typeof(JsonHideConverter)...]标注
/// <summary>
/// 输出从第3位开始隐藏4位的手机号
/// </summary>
[JsonConverter(typeof(JsonHideConverter), 3, 4)]
public string Phone { get; set; }
DTO输出时的枚举翻译,使用枚举字段.GetDescription()即可
/// <summary>
/// 字典类型
/// </summary>
public DictionaryTypeEnum DictionaryType { get; set; } = 0;
/// <summary>
/// 字典类型文本
/// </summary>
public string DictionaryTypeTxt => DictionaryType.GetDescription();//枚举转换
DTO输出时的字典翻译,使用字典Code字段.GetDictNameByCode(字典编码)即可
/// <summary>
/// Code
/// </summary>
[MaxLength(20)]
[NoRepeat]
public string Code { get; set; }
/// <summary>
/// Gets the code text.
/// </summary>
public string CodeTxt => Code.GetDictNameByCode("DictCode");//字典转换
DTO输出时不输出指定的属性(输出null),只需要在MapTo时指定忽略的列属性即可,或者标注[JsonIgnore]
entity.MapTo<TOutputDto>(q => new { q.Id });//不转换Id
//批量转换
//dataList.MapTo<List<TOutputDto>>();
dataList.Select(q => q.MapTo<TOutputDto>(w => w.Id)).ToList();
实体属性JSON内容转CLASS功能
IHasExtraProperties,必须有一个[Key]注解/// <summary>
/// 测试Json子类
/// </summary>
public class AdminAreasExtJson: IHasExtraProperties
{
[Key]
public bool IsOK { get; set; } = true;
public string Tips { get; set; } = "";
}
public AdminAreasExtJson ExtJson { get; set; }
内置控制器/动态代理接口注解功能
[PreventDuplicateSubmit] //防止一段时间内的重复提交
[DynamicService(ConstAuthPermission.ServiceName)] //指定动态代理服务
[ApiExplorer("AdminManage")] //指定Swagger分组
[UnitOfWork] //指定使用工作单元包裹方法
public interface ITempFullAuditTableUseCase : IDynamicService
{
/// <summary>
/// 新增
/// </summary>
/// <param name="input">dto</param>
/// <returns></returns>
[HttpPost]
[Route("/api/biz/TempFullAuditTable")]
[ApiAuth(ConstAuthPermission.BaseAdmin, RoleAction.Create)]
ValueTask<ResultEntity<bool>> CreateAsync(CreateTempFullAuditTableDto input);
}
内置实体注解实体功能
/// <summary>
/// 编码
/// </summary>
[MaxLength(50)] //实体验证配置
[Required] //不能为null
[KeyWord] //关键字搜索
[NoRepeat("编码")] //不允许重复,提示名称为‘编码’
[MemberGroup("Update|ABC")]//指定可以通过update和ABC来进行分组检索字段
[IgnoreMap("Out|MiniOut")]//指定可以通过Out和MiniOut来忽略MapTo时的字段映射
public string DictionaryCode { get; set; }
内置实体可继承接口功能
BaseAggregateRoot、BaseEntity、BaseAggregateRootCross、BaseEntityCross,默认有CreateBy、CreateTime、SetNewId()、GetMemberListByMemberGroup()、GetMemberNameListByMemberGroup()ICreateAggregateRoot,默认有CreateBy、CreateTimeIUpdateAggregateRoot,默认有ICreateAggregateRoot所有属性加上UpdateBy、UpdateTimeIFullAuditedAggregateRoot,默认有IUpdateAggregateRoot所有属性加上DeleteBy、DeleteTime、IsDeleteIDepartment用户及部门数据权限支持,默认有UserId、DepartIdITenant多租户支持,默认有TenantIdIRowVersion更新时版本号检查(乐观锁),默认有RowVersionIHasExtraProperties扩展列接口标识(将JSON转成Class,限EF),先将子类继承于本接口(子类必须有一个属性为[Key]),再在父类中使用子类为属性类型即可IHistoryRecord自动历史记录,将在历史记录表中创建数据,要在前端查看则需要返回附加数据自动注入
[AutoWired]即可,public、internal、protected均可注入,建议使用internal延迟注入
[AutoWired] protected Lazy<SystemPermissionQuery> LazySystemPermissionQuery { get; set; }
protected SystemPermissionQuery SystemPermissionQuery => LazySystemPermissionQuery.Value;
[AutoWired] protected Lazy<SystemPermissionUseCase> LazySystemPermissionUseCase { get; set; }
private SystemPermissionUseCase SystemPermissionUseCase => LazySystemPermissionUseCase.Value;
数据库内容加密(仅适用于string类型,目前仅自动处理EF的新增/修改/查询),EncodeType支持三种:FuzzySearch自动模糊搜索,MyBase64乱序Base64,AES对称加密
[DkEncode]即可 [DkEncode(encodeType)]
public string Name { get; set; }
缓存使用
//分布式缓存(Redis)
protected IRedisHelper RedisHelper
//内存缓存
protected IMemoryCache MemoryCache
分布式消息队列,需要引用Nuget包Dakua.Common.CAP,然后注入ICAPHelper capHelper后使用
//发送消息队列
await _capHelper.RecordAndPublishMQ<AdminDbContext>(sendDto.MqEvent, sendDto.DataJson, sendDto.BizNo, (dataId) => new Dictionary<string, string>()
{
//MQRecordId
["SendMqId"] = dataId.ToString(),
//失败重试次数(通过定时服务)
["FailedRetryCount"] = "2",
//失败重试间隔(分)
["FailedRetryInterval"] = "5",
});
/// <summary>
/// 分布式消息处理服务
/// </summary>
public class MyCapService : ICapSubscribe
{
private readonly ICAPHelper _capHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MyCapService"/> class.
/// </summary>
/// <param name="capHelper">The cap helper.</param>
public MyCapService(ICAPHelper capHelper)
{
_capHelper = capHelper;
}
/// <summary>
/// 接收指定为[mqEvent]的消息,并处理
/// </summary>
/// <param name="eventData">The event data.</param>
[CapSubscribe("mqEvent")]
public async void MyEventTodo(CapEventData<object> eventData)
{
await _capHelper.UpdateAndRunMq<AdminDbContext>(eventData, async (record) =>
{
//业务处理
var isOk = await todoBiz(record);
return new CapActionResult() { IsOK = isOk, Msg = isOk ? "执行成功" : "执行失败" };
});
}
}
JSON序列化,使用Newtonsoft.Json,需要:using Dakua.Common;
class.ToJsonString()"jsonString".ToClassByJson<class>();应用程序级消息,参考使用MediatR
工作单元提交,基于注解[UnitOfWork]
动态代理的接口和Controller类及方法上[UnitOfWork],可以方法上标注[NoUnitOfWork]取消本方法使用工作单元提交认证授权与权限菜单
[AllowAnonymous][ApiAuth(ConstAuthPermission.BaseAdmin, RoleAction.Create)],分别为权限字符串及权限动作。增加权限菜单,然后通过账号及角色授权到权限。实体校验
System.ComponentModel.DataAnnotations/// <summary>
/// 排序号
/// </summary>
[Required]
public int IndexNo { get; set; } = 0;
/// <summary>
/// 扩展信息
/// </summary>
[MaxLength(8000)]
public string ExtJson { get; set; }
IEntityValidate<EntityClassName>//实体验证
public void ValidateAdd(ValidateContext<EntityClassName> validateContext)
{
ValidateCommon(validateContext, true);
}
public void ValidateUpdate(ValidateContext<EntityClassName> validateContext)
{
ValidateCommon(validateContext);
}
private static void ValidateCommon(ValidateContext<EntityClassName> validateContext, bool isAdd = false)
{
//请求实体不能为null,否则直接中断校验
validateContext.MustNotNull().IfFailThenExit();
//必须不为0
//validateContext.RuleFor(i => i.Level).MustNotEqualTo(0);
//不能为空且长度在1-4之间
//validateContext.RuleFor(i => i.FullName).MustNotNullOrEmptyOrWhiteSpace().MustLengthInRange(2, 20);
}
DTO校验
System.ComponentModel.DataAnnotations/// <summary>
/// 排序号
/// </summary>
[Required]
public int IndexNo { get; set; } = 0;
/// <summary>
/// 扩展信息
/// </summary>
[MaxLength(8000)]
public string ExtJson { get; set; }
IDtoValidate<DtoClassName>public void Validate(ValidateContext<DtoClassName> validateContext)
{
//请求实体不能为null,否则直接中断校验
validateContext.MustNotNull().IfFailThenExit();
//必须不为string
//validateContext.RuleFor(i => i.Province).MustNotEqualTo("string");
}
自动CRUD泛型服务,需要继承自AutoUseCaseApplicationService和AutoQueryApplicationService、AutoFullQueryApplicationService
public virtual async ValueTask<ResultEntity<bool>> CreateAsync(TCreateInput input);
public virtual async ValueTask<ResultEntity<bool>> UpdateAsync(TUpdateInput input);
public virtual async ValueTask<ResultEntity<bool>> DeleteAsync(Guid id);
public virtual async ValueTask<ResultEntity<bool>> DeleteBatchAsync(string ids);
/// <summary>
/// 获取单个
/// </summary>
/// <param name="id">Id</param>
/// <returns></returns>
public async ValueTask<ResultEntity<TGetOutputDto>> GetAsync(Guid id);
/// <summary>
/// 获取翻页列表
/// </summary>
/// <param name="input">翻页数据</param>
/// <returns></returns>
public async ValueTask<ResultEntity<List<TGetListOutputDto>>> GetListAsync(TGetListInput input);
/// <summary>
/// 回收站(AutoFullQueryApplicationService 专有)
/// </summary>
/// <param name="input">翻页数据</param>
/// <returns></returns>
public async ValueTask<ResultEntity<List<TGetListOutputDto>>> GetListRecycleAsync(TGetListInput input);
本地化及多语言支持,依赖注入IStringLocalizer或IStringLocalizer<Class>即可,Class最好标注LocalizationResourceName来指定查找路径
//service中配置
services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); //在/Resources目录中配置语言
//Configure中配置支持的多语言版本(建议从配置文件读取)
app.UseRequestLocalization("en-US", "fr-FR");
//MVC的view使用需要配置
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(RegisterViewModel));
});
/***
* 使用:
* 1、在Resources目录中给出对应的语言,如en.json的内容为{ "你好":"Hello","操作异常,请稍候再试": "Abnormal operation, please try again later",... }
* 2、注入IStringLocalizer或者继承自BaseApplicationService后使用L["你好"]
***/
基础服务继承可使用的属性,需要继承自BaseApplicationService或他的子类
//多语言支持,如:L["你好"]
protected IStringLocalizer L;
//时间顺序GUID工具类
protected IGuidGenerate GuidGenerator;
//当前认证账号
protected ICurrentIdentifyUser CurrentUser;
//HttpContextAccessor
protected IHttpContextAccessor HttpContextAccessor;
//Redis工具类
protected IRedisHelper RedisHelper;
//Excel工具类
protected IExcelHelper ExcelHelper;
//Mediator
protected IMediator Mediator;
//字典转换类
protected ITranslateDict TranslateDict;
//全局配置类
protected IGlobalConfig GlobalConfig;
//用户工具类
protected IAdminUserHelper AdminUserHelper;
//租户工具类
protected ITenantHelper TenantHelper;
//Entity工具类
protected IEntityHelper EntityHelper;
//当前用户Id
protected Guid CurrentUserId => CurrentUser.Id;
//当前用户信息
protected RedisAdminUserDto CurrentAdminUser;
//当前用户是否超级管理员
protected bool IsSuperAdmin;
//当前租户Id
protected Guid CurrentTenantId;
//当前租户信息
protected AdminTenantDto CurrentTenant;
//当前用户权限部门(不包括子部门,仅包括选择部门)
protected List<AdminUserDepartment> CurrentUserDepartments;
//当前用户登录选择的权限部门(包括子部门继承)
protected List<AdminUserDepartment> CurrentUserAllDepartments;
//执行事务提交,包含仓储未保存的修改
protected async ValueTask<int> CommitRepositoryAsync(params IEFCoreRepository[] repositories);
//执行事务提交,包含仓储未保存的修改,同时执行SQL
protected async ValueTask<int> CommitRepositoryAsync(List<CommitSQLDto> commitSqlList, params IEFCoreRepository[] repositories);
// 雪花Id
protected long NewSnowFlakeID();
// 时间顺序的GUID
protected Guid NewTimeGuid();
/// <summary>
/// 获取模糊匹配查询表达式(根据查询DTO)
/// </summary>
/// <typeparam name="TDto">The type of the dto.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="dto">查询dto</param>
/// <param name="exp">现有需要附加的表达式</param>
/// <param name="noExp">不处理的字段表达式,如:q => new { q.Name }</param>
/// <param name="isAnd">表达式之间是否使用And连接,为false时将使用Or连接</param>
/// <returns></returns>
public static Expression<Func<TEntity, bool>> GetExpressionByQueryDto<TDto, TEntity>(TDto dto
, Expression<Func<TEntity, bool>> exp = null
, Expression<Func<TDto, object>> noExp = null
, bool isAnd = true)
where TEntity : class, new()
where TDto : class, new()
=> GetExpressionEqualOrContainsByQueryDto(dto, exp, noExp, isAnd);
/// <summary>
/// 获取精确匹配查询表达式(根据查询DTO)
/// </summary>
/// <typeparam name="TDto">The type of the dto.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="dto">查询dto</param>
/// <param name="exp">现有需要附加的表达式</param>
/// <param name="noExp">不处理的字段表达式,如:q => new { q.Name }</param>
/// <param name="isAnd">表达式之间是否使用And连接,为false时将使用Or连接</param>
/// <returns></returns>
public static Expression<Func<TEntity, bool>> GetExpressionEqualByQueryDto<TDto, TEntity>(TDto dto
, Expression<Func<TEntity, bool>> exp = null
, Expression<Func<TDto, object>> noExp = null
, bool isAnd = true)
where TEntity : class, new()
where TDto : class, new()
=> GetExpressionEqualOrContainsByQueryDto(dto, exp, noExp, isAnd, false);
常见问题:
last update 2023-11-10, by www.dakua.net