admin/src/Oms.Application/Services/Channels/ServiceChannelService.cs
2026-06-25 19:59:06 +08:00

506 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 服务渠道 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public partial class ServiceChannelService : ApplicationService
{
private const string PriceOperatorType = "price";
private const string RulesOperatorType = "rules";
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
/// <summary>
/// 查询服务渠道列表。
/// </summary>
/// <param name="input">入参:服务渠道列表查询条件。</param>
/// <returns>服务渠道分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpGet("/channel/service-channel/list")]
public async Task<OmsPageResult<ServiceChannelListOutput>> GetListAsync([FromQuery] ServiceChannelListInput input)
{
var predicate = BuildListPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await ServiceChannelRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(ServiceChannel.UpdateTime)] = false, [nameof(ServiceChannel.Id)] = false });
var total = await ServiceChannelRepository.CountAsync(predicate);
var groupMap = await GetGroupMapAsync();
return new OmsPageResult<ServiceChannelListOutput>
{
Items = entities.Select(entity => MapListOutput(entity, groupMap)).ToList(),
Total = total
};
}
/// <summary>
/// 查询服务渠道详情。
/// </summary>
/// <param name="id">入参:目标主键。</param>
/// <returns>服务渠道详情。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据标识查询目标对象。
/// 2. 目标不存在时抛出业务异常。
/// 3. 映射并返回详情结果。
/// </remarks>
[HttpGet("/channel/service-channel/detail")]
public async Task<ServiceChannelDetailOutput> GetDetailAsync([FromQuery] int id)
{
var entity = await GetEntityOrThrowAsync(id);
return MapDetailOutput(entity, await GetGroupMapAsync());
}
/// <summary>
/// 新增服务渠道。
/// </summary>
/// <param name="input">入参:服务渠道保存参数。</param>
/// <returns>无返回体,仅完成新增服务渠道。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验入参与关联对象、唯一性等前置条件。
/// 2. 组装目标实体及相关明细数据。
/// 3. 写入审计字段并持久化结果。
/// </remarks>
[HttpPost("/channel/service-channel/create")]
public async Task CreateAsync(ServiceChannelSaveInput input)
{
await ValidateChannelCodeAsync(input.ChannelCode, null);
var entity = new ServiceChannel();
ApplyInput(entity, input);
await ServiceChannelRepository.InsertAsync(entity);
}
/// <summary>
/// 更新服务渠道。
/// </summary>
/// <param name="input">入参:服务渠道保存参数。</param>
/// <returns>无返回体,仅完成更新服务渠道。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 按入参更新主体数据和关联配置。
/// 3. 写入更新审计字段并保存结果。
/// </remarks>
[HttpPost("/channel/service-channel/update")]
public async Task UpdateAsync(ServiceChannelSaveInput input)
{
var entity = await GetEntityOrThrowAsync(input.Id);
await ValidateChannelCodeAsync(input.ChannelCode, entity.Id);
ApplyInput(entity, input);
await ServiceChannelRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除服务渠道。
/// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体,仅完成删除服务渠道。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 执行软删除或删除关联明细。
/// 3. 持久化删除结果。
/// </remarks>
[HttpPost("/channel/service-channel/delete")]
public async Task DeleteAsync(IdInput input)
{
var entity = await GetEntityOrThrowAsync(input.Id);
await ServiceChannelRepository.DeleteAsync(entity);
}
/// <summary>
/// 查询服务渠道分组列表。
/// </summary>
/// <param name="input">入参:分组名称、应用 ID、API 服务商 ID 等筛选条件。</param>
/// <returns>服务渠道分组列表,供下拉与左侧树筛选使用。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 按 keyword、appId、apiServiceProviderId 构造筛选条件。
/// 2. 查询未删除分组并按 id 倒序返回。
/// 3. 无数据时返回空数组。
/// </remarks>
[HttpGet("/channel/service-channel/group/list")]
public async Task<List<ServiceChannelGroupListOutput>> GetGroupListAsync([FromQuery] ServiceChannelGroupListInput input)
{
var predicate = BuildGroupListPredicate(input);
var entities = await ServiceChannelGroupRepository.GetListAsync(
predicate,
new Dictionary<string, bool> { [nameof(ServiceChannelGroup.Id)] = false });
return entities.Select(MapGroupListOutput).ToList();
}
/// <summary>
/// 新增服务渠道分组。
/// </summary>
/// <param name="input">入参:服务渠道分组保存参数。</param>
/// <returns>无返回体,仅完成新增服务渠道分组。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验分组名称、应用ID、API服务商ID等基础字段。
/// 2. 检查分组名称唯一性,避免重复配置。
/// 3. 保存分组主数据供服务渠道绑定。
/// </remarks>
[HttpPost("/channel/service-channel/group/create")]
public async Task CreateGroupAsync(ServiceChannelGroupSaveInput input)
{
await ValidateGroupInputAsync(input, null);
await ServiceChannelGroupRepository.InsertAsync(new ServiceChannelGroup
{
Name = input.Name.Trim(),
AppId = input.AppId,
ApiServiceProviderId = input.ApiServiceProviderId
});
}
/// <summary>
/// 更新服务渠道分组。
/// </summary>
/// <param name="input">入参:服务渠道分组保存参数。</param>
/// <returns>无返回体,仅完成更新服务渠道分组。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标分组是否存在。
/// 2. 检查名称唯一性并更新分组主数据。
/// 3. 保留既有渠道关联,只更新分组属性。
/// </remarks>
[HttpPost("/channel/service-channel/group/update")]
public async Task UpdateGroupAsync(ServiceChannelGroupSaveInput input)
{
var group = await GetGroupOrThrowAsync(input.Id);
await ValidateGroupInputAsync(input, group.Id);
group.Name = input.Name.Trim();
group.AppId = input.AppId;
group.ApiServiceProviderId = input.ApiServiceProviderId;
await ServiceChannelGroupRepository.UpdateAsync(group);
}
/// <summary>
/// 删除服务渠道分组。
/// </summary>
/// <param name="input">入参:目标分组主键。</param>
/// <returns>无返回体,仅完成删除服务渠道分组。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标分组是否存在。
/// 2. 检查分组下是否仍有关联渠道。
/// 3. 无关联时执行删除,保留框架软删语义。
/// </remarks>
[HttpPost("/channel/service-channel/group/delete")]
public async Task DeleteGroupAsync(IdInput input)
{
var group = await GetGroupOrThrowAsync(input.Id);
if (await ServiceChannelRepository.AnyAsync(x => x.GroupId == group.Id))
{
throw new BusinessException("当前分组下仍有关联渠道,无法删除");
}
await ServiceChannelGroupRepository.DeleteAsync(group);
}
/// <summary>
/// 保存服务渠道配置。
/// </summary>
/// <param name="input">入参:服务渠道保存参数。</param>
/// <returns>无返回体,仅完成保存服务渠道配置。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 按入参更新主体数据和关联配置。
/// 3. 写入更新审计字段并保存结果。
/// </remarks>
[ApiExplorerSettings(IgnoreApi = true)]
[HttpPost("/channel/service-channel/config/save")]
public async Task SaveConfigAsync(ServiceChannelSaveInput input)
{
var entity = await GetEntityOrThrowAsync(input.Id);
await ValidateChannelCodeAsync(input.ChannelCode, entity.Id);
ApplyInput(entity, input);
await ServiceChannelRepository.UpdateAsync(entity);
}
/// <summary>
/// 查询服务渠道价格项列表。
/// </summary>
/// <param name="serviceChannelId">入参:serviceChannelId。</param>
/// <returns>服务渠道价格项列表结果。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpGet("/channel/service-channel/price/list")]
public async Task<OmsItemsResult<ServiceChannelPriceListOutput>> GetPriceListAsync([FromQuery] int serviceChannelId)
{
await GetEntityOrThrowAsync(serviceChannelId);
return new OmsItemsResult<ServiceChannelPriceListOutput>
{
Items = await GetPriceOutputsAsync(serviceChannelId)
};
}
/// <summary>
/// 新增服务渠道价格项。
/// </summary>
/// <param name="input">入参:服务渠道价格项保存参数。</param>
/// <returns>无返回体,仅完成新增服务渠道价格项。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验入参与关联对象、唯一性等前置条件。
/// 2. 组装目标实体及相关明细数据。
/// 3. 写入审计字段并持久化结果。
/// </remarks>
[HttpPost("/channel/service-channel/price/create")]
public async Task CreatePriceAsync(ServiceChannelPriceSaveInput input)
{
await GetEntityOrThrowAsync(input.ServiceChannelId);
await ServiceChannelRepository.InsertPriceAsync(
new ServiceChannelPrice
{
ServiceChannelId = input.ServiceChannelId,
SerialNumber = input.SerialNumber,
Status = input.Status,
PriceName = input.PriceName,
CostItem = input.CostItem,
CostUnit = input.CostUnit,
Currency = input.Currency,
ChargingTime = input.ChargingTime,
ChargingCondition = input.ChargingCondition
},
BuildPriceFactors(input.Factors),
BuildPriceDetails(input.Details));
}
/// <summary>
/// 更新服务渠道价格项。
/// </summary>
/// <param name="input">入参:服务渠道价格项保存参数。</param>
/// <returns>无返回体,仅完成更新服务渠道价格项。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 按入参更新主体数据和关联配置。
/// 3. 写入更新审计字段并保存结果。
/// </remarks>
[HttpPost("/channel/service-channel/price/update")]
public async Task UpdatePriceAsync(ServiceChannelPriceSaveInput input)
{
var price = await GetPriceOrThrowAsync(input.Id);
if (price.ServiceChannelId != input.ServiceChannelId)
{
throw new BusinessException("买价项归属不匹配");
}
price.SerialNumber = input.SerialNumber;
price.Status = input.Status;
price.PriceName = input.PriceName;
price.CostItem = input.CostItem;
price.CostUnit = input.CostUnit;
price.Currency = input.Currency;
price.ChargingTime = input.ChargingTime;
price.ChargingCondition = input.ChargingCondition;
await ServiceChannelRepository.UpdatePriceAsync(
price,
BuildPriceFactors(input.Factors),
BuildPriceDetails(input.Details));
}
/// <summary>
/// 删除服务渠道价格项。
/// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体,仅完成删除服务渠道价格项。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 执行软删除或删除关联明细。
/// 3. 持久化删除结果。
/// </remarks>
[HttpPost("/channel/service-channel/price/delete")]
public async Task DeletePriceAsync(IdInput input)
{
await GetPriceOrThrowAsync(input.Id);
await ServiceChannelRepository.DeletePriceAsync(input.Id);
}
/// <summary>
/// 切换服务渠道价格项状态。
/// </summary>
/// <param name="input">入参:目标主键和状态值。</param>
/// <returns>无返回体,仅完成切换服务渠道价格项状态。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 更新启停状态。
/// 3. 保存状态变更结果。
/// </remarks>
[HttpPost("/channel/service-channel/price/toggleStatus")]
public async Task TogglePriceStatusAsync(ToggleStatusInput input)
{
await GetPriceOrThrowAsync(input.Id);
await ServiceChannelRepository.UpdatePriceStatusAsync(input.Id, input.Status);
}
/// <summary>
/// 批量保存服务渠道价格项。
/// </summary>
/// <param name="input">入参:服务渠道价格项批量保存参数。</param>
/// <returns>无返回体,仅完成批量保存服务渠道价格项。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象。
/// 2. 按入参更新主体数据和关联配置。
/// 3. 写入更新审计字段并保存结果。
/// </remarks>
[HttpPost("/channel/service-channel/price/save")]
public async Task SavePriceAsync(ServiceChannelPriceSaveAllInput input)
{
await GetEntityOrThrowAsync(input.ServiceChannelId);
var prices = new List<ServiceChannelPrice>();
var factors = new List<ServiceChannelPriceFactor>();
var details = new List<ServiceChannelPriceDetail>();
var tempId = -1;
// 使用临时负数 Id 保持买价项与子表数据在批量替换时的关联关系。
foreach (var priceInput in input.Prices.OrderBy(x => x.SerialNumber))
{
var currentTempId = tempId--;
prices.Add(new ServiceChannelPrice
{
Id = currentTempId,
ServiceChannelId = input.ServiceChannelId,
SerialNumber = priceInput.SerialNumber,
Status = priceInput.Status,
PriceName = priceInput.PriceName,
CostItem = priceInput.CostItem,
CostUnit = priceInput.CostUnit,
Currency = priceInput.Currency,
ChargingTime = priceInput.ChargingTime,
ChargingCondition = priceInput.ChargingCondition
});
factors.AddRange(BuildPriceFactors(priceInput.Factors, currentTempId));
details.AddRange(BuildPriceDetails(priceInput.Details, currentTempId));
}
await ServiceChannelRepository.ReplacePricesAsync(input.ServiceChannelId, prices, factors, details);
if (input.Publish)
{
await CreatePublishRecordAsync(input.ServiceChannelId, PriceOperatorType, "保存并发布买价");
}
}
/// <summary>
/// 查询服务渠道发布价格列表。
/// </summary>
/// <param name="input">入参:服务渠道发布列表查询条件。</param>
/// <returns>服务渠道发布价格分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpGet("/channel/service-channel/publish/price/list")]
public Task<OmsPageResult<ServiceChannelPublishRecordOutput>> GetPricePublishListAsync([FromQuery] ServiceChannelPublishListInput input)
{
return GetPublishListAsync(input, PriceOperatorType);
}
/// <summary>
/// 查询服务渠道发布规则列表。
/// </summary>
/// <param name="input">入参:服务渠道发布列表查询条件。</param>
/// <returns>服务渠道发布规则分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpGet("/channel/service-channel/publish/rules/list")]
public Task<OmsPageResult<ServiceChannelPublishRecordOutput>> GetRulesPublishListAsync([FromQuery] ServiceChannelPublishListInput input)
{
return GetPublishListAsync(input, RulesOperatorType);
}
/// <summary>
/// 执行服务渠道发布。
/// </summary>
/// <param name="input">入参:服务渠道发布参数。</param>
/// <returns>无返回体,仅完成服务渠道发布处理。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验目标对象和发布前提条件。
/// 2. 生成、切换或回退发布版本数据。
/// 3. 保存发布结果并返回处理信息。
/// </remarks>
[HttpPost("/channel/service-channel/publish")]
public async Task PublishAsync(ServiceChannelPublishInput input)
{
await CreatePublishRecordAsync(input.ServiceChannelId, NormalizeOperatorType(input.OperatorType), input.Description);
}
/// <summary>
/// 回滚服务渠道发布版本。
/// </summary>
/// <param name="input">入参:服务渠道发布回滚参数。</param>
/// <returns>无返回体,仅完成回滚服务渠道发布版本。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 校验目标对象和发布前提条件。
/// 2. 生成、切换或回退发布版本数据。
/// 3. 保存发布结果并返回处理信息。
/// </remarks>
[HttpPost("/channel/service-channel/publish/rollback")]
public Task RollbackAsync(ServiceChannelRollbackInput input)
{
return RollbackCoreAsync(input);
}
private static Expression<Func<ServiceChannel, bool>> BuildListPredicate(ServiceChannelListInput input)
{
var keyword = input.Keyword?.Trim();
var hasGroupFilter = input.GroupId.HasValue;
var groupIdValue = input.GroupId.GetValueOrDefault();
var hasIsActiveFilter = input.IsActive.HasValue;
var isActiveValue = input.IsActive.GetValueOrDefault();
return entity =>
(!hasGroupFilter || entity.GroupId == groupIdValue) &&
(string.IsNullOrWhiteSpace(keyword) || entity.ChannelCode.Contains(keyword) || entity.ChannelName.Contains(keyword)) &&
(!hasIsActiveFilter || entity.IsActive == isActiveValue);
}
private static Expression<Func<ServiceChannelGroup, bool>> BuildGroupListPredicate(ServiceChannelGroupListInput input)
{
var keyword = input.Keyword?.Trim();
var hasAppId = input.AppId.HasValue;
var appId = input.AppId.GetValueOrDefault();
var hasApiServiceProviderId = input.ApiServiceProviderId.HasValue;
var apiServiceProviderId = input.ApiServiceProviderId.GetValueOrDefault();
return entity =>
(string.IsNullOrWhiteSpace(keyword) || entity.Name.Contains(keyword)) &&
(!hasAppId || entity.AppId == appId) &&
(!hasApiServiceProviderId || entity.ApiServiceProviderId == apiServiceProviderId);
}
}