You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DDD实现按请求类型和发起者分配审批人的方案问询

DDD + State Pattern for Request Approval System: Solutions to Your Challenges

Awesome question—let’s walk through how to solve these DDD and state pattern hurdles without relying on external workflow tools. I’ve built similar approval systems using DDD principles, so here’s what I’d recommend:

1. Where to Put the Next Approver Logic? Request Aggregate vs State Classes

The key here is to split responsibilities cleanly, per DDD and single-responsibility principles:

  • State classes: Focus solely on state transition rules (e.g., "from AwaitingApproval, what state comes next when approved?"). They shouldn’t hold business data about the request itself.
  • Request aggregate root: Owns all core business data (request types, initiator level, amount increase) and handles the logic to calculate the next approver. This keeps your state classes lightweight and reusable, while keeping business rules encapsulated within the aggregate that owns the data.

Here’s how that might look in code:

class Request {
    public RequestType[] RequestTypes { get; }
    public Employee Initiator { get; }
    public decimal AmountIncrease { get; }
    public List<ApprovalHistoryEntry> ApprovalHistory { get; private set; } = new();
    private IRequestState _currentState;
    public string CurrentAssignee { get; private set; }

    public Request(RequestType[] types, Employee initiator, decimal amountIncrease) {
        RequestTypes = types;
        Initiator = initiator;
        AmountIncrease = amountIncrease;
        _currentState = new AwaitingApprovalState();
        // Handle initial assignment (including Rule 1)
        CurrentAssignee = GetInitialApprover();
    }

    // Core logic to calculate initial approver (handles Rule 1)
    private string GetInitialApprover() {
        // Rule 1: Check if all request types skip intermediate approval
        var skipIntermediate = RequestTypes.All(t => !t.RequiresIntermediateApproval);
        if (skipIntermediate) {
            return GetProcessingDepartmentContact();
        }
        // Fall back to initiator level rules (Rule 2/3)
        return Initiator.Level == EmployeeLevel.Level1 ? GetLevel2Manager() : GetLevel3Manager();
    }

    // Calculate next approver based on current state and request data
    private string GetNextApprover() {
        return _currentState.Type switch {
            RequestStateType.AwaitingApproval => Initiator.Level == EmployeeLevel.Level1 ? GetLevel2Manager() : GetLevel3Manager(),
            RequestStateType.Level2Approved => GetLevel3Manager(),
            RequestStateType.Level3Approved => 
                // Rule 4: Force Level4 if amount > $500, else processing dept
                AmountIncrease > 500 ? GetLevel4Manager() : GetProcessingDepartmentContact(),
            RequestStateType.Level4Approved => GetProcessingDepartmentContact(),
            _ => throw new InvalidOperationException("Unsupported state for approval")
        };
    }

    public void Approve() {
        // Let the state class handle state transition
        var nextState = _currentState.Approve(this);
        // Log the approval to history
        ApprovalHistory.Add(new ApprovalHistoryEntry(_currentState.Type, CurrentAssignee, DateTime.UtcNow, ApprovalAction.Approved));
        _currentState = nextState;
        // Assign next approver if we're not in a terminal state
        if (!_currentState.IsTerminal) {
            CurrentAssignee = GetNextApprover();
        }
    }

    // Helper methods (GetLevel2Manager, GetProcessingDepartmentContact, etc.)
}

// State interface with type tracking for history
interface IRequestState {
    RequestStateType Type { get; }
    IRequestState Approve(Request request);
    IRequestState Reject(Request request);
    IRequestState RejectWithChanges(Request request);
    bool IsTerminal { get; }
}

class AwaitingApprovalState : IRequestState {
    public RequestStateType Type => RequestStateType.AwaitingApproval;
    public bool IsTerminal => false;

    public IRequestState Approve(Request request) {
        // State transition logic based on initiator level
        return request.Initiator.Level == EmployeeLevel.Level1 
            ? new Level2ApprovedState() 
            : new Level3ApprovedState();
    }

    // Implement Reject and RejectWithChanges...
}

public enum RequestStateType {
    AwaitingApproval, Level2Approved, Level3Approved, Level4Approved, ProcessingDeptPending, Approved, Rejected
}

2. Implementing "Directly Assign to Processing Department" Logic

As shown in the GetInitialApprover method above:

  1. Prioritize Rule 1 first: Check if all request types don’t require intermediate approval. If true, skip all manager approvals and assign directly to the processing department.
  2. Create a dedicated state (e.g., ProcessingDeptPendingState) for this scenario. When approved, this state transitions directly to a terminal ApprovedState since no further approvals are needed.

This ensures Rule 1 takes precedence over initiator-level rules, as specified in your requirements.

3. Handling Reject With Changes: Is StatusHistory Valid?

Absolutely—tracking approval history is not just valid, it’s a core requirement for this domain. Your ApprovalHistory (I’d call it ApprovalHistoryEntry as a value object, since it’s part of the Request aggregate and has no independent lifecycle) will let you:

  • Verify that the target approver is a valid previous participant in the flow
  • Revert to the correct state for that approver
  • Audit the entire flow for compliance

Here’s how to implement the backtracking logic:

class Request {
    public void RejectWithChanges(string targetAssignee, string reason) {
        // Find the historical entry where this user approved the request
        var targetEntry = ApprovalHistory.FirstOrDefault(
            h => h.Assignee == targetAssignee && h.Action == ApprovalAction.Approved
        );

        if (targetEntry == null) {
            throw new InvalidOperationException("Cannot revert to a user who hasn't approved this request");
        }

        // Revert to the state before this user's approval
        _currentState = MapStateTypeToState(targetEntry.PreviousStateType);
        CurrentAssignee = targetAssignee;
        // Log the revert action
        ApprovalHistory.Add(new ApprovalHistoryEntry(
            _currentState.Type, 
            CurrentAssignee, 
            DateTime.UtcNow, 
            ApprovalAction.RejectedWithChanges, 
            reason
        ));
    }

    // Helper to convert state enum back to state object
    private IRequestState MapStateTypeToState(RequestStateType type) {
        return type switch {
            RequestStateType.AwaitingApproval => new AwaitingApprovalState(),
            RequestStateType.Level2Approved => new Level2ApprovedState(),
            // ... map all state types
            _ => throw new InvalidOperationException("Unknown state type")
        };
    }
}

// Value object for approval history
public record ApprovalHistoryEntry(
    RequestStateType PreviousStateType,
    string Assignee,
    DateTime Timestamp,
    ApprovalAction Action,
    string? Reason = null
);

public enum ApprovalAction { Approved, Rejected, RejectedWithChanges }

By storing the state type (not the state object) in history, you avoid serialization issues and keep your aggregate persistence-friendly.

Bonus: Keep Rules Flexible (Optional)

If your approval rules might change over time, you can extract the approver calculation logic into a domain service (e.g., ApprovalRoutingService) instead of keeping it directly in the Request aggregate. This keeps the Request focused on state management and flow execution, while the service handles the complex rule logic.


内容的提问来源于stack exchange,提问作者lostinwpf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:02:55