znlgis 博客

GIS开发与技术分享

第十章:最佳实践与常见问题解答

目录

  1. 编码规范与最佳实践
  2. 性能优化指南
  3. 安全加固措施
  4. 数据库优化
  5. 常见问题解答
  6. 故障排查指南
  7. 版本升级指南
  8. 学习资源与社区支持

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:确保已正确设置启动项目:

  1. 右键点击Admin.NET.Web.Entry项目
  2. 选择”设为启动项目”
  3. 重新生成解决方案

Q2:数据库连接失败?

A:检查以下配置:

  1. 确认数据库服务已启动
  2. 检查连接字符串是否正确
  3. 检查用户名密码是否正确
  4. 检查防火墙是否开放端口
  5. 对于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:可能原因:

  1. 后端服务未完全启动
  2. 存在接口定义错误
  3. 浏览器缓存问题

解决方法:

# 清理并重新生成
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:检查以下几点:

  1. 浏览器控制台是否有错误
  2. API请求是否返回正确数据
  3. 用户权限是否配置正确
  4. 清除浏览器缓存和localStorage

Q3:接口请求跨域?

A:确认以下配置:

  1. 后端CORS配置正确
  2. 前端API地址配置正确
  3. 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:可能原因:

  1. 后端服务未启动
  2. 后端端口配置错误
  3. 防火墙阻止了端口
# 检查后端服务
curl http://localhost:5005/health

# 检查Nginx配置
nginx -t

# 查看Nginx错误日志
tail -f /var/log/nginx/error.log

6. 故障排查指南

6.1 日志分析

后端日志位置

# 查看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 升级前准备

  1. 备份数据库
    mysqldump -u root -p AdminNET > backup_$(date +%Y%m%d).sql
    
  2. 备份代码
    git stash  # 保存本地修改
    git fetch origin
    
  3. 查看更新日志
    • 访问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 官方资源

代码仓库

文档资料

8.2 相关技术文档

后端技术

前端技术

8.3 社区交流

QQ交流群

问题反馈

8.4 学习建议

  1. 系统学习:按照本教程章节顺序学习
  2. 动手实践:边学边做,创建自己的模块
  3. 阅读源码:深入理解框架实现原理
  4. 参与社区:回答问题、提交PR
  5. 持续更新:关注框架版本更新

总结

本章涵盖了Admin.NET开发和运维的最佳实践:

  1. 编码规范:命名规范、代码结构、Git提交规范
  2. 性能优化:数据库查询、缓存使用、异步编程
  3. 安全加固:认证授权、SQL注入、XSS防护
  4. 数据库优化:索引、分表、读写分离
  5. 常见问题:项目运行、前端、部署问题解答
  6. 故障排查:日志分析、性能诊断、数据库诊断
  7. 版本升级:升级准备、步骤、注意事项
  8. 学习资源:官方资源、技术文档、社区支持

通过本教程的学习,你应该已经掌握了Admin.NET的核心概念和开发技能。希望这些内容能够帮助你在实际项目中更好地使用Admin.NET框架。

祝你开发顺利!🎉