Skip to content
bitzorcas
中EN

Concept

Catalog 模块架构概览:多租户商品与服务目录体系

深入解析 BitzOrcas.Modern Catalog 目录体系,掌握服务规格(OfferingAggregate)、SKU 属性集、阶梯定价与多版本发布管理。

Last updated

在面向企事业单位的 B2B SaaS 与服务交易系统中,商品与服务目录(Catalog)绝非简单的“电商商品列表”:

  • 服务包与复杂 SKU(Configurable Offerings):一个云服务或 SaaS 订阅可能包含基础席位数、存储空间包、AI Token 扩展包等动态附加包(Add-ons);
  • 租户专属定制价(Tiered & Custom Pricing):不同企业租户由于战略采购合同,享有不同的折扣矩阵、阶梯计费与私有服务项;
  • 发布版本快照与防窜改(Version Snapshotting):一旦订单生成,历史购买的服务规格必须永久冻结快照,严禁因后续商品调价影响已生效订单!

BitzOrcas.Modern Catalog 模块构建了“商品聚合 + 规格版本快照 + 动态定价引擎”三位一体的目录底座。

Catalog 目录体系全生命周期流转拓扑

1. 运营管理员 (发布/调整服务规格)

2. CreateOfferingCommand / PublishVersionCommand

3. 垂直切片 Handler

4. OfferingAggregate 统一聚合根 (草稿 -> 审核 -> 激活发布)

5. OfferingVersionSnapshot (生成不可变版本快照)

6. IAggregateRepository

7. CAP 领域事件 (catalog.offering_published)

8. Search Consumer (异步同步 ElasticSearch / 向量索引)


第一步:核心领域聚合根 OfferingAggregate

服务规格采用统一聚合根范式,直接映射物理表 CatOffering:

OfferingAggregate.cs: 服务规格领域聚合根
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 零样板实现高效分页:

ListOfferingsQuery.cs: 商品列表查询切片
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 支撑百万级商品高频读;
  • 事件驱动索引:商品上下架毫秒级通知搜索中心更新倒排索引。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%