diff --git a/sql/migrate_t_customer_account_flow.sql b/sql/migrate_t_customer_account_flow.sql
new file mode 100644
index 0000000..9e9078b
--- /dev/null
+++ b/sql/migrate_t_customer_account_flow.sql
@@ -0,0 +1,23 @@
+-- 客户账户资金流水
+CREATE TABLE IF NOT EXISTS `t_customer_account_flow` (
+ `Id` int NOT NULL AUTO_INCREMENT,
+ `CustomerId` int NOT NULL COMMENT '客户 ID',
+ `SerialNumber` varchar(64) NOT NULL COMMENT '流水号',
+ `BizType` varchar(32) NOT NULL COMMENT '业务类型 recharge/orderPay',
+ `BizId` int DEFAULT NULL COMMENT '关联业务主键',
+ `BizNo` varchar(64) DEFAULT NULL COMMENT '关联业务单号',
+ `Amount` decimal(18,2) NOT NULL COMMENT '变动金额,正入账负出账',
+ `BalanceBefore` decimal(18,2) NOT NULL COMMENT '变动前余额',
+ `BalanceAfter` decimal(18,2) NOT NULL COMMENT '变动后余额',
+ `Currency` varchar(8) NOT NULL DEFAULT 'CNY' COMMENT '币种',
+ `Remark` varchar(512) DEFAULT NULL COMMENT '备注',
+ `IsDeleted` tinyint(1) NOT NULL DEFAULT '0',
+ `CreateUserId` int NOT NULL DEFAULT '0',
+ `CreateTime` datetime NOT NULL,
+ `UpdateUserId` int DEFAULT NULL,
+ `UpdateTime` datetime DEFAULT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_customer_account_flow_serial` (`SerialNumber`),
+ KEY `idx_customer_account_flow_customer_time` (`CustomerId`, `CreateTime`),
+ KEY `idx_customer_account_flow_biz` (`BizType`, `BizId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户账户资金流水';
diff --git a/src/Oms.Application/Dtos/Customers/CustomerAccountFlowOutput.cs b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowOutput.cs
new file mode 100644
index 0000000..1997c6c
--- /dev/null
+++ b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowOutput.cs
@@ -0,0 +1,35 @@
+namespace Oms.Application.Dtos.Customers;
+
+///
+/// 客户账户流水列表项。
+///
+public class CustomerAccountFlowOutput
+{
+ public int Id { get; set; }
+
+ public int CustomerId { get; set; }
+
+ public string? CustomerCode { get; set; }
+
+ public string SerialNumber { get; set; } = string.Empty;
+
+ public string BizType { get; set; } = string.Empty;
+
+ public string? BizTypeName { get; set; }
+
+ public int? BizId { get; set; }
+
+ public string? BizNo { get; set; }
+
+ public decimal Amount { get; set; }
+
+ public decimal BalanceBefore { get; set; }
+
+ public decimal BalanceAfter { get; set; }
+
+ public string Currency { get; set; } = "CNY";
+
+ public string? Remark { get; set; }
+
+ public DateTime CreateTime { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs
new file mode 100644
index 0000000..50198ad
--- /dev/null
+++ b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs
@@ -0,0 +1,27 @@
+namespace Oms.Application.Dtos.Customers;
+
+///
+/// 客户账户流水分页查询入参。
+///
+public class CustomerAccountFlowPaginatedInput
+{
+ public int Page { get; set; } = 1;
+
+ public int PageSize { get; set; } = 20;
+
+ ///
+ /// 流水号或关联单号关键词。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 业务类型:recharge / orderPay,空表示全部。
+ ///
+ public string? BizType { get; set; }
+
+ public int? CustomerId { get; set; }
+
+ public DateTime? StartTime { get; set; }
+
+ public DateTime? EndTime { get; set; }
+}
diff --git a/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs b/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs
new file mode 100644
index 0000000..42991af
--- /dev/null
+++ b/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs
@@ -0,0 +1,17 @@
+namespace Oms.Application.Dtos.Orders;
+
+///
+/// 各状态订单数量统计(单次聚合查询,非分页列表)。
+///
+public class OrderStatusCountsOutput
+{
+ ///
+ /// 全部订单数量。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 按 OrderStatus 分组的数量,key 为状态码。
+ ///
+ public Dictionary Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+}
diff --git a/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs b/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs
new file mode 100644
index 0000000..1ae82e2
--- /dev/null
+++ b/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs
@@ -0,0 +1,129 @@
+using System.Linq.Expressions;
+using Fake.Application;
+using Microsoft.AspNetCore.Mvc;
+using Oms.Application.Dtos.Customers;
+using Oms.Application.Dtos.Shared;
+using Oms.Application.Shared;
+using Oms.Domain.CustomersAggregate.Consts;
+using Oms.Domain.CustomersAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Repositories;
+
+namespace Oms.Application.Services.Customers;
+
+///
+/// 客户账户流水 API。
+///
+[ApiExplorerSettings(GroupName = "OMS-Customer")]
+public class CustomerAccountFlowService : ApplicationService
+{
+ private ICustomerAccountFlowRepository FlowRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ private ICustomerRepository CustomerRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ ///
+ /// 分页查询客户账户流水。
+ ///
+ [HttpGet("/customer/account-flow/list")]
+ public async Task> GetPaginatedListAsync(
+ [FromQuery] CustomerAccountFlowPaginatedInput input)
+ {
+ var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
+ CustomerRepository,
+ CurrentUser,
+ input.CustomerId);
+ var predicate = BuildPaginatedPredicate(input, customerId);
+ var pageIndex = OmsDocHelper.NormalizePage(input.Page);
+ var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
+
+ var entities = await FlowRepository.GetPagedListAsync(
+ predicate,
+ pageIndex,
+ pageSize,
+ new Dictionary
+ {
+ [nameof(CustomerAccountFlow.CreateTime)] = false,
+ [nameof(CustomerAccountFlow.Id)] = false
+ });
+
+ var total = await FlowRepository.CountAsync(predicate);
+ var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
+ var customers = customerIds.Length == 0
+ ? []
+ : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
+ var customerMap = customers.ToDictionary(x => x.Id);
+
+ var items = entities.Select(entity =>
+ {
+ customerMap.TryGetValue(entity.CustomerId, out var customer);
+ return new CustomerAccountFlowOutput
+ {
+ Id = entity.Id,
+ CustomerId = entity.CustomerId,
+ CustomerCode = customer?.CustomerCode,
+ SerialNumber = entity.SerialNumber,
+ BizType = entity.BizType,
+ BizTypeName = GetBizTypeName(entity.BizType),
+ BizId = entity.BizId,
+ BizNo = entity.BizNo,
+ Amount = entity.Amount,
+ BalanceBefore = entity.BalanceBefore,
+ BalanceAfter = entity.BalanceAfter,
+ Currency = entity.Currency,
+ Remark = entity.Remark,
+ CreateTime = entity.CreateTime
+ };
+ }).ToList();
+
+ return new OmsPageResult
+ {
+ Items = items,
+ Total = total
+ };
+ }
+
+ private static Expression> BuildPaginatedPredicate(
+ CustomerAccountFlowPaginatedInput input,
+ int? customerId)
+ {
+ var keyword = input.Keyword?.Trim();
+ var bizType = input.BizType?.Trim();
+ var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
+ var hasBizType = !string.IsNullOrWhiteSpace(bizType)
+ && !string.Equals(bizType, "all", StringComparison.OrdinalIgnoreCase);
+ var hasCustomerFilter = customerId.HasValue;
+ var customerIdValue = customerId.GetValueOrDefault();
+
+ if (input.StartTime.HasValue && input.EndTime.HasValue)
+ {
+ var start = input.StartTime.Value;
+ var end = input.EndTime.Value;
+ return flow =>
+ (!hasCustomerFilter || flow.CustomerId == customerIdValue) &&
+ (!hasBizType || flow.BizType == bizType) &&
+ (!hasKeyword ||
+ flow.SerialNumber.Contains(keyword!) ||
+ (flow.BizNo ?? string.Empty).Contains(keyword!)) &&
+ flow.CreateTime >= start &&
+ flow.CreateTime <= end;
+ }
+
+ return flow =>
+ (!hasCustomerFilter || flow.CustomerId == customerIdValue) &&
+ (!hasBizType || flow.BizType == bizType) &&
+ (!hasKeyword ||
+ flow.SerialNumber.Contains(keyword!) ||
+ (flow.BizNo ?? string.Empty).Contains(keyword!));
+ }
+
+ private static string GetBizTypeName(string bizType)
+ {
+ return bizType switch
+ {
+ CustomerAccountFlowBizTypes.Recharge => "充值入账",
+ CustomerAccountFlowBizTypes.OrderPay => "订单支付",
+ _ => bizType
+ };
+ }
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index d582717..1b794bb 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -8,6 +8,7 @@ using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
+using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
@@ -58,6 +59,28 @@ public partial class OrderService : ApplicationService
return new OmsPageResult { Items = await MapOrderOutputsAsync(orders), Total = total };
}
+ ///
+ /// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。
+ ///
+ [HttpGet("/order/status-counts")]
+ public async Task GetStatusCountsAsync()
+ {
+ var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
+ CustomerRepository,
+ CurrentUser,
+ null);
+ var rows = await OrderRepository.GetStatusCountsAsync(customerId);
+ var counts = rows.ToDictionary(
+ x => x.Status,
+ x => x.Count,
+ StringComparer.OrdinalIgnoreCase);
+ return new OrderStatusCountsOutput
+ {
+ Total = rows.Sum(x => x.Count),
+ Counts = counts
+ };
+ }
+
///
/// 查询订单详情。
///
@@ -91,6 +114,44 @@ public partial class OrderService : ApplicationService
public async Task PayAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
+ if (order.OrderStatus == OrderStatusCodes.PendingShip && order.PayTime.HasValue)
+ {
+ throw new BusinessException("订单已支付");
+ }
+
+ if (order.OrderStatus != OrderStatusCodes.PendingPayment)
+ {
+ throw new BusinessException("当前订单状态不可支付");
+ }
+
+ if (!order.CustomerId.HasValue)
+ {
+ throw new BusinessException("订单未关联客户,无法支付");
+ }
+
+ var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
+ var payAmount = fees
+ .Where(x => x.Type == OrderFeeTypeCodes.Receivable)
+ .Sum(x => x.Amount);
+ if (payAmount <= 0)
+ {
+ throw new BusinessException("订单应收金额无效,无法支付");
+ }
+
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
+ if (customer == null)
+ {
+ throw new BusinessException("客户不存在");
+ }
+
+ var balanceMutator = LazyServiceProvider.GetRequiredService();
+ await balanceMutator.DebitOrderPayAsync(
+ customer,
+ payAmount,
+ order.Id,
+ order.ServiceNo,
+ $"订单支付 {order.ServiceNo}");
+
order.PayTime = DateTime.Now;
order.OrderStatus = OrderStatusCodes.PendingShip;
await UpdateOrderAsync(order);
diff --git a/src/Oms.Application/Services/Recharge/RechargeService.cs b/src/Oms.Application/Services/Recharge/RechargeService.cs
index bba5176..d624f5f 100644
--- a/src/Oms.Application/Services/Recharge/RechargeService.cs
+++ b/src/Oms.Application/Services/Recharge/RechargeService.cs
@@ -96,10 +96,15 @@ public class RechargeService : ApplicationService
record.AuditTime = now;
record.AuditBy = CurrentUser.UserName;
- customer.Balance += record.RechargeAmount;
+ var balanceMutator = LazyServiceProvider.GetRequiredService();
+ await balanceMutator.CreditRechargeAsync(
+ customer,
+ record.RechargeAmount,
+ record.Id,
+ record.SerialNumber,
+ input.Remark ?? "充值审核通过");
await RechargeRecordRepository.UpdateAsync(record);
- await CustomerRepository.UpdateAsync(customer);
}
///
diff --git a/src/Oms.Application/Shared/CustomerBalanceMutator.cs b/src/Oms.Application/Shared/CustomerBalanceMutator.cs
new file mode 100644
index 0000000..0e7c839
--- /dev/null
+++ b/src/Oms.Application/Shared/CustomerBalanceMutator.cs
@@ -0,0 +1,119 @@
+using Fake.DependencyInjection;
+using Fake.Application;
+using Oms.Domain.CustomersAggregate.Consts;
+using Oms.Domain.CustomersAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Repositories;
+
+namespace Oms.Application.Shared;
+
+///
+/// 客户余额变动与流水写入。
+///
+public class CustomerBalanceMutator : IScopedDependency
+{
+ private readonly ICustomerRepository _customerRepository;
+ private readonly ICustomerAccountFlowRepository _flowRepository;
+
+ public CustomerBalanceMutator(
+ ICustomerRepository customerRepository,
+ ICustomerAccountFlowRepository flowRepository)
+ {
+ _customerRepository = customerRepository;
+ _flowRepository = flowRepository;
+ }
+
+ public Task CreditRechargeAsync(
+ Customer customer,
+ decimal amount,
+ int rechargeRecordId,
+ string rechargeSerialNumber,
+ string? remark,
+ CancellationToken cancellationToken = default)
+ {
+ return ApplyAsync(
+ customer,
+ amount,
+ CustomerAccountFlowBizTypes.Recharge,
+ rechargeRecordId,
+ rechargeSerialNumber,
+ remark,
+ cancellationToken);
+ }
+
+ public Task DebitOrderPayAsync(
+ Customer customer,
+ decimal amount,
+ int orderId,
+ string serviceNo,
+ string? remark,
+ CancellationToken cancellationToken = default)
+ {
+ if (amount <= 0)
+ {
+ throw new BusinessException("支付金额必须大于 0");
+ }
+
+ return ApplyAsync(
+ customer,
+ -amount,
+ CustomerAccountFlowBizTypes.OrderPay,
+ orderId,
+ serviceNo,
+ remark,
+ cancellationToken);
+ }
+
+ private async Task ApplyAsync(
+ Customer customer,
+ decimal signedAmount,
+ string bizType,
+ int? bizId,
+ string? bizNo,
+ string? remark,
+ CancellationToken cancellationToken = default)
+ {
+ if (signedAmount == 0)
+ {
+ throw new BusinessException("流水金额不能为 0");
+ }
+
+ var balanceBefore = customer.Balance;
+ var balanceAfter = balanceBefore + signedAmount;
+ if (balanceAfter < 0)
+ {
+ throw new BusinessException("账户余额不足");
+ }
+
+ customer.Balance = balanceAfter;
+ await _customerRepository.UpdateAsync(customer, cancellationToken);
+
+ var flow = new CustomerAccountFlow
+ {
+ CustomerId = customer.Id,
+ SerialNumber = await GenerateSerialNumberAsync(cancellationToken),
+ BizType = bizType,
+ BizId = bizId,
+ BizNo = bizNo,
+ Amount = signedAmount,
+ BalanceBefore = balanceBefore,
+ BalanceAfter = balanceAfter,
+ Currency = "CNY",
+ Remark = remark
+ };
+ await _flowRepository.InsertAsync(flow, cancellationToken);
+ }
+
+ private async Task GenerateSerialNumberAsync(CancellationToken cancellationToken)
+ {
+ for (var i = 0; i < 5; i++)
+ {
+ var serialNumber = $"FLOW{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(100000, 999999)}";
+ if (!await _flowRepository.ExistsSerialNumberAsync(serialNumber, cancellationToken))
+ {
+ return serialNumber;
+ }
+ }
+
+ throw new BusinessException("账户流水号生成失败");
+ }
+}
diff --git a/src/Oms.Domain/CustomersAggregate/Consts/CustomerAccountFlowBizTypes.cs b/src/Oms.Domain/CustomersAggregate/Consts/CustomerAccountFlowBizTypes.cs
new file mode 100644
index 0000000..eecdc63
--- /dev/null
+++ b/src/Oms.Domain/CustomersAggregate/Consts/CustomerAccountFlowBizTypes.cs
@@ -0,0 +1,11 @@
+namespace Oms.Domain.CustomersAggregate.Consts;
+
+///
+/// 客户账户流水业务类型。
+///
+public static class CustomerAccountFlowBizTypes
+{
+ public const string Recharge = "recharge";
+
+ public const string OrderPay = "orderPay";
+}
diff --git a/src/Oms.Domain/CustomersAggregate/Entities/CustomerAccountFlow.cs b/src/Oms.Domain/CustomersAggregate/Entities/CustomerAccountFlow.cs
new file mode 100644
index 0000000..0083f01
--- /dev/null
+++ b/src/Oms.Domain/CustomersAggregate/Entities/CustomerAccountFlow.cs
@@ -0,0 +1,34 @@
+using Oms.Domain.Shared;
+using SqlSugar;
+
+namespace Oms.Domain.CustomersAggregate.Entities;
+
+[SugarTable("t_customer_account_flow")]
+public class CustomerAccountFlow : OmsAuditedAggregateRoot
+{
+ public int CustomerId { get; set; }
+
+ public string SerialNumber { get; set; } = string.Empty;
+
+ ///
+ /// 业务类型:recharge / orderPay 等。
+ ///
+ public string BizType { get; set; } = string.Empty;
+
+ public int? BizId { get; set; }
+
+ public string? BizNo { get; set; }
+
+ ///
+ /// 变动金额:正数为入账,负数为出账。
+ ///
+ public decimal Amount { get; set; }
+
+ public decimal BalanceBefore { get; set; }
+
+ public decimal BalanceAfter { get; set; }
+
+ public string Currency { get; set; } = "CNY";
+
+ public string? Remark { get; set; }
+}
diff --git a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerAccountFlowRepository.cs b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerAccountFlowRepository.cs
new file mode 100644
index 0000000..936ce1c
--- /dev/null
+++ b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerAccountFlowRepository.cs
@@ -0,0 +1,9 @@
+using Fake.Domain.Repositories;
+using Oms.Domain.CustomersAggregate.Entities;
+
+namespace Oms.Domain.CustomersAggregate.Repositories;
+
+public interface ICustomerAccountFlowRepository : IRepository
+{
+ Task ExistsSerialNumberAsync(string serialNumber, CancellationToken cancellationToken = default);
+}
diff --git a/src/Oms.Domain/OrdersAggregate/OrderStatusCountRow.cs b/src/Oms.Domain/OrdersAggregate/OrderStatusCountRow.cs
new file mode 100644
index 0000000..8a0e00c
--- /dev/null
+++ b/src/Oms.Domain/OrdersAggregate/OrderStatusCountRow.cs
@@ -0,0 +1,11 @@
+namespace Oms.Domain.OrdersAggregate;
+
+///
+/// 订单按状态分组统计行。
+///
+public class OrderStatusCountRow
+{
+ public string Status { get; set; } = string.Empty;
+
+ public int Count { get; set; }
+}
diff --git a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
index 6587900..d59444b 100644
--- a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
+++ b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
@@ -48,4 +48,6 @@ public interface IOrderRepository : IRepository
Task UpdateRecipientAsync(OrderRecipient recipient, CancellationToken cancellationToken = default);
Task> GetFeesByOrderIdsAsync(IEnumerable orderIds, CancellationToken cancellationToken = default);
+
+ Task> GetStatusCountsAsync(int? customerId, CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.SqlSugarCore/Customers/CustomerAccountFlowRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerAccountFlowRepository.cs
new file mode 100644
index 0000000..13efb8d
--- /dev/null
+++ b/src/Oms.SqlSugarCore/Customers/CustomerAccountFlowRepository.cs
@@ -0,0 +1,15 @@
+using Fake.DependencyInjection;
+using Oms.Domain.CustomersAggregate.Entities;
+using Oms.Domain.CustomersAggregate.Repositories;
+using Oms.SqlSugarCore.Repositories;
+
+namespace Oms.SqlSugarCore.Customers;
+
+public class CustomerAccountFlowRepository
+ : OmsSqlSugarRepository, ICustomerAccountFlowRepository, IScopedDependency
+{
+ public Task ExistsSerialNumberAsync(string serialNumber, CancellationToken cancellationToken = default)
+ {
+ return AnyAsync(x => x.SerialNumber == serialNumber, cancellationToken);
+ }
+}
diff --git a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
index 8bd25c1..24403c2 100644
--- a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
+++ b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
@@ -1,8 +1,10 @@
using Fake.DependencyInjection;
+using Oms.Domain.OrdersAggregate;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
+using SqlSugar;
namespace Oms.SqlSugarCore.Orders;
@@ -249,4 +251,24 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I
.Where(x => ids.Contains(x.OrderId))
.ToListAsync(cancellationToken);
}
+
+ public async Task> GetStatusCountsAsync(int? customerId, CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ var query = db.Queryable();
+ if (customerId.HasValue)
+ {
+ var cid = customerId.Value;
+ query = query.Where(x => x.CustomerId == cid);
+ }
+
+ return await query
+ .GroupBy(x => x.OrderStatus)
+ .Select(x => new OrderStatusCountRow
+ {
+ Status = x.OrderStatus,
+ Count = SqlFunc.AggregateCount(x.Id)
+ })
+ .ToListAsync(cancellationToken);
+ }
}