50. 后端常见问题排查速查
后端手册排错FAQ大约 13 分钟约 3906 字
按「现象 → 原因 → 解决」排查。多数问题都能用三个开关定位:Database.json 的 EnableConsoleSql(看 SQL)、Logging.json 的日志级别(看异常)、以及 /api/sysCache/keyList(看缓存)。
| 现象 | 原因 | 解决 |
|---|
| 所有接口 401,登录页反复跳转 | Token 过期且刷新失败。JwtHandler 用 JWTEncryption.AutoRefreshToken 校验,过期时间取 SysConfigService.GetTokenExpire()(默认 20 分钟)与 GetRefreshTokenExpire()(默认 40 分钟) | 确认 SysConfig 里 token 过期配置未被改坏;把 JWT.json 的 ClockSkew 调大(单位秒,默认 60)容错服务器时间差;确保服务器时间同步(NTP) |
| 刚改完密码就被踢下线 | JwtHandler 校验 ClaimConst.TokenVersion 与缓存 sys_user_token:{userId} 是否一致,改密码/重置密码会递增 TokenVersion | 预期行为。若不想踢下线,改密码后不递增 TokenVersion;排查时查缓存 key sys_user_token:{userId} |
| "令牌已失效,请重新登录" | Token 进了黑名单:sysCacheService.ExistKey($"{CacheConst.KeyTokenBlacklist}:{accessToken}")。退出登录、强制下线会加黑名单 | 清黑名单缓存前缀 sys_token_blacklist:;确认退出接口是否被误调 |
| "账号不存在或已被停用" | SysUser.Status == StatusEnum.Disable | 在用户管理里启用账号;或检查是否被密码错误次数锁定(sys_password_error_times: 缓存),用 sysUser/unlockLogin 解锁 |
| "租户已过期,请联系管理员" | SysTenant.ExpirationTime < DateTime.Now,JwtHandler 校验缓存 sys_tenant_hash | 延长租户有效期,或调 /api/sysTenant/cacheTenantAll 刷新租户缓存 |
| 接口 403(有 Token 但无权限) | JwtHandler.CheckAuthorizeAsync:路由不在用户按钮权限集合 userApiList[0] 里,或命中角色接口黑名单 userApiList[1] | 角色管理 → 授权接口,给角色勾上该接口;检查 SysRoleApi(接口黑名单)是否误加;改完清 sys_user_api:{userId} 缓存 |
| 超管能访问,普通账号 403 | 超管在 CheckAuthorizeAsync 开头直接 return true,跳过权限判断 | 用普通账号验证权限;不要拿超管结果当结论 |
| Swagger 登录后立刻掉线 | JwtHandler 失败时会 SignoutToSwagger(),这是设计行为 | 确认 Swagger 分组(Swagger.json 的 DefaultGroupName)与登录的分组一致 |
排查 401 最快的办法:
# 1. 用返回的 token 解出 exp / TenantId / TokenVersion
TOKEN='eyJhbGciOi...'
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python -m json.tool
# 2. 看该用户的 token 版本缓存(Redis 里前缀是 adminnet_)
redis-cli --scan --pattern 'adminnet_sys_user_token:*'
redis-cli GET adminnet_sys_user_token:1300000000001
# 3. 看黑名单
redis-cli --scan --pattern 'adminnet_sys_token_blacklist:*'
| 现象 | 原因 | 解决 |
|---|
| Swagger 里看不到新写的接口 | 类没实现 IDynamicApiController,或没贴 [ApiDescriptionSettings];也可能程序集未被扫描 | 补 IDynamicApiController, ITransient;新程序集要在 Admin.NET.Application/Startup.cs 或插件里注册 |
| Swagger 显示接口但调用 404 | 路由前缀不对。默认前缀在 App.json 的 DynamicApiControllerSettingsOptions.DefaultRoutePrefix(注释里默认是 api),实际路由是 /api/sysXxx/action,大小写是驼峰 | 用 /api/sysCommon/allSysApiList 拿真实路由表,别手拼 |
| 方法名改了但路由没变 | 方法上贴了 [ApiDescriptionSettings(Name = "xxx")],路由以 Name 为准,与方法名无关 | 改 Name 或删掉让它回退到方法名 |
| 生成 XML 注释不显示 | GetAllSysApiList 会读程序集同名 .xml(Path.ChangeExtension(asmLocation, ".xml"))兜底 | 确认 csproj 的 GenerateDocumentationFile=True 且发布包含 xml |
| 接口树不刷新 | GetAllSysApiList 结果缓存到 CacheConst.KeyAllApi(sys_all_api);StartHostedService 只在启动时清一次 | 调 /api/sysCache/delete/sys_all_api,或重启 |
| 移动端接口调不通 | CheckAuthorizeAsync 对 LoginModeEnum.APP 走 GetAppApiList(),只认贴了 [AppApiDescription] 的接口 | 给控制器/方法贴 [AppApiDescription],重建 /api/sysCommon/generateAppApi |
| Swagger 分组列表为空 | Swagger.json 的 GroupOpenApiInfos 没配该分组 | 在 Swagger.json 加分组,/api/sysCommon/swaggerGroups 可查当前分组 |
| 现象 | 原因 | 解决 |
|---|
| 列表查不到刚插入的数据 | 全局过滤器生效:软删除 IDeletedFilter(IsDelete == false)、租户 ITenantIdFilter、机构数据范围 SetOrgEntityFilter | 开 EnableConsoleSql 看生成的 SQL 多了哪些条件;需要跨隔离用 ClearFilter<ITenantIdFilter>() / ClearFilter() / RunWithoutFilter() |
| 租户之间串数据 | ①实体没继承 EntityTenantId(ITenantIdFilter);②后台任务/事件总线里没有 App.User,租户过滤器根本不加;③超管跳过全部过滤器 | 业务表继承 EntityTenantId;后台任务里手工 Where(u => u.TenantId == xxx);验证数据范围必须用普通账号 |
| 超管能看到全部数据,普通用户看不到 | 设计如此:SetDbAop 里 AccountType == SuperAdmin 时直接 return,不加任何过滤器 | 用普通账号测;要在代码里绕过用 IgnoreTenant() |
| 数据范围改了不生效 | 机构过滤器缓存在内存 db:{ConfigId}:orgList:{userId} | 调 SqlSugarFilter.DeleteUserOrgCache(userId, dbConfigId),或重启 |
| 新增/更新没自动填创建人 | Aop.DataExecuting 依赖 App.User;后台任务里 App.User == null,CreateUserId 等字段为空 | 后台任务手工赋值,或用 Scoped.Create 模拟用户上下文 |
| 主键为空导致插入失败 | 主键为 long 且值为 0 时,Aop.DataExecuting 会自动 YitIdHelper.NextId();Guid 主键空值时自动 Guid.NewGuid()。若实体主键不是 long/Guid 则不会自动补 | 主键统一用 long(继承 EntityBaseId),非标类型自己赋值 |
| 过滤器把种子数据也过滤掉了 | 正常:InitTableSeedData 里先 QueryFilter.ClearAndBackup(),写完再 Restore() | 自定义种子逻辑要照抄这个模式 |
| 现象 | 原因 | 解决 |
|---|
| 任务完全不执行 | App.json 的 JobSchedule.Enabled 为 false,或作业/触发器被暂停 | 置为 true;任务调度页执行 startJob / startTrigger,或 startAllJob |
| 任务执行了但没生效 | 作业类未被扫描,或 JobMonitor.OnExecutingAsync 抢不到分布式锁直接 return | 看异常日志;确认 Redis 可用(锁依赖 SysCacheService.BeginCacheLock) |
| 多节点重复执行 | 分布式锁 key 是 dis_lock_job:{jobId},锁竞争失败会跳过。内存缓存模式下各节点锁互不可见,必然重复 | 多节点必须 Cache:CacheType = Redis |
| 上一次没跑完又触发一次 | JobMonitor 只写 {key}:running 标记(60 分钟过期),不阻塞触发 | 业务代码内部自己加幂等(SysCacheService.BeginCacheLock)或缩短执行时间 |
| 触发器记录暴涨 | SysJobTriggerRecord 长期未清理 | 任务调度页 clearJobTriggerRecord,或建定时清理作业(仓库已带 CleanSysLogJob 可参考) |
| 修改了作业代码不生效 | 作业是编译型(SysJobDetail 存的是程序集/类型信息),不是脚本 | 重新编译发布;需要动态任务用 DynamicJobCompiler |
| 现象 | 原因 | 解决 |
|---|
| Redis 挂了,系统还能用但数据不一 | NewLife.Redis 会降级为内存缓存并后台重连,接口不会 500 | 短期可接受;但 Token、在线用户、任务锁、雪花 WorkerId 都会错乱,务必尽快恢复 Redis 或把所有节点停到单节点 |
| 改了配置不生效 | SysConfig 有缓存(前缀 sys_config:),SysConfigService.RemoveConfigCache 要清 4 个 key | 走接口改配置;直接改库后调 /api/sysCache/deleteByPreKey/sys_config: |
| 改了字典不生效 | 字典缓存前缀 sys_dict: | 清 sys_dict: 前缀;前端还要重新拉取(setDictList) |
| 在线用户数不对 | 在线用户是缓存 sys_user_online,StartHostedService 启动时会清掉 | 预期行为(重启即空);实时性靠 SignalR 推送 |
| 缓存 key 清不掉 | SysCacheService 内部会自动拼 CacheOptions.Prefix(默认 adminnet_),你传入的 key 不含前缀 | 传业务 key 即可(如 sys_config:),前缀由框架拼 |
| 分布式锁一直拿不到 | BeginCacheLock(key, msTimeout, msExpire, throwOnFailure),默认等 500ms、锁 10s | 调大 msTimeout;排查是否有任务崩溃未释放锁(等待 msExpire 自动过期) |
常用排查命令:
# 看全部缓存 key(会去掉 adminnet_ 前缀展示)
curl -s -X POST http://localhost:5005/api/sysCache/keyList -H "Authorization: Bearer $TOKEN"
# 按前缀删
curl -s -X POST "http://localhost:5005/api/sysCache/deleteByPreKey/sys_config:" \
-H "Authorization: Bearer $TOKEN"
# 直接看 Redis
redis-cli -n 5 --scan --pattern 'adminnet_sys_*' | head -50
| 现象 | 原因 | 解决 |
|---|
| 新表没建出来 | ①EnableInitTable 为 false;②开了 EnableIncreTable=true 但实体没贴 [IncreTable];③实体贴了 [IgnoreTable] | 生产保持 EnableInitTable=false + EnableIncreTable=true,新实体补 [IncreTable] |
| 表建到了错的库 | 归属由 [SysTable] / [LogTable] / [Tenant(configId)] 决定;都没贴则按"非 Log 非 Tenant"落主库 | 改特性;已建错的表手工 DROP 后重建 |
| 种子数据没生效 | ①EnableInitSeed=false;②开了 EnableIncreSeed=true 但种子类没贴 [IncreSeed];③种子类贴了 [TenantSeed](主库初始化会跳过);④HasData() 返回空 | 逐项核对;启动日志会打印"忽略 xxx 原因: 非SysTable…/没有种子数据" |
| 种子执行了但值没变 | 种子类贴了 [IgnoreUpdateSeed](只插入不更新),或列上贴了 [IgnoreUpdateSeedColumn] | 确认是否期望此行为;需要强制更新则临时移除特性 |
| 客户数据被种子覆盖 | 开了全量种子(EnableIncreSeed=false) | 改回 true;被覆盖的值从 SysLogDiff.BeforeData 里捞回 |
| 升级后菜单乱了 | SysMenuSeedData 的新 Id 与库中已有 Id 撞号,upsert 覆盖了 | 新菜单 Id 用全新雪花 Id;用 /api/sysMenu/list 核对 |
| 启动卡在建表阶段 | 大表加列锁表,或数据库账号无 DDL 权限 | 提升账号权限 / 停机窗口执行 / 改手工 DDL |
| 视图不见了 | InitTable 开头会 DropView,EnableInitView=false 时不重建 | 需要视图就把 EnableInitView 置 true,或手工重建 |
| 差异日志表暴涨 | EnableDiffLog=true 忘记关 | 关掉并清理 SysLogDiff |
| 现象 | 原因 | 解决 |
|---|
| 上传报 404 / 文件不存在 | 上传目录由 Upload.json 的 Path(默认 upload/{yyyy}/{MM}/{dd})决定,相对 wwwroot;发布时该目录未带上 | 确认 wwwroot/upload 存在且有写权限;Docker 要挂 volume |
| 下载 404 | 文件记录在 SysFile 表但磁盘文件被删;或 OSSProvider.Enabled=true 但 OSS 配置不对 | 查 SysFile 的 Url/Path;确认 OSS 的 Endpoint/Bucket/CustomHost |
| 上传报类型不允许 | Upload.ContentType 白名单 | 在白名单里补 MIME |
| 上传报超过大小 | Upload.MaxSize(单位 KB,默认 1048576) | 调大;同时确认 nginx/网关的 client_max_body_size |
| 重复文件上传后被去重 | Upload.EnableMd5=true 开启 MD5 秒传 | 关掉或接受行为 |
| 图片能传但预览不了 | 预览走 /api/sysFile/preview,需要权限;UrlSigning.SecretKey 参与签名 | 检查 App.json 的 UrlSigning;确认登录态 |
| 容器重启后文件没了 | 没挂 volume | docker-compose 里给 wwwroot/upload 加 volume,或改用 OSS(OSSProvider.Enabled=true) |
| 现象 | 原因 | 解决 |
|---|
| Docker 里连不上数据库 | 连接串写了 localhost/127.0.0.1。容器网络里要写服务名,docker-compose.yml 里 MySQL 服务名就是 mysql | 改成 server=mysql;port=3306;...;宿主机访问才用 127.0.0.1:9101 |
| 启动时报连不上,但数据库其实是好的 | 应用启动比 MySQL 快。docker-compose 已用 wait-for-it.sh mysql:3306 -t 120 等待 | 保留等待脚本;等待超时就调大 -t |
| 改了配置不生效 | docker-compose 把 ./app/Configuration/ 挂到了 /app/Configuration/,宿主目录优先 | 改宿主目录下的 docker/app/Configuration/*.json |
| 端口访问不到 | docker-compose.yml 端口映射:nginx 9100、mysql 9101、应用 9102、redis 6379、minio 9104/9105 | 核对映射与防火墙 |
| 时区不对 | 容器需要 TZ=Asia/Shanghai(compose 里已配) | 补 TZ 环境变量 |
| 发布后配置没更新 | csproj 用 CopyToPublishDirectory=PreserveNewest,不会覆盖目标机器上较新的文件 | 手动合并配置,不要只丢文件 |
| 现象 | 原因 | 解决 |
|---|
| 主键重复 / 插入报主键冲突 | 多节点 SnowId:WorkerId 相同。YitIdHelperExtension 只在 Redis 缓存模式下自动分配 WorkerId;Memory 模式直接用配置里的固定 WorkerId | 多节点部署必须 CacheType=Redis(自动分配),或手工给每个节点配不同的 WorkerId(取值 < 2^WorkerIdBitLength,默认 6 位即 0–63) |
| 报"雪花Id未设置有效的机器码" | WorkerId 池被占满(2^WorkerIdBitLength 个节点都在跑) | 调大 SnowId:WorkerIdBitLength(1–19);注意位长变大后 Id 位数也变长 |
| Id 忽然变小 / 与历史 Id 冲突 | 改了 SnowId:BaseTime。基准时间决定 Id 的"起点" | 上线后绝对不可改 BaseTime,只能从备份恢复 |
| Id 位数太长 | SeqBitLength 越大 Id 越长(默认 6) | 调小 SeqBitLength(最小 3,建议 ≥ 4);但越小并发性能越低 |
手工 new 的实体主键为 0 | Aop.DataExecuting 只在主键为 long 且值为 0/null 时补 YitIdHelper.NextId() | 用 YitIdHelper.NextId() 显式赋值,或交给仓储插入 |
| 现象 | 原因 | 解决 |
|---|
| 接口返回 429"访问过于频繁" | Limit.json 的 IP 限流生效,EnableEndpointRateLimiting=true 表示按端点分别计数 | 调大 GeneralRules 的配额;或把 IP 加进 IpWhitelist;反向代理后要配 RealIpHeader: X-Real-IP,否则所有请求算同一个 IP |
| 代理后所有人一起被限流 | 取到的是网关 IP。RealIpHeader 未配或网关没转发该头 | 网关加 X-Real-IP,并确认 Limit.json 的 RealIpHeader 一致 |
| "您的操作过于频繁,请稍后再试!" | 贴了 [Idempotent] 特性:它用 CacheConst.KeyIdempotent(sys_idempotent_lock:)+ MD5(路径+用户+参数) 做分布式锁 | 等 intervalTime(默认 5 秒)后重试;或调大间隔;确认 Redis 正常(锁依赖缓存) |
返回体不是统一的 {code,message,result} | 方法贴了 [NonUnify],或返回值类型不被规范化(IActionResult、FileStreamResult 等) | 需要统一返回就去掉 [NonUnify];文件下载/健康检查这类保持原样是设计如此 |
| 前端拿不到错误信息 | 异常被 Oops.Oh 抛出后走统一结果,前端只读 message | 用 ErrorCodeEnum 里的错误码定位;排查看 SysLogEx |
| 错误码不知道什么意思 | 枚举在 Admin.NET.Core/Enum/ErrorCodeEnum.cs(如 D1003 账号已存在、Z1002 Oracle 不支持、Z1004 连接串无效) | 直接搜枚举名 |
1. 看启动日志:控制台绿色输出(建表/种子耗时)→ 有无红色异常
2. 看接口返回:/api/health 是否 OK([AllowAnonymous],不需要登录)
3. 看异常日志:/api/sysLogEx/page 或日志文件(Logging.json 配置)
4. 看 SQL:Database.json 开 EnableConsoleSql=true,复现一次
5. 看缓存:/api/sysCache/keyList 找对应 key,确认是否存在/是否过期
6. 看资源:/api/sysServer/hardwareInfo、/api/sysServer/runtimeInfo
7. 最小化复现:用超管 + 单节点 + 内存缓存跑一遍,排除权限/集群干扰
一键体检脚本:
#!/usr/bin/env bash
# health-check.sh —— 需要 jq 与 curl
BASE=${BASE:-http://localhost:5005}
echo "== 1. 健康检查 =="
curl -s -o /dev/null -w "health: %{http_code}\n" "$BASE/api/health"
echo "== 2. 登录 =="
TOKEN=$(curl -s -X POST "$BASE/api/sysAuth/login" \
-H "Content-Type: application/json" \
-d '{"account":"superAdmin","password":"Admin.NET++010101"}' | jq -r '.result.accessToken')
echo "token: ${TOKEN:0:24}..."
echo "== 3. 缓存 key 数 =="
curl -s -X POST "$BASE/api/sysCache/keyList" -H "Authorization: Bearer $TOKEN" | jq 'length'
echo "== 4. 硬件/运行时 =="
curl -s "$BASE/api/sysServer/hardwareInfo" -H "Authorization: Bearer $TOKEN" | jq '.result | {cpu, memory}'
echo "== 5. 最近异常日志 =="
curl -s -X POST "$BASE/api/sysLogEx/page" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"page":1,"pageSize":5}' | jq '.result.items | length'
- 排障第一刀是开
EnableConsoleSql:全局过滤器(软删除、租户、数据范围)默认生效,90% 的"查不到数据"都是过滤器加的条件,看 SQL 立刻明白。 - 超管不等于真实用户:超管跳过所有过滤器和权限校验,任何"数据范围/权限"类问题都必须换普通账号复现。
- 清缓存要清对 key:
SysCacheService 自动拼 adminnet_ 前缀,你只需要传业务 key(如 sys_config:);RemoveByPrefixKey 才是批量清。 - 接口树有缓存:改了接口后必须清
sys_all_api,否则菜单授权里看到的还是旧的。 - 后台任务没有
App.User:过滤器、创建人自动填充全部失效,需要手工补 TenantId / CreateUserId。 - Memory 缓存 + 多节点 = 定时炸弹:任务锁、Token、在线用户、雪花 WorkerId 全都不可靠,上线前必须切 Redis。
- Docker 里不要写
localhost 连数据库:写 compose 服务名(mysql、redis、minio)。 SnowId:BaseTime、EnableUnderLine、加密密钥三者上线后不可改,改动会造成不可逆的数据问题。- 种子 Id 要写死且全局唯一:内置菜单用的是固定 Id(如
1310000000111),新增种子撞号会覆盖已有记录。 - 长时间跑不到的任务先看
JobSchedule.Enabled:很多"任务不执行"只是这个开关被关了。
绝大多数后端问题落在四个位置:鉴权链(JwtHandler)、全局过滤器(SetDbAop / SqlSugarFilter)、缓存(SysCacheService + CacheConst)、启动初始化(SqlSugarSetup.InitDatabase + StartHostedService)。先按第 10 节的顺序做体检,再对照对应章节的表格定位,基本都能在几分钟内收敛到具体配置或代码行。