440 lines
14 KiB
C#
440 lines
14 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using MilliSim.Common.Dtos;
|
||
using MilliSim.Common.Entities;
|
||
using MilliSim.Common.Models;
|
||
using MilliSim.Data;
|
||
using MilliSim.Services.Infrastructure;
|
||
|
||
namespace MilliSim.Services;
|
||
|
||
public class ContentService : IContentService
|
||
{
|
||
private readonly MilliSimDbContext _db;
|
||
private readonly ICurrentUserService _currentUser;
|
||
private readonly IOperationLogService _operationLog;
|
||
private readonly IFileStorageService _fileStorage;
|
||
private readonly INotificationService _notification;
|
||
private readonly IUploadTrackingService _uploadTracking;
|
||
|
||
public ContentService(
|
||
MilliSimDbContext db,
|
||
ICurrentUserService currentUser,
|
||
IOperationLogService operationLog,
|
||
IFileStorageService fileStorage,
|
||
INotificationService notification,
|
||
IUploadTrackingService uploadTracking)
|
||
{
|
||
_db = db;
|
||
_currentUser = currentUser;
|
||
_operationLog = operationLog;
|
||
_fileStorage = fileStorage;
|
||
_notification = notification;
|
||
_uploadTracking = uploadTracking;
|
||
}
|
||
|
||
// ===== 轮播图 =====
|
||
|
||
public async Task<ApiResponse<List<BannerDto>>> GetBannersAsync(bool onlyActive = false)
|
||
{
|
||
var query = _db.Banners.AsQueryable();
|
||
|
||
if (onlyActive)
|
||
{
|
||
query = query.Where(b => b.Status == UserStatus.Active);
|
||
}
|
||
|
||
// 先查询实体,再在内存中转换为DTO(因为需要调用GetSignedUrl生成签名URL)
|
||
var entities = await query
|
||
.OrderBy(b => b.SortOrder)
|
||
.ThenByDescending(b => b.CreatedAt)
|
||
.ToListAsync();
|
||
|
||
// 返回时将对象键转换为签名URL,确保前端能访问COS私有文件
|
||
var items = entities.Select(b => new BannerDto
|
||
{
|
||
Id = b.Id,
|
||
Title = b.Title,
|
||
Image = _fileStorage.GetSignedUrl(b.Image),
|
||
Link = b.Link,
|
||
SortOrder = b.SortOrder,
|
||
Status = b.Status
|
||
}).ToList();
|
||
|
||
return ApiResponse<List<BannerDto>>.Success(items);
|
||
}
|
||
|
||
public async Task<ApiResponse<BannerDto>> CreateBannerAsync(CreateBannerRequest request)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request.Image))
|
||
{
|
||
return ApiResponse<BannerDto>.Fail("图片不能为空");
|
||
}
|
||
|
||
var entity = new Banner
|
||
{
|
||
Title = request.Title,
|
||
// 保存时将签名URL转换为对象键存储,避免签名URL过期导致数据丢失
|
||
Image = _fileStorage.ExtractKey(request.Image),
|
||
Link = request.Link,
|
||
SortOrder = request.SortOrder,
|
||
Status = request.Status
|
||
};
|
||
|
||
_db.Banners.Add(entity);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 消费上传跟踪
|
||
await _uploadTracking.ConsumeAsync(entity.Image);
|
||
|
||
await _operationLog.LogAsync("内容-轮播图", "创建",
|
||
$"创建轮播图:{request.Title},ID: {entity.Id}");
|
||
|
||
return ApiResponse<BannerDto>.Success(MapToDto(entity));
|
||
}
|
||
|
||
public async Task<ApiResponse<BannerDto>> UpdateBannerAsync(long id, CreateBannerRequest request)
|
||
{
|
||
var entity = await _db.Banners.FirstOrDefaultAsync(b => b.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse<BannerDto>.Fail("轮播图不存在", 404);
|
||
}
|
||
|
||
var oldImageKey = entity.Image;
|
||
var newImageKey = _fileStorage.ExtractKey(request.Image);
|
||
|
||
entity.Title = request.Title;
|
||
// 保存时将签名URL转换为对象键存储,避免签名URL过期导致数据丢失
|
||
entity.Image = newImageKey;
|
||
entity.Link = request.Link;
|
||
entity.SortOrder = request.SortOrder;
|
||
entity.Status = request.Status;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 图片替换:删除旧文件
|
||
if (!string.IsNullOrEmpty(oldImageKey) && oldImageKey != newImageKey)
|
||
{
|
||
await _fileStorage.DeleteAsync(oldImageKey);
|
||
}
|
||
// 消费新上传的文件
|
||
await _uploadTracking.ConsumeAsync(newImageKey);
|
||
|
||
await _operationLog.LogAsync("内容-轮播图", "更新",
|
||
$"更新轮播图 ID: {id}");
|
||
|
||
return ApiResponse<BannerDto>.Success(MapToDto(entity));
|
||
}
|
||
|
||
public async Task<ApiResponse> DeleteBannerAsync(long id)
|
||
{
|
||
var entity = await _db.Banners.FirstOrDefaultAsync(b => b.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse.Fail("轮播图不存在", 404);
|
||
}
|
||
|
||
entity.IsDeleted = true;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
await _operationLog.LogAsync("内容-轮播图", "删除",
|
||
$"删除轮播图 ID: {id}");
|
||
|
||
return ApiResponse.Success("删除成功");
|
||
}
|
||
|
||
// ===== 合作伙伴 =====
|
||
|
||
public async Task<ApiResponse<List<PartnerDto>>> GetPartnersAsync(bool onlyVisible = false)
|
||
{
|
||
var query = _db.Partners.AsQueryable();
|
||
|
||
if (onlyVisible)
|
||
{
|
||
query = query.Where(p => p.IsVisible);
|
||
}
|
||
|
||
// 先查询实体,再在内存中转换为DTO(因为需要调用GetSignedUrl生成签名URL)
|
||
var entities = await query
|
||
.OrderBy(p => p.SortOrder)
|
||
.ThenByDescending(p => p.CreatedAt)
|
||
.ToListAsync();
|
||
|
||
// 返回时将对象键转换为签名URL
|
||
var items = entities.Select(p => new PartnerDto
|
||
{
|
||
Id = p.Id,
|
||
Name = p.Name,
|
||
Logo = _fileStorage.GetSignedUrl(p.Logo),
|
||
Link = p.Link,
|
||
SortOrder = p.SortOrder,
|
||
IsVisible = p.IsVisible
|
||
}).ToList();
|
||
|
||
return ApiResponse<List<PartnerDto>>.Success(items);
|
||
}
|
||
|
||
public async Task<ApiResponse<PartnerDto>> CreatePartnerAsync(CreatePartnerRequest request)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request.Name))
|
||
{
|
||
return ApiResponse<PartnerDto>.Fail("名称不能为空");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.Logo))
|
||
{
|
||
return ApiResponse<PartnerDto>.Fail("Logo 不能为空");
|
||
}
|
||
|
||
var entity = new Partner
|
||
{
|
||
Name = request.Name,
|
||
// 保存时将签名URL转换为对象键存储
|
||
Logo = _fileStorage.ExtractKey(request.Logo),
|
||
Link = request.Link,
|
||
SortOrder = request.SortOrder,
|
||
IsVisible = request.IsVisible
|
||
};
|
||
|
||
_db.Partners.Add(entity);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 消费上传跟踪
|
||
await _uploadTracking.ConsumeAsync(entity.Logo);
|
||
|
||
await _operationLog.LogAsync("内容-合作伙伴", "创建",
|
||
$"创建合作伙伴:{request.Name},ID: {entity.Id}");
|
||
|
||
return ApiResponse<PartnerDto>.Success(MapToDto(entity));
|
||
}
|
||
|
||
public async Task<ApiResponse<PartnerDto>> UpdatePartnerAsync(long id, CreatePartnerRequest request)
|
||
{
|
||
var entity = await _db.Partners.FirstOrDefaultAsync(p => p.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse<PartnerDto>.Fail("合作伙伴不存在", 404);
|
||
}
|
||
|
||
var oldLogoKey = entity.Logo;
|
||
var newLogoKey = _fileStorage.ExtractKey(request.Logo);
|
||
|
||
entity.Name = request.Name;
|
||
// 保存时将签名URL转换为对象键存储
|
||
entity.Logo = newLogoKey;
|
||
entity.Link = request.Link;
|
||
entity.SortOrder = request.SortOrder;
|
||
entity.IsVisible = request.IsVisible;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
|
||
await _db.SaveChangesAsync();
|
||
|
||
// Logo 替换:删除旧文件
|
||
if (!string.IsNullOrEmpty(oldLogoKey) && oldLogoKey != newLogoKey)
|
||
{
|
||
await _fileStorage.DeleteAsync(oldLogoKey);
|
||
}
|
||
// 消费新上传的文件
|
||
await _uploadTracking.ConsumeAsync(newLogoKey);
|
||
|
||
await _operationLog.LogAsync("内容-合作伙伴", "更新",
|
||
$"更新合作伙伴 ID: {id}");
|
||
|
||
return ApiResponse<PartnerDto>.Success(MapToDto(entity));
|
||
}
|
||
|
||
public async Task<ApiResponse> DeletePartnerAsync(long id)
|
||
{
|
||
var entity = await _db.Partners.FirstOrDefaultAsync(p => p.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse.Fail("合作伙伴不存在", 404);
|
||
}
|
||
|
||
entity.IsDeleted = true;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
await _operationLog.LogAsync("内容-合作伙伴", "删除",
|
||
$"删除合作伙伴 ID: {id}");
|
||
|
||
return ApiResponse.Success("删除成功");
|
||
}
|
||
|
||
// ===== 咨询消息 =====
|
||
|
||
public async Task<ApiResponse<PagedResult<ConsultationDto>>> GetConsultationsAsync(PageRequest request, bool? isRead = null)
|
||
{
|
||
var query = _db.Consultations.AsQueryable();
|
||
|
||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||
{
|
||
var keyword = request.Keyword.Trim();
|
||
query = query.Where(c =>
|
||
c.Name.Contains(keyword) ||
|
||
c.Contact.Contains(keyword) ||
|
||
c.Content.Contains(keyword));
|
||
}
|
||
|
||
// 按已读/未读状态过滤
|
||
if (isRead.HasValue)
|
||
{
|
||
query = query.Where(c => c.IsRead == isRead.Value);
|
||
}
|
||
|
||
var total = await query.LongCountAsync();
|
||
|
||
var items = await query
|
||
.OrderByDescending(c => c.CreatedAt)
|
||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||
.Take(request.PageSize)
|
||
.Select(c => new ConsultationDto
|
||
{
|
||
Id = c.Id,
|
||
Name = c.Name,
|
||
Contact = c.Contact,
|
||
Content = c.Content,
|
||
IsRead = c.IsRead,
|
||
CreatedAt = c.CreatedAt
|
||
})
|
||
.ToListAsync();
|
||
|
||
return ApiResponse<PagedResult<ConsultationDto>>.Success(
|
||
PagedResult<ConsultationDto>.Create(items, total, request.PageIndex, request.PageSize));
|
||
}
|
||
|
||
public async Task<ApiResponse<ConsultationDto>> CreateConsultationAsync(CreateConsultationRequest request)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request.Name))
|
||
{
|
||
return ApiResponse<ConsultationDto>.Fail("姓名不能为空");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.Contact))
|
||
{
|
||
return ApiResponse<ConsultationDto>.Fail("联系方式不能为空");
|
||
}
|
||
|
||
var entity = new Consultation
|
||
{
|
||
Name = request.Name,
|
||
Contact = request.Contact,
|
||
Content = request.Content,
|
||
IsRead = false
|
||
};
|
||
|
||
_db.Consultations.Add(entity);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 通知所有管理员/销售:有新咨询需要处理
|
||
try
|
||
{
|
||
var staffUsers = await _db.Users
|
||
.Where(u => u.Role == UserRole.Admin || u.Role == UserRole.Sales)
|
||
.Select(u => u.Id)
|
||
.ToListAsync();
|
||
var contentPreview = string.IsNullOrWhiteSpace(request.Content)
|
||
? "(无内容)"
|
||
: (request.Content.Length > 50 ? request.Content[..50] + "..." : request.Content);
|
||
foreach (var staffId in staffUsers)
|
||
{
|
||
await _notification.CreateNotificationAsync(
|
||
NotificationType.System,
|
||
"新咨询消息",
|
||
$"{request.Name}({request.Contact})提交了一条咨询:{contentPreview}",
|
||
userId: staffId,
|
||
eventId: $"consultation-{entity.Id}-notify-{staffId}");
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 通知发送失败不影响咨询提交结果
|
||
}
|
||
|
||
return ApiResponse<ConsultationDto>.Success(MapToDto(entity), "提交成功");
|
||
}
|
||
|
||
public async Task<ApiResponse> MarkAsReadAsync(long id)
|
||
{
|
||
var entity = await _db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse.Fail("咨询消息不存在", 404);
|
||
}
|
||
|
||
if (!entity.IsRead)
|
||
{
|
||
entity.IsRead = true;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
}
|
||
|
||
return ApiResponse.Success("已标记为已读");
|
||
}
|
||
|
||
public async Task<ApiResponse> DeleteConsultationAsync(long id)
|
||
{
|
||
var entity = await _db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||
if (entity == null)
|
||
{
|
||
return ApiResponse.Fail("咨询消息不存在", 404);
|
||
}
|
||
|
||
entity.IsDeleted = true;
|
||
entity.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
await _operationLog.LogAsync("内容-咨询", "删除",
|
||
$"删除咨询消息 ID: {id}");
|
||
|
||
return ApiResponse.Success("删除成功");
|
||
}
|
||
|
||
// ===== 私有映射方法 =====
|
||
|
||
// 改为非静态方法,以便调用 _fileStorage.GetSignedUrl 生成签名URL
|
||
private BannerDto MapToDto(Banner entity)
|
||
{
|
||
return new BannerDto
|
||
{
|
||
Id = entity.Id,
|
||
Title = entity.Title,
|
||
// 返回时将对象键转换为签名URL,确保前端能访问COS私有文件
|
||
Image = _fileStorage.GetSignedUrl(entity.Image),
|
||
Link = entity.Link,
|
||
SortOrder = entity.SortOrder,
|
||
Status = entity.Status
|
||
};
|
||
}
|
||
|
||
// 改为非静态方法,以便调用 _fileStorage.GetSignedUrl 生成签名URL
|
||
private PartnerDto MapToDto(Partner entity)
|
||
{
|
||
return new PartnerDto
|
||
{
|
||
Id = entity.Id,
|
||
Name = entity.Name,
|
||
// 返回时将对象键转换为签名URL
|
||
Logo = _fileStorage.GetSignedUrl(entity.Logo),
|
||
Link = entity.Link,
|
||
SortOrder = entity.SortOrder,
|
||
IsVisible = entity.IsVisible
|
||
};
|
||
}
|
||
|
||
private static ConsultationDto MapToDto(Consultation entity)
|
||
{
|
||
return new ConsultationDto
|
||
{
|
||
Id = entity.Id,
|
||
Name = entity.Name,
|
||
Contact = entity.Contact,
|
||
Content = entity.Content,
|
||
IsRead = entity.IsRead,
|
||
CreatedAt = entity.CreatedAt
|
||
};
|
||
}
|
||
}
|