在面向企事业单位的 B2B SaaS 与服务交易系统中,商品与服务目录(Catalog)绝非简单的“电商商品列表”:
- 服务包与复杂 SKU(Configurable Offerings):一个云服务或 SaaS 订阅可能包含基础席位数、存储空间包、AI Token 扩展包等动态附加包(Add-ons);
- 租户专属定制价(Tiered & Custom Pricing):不同企业租户由于战略采购合同,享有不同的折扣矩阵、阶梯计费与私有服务项;
- 发布版本快照与防窜改(Version Snapshotting):一旦订单生成,历史购买的服务规格必须永久冻结快照,严禁因后续商品调价影响已生效订单!
BitzOrcas.Modern Catalog 模块构建了“商品聚合 + 规格版本快照 + 动态定价引擎”三位一体的目录底座。
Catalog 目录体系全生命周期流转拓扑
第一步:核心领域聚合根 OfferingAggregate
服务规格采用统一聚合根范式,直接映射物理表 CatOffering:
using System;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Catalog.Domain;
public static class CatalogErrors{ public static readonly Error Archived = Error.Conflict("Catalog.Archived", "已归档的商品规格无法发布新版本。");}
[BitzTable("CatOffering", IsTenant = true, IsSoftDelete = true, Description = "服务与商品规格表")][BitzIndex("IX_CatOffering_Code", nameof(Code), nameof(TenantId), IsUnique = true)]public sealed class OfferingAggregate : TenantAggregateRoot<string>{ [BitzColumn(Length = 64, IsRequired = true)] public string Code { get; private set; } = string.Empty;
[BitzColumn(Length = 120, IsRequired = true)] public string Name { get; private set; } = string.Empty;
[BitzColumn(Length = 2000)] public string Description { get; private set; } = string.Empty;
// 当前激活的正式版本号 [BitzColumn(IsRequired = true)] public int ActiveVersion { get; private set; } = 1;
// 基础包定价与计费周期(按月/按年) [BitzColumn(Precision = 18, Scale = 4, IsRequired = true)] public decimal BasePrice { get; private set; }
[BitzColumn(IsRequired = true)] public OfferingStatus Status { get; private set; } = OfferingStatus.Draft;
[Obsolete("仅供 ORM 持久化物化使用。请使用 Create 工厂方法。", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public OfferingAggregate() : base("0") { }
// 领域方法:发布新版本并冻结快照 public Result PublishNewVersion(decimal newPrice, string operatorId) { // 1. 业务不变量检查:已归档商品禁止发布 if (Status == OfferingStatus.Archived) { return Result.Failure(CatalogErrors.Archived); }
// 2. 状态机递进 BasePrice = newPrice; ActiveVersion++; Status = OfferingStatus.Active;
// 3. 发布版本事件 AddDomainEvent(new OfferingPublishedDomainEvent(Id, Code, ActiveVersion, BasePrice, operatorId));
return Result.Success(); }}第二步:只读查询切片与强类型 QueryShape
前端商品列表查询直接利用 IDeclareQueryShape 零样板实现高效分页:
using BitzOrcas.Application.Abstractions.Queries;using BitzOrcas.Domain.Results;using BitzOrcas.Endpoint.Attributes;using Mediator;
namespace BitzOrcas.Catalog.Application.Queries;
// 1. 声明式查询端点:编译期生成 Minimal API 路由映射与 OpenAPI 描述[GenerateEndpoint(HttpRoute.Query, "/api/catalog/offerings", Tag = "Catalog")]// 2. 强类型查询契约:携带过滤字段与标准化分页参数public sealed record ListOfferingsQuery( string? Keyword, OfferingStatus? Status, int PageIndex = 1, int PageSize = 20) : IQuery<Result<PagedList<OfferingDto>>>;总结
Catalog 模块通过清晰的领域抽象保障高可用:
- 版本快照隔离:保证订单历史真实性,永不因商品调价引起财务争议;
- 双 ORM 映射:EF Core 支撑复杂聚合写,SqlSugar / Dapper 支撑百万级商品高频读;
- 事件驱动索引:商品上下架毫秒级通知搜索中心更新倒排索引。