1. 项目概述:一个从线下到云端的预约系统演进史

如果你也负责过学校、社区或者公司的活动室、会议室预约,那你一定对那种“先到先得、手写登记、电话确认”的混乱流程深有感触。几年前,我所在的学校就面临这样的困境:几十个活动室,全靠一张贴在墙上的纸质表格和一部电话来管理,冲突、遗忘、扯皮是家常便饭。为了解决这个问题,我动手写了一个最简单的网页,让同学们可以线上提交预约申请,管理员后台审核。这个最初为了解决“活动室预约”而生的简单系统,就是 OpenReservation 的雏形。

从那时起,这个项目就像一棵树,随着需求和技术的发展不断生长、分叉。它从一个简单的 ASP.NET WebForm 应用,演变为 MVC 架构,最终全面拥抱了 ASP.NET Core。它的部署方式也从传统的 IIS 服务器,迁移到了 Docker 容器,并最终在 Kubernetes 集群中稳定运行。如今,它已经是一个支持多端访问(Web、SPA、微信小程序)、具备用户系统、并初步具备多租户潜力的预约服务平台。这篇文章,我想和你分享的,不仅仅是这个系统的功能列表,更是它背后近十年的技术选型、架构演进和那些“踩坑填坑”的实战经验。无论你是想了解一个真实项目的完整生命周期,还是正在规划自己的业务系统技术栈,希望这些一手经验能给你带来一些启发。

2. 核心架构与设计思路拆解

2.1 从单体到微服务:为什么选择 ASP.NET Core 与 Kubernetes?

OpenReservation 的起点是一个典型的单体应用。在早期,使用 ASP.NET WebForm 或 MVC,将所有功能(用户界面、业务逻辑、数据访问)打包在一起,部署在一台 IIS 服务器上,是最简单直接的选择。这种架构在项目初期、团队小、业务简单时,开发效率极高。

但随着预约场景的复杂化(比如增加了微信小程序、需要支持更高的并发预约请求),单体架构的弊端开始显现:任何微小的功能修改都需要重新部署整个应用;某个模块的 bug 可能导致整个服务不可用;技术栈升级(比如从 .NET Framework 迁移到 .NET Core)也变得异常困难。

技术选型的转折点出现在 ASP.NET Core 的成熟。 我们选择 ASP.NET Core 作为新一代的技术栈,核心原因有三点:

  1. 跨平台与高性能 :.NET Core 天生支持 Linux,这为我们后续采用 Docker 和 Kubernetes 部署扫清了障碍。其高性能的 Kestrel Web 服务器也为应对可能的流量高峰打下了基础。
  2. 现代化的开发体验 :依赖注入、配置系统、中间件管道等设计,让应用的模块化、可测试性大大增强。这对于一个需要长期维护和扩展的项目至关重要。
  3. 活跃的生态 :Entity Framework Core、Identity Server 4(用于用户认证授权)、Swagger 等优秀的开源库与 ASP.NET Core 集成得天衣无缝,能极大提升开发效率。

确定了应用框架,下一步就是部署架构。从 IIS 到 “Docker + Nginx” 是一个自然的过渡。Docker 化解决了环境一致性问题,但单机 Docker 在可用性和伸缩性上仍有瓶颈。最终,我们选择了 Kubernetes

注意: 从单机部署迁移到 Kubernetes 集群并非一蹴而就。我们经历了数据持久化、服务发现、配置管理、日志收集等一系列挑战。例如,在单机时,应用可以直接读写服务器本地磁盘的文件(如上传的预约附件)。在 K8s 中,Pod 是随时可能被销毁和重建的,必须将这类有状态的数据迁移到持久卷(Persistent Volume, PV)或外部对象存储(如 Azure Blob Storage、AWS S3)中。这是一个关键的架构认知转变。

2.2 功能模块化设计:如何构建一个灵活的预约核心?

尽管我们谈论了微服务,但在 OpenReservation 的当前阶段,它仍然是一个基于领域驱动设计(DDD)思想进行模块化拆分的单体应用(或者说是一个“模块化单体”)。这比直接拆分为多个微服务更适合我们当前的团队规模和运维能力。我们的核心领域模型围绕“预约”展开:

  • 预约(Reservation) :这是系统的核心聚合根。它包含预约时间段、预约的活动室、预约人、预约状态(待审核、已通过、已拒绝、已取消)等属性。
  • 活动室(Place) :代表可被预约的物理或逻辑空间。除了基本信息,它还关联了“禁用时间段”规则,用于处理节假日、维修等不可预约的情况。
  • 用户(User) :系统使用者。我们集成了 IdentityServer4 来处理用户认证和授权,支持本地账号和第三方登录(如 GitHub)。
  • 黑名单(Blacklist) :用于限制违规用户的预约权限,是维护预约公平性的重要手段。
  • 公告(Notice)与系统设置(SystemSettings) :用于运营和管理。

这些领域模块在代码层通过清晰的文件夹结构进行隔离,并通过接口定义契约。例如, IReservationService 接口定义了创建预约、查询预约、取消预约等所有核心业务操作,其具体实现 ReservationService 则封装了所有业务规则(如“同一活动室在同一时间段内只能有一个有效预约”、“用户不能预约自己已在黑名单中的时间段”)。

这种设计的好处是,未来如果流量激增,需要将“预约查询”这个读多写少的服务独立出来,我们可以相对清晰地将 IReservationService 和相关的领域模型打包,部署为一个独立的微服务,而其他模块的改动可以降到最低。

3. 关键实现细节与实操要点

3.1 预约冲突检测:业务逻辑的核心

这是预约系统的“灵魂”。逻辑必须绝对严谨,否则就会产生双重预订,引发用户冲突。我们的冲突检测发生在创建预约和修改预约时。

核心算法并不复杂,但细节决定成败:

  1. 用户提交一个预约请求,包含目标活动室 PlaceId 、开始时间 ReservedFor 、结束时间 ReservedUntil
  2. 系统查询该活动室所有**状态为“已通过”或“待审核”**的预约记录(“已拒绝”和“已取消”的不计入)。
  3. 遍历这些已有预约,判断新预约的时间段 [NewStart, NewEnd) 与每一个已有预约的时间段 [ExistingStart, ExistingEnd) 是否存在重叠。
    • 重叠条件 NewStart < ExistingEnd && NewEnd > ExistingStart 。这里使用半开区间 [start, end) 是更佳实践,可以避免“结束时间等于另一个开始时间”是否算冲突的歧义。
  4. 如果存在重叠,则抛出业务异常,提示用户“该时间段已被预约”。

实操中的难点与解决方案:

  • 数据库查询优化 :如果直接 SELECT * FROM Reservations WHERE PlaceId = @PlaceId ,然后在内存中遍历,当该活动室历史预约很多时,性能堪忧。我们会在数据库层面进行初步过滤。例如,只查询与目标日期相关的预约,或者使用数据库的日期范围类型和 GiST 索引(如在 PostgreSQL 中)来高效执行重叠查询。
    // 示例:使用 Entity Framework Core 进行初步日期范围查询
    var conflictingReservations = await _dbContext.Reservations
        .Where(r => r.PlaceId == request.PlaceId &&
                   r.Status != ReservationStatus.Cancelled &&
                   r.Status != ReservationStatus.Rejected &&
                   r.ReservedFor < request.ReservedUntil && // 新结束时间 > 旧开始时间
                   r.ReservedUntil > request.ReservedFor)   // 新开始时间 < 旧结束时间
        .AnyAsync();
    
  • 并发请求下的“幻读” :在高并发场景下,两个用户可能几乎同时查询,都发现时间段空闲,然后同时插入预约,导致冲突检测失效。解决方案是 使用悲观锁或乐观锁
    • 悲观锁 :在事务开始时,直接锁定该活动室相关的预约记录行( SELECT ... FOR UPDATE )。这简单有效,但会降低并发性能。适用于预约频率不极高的场景。
    • 乐观锁 :为活动室或预约记录增加一个版本号字段。在更新时检查版本号是否变化。如果变化,说明期间有其他操作,则让用户重试。在 OpenReservation 中,我们目前采用了在业务逻辑层加强校验并结合数据库唯一约束(例如,虽然不能完全防止并发,但可以结合状态和时间创建复杂约束)的方式,对于更高并发的场景,悲观锁是更稳妥的选择。

3.2 多端接入与 API 设计:SPA、小程序与 OpenAPI

为了让用户更方便地预约,我们提供了多种前端:

  1. 传统多页面后台管理端 :基于 ASP.NET Core Razor Pages,供管理员进行复杂的配置和审核操作。优势是开发快捷,服务端渲染,SEO友好(虽然对后台不重要)。
  2. Angular SPA 客户端 :这是一个独立的项目,使用 Angular 和 Angular Material 组件库构建。它通过调用后端 REST API 提供流畅的、接近原生应用的用户体验,主要用于普通用户的预约操作。
  3. 微信小程序客户端 :针对移动场景,特别是校内、社区内推广。小程序通过微信登录与我们的用户系统对接,提供最便捷的移动端预约入口。

如何保证后端 API 能同时支撑这么多前端?答案是一个设计良好、文档清晰的 RESTful API。

我们使用 Swagger/OpenAPI 来自动生成 API 文档。在 ASP.NET Core 项目中,通过引入 Swashbuckle.AspNetCore 库,可以几乎零配置地生成一个交互式的 API 文档页面(就是项目简介里的 Swagger 链接)。

API 设计要点:

  • 资源导向 :URL 路径以资源为中心,如 GET /api/places 获取所有活动室, POST /api/reservations 创建预约。
  • 统一的响应格式 :所有 API 返回一个封装好的标准响应体,包含状态码 code 、消息 msg 和数据 data 。这便于前端统一处理成功和错误情况。
    {
      "code": 200,
      "msg": "success",
      "data": { /* 实际数据 */ }
    }
    
  • 认证与授权 :对于需要登录的接口(如创建预约),我们使用 JWT (JSON Web Token)。用户在前端登录后,后端颁发一个 JWT,前端将其放在后续请求的 Authorization 请求头中。后端通过 IdentityServer4 中间件来验证 Token 并获取用户身份信息。

    实操心得: 在开发阶段,Swagger UI 可以直接配置 JWT,方便测试。在生产环境,务必设置正确的 Token 签发者(Issuer)和受众(Audience),并确保密钥的安全存储(如从 Azure Key Vault 或环境变量中读取,而非硬编码在配置文件中)。

3.3 数据持久化与缓存策略

我们使用 Entity Framework Core 作为 ORM,数据库目前是 SQL Server (早期版本),但也支持迁移到 PostgreSQL MySQL ,这得益于 EF Core 对多数据库提供者的良好支持。

关于数据库迁移: EF Core 的 Code First 迁移功能是我们的首选。每当领域模型发生变化(如为预约表增加一个“取消原因”字段),我们就在代码中修改对应的 Entity 类,然后通过 dotnet ef migrations add <MigrationName> 命令生成迁移脚本。这些脚本会被纳入版本控制。在部署时,通过 dotnet ef database update 或在应用启动时自动执行迁移,来同步数据库结构。这保证了开发、测试、生产环境数据库结构的一致性。

缓存的使用: 预约系统的数据读多写少(比如频繁查询活动室列表、公告)。我们使用 IMemoryCache (内存缓存)来缓存一些不常变化的数据。

  • 缓存什么? 例如,系统设置、活动室基本信息列表、首页公告。这些数据可能在多个请求间共享,且更新频率低。
  • 缓存策略: 通常设置一个绝对过期时间(如5分钟)。当管理员在后台修改了活动室信息后,我们需要 主动清除或更新 对应的缓存项。这可以通过在保存操作成功后,调用 IMemoryCache.Remove(key) 来实现。
    public async Task UpdatePlace(Place place)
    {
        // ... 更新数据库逻辑
        await _dbContext.SaveChangesAsync();
        // 清除缓存
        _memoryCache.Remove(CacheKeys.AllPlaces);
    }
    

    注意事项: 在 Kubernetes 多副本部署时,内存缓存是副本独立的。一个副本清除了缓存,其他副本的缓存依然存在旧数据。对于一致性要求高的场景,需要考虑使用分布式缓存,如 Redis 。OpenReservation 目前因为数据更新不频繁且对短暂不一致容忍度较高,暂未引入 Redis,但这是架构演进的一个明确方向。

4. 基于 Kubernetes 的部署与 DevOps 实践

4.1 从 Dockerfile 到 Helm Chart:应用容器化

将 ASP.NET Core 应用容器化的第一步是编写 Dockerfile 。我们的目标是构建一个尽可能小的生产镜像。

Dockerfile 优化要点:

# 阶段1:构建
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["OpenReservation/OpenReservation.csproj", "OpenReservation/"]
RUN dotnet restore "OpenReservation/OpenReservation.csproj"
COPY . .
WORKDIR "/src/OpenReservation"
RUN dotnet build "OpenReservation.csproj" -c Release -o /app/build

# 阶段2:发布
FROM build AS publish
RUN dotnet publish "OpenReservation.csproj" -c Release -o /app/publish

# 阶段3:运行时
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
# 安装一些可能需要的运行时依赖,如用于生成验证码的libgdiplus(如果需要)
# RUN apt-get update && apt-get install -y libgdiplus && rm -rf /var/lib/apt/lists/*
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OpenReservation.dll"]
  • 多阶段构建 :这是关键。它确保最终的运行时镜像( final 阶段)只包含运行应用所需的最少内容(.NET 运行时和发布后的程序集),而不包含 SDK、源代码等,这能显著减小镜像体积(通常从 ~1GB 缩减到 ~200MB)。
  • 使用官方镜像 :始终使用 Microsoft 官方的 dotnet/aspnet dotnet/sdk 镜像,以保证安全性和兼容性。
  • 非 root 用户运行 :在生产环境中,出于安全考虑,应该在 Dockerfile 中创建一个非 root 用户来运行应用。我们可以在 final 阶段添加 RUN adduser -u 1000 --disabled-password --gecos "" appuser && chown -R appuser:appuser /app USER appuser

容器化之后,在 Kubernetes 上管理一个应用需要定义多个 YAML 文件:Deployment(定义Pod副本)、Service(内部服务发现)、Ingress(外部访问)、ConfigMap(配置)、Secret(敏感信息)等。手动管理这些文件非常繁琐。为此,我们引入了 Helm ,一个 Kubernetes 的包管理工具。

Helm Chart 的价值:

  • 模板化 :我们将 K8s 资源定义写成模板( templates/ 目录下的 yaml 文件),使用 {{ .Values.xxx }} 语法来注入变量。
  • 参数化 :所有可配置项(如镜像标签、副本数、环境变量)都集中定义在一个 values.yaml 文件中。
  • 一键部署 :通过 helm install openreservation ./chart -f my-values.yaml 命令,即可一键部署或升级整个应用。这极大地简化了部署流程,并使其可重复。

4.2 CI/CD 流水线:从代码提交到自动部署

我们的 CI/CD 旅程经历了 AppVeyor、Travis CI,最终稳定在 Azure Pipelines (同时也用 GitHub Actions 作为补充)。它们的目标一致:自动化构建、测试和部署。

一个典型的 Azure Pipeline 流程(.azure-pipelines.yml)包含以下阶段:

  1. 触发 :当代码推送到 dev main 分支时,自动触发流水线。
  2. 构建
    • 恢复 NuGet 包依赖。
    • 构建项目: dotnet build --configuration Release
    • 运行单元测试和集成测试: dotnet test 。这是保证代码质量的关键门禁。
  3. 打包
    • 发布应用: dotnet publish --configuration Release --output $(Build.ArtifactStagingDirectory)
    • 构建 Docker 镜像,并推送到容器镜像仓库(如 Docker Hub 或 Azure Container Registry, ACR)。
    - task: Docker@2
      inputs:
        containerRegistry: 'my-acr-connection'
        repository: 'openreservation'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'
        tags: '$(Build.BuildId)'
    
  4. 部署(到 Kubernetes)
    • 使用 helm upgrade --install 命令,将上一步构建的新镜像部署到测试或生产环境的 K8s 集群。
    • 这一步通常需要配置 K8s 集群的认证信息(如 kubeconfig),Azure Pipelines 提供了 Kubernetes Helm 任务来安全地完成此操作。

踩坑实录: 在配置 CD 时,最容易出错的是镜像拉取策略( imagePullPolicy )和标签(tag)。我们最初使用 latest 标签,但这会导致 K8s 无法感知到镜像已更新(如果节点上已有 latest 镜像)。最佳实践是使用 唯一的、与构建号关联的标签 ,如 $(Build.BuildId) 。在 Helm Chart 的 values.yaml 中,将镜像标签设置为变量 {{ .Values.image.tag }} ,然后在流水线部署时,通过 --set image.tag=$(Build.BuildId) 动态传入。

4.3 生产环境运维:监控、日志与高可用

应用在 K8s 上跑起来只是第一步,保证其稳定、可观测才是运维的重点。

1. 健康检查(Liveness & Readiness Probes): Kubernetes 通过探针来判断 Pod 的健康状态。我们在 ASP.NET Core 应用中,使用 Microsoft.AspNetCore.Diagnostics.HealthChecks 库轻松添加健康检查端点。

// Startup.cs
services.AddHealthChecks()
    .AddDbContextCheck<ApplicationDbContext>(); // 检查数据库连接
    // .AddRedis("redis-connection-string") // 如果需要检查Redis
    // .AddUrlGroup(new Uri("https://api.example.com"), "external-api");

app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
    // ... 其他端点
});

在 K8s 的 Deployment 中配置:

livenessProbe:
  httpGet:
    path: /health
    port: 80
  initialDelaySeconds: 30 # 给应用足够的启动时间
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /health
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 5
  • 就绪探针(Readiness) 告诉 K8s 何时可以将流量路由到 Pod。
  • 存活探针(Liveness) 告诉 K8s 何时应该重启不健康的 Pod。

2. 集中式日志收集: 在 K8s 中,Pod 的日志是临时的。我们使用 EFK Stack (Elasticsearch, Fluentd, Kibana)或 Loki 来收集日志。应用中使用 Serilog NLog 等日志库,将日志以结构化格式(如 JSON)输出到标准输出(stdout)。Fluentd 的 DaemonSet 会收集每个节点上所有容器的 stdout 日志,处理后发送到 Elasticsearch 进行索引和存储,最后在 Kibana 中提供强大的搜索和可视化界面。

3. 应用性能监控(APM): 我们使用 Application Insights (Azure 的服务)来监控应用性能。在 ASP.NET Core 中集成非常简单,只需添加 Microsoft.ApplicationInsights.AspNetCore 包,并在配置中提供连接字符串。它可以自动追踪 HTTP 请求、依赖调用(如数据库查询)、异常,并生成性能图表和失败请求详情,是定位生产问题的利器。

4. 高可用与伸缩:

  • 多副本 :在 Deployment 中设置 replicas: 3 ,K8s 会确保始终有 3 个 Pod 实例在运行,即使某个节点故障。
  • Pod 反亲和性 :可以配置 Pod 分散在不同的节点上运行,避免单点故障。
  • 水平自动伸缩(HPA) :根据 CPU 或内存使用率等指标,自动增加或减少 Pod 副本数。例如,当平均 CPU 使用率超过 70% 时,自动扩容到最多 5 个副本。
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: openreservation-hpa
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: openreservation
      minReplicas: 2
      maxReplicas: 5
      metrics:
      - type: Resource
        resource:
          name: cpu
          target:
            type: Utilization
            averageUtilization: 70
    

5. 常见问题排查与经验总结

5.1 数据库连接池耗尽

现象 :应用运行一段时间后,开始出现 Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool 错误,随后大量请求失败。

原因分析 :这是 .NET 应用中非常经典的问题。ADO.NET 默认会维护一个数据库连接池。如果代码中打开连接( SqlConnection )后没有正确关闭(或 Dispose),连接就不会被释放回连接池。随着请求增多,连接池中的连接被耗尽,新的请求就必须等待,直到超时。

解决方案与排查步骤:

  1. 确保使用 using 语句 :这是最基本也是最重要的。所有 DbContext SqlConnection 的实例化,都应该包裹在 using 语句中,以确保其被及时释放。
    using (var context = new ApplicationDbContext())
    {
        // 操作数据库
    } // 离开作用域时,context 会被 Dispose,连接会释放回池中
    
  2. 在 ASP.NET Core 中,依赖注入是首选 :让框架来管理 DbContext 的生命周期(通常使用 Scoped 生命周期)。在控制器或服务中直接注入,不要手动 new
  3. 检查异步方法中的潜在泄露 :确保所有异步数据库操作都正确使用了 await ,并且没有在未完成的情况下被意外阻塞或丢弃。
  4. 监控连接数 :可以在 SQL Server 中执行 SELECT * FROM sys.dm_exec_connections 来查看当前连接情况。如果发现大量 sleeping 状态且 last_batch 时间很旧的连接,很可能存在泄露。
  5. 调整连接池大小 :在极端情况下,可以尝试在连接字符串中调整 Max Pool Size (默认100)和 Min Pool Size ,但这只是治标,找到并修复泄露的代码才是根本。

5.2 Kubernetes 中 Pod 不断重启(CrashLoopBackOff)

现象 kubectl get pods 显示 Pod 状态为 CrashLoopBackOff kubectl logs <pod-name> 查看日志,可能看到应用启动失败的错误信息,也可能看不到任何错误(如果启动太快,来不及记录)。

排查思路:

  1. 查看 Pod 描述 kubectl describe pod <pod-name> 。这是最重要的诊断命令。关注 Events 部分,这里会显示调度、拉取镜像、启动容器等过程中的具体事件和错误信息。常见原因有:
    • Failed to pull image "xxx" :镜像不存在或拉取权限不足。
    • Error: ErrImagePull ImagePullBackOff :同上。
    • Back-off restarting failed container :容器内进程启动失败。
  2. 查看容器日志 kubectl logs <pod-name> --previous 可以查看前一个崩溃容器的日志,有时比当前容器的日志更有用。
  3. 检查应用配置 :最常见的原因是应用配置文件(如 appsettings.json )中的连接字符串、密钥等配置,在 K8s 环境中不正确。确保 ConfigMap 或 Secret 已正确挂载,且环境变量名与代码中读取的 ( IConfiguration ) 一致。
  4. 检查资源限制 :Pod 可能因为请求的内存或 CPU 不足而启动失败。检查 Deployment 中的 resources.requests/limits 配置,并查看 describe 输出中是否有 OOMKilled 事件。
  5. 检查依赖服务 :应用启动时需要连接数据库、Redis 等外部服务。如果这些服务不可达或认证失败,也会导致启动崩溃。确保网络策略(NetworkPolicy)允许 Pod 访问这些服务端点。

一个快速进入 Pod 进行诊断的技巧 :如果应用启动失败很快,来不及看日志,可以尝试修改 Deployment 的启动命令,让它先“睡”一会儿,给你时间进入容器检查。

# 在 deployment.yaml 的容器 spec 中临时修改
command: ["/bin/sh"]
args: ["-c", "sleep 3600"] # 先休眠一小时

然后 kubectl exec -it <pod-name> -- /bin/sh 进入容器,手动执行你的应用启动命令(如 dotnet YourApp.dll ),观察输出。

5.3 微信小程序登录集成问题

现象 :用户在小程序端无法登录,或登录后无法与后端服务关联。

排查要点:

  1. 小程序 AppID 和 Secret 配置 :这是第一步。确保后端配置的 AppID 和 AppSecret 与微信公众平台(小程序后台)的完全一致,且没有多余空格。 Secret 必须严格保密 ,应存储在 K8s 的 Secret 中,而非代码或普通配置文件中。
  2. code 换取 session_key 和 openid :小程序前端调用 wx.login() 获取临时登录凭证 code ,并发送到你的后端。后端需要用此 code 、AppID 和 AppSecret 调用微信接口换取 openid (用户唯一标识)和 session_key (会话密钥)。 这个后端调用必须是服务器到服务器的,不能在前端进行 。检查这个网络调用是否成功,微信接口返回了什么错误码。
  3. 生成自定义登录态(Token) :获取到 openid 后,你需要在自己的用户系统中查找或创建一个与之关联的用户账户。然后,像普通登录一样,为该用户生成一个 JWT Token 返回给小程序。小程序后续请求时携带此 Token。
  4. Token 存储与验证 :小程序可以将 Token 存储在 wx.setStorageSync 中。后端需要验证这个 Token 的签名和有效期。确保你的 IdentityServer4 或 JWT 生成/验证配置正确,特别是 Issuer Audience 要匹配。
  5. 网络与域名 :确保你的后端 API 域名已经在小程序后台的“开发设置”->“服务器域名”中配置。同时,检查服务器防火墙和 K8s Ingress 规则,允许来自微信服务器的请求。

5.4 性能优化实战记录

随着用户量增长,一些性能问题逐渐暴露。以下是我们做过的一些有效优化:

1. 数据库查询优化:

  • N+1 查询问题 :在列表页显示预约记录及其关联的活动室名称时,最初的代码会导致先查询1次获取预约列表,再为每条预约记录查询1次活动室信息。使用 EF Core 的 .Include() 或投影( .Select() )进行 预先加载(Eager Loading) 解决。
    // 优化前(N+1)
    var reservations = await _dbContext.Reservations.ToListAsync();
    foreach (var r in reservations) {
        var place = await _dbContext.Places.FindAsync(r.PlaceId); // 每次循环都查库
    }
    // 优化后(1次查询)
    var reservationsWithPlace = await _dbContext.Reservations
        .Include(r => r.Place) // 一次性加载关联的Place数据
        .ToListAsync();
    
  • 添加索引 :为经常用于查询条件的字段添加数据库索引,如 Reservations 表的 PlaceId ReservedFor Status 字段。使用 EF Core 迁移来创建索引。
  • 只查询需要的字段 :使用 Select 只返回前端需要的字段,避免 SELECT *

2. 前端资源优化(针对 Angular SPA):

  • 启用生产构建 :使用 ng build --configuration production ,它会进行 AOT 编译、代码压缩、Tree Shaking 等优化。
  • 配置懒加载 :将不同的功能模块(如用户模块、管理模块)设置为懒加载,只有用户访问时才加载对应的 JavaScript 包,减少初始加载时间。
  • 利用浏览器缓存 :为静态资源(JS、CSS、图片)设置合适的 Cache-Control 头,例如 max-age=31536000 (一年)。

3. 后端响应缓存: 对于极少变化的数据接口,如“获取所有活动室类型”,我们在 Controller 动作上添加 [ResponseCache] 特性,利用 HTTP 缓存。

[HttpGet("place-types")]
[ResponseCache(Duration = 300)] // 缓存5分钟
public IActionResult GetPlaceTypes() {
    // ...
}

这会在响应头中添加 Cache-Control: public,max-age=300 ,告诉浏览器和中间的 CDN/代理服务器可以缓存此响应。

回顾 OpenReservation 的整个演进过程,从解决一个具体的线下效率问题开始,到如今成为一个具备现代化技术栈的云原生应用,最大的体会是: 架构是演进而来的,不是设计出来的 。不要一开始就追求最完美的微服务、最前沿的技术,而是根据当前团队的认知、业务的规模和运维的能力,选择最合适、最能快速交付价值的方案。同时,保持代码的整洁、模块的清晰,为未来的变化留出空间。当痛点真正出现时(如部署困难、性能瓶颈),再果断地进行架构升级,每一次升级的目标都应该是为了解决具体问题,而不是为了技术而技术。这个项目还在持续迭代,下一步我们正在探索如何更好地实现“多租户”和“预约即服务”的构想,让这套系统能更便捷地服务于更多的组织和场景。

更多推荐