diff --git a/BPA.SaaS.Stock.Api.sln b/BPA.SaaS.Stock.Api.sln index 3d730eb..3f6207b 100644 --- a/BPA.SaaS.Stock.Api.sln +++ b/BPA.SaaS.Stock.Api.sln @@ -49,6 +49,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".pack", ".pack", "{5CE18647 src\.pack\BPA.SaaS.Stock.Api.SDK.nuspec = src\.pack\BPA.SaaS.Stock.Api.SDK.nuspec EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Script", ".Script", "{FBC5FEFB-0107-45CD-B2A4-8EA9ABDA9082}" + ProjectSection(SolutionItems) = preProject + src\SQLScript\20220418.sql = src\SQLScript\20220418.sql + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginInput.cs b/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginInput.cs index 446fa61..22ed81e 100644 --- a/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginInput.cs +++ b/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginInput.cs @@ -20,13 +20,16 @@ namespace BPA.SaaS.Stock.Api.DTO.Basic.Auth /// 123456 [Required(ErrorMessage = "密码不能为空")] public string Password { get; set; } + public string RoleId { get; set; } public string Name { get; set; } + /// /// 0正常 1停用 2删除 /// public int Stutas { get; set; } } + //public class PersonValidator : AbstractValidator //{ // public PersonValidator() @@ -43,6 +46,7 @@ namespace BPA.SaaS.Stock.Api.DTO.Basic.Auth public int PageIndex { get; set; } public int PageSize { get; set; } } + public class UserInputDTO { public string OldPwd { get; set; } diff --git a/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginOutput.cs b/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginOutput.cs index dbae463..3df2f94 100644 --- a/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginOutput.cs +++ b/src/BPA.SaaS.Stock.Api.DTO/Basic/Auth/LoginOutput.cs @@ -72,7 +72,6 @@ namespace BPA.SaaS.Stock.Api.DTO.Basic.Auth /// public String Tel { get; set; } - /// /// 最后登陆IP /// @@ -101,7 +100,7 @@ namespace BPA.SaaS.Stock.Api.DTO.Basic.Auth /// /// 登录菜单信息---AntDesign版本菜单 /// - public List Menus { get; set; } = new List(); + public List Menus { get; set; } = new(); /// /// 数据范围(机构)信息 diff --git a/src/BPA.SaaS.Stock.Api.DTO/EnumErrorCode.cs b/src/BPA.SaaS.Stock.Api.DTO/EnumErrorCode.cs new file mode 100644 index 0000000..910fad7 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.DTO/EnumErrorCode.cs @@ -0,0 +1,394 @@ +using System.ComponentModel; + +namespace BPA.SaaS.Stock.Api.DTO +{ + /// + /// 系统错误码 + /// + public enum ErrorCode + { + /// + /// 用户名或密码不正确 + /// + [Description("用户名或密码不正确")] + D1000, + + /// + /// 非法操作!禁止删除自己 + /// + [Description("非法操作,禁止删除自己")] + D1001, + + /// + /// 记录不存在 + /// + [Description("记录不存在")] + D1002, + + /// + /// 账号已存在 + /// + [Description("账号已存在")] + D1003, + + /// + /// 旧密码不匹配 + /// + [Description("旧密码输入错误")] + D1004, + + /// + /// 测试数据禁止更改admin密码 + /// + [Description("测试数据禁止更改用户【admin】密码")] + D1005, + + /// + /// 数据已存在 + /// + [Description("数据已存在")] + D1006, + + /// + /// 数据不存在或含有关联引用,禁止删除 + /// + [Description("数据不存在或含有关联引用,禁止删除")] + D1007, + + /// + /// 禁止为管理员分配角色 + /// + [Description("禁止为管理员分配角色")] + D1008, + + /// + /// 重复数据或记录含有不存在数据 + /// + [Description("重复数据或记录含有不存在数据")] + D1009, + + /// + /// 禁止为超级管理员角色分配权限 + /// + [Description("禁止为超级管理员角色分配权限")] + D1010, + + /// + /// 非法数据 + /// + [Description("非法数据")] + D1011, + + /// + /// Id不能为空 + /// + [Description("Id不能为空")] + D1012, + + /// + /// 所属机构不在自己的数据范围内 + /// + [Description("没有权限操作该数据")] + D1013, + + /// + /// 禁止删除超级管理员 + /// + [Description("禁止删除超级管理员")] + D1014, + + /// + /// 禁止修改超级管理员状态 + /// + [Description("禁止修改超级管理员状态")] + D1015, + + /// + /// 没有权限 + /// + [Description("没有权限")] + D1016, + + /// + /// 账号已冻结 + /// + [Description("账号已冻结")] + D1017, + + /// + /// 父机构不存在 + /// + [Description("父机构不存在")] + D2000, + + /// + /// 当前机构Id不能与父机构Id相同 + /// + [Description("当前机构Id不能与父机构Id相同")] + D2001, + + /// + /// 已有相同组织机构,编码或名称相同 + /// + [Description("已有相同组织机构,编码或名称相同")] + D2002, + + /// + /// 没有权限操作机构 + /// + [Description("没有权限操作机构")] + D2003, + + /// + /// 该机构下有员工禁止删除 + /// + [Description("该机构下有员工禁止删除")] + D2004, + + /// + /// 附属机构下有员工禁止删除 + /// + [Description("附属机构下有员工禁止删除")] + D2005, + + /// + /// 只能增加下级机构 + /// + [Description("只能增加下级机构")] + D2006, + + /// + /// 字典类型不存在 + /// + [Description("字典类型不存在")] + D3000, + + /// + /// 字典类型已存在 + /// + [Description("字典类型已存在,名称或编码重复")] + D3001, + + /// + /// 字典类型下面有字典值禁止删除 + /// + [Description("字典类型下面有字典值禁止删除")] + D3002, + + /// + /// 字典值已存在 + /// + [Description("字典值已存在,名称或编码重复")] + D3003, + + /// + /// 字典值不存在 + /// + [Description("字典值不存在")] + D3004, + + /// + /// 字典状态错误 + /// + [Description("字典状态错误")] + D3005, + + /// + /// 菜单已存在 + /// + [Description("菜单已存在")] + D4000, + + /// + /// 路由地址为空 + /// + [Description("路由地址为空")] + D4001, + + /// + /// 打开方式为空 + /// + [Description("打开方式为空")] + D4002, + + /// + /// 权限标识格式为空 + /// + [Description("权限标识格式为空")] + D4003, + + /// + /// 权限标识格式错误 + /// + [Description("权限标识格式错误")] + D4004, + + /// + /// 权限不存在 + /// + [Description("权限不存在")] + D4005, + + /// + /// 父级菜单不能为当前节点,请重新选择父级菜单 + /// + [Description("父级菜单不能为当前节点,请重新选择父级菜单")] + D4006, + + /// + /// 不能移动根节点 + /// + [Description("不能移动根节点")] + D4007, + + /// + /// 已存在同名或同编码应用 + /// + [Description("已存在同名或同编码应用")] + D5000, + + /// + /// 默认激活系统只能有一个 + /// + [Description("默认激活系统只能有一个")] + D5001, + + /// + /// 该应用下有菜单禁止删除 + /// + [Description("该应用下有菜单禁止删除")] + D5002, + + /// + /// 已存在同名或同编码应用 + /// + [Description("已存在同名或同编码应用")] + D5003, + + /// + /// 已存在同名或同编码职位 + /// + [Description("已存在同名或同编码职位")] + D6000, + + /// + /// 该职位下有员工禁止删除 + /// + [Description("该职位下有员工禁止删除")] + D6001, + + /// + /// 通知公告状态错误 + /// + [Description("通知公告状态错误")] + D7000, + + /// + /// 通知公告删除失败 + /// + [Description("通知公告删除失败")] + D7001, + + /// + /// 通知公告编辑失败 + /// + [Description("通知公告编辑失败,类型必须为草稿")] + D7002, + + /// + /// 文件不存在 + /// + [Description("文件不存在")] + D8000, + + /// + /// 已存在同名或同编码参数配置 + /// + [Description("已存在同名或同编码参数配置")] + D9000, + + /// + /// 禁止删除系统参数 + /// + [Description("禁止删除系统参数")] + D9001, + + /// + /// 已存在同名任务调度 + /// + [Description("已存在同名任务调度")] + D1100, + + /// + /// 任务调度不存在 + /// + [Description("任务调度不存在")] + D1101, + + /// + /// 演示环境禁止修改数据 + /// + [Description("演示环境禁止修改数据")] + D1200, + + /// + /// 已存在同名或同管理员或同主机租户 + /// + [Description("已存在同名或同主机租户")] + D1300, + + /// + /// 该表代码模板已经生成过 + /// + [Description("该表代码模板已经生成过")] + D1400, + + /// + /// 该类型不存在 + /// + [Description("该类型不存在")] + D1501, + + /// + /// 该字段不存在 + /// + [Description("该字段不存在")] + D1502, + + /// + /// 该类型不是枚举类型 + /// + [Description("该类型不是枚举类型")] + D1503, + + /// + /// 该实体不存在 + /// + [Description("该实体不存在")] + D1504, + + /// + /// 父菜单不存在 + /// + [Description("父菜单不存在")] + D1505, + + /// + /// 已存在同名或同编码项目 + /// + [Description("已存在同名或同编码项目")] + xg1000, + + /// + /// 已存在相同证件号码人员 + /// + [Description("已存在相同证件号码人员")] + xg1001, + + /// + /// 检测数据不存在 + /// + [Description("检测数据不存在")] + xg1002, + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Entity/BaseGroupIdEntity.cs b/src/BPA.SaaS.Stock.Api.Entity/BaseGroupIdEntity.cs index ebd5b47..5cc4d06 100644 --- a/src/BPA.SaaS.Stock.Api.Entity/BaseGroupIdEntity.cs +++ b/src/BPA.SaaS.Stock.Api.Entity/BaseGroupIdEntity.cs @@ -1,11 +1,10 @@ using SqlSugar; -namespace BPA.SaaS.Stock.Api.Entity +namespace BPA.SaaS.Stock.Api.Entity; + +public class BaseGroupIdEntity : Entity { - public class BaseGroupIdEntity : Entity - { - // TODO:MOD - [SugarColumn(ColumnDataType = "nvarchar(64)", ColumnDescription = "GroupId", IsNullable = true)] - public string GroupId { get; set; } /*= App.User?.FindFirst(ClaimConst.GroupId)?.Value;*/ - } + // TODO:MOD + [SugarColumn(ColumnDataType = "nvarchar(64)", ColumnDescription = "GroupId", IsNullable = true)] + public string GroupId { get; set; } /*= App.User?.FindFirst(ClaimConst.GroupId)?.Value;*/ } \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Entity/BaseOPEntity.cs b/src/BPA.SaaS.Stock.Api.Entity/BaseOPEntity.cs index ae375c9..a12f3e3 100644 --- a/src/BPA.SaaS.Stock.Api.Entity/BaseOPEntity.cs +++ b/src/BPA.SaaS.Stock.Api.Entity/BaseOPEntity.cs @@ -1,4 +1,5 @@ -using SqlSugar; +using BPA.SaaS.Stock.Api.DTO.Basic.Claim; +using SqlSugar; namespace BPA.SaaS.Stock.Api.Entity { @@ -12,8 +13,8 @@ namespace BPA.SaaS.Stock.Api.Entity /// 0=> 正常使用 /// 1=> 已经被标记删除 /// - [SugarColumn(ColumnDataType = "int", ColumnDescription = "是否删除", IsNullable = false)] - public int IsDeleted { get; set; } = 0; + [SugarColumn(ColumnDataType = "bit", ColumnDescription = "是否删除", IsNullable = false)] + public bool IsDeleted { get; set; } /// /// 删除时间 @@ -58,52 +59,31 @@ namespace BPA.SaaS.Stock.Api.Entity // { // var userId = App.User?.FindFirst(ClaimConst.CLAINM_USERID)?.Value; // var userName = App.User?.FindFirst(ClaimConst.CLAINM_ACCOUNT)?.Value; - // this.Id = Guid.NewGuid().ToString(); - // this.CreateAt = DateTime.Now; - // if (!string.IsNullOrEmpty(userId)) - // { - // this.CreateBy = userId; - // } - // else - // { - // this.CreateBy = "admin"; - // } + // Id = Guid.NewGuid().ToString(); + // CreateAt = DateTime.Now; + // CreateBy = !string.IsNullOrEmpty(userId) ? userId : "admin"; // } // // /// // /// 修改 // /// - // public void Modify() + // public virtual void Modify() // { // var userId = App.User?.FindFirst(ClaimConst.CLAINM_USERID)?.Value; // var userName = App.User?.FindFirst(ClaimConst.CLAINM_ACCOUNT)?.Value; - // this.UpdateAt = DateTime.Now; - // if (!string.IsNullOrEmpty(userId)) - // { - // this.UpdateBy = userId; - // } - // else - // { - // this.UpdateBy = "admin"; - // } + // UpdateAt = DateTime.Now; + // UpdateBy = !string.IsNullOrEmpty(userId) ? userId : "admin"; // } // // /// // /// 删除 // /// - // public void Delete() + // public virtual void Delete() // { // var userId = App.User?.FindFirst(ClaimConst.CLAINM_USERID)?.Value; // var userName = App.User?.FindFirst(ClaimConst.CLAINM_ACCOUNT)?.Value; - // this.DeleteAt = DateTime.Now; - // if (!string.IsNullOrEmpty(userId)) - // { - // this.DeleteBy = userId; - // } - // else - // { - // this.DeleteBy = "admin"; - // } + // DeleteAt = DateTime.Now; + // DeleteBy = !string.IsNullOrEmpty(userId) ? userId : "admin"; // } } } \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Entity/Entity.cs b/src/BPA.SaaS.Stock.Api.Entity/Entity.cs index 6a53f0f..19d2617 100644 --- a/src/BPA.SaaS.Stock.Api.Entity/Entity.cs +++ b/src/BPA.SaaS.Stock.Api.Entity/Entity.cs @@ -6,14 +6,12 @@ namespace BPA.SaaS.Stock.Api.Entity /// /// 自定义实体基类 /// - public abstract class Entity: BaseOPEntity + public abstract class Entity : BaseOPEntity { /// /// 状态 /// [SugarColumn(ColumnDataType = "int", ColumnDescription = "状态", IsNullable = false)] public EnumCommonStatus Status { get; set; } = EnumCommonStatus.ENABLE; - - } -} +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.IRepository/Auth/IAuthRepository.cs b/src/BPA.SaaS.Stock.Api.IRepository/Auth/IAuthRepository.cs new file mode 100644 index 0000000..eeea2bc --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.IRepository/Auth/IAuthRepository.cs @@ -0,0 +1,6 @@ +namespace BPA.SaaS.Stock.Api.IRepository.Auth; + +public interface IAuthRepository +{ + +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.IRepository/BPA.SaaS.Stock.Api.IRepository.csproj b/src/BPA.SaaS.Stock.Api.IRepository/BPA.SaaS.Stock.Api.IRepository.csproj index 01bbf6e..e566d4c 100644 --- a/src/BPA.SaaS.Stock.Api.IRepository/BPA.SaaS.Stock.Api.IRepository.csproj +++ b/src/BPA.SaaS.Stock.Api.IRepository/BPA.SaaS.Stock.Api.IRepository.csproj @@ -12,4 +12,7 @@ + + + \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.IService/Auth/IAuthService.cs b/src/BPA.SaaS.Stock.Api.IService/Auth/IAuthService.cs new file mode 100644 index 0000000..8ab147a --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.IService/Auth/IAuthService.cs @@ -0,0 +1,28 @@ +using BPA.Component.DTOCommon.BaseDTOs; +using BPA.SaaS.Stock.Api.DTO.Basic.Auth; + +namespace BPA.SaaS.Stock.Api.IService.Auth +{ + public interface IAuthService + { + /// + /// 获取当前登录用户信息 + /// + /// + Task> GetLoginUserAsync(string logintype); + + /// + /// 用户名密码登录 + /// + /// 1平台用户登录,0加盟商登录 + /// + /// + Task> LoginAsync(string LoginType, LoginInput input); + + /// + /// 退出 + /// + /// + BaseResult LogoutAsync(); + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.IService/Cache/ISysCacheService.cs b/src/BPA.SaaS.Stock.Api.IService/Cache/ISysCacheService.cs new file mode 100644 index 0000000..212f341 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.IService/Cache/ISysCacheService.cs @@ -0,0 +1,39 @@ +using BPA.SaaS.Stock.Api.DTO.Basic.Cache; + +namespace BPA.SaaS.Stock.Api.IService.Cache +{ + public interface ISysCacheService + { + Task AddCacheKey(string cacheKey); + + Task DelByPatternAsync(string key); + + Task DelCacheKey(string cacheKey); + + bool Exists(string cacheKey); + + Task> GetAllCacheKeys(); + + Task GetAsync(string cacheKey); + + Task> GetDataScope(long userId); + + Task> GetMenu(long userId, string appCode); + + Task> GetPermission(long userId); + + Task GetStringAsync(string cacheKey); + + Task RemoveAsync(string key); + + Task SetAsync(string cacheKey, object value); + + Task SetDataScope(long userId, List dataScopes); + + Task SetMenu(long userId, string appCode, List menus); + + Task SetPermission(long userId, List permissions); + + Task SetStringAsync(string cacheKey, string value); + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Repository/Auth/AuthRepository.cs b/src/BPA.SaaS.Stock.Api.Repository/Auth/AuthRepository.cs new file mode 100644 index 0000000..c538290 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Repository/Auth/AuthRepository.cs @@ -0,0 +1,12 @@ +using BPA.Component.DbClient.RepositoryModel; +using BPA.SaaS.Stock.Api.Entity.SYS; +using BPA.SaaS.Stock.Api.IRepository.Auth; + +namespace BPA.SaaS.Stock.Api.Repository.Auth; + +public class AuthRepository : BaseDbRepository, IAuthRepository +{ + public AuthRepository(StockDbSugarClient dbClient) : base(dbClient) + { + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Repository/Product/ProductCodeRepository.cs b/src/BPA.SaaS.Stock.Api.Repository/Product/ProductCodeRepository.cs index 5ac5340..a685fac 100644 --- a/src/BPA.SaaS.Stock.Api.Repository/Product/ProductCodeRepository.cs +++ b/src/BPA.SaaS.Stock.Api.Repository/Product/ProductCodeRepository.cs @@ -20,7 +20,7 @@ public class ProductCodeRepository : BaseDbRepository a.IsDeleted == 0) + .Where(a => a.IsDeleted == false) .WhereIF(dto.Code.IsNotEmpty(), code => code.Code == dto.Code) .WhereIF(dto.Id.IsNotEmpty(), code => code.ProductID == dto.Code) // where etc.. diff --git a/src/BPA.SaaS.Stock.Api.Repository/Product/ProductRepository.cs b/src/BPA.SaaS.Stock.Api.Repository/Product/ProductRepository.cs index 7d9f3f8..9e02ce3 100644 --- a/src/BPA.SaaS.Stock.Api.Repository/Product/ProductRepository.cs +++ b/src/BPA.SaaS.Stock.Api.Repository/Product/ProductRepository.cs @@ -20,7 +20,7 @@ public class ProductRepository : BaseDbRepository((a, b) => new JoinQueryInfos(JoinType.Inner, a.Id == b.ProductID)) - .Where((a, b) => a.IsDeleted == 0 && b.IsDeleted == 0) + .Where((a, b) => a.IsDeleted == false && b.IsDeleted == false) .Select((a, b) => new ProductListByCodeQuery { Id = a.Id, @@ -43,14 +43,14 @@ public class ProductRepository : BaseDbRepository((a, b, c) => new JoinQueryInfos(JoinType.Inner, a.TypeID == b.Id, JoinType.Inner, a.StockUint == c.Id)) - .Where((a, b, c) => a.IsDeleted == 0 && b.IsDeleted == 0 && c.IsDeleted == 0) + .Where((a, b, c) => a.IsDeleted == false && b.IsDeleted == false && c.IsDeleted == false) .WhereIF(groupId.IsNotEmpty(), a => a.GroupId == groupId) .WhereIF(dto.Name.IsNotEmpty(), a => a.Batching_Name == dto.Name) .WhereIF(dto.Code.IsNotEmpty(), a => a.Code == dto.Code) - .WhereIF(dto.StockUint.IsNotEmpty(), c => c.Id == dto.StockUint) + .WhereIF(dto.StockUint.IsNotEmpty(), a => a.Id == dto.StockUint) .WhereIF(dto.Specs.IsNotEmpty(), a => a.Specs == dto.Specs) .WhereIF(dto.Aittribute.IsNotEmpty(), a => a.Aittribute.ToString() == dto.Aittribute) - .WhereIF(dto.TypeID.IsNotEmpty(), b => b.TypeID == dto.TypeID) + .WhereIF(dto.TypeID.IsNotEmpty(), a => a.TypeID == dto.TypeID) .WhereIF(dto.Status.HasValue, a => a.Status == dto.Status) .WhereIF(dto.CreateAt.HasValue, a => SqlFunc.DateIsSame(a.CreateAt, Convert.ToDateTime(dto.CreateAt), DateType.Day)) .WhereIF(dto.UpdateAt.HasValue, a => SqlFunc.DateIsSame(a.UpdateAt, Convert.ToDateTime(dto.UpdateAt), DateType.Day)) @@ -69,7 +69,7 @@ public class ProductRepository : BaseDbRepository().Where(x => x.IsDeleted == 0 && x.ProductID == dto.Id).ToListAsync(); + var productCode = await DbClient.Queryable().Where(x => x.IsDeleted == false && x.ProductID == dto.Id).ToListAsync(); if (productCode.Any()) { foreach (var x in productCode) { - x.IsDeleted = 1; + x.IsDeleted = true; x.DeleteAt = DateTime.Now; x.DeleteBy = ""; //TODO: } @@ -149,18 +149,18 @@ public class ProductRepository : BaseDbRepository().In(ids).ToListAsync(); bpaBatching.ForEach(x => { - x.IsDeleted = 1; + x.IsDeleted = true; x.DeleteAt = DateTime.Now; x.DeleteBy = ""; //TODO }); await DbClient.Updateable(bpaBatching).ExecuteCommandAsync(); - var productCode = await DbClient.Queryable().Where(x => x.IsDeleted == 0 && ids.Contains(x.ProductID)).ToListAsync(); + var productCode = await DbClient.Queryable().Where(x => x.IsDeleted == false && ids.Contains(x.ProductID)).ToListAsync(); if (productCode.Any()) { productCode.ForEach(x => { - x.IsDeleted = 1; + x.IsDeleted = true; x.DeleteAt = DateTime.Now; x.DeleteBy = ""; //TODO }); @@ -174,7 +174,7 @@ public class ProductRepository : BaseDbRepository() - .Where(x => x.IsDeleted == 0 && x.ProductID == productId) + .Where(x => x.IsDeleted == false && x.ProductID == productId) .Select(x => new ProductDetailedDto { Id = x.Id, @@ -222,7 +222,7 @@ public class ProductRepository : BaseDbRepository(); if (typeValue == "1") { - items = await DbClient.SqlQueryable(sql).Where(it => it.IsDeleted == 0).Select(a => new ProductInfoDto + items = await DbClient.SqlQueryable(sql).Where(it => it.IsDeleted == false).Select(a => new ProductInfoDto { TypeID = a.TypeID, Status = a.Status @@ -230,7 +230,7 @@ public class ProductRepository : BaseDbRepository(sql).Where(it => it.IsDeleted == 0).Select(a => new ProductInfoDto + items = await DbClient.SqlQueryable(sql).Where(it => it.IsDeleted == false).Select(a => new ProductInfoDto { StockUint = a.StockUint, Status = a.Status @@ -256,7 +256,7 @@ public class ProductRepository : BaseDbRepository().Where(it => it.IsDeleted == 0 && it.Id == x.TypeID).ToListAsync(); + itemCls = await DbClient.Queryable().Where(it => it.IsDeleted == false && it.Id == x.TypeID).ToListAsync(); if (itemCls.Count > 0) { itemCls.ForEach(x => @@ -272,7 +272,7 @@ public class ProductRepository : BaseDbRepository().Where(it => it.IsDeleted == 0 && it.Id == x.StockUint).ToListAsync(); + itemUint = await DbClient.Queryable().Where(it => it.IsDeleted == false && it.Id == x.StockUint).ToListAsync(); if (itemUint.Count > 0) { itemUint.ForEach(x => @@ -299,7 +299,7 @@ public class ProductRepository : BaseDbRepository().Where(it => it.IsDeleted == 0 && it.Id == x.TypeID).ToListAsync(); + var itemCls = await DbClient.Queryable().Where(it => it.IsDeleted == false && it.Id == x.TypeID).ToListAsync(); if (itemCls.Count > 0) { itemCls.ForEach(x => @@ -315,7 +315,7 @@ public class ProductRepository : BaseDbRepository().Where(it => it.IsDeleted == 0 && it.Id == x.StockUint).ToListAsync(); + var itemUint = await DbClient.Queryable().Where(it => it.IsDeleted == false && it.Id == x.StockUint).ToListAsync(); if (itemUint.Count > 0) { itemUint.ForEach(x => diff --git a/src/BPA.SaaS.Stock.Api.Service/Auth/AuthService.cs b/src/BPA.SaaS.Stock.Api.Service/Auth/AuthService.cs new file mode 100644 index 0000000..93d2183 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Service/Auth/AuthService.cs @@ -0,0 +1,227 @@ +using BPA.Common.Enums.CRM; +using BPA.Component.DTOCommon.BaseDTOs; +using BPA.Component.Extensions; +using BPA.SaaS.Stock.Api.DTO; +using BPA.SaaS.Stock.Api.DTO.Basic.Auth; +using BPA.SaaS.Stock.Api.DTO.Basic.Claim; +using BPA.SaaS.Stock.Api.Entity.SYS; +using BPA.SaaS.Stock.Api.IService.Auth; +using BPA.SaaS.Stock.Api.Repository.Auth; +using BPA.SaaS.Stock.Api.Service.SimpleJWT; +using Microsoft.AspNetCore.Http; + +namespace BPA.SaaS.Stock.Api.Service.Auth +{ + /// + /// 登录授权相关 + /// + /// + public class AuthService : IAuthService + { + private readonly IHttpContextAccessor _httpContextAccessor; + // TODO: 不可以这样访问仓储层 + private readonly AuthRepository _authRepository; + + /// + /// + /// + public AuthService(IHttpContextAccessor httpContextAccessor, AuthRepository authRepository) + { + _httpContextAccessor = httpContextAccessor; + _authRepository = authRepository; + } + + #region 用户 + + /// + /// 用户名密码登录 + /// + /// 1平台用户登录,0加盟商登录 + /// + /// + public async Task> LoginAsync(string LoginType, LoginInput input) + { + // 获取加密后的密码 + var encryptPassword = input.Password.ToLower().Md5(); + + // 判断用户名和密码是否正确 忽略全局过滤器 + var user = await _authRepository.DbClient.Queryable().Where(u => u.Account.Equals(input.Account) + && u.Password.Equals(encryptPassword) + && u.IsDeleted == false + && u.Status == EnumCommonStatus.ENABLE).FirstAsync(); + _ = user ?? throw new Exception(ErrorCode.D1000.GetDescribe()); + + //获取权限 + var company = await _authRepository.DbClient.Queryable().FirstAsync(x => x.Id == user.GroupId); + var accessToken = JWTEncryption.Encrypt(new Dictionary + { + {ClaimConst.CLAINM_USERID, user.Id}, + {ClaimConst.LoginType, LoginType}, + {ClaimConst.CLAINM_ACCOUNT, user.Account}, + {ClaimConst.CLAINM_NAME, user.Name}, + {ClaimConst.CLAINM_SUPERADMIN, user.Account == "admin" ? "1" : "Customer"}, + {ClaimConst.GroupId, user.GroupId}, + {ClaimConst.OrgId, user.SysOrgId}, + {ClaimConst.SupplyPlatformId, company?.SupplyPlatformId} + }); + + // 生成刷新Token令牌 + var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, 30); + + // 设置刷新Token令牌 + if (_httpContextAccessor.HttpContext != null) _httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken; + var loginOutInfo = new LoginOutInfo + { + userID = user.Id, + token = accessToken + }; + return BaseResult.BuildOK(loginOutInfo); + } + + /// + /// 获取当前登录用户信息 + /// + /// + public async Task> GetLoginUserAsync(string logintype) + { + var userId = _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.CLAINM_USERID)?.Value; + + var user = await _authRepository.DbClient.Queryable().Where(u => u.Id == userId).FirstAsync(); + if (user != null) + { + return BaseResult.BuildOK(new LoginOutput + { + Account = user.Account, + Name = user.Name, + NickName = user.Name, + Avatar = "", + isAdmin = user.Account.Equals("admin"), + Id = user.Id + }); + } + + throw new Exception("用户不存在"); + } + + /// + /// 退出 + /// + /// + public BaseResult LogoutAsync() => BaseResult.BuildOK("OK"); + + // /// + // /// 添加用户 + // /// + // /// + // /// + // public bool AddUsers(LoginInput input) + // { + // var flg = db.Queryable().First(t => t.Account == input.Account); + // if (flg != null) + // throw Oops.Oh("当前用户已存在请勿重复添加"); + // BPA_Users users = new BPA_Users() + // { + // Id = Guid.NewGuid().ToString(), + // Account = input.Account, + // CreateAt = DateTime.Now, + // // Name = input.Name, + // Password = MD5Encryption.Encrypt(input.Password), + // //RoleId = input.RoleId, + // Status = CommonStatus.ENABLE, + // CreateBy = App.User.FindFirst(ClaimConst.CLAINM_NAME).Value + // }; + // return db.Insertable(users).ExecuteCommand() > 0; + // } + // + // /// + // /// 重置密码 + // /// + // /// + // /// + // public bool RestorePwd(string Id) + // { + // var Acc = App.User?.FindFirst(ClaimConst.CLAINM_ACCOUNT).Value; + // if (string.IsNullOrEmpty(Acc)) + // throw Oops.Oh("未登录"); + // + // var md5 = MD5Encryption.Encrypt("123456"); + // return db.Updateable(t => t.Password == md5).Where(t => t.Id == Id).ExecuteCommandHasChange(); + // } + // + // /// + // /// 用户页面分页 + // /// + // /// + // /// + // public PageUtil UserPage(LoginPageInput input) + // { + // int total = 0; + // var data = db.Queryable((t, x) => new JoinQueryInfos(JoinType.Left, t.Id == x.Id)) + // .Where(t => t.Account.ToLower() != "admin") + // .WhereIF(!string.IsNullOrEmpty(input.Name), t => t.Name.Contains(input.Name)) + // .Select((t, x) => new LoginOutput + // { + // Id = t.Id, + // Account = t.Account, + // Name = t.Name, + // Stutas = (int) t.Status, + // RoleName = x.Name + // }).ToPageList(input.PageIndex, input.PageSize, ref total); + // return new PageUtil + // { + // Data = data, + // Total = total + // }; + // } + // + // /// + // /// 修改用户密码 + // /// + // /// + // /// + // public bool AlterPwd(UserInputDTO input) + // { + // var Old = MD5Encryption.Encrypt(input.OldPwd); + // var New = MD5Encryption.Encrypt(input.NewPwd); + // BPA_Users User = db.Queryable().Where(t => t.Id == input.Id && t.Password == Old).First(); + // if (User == null) throw Oops.Oh("旧密码错误"); + // return db.Updateable(t => t.Password == New).Where(t => t.Id == input.Id).ExecuteCommandHasChange(); + // } + // + // /// + // /// 启用禁用 + // /// + // /// + // /// + // /// + // public bool AlterUserStatus(string Id, bool type) + // { + // return db.Updateable().SetColumnsIF(type == true, t => t.Status == CommonStatus.ENABLE) + // .SetColumnsIF(type == false, t => t.Status == CommonStatus.DISABLE) + // .Where(t => t.Id == Id).ExecuteCommandHasChange(); + // } + // + // /// + // /// 删除用户 + // /// + // /// + // /// + // public bool RemovUser(string Id) + // { + // return db.Deleteable(t => t.Id == Id).ExecuteCommandHasChange(); + // } + // + // /// + // /// 变更用户权限 + // /// + // /// + // /// + // /// + // public bool AlterUserRole(string Id, string RoleId) + // { + // return db.Updateable().SetColumns(t => t.Id == RoleId).Where(t => t.Id == Id).ExecuteCommandHasChange(); + // } + + #endregion + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Service/BPA.SaaS.Stock.Api.Service.csproj b/src/BPA.SaaS.Stock.Api.Service/BPA.SaaS.Stock.Api.Service.csproj index 6d5e75d..b138df9 100644 --- a/src/BPA.SaaS.Stock.Api.Service/BPA.SaaS.Stock.Api.Service.csproj +++ b/src/BPA.SaaS.Stock.Api.Service/BPA.SaaS.Stock.Api.Service.csproj @@ -13,5 +13,6 @@ + \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Service/Cache/SysCacheService.cs b/src/BPA.SaaS.Stock.Api.Service/Cache/SysCacheService.cs new file mode 100644 index 0000000..0050736 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Service/Cache/SysCacheService.cs @@ -0,0 +1,229 @@ +using System.Text; +using BPA.Component.Extensions; +using BPA.SaaS.Stock.Api.DTO.Basic.Cache; +using BPA.SaaS.Stock.Api.DTO.Basic.Claim; +using BPA.SaaS.Stock.Api.IService.Cache; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; + +namespace BPA.SaaS.Stock.Api.Service.Cache +{ + /// + /// 系统缓存服务 + /// + public class SysCacheService : ISysCacheService + { + private readonly IDistributedCache _cache; + + public SysCacheService(IDistributedCache cache) + { + _cache = cache; + } + + /// + /// 获取数据范围缓存(机构Id集合) + /// + /// + /// + public async Task> GetDataScope(long userId) + { + var cacheKey = CommonConst.CACHE_KEY_DATASCOPE + $"{userId}"; + var res = await _cache.GetStringAsync(cacheKey); + return string.IsNullOrWhiteSpace(res) ? null : res.JsonToObj>(); + } + + /// + /// 缓存数据范围(机构Id集合) + /// + /// + /// + /// + public async Task SetDataScope(long userId, List dataScopes) + { + var cacheKey = CommonConst.CACHE_KEY_DATASCOPE + $"{userId}"; + await _cache.SetStringAsync(cacheKey, dataScopes.ToJson()); + await AddCacheKey(cacheKey); + } + + /// + /// 获取菜单缓存 + /// + /// + /// + /// + public async Task> GetMenu(long userId, string appCode) + { + var cacheKey = CommonConst.CACHE_KEY_MENU + $"{userId}-{appCode}"; + var res = await _cache.GetStringAsync(cacheKey); + return string.IsNullOrWhiteSpace(res) ? null : res.JsonToObj>(); + } + + /// + /// 缓存菜单 + /// + /// + /// + /// + /// + public async Task SetMenu(long userId, string appCode, List menus) + { + var cacheKey = CommonConst.CACHE_KEY_MENU + $"{userId}-{appCode}"; + await _cache.SetStringAsync(cacheKey, menus.ToJson()); + + await AddCacheKey(cacheKey); + } + + /// + /// 获取权限缓存(按钮) + /// + /// + /// + public async Task> GetPermission(long userId) + { + var cacheKey = CommonConst.CACHE_KEY_PERMISSION + $"{userId}"; + var res = await _cache.GetStringAsync(cacheKey); + return string.IsNullOrWhiteSpace(res) ? null : res.JsonToObj>(); + } + + /// + /// 缓存权限 + /// + /// + /// + /// + public async Task SetPermission(long userId, List permissions) + { + var cacheKey = CommonConst.CACHE_KEY_PERMISSION + $"{userId}"; + await _cache.SetStringAsync(cacheKey, permissions.ToJson()); + + await AddCacheKey(cacheKey); + } + + /// + /// 获取所有缓存关键字 + /// + /// + [HttpGet("/api/sysCache/keyList")] + public async Task> GetAllCacheKeys() + { + var res = await _cache.GetStringAsync(CommonConst.CACHE_KEY_ALL); + return string.IsNullOrWhiteSpace(res) ? null : res.JsonToObj>(); + } + + /// + /// 删除指定关键字缓存 + /// + /// + /// + [HttpGet("/api/sysCache/remove")] + public async Task RemoveAsync(string key) + { + await _cache.RemoveAsync(key); + + await DelCacheKey(key); + } + + /// + /// 删除某特征关键字缓存 + /// + /// + /// + public async Task DelByPatternAsync(string key) + { + var allkeys = await GetAllCacheKeys(); + var delAllkeys = allkeys.Where(u => u.Contains(key)).ToList(); + + // 删除相应的缓存 + delAllkeys.ForEach(u => { _cache.Remove(u); }); + + // 更新所有缓存键 + allkeys = allkeys.Where(u => !u.Contains(key)).ToList(); + await _cache.SetStringAsync(CommonConst.CACHE_KEY_ALL, allkeys.ToJson()); + } + + /// + /// 设置缓存 + /// + /// + /// + /// + public async Task SetAsync(string cacheKey, object value) + { + await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(value.ToJson())); + + await AddCacheKey(cacheKey); + } + + /// + /// 设置缓存 + /// + /// + /// + /// + public async Task SetStringAsync(string cacheKey, string value) + { + await _cache.SetStringAsync(cacheKey, value); + + await AddCacheKey(cacheKey); + } + + /// + /// 获取缓存 + /// + /// + /// + [HttpGet("sysCache/detail")] + public async Task GetStringAsync(string cacheKey) + { + return await _cache.GetStringAsync(cacheKey); + } + + /// + /// 获取缓存 + /// + /// + /// + /// + public async Task GetAsync(string cacheKey) + { + var res = await _cache.GetAsync(cacheKey); + return res == null ? default : Encoding.UTF8.GetString(res).JsonToObj(); + } + + /// + /// 检查给定 key 是否存在 + /// + /// 键 + /// + public bool Exists(string cacheKey) + { + return _cache.Equals(cacheKey); + } + + /// + /// 增加缓存Key + /// + /// + /// + public async Task AddCacheKey(string cacheKey) + { + var res = await _cache.GetStringAsync(CommonConst.CACHE_KEY_ALL); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : res.JsonToObj>(); + allkeys.Add(cacheKey); + await _cache.SetStringAsync(CommonConst.CACHE_KEY_ALL, allkeys.ToJson()); + } + + /// + /// + /// + /// + /// + public async Task DelCacheKey(string cacheKey) + { + var res = await _cache.GetStringAsync(CommonConst.CACHE_KEY_ALL); + var allkeys = string.IsNullOrWhiteSpace(res) ? new List() : res.JsonToObj>(); + allkeys.Remove(cacheKey); + await _cache.SetStringAsync(CommonConst.CACHE_KEY_ALL, allkeys.ToJson()); + } + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTEncryption.cs b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTEncryption.cs new file mode 100644 index 0000000..1f9703b --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTEncryption.cs @@ -0,0 +1,455 @@ +using System.Reflection; +using System.Runtime.Loader; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace BPA.SaaS.Stock.Api.Service.SimpleJWT; + +/// +/// JWT 加解密 +/// +public class JWTEncryption +{ + /// + /// 生成 Token + /// + /// + /// 过期时间(分钟) + /// + public static string Encrypt(IDictionary payload, long? expiredTime = null) + { + var (Payload, JWTSettings) = CombinePayload(payload, expiredTime); + return Encrypt(JWTSettings.IssuerSigningKey, Payload, JWTSettings.Algorithm); + } + + /// + /// 生成 Token + /// + /// + /// + /// + /// + public static string Encrypt(string issuerSigningKey, IDictionary payload, string algorithm = SecurityAlgorithms.HmacSha256) + { + return Encrypt(issuerSigningKey, JsonSerializer.Serialize(payload), algorithm); + } + + /// + /// 生成 Token + /// + /// + /// + /// + /// + public static string Encrypt(string issuerSigningKey, string payload, string algorithm = SecurityAlgorithms.HmacSha256) + { + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(issuerSigningKey)); + var credentials = new SigningCredentials(securityKey, algorithm); + + var tokenHandler = new JsonWebTokenHandler(); + return tokenHandler.CreateToken(payload, credentials); + } + + /// + /// 生成刷新 Token + /// + /// + /// 刷新 Token 有效期(分钟) + /// + public static string GenerateRefreshToken(string accessToken, int expiredTime = 43200) + { + // 分割Token + var tokenParagraphs = accessToken.Split('.', StringSplitOptions.RemoveEmptyEntries); + + var s = RandomNumberGenerator.GetInt32(10, tokenParagraphs[1].Length / 2 + 2); + var l = RandomNumberGenerator.GetInt32(3, 13); + + var payload = new Dictionary + { + {"f", tokenParagraphs[0]}, + {"e", tokenParagraphs[2]}, + {"s", s}, + {"l", l}, + {"k", tokenParagraphs[1].Substring(s, l)} + }; + + return Encrypt(payload, expiredTime); + } + + /// + /// 通过过期Token 和 刷新Token 换取新的 Token + /// + /// + /// + /// 过期时间(分钟) + /// 刷新token容差值,秒做单位 + /// + public static string Exchange(string expiredToken, string refreshToken, long? expiredTime = null, long clockSkew = 5) + { + // 交换刷新Token 必须原Token 已过期 + var (_isValid, _, _) = Validate(expiredToken); + if (_isValid) return default; + + // 判断刷新Token 是否过期 + var (isValid, refreshTokenObj, _) = Validate(refreshToken); + if (!isValid) return default; + + // 解析 HttpContext + var httpContext = GetCurrentHttpContext(); + + // 判断这个刷新Token 是否已刷新过 + var blacklistRefreshKey = "BLACKLIST_REFRESH_TOKEN:" + refreshToken; + var distributedCache = httpContext?.RequestServices?.GetService(); + + // 处理token并发容错问题 + var nowTime = DateTimeOffset.UtcNow; + var cachedValue = distributedCache?.GetString(blacklistRefreshKey); + var isRefresh = !string.IsNullOrWhiteSpace(cachedValue); // 判断是否刷新过 + if (isRefresh) + { + var refreshTime = new DateTimeOffset(long.Parse(cachedValue), TimeSpan.Zero); + // 处理并发时容差值 + if ((nowTime - refreshTime).TotalSeconds > clockSkew) return default; + } + + // 分割过期Token + var tokenParagraphs = expiredToken.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (tokenParagraphs.Length < 3) return default; + + // 判断各个部分是否匹配 + if (!refreshTokenObj.GetPayloadValue("f").Equals(tokenParagraphs[0])) return default; + if (!refreshTokenObj.GetPayloadValue("e").Equals(tokenParagraphs[2])) return default; + if (!tokenParagraphs[1].Substring(refreshTokenObj.GetPayloadValue("s"), refreshTokenObj.GetPayloadValue("l")) + .Equals(refreshTokenObj.GetPayloadValue("k"))) return default; + + // 获取过期 Token 的存储信息 + var oldToken = ReadJwtToken(expiredToken); + var payload = oldToken.Claims.Where(u => !StationaryClaimTypes.Contains(u.Type)) + .ToDictionary(u => u.Type, u => (object) u.Value, new MultiClaimsDictionaryComparer()); + + // 交换成功后登记刷新Token,标记失效 + if (!isRefresh) + { + distributedCache?.SetString(blacklistRefreshKey, nowTime.Ticks.ToString(), new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(refreshTokenObj.GetPayloadValue(JwtRegisteredClaimNames.Exp)) + }); + } + + return Encrypt(payload, expiredTime); + } + + /// + /// 自动刷新 Token 信息 + /// + /// + /// + /// 新 Token 过期时间(分钟) + /// 新刷新 Token 有效期(分钟) + /// + /// + /// + public static bool AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, + string tokenPrefix = "Bearer ", long clockSkew = 5) + { + // 如果验证有效,则跳过刷新 + if (context.User.Identity.IsAuthenticated) return true; + + // 判断是否含有匿名特性 + if (httpContext.GetEndpoint()?.Metadata?.GetMetadata() != null) return true; + + // 获取过期Token 和 刷新Token + var expiredToken = GetJwtBearerToken(httpContext, tokenPrefix: tokenPrefix); + var refreshToken = GetJwtBearerToken(httpContext, "X-Authorization", tokenPrefix: tokenPrefix); + if (string.IsNullOrWhiteSpace(expiredToken) || string.IsNullOrWhiteSpace(refreshToken)) return false; + + // 交换新的 Token + var accessToken = Exchange(expiredToken, refreshToken, expiredTime, clockSkew); + if (string.IsNullOrWhiteSpace(accessToken)) return false; + + // 读取新的 Token Clamis + var claims = ReadJwtToken(accessToken)?.Claims; + if (claims == null) return false; + + // 创建身份信息 + var claimIdentity = new ClaimsIdentity("AuthenticationTypes.Federation"); + claimIdentity.AddClaims(claims); + var claimsPrincipal = new ClaimsPrincipal(claimIdentity); + + // 设置 HttpContext.User 并登录 + httpContext.User = claimsPrincipal; + httpContext.SignInAsync(claimsPrincipal); + + string accessTokenKey = "access-token", xAccessTokenKey = "x-access-token", accessControlExposeKey = "Access-Control-Expose-Headers"; + + // 返回新的 Token + httpContext.Response.Headers[accessTokenKey] = accessToken; + // 返回新的 刷新Token + httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); + + // 处理 axios 问题 + httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs); + httpContext.Response.Headers[accessControlExposeKey] = string.Join(',', StringValues.Concat(acehs, new StringValues(new[] {accessTokenKey, xAccessTokenKey})).Distinct()); + + return true; + } + + /// + /// 验证 Token + /// + /// + /// + public static (bool IsValid, JsonWebToken Token, TokenValidationResult validationResult) Validate(string accessToken) + { + var jwtSettings = GetJWTSettings(); + if (jwtSettings == null) return (false, default, default); + + // 加密Key + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey)); + var creds = new SigningCredentials(key, jwtSettings.Algorithm); + + // 创建Token验证参数 + var tokenValidationParameters = CreateTokenValidationParameters(jwtSettings); + if (tokenValidationParameters.IssuerSigningKey == null) tokenValidationParameters.IssuerSigningKey = creds.Key; + + // 验证 Token + var tokenHandler = new JsonWebTokenHandler(); + try + { + var tokenValidationResult = tokenHandler.ValidateToken(accessToken, tokenValidationParameters); + if (!tokenValidationResult.IsValid) return (false, null, tokenValidationResult); + + var jsonWebToken = tokenValidationResult.SecurityToken as JsonWebToken; + return (true, jsonWebToken, tokenValidationResult); + } + catch + { + return (false, default, default); + } + } + + /// + /// 验证 Token + /// + /// + /// + /// + /// + /// + public static bool ValidateJwtBearerToken(DefaultHttpContext httpContext, out JsonWebToken token, string headerKey = "Authorization", string tokenPrefix = "Bearer ") + { + // 获取 token + var accessToken = GetJwtBearerToken(httpContext, headerKey, tokenPrefix); + if (string.IsNullOrWhiteSpace(accessToken)) + { + token = null; + return false; + } + + // 验证token + var (IsValid, Token, _) = Validate(accessToken); + token = IsValid ? Token : null; + + return IsValid; + } + + /// + /// 读取 Token,不含验证 + /// + /// + /// + public static JsonWebToken ReadJwtToken(string accessToken) + { + var tokenHandler = new JsonWebTokenHandler(); + if (tokenHandler.CanReadToken(accessToken)) + { + return tokenHandler.ReadJsonWebToken(accessToken); + } + + return default; + } + + /// + /// 获取 JWT Bearer Token + /// + /// + /// + /// + /// + public static string GetJwtBearerToken(DefaultHttpContext httpContext, string headerKey = "Authorization", string tokenPrefix = "Bearer ") + { + // 判断请求报文头中是否有 "Authorization" 报文头 + var bearerToken = httpContext.Request.Headers[headerKey].ToString(); + if (string.IsNullOrWhiteSpace(bearerToken)) return default; + + var prefixLenght = tokenPrefix.Length; + return bearerToken.StartsWith(tokenPrefix, true, null) && bearerToken.Length > prefixLenght ? bearerToken[prefixLenght..] : default; + } + + /// + /// 获取 JWT 配置 + /// + /// + public static JWTSettingsOptions GetJWTSettings() + { + return FrameworkApp.GetMethod("GetOptions").MakeGenericMethod(typeof(JWTSettingsOptions)).Invoke(null, new object[] {null}) as JWTSettingsOptions ?? + SetDefaultJwtSettings(new JWTSettingsOptions()); + } + + /// + /// 生成Token验证参数 + /// + /// + /// + public static TokenValidationParameters CreateTokenValidationParameters(JWTSettingsOptions jwtSettings) + { + return new TokenValidationParameters + { + // 验证签发方密钥 + ValidateIssuerSigningKey = jwtSettings.ValidateIssuerSigningKey.Value, + // 签发方密钥 + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey)), + // 验证签发方 + ValidateIssuer = jwtSettings.ValidateIssuer.Value, + // 设置签发方 + ValidIssuer = jwtSettings.ValidIssuer, + // 验证签收方 + ValidateAudience = jwtSettings.ValidateAudience.Value, + // 设置接收方 + ValidAudience = jwtSettings.ValidAudience, + // 验证生存期 + ValidateLifetime = jwtSettings.ValidateLifetime.Value, + // 过期时间容错值 + ClockSkew = TimeSpan.FromSeconds(jwtSettings.ClockSkew.Value), + }; + } + + /// + /// 组合 Claims 负荷 + /// + /// + /// 过期时间,单位:分钟 + /// + private static (IDictionary Payload, JWTSettingsOptions JWTSettings) CombinePayload(IDictionary payload, long? expiredTime = null) + { + var jwtSettings = GetJWTSettings(); + var datetimeOffset = DateTimeOffset.UtcNow; + + if (!payload.ContainsKey(JwtRegisteredClaimNames.Iat)) + { + payload.Add(JwtRegisteredClaimNames.Iat, datetimeOffset.ToUnixTimeSeconds()); + } + + if (!payload.ContainsKey(JwtRegisteredClaimNames.Nbf)) + { + payload.Add(JwtRegisteredClaimNames.Nbf, datetimeOffset.ToUnixTimeSeconds()); + } + + if (!payload.ContainsKey(JwtRegisteredClaimNames.Exp)) + { + var minute = expiredTime ?? jwtSettings?.ExpiredTime ?? 20; + payload.Add(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(minute).ToUnixTimeSeconds()); + } + + if (!payload.ContainsKey(JwtRegisteredClaimNames.Iss)) + { + payload.Add(JwtRegisteredClaimNames.Iss, jwtSettings?.ValidIssuer); + } + + if (!payload.ContainsKey(JwtRegisteredClaimNames.Aud)) + { + payload.Add(JwtRegisteredClaimNames.Aud, jwtSettings?.ValidAudience); + } + + return (payload, jwtSettings); + } + + /// + /// 设置默认 Jwt 配置 + /// + /// + /// + internal static JWTSettingsOptions SetDefaultJwtSettings(JWTSettingsOptions options) + { + options.ValidateIssuerSigningKey ??= true; + if (options.ValidateIssuerSigningKey == true) + { + options.IssuerSigningKey ??= "U2FsdGVkX1+6H3D8Q//yQMhInzTdRZI9DbUGetbyaag="; + } + + options.ValidateIssuer ??= true; + if (options.ValidateIssuer == true) + { + options.ValidIssuer ??= "dotnetchina"; + } + + options.ValidateAudience ??= true; + if (options.ValidateAudience == true) + { + options.ValidAudience ??= "powerby Furion"; + } + + options.ValidateLifetime ??= true; + if (options.ValidateLifetime == true) + { + options.ClockSkew ??= 10; + } + + options.ExpiredTime ??= 20; + options.Algorithm ??= SecurityAlgorithms.HmacSha256; + + return options; + } + + /// + /// 获取当前的 HttpContext + /// + /// + private static HttpContext GetCurrentHttpContext() + { + return FrameworkApp.GetProperty("HttpContext").GetValue(null) as HttpContext; + } + + /// + /// 固定的 Claim 类型 + /// + private static readonly string[] StationaryClaimTypes = new[] + {JwtRegisteredClaimNames.Iat, JwtRegisteredClaimNames.Nbf, JwtRegisteredClaimNames.Exp, JwtRegisteredClaimNames.Iss, JwtRegisteredClaimNames.Aud}; + + /// + /// 框架 App 静态类 + /// + internal static Type FrameworkApp { get; set; } + + /// + /// 获取框架上下文 + /// + /// + internal static Assembly GetFrameworkContext(Assembly callAssembly) + { + if (FrameworkApp != null) return FrameworkApp.Assembly; + + // 获取 Furion 程序集名称 + var furionAssemblyName = callAssembly.GetReferencedAssemblies() + .FirstOrDefault(u => u.Name == "Furion" || u.Name == "Furion.Pure") + ?? throw new InvalidOperationException("No `Furion` assembly installed in the current project was detected."); + + // 加载 Furion 程序集 + var furionAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(furionAssemblyName); + + // 获取 Furion.App 静态类 + FrameworkApp = furionAssembly.GetType("Furion.App"); + + return furionAssembly; + } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTSettingsOptions.cs b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTSettingsOptions.cs new file mode 100644 index 0000000..40578bc --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/JWTSettingsOptions.cs @@ -0,0 +1,34 @@ +namespace BPA.SaaS.Stock.Api.Service; + +public class JWTSettingsOptions +{ + /// 验证签发方密钥 + public bool? ValidateIssuerSigningKey { get; set; } + + /// 签发方密钥 + public string IssuerSigningKey { get; set; } + + /// 验证签发方 + public bool? ValidateIssuer { get; set; } + + /// 签发方 + public string ValidIssuer { get; set; } + + /// 验证签收方 + public bool? ValidateAudience { get; set; } + + /// 签收方 + public string ValidAudience { get; set; } + + /// 验证生存期 + public bool? ValidateLifetime { get; set; } + + /// 过期时间容错值,解决服务器端时间不同步问题(秒) + public long? ClockSkew { get; set; } + + /// 过期时间(分钟) + public long? ExpiredTime { get; set; } + + /// 加密算法 + public string Algorithm { get; set; } +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/MultiClaimsDictionaryComparer.cs b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/MultiClaimsDictionaryComparer.cs new file mode 100644 index 0000000..ea83e57 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.Service/SimpleJWT/MultiClaimsDictionaryComparer.cs @@ -0,0 +1,15 @@ +namespace BPA.SaaS.Stock.Api.Service.SimpleJWT; + +internal sealed class MultiClaimsDictionaryComparer : IEqualityComparer +{ + /// 设置字符串永不相等 + /// + /// + /// + public bool Equals(string x, string y) => x != y; + + /// 返回字符串 hashCode + /// + /// + public int GetHashCode(string obj) => obj.GetHashCode(); +} \ No newline at end of file diff --git a/src/BPA.SaaS.Stock.Api.WebApi/Controllers/Auth/AuthController.cs b/src/BPA.SaaS.Stock.Api.WebApi/Controllers/Auth/AuthController.cs new file mode 100644 index 0000000..3e26454 --- /dev/null +++ b/src/BPA.SaaS.Stock.Api.WebApi/Controllers/Auth/AuthController.cs @@ -0,0 +1,38 @@ +using BPA.Component.DTOCommon.BaseDTOs; +using BPA.SaaS.Stock.Api.DTO.Basic.Auth; +using BPA.SaaS.Stock.Api.IService.Auth; +using Microsoft.AspNetCore.Mvc; + +namespace BPA.SaaS.Stock.Api.WebApi.Controllers.Auth; + +[ApiController] +[Route("api/[controller]/[action]")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + /// + /// 获取当前登录用户信息 + /// + /// + Task> GetLoginUserAsync(string logintype) => _authService.GetLoginUserAsync(logintype); + + /// + /// 用户名密码登录 + /// + /// 1平台用户登录,0加盟商登录 + /// + /// + Task> LoginAsync(string LoginType, LoginInput input) => _authService.LoginAsync(LoginType, input); + + /// + /// 退出 + /// + /// + BaseResult LogoutAsync() => _authService.LogoutAsync(); +} \ No newline at end of file diff --git a/src/SQLScript/20220418.sql b/src/SQLScript/20220418.sql new file mode 100644 index 0000000..09294d9 --- /dev/null +++ b/src/SQLScript/20220418.sql @@ -0,0 +1,7 @@ +--SqlServer script +--修改所有表中的'IsDeleted'的字段类型为'bit' +alter table BPA_Company alter column IsDeleted bit not null +alter table BPA_Dict_Data alter column IsDeleted bit not null +alter table BPA_Dict_Type alter column IsDeleted bit not null +alter table BPA_Menu alter column IsDeleted bit not null +alter table BPA_SplitPlan_Franchisee alter column IsDeleted bit not null