第十章:最佳实践与常见问题解答
目录
1. 编码规范与最佳实践
1.1 后端编码规范
命名规范:
// ✅ 正确示例
// 类名:PascalCase
public class SysUserService { }
public class OrderDetailDto { }
// 接口名:I + PascalCase
public interface IUserService { }
public interface IEmailSender { }
// 方法名:PascalCase
public async Task<List<User>> GetUserListAsync() { }
public void ProcessOrder() { }
// 变量名:camelCase
private readonly ISqlSugarClient _db;
private string userName;
private int orderCount;
// 常量:UPPER_SNAKE_CASE 或 PascalCase
public const string CACHE_KEY_PREFIX = "user:";
public const int MaxRetryCount = 3;
// 异步方法:以Async结尾
public async Task<User> GetUserByIdAsync(long id) { }
代码结构规范:
/// <summary>
/// 用户服务
/// </summary>
[ApiDescriptionSettings(Order = 100)]
public class SysUserService : IDynamicApiController, ITransient
{
#region 依赖注入
private readonly SqlSugarRepository<SysUser> _userRep;
private readonly SysCacheService _cache;
private readonly ILogger<SysUserService> _logger;
public SysUserService(
SqlSugarRepository<SysUser> userRep,
SysCacheService cache,
ILogger<SysUserService> logger)
{
_userRep = userRep;
_cache = cache;
_logger = logger;
}
#endregion
#region 查询方法
/// <summary>
/// 获取用户分页列表
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>分页结果</returns>
[DisplayName("获取用户分页列表")]
public async Task<SqlSugarPagedList<UserOutput>> Page(PageUserInput input)
{
// 1. 参数验证
if (input == null)
throw Oops.Oh("参数不能为空");
// 2. 构建查询
var query = _userRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account.Contains(input.Account))
.WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.RealName.Contains(input.RealName));
// 3. 执行查询
return await query
.OrderBy(u => u.OrderNo)
.Select<UserOutput>()
.ToPagedListAsync(input.Page, input.PageSize);
}
#endregion
#region 新增/编辑方法
/// <summary>
/// 新增用户
/// </summary>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("新增用户")]
public async Task<long> Add(AddUserInput input)
{
// 1. 业务验证
await ValidateUserInput(input);
// 2. 数据转换
var user = input.Adapt<SysUser>();
user.Password = CryptogramUtil.Encrypt(input.Password);
// 3. 保存数据
await _userRep.InsertAsync(user);
// 4. 记录日志
_logger.LogInformation($"新增用户成功:{user.Account}");
return user.Id;
}
#endregion
#region 私有方法
private async Task ValidateUserInput(AddUserInput input)
{
// 验证账号唯一
var exist = await _userRep.IsAnyAsync(u => u.Account == input.Account);
if (exist)
throw Oops.Oh(ErrorCodeEnum.D1003);
// 验证手机号唯一
if (!string.IsNullOrEmpty(input.Phone))
{
exist = await _userRep.IsAnyAsync(u => u.Phone == input.Phone);
if (exist)
throw Oops.Oh("手机号已存在");
}
}
#endregion
}
1.2 前端编码规范
Vue组件规范:
<!-- ✅ 正确示例 -->
<template>
<div class="user-list">
<!-- 组件结构清晰 -->
<SearchForm v-model="queryParams" @search="handleSearch" />
<DataTable
:data="tableData"
:loading="loading"
@edit="handleEdit"
@delete="handleDelete"
/>
<Pagination
v-model:page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
@change="getList"
/>
<!-- 对话框放在最后 -->
<UserForm v-model="formVisible" :data="currentRow" @success="getList" />
</div>
</template>
<script setup lang="ts">
// 1. 导入声明
import { ref, reactive, onMounted } from 'vue'
import { userApi } from '@/api/system/user'
import type { UserInfo, UserQuery } from '@/api/model/user'
import { ElMessage } from 'element-plus'
// 2. 组件声明
import SearchForm from './components/SearchForm.vue'
import DataTable from './components/DataTable.vue'
import UserForm from './components/UserForm.vue'
// 3. Props/Emits定义
const props = defineProps<{
orgId?: number
}>()
// 4. 响应式数据
const loading = ref(false)
const tableData = ref<UserInfo[]>([])
const total = ref(0)
const formVisible = ref(false)
const currentRow = ref<UserInfo | null>(null)
const queryParams = reactive<UserQuery>({
account: '',
realName: '',
page: 1,
pageSize: 10
})
// 5. 计算属性
// ...
// 6. 方法定义
const getList = async () => {
loading.value = true
try {
const res = await userApi.getPage(queryParams)
tableData.value = res.data.items
total.value = res.data.total
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
queryParams.page = 1
getList()
}
const handleEdit = (row: UserInfo) => {
currentRow.value = row
formVisible.value = true
}
const handleDelete = async (row: UserInfo) => {
try {
await userApi.delete(row.id)
ElMessage.success('删除成功')
getList()
} catch (error) {
console.error('删除失败:', error)
}
}
// 7. 生命周期
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.user-list {
padding: 16px;
:deep(.el-table) {
margin-top: 16px;
}
}
</style>
1.3 Git提交规范
# 提交格式
<type>(<scope>): <subject>
# type类型
# feat: 新功能
# fix: 修复bug
# docs: 文档更新
# style: 代码格式
# refactor: 重构
# test: 测试
# chore: 构建/工具
# 示例
git commit -m "feat(user): 新增用户导入功能"
git commit -m "fix(order): 修复订单金额计算错误"
git commit -m "docs(readme): 更新部署文档"
git commit -m "refactor(auth): 优化登录验证逻辑"
2. 性能优化指南
2.1 后端性能优化
数据库查询优化:
// ❌ 错误示例:N+1查询问题
var users = await _userRep.GetListAsync();
foreach (var user in users)
{
user.Roles = await _roleRep.GetByUserIdAsync(user.Id); // 每次循环都查询
}
// ✅ 正确示例:使用导航属性一次性加载
var users = await _userRep.AsQueryable()
.Includes(u => u.Roles)
.ToListAsync();
// ✅ 正确示例:只查询需要的列
var users = await _userRep.AsQueryable()
.Select(u => new UserSimpleDto
{
Id = u.Id,
Account = u.Account,
RealName = u.RealName
})
.ToListAsync();
// ✅ 正确示例:批量查询
var roleIds = users.Select(u => u.Id).ToList();
var roles = await _roleRep.AsQueryable()
.Where(r => roleIds.Contains(r.UserId))
.ToListAsync();
缓存使用:
// 合理使用缓存
public async Task<UserInfo> GetUserInfo(long userId)
{
var cacheKey = $"user:info:{userId}";
// 先查缓存
var user = _cache.Get<UserInfo>(cacheKey);
if (user != null)
return user;
// 缓存不存在,查数据库
user = await _userRep.AsQueryable()
.Includes(u => u.Roles)
.FirstAsync(u => u.Id == userId);
// 写入缓存(设置合理的过期时间)
if (user != null)
_cache.Set(cacheKey, user, TimeSpan.FromMinutes(30));
return user;
}
// 数据变更时清除缓存
public async Task UpdateUser(UpdateUserInput input)
{
await _userRep.UpdateAsync(input.Adapt<SysUser>());
// 清除相关缓存
_cache.Remove($"user:info:{input.Id}");
_cache.RemoveByPrefix("user:list");
}
异步编程:
// ✅ 正确使用异步
public async Task<OrderOutput> ProcessOrder(long orderId)
{
// 并行执行独立任务
var orderTask = _orderRep.GetByIdAsync(orderId);
var itemsTask = _itemRep.GetListAsync(i => i.OrderId == orderId);
var logTask = RecordOperationLog(orderId, "查看订单");
await Task.WhenAll(orderTask, itemsTask, logTask);
var order = await orderTask;
var items = await itemsTask;
return new OrderOutput
{
Order = order,
Items = items
};
}
// ❌ 避免在循环中使用异步
foreach (var item in items)
{
await ProcessItem(item); // 串行执行,效率低
}
// ✅ 使用并行处理
await Parallel.ForEachAsync(items, async (item, ct) =>
{
await ProcessItem(item);
});
2.2 前端性能优化
组件懒加载:
// 路由懒加载
const routes = [
{
path: '/user',
component: () => import('@/views/user/index.vue')
}
]
// 组件异步加载
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
虚拟滚动:
<!-- 大列表使用虚拟滚动 -->
<template>
<el-table-v2
:columns="columns"
:data="tableData"
:width="700"
:height="400"
fixed
/>
</template>
防抖节流:
import { debounce, throttle } from 'lodash-es'
// 搜索输入防抖
const handleSearch = debounce(async (keyword: string) => {
await searchApi(keyword)
}, 300)
// 滚动事件节流
const handleScroll = throttle(() => {
// 处理滚动
}, 100)
3. 安全加固措施
3.1 认证授权安全
// Token安全配置
services.AddJwt<JwtHandler>(options =>
{
options.ExpiredTime = 120; // 2小时过期
options.ClockSkew = 10; // 时钟偏移
});
// 密码加密
public static class PasswordHelper
{
public static string HashPassword(string password)
{
// 使用BCrypt加密
return BCrypt.Net.BCrypt.HashPassword(password, 12);
}
public static bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}
// 登录安全控制
public async Task<LoginOutput> Login(LoginInput input)
{
// 验证码校验
if (!await ValidateCaptcha(input.CaptchaId, input.CaptchaCode))
throw Oops.Oh("验证码错误");
// 账号锁定检查
var lockKey = $"login:lock:{input.Account}";
if (_cache.Exists(lockKey))
throw Oops.Oh("账号已锁定,请15分钟后重试");
// 密码错误次数检查
var failKey = $"login:fail:{input.Account}";
var failCount = _cache.Get<int>(failKey);
if (failCount >= 5)
{
_cache.Set(lockKey, 1, TimeSpan.FromMinutes(15));
throw Oops.Oh("密码错误次数过多,账号已锁定");
}
// 验证密码
var user = await _userRep.GetFirstAsync(u => u.Account == input.Account);
if (user == null || !PasswordHelper.VerifyPassword(input.Password, user.Password))
{
_cache.Set(failKey, failCount + 1, TimeSpan.FromMinutes(30));
throw Oops.Oh($"账号或密码错误,还剩{4 - failCount}次机会");
}
// 登录成功,清除失败次数
_cache.Remove(failKey);
// 生成Token...
}
3.2 SQL注入防护
// ✅ 使用参数化查询
var users = await _db.Queryable<SysUser>()
.Where(u => u.Account == input.Account) // SqlSugar自动参数化
.ToListAsync();
// ✅ 使用原生SQL时也要参数化
var sql = "SELECT * FROM sys_user WHERE account = @account";
var users = await _db.Ado.SqlQueryAsync<SysUser>(sql, new { account = input.Account });
// ❌ 禁止字符串拼接
var sql = $"SELECT * FROM sys_user WHERE account = '{input.Account}'"; // 危险!
3.3 XSS防护
// 输出编码
public string SanitizeHtml(string input)
{
return System.Web.HttpUtility.HtmlEncode(input);
}
// 富文本过滤
public string FilterRichText(string html)
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Add("p");
sanitizer.AllowedTags.Add("br");
sanitizer.AllowedTags.Add("b");
sanitizer.AllowedTags.Add("i");
sanitizer.AllowedTags.Add("u");
return sanitizer.Sanitize(html);
}
3.4 敏感数据保护
// 敏感字段脱敏
public class UserOutput
{
public string Account { get; set; }
[SensitiveData(SensitiveType.Phone)]
public string Phone { get; set; }
[SensitiveData(SensitiveType.IdCard)]
public string IdCard { get; set; }
[SensitiveData(SensitiveType.Email)]
public string Email { get; set; }
}
// 脱敏处理
public static class DataMaskHelper
{
public static string MaskPhone(string phone)
{
if (string.IsNullOrEmpty(phone) || phone.Length != 11)
return phone;
return phone.Substring(0, 3) + "****" + phone.Substring(7);
}
public static string MaskIdCard(string idCard)
{
if (string.IsNullOrEmpty(idCard) || idCard.Length < 8)
return idCard;
return idCard.Substring(0, 4) + "**********" + idCard.Substring(idCard.Length - 4);
}
public static string MaskEmail(string email)
{
if (string.IsNullOrEmpty(email) || !email.Contains("@"))
return email;
var parts = email.Split('@');
var name = parts[0];
var masked = name.Length > 2
? name.Substring(0, 2) + "***"
: "***";
return masked + "@" + parts[1];
}
}
4. 数据库优化
4.1 索引优化
-- 为常用查询字段添加索引
CREATE INDEX idx_user_account ON sys_user(account);
CREATE INDEX idx_user_phone ON sys_user(phone);
CREATE INDEX idx_user_org_id ON sys_user(org_id);
CREATE INDEX idx_user_status_create_time ON sys_user(status, create_time);
-- 复合索引(注意字段顺序)
CREATE INDEX idx_order_customer_status_time ON order(customer_id, status, create_time);
-- 查看索引使用情况
SHOW INDEX FROM sys_user;
-- 分析查询计划
EXPLAIN SELECT * FROM sys_user WHERE account = 'admin';
4.2 分表策略
// 按月份分表
[SplitTable(SplitType.Month)]
[SugarTable("sys_log_op_{year}{month}{day}")]
public class SysLogOp : EntityBase
{
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
[SplitField]
public DateTime CreateTime { get; set; }
// 其他字段...
}
// 查询时自动路由到对应表
var logs = await _db.Queryable<SysLogOp>()
.SplitTable(DateTime.Now.AddMonths(-1), DateTime.Now) // 查询最近一个月
.ToListAsync();
// 插入时自动路由
await _db.Insertable(new SysLogOp
{
CreateTime = DateTime.Now
}).SplitTable().ExecuteCommandAsync();
4.3 读写分离
// 配置主从数据库
{
"DbSettings": {
"DbConfigs": [
{
"ConfigId": "master",
"ConnectionString": "主库连接",
"SlaveConnectionConfigs": [
{
"HitRate": 10,
"ConnectionString": "从库1连接"
},
{
"HitRate": 10,
"ConnectionString": "从库2连接"
}
]
}
]
}
}
// 强制读主库(用于刚写入后立即读取的场景)
var order = await _db.Queryable<Order>()
.Master() // 强制走主库
.FirstAsync(o => o.Id == orderId);
5. 常见问题解答
5.1 项目运行问题
Q1:运行时提示找不到Admin.NET.Web.Entry.dll?
A:确保已正确设置启动项目:
- 右键点击
Admin.NET.Web.Entry项目 - 选择”设为启动项目”
- 重新生成解决方案
Q2:数据库连接失败?
A:检查以下配置:
- 确认数据库服务已启动
- 检查连接字符串是否正确
- 检查用户名密码是否正确
- 检查防火墙是否开放端口
- 对于MySQL,确认已安装
CharSet=utf8mb4
// 正确的MySQL连接字符串
"ConnectionString": "Data Source=localhost;Database=AdminNET;User ID=root;Password=123456;pooling=true;port=3306;sslmode=none;CharSet=utf8mb4;AllowLoadLocalInfile=true"
Q3:Swagger页面空白?
A:可能原因:
- 后端服务未完全启动
- 存在接口定义错误
- 浏览器缓存问题
解决方法:
# 清理并重新生成
dotnet clean
dotnet build
dotnet run
5.2 前端问题
Q1:pnpm install失败?
A:尝试以下方法:
# 清除缓存
pnpm store prune
# 删除lock文件和node_modules
rm -rf node_modules pnpm-lock.yaml
# 使用淘宝镜像
pnpm config set registry https://registry.npmmirror.com
# 重新安装
pnpm install
Q2:登录后页面空白?
A:检查以下几点:
- 浏览器控制台是否有错误
- API请求是否返回正确数据
- 用户权限是否配置正确
- 清除浏览器缓存和localStorage
Q3:接口请求跨域?
A:确认以下配置:
- 后端CORS配置正确
- 前端API地址配置正确
- Nginx反向代理配置正确
// 后端CORS配置
services.AddCorsAccessor(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
5.3 部署问题
Q1:Docker启动失败?
A:检查步骤:
# 查看容器日志
docker logs adminnet-api
# 检查端口占用
netstat -tlnp | grep 5005
# 确认环境变量
docker exec adminnet-api env
Q2:Nginx 502错误?
A:可能原因:
- 后端服务未启动
- 后端端口配置错误
- 防火墙阻止了端口
# 检查后端服务
curl http://localhost:5005/health
# 检查Nginx配置
nginx -t
# 查看Nginx错误日志
tail -f /var/log/nginx/error.log
6. 故障排查指南
6.1 日志分析
后端日志位置:
- 控制台输出
logs/目录下的日志文件- Windows事件查看器(IIS部署时)
- systemd日志(Linux部署时)
# 查看systemd日志
journalctl -u adminnet -f
# 查看最近100行日志
journalctl -u adminnet -n 100
# 按时间查看日志
journalctl -u adminnet --since "2024-01-01 00:00:00"
6.2 性能分析
使用诊断工具:
# 收集性能数据
dotnet-trace collect -p <PID> --duration 00:00:30
# 分析内存
dotnet-dump collect -p <PID>
dotnet-dump analyze <dumpfile>
# 实时监控
dotnet-counters monitor -p <PID>
6.3 数据库诊断
-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- 查看当前连接
SHOW PROCESSLIST;
-- 查看表状态
SHOW TABLE STATUS;
-- 分析表
ANALYZE TABLE sys_user;
-- 优化表
OPTIMIZE TABLE sys_log_op;
7. 版本升级指南
7.1 升级前准备
- 备份数据库
mysqldump -u root -p AdminNET > backup_$(date +%Y%m%d).sql - 备份代码
git stash # 保存本地修改 git fetch origin - 查看更新日志
- 访问GitHub/Gitee查看Release Notes
- 关注Breaking Changes
7.2 升级步骤
# 1. 拉取最新代码
git pull origin master
# 2. 恢复本地修改
git stash pop
# 3. 解决冲突(如果有)
git mergetool
# 4. 更新NuGet包
dotnet restore
# 5. 重新编译
dotnet build
# 6. 运行数据库迁移(如需要)
# 查看是否有新的种子数据或表结构变更
# 7. 测试运行
dotnet run
7.3 前端升级
# 1. 更新依赖
pnpm update
# 2. 检查破坏性变更
pnpm outdated
# 3. 重新构建
pnpm build
8. 学习资源与社区支持
8.1 官方资源
代码仓库:
- Gitee:https://gitee.com/zuohuaijun/Admin.NET
- GitHub:https://github.com/zuohuaijun/Admin.NET
- GitCode:https://gitcode.com/zuohuaijun/Admin.NET
文档资料:
- 官方文档:https://adminnet.top/
- 在线演示:https://demo.adminnet.top
8.2 相关技术文档
后端技术:
- Furion文档:https://dotnetchina.gitee.io/furion
- SqlSugar文档:https://www.donet5.com/Home/Doc
- .NET文档:https://docs.microsoft.com/dotnet
前端技术:
- Vue3文档:https://cn.vuejs.org/
- Element Plus:https://element-plus.org/zh-CN/
- Vite文档:https://cn.vitejs.dev/
8.3 社区交流
QQ交流群:
- 交流群1:87333204
- 交流群2:252381476
问题反馈:
- 在Gitee/GitHub提交Issue
- 详细描述问题现象
- 提供复现步骤
- 附上错误日志
8.4 学习建议
- 系统学习:按照本教程章节顺序学习
- 动手实践:边学边做,创建自己的模块
- 阅读源码:深入理解框架实现原理
- 参与社区:回答问题、提交PR
- 持续更新:关注框架版本更新
总结
本章涵盖了Admin.NET开发和运维的最佳实践:
- 编码规范:命名规范、代码结构、Git提交规范
- 性能优化:数据库查询、缓存使用、异步编程
- 安全加固:认证授权、SQL注入、XSS防护
- 数据库优化:索引、分表、读写分离
- 常见问题:项目运行、前端、部署问题解答
- 故障排查:日志分析、性能诊断、数据库诊断
- 版本升级:升级准备、步骤、注意事项
- 学习资源:官方资源、技术文档、社区支持
通过本教程的学习,你应该已经掌握了Admin.NET的核心概念和开发技能。希望这些内容能够帮助你在实际项目中更好地使用Admin.NET框架。
祝你开发顺利!🎉