MilliSim/backend/MilliSim.Api/Services/AuthorizationService.cs

610 lines
22 KiB
C#
Raw Permalink 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 Microsoft.EntityFrameworkCore;
using MilliSim.Common.Dtos;
using MilliSim.Common.Entities;
using MilliSim.Common.Models;
using MilliSim.Data;
using MilliSim.Services.Infrastructure;
namespace MilliSim.Services;
/// <summary>
/// 授权服务(客户级授权:对客户授权后,该客户能运行所有项目)
/// </summary>
public class AuthorizationService : IAuthorizationService
{
private readonly MilliSimDbContext _db;
private readonly ICurrentUserService _currentUser;
private readonly IOperationLogService _operationLog;
private readonly INotificationService _notification;
public AuthorizationService(
MilliSimDbContext db,
ICurrentUserService currentUser,
IOperationLogService operationLog,
INotificationService notification)
{
_db = db;
_currentUser = currentUser;
_operationLog = operationLog;
_notification = notification;
}
/// <summary>
/// 获取授权列表(销售只能查看自己创建的客户的授权)
/// </summary>
public async Task<ApiResponse<PagedResult<AuthorizationDto>>> GetAuthorizationsAsync(AuthorizationQueryRequest request)
{
// 自动将过期授权标记为 Expired
await MarkExpiredAsync();
var query = from a in _db.Authorizations
join u in _db.Users on a.UserId equals u.Id into ug
from u in ug.DefaultIfEmpty()
join au in _db.Users on a.AuthorizedBy equals au.Id into aug
from au in aug.DefaultIfEmpty()
select new { a, u, au };
// 销售数据隔离:销售只能查看自己创建的客户的授权
if (_currentUser.Role == UserRole.Sales)
{
query = query.Where(x => x.u.CreatedBy == _currentUser.UserId);
}
if (request.UserId.HasValue)
{
query = query.Where(x => x.a.UserId == request.UserId.Value);
}
if (request.Status.HasValue)
{
query = query.Where(x => x.a.Status == request.Status.Value);
}
if (!string.IsNullOrWhiteSpace(request.Keyword))
{
var keyword = request.Keyword.Trim();
query = query.Where(x =>
(x.u != null && x.u.Username.Contains(keyword)) ||
(x.u != null && x.u.Nickname.Contains(keyword)));
}
var total = await query.LongCountAsync();
var items = await query
.OrderByDescending(x => x.a.CreatedAt)
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.Select(x => new AuthorizationDto
{
Id = x.a.Id,
UserId = x.a.UserId,
Username = x.u != null ? x.u.Username : string.Empty,
UserNickname = x.u != null ? x.u.Nickname : string.Empty,
AuthorizedBy = x.a.AuthorizedBy,
AuthorizedByName = x.au != null ? x.au.Nickname : string.Empty,
StartTime = x.a.StartTime,
EndTime = x.a.EndTime,
Status = x.a.Status,
StatusName = x.a.Status.ToString(),
CreatedAt = x.a.CreatedAt
})
.ToListAsync();
return ApiResponse<PagedResult<AuthorizationDto>>.Success(
PagedResult<AuthorizationDto>.Create(items, total, request.PageIndex, request.PageSize));
}
/// <summary>
/// 获取授权详情
/// </summary>
public async Task<ApiResponse<AuthorizationDto>> GetAuthorizationByIdAsync(long id)
{
await MarkExpiredAsync();
var entity = await _db.Authorizations
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.Id == id);
if (entity == null)
{
return ApiResponse<AuthorizationDto>.Fail("授权不存在", 404);
}
// 销售数据隔离
if (_currentUser.Role == UserRole.Sales)
{
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
if (customer == null || customer.CreatedBy != _currentUser.UserId)
{
return ApiResponse<AuthorizationDto>.Fail("无权查看该授权", 403);
}
}
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.AuthorizedBy);
var dto = MapToDto(entity, entity.User, authorizer);
return ApiResponse<AuthorizationDto>.Success(dto);
}
/// <summary>
/// 创建/重新授权(客户级授权):一个客户只能有一个授权记录。
/// 若客户已有授权任意状态Active 则续期EndTime += daysCancelled/Expired 则重置为 Active 重新开通。
/// 若客户无授权:创建新记录。
/// </summary>
public async Task<ApiResponse<AuthorizationDto>> CreateAuthorizationAsync(CreateAuthorizationRequest request)
{
if (request.Days <= 0)
{
request.Days = 365;
}
// 校验用户存在
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null)
{
return ApiResponse<AuthorizationDto>.Fail("用户不存在");
}
// 销售数据隔离:销售只能为自己创建的客户授权
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
{
return ApiResponse<AuthorizationDto>.Fail("无权为该用户授权", 403);
}
await MarkExpiredAsync();
// 一个客户只能有一个授权记录:查询任意状态的现有授权
var existing = await _db.Authorizations
.FirstOrDefaultAsync(a => a.UserId == request.UserId);
var now = DateTime.Now;
if (existing != null)
{
// 捕获原始状态,用于区分续期/重新授权的通知文案
var wasActive = existing.Status == AuthorizationStatus.Active;
if (wasActive)
{
// 续期:在原 EndTime 基础上加天数(如果已过期则从当前时间开始)
var baseTime = existing.EndTime > now ? existing.EndTime : now;
existing.EndTime = baseTime.AddDays(request.Days);
}
else
{
// 重新授权重置起止时间和状态Cancelled / Expired → Active
existing.StartTime = now;
existing.EndTime = now.AddDays(request.Days);
existing.Status = AuthorizationStatus.Active;
existing.ProjectId = null; // 客户级授权
}
existing.AuthorizedBy = _currentUser.UserId;
existing.UpdatedAt = now;
await _db.SaveChangesAsync();
var actionLabel = wasActive ? "续期" : "重新授权";
await _operationLog.LogAsync("授权", actionLabel,
$"用户 {user.Username} {actionLabel} {request.Days} 天,授权 ID: {existing.Id}");
// 通知被授权用户
try
{
var title = wasActive ? "授权已续期" : "授权已重新开通";
var content = wasActive
? $"您的客户级授权已续期 {request.Days} 天,新到期时间:{existing.EndTime:yyyy-MM-dd}。"
: $"您的客户级授权已重新开通 {request.Days} 天,到期时间:{existing.EndTime:yyyy-MM-dd},可体验全部项目。";
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
title,
content,
userId: user.Id,
eventId: $"{(wasActive ? "auth-renewed" : "auth-reactivated")}-{existing.Id}-{existing.UpdatedAt:yyyyMMddHHmmss}");
}
catch
{
// 通知发送失败不影响业务流程
}
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == existing.AuthorizedBy);
var msg = wasActive ? "授权续期成功" : "授权重新开通成功";
return ApiResponse<AuthorizationDto>.Success(MapToDto(existing, user, authorizer), msg);
}
// 创建新授权(客户级授权,不关联项目)
var entity = new Authorization
{
UserId = request.UserId,
AuthorizedBy = _currentUser.UserId,
StartTime = now,
EndTime = now.AddDays(request.Days),
Status = AuthorizationStatus.Active,
ProjectId = null
};
_db.Authorizations.Add(entity);
await _db.SaveChangesAsync();
await _operationLog.LogAsync("授权", "创建",
$"为用户 {user.Username} 创建客户级授权,{request.Days} 天,授权 ID: {entity.Id}");
// 通知被授权用户:授权开通
try
{
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
"授权已开通",
$"您已获得 {request.Days} 天客户级授权,可体验全部项目,到期时间:{entity.EndTime:yyyy-MM-dd}。",
userId: user.Id,
eventId: $"auth-created-{entity.Id}");
}
catch
{
// 通知发送失败不影响业务流程
}
var currentUser = await _db.Users.FirstOrDefaultAsync(u => u.Id == _currentUser.UserId);
return ApiResponse<AuthorizationDto>.Success(MapToDto(entity, user, currentUser));
}
/// <summary>
/// 批量授权:一个客户一个授权记录,对每个 UserId 调用复用/新建逻辑。
/// Active 则续期Cancelled/Expired 则重置为 Active 重新开通,无授权则新建。
/// </summary>
public async Task<ApiResponse> BatchAuthorizeAsync(BatchAuthorizationRequest request)
{
if (request.UserIds == null || request.UserIds.Count == 0)
{
return ApiResponse.Fail("用户列表不能为空");
}
if (request.Days <= 0)
{
request.Days = 365;
}
await MarkExpiredAsync();
var now = DateTime.Now;
var successCount = 0;
var skipCount = 0;
foreach (var userId in request.UserIds.Distinct())
{
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null)
{
skipCount++;
continue;
}
// 销售数据隔离
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
{
skipCount++;
continue;
}
// 一个客户一个授权记录:查询任意状态的现有授权
var existing = await _db.Authorizations
.FirstOrDefaultAsync(a => a.UserId == userId);
if (existing != null)
{
var wasActive = existing.Status == AuthorizationStatus.Active;
if (wasActive)
{
// 续期
var baseTime = existing.EndTime > now ? existing.EndTime : now;
existing.EndTime = baseTime.AddDays(request.Days);
}
else
{
// 重新授权Cancelled / Expired → Active
existing.StartTime = now;
existing.EndTime = now.AddDays(request.Days);
existing.Status = AuthorizationStatus.Active;
existing.ProjectId = null;
}
existing.AuthorizedBy = _currentUser.UserId;
existing.UpdatedAt = now;
successCount++;
// 通知用户
try
{
var title = wasActive ? "授权已续期" : "授权已重新开通";
var content = wasActive
? $"您的客户级授权已续期 {request.Days} 天,新到期时间:{existing.EndTime:yyyy-MM-dd}。"
: $"您的客户级授权已重新开通 {request.Days} 天,到期时间:{existing.EndTime:yyyy-MM-dd},可体验全部项目。";
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
title,
content,
userId: user.Id,
eventId: $"{(wasActive ? "auth-renewed" : "auth-reactivated")}-{existing.Id}-{existing.UpdatedAt:yyyyMMddHHmmss}");
}
catch
{
// 通知失败不影响业务
}
}
else
{
// 创建新授权(客户级授权,不关联项目)
var entity = new Authorization
{
UserId = userId,
AuthorizedBy = _currentUser.UserId,
StartTime = now,
EndTime = now.AddDays(request.Days),
Status = AuthorizationStatus.Active,
ProjectId = null
};
_db.Authorizations.Add(entity);
successCount++;
// 先保存以获取 entity.Id
try
{
await _db.SaveChangesAsync();
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
"授权已开通",
$"您已获得 {request.Days} 天客户级授权,可体验全部项目,到期时间:{entity.EndTime:yyyy-MM-dd}。",
userId: user.Id,
eventId: $"auth-created-{entity.Id}");
}
catch
{
// 通知失败不影响业务
}
}
}
await _db.SaveChangesAsync();
await _operationLog.LogAsync("授权", "批量授权",
$"批量授权:成功 {successCount} 条,跳过 {skipCount} 条,{request.Days} 天");
return ApiResponse.Success($"批量授权完成,成功 {successCount} 条,跳过 {skipCount} 条");
}
/// <summary>
/// 续期授权(按授权 ID 续期)
/// </summary>
public async Task<ApiResponse<AuthorizationDto>> RenewAuthorizationAsync(long id, int days)
{
if (days <= 0)
{
return ApiResponse<AuthorizationDto>.Fail("续期天数必须大于 0");
}
await MarkExpiredAsync();
var entity = await _db.Authorizations
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.Id == id);
if (entity == null)
{
return ApiResponse<AuthorizationDto>.Fail("授权不存在", 404);
}
// 销售数据隔离
if (_currentUser.Role == UserRole.Sales)
{
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
if (customer == null || customer.CreatedBy != _currentUser.UserId)
{
return ApiResponse<AuthorizationDto>.Fail("无权操作该授权", 403);
}
}
// 仅 Active 状态的授权可续期Cancelled/Expired 请使用重新授权功能
if (entity.Status != AuthorizationStatus.Active)
{
var hint = entity.Status == AuthorizationStatus.Cancelled
? "已取消的授权请使用重新授权功能"
: "已过期的授权请使用重新授权功能";
return ApiResponse<AuthorizationDto>.Fail(hint);
}
// 续期:在原 EndTime 基础上加天数(如果已过期则从当前时间开始)
var baseTime = entity.EndTime > DateTime.Now ? entity.EndTime : DateTime.Now;
entity.EndTime = baseTime.AddDays(days);
entity.Status = AuthorizationStatus.Active;
entity.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
var authorizer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.AuthorizedBy);
await _operationLog.LogAsync("授权", "续期",
$"授权 ID: {id} 续期 {days} 天");
// 通知被授权用户:续期
try
{
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
"授权已续期",
$"您的客户级授权已续期 {days} 天,新到期时间:{entity.EndTime:yyyy-MM-dd}。",
userId: entity.UserId,
eventId: $"auth-renewed-{entity.Id}-{entity.UpdatedAt:yyyyMMddHHmmss}");
}
catch
{
// 通知失败不影响业务
}
return ApiResponse<AuthorizationDto>.Success(MapToDto(entity, entity.User, authorizer), "续期成功");
}
/// <summary>
/// 取消授权(仅 Active 状态可取消)
/// </summary>
public async Task<ApiResponse> CancelAuthorizationAsync(long id)
{
var entity = await _db.Authorizations.FirstOrDefaultAsync(a => a.Id == id);
if (entity == null)
{
return ApiResponse.Fail("授权不存在", 404);
}
// 销售数据隔离
if (_currentUser.Role == UserRole.Sales)
{
var customer = await _db.Users.FirstOrDefaultAsync(u => u.Id == entity.UserId);
if (customer == null || customer.CreatedBy != _currentUser.UserId)
{
return ApiResponse.Fail("无权操作该授权", 403);
}
}
// 仅 Active 状态可取消;已取消/已过期的不可重复取消
if (entity.Status == AuthorizationStatus.Cancelled)
{
return ApiResponse.Fail("该授权已取消,无需重复操作");
}
if (entity.Status == AuthorizationStatus.Expired)
{
return ApiResponse.Fail("该授权已过期,请使用重新授权功能");
}
entity.Status = AuthorizationStatus.Cancelled;
entity.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
await _operationLog.LogAsync("授权", "取消",
$"取消授权 ID: {id}");
// 通知被授权用户:授权已取消
try
{
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
"授权已取消",
"您的客户级授权已被取消。如有疑问请联系销售或客服。",
userId: entity.UserId,
eventId: $"auth-cancelled-{entity.Id}");
}
catch
{
// 通知失败不影响业务
}
return ApiResponse.Success("授权已取消");
}
/// <summary>
/// 检查用户是否有任何有效授权(客户级授权,不检查项目)
/// </summary>
public async Task<ApiResponse<bool>> CheckAuthorizationAsync(long userId)
{
await MarkExpiredAsync();
var hasAuth = await _db.Authorizations
.AnyAsync(a => a.UserId == userId
&& a.Status == AuthorizationStatus.Active
&& a.EndTime > DateTime.Now);
return ApiResponse<bool>.Success(hasAuth);
}
/// <summary>
/// 获取当前客户的授权列表
/// </summary>
public async Task<ApiResponse<List<AuthorizationDto>>> GetMyAuthorizationsAsync()
{
await MarkExpiredAsync();
var userId = _currentUser.UserId;
var query = from a in _db.Authorizations
join u in _db.Users on a.UserId equals u.Id into ug
from u in ug.DefaultIfEmpty()
join au in _db.Users on a.AuthorizedBy equals au.Id into aug
from au in aug.DefaultIfEmpty()
where a.UserId == userId
select new { a, u, au };
var items = await query
.OrderByDescending(x => x.a.CreatedAt)
.Select(x => new AuthorizationDto
{
Id = x.a.Id,
UserId = x.a.UserId,
Username = x.u != null ? x.u.Username : string.Empty,
UserNickname = x.u != null ? x.u.Nickname : string.Empty,
AuthorizedBy = x.a.AuthorizedBy,
AuthorizedByName = x.au != null ? x.au.Nickname : string.Empty,
StartTime = x.a.StartTime,
EndTime = x.a.EndTime,
Status = x.a.Status,
StatusName = x.a.Status.ToString(),
CreatedAt = x.a.CreatedAt
})
.ToListAsync();
return ApiResponse<List<AuthorizationDto>>.Success(items);
}
/// <summary>
/// 自动将过期授权标记为 ExpiredEndTime &lt; Now 且 Status == Active并通知用户
/// </summary>
private async Task MarkExpiredAsync()
{
var now = DateTime.Now;
var expired = await _db.Authorizations
.Where(a => a.Status == AuthorizationStatus.Active && a.EndTime < now)
.ToListAsync();
if (expired.Count > 0)
{
foreach (var item in expired)
{
item.Status = AuthorizationStatus.Expired;
item.UpdatedAt = now;
// 通知用户:授权已过期
try
{
await _notification.CreateNotificationAsync(
NotificationType.Authorization,
"授权已过期",
"您的客户级授权已过期,请联系销售续期后继续体验项目。",
userId: item.UserId,
eventId: $"auth-expired-{item.Id}");
}
catch
{
// 通知失败不影响业务
}
}
await _db.SaveChangesAsync();
}
}
/// <summary>
/// 映射为 DTO不再映射项目字段
/// </summary>
private static AuthorizationDto MapToDto(Authorization entity, User? user, User? authorizer)
{
return new AuthorizationDto
{
Id = entity.Id,
UserId = entity.UserId,
Username = user?.Username ?? string.Empty,
UserNickname = user?.Nickname ?? string.Empty,
AuthorizedBy = entity.AuthorizedBy,
AuthorizedByName = authorizer?.Nickname ?? string.Empty,
StartTime = entity.StartTime,
EndTime = entity.EndTime,
Status = entity.Status,
StatusName = entity.Status.ToString(),
CreatedAt = entity.CreatedAt
};
}
}