Skip to content
bitzorcas
中EN

Concept

Ticketing 工单模块架构概览:多租户协作、SLA 履约与事件驱动

深入解析 BitzOrcas.Modern 企业级工单协作体系,掌握 TicketAggregate 聚合根、SLA 升级告警状态机、工单流转与工单只读投影。

Last updated

在企业级客户支持与内部 IT 协作场景中,工单系统(Ticketing Subsystem)面临复杂的业务流转与履约考核挑战:

  • SLA 履约违约风险(SLA Breach):高优先级工单(如 P0 故障)若在 15 分钟内未响应,必须自动触发升级调度与多渠道催办通知;
  • 并发指派与工单状态冲突(Concurrent Assignment Conflict):多名客服同时点击“抢单”或转派给不同工程师,若无并发版本控制会导致状态混乱;
  • 多租户数据穿透防护:租户 A 的工单及其内部附件绝不能被租户 B 查询或下载。

BitzOrcas.Modern Ticketing 模块以“统一聚合根金样板 + SLA 状态机 + 事件驱动通知”构建了现代化服务台底座。

工单生命周期流转与 SLA 考核拓扑

超时未响应

1. 客户提交工单 (CreateTicketCommand)

2. 垂直切片 Handler

3. Ticket 聚合根 (初始化状态: Open, 计算 SLA 响应截至时间)

4. IAggregateRepository (本地事务保存)

5. CAP 领域事件 (ticket.created)

6. SLA 监控调度器 (Quartz 自动注册超时检查 Job)

7. NotificationConsumer (异步向客服组发送待办通知)

8. EscalateTicketCommand (自动升级为 P0 并通知主管)


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

Ticket.cs: 业务工单聚合根
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Ticket.Domain;
public static class TicketErrors
{
public static readonly Error AlreadyClosed =
Error.Conflict("Ticket.AlreadyClosed", "已关闭的工单无法指派。");
public static readonly Error NotFound =
Error.NotFound("Ticket.NotFound", "工单不存在。");
}
[BitzTable("SysTicket", IsTenant = true, IsSoftDelete = true, Description = "业务工单主表")]
[BitzIndex("IX_SysTicket_Status_Tenant", nameof(Status), nameof(TenantId))]
public sealed class Ticket : TenantAggregateRoot<string>
{
[BitzColumn(Length = 200, IsRequired = true)]
public string Title { get; private set; } = string.Empty;
[BitzColumn(Length = 4000, IsRequired = true)]
public string Description { get; private set; } = string.Empty;
[BitzColumn(IsRequired = true)]
public TicketPriority Priority { get; private set; } = TicketPriority.Medium;
[BitzColumn(IsRequired = true)]
public TicketStatus Status { get; private set; } = TicketStatus.Open;
[BitzColumn(Length = 64)]
public string? AssigneeUserId { get; private set; }
[BitzColumn(IsRequired = true)]
public DateTimeOffset SlaResponseDueAt { get; private set; }
[Obsolete("仅供 ORM 持久化物化使用。请使用 Create 工厂方法。", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Ticket()
: base("0")
{
}
// 领域方法:指派工单并进入处理中
public Result AssignTo(string engineerUserId, string operatorId, DateTimeOffset now)
{
if (Status == TicketStatus.Closed || Status == TicketStatus.Resolved)
{
return Result.Failure(TicketErrors.AlreadyClosed);
}
AssigneeUserId = engineerUserId;
Status = TicketStatus.InProgress;
// 产生指派领域事件
AddDomainEvent(new TicketAssignedDomainEvent(Id, TenantId, engineerUserId, operatorId, now));
return Result.Success();
}
}

第二步:指派工单命令垂直切片(AssignTicketCommandHandler)

AssignTicketCommandHandler.cs: 工单指派切片
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions.Security;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
using BitzOrcas.Ticket.Domain;
public sealed class AssignTicketCommandHandler(
ICommandRepository<Ticket, string> ticketRepo,
ICurrentUser currentUser,
IAppClock clock)
{
public async ValueTask<Result> Handle(AssignTicketCommand command, CancellationToken ct)
{
// 1. 加载目标工单聚合
var ticketResult = await ticketRepo.FindAsync(command.TicketId, ct);
if (ticketResult.IsFailure)
{
return Result.Failure(TicketErrors.NotFound);
}
var ticket = ticketResult.Value;
// 2. 执行领域指派
var assignResult = ticket.AssignTo(command.AssigneeUserId, currentUser.UserId, clock.UtcNow);
if (assignResult.IsFailure) return assignResult;
// 3. 持久化聚合(自动检查乐观并发版本,由 TransactionPipeline 提交)
await ticketRepo.UpdateAsync(ticket, ct);
return Result.Success();
}
}

总结

Ticketing 模块为客服与售后赋能:

  • 乐观锁防并发抢单:并发修改自动检测版本冲突并返回 409;
  • 全链路 SLA 监控:基于时钟端口精准计算超时升级;
  • 事件驱动多渠道通知:状态变更毫秒级通知客户与工程师。

100%

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