后端手册
后端手册
配置文件
appsetting.json说明
{
"dbConfigs": [
{
"Conn": "Data Source=LAPTOP-STKF2M8H\\SQLEXPRESS;User ID=admin;Password=admin123;Initial Catalog=ZrAdmin;",
"DbType": 1, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3,PgSql = 4
"ConfigId": "0", //多租户唯一标识
"IsAutoCloseConnection": true
},
{
"Conn": "Data Source=LAPTOP-STKF2M8H\\SQLEXPRESS;User ID=admin;Password=admin123;Initial Catalog=ZrAdmin;",
"DbType": 1,
"ConfigId": "1", //多租户唯一标识(商城数据库)
"IsAutoCloseConnection": true
}
//...下面添加更多的数据库源
],
//代码生成数据库配置
"CodeGenDbConfig": {
//代码生成连接字符串,注意{dbName}为固定格式,不要填写数据库名
"Conn": "Data Source=LAPTOP-STKF2M8H\\SQLEXPRESS;User ID=admin;Password=admin123;Initial Catalog={dbName};",
"DbType": 1,
"IsAutoCloseConnection": true,
"DbName": "ZrAdmin" //代码生成默认连接数据库
},
"urls": "http://localhost:8888", //项目启动url,非必要不修改,(本地开发、控制台启动有效、linux启动)
"corsUrls": ["http://localhost:8887", "http://localhost:8886"], //跨域地址(前端启动项目,前后端分离单独部署需要设置),多个用","隔开
//jwt 授权认证配置
"JwtSettings": {
"Issuer": "https://localhost:8888",
"Audience": "https://localhost:8888",
"SecretKey": "Hello-key-ZRADMIN.NET-20210101",
"Expire": 30, //jwt登录超时时间(分)
"RefreshTokenTime": 5, //token有效期剩多久(分钟)刷新token
"TokenType": "Bearer"
},
"InjectClass": ["ZR.Repository", "ZR.Service", "ZR.Tasks", "ZR.ServiceCore", "ZR.Mall"], //自动注入类
"ShowDbLog": true, //是否打印db日志
"InitDb": false, //是否初始化db
"DemoMode": false, //是否演示模式
//本地资源上传访问配置
"Upload": {
//项目默认上传配置
"UploadUrl": "http://localhost:8888/uploads", //默认存储访问域名,线上地址eg:http://xxx.xxx.cn/prod-api
"localSavePath": "", //如果为空将默认保存到项目根目录wwwroot里面,本地上传文件存储目录eg:/home/website/zradmin/wwwroot
"maxSize": 15, //上传文件大小限制 15M
"notAllowedExt": [".bat", ".exe", ".jar", ".js"]
},
//阿里云存储配置 前端访问地址 /common/UploadFileAliyun
"ALIYUN_OSS": {
"REGIONID": "cn-hangzhou",
"KEY": "XX",
"SECRET": "XX",
"bucketName": "bucketName",
"domainUrl": "http://xxx.xxx.com" //访问资源域名
},
//企业微信通知配置
"WxCorp": {
"AgentID": "",
"CorpID": "",
"CorpSecret": "",
"SendUser": "@all"
},
//微信公众号设置
"WxOpen": {
"AppID": "",
"AppSecret": ""
},
//邮箱配置信息
"MailOptions": [
{
//发件人名称
"FromName": "system",
//发送人邮箱
"FromEmail": "", //eg:xxxx@qq.com
//发送人邮箱密码
"Password": "123456",
//协议
"Smtp": "smtp.qq.com",
"Port": 587,
"Signature": "系统邮件,请勿回复!",
"UseSsl": true
}
],
//redis服务配置
"RedisServer": {
"open": 0, //是否启用redis
"dbCache": false, //数据库是否使用Redis缓存,如果启用open要为1
"Cache": "127.0.0.1:6379,defaultDatabase=0,poolsize=50,ssl=false,writeBuffer=10240,prefix=cache:",
"Session": "127.0.0.1:6379,defaultDatabase=0,poolsize=50,ssl=false,writeBuffer=10240,prefix=session:"
},
//代码生成配置
"CodeGen": {
//是否显示移动端代码生成
"showApp": false,
//自动去除表前缀
"autoPre": true,
//默认生成业务模块名
"moduleName": "business",
"author": "admin",
"tablePrefix": "sys_", //"表前缀(生成类名不会包含表前缀,多个用逗号分隔)",
"vuePath": "" //前端代码存储路径eg:D:\Work\ZRAdmin-Vue3
}
}
codeGen.json代码生成
{
"CodeGen": {
"csharpTypeArr": {
"string": ["varchar", "nvarchar", "text", "longtext"],
"int": ["int", "integer", "smallint", "int4", "int8", "int2"],
"long": ["bigint", "number"],
"float": ["numeric", "real", "float"],
"decimal": ["money", "decimal", "smallmoney"],
"dateTime": ["date", "datetime", "datetime2", "smalldatetime", "timestamp"],
"byte": ["tinyint"],
"bool": ["bit"]
}
}
}
iprate.jsonIP 限流
{
//接口请求限制
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"EndpointWhitelist": ["post:/system/dict/data/types", "*:/msghub/negotiate", "*:/LogOut", "*:/common/uploadfile"],
"QuotaExceededResponse": {
"Content": "{{\"code\":429,\"msg\":\"访问过于频繁,请稍后重试\"}}",
"ContentType": "application/json",
"StatusCode": 429
},
//通用规则,api规则,结尾一定要带*
"GeneralRules": [
{
"Endpoint": "*:/captchaImage",
//时间段,格式:{数字}{单位};可使用单位:s, m, h, d
"Period": "3s",
"Limit": 5
},
{
"Endpoint": "((post)|(put)):*",
"Period": "3s",
"Limit": 1
}
],
"IpRateLimitPolicies": {}
}
}
upload文件上传
//本地环境配置
"Upload": {
"UploadUrl": "http://xxx.xx.com/uploads", //默认存储访问域名
"localSavePath": "", //如果为空将默认保存到项目根目录wwwroot里面,本地上传文件存储目录
},
//线上环境配置
"Upload": {
"UploadUrl": "http://xxx.xx.com/prod-api/uploads",
"localSavePath": "uploads",
},
提示
建议自己项目新建 如
- 生产环境文件
appsettings.Production.json - 开发环境文件
appsettings.Development.json
有关跨域配置
使用情景:前后端单独部署
//跨域地址,配置前端启动地址多个用","隔开。注意:http://localhost:8887 和 http://localhost:8887/是不同的
"corsUrls": ["http://localhost:8887"]
获取用户信息
在 Controller 或 Service 层中获取
//Controller获取id
var id = HttpContext.GetUId()
//Controller获取用户名
var userName = HttpContext.GetName()
//Service获取用户名
var userName = HttpContextExtension.GetName(App.HttpContext)
//获取登录信息
LoginUser info = Framework.JwtUtil.GetLoginUser(context.HttpContext);
分页实现
- 前端基于 element 封装的分页组件 pagination
- 后端基于 sqlSugar
前端调用实现
- 前端定义分页流程
// 一般在查询参数中定义分页变量
queryParams: {
pageNum: 1,
pageSize: 10
}
// 页面添加分页组件,传入分页变量
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"/>
// 调用后台方法,传入参数 获取结果
listUser(this.queryParams).then(response => {
this.userList = response.data.result;
this.total = response.data.totalNum;
}
)
vue3 需要注意:
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"/>
后台逻辑实现
- 扩展实现
// 查询方法
public List<SysLogininfor> GetLoginLog(PagerInfo pager)
{
int totalCount = 0;
var list = Context.Queryable<SysLogininfor>()
.ToPageList(pager.PageNum, pager.PageSize, ref totalCount);
pager.TotalNum = totalCount;
return list;
}
//前端返回
public IActionResult LoignLogList([FromQuery] PagerInfo pagerInfo)
{
var list = sysLoginService.GetLoginLog(pagerInfo);
return SUCCESS(list.ToPage(pagerInfo));
}
- 直接返回
public IActionResult List([FromQuery] SysPost post, [FromQuery] PagerInfo pagerInfo)
{
//开始拼装查询条件
var predicate = Expressionable.Create<SysPost>();
var list = PostService.GetPages(predicate.ToExpression(), pagerInfo, s => new { s.PostSort });
return SUCCESS(list);
}
异常处理
数据校验
导出 Excel
发送邮件
多数据库源设置
添加数据库配置
修改 appsettings.json 项目已经支持了多库配置,只需要在配置文件中添加对应的数据库既可既可。
"dbConfigs": [
{
"Conn": "Data Source=LAPTOP-STKF2M8H\\SQLEXPRESS;User ID=admin;Password=admin123;Initial Catalog=ZrAdmin;",
"DbType": 1, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3,PgSql = 4
"ConfigId": "0", //多租户唯一标识
"IsAutoCloseConnection": true
},
{
"Conn": "Data Source=LAPTOP-STKF2M8H\\SQLEXPRESS;User ID=admin;Password=admin123;Initial Catalog=myAdmin;",
"DbType": 1, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3,PgSql = 4
"ConfigId": "1", //多租户唯一标识
"IsAutoCloseConnection": true
}
//...下面添加更多的数据库源
],
});
实体类配置
[Tenant(1)]
public class GenDemo
{
//...属性
}
Tenant(1) 表示连接上面的 ConfigId = "1" 的数据库,配置后会自动连接到该数据库
如何使用
使用请前往 Sqlsugar 官网查看 点击查看
雪花 id
使用雪花 id 作为 long 类型后前端输出异常
[JsonConverter(typeof(ValueToStringConverter))]
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
更多使用,请参考sqlsugar 语法
- 雪花 ID 重复 问题 使用雪花 id 需要配置 workid 避免 id 重复(program.cs 已配置),配置请参考文档sqlsugar 雪花 id 配置
JnTemplate 模板引擎使用
我们可以使用模板设置一些邮件模板、来轻松读取绑定数据
首先引用命名空间 using JinianNet.JNTemplate;
读取模板
var tpl = JnHelper.ReadTemplate("wwwroot 下面的文件夹", "文件名");
比如读取项目启动 logo 图标
var contentTpl = JnHelper.ReadTemplate("", "logo.txt");
var content = contentTpl?.Render();
//打印内容
console.log(content)
设置变量
tpl.Set("变量名 1", "值") tpl.Set("user", new { Name = 'Lisa', id: 1, sex: 0})
输出结果
var result = tpl.Render();
Console.WriteLine(result);
调用接口
在特殊方法里面不使用注入形式调用接口方法
public void Test(){
ITaskSchedulerServer _schedulerServer = App.GetRequiredService<ITaskSchedulerServer>();
}
网络请求
系统内置了网络请求模块 ,位置:Infrastructure.Helper.HttpHelper.cs
根据 IP 获取地理位置
使用 var ip_info = IpTool.Search(ip);
提示
注意:ip2region.db 是他的数据库文件,开源库传送门
缓存监控
加密解密工具使用
本项目内置了常用的加密工具类,可以满足你基本的开发需求
- md5
NETCore.Encrypt.EncryptProvider.Md5('123456');
- Base64
NETCore.Encrypt.EncryptProvider.Base64Encrypt('123456');
其他加密方式自己可通过EncryptProvider类库查看
接口限流
系统内置IpRateLimiting组件对接口进行限流处理,默认对(post)|(put)请求方法进行了限流操作,即 3 秒内只能访问一次,超过的请求将会提示访问过于频繁,请稍后重试
看上面 👆配置文件部分
分库分表 🔥
- 自动分表https://www.donet5.com/home/Doc?typeId=1201
- 多租户多数据库https://www.donet5.com/home/Doc?typeId=2246
- Saas 分库https://www.donet5.com/home/Doc?typeId=2403
分库式多租户
1. 方案说明
当前项目采用 DB-per-tenant:
主库:放系统公共数据(配置、权限、任务、日志等)租户库:每个租户独立一套业务库- 路由方式:请求中的
tenantId(Header/Token)决定连接到哪个库
核心代码入口:
- 租户解析:
ZR.ServiceCore/Middleware/TenantResolveMiddleware.cs - 当前租户:
Infrastructure/App/App.cs -> GetCurrentTenantId() - 仓储切库:
ZR.Repository/BaseRepository.cs - 登录绑定租户:
ZR.Admin.WebApi/Controllers/System/SysLoginController.cs
2. 哪些库是公共库
公共库即 ConfigId = 0(MainDb)对应的数据库。
在当前代码里,带 [Tenant("0")] 的实体都走公共库,主要包括:
2.1 系统管理
SysUserSysRoleSysDeptSysPostSysMenuSysRoleMenuSysUserRoleSysUserPostSysConfigSysDictTypeSysDictData
2.2 系统运行/日志
SysLogininforSysOperLogUserOnlineLogSqlDiffLogSysTasksSysTasksLog
2.3 系统能力
SysFileCommonLangEmailTplEmailLogSmsCodeLogGenTableGenTableColumnSysTenant
2.4 内容模块(当前也在公共库)
ArticleArticleCategoryArticleBrowsingLogArticlePraise
3. 租户库
商城模块实体目前带 [Tenant("1")],即默认连到 ConfigId = 1 对应库,例如:
ProductSkusCategoryBrandOMSOrderOMSOrderItemMMSUserAddressProductSpecSpecTemplate
如果要按“每租户独立商城库”,建议把固定 Tenant("1") 改造成按 tenantId 动态切库(通过 BaseRepository 路由),避免所有租户共用同一个商城库。
4. 配置与初始化
4.1 appsettings
ZR.Admin.WebApi/appsettings*.json 需要至少包含:
UseTenant = 1MainDb = "0"dbConfigs中配置:ConfigId = "0"(主库)- 每个租户一个
ConfigId(与tenantId一致)
示例:
- 主库:
ConfigId = 0 - 租户A:
ConfigId = tenant_a - 租户B:
ConfigId = tenant_b
4.2 主库租户表
主库 sys_tenant 维护租户状态:
TenantId(必须与dbConfigs[].ConfigId一致)Status(0正常/1停用)ExpireTime
登录时会校验此表(SysTenantService.CheckTenant)。
5. 请求如何带租户
5.1 登录
POST /login 请求体携带:
tenantIdusernamepassword
登录成功后,tenantId 会写入 JWT。
5.2 业务请求
建议所有请求都携带 Header:
tenantId: xxx
中间件会做一致性检查:
- Header 的
tenantId与 Token 内tenantId不一致 -> 拒绝访问
6. 计划任务(已做租户隔离)
计划任务已按租户增强:
SysTasks增加TenantIdSysTasksLog增加TenantId- 任务创建/查询/更新/删除/启动/停止/执行/导出按租户校验
- Quartz 通过
JobDataMap传递TenantId - 执行时恢复租户上下文(
JobBase)
结论:任务不会只靠“查询过滤”,而是全链路隔离。
7. 日常操作流程(建议)
新增租户
- 新建租户数据库
- 在
dbConfigs增加该租户ConfigId - 在主库
sys_tenant插入租户记录(同名TenantId)
开通账号
- 在主库创建用户/角色/菜单关系
登录验证
- 前端登录时提交
tenantId - 后续请求 Header 保持同一
tenantId
- 前端登录时提交
停用租户
- 将
sys_tenant.status置为1
- 将
8. 排查清单
- 登录提示“租户不存在/停用/过期”
- 检查主库
sys_tenant
- 检查主库
- 访问命中错误库
- 检查
tenantId是否与dbConfigs.ConfigId一致
- 检查
- Token 可用但请求被拒绝
- 检查请求 Header 的
tenantId是否和 Token 一致
- 检查请求 Header 的
- 任务跨租户
- 检查
sys_tasks.tenant_id、JobDataMap是否带租户
- 检查
9. 注意事项
tenantId命名需全局唯一且稳定,不建议用可变展示名- 缓存必须按租户前缀隔离(当前
CacheService已处理) - 新增模块时,先决定是公共库(
Tenant("0"))还是租户库(动态路由) - 如果未来要做租户自助开通,建议再补“租户管理 API + 配置热加载”
如果你需要将其他表也设置成公共表,只需要在后面继承接口IMainDbEntity
例如:
[SugarTable("sys_config", "配置表")]
public class SysConfig : SysBase, IMainDbEntity
{
}
- 前端启用
修改 settings.js 里面的showTenant 设置成true
- 后端启用
修改 appsettings.json
{
"MainDb": "0", // 多租户主库配置ID
"UseTenant": 1 //是否启用多租户 0:不启用 1:启用
}
多库事务
try
{
itenant.BeginTran();
//库1
itenant.GetConnection("1").Insertable(new SysUser { });
//库2
itenant.GetConnection("2").Insertable(new SysUser { });
itenant.CommitTran();
}
catch (Exception ex)
{
itenant.RollbackTran();
Console.WriteLine(ex.Message);
}
关于自动刷新 token
默认jwt token 有效时间为 1440 分钟,可在JwtSettings里面的Expire中设置
自动刷新 token 机制是在 jwt 的有效期快结束后的 5 分钟之内有操作,设置在 JwtAuthMiddleware,你可以根据自己实际情况适当调整刷新 token 的时间
if (!CacheHelper.Exists(cacheKey) && ts.TotalMinutes < 5 && ts.TotalMinutes > 0)
{
}
数据脱敏
该解决方案是根据用户权限进行判断
系统内置 4 种常见的敏感数据,如果自己需要增加请一定要使用p:开头的固定格式,因为会根据这个存入到jwt中
/// <summary>
/// 敏感数据常量字符串
/// </summary>
public static class SensitivePerms
{
/// <summary>
/// 手机号
/// </summary>
public const string ViewRealPhone = "p:vrp";
/// <summary>
/// 身份证
/// </summary>
public const string ViewRealIdCard = "p:vri";
/// <summary>
/// 邮箱
/// </summary>
public const string ViewEmail = "p:ve";
/// <summary>
/// IP地址
/// </summary>
public const string ViewRealIP = "p:vip";
}
- 如何使用
- 后端代码
var list = query.ToPage(pager);
foreach (var item in list.Result)
{
if (!HttpContextExtension.HasSensitivePerm(App.HttpContext, SensitivePerms.ViewRealPhone))
{
item.Phonenumber = MaskUtil.MaskPhone(item.Phonenumber);
}
if (!HttpContextExtension.HasSensitivePerm(App.HttpContext, SensitivePerms.ViewEmail))
{
item.Email = MaskUtil.MaskPhone(item.Email);
}
}
- 前端需要给当前角色授权对应的权限

