17.核心服务实现(上)

从本文开始,我们将基于《16.项目架构设计》中的设计与目录结构,落地实现核心服务,涵盖用户服务、认证授权服务、商品服务的关键实现。示例基于 .NET 10、ASP.NET Core Minimal API、EF Core、JWT,结合 .NET Aspire 的 AppHost/ServiceDefaults 编排与观测。

一、用户服务实现

用户服务是整个电商平台的基础服务之一,负责管理用户的注册、登录、个人信息等核心功能。在微服务架构中,用户服务需要保持高内聚、低耦合的特点,只关注用户相关的业务逻辑。本节将从领域模型设计开始,逐步构建一个完整的用户服务,包括数据持久化、业务逻辑处理以及 API 接口暴露。

1.1 领域模型设计

领域模型是业务逻辑的核心载体,它应该反映真实的业务概念和规则。在设计用户模型时,我们遵循领域驱动设计(DDD)的原则,将用户实体定义在领域层,确保它不依赖于任何基础设施细节。

csharp 复制代码
// Domain/Entities/User.cs
public class User
{
    public long Id { get; set; }
    public string Username { get; set; } = default!;
    public string Email { get; set; } = default!;
    public string PasswordHash { get; set; } = default!;
    public int Status { get; set; } = 1;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

在这个用户实体中,Id 属性采用 long 类型而非传统的 int,这是为了应对大规模用户场景下的主键容量需求。UsernameEmail 使用了 C# 10 引入的 default! 语法,这告诉编译器这些属性在构造时必定会被赋值,从而避免了可空引用类型的警告。PasswordHash 存储的是密码的哈希值而非明文,这是安全性的基本要求,我们使用 ASP.NET Core Identity 提供的密码哈希器来生成和验证哈希值。

Status 字段用于标识用户状态,默认值为 1 表示正常状态,可以扩展为 0(禁用)、2(待审核)等多种状态。CreatedAt 记录用户创建时间,使用 DateTime.UtcNow 确保时间以 UTC 标准存储,避免时区转换问题。这个简洁的模型设计遵循了单一职责原则,只包含用户的核心属性,复杂的扩展信息(如地址、偏好设置)将通过关联实体来实现。

作为本小节的收尾,需要强调的是领域模型不只是字段集合,更是业务语义的载体。通过选择 long 作为主键、使用不可空约束、以及将密码以哈希形式存储,我们将安全与可扩展性嵌入到模型层。这一模型为后续的持久化映射、领域服务以及 API 暴露提供了清晰稳定的契约,也为演进到更复杂的用户画像和权限体系留下了充足的扩展点。

1.2 数据访问层与 Entity Framework Core 配置

数据访问层负责将领域模型映射到数据库表,Entity Framework Core 作为 .NET 生态中最成熟的 ORM 框架,提供了强大的对象关系映射能力。我们通过 DbContext 来定义数据库上下文,它是 EF Core 与数据库交互的桥梁。

csharp 复制代码
// Infrastructure/Data/AppDbContext.cs
public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(b =>
        {
            b.ToTable("Users");
            b.HasKey(x => x.Id);
            b.Property(x => x.Username).IsRequired().HasMaxLength(50);
            b.Property(x => x.Email).IsRequired().HasMaxLength(100);
            b.HasIndex(x => x.Username).IsUnique();
            b.HasIndex(x => x.Email).IsUnique();
        });
    }
}

DbSet<User> 属性定义了用户实体的集合,通过 Set<User>() 方法返回一个可查询的集合。构造函数接收 DbContextOptions<AppDbContext> 参数,这个参数由依赖注入容器提供,包含了数据库连接字符串等配置信息。在 .NET Aspire 环境中,这些配置会自动从 AppHost 的资源定义中注入。

OnModelCreating 方法是配置实体映射的核心位置。ToTable("Users") 明确指定了表名,虽然 EF Core 会根据约定自动推断表名,但显式指定能提高代码的可读性和可维护性。HasKey(x => x.Id) 定义了主键,EF Core 会自动将其配置为自增列。

属性配置部分,IsRequired() 标记字段为非空约束,HasMaxLength() 限制字符串长度,这不仅是数据库层面的约束,也能帮助 EF Core 优化 SQL 生成。索引配置通过 HasIndex()IsUnique() 实现,为 UsernameEmail 创建唯一索引,这既保证了业务规则(用户名和邮箱不能重复),又优化了查询性能。在生产环境中,这些索引对于用户登录和查重场景至关重要。

作为本小节的总结,DbContext 与 Fluent API 的精细化配置确保了领域模型与数据库之间的精确映射。唯一索引、长度与非空约束共同构成了数据质量防线,减少了业务层的负担。结合 .NET Aspire 的连接字符串自动注入机制,这一数据访问层实现既可移植又易维护,为部署到不同环境时的稳定运行打下基础。

1.3 应用服务层与密码安全处理

应用服务层位于领域层和表现层之间,负责协调业务逻辑的执行。在用户服务中,注册是最核心的功能之一,它涉及数据验证、密码加密、数据持久化等多个环节。密码安全是用户服务的重中之重,我们绝不能以明文形式存储密码,必须使用成熟的哈希算法进行不可逆加密。

csharp 复制代码
// Application/Services/UserService.cs
public class UserService
{
    private readonly AppDbContext _db;
    private readonly IPasswordHasher<User> _hasher;

    public UserService(AppDbContext db, IPasswordHasher<User> hasher)
    {
        _db = db;
        _hasher = hasher;
    }

    public async Task<long> RegisterAsync(string username, string email, string password)
    {
        if (await _db.Users.AnyAsync(x => x.Username == username || x.Email == email))
            throw new InvalidOperationException("User exists");

        var user = new User
        {
            Username = username,
            Email = email,
            PasswordHash = _hasher.HashPassword(null!, password)
        };
        _db.Users.Add(user);
        await _db.SaveChangesAsync();
        return user.Id;
    }
}

UserService 类通过构造函数注入了 AppDbContextIPasswordHasher<User>,前者用于数据库操作,后者提供密码哈希功能。 RegisterAsync 方法实现了用户注册逻辑。首先,使用 AnyAsync 方法检查用户名或邮箱是否已存在,这个查询会被翻译为高效的 SQL EXISTS 语句,避免了不必要的数据加载。如果存在重复用户,抛出 InvalidOperationException,这是一种简单的错误处理方式,实际应用中可以定义更具体的异常类型。

在结尾处补充两点工程化考量。其一,PasswordHasher 默认采用 PBKDF2(基于 HMAC-SHA256)并带随机盐与迭代次数,能够有效抵御字典和彩虹表攻击;当安全参数升级时,SuccessRehashNeeded 可触发透明的重新哈希流程。其二,注册流程可放入数据库事务以确保写入原子性,并在发生并发冲突时配合唯一索引得到一致的失败语义。通过这些加固措施,用户注册的可靠性与安全性得到进一步保障。

1.4 HTTP API 端点与服务配置

API 层是服务对外的接口,负责接收 HTTP 请求、验证输入、调用业务逻辑并返回响应。.NET 8 引入的 Minimal API 风格简化了 API 的定义方式,减少了样板代码,同时保持了强类型检查和依赖注入的优势。我们需要在 Program.cs 中完成服务注册和端点映射。

csharp 复制代码
// Api/Program.cs (片段)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("UserDb")));
builder.Services.AddScoped<UserService>();
builder.Services.AddIdentityCore<User>()
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>();

var app = builder.Build();

app.MapPost("/api/users", async (UserService svc, RegisterRequest req) =>
{
    var id = await svc.RegisterAsync(req.Username, req.Email, req.Password);
    return Results.Created($"/api/users/{id}", new { id });
});

app.MapGet("/api/users/{id:long}", async (AppDbContext db, long id) =>
{
    var user = await db.Users.FindAsync(id);
    return user is null ? Results.NotFound() : Results.Ok(user);
});

app.Run();

服务配置部分,AddDbContext 方法注册了数据库上下文,UseNpgsql 指定使用 PostgreSQL 数据库。连接字符串通过 GetConnectionString("UserDb") 获取,在 .NET Aspire 环境中,这个连接字符串会由 AppHost 自动注入,开发者无需手动配置。这种设计极大简化了本地开发体验,同时保证了不同环境下的配置一致性。

AddScoped<UserService> 将用户服务注册为 Scoped 生命周期,这意味着每个 HTTP 请求会创建一个新实例,请求结束后释放。这是 Web 应用的典型模式,确保了线程安全和资源正确释放。

AddIdentityCore<User> 引入了 ASP.NET Core Identity 的核心功能,虽然我们没有使用完整的 Identity 系统,但需要它提供的密码哈希器。AddEntityFrameworkStores<AppDbContext> 配置 Identity 使用 EF Core 作为存储提供程序。

API 端点映射使用了 Minimal API 的声明式语法。MapPost 定义了用户注册端点,路径为 /api/users,处理函数通过参数注入获取 UserService 和请求体 RegisterRequest。这里的依赖注入是自动的,框架会从服务容器中解析 UserService,从请求体中反序列化 RegisterRequest

注册成功后,返回 Results.Created 响应,状态码为 201,包含新资源的 URI 和资源标识。这符合 RESTful 设计规范,客户端可以通过 Location 响应头获取新资源的地址。

MapGet 定义了查询用户端点,路径参数 {id:long} 使用了路由约束,只匹配 long 类型的值。处理函数直接注入 AppDbContext,这在简单查询场景下是可以接受的,复杂场景应该通过应用服务层封装。FindAsync 是 EF Core 提供的高效主键查询方法,会优先从变更追踪器中查找,找不到才查询数据库。

这种 API 设计模式简洁明了,每个端点的职责清晰,易于测试和维护。在用户服务的基础上,我们可以继续扩展更新、删除等其他 CRUD 操作,形成完整的用户管理功能。

作为本节的收尾,Minimal API 的声明式路由与依赖注入让端点逻辑紧凑而清晰;对应的返回语义(201 Created、404 NotFound、200 Ok)严格遵循 REST 约定,提升客户端集成的可预期性。结合环境注入的连接信息与生命周期管理,这一用户服务从模型到接口的链路已成闭环,后续可在不破坏契约的前提下平滑扩展更多用户场景。

通过本节的实现,我们完成了用户服务从领域模型到 HTTP API 的完整构建。领域模型以 DDD 原则定义了用户实体的核心属性,确保业务语义清晰且独立于基础设施;Entity Framework Core 的 Fluent API 配置建立了精确的对象关系映射,唯一索引和约束在数据库层面保障了数据质量;应用服务层使用 PBKDF2 密码哈希算法提供了企业级的安全防护,同时通过事务确保注册流程的原子性;Minimal API 端点以声明式风格暴露服务能力,RESTful 响应语义使客户端集成更加直观。

在 .NET Aspire 编排下,连接字符串、依赖注入和生命周期管理都得到自动化处理,开发者可以专注于业务逻辑而非基础设施配置。这种实现模式不仅适用于用户服务,也可以推广到其他领域服务,形成统一的开发范式。随着业务的演进,用户服务可以在此基础上扩展用户画像、多因素认证、社交登录等高级功能,而核心架构保持稳定。接下来我们将转向认证授权服务,构建基于 JWT 的安全体系,为整个微服务架构提供身份认证和访问控制能力。

二、认证授权服务

认证(Authentication)和授权(Authorization)是现代 Web 应用安全体系的两大支柱。认证解决的是"你是谁"的问题,授权解决的是"你能做什么"的问题。在微服务架构中,我们通常采用基于令牌的认证方案,JWT(JSON Web Token)因其无状态、跨域友好的特性成为主流选择。本节将实现一个完整的 JWT 认证授权流程,从配置、令牌颁发到保护资源的全链路实现。

2.1 JWT 认证体系配置与原理解析

JWT 是一种紧凑的、URL 安全的表示声明的方式,由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部描述令牌类型和签名算法,载荷包含用户信息和声明,签名确保令牌未被篡改。在 ASP.NET Core 中配置 JWT 认证需要安装 Microsoft.AspNetCore.Authentication.JwtBearer 包,并进行详细的参数设置。

csharp 复制代码
// Api/Program.cs (认证部分)
var jwtSection = builder.Configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwtSection["Key"]!);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtSection["Issuer"],
            ValidAudience = jwtSection["Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(key)
        };
    });

builder.Services.AddAuthorization();

配置的第一步是从配置文件中读取 JWT 相关参数。GetSection("Jwt") 获取配置节,其中应包含 KeyIssuerAudience 等关键信息。Key 是签名密钥,必须足够复杂且妥善保管,生产环境应使用至少 256 位的随机字符串,并存储在安全的配置管理系统(如 Azure Key Vault)中。这里使用 Encoding.UTF8.GetBytes 将字符串转换为字节数组,供后续的 HMAC-SHA256 签名算法使用。

AddAuthentication 方法注册认证服务,JwtBearerDefaults.AuthenticationScheme 指定使用 Bearer 认证方案,这是 OAuth 2.0 标准中定义的令牌传递方式。客户端在 HTTP 请求头中添加 Authorization: Bearer <token> 来携带令牌。

TokenValidationParameters 是令牌验证的核心配置对象。ValidateIssuerValidIssuer 确保令牌是由指定的颁发者签发的,防止跨域令牌攻击。Issuer 通常是认证服务的标识,如 https://auth.example.comValidateAudienceValidAudience 限制令牌的受众,即令牌只能用于特定的服务或 API,这在多服务场景下尤为重要。

ValidateIssuerSigningKeyIssuerSigningKey 是签名验证的关键。SymmetricSecurityKey 使用对称密钥签名,这意味着签发和验证使用同一密钥。在单体应用或服务间完全信任的场景下,对称密钥简单高效。如果需要分布式签名验证,可以考虑使用非对称密钥(RSA),此时签发方持有私钥,验证方只需公钥。

AddAuthorization 方法注册授权服务,虽然这里没有额外配置,但它是使用 [Authorize] 特性的前提。授权策略可以基于角色、声明或自定义逻辑,提供细粒度的访问控制。整个认证配置遵循了安全最佳实践,确保令牌的完整性和合法性。

作为本小节的总结,JWT 参数校验是安全基线的核心环节:颁发者与受众限定避免令牌跨域滥用,对称密钥签名确保传输中不可篡改。将这些策略集中在 TokenValidationParameters,既提高了可读性,也便于在不同环境中以配置方式进行审计与演进,为后续的刷新令牌与密钥轮换奠定基础。

2.2 用户登录与令牌生成流程

登录是获取访问令牌的入口,用户提供凭据(用户名和密码),服务器验证后颁发令牌。令牌颁发过程需要验证密码哈希、构建声明集合、生成令牌字符串等步骤。这个过程必须高效且安全,因为登录是高频操作,任何性能瓶颈或安全漏洞都会被放大。

csharp 复制代码
app.MapPost("/api/auth/login", async (AppDbContext db, IPasswordHasher<User> hasher, LoginRequest req) =>
{
    var user = await db.Users.FirstOrDefaultAsync(x => x.Username == req.Username);
    if (user is null)
        return Results.Unauthorized();

    var verify = hasher.VerifyHashedPassword(user, user.PasswordHash, req.Password);
    if (verify == PasswordVerificationResult.Failed)
        return Results.Unauthorized();

    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Username)
    };

    var token = new JwtSecurityToken(
        issuer: jwtSection["Issuer"],
        audience: jwtSection["Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddHours(2),
        signingCredentials: new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
    );

    return Results.Ok(new { access_token = new JwtSecurityTokenHandler().WriteToken(token) });
});

登录端点接收三个参数:AppDbContext 用于查询用户,IPasswordHasher<User> 用于验证密码,LoginRequest 包含用户提交的凭据。参数通过依赖注入自动解析,这是 Minimal API 的便利之处。

第一步是根据用户名查询用户记录。FirstOrDefaultAsync 会生成 SELECT TOP 1 查询,只返回第一条匹配记录或 null。这里有一个重要的安全考量:无论是用户不存在还是密码错误,我们都返回 Unauthorized,不提供具体的失败原因。这是为了防止用户枚举攻击,攻击者无法通过响应差异来判断用户名是否存在。

密码验证使用 VerifyHashedPassword 方法,它会解析存储的哈希字符串,提取盐值和算法标识,然后对提交的密码执行相同的哈希过程并比较结果。验证结果有三种可能:Success(成功)、Failed(失败)、SuccessRehashNeeded(成功但需要重新哈希)。最后一种情况发生在哈希算法升级时,可以在此时重新哈希密码以提高安全性。

验证通过后,构建 Claims 集合。Claims 是键值对形式的声明,描述用户的身份和属性。ClaimTypes.NameIdentifier 是用户的唯一标识符,通常是数据库主键,这是 ASP.NET Core 身份系统的约定。ClaimTypes.Name 是用户名,可用于显示和日志记录。Claims 会被编码到令牌的 Payload 部分,验证令牌时这些信息会被提取出来。

令牌生成使用 JwtSecurityToken 类,构造函数接收多个参数。issueraudience 对应配置中的值,确保令牌的来源和目标明确。claims 是我们刚构建的声明集合。expires 设置令牌过期时间,这里是 2 小时,过期后令牌失效,用户需要重新登录。过期时间是安全性和用户体验的平衡,太短影响体验,太长增加安全风险。

signingCredentials 指定签名凭据和算法。HmacSha256 是对称签名算法,使用我们之前配置的密钥。签名确保令牌内容不被篡改,任何修改都会导致签名验证失败。

最后,JwtSecurityTokenHandler().WriteToken(token) 将令牌对象序列化为标准的 JWT 字符串,格式为 header.payload.signature,每部分都经过 Base64Url 编码。客户端应该妥善保存这个令牌,在后续请求中携带它来访问受保护资源。整个登录流程体现了认证的完整性和安全性。

作为收尾,本登录流程以最小表面暴露实现了稳健的令牌颁发:统一的失败响应避免枚举攻击,声明集合承载必需的身份信息,过期时间平衡安全与体验。实际工程中可进一步加入刷新令牌与黑名单机制,并通过设备指纹与风控策略加强异常登录检测,使认证体系更贴近生产级要求。

2.3 受保护资源与授权验证

有了令牌颁发机制,下一步是保护需要认证才能访问的资源。[Authorize] 特性是 ASP.NET Core 提供的声明式授权方式,它会拦截请求,验证令牌的有效性,提取用户信息并填充到 ClaimsPrincipal 对象中。这个对象代表当前认证用户,包含了令牌中的所有声明信息。

csharp 复制代码
app.MapGet("/api/auth/profile", [Authorize] async (AppDbContext db, ClaimsPrincipal user) =>
{
    var id = long.Parse(user.FindFirstValue(ClaimTypes.NameIdentifier)!);
    var entity = await db.Users.FindAsync(id);
    return entity is null ? Results.NotFound() : Results.Ok(entity);
});

这个端点定义了获取当前用户信息的 API,它被 [Authorize] 特性标记,意味着只有携带有效令牌的请求才能访问。当请求到达时,JWT Bearer 中间件会自动拦截,从 Authorization 请求头中提取令牌,验证签名和有效期,然后将声明解析为 ClaimsPrincipal 对象。

如果令牌无效、过期或不存在,中间件会直接返回 401 Unauthorized 响应,不会执行处理函数。这种拦截机制保证了受保护资源的安全性,开发者无需在每个端点中手动验证令牌。

处理函数通过参数注入获取 ClaimsPrincipal user 对象,这是当前认证用户的表示。FindFirstValue(ClaimTypes.NameIdentifier) 方法从声明集合中提取用户 ID,这个 ID 是我们在登录时放入令牌的。使用 long.Parse 将字符串转换为 long 类型,因为声明值总是字符串形式。

有了用户 ID,我们可以从数据库中查询用户的完整信息。FindAsync 是高效的主键查询方法,EF Core 会先检查变更追踪器,如果实体已加载则直接返回,否则查询数据库。这避免了重复查询的性能开销。

如果用户不存在(理论上不应该发生,因为令牌中的 ID 来自登录时的验证),返回 404 Not Found。正常情况下返回用户信息,状态码 200 OK。这个端点的典型用途是获取当前登录用户的个人资料,用于前端显示或权限判断。

在更复杂的场景中,我们可以基于角色或自定义声明进行授权。例如 [Authorize(Roles = "Admin")] 限制只有管理员角色才能访问,或者使用策略授权 [Authorize(Policy = "RequireAdminRole")] 实现更灵活的权限控制。授权策略可以组合多个条件,如要求特定声明、角色或自定义逻辑。这套认证授权体系为微服务提供了完整的安全保障,确保只有合法用户才能访问受保护资源。

作为授权部分的小结,声明式的 [Authorize] 与策略授权为资源保护提供了清晰边界;中间件在管线前段完成令牌校验与主体构造,使后续业务仅关心"已认证的谁"。当系统演进到多角色多租户场景时,可在策略中引入租户、范围与细粒度权限校验,从而保持安全与灵活性的动态平衡。

本节实现了一套完整的 JWT 认证授权体系,为微服务架构提供了无状态的安全基础设施。JWT 配置部分明确了令牌验证的核心参数,通过颁发者、受众和签名密钥的三重校验确保令牌的合法性和完整性;登录流程采用统一的失败响应机制防御用户枚举攻击,使用 PBKDF2 密码验证确保凭据安全,并将用户身份信息编码为标准 Claims 嵌入令牌;授权验证通过声明式的 [Authorize] 特性实现资源保护,中间件自动完成令牌校验和用户主体构建,业务代码只需关注已认证用户的身份信息。

这套认证体系的核心优势在于无状态设计,令牌自包含用户信息,无需服务端维护会话状态,天然适合分布式和跨域场景。对称密钥签名在服务间完全信任的前提下提供了简洁高效的实现,未来若需要跨信任边界验证,可平滑升级为 RSA 非对称签名。令牌过期时间的设置在安全性和用户体验之间取得平衡,生产环境中可进一步引入刷新令牌机制,在不牺牲安全性的前提下延长用户会话。

在实际工程中,这套认证授权体系还可以扩展为更复杂的权限模型。基于角色的授权(RBAC)可以通过 [Authorize(Roles = "Admin")] 快速实现粗粒度控制;基于策略的授权支持组合多个条件,可以实现基于资源、租户、时间窗口的细粒度权限校验;在多租户场景下,可以在 Claims 中注入租户标识,结合授权策略实现租户隔离。随着系统演进到多角色多租户架构,这套认证授权体系的扩展性将体现其价值。接下来我们将实现商品服务,展示如何在高并发读取场景下通过缓存策略优化性能。

三、商品服务实现

商品服务是电商平台的核心业务服务,负责管理商品信息、库存数据以及相关的查询操作。与用户服务不同,商品服务面临大量的读取请求,商品详情页是电商系统访问量最大的页面之一。因此,商品服务的设计必须考虑高并发读取的性能优化,缓存策略成为关键设计要点。本节将实现一个包含缓存层的商品服务,展示如何在 .NET Aspire 环境中集成 Redis 缓存。

3.1 商品领域模型与数据库配置

商品模型需要包含商品的基本属性和业务信息。在电商系统中,商品模型可能非常复杂,包含 SKU、规格、图片等多个关联实体。这里我们先实现核心模型,包含名称、描述、价格和库存等基本字段。库存字段的设计尤为重要,它是后续订单处理和库存扣减的基础。

csharp 复制代码
public class Product
{
    public long Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

public class ProductDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();
    public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
}

Product 实体包含六个核心字段。Id 作为主键,采用 long 类型以支持海量商品。Name 是商品名称,应该支持全文搜索和模糊匹配,在实际项目中可以为其创建全文索引或集成 Elasticsearch。Description 存储商品描述,这个字段可能包含富文本或 HTML,需要在前端进行安全处理以防止 XSS 攻击。

Price 使用 decimal 类型而非 floatdouble,这是财务计算的黄金法则。浮点数存在精度问题,在货币计算中可能产生累积误差,而 decimal 是定点数,精度可控且符合财务规范。在数据库中,decimal 映射为 DECIMALNUMERIC 类型,可以指定精度和小数位数,如 decimal(18,2) 表示总共 18 位数字,其中 2 位小数。

Stock 字段存储当前库存数量,这是一个高频更新的字段。在高并发场景下,库存更新可能面临超卖问题,需要通过乐观锁或悲观锁来保证数据一致性。后续章节中我们会详细讨论库存扣减的并发控制策略。CreatedAt 记录商品创建时间,可用于排序和数据分析。

ProductDbContext 是商品服务的数据库上下文,目前只包含一个 Products DbSet。在真实项目中,可能还包括商品分类、品牌、评价等关联实体。保持 DbContext 的简洁性有助于提高查询性能和代码可维护性,复杂的关联查询应该通过仓储模式封装。

本小节收尾强调两点实践经验。其一,价格字段使用 decimal(18,2) 等精度设置时,需在迁移与数据库层保持一致以避免舍入误差;其二,库存字段后续会涉及并发扣减与一致性问题,建议预留版本号或事件溯源设计,以便在高并发与审计场景下维持数据可信度。

3.2 分布式缓存集成与查询优化

商品详情查询是典型的读多写少场景,非常适合引入缓存层。ASP.NET Core 提供了 IDistributedCache 抽象接口,支持多种缓存实现(内存缓存、Redis、SQL Server 等)。在 .NET Aspire 环境中,Redis 缓存的配置和注入都由 AppHost 自动完成,我们只需专注于缓存逻辑的实现。

csharp 复制代码
// 查询商品详情,优先读取缓存
app.MapGet("/api/products/{id:long}", async (ProductDbContext db, IDistributedCache cache, long id) =>
{
    var cacheKey = $"product:detail:{id}";
    var cached = await cache.GetStringAsync(cacheKey);
    if (cached is not null)
        return Results.Ok(JsonSerializer.Deserialize<Product>(cached));

    var product = await db.Products.FindAsync(id);
    if (product is null) return Results.NotFound();

    await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product),
        new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60) });
    return Results.Ok(product);
});

查询端点首先构建缓存键 product:detail:{id},这是一个命名空间风格的键设计,有助于组织和管理缓存数据。使用 GetStringAsync 方法尝试从缓存中获取商品详情,如果命中缓存,直接反序列化并返回结果,避免了数据库查询的开销。

作为小结,当前实现采用 Cache-Aside 模式:读路径优先命中缓存,未命中回源数据库并写回缓存;在写路径上,商品更新或下架需执行缓存失效以维持数据新鲜度。TTL 的设置应结合业务侧页面停留与更新频率,避免缓存雪崩可通过随机过期与多层降级策略缓解,从而在高并发读取场景中稳住延迟与成本。

3.3 商品创建与数据验证

商品创建是管理后台的核心功能,需要确保输入数据的合法性和完整性。虽然简单的 CRUD 操作看似平凡,但细节决定成败。良好的数据验证、错误处理和响应设计能显著提升 API 的健壮性和可用性。在商品创建中,我们需要验证价格的合理性、库存的非负性以及必填字段的完整性。

csharp 复制代码
app.MapPost("/api/products", async (ProductDbContext db, CreateProductRequest req) =>
{
    var entity = new Product
    {
        Name = req.Name,
        Description = req.Description,
        Price = req.Price,
        Stock = req.Stock
    };
    db.Products.Add(entity);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{entity.Id}", new { entity.Id });
});

CreateProductRequest 是一个 DTO(数据传输对象),用于封装客户端提交的商品数据。我们可以在这个类上使用数据注解(Data Annotations)来实现基本的验证,如 [Required][Range] 等,确保传入的数据符合业务规则。

在结尾处,商品创建的响应以 201 Created 返回资源定位,符合 REST 约定;在工程化层面应补充服务端验证与异常映射,避免将数据库约束泄漏为不友好的 500。对于热门商品可以在创建后进行预热缓存以减少冷启动延迟,并在后续更新与库存变更时配合失效策略保证读路径的一致性。

本节完成了商品服务的核心实现,展示了如何在高并发读取场景下通过缓存策略优化性能。商品领域模型使用 decimal 类型存储价格确保财务计算精度,库存字段为后续的并发扣减和一致性控制预留了扩展空间;分布式缓存集成采用 Cache-Aside 模式,在 .NET Aspire 环境中通过 IDistributedCache 抽象接口无缝对接 Redis,查询路径优先命中缓存,未命中时回源数据库并写回缓存,TTL 设置为 60 分钟平衡了数据新鲜度和缓存命中率;商品创建端点遵循 RESTful 规范返回 201 状态码和资源定位,为后续的数据验证和异常处理留下了清晰的扩展点。

商品服务的实现揭示了电商系统中读多写少场景的优化策略。商品详情页作为电商平台访问量最大的页面,缓存的引入能够将数据库查询压力降低一到两个数量级,从而支撑更高的并发访问。在实际工程中,缓存策略需要配合缓存失效机制,当商品信息更新、下架或库存变化时,需要主动清除或更新缓存以保证数据一致性。对于热门商品,可以采用缓存预热策略,在商品上架或促销活动开始前提前加载到缓存,避免冷启动时的缓存击穿。

四、总结

至此,我们已经完成了用户服务、认证授权服务和商品服务三个核心服务的实现,为电商平台搭建了坚实的基础。这些服务在 .NET Aspire 的编排下协同工作,通过 AppHost 自动完成资源注入和服务发现,通过 ServiceDefaults 共享可观测性和弹性策略。下一篇将继续深入,实现订单、库存、支付等更复杂的业务逻辑,并引入消息队列处理异步流程,让整个微服务架构更加完善和贴近生产实际。

相关推荐
PfCoder3 小时前
C#中定时器之System.Timers.Timer
c#·.net·visual studio·winform
一点程序4 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
怪兽源码6 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
csdn_aspnet7 小时前
ASP.NET Core 中的依赖注入
后端·asp.net·di·.net core
ahxdyz8 小时前
.NET平台MCP
ai·.net·mcp
昊坤说不出的梦8 小时前
【实战】监控上下文切换及其优化方案
java·后端
疯狂踩坑人8 小时前
【Python版 2026 从零学Langchain 1.x】(二)结构化输出和工具调用
后端·python·langchain
の天命喵星人8 小时前
.net 使用NLog记录日志
.net
绿荫阿广9 小时前
将SignalR移植到Esp32—让小智设备无缝连接.NET功能拓展MCP服务
.net·asp.net core·mcp
橘子师兄9 小时前
C++AI大模型接入SDK—ChatSDK封装
开发语言·c++·人工智能·后端