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 共享可观测性和弹性策略。下一篇将继续深入,实现订单、库存、支付等更复杂的业务逻辑,并引入消息队列处理异步流程,让整个微服务架构更加完善和贴近生产实际。

相关推荐
Way2top18 小时前
Go语言动手写Web框架 - Gee第四天 分组控制
后端·go
李梨同学丶18 小时前
好虫子周刊:1-bit LLM、物理 AI、DeepSeek-R1
后端
bruce_哈哈哈19 小时前
go语言初认识
开发语言·后端·golang
最贪吃的虎19 小时前
Redis其实并不是线程安全的
java·开发语言·数据库·redis·后端·缓存·lua
武子康19 小时前
大数据-208 岭回归与Lasso回归:区别、应用与选择指南
大数据·后端·机器学习
qq_124987075319 小时前
基于springboot归家租房小程序的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·小程序·毕业设计·计算机毕业设计
moxiaoran575319 小时前
Go语言的接口
开发语言·后端·golang
清风徐来QCQ19 小时前
Cookie和JWT
后端·cookie
2301_7806698619 小时前
List(特有方法、遍历方式、ArrayList底层原理、LinkedList底层原理,二者区别)
java·数据结构·后端·list