后端手册
后端手册
配置文件
各节点含义、默认值与易错项见《配置文件(appsettings.json)》;下面只列完整示例。
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", //系统库固定 "0";租户库填与 sys_tenant.TenantId 一致的值
"IsAutoCloseConnection": true
},
// 单库部署到此为止:系统库 / 业务主库 / 商城库共用 ConfigId = "0",不需要任何额外库配置项。
// 需要多库时继续添加数据源(租户库的 ConfigId 必须与 sys_tenant.TenantId 严格一致):
// {
// "Conn": "Data Source=localhost;Initial Catalog=ZrAdmin_TenantA;...",
// "DbType": 1,
// "ConfigId": "tenant_a",
// "IsAutoCloseConnection": true
// }
],
// 系统库 ConfigId 固定为 "0"(常量 App.SystemDbConfigId,配置项 SystemDb 已废弃,配了会被忽略并告警)。
// 业务主库([Tenant("MainDb")] 实体)默认落 "0",需独立建库时才加 "BizMainDb": "<ConfigId>"(旧键 MainDb 兼容);
// 商城库同理,仅非 SaaS 且独立建库时才加 "MallDb": "<ConfigId>"。详见《SaaS 多租户》。
//代码生成数据库配置
"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
"corsUrls": ["http://localhost:8887", "http://localhost:8886"], //跨域地址(后台前端 + 公网商城 H5 域名),多个用","隔开
//jwt 授权认证配置(SecretKey 禁止明文提交,用环境变量 JwtSettings__SecretKey 或 UserSecrets 覆盖)
"JwtSettings": {
"Issuer": "ZRAdmin.NET",
"Audience": "ZRAdmin.NET",
"SecretKey": "",
"Expire": 1440, //jwt 登录超时时间(分)
"RefreshTokenTime": 30, //剩余有效期少于该分钟数时自动续期
"MaxLifetime": 10080, //会话绝对最长存活时间(分,默认 7 天),续期不能突破;0=不限制
"TokenType": "Bearer"
},
// 双因子认证(TOTP + Passkey)。全部可缺省,缺省即只提供 TOTP。详见《双因子认证(2FA)》
"TwoFactor": {
"Issuer": "ZRAdmin", //认证器 App 显示的发行方
"TrustDeviceDays": 7, //"记住此设备"的信任天数
"WebAuthnEnabled": false, //Passkey 开关:仅当下面 Origins 配好且为 HTTPS 才生效
"WebAuthnOrigins": [], //例:["https://admin.your-domain.com"]
"WebAuthnRpId": "", //默认取 Origins 首项域名,多子域建议配根域名
"WebAuthnRpName": "ZRAdmin",
"WebAuthnChallengeMinutes": 5,
"WebAuthnMaxCredentials": 10
},
// 商城业务参数(赞助模块)。详见《商城系统文档》
"MallSettings": {
"AutoConfirmReceiptDays": 7, //发货后自动确认收货天数
"AfterSaleDays": 15, //确认收货后可申请售后的天数,0=不限
"AllowMockPay": false, //模拟支付,仅联调开启,生产必须 false
"ClosePendingMinutes": 30, //未支付订单超时关单分钟数
"ClosePendingBatchSize": 500, //超时关单每轮处理上限
"WaitDeliveryExportLimit": 50000 //待发货导出行数上限
},
// 多租户配置。详见《SaaS 多租户》
"TenantSettings": {
"UseTenant": 0, //多租户开关 0:关闭, 1/true/on:启用
"RootDomains": ["zradmin.net"], //租户根域名,按子域解析时据此剥离(a.zradmin.net → a)
"ReservedSubDomains": ["www", "admin", "api", "doc"], //保留子域,不解析为租户
"PlatformMenuPermPrefixes": ["system:menu:", "system:tenant:", "system:tenantplanmenu:", "monitor:job:"] //平台专属权限前缀
},
"InjectClass": [], //可选:额外扫描 [AppService] 的插件程序集;ZR.* 业务程序集已自动扫描,一般留空
"Quartz": {
"quartz.serializer.type": "newtonsoft" //binary 依赖的 BinaryFormatter 已被 .NET 9+ 移除;持久化时再加 jobStore.*
},
"ShowDbLog": false, //是否打印db日志
// 模块初始化开关:--initdb 时纳入初始化的模块(默认全部 false;Appsettings 中的 InitDb / InitSeed 已移除)
"ModuleInit": {
"Saas": false, //初始化 SaaS 平台菜单/权限(赞助模块,无独立业务建表)
"Member": false, //初始化会员表 ums_member(与商城同库)
"Mall": false, //初始化商城模块(建表 + 内置定时任务 + 菜单,赞助模块)
"Workflow": false, //初始化工作流模块(建表 + 内置定时任务 + 菜单,赞助模块)
"Pro": false //初始化专业版(AI)菜单与按钮权限(AI 能力本身由 AiOptions.Enable 控制)
},
// 数据库结构迁移(详见《数据库迁移》)
"DbMigration": {
"ReportOnly": false, //true=仅输出差异报告不执行 DDL
"AdditionalTypes": [] //业务模块实体由各自 ITenantModuleInitializer 注册,一般无需声明
},
"DemoMode": false, //是否演示模式
"SingleLogin": false, //是否允许多设备/浏览器登录(跨节点生效需启用 Redis,见《缓存切换》)
"workId": 1, //雪花id唯一数字
"sqlExecutionTime": 5, //Sql执行时间超过多少秒记录日志并警报
//上传配置
"Upload": {
"uploadUrl": "http://localhost:8888", //本地存储资源访问路径,生产请带上访问前缀,如 https://xxx.com/prod-api
"localSavePath": "", //本地存储目录,空=wwwroot
"maxSize": 15, //上传文件大小限制 15M
"requestLimitSize": 50, //请求body大小限制 50M
"notAllowedExt": [".bat", ".exe", ".jar", ".js"],
"defaultStoreType": "Local" //默认存储类型:Local / ALIYUN / S3(调用方未显式指定 storeType 时生效)
},
//默认对象存储提供程序:s3 / aliyun(决定 IOssProvider 默认实现,缺省为 Aliyun)
"OssProvider": "s3",
//S3 兼容对象存储(RustFS / MinIO / Ceph RGW / R2 等)
"S3": {
"displayName": "RustFS",
"endpoint": "http://127.0.0.1:9000",
"accessKey": "",
"secretKey": "",
"bucketName": "",
"domainUrl": "", //自建直连必须带桶名,如 http://122.0.0.1:8000/zradmin
"forcePathStyle": true, //自建 S3 必须 true
"region": "us-east-1",
"maxSize": 100,
"cacheControl": "public, max-age=2592000"
},
//阿里云存储配置
"ALIYUN_OSS": {
"REGIONID": "cn-hangzhou",
"KEY": "XX",
"SECRET": "XX",
"bucketName": "bucketName",
"domainUrl": "http://xxx.xxx.com", //访问资源域名
"maxSize": 100 //上传文件大小限制 100M
},
//企业微信通知配置
"WxCorp": {
"AgentID": "",
"CorpID": "",
"CorpSecret": "",
"SendUser": "@all"
},
//微信公众号设置
"WxOpen": {
"AppID": "",
"AppSecret": ""
},
//短信服务配置(Enabled=false 时为模拟发送仅记录日志)
"SmsOptions": {
"Enabled": false,
"Provider": "None", //None / Aliyun / TencentCloud
"AccessKeyId": "", //阿里云 AccessKeyId / 腾讯云 SecretId
"AccessKeySecret": "",
"SignName": "", //短信签名
"Endpoint": "", //阿里云 dysmsapi.aliyuncs.com / 腾讯云地域 ap-guangzhou
"SdkAppId": "", //腾讯云 SdkAppId
"Templates": {} //业务场景 → 模板编号,如 { "login": "SMS_10001" }
},
//工作流配置(赞助模块):站内信始终发送,邮件/短信只对"行动类"通知额外分发
"Workflow": {
"Notify": {
"EmailEnabled": false, //需先配置下方 MailOptions 发件邮箱
"SmsEnabled": false, //真实发送还需 SmsOptions.Enabled=true 并配置 workflow 模板
"MailFromName": "system" //使用 MailOptions 数组中哪个发件人
}
},
//验证码配置
"CaptchaOptions": {
"IgnoreCase": true //比较时是否忽略大小写
},
//第三方登录(OAuth2):已统一到 OAuth 节,多租户用 "渠道@租户" 覆盖;详见《第三方授权登录》
"OAuth": {
"Enabled": false,
"Clients": {
"WECHAT_MINI": { "ClientId": "", "ClientSecret": "" }
}
},
//微信支付 V3(商城 / 租户续费)。密钥建议用环境变量覆盖,生产勿明文提交
"WechatPay": {
"Enabled": false, //false 时商城前台走模拟支付(还需 MallSettings:AllowMockPay=true)
"MerchantId": "",
"AppId": "",
"MerchantCertificateSerialNumber": "",
"MerchantCertificatePrivateKey": "",
"MerchantV3Key": "",
"WechatPayPublicKeyId": "",
"WechatPayPublicKey": "",
"NotifyUrl": "", //支付结果回调(公网 HTTPS,指向公网商城宿主)
"H5ReturnUrl": "" //H5 场景 Wap 站点地址
},
//邮箱配置信息
"MailOptions": [
{
"FromName": "system",
"FromEmail": "", //eg:xxxx@qq.com
"Password": "",
"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.json代码生成
代码生成配置已从
appsettings.json迁移到独立的codeGen.json(appsettings 中不再保留CodeGen节)。加载顺序:codeGen.json<appsettings.{Environment}.json< UserSecrets < 环境变量(CodeGen__xxx)< 命令行,可用环境变量覆盖。
查看完整配置
{
"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 限流
查看完整配置
{
// 客户端模式:计数按登录用户 user:{id},匿名请求回退按客户端 IP ip:{ip}
"ClientRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"EndpointWhitelist": ["post:/system/dict/data/types", "*:/msghub/negotiate", "*:/LogOut", "*:/common/uploadfile", "*:/VerifyScan"],
"QuotaExceededResponse": {
"Content": "{{\"code\":429,\"msg\":\"访问过于频繁,请稍后重试\"}}",
"ContentType": "application/json",
"StatusCode": 429
},
"GeneralRules": [
{ "Endpoint": "*:/captchaImage", "Period": "3s", "Limit": 5 },
{ "Endpoint": "*:/checkMobile", "Period": "60s", "Limit": 1 },
{ "Endpoint": "((post)|(put)):*", "Period": "5s", "Limit": 1 },
// 直连大模型且计费,按成本单独限流
{ "Endpoint": "*:/workflow/ai/generate", "Period": "60s", "Limit": 3 },
{ "Endpoint": "*:/workflow/ai/approval-suggest", "Period": "20s", "Limit": 5 },
{ "Endpoint": "*:/workflow/ai/flow-analyze", "Period": "60s", "Limit": 5 },
{ "Endpoint": "*:/workflow/ai/match-fill", "Period": "20s", "Limit": 5 },
// 第三方授权入口会写 state 缓存,匿名请求同样限流
{ "Endpoint": "*:/oauth/authorize", "Period": "60s", "Limit": 10 },
{ "Endpoint": "*:/oauth/bind/authorize", "Period": "60s", "Limit": 10 },
{ "Endpoint": "*:/oauth/callback", "Period": "60s", "Limit": 20 }
]
}
}
ai.jsonAI 配置(赞助模块)
AI 配置独立在
ai.json(AiOptions节)与其环境覆盖文件ai.{Environment}.json中,不放在appsettings.json里。加载顺序:ai.json<ai.{Environment}.json< UserSecrets < 环境变量(AiOptions__xxx)< 命令行。供应商密钥请用环境变量覆盖,勿明文提交。字段说明见《AI 使用》。
upload文件上传
//本地环境配置
"Upload": {
"uploadUrl": "http://localhost:8888",
"localSavePath": "",
"defaultStoreType": "Local", //默认存储类型 Local / ALIYUN / S3
},
//线上环境配置(反向代理带访问前缀时,uploadUrl 要一并带上该前缀,且必须以 / 结尾)
"Upload": {
"uploadUrl": "http://xxx.xx.com/prod-api/", // 少了结尾的 / 会把 prod-api 这一段吃掉
"localSavePath": "uploads",
},
- 文件上传与对象存储(本地存储、S3 兼容存储接入、配置与常见坑):文件上传与对象存储
环境配置
建议自己项目新建对应环境的配置文件:
- 生产环境:
appsettings.Production.json - 开发环境:
appsettings.Development.json
有关跨域配置
前后端单独部署时需要配置跨域:
//跨域地址,配置前端启动地址多个用","隔开。注意:http://localhost:8887 和 http://localhost:8887/是不同的
"corsUrls": ["http://localhost:8887"]
获取用户信息
在 Controller 或 Service 层中获取:
// Controller 中获取
long userId = HttpContext.GetUId(); // 用户ID
string userName = HttpContext.GetName(); // 用户名
long deptId = HttpContext.GetDeptId(); // 部门ID
// Service 层获取(通过 App.HttpContext,等价于上面)
string userName = App.HttpContext.GetName();
long userId = App.HttpContext.GetUId();
// 获取完整登录信息(含 Permissions,首次从 Redis 补全并回写 Items)
LoginUser info = HttpContext.GetCurrentUser(); // Controller 内
LoginUser info = App.HttpContext.GetCurrentUser(); // Service 内
//获取当前租户ID
string tenantId = App.GetCurrentTenantId();
//库 ConfigId(注意:MainDbConfigId 是"主租户标识",恒等于系统库 "0",不是配置项 MainDb)
string systemDb = App.SystemDbConfigId; // 系统库,固定 "0"
string bizMainDb = App.BizMainDbConfigId; // 业务主库([Tenant("MainDb")] 实体),未配置回退 "0"
string mallDb = App.MallDbConfigId; // 商城库,未配置回退 "0"
分页实现
- 前端基于 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 LoginLogList([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=localhost;Initial Catalog=ZrAdmin;...",
"DbType": 1, //数据库类型 MySql = 0, SqlServer = 1, Oracle = 3, PgSql = 4
"ConfigId": "0",
"IsAutoCloseConnection": true
},
{
"Conn": "Data Source=localhost;Initial Catalog=myAdmin;...",
"DbType": 1,
"ConfigId": "1",
"IsAutoCloseConnection": true
}
],
实体类配置
[Tenant("1")]
public class GenDemo
{
//...属性
}
Tenant("1") 表示连接 ConfigId = "1" 的数据库,配置后自动路由。特殊写法:
| 写法 | 含义 |
|---|---|
[Tenant("0")] | 固定系统库(等价于实现 IMainDbEntity,SaaS 下依然指向 "0") |
[Tenant("MainDb")] | 业务主库(BizMainDb,未配置回退 "0") |
[Tenant("MallDb")] | 商城库:非 SaaS 下按 MallDb 路由,SaaS 下走当前租户库 |
更多用法:SqlSugar 多租户文档
库角色与路由
| 库角色 | ConfigId | 说明 |
|---|---|---|
| 系统库 | 固定 "0"(不可配置) | 租户/套餐/菜单/字典/任务/文件/AI 日志等系统级数据;配置项 SystemDb 已废弃 |
| 业务主库 | BizMainDb,缺省回退 "0" | 标记 [Tenant("MainDb")] 的跨租户共享业务实体(旧键 MainDb 兼容) |
| 商城库 | MallDb,缺省回退 "0" | 仅非 SaaS 模式下商城实体使用;SaaS 下按租户库路由,该项不生效 |
| 租户库 | 与 sys_tenant.TenantId 严格一致 | 多租户下各租户独立库 |
单库部署时以上库配置项全部可省略(系统库 = 业务主库 = 商城库 =
"0")。完整路由优先级、IMainDbEntity实体清单与[Tenant]在 SaaS / 非 SaaS 下的差异见《SaaS 多租户》。
多库事务
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);
}
雪花 id
使用雪花 id 作为 long 类型后,前端 JS 精度丢失需要做序列化转换:
[JsonConverter(typeof(ValueToStringConverter))]
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
- 雪花 ID 需要配置
workId避免 id 重复,program.cs中已配置 - 更多:SqlSugar 雪花 ID 文档
关于自动刷新 token
三个时间参数配合决定会话长度:
| 配置 | 默认值 | 含义 |
|---|---|---|
JwtSettings.Expire | 1440 分 | 单次签发 token 的有效期 |
JwtSettings.RefreshTokenTime | 30 分 | 续期阈值:剩余有效期 ≤ 该值时自动续期(JwtAuthMiddleware) |
JwtSettings.MaxLifetime | 10080 分(7 天) | 会话绝对上限,从首次登录起算,续期不能突破;0=不限制 |
- 续期通过响应头
X-Refresh-Token下发新 token,并用缓存锁防止并发重复续期; - 续期判定在验签、用户状态、租户状态复核之后:禁用用户 / 停服或到期租户的存量 token 立即失效,不会因续期"复活";
- 超过
MaxLifetime直接返回 401 要求重新登录(会话缓存同时清除)。
详见《认证鉴权流程》。
登录与会话安全
| 能力 | 要点 | 文档 |
|---|---|---|
| 双因子认证 | 密码 / 短信 / 扫码登录统一闸门:第一步返回 203 MFA_REQUIRED + 5 分钟票据,第二步 POST /login/2fa/verify 校验 TOTP 或恢复码后才签发 token;支持可信设备、Passkey(配 TwoFactor:WebAuthnOrigins) | 双因子认证(2FA) |
| 登录失败锁定 | 凭据错误累计达 sys.account.maxRetryCount 后按 lockTime 锁定(短信登录同样计入),解锁提醒 60 分钟限频 | 认证鉴权流程 |
| 单设备登录 | SingleLogin: true 生效;多节点要跨节点生效需启用 Redis(会话落 Session 库) | 缓存切换 |
| 令牌状态复核 | 验签后复核用户与租户状态(60 秒缓存),禁用用户、停服或到期租户的存量 token 立即失效 | 认证鉴权流程 |
| 会话上限 | JwtSettings.MaxLifetime 为绝对上限,续期不可突破 | 见上文「自动刷新 token」 |
数据脱敏
- 后端使用
1. 概述
项目对手机号、邮箱、身份证、IP、昵称等敏感字段提供统一脱敏能力,由两部分组成:
MaskUtil(命名空间ZR.Infrastructure.Helper):纯函数式脱敏工具,输入原值、返回脱敏值。MaskExtensions(命名空间ZR.Infrastructure.Helper):扩展方法,把「权限判断 + 循环/赋值 + 脱敏写回」收口为一行调用,消除各处重复的foreach + if样板代码。
脱敏是否生效取决于当前登录用户是否拥有对应敏感数据查看权限(见 SensitivePerms):无权限才脱敏,有权限返回明文。
2. 敏感权限常量(SensitivePerms)
位于 Infrastructure/Constant/SensitivePerms.cs,命名空间 ZR.Infrastructure.Constant。
| 常量 | 值 | 含义 |
|---|---|---|
ViewRealPhone | p:vrp | 查看真实手机号 |
ViewRealIdCard | p:vri | 查看真实身份证 |
ViewEmail | p:ve | 查看真实邮箱 |
ViewRealIP | p:vip | 查看真实 IP |
权限判断:HttpContextExtension.HasSensitivePerm(App.HttpContext, SensitivePerms.XXX),返回 true 表示拥有权限(即不脱敏)。
3. MaskUtil 脱敏方法
命名空间 ZR.Infrastructure.Helper。均为 public static string,入参为 null/空时原样返回。
| 方法 | 规则 | 示例 |
|---|---|---|
MaskPhone | 保留前 3 后 4,中间 **** | 13812345678 → 138****5678 |
MaskEmail | 保留首字符与完整域名 | zhangsan@example.com → z****@example.com |
MaskIdCard | 保留前 4 后 4,中间 8 位 * | 110101199001011234 → 1101********1234 |
MaskName | 首尾各 1,中间 * | 张三 → 张*;张三丰 → 张*丰 |
MaskIp | IPv4 保留前 2 段,IPv6 保留前 3 段 | 123.45.67.89 → 123.45.*.* |
4. MaskExtensions 扩展方法(推荐用法)
命名空间 ZR.Infrastructure.Helper。把权限判断与脱敏统一收口,避免重复代码,且权限判断只执行一次。
4.1 列表脱敏
list.Result.MaskField(
HttpContextExtension.HasSensitivePerm(App.HttpContext, SensitivePerms.ViewRealIP), // hasPerm:true=不脱敏
it => it.OperIp, // getter:读取字段
(it, v) => it.OperIp = v, // setter:写回字段
MaskUtil.MaskIp); // 脱敏方法
4.2 单个对象脱敏
user.MaskField(
HttpContextExtension.HasSensitivePerm(App.HttpContext, SensitivePerms.ViewRealPhone),
it => it.Phonenumber, (it, v) => it.Phonenumber = v, MaskUtil.MaskPhone);
参数为
true(拥有权限)或对象为null时直接返回,不做任何处理。
5. 业务示例
| 场景 | 位置 |
|---|---|
| 操作日志 IP | SysOperLogService.SelectOperLogList |
| 登录日志 IP | SysLoginService.GetLoginLog |
| 用户列表 手机/邮箱 | SysUserService 列表查询 |
| 单个用户 手机/邮箱 | SysUserService.SelectUserById |
6. 新增一个脱敏字段的步骤
- 在
MaskUtil增加对应脱敏方法(如尚无匹配规则)。 - 在
SensitivePerms增加权限常量(如尚无)。 - 在返回数据处调用
xxx.MaskField(hasPerm, getter, setter, mask)。
注意:邮箱务必使用
MaskUtil.MaskEmail,不要复用MaskUtil.MaskPhone(二者规则不同)。
- 前端:需给当前角色授权对应的敏感权限。

JnTemplate 模板引擎使用
可用于邮件模板等场景,引用命名空间 using JinianNet.JNTemplate;
读取模板
var tpl = JnHelper.ReadTemplate("wwwroot下面的文件夹", "文件名");
比如读取启动 logo:
var contentTpl = JnHelper.ReadTemplate("", "logo.txt");
var content = contentTpl?.Render();
Console.WriteLine(content);
设置变量 & 输出
tpl.Set("user", new { Name = "Lisa", Id = 1, Sex = 0 });
var result = tpl.Render();
Console.WriteLine(result);
调用接口(非注入方式)
在无法使用 DI 注入的特殊场景中获取服务:
public void Test()
{
ITaskSchedulerServer _schedulerServer = App.GetRequiredService<ITaskSchedulerServer>();
}
网络请求
系统内置了网络请求模块,位置:Infrastructure.Helper.HttpHelper.cs
根据 IP 获取地理位置
var ip_info = IpTool.Search(ip);
提示
ip2region.xdb 是其数据库文件,开源库传送门
缓存切换(内存 / Redis)
新功能统一走可插拔缓存抽象 CacheStore(泛型 ICache):RedisServer:open = 1 时自动走 Redis(跨节点生效),否则回退本地内存。
CacheStore.Set("key", value, 30); // 过期时间单位:分钟
var v = CacheStore.Get<MyType>("key");
CacheStore.Remove("key");
// 指定缓存库(Session 库常放会话态数据)
CacheStore.For(CacheBackend.Session).Set("sid", sessionId, 30);
单设备登录要跨节点生效,必须启用 Redis(会话落
Session库)。详见《缓存后端切换说明》。
缓存监控
加密解密工具使用
第三方加密库 NETCore.Encrypt 已移除,改用 BCL 与内置工具类。
MD5(前端密码传输)
using ZR.Common;
SecurityHelper.Md5("123456"); // 32 位十六进制
前端登录前把明文密码 MD5 一次再提交,服务端拿到的就是这个 32 位串。
密码哈希(数据库存储)
落库的密码由 ZR.Common/Security/PasswordHasher.cs 处理,不要自己 MD5 后直接存库:
using ZR.Common.Security;
string stored = PasswordHasher.Hash(md5OfPassword); // PBKDF2$SHA256$100000$salt$key
bool ok = PasswordHasher.Verify(md5OfPassword, stored); // 兼容旧 MD5 值
bool needUpgrade = PasswordHasher.NeedsUpgrade(stored); // 旧值需要升级
- 算法为 PBKDF2-SHA256,10 万次迭代,16 字节盐、32 字节密钥,每次生成的盐不同,同一密码两次哈希结果不同;
- 输入是前端传来的 32 位 MD5(不是明文),内部统一按小写参与计算,避免大小写写法不同生成两套互不兼容的值;
Verify遇到 32 位十六进制的历史值走大小写不敏感比对,老账号无需重置密码;- 登录成功后
SysLoginService会检查NeedsUpgrade,把旧 MD5 值自动重写成 PBKDF2(用户无感知)。
Base64
Convert.ToBase64String(Encoding.UTF8.GetBytes("123456"));
Convert.FromBase64String(base64);
接口限流
系统内置限流组件(客户端模式):登录用户按 user:{id} 计数,匿名请求回退按 ip:{ip}。默认对 (post)|(put) 请求方法做 5 秒内只允许一次的限流,超过返回 429 并提示访问过于频繁,请稍后重试;验证码、短信校验、工作流 AI、第三方授权等接口另有单独规则。
详见上方 👆 配置文件 iprate.json 部分。
