添加项目文件。
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.ASPNETCore\IM.ASPNETCore.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.InitCommon\IM.InitCommon.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.Protocols\IM.Protocols.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@ConnectorService_HostAddress = http://localhost:5100
|
||||||
|
|
||||||
|
GET {{ConnectorService_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ConnectorService.Dtos;
|
||||||
|
using ConnectorService.Hubs;
|
||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace ConnectorService.Consumers
|
||||||
|
{
|
||||||
|
public class MessageConsumer : IConsumer<MsgCreatedEvent>
|
||||||
|
{
|
||||||
|
private readonly IHubContext<ChatHub> hub;
|
||||||
|
|
||||||
|
public MessageConsumer(IHubContext<ChatHub> hub)
|
||||||
|
{
|
||||||
|
this.hub = hub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<MsgCreatedEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
await hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveNewMessage", @event.ToHubResponse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY IM_API_NEW.sln ./
|
||||||
|
|
||||||
|
COPY ConnectorService/ConnectorService.csproj ConnectorService/
|
||||||
|
COPY IM.Commons/IM.Commons.csproj IM.Commons/
|
||||||
|
COPY IM.InitCommon/IM.InitCommon.csproj IM.InitCommon/
|
||||||
|
COPY IM.Protocols/IM.Protocols.csproj IM.Protocols/
|
||||||
|
|
||||||
|
RUN dotnet restore ConnectorService/ConnectorService.csproj
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN dotnet publish ConnectorService/ConnectorService.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /app/publish \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "ConnectorService.dll"]
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
|
||||||
|
namespace ConnectorService.Dtos
|
||||||
|
{
|
||||||
|
public record MessageHubResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 客户端去重/回执使用的本地 ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid ClientId { get; init; }
|
||||||
|
|
||||||
|
public string ChatType { get; init; } = string.Empty;
|
||||||
|
public string MsgType { get; init; } = string.Empty;
|
||||||
|
public Guid SenderId { get; init; }
|
||||||
|
public Guid TargetId { get; init; }
|
||||||
|
public string State { get; init; } = string.Empty;
|
||||||
|
public string StreamKey { get; init; } = string.Empty;
|
||||||
|
public long SequenceId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务器推送到达时间 (毫秒级时间戳,强烈建议加上)
|
||||||
|
/// </summary>
|
||||||
|
public long PushTimestamp { get; init; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
public HubMsgContentDto Content { get; init; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 嵌套的内容对象,允许 Ext 和 Quote 为 null 以缩减 JSON 体积
|
||||||
|
public record HubMsgContentDto(
|
||||||
|
string Fallback,
|
||||||
|
object Body,
|
||||||
|
Dictionary<string, string>? Ext,
|
||||||
|
HubQuoteInfoDto? Quote
|
||||||
|
);
|
||||||
|
|
||||||
|
public record HubQuoteInfoDto(
|
||||||
|
Guid MessageId,
|
||||||
|
Guid SenderId,
|
||||||
|
string SenderName,
|
||||||
|
string MessageType,
|
||||||
|
string Preview
|
||||||
|
);
|
||||||
|
public static class MessageEventMapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将内部集成事件转换为对外推送的 DTO
|
||||||
|
/// </summary>
|
||||||
|
public static MessageHubResponse ToHubResponse(this MsgCreatedEvent @event)
|
||||||
|
{
|
||||||
|
if (@event == null) throw new ArgumentNullException(nameof(@event));
|
||||||
|
|
||||||
|
return new MessageHubResponse
|
||||||
|
{
|
||||||
|
Id = @event.Id,
|
||||||
|
ClientId = @event.ClientId,
|
||||||
|
ChatType = @event.ChatType,
|
||||||
|
MsgType = @event.MsgType,
|
||||||
|
SenderId = @event.SenderId,
|
||||||
|
TargetId = @event.TargetId,
|
||||||
|
State = @event.State,
|
||||||
|
StreamKey = @event.StreamKey,
|
||||||
|
SequenceId = @event.SequenceId,
|
||||||
|
// 嵌套映射
|
||||||
|
Content = @event.Content != null ? new HubMsgContentDto(
|
||||||
|
@event.Content.Fallback,
|
||||||
|
@event.Content.Body,
|
||||||
|
@event.Content.Ext,
|
||||||
|
@event.Content.Quote != null ? new HubQuoteInfoDto(
|
||||||
|
@event.Content.Quote.MessageId,
|
||||||
|
@event.Content.Quote.SenderId,
|
||||||
|
@event.Content.Quote.SenderName,
|
||||||
|
@event.Content.Quote.MessageType,
|
||||||
|
@event.Content.Quote.Preview
|
||||||
|
) : null
|
||||||
|
) : null!
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using ConnectorService.Services;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace ConnectorService.Hubs
|
||||||
|
{
|
||||||
|
public class ChatHub : Hub
|
||||||
|
{
|
||||||
|
private readonly IConversationIntergrationService conService;
|
||||||
|
private readonly StackExchange.Redis.IDatabase redis;
|
||||||
|
|
||||||
|
public ChatHub(IConversationIntergrationService conService, IConnectionMultiplexer multiplexer)
|
||||||
|
{
|
||||||
|
this.conService = conService;
|
||||||
|
this.redis = multiplexer.GetDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async override Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
if (!Context.User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
Context.Abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
var res = await conService.GetUserStreamKeysAsync(Guid.Parse(userId));
|
||||||
|
foreach (var streamkey in res)
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, streamkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.SetAddAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId);
|
||||||
|
|
||||||
|
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async override Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
if (Context.User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
await redis.SetRemoveAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId);
|
||||||
|
}
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using IM.Commons;
|
||||||
|
using IM.Protocols.Grpc.Conversation;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ConnectorService
|
||||||
|
{
|
||||||
|
public class ModuleInit : IModuleInitializer
|
||||||
|
{
|
||||||
|
public void Initialize(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddRedisCache();
|
||||||
|
services.AddGrpcClient<ConversationInternal.ConversationInternalClient>((sp ,o) =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptionsMonitor<GrpcOptions>>();
|
||||||
|
o.Address = new Uri(options.CurrentValue.MessageServiceUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
using ConnectorService.Hubs;
|
||||||
|
using IM.InitCommon;
|
||||||
|
|
||||||
|
namespace ConnectorService
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.ConfigureDbConfiguration();
|
||||||
|
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.ConfigExtraServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseAppDefault();
|
||||||
|
|
||||||
|
|
||||||
|
app.MapHub<ChatHub>("/chat");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:46392",
|
||||||
|
"sslPort": 44313
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5100",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7115;http://localhost:5100",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using IM.Commons;
|
||||||
|
using IM.Protocols.Grpc.Conversation;
|
||||||
|
|
||||||
|
namespace ConnectorService.Services
|
||||||
|
{
|
||||||
|
public class ConversationIntegrationService : IConversationIntergrationService
|
||||||
|
{
|
||||||
|
private readonly ConversationInternal.ConversationInternalClient client;
|
||||||
|
public async Task<List<string>> GetUserStreamKeysAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var req = new GetUserStreamKeysRequest()
|
||||||
|
{
|
||||||
|
UserId = userId.ToString()
|
||||||
|
};
|
||||||
|
var res = await client.GetUserStreamKeysAsync(req);
|
||||||
|
if(res == null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new List<string>();
|
||||||
|
foreach(var item in res.StreamKeys)
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ConnectorService.Services
|
||||||
|
{
|
||||||
|
public interface IConversationIntergrationService
|
||||||
|
{
|
||||||
|
Task<List<string>> GetUserStreamKeysAsync(Guid userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||||
|
<ProjectReference Include="..\DomainCommons\IM.DomainCommons.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using ContactService.Domain.Events;
|
||||||
|
using ContactService.Domain.ValueObjects;
|
||||||
|
using IM.DomainCommons;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Entities
|
||||||
|
{
|
||||||
|
public class Friend : AggregateRootEntity
|
||||||
|
{
|
||||||
|
public UserProfile Owner { get; private set; }
|
||||||
|
public UserProfile Target { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 好友备注名
|
||||||
|
/// </summary>
|
||||||
|
public string? RemarkName { get; private set; }
|
||||||
|
public FriendStatus Status { get; private set; }
|
||||||
|
|
||||||
|
private Friend() { }
|
||||||
|
|
||||||
|
public Friend(UserProfile owner, UserProfile target, string? remarkName)
|
||||||
|
{
|
||||||
|
Owner = owner;
|
||||||
|
Target = target;
|
||||||
|
RemarkName = remarkName;
|
||||||
|
Status = FriendStatus.Added;
|
||||||
|
|
||||||
|
AddDomainEvent(new FriendAddedDomainEvent(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemarkName(string? remarkName)
|
||||||
|
{
|
||||||
|
if (remarkName.Length > 20)
|
||||||
|
{
|
||||||
|
throw new DomainException("备注名过长");
|
||||||
|
}
|
||||||
|
RemarkName = remarkName ?? RemarkName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Block()
|
||||||
|
{
|
||||||
|
Status = FriendStatus.Blocked;
|
||||||
|
AddDomainEvent(new FriendBlockDomainEvent(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateUserInfo(UserProfile profile)
|
||||||
|
{
|
||||||
|
if (profile.Id != Target.Id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Target = profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using ContactService.Domain.Events;
|
||||||
|
using IM.DomainCommons;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Entities
|
||||||
|
{
|
||||||
|
public class FriendRequest : AggregateRootEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 申请人
|
||||||
|
/// </summary>
|
||||||
|
public Guid OwnerId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被申请人
|
||||||
|
/// </summary>
|
||||||
|
public Guid TargetId { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 申请附言
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; private set; } = "申请添加好友";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑)
|
||||||
|
/// </summary>
|
||||||
|
public FriendRequestStatus State { get; private set; } = FriendRequestStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
public string? RemarkName { get; private set; }
|
||||||
|
|
||||||
|
private FriendRequest() { }
|
||||||
|
|
||||||
|
public FriendRequest(Guid ownerId, Guid targetId, string? description, string? remarkName)
|
||||||
|
{
|
||||||
|
OwnerId = ownerId;
|
||||||
|
TargetId = targetId;
|
||||||
|
Description = description ?? Description;
|
||||||
|
RemarkName = remarkName;
|
||||||
|
AddDomainEvent(new FriendRequestCreatedDomainEvent(this));
|
||||||
|
}
|
||||||
|
public void Accept(string remarkName)
|
||||||
|
{
|
||||||
|
if (State != FriendRequestStatus.Pending)
|
||||||
|
{
|
||||||
|
throw new DomainException("只能处理待处理的好友请求");
|
||||||
|
}
|
||||||
|
State = FriendRequestStatus.Passed;
|
||||||
|
AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this,remarkName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reject()
|
||||||
|
{
|
||||||
|
if (State != FriendRequestStatus.Pending)
|
||||||
|
{
|
||||||
|
throw new DomainException("只能处理待处理的好友请求");
|
||||||
|
}
|
||||||
|
State = FriendRequestStatus.Declined;
|
||||||
|
AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this));
|
||||||
|
}
|
||||||
|
public void Block()
|
||||||
|
{
|
||||||
|
if (State != FriendRequestStatus.Pending)
|
||||||
|
{
|
||||||
|
throw new DomainException("只能处理待处理的好友请求");
|
||||||
|
}
|
||||||
|
State = FriendRequestStatus.Blocked;
|
||||||
|
AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Events
|
||||||
|
{
|
||||||
|
public record FriendAddedDomainEvent(Friend Friend) : INotification;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Events
|
||||||
|
{
|
||||||
|
public record FriendBlockDomainEvent(Friend Friend) : INotification;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Events
|
||||||
|
{
|
||||||
|
public record FriendRequestCreatedDomainEvent(FriendRequest Request) : INotification;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.Domain.Events
|
||||||
|
{
|
||||||
|
public record FriendRequestStateUpdateDomainEvent(FriendRequest Request,string? AcceptRemarkName = default) : INotification;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using ContactService.Domain.ValueObjects;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public class FriendDomainService
|
||||||
|
{
|
||||||
|
private readonly IFriendReposity reposity;
|
||||||
|
|
||||||
|
public FriendDomainService(IFriendReposity reposity)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<Friend?>> CreateAsync(UserProfile owner, UserProfile target, string? remarkName)
|
||||||
|
{
|
||||||
|
var isExist = await reposity.CheckOwnerIdAndTargetIdAsync(owner.Id, target.Id);
|
||||||
|
if (isExist)
|
||||||
|
{
|
||||||
|
return Result<Friend?>.Fail(ResultCode.ALREADY_FRIENDS);
|
||||||
|
}
|
||||||
|
var friend = new Friend(owner, target, remarkName);
|
||||||
|
var res = await reposity.CreateAsync(friend);
|
||||||
|
return Result<Friend?>.Success(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public class FriendRequestDomainService
|
||||||
|
{
|
||||||
|
private readonly IFriendRequestReposity reposity;
|
||||||
|
|
||||||
|
public FriendRequestDomainService(IFriendRequestReposity reposity)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FriendRequest?>> CreateAsync(Guid ownerId, Guid targetId, string? description, string? remarkName)
|
||||||
|
{
|
||||||
|
var friendRequest = new FriendRequest(ownerId, targetId, description, remarkName);
|
||||||
|
await reposity.CreateAsync(friendRequest);
|
||||||
|
return Result<FriendRequest?>.Success(friendRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public enum FriendRequestStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待处理
|
||||||
|
/// </summary>
|
||||||
|
Pending = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 已通过
|
||||||
|
/// </summary>
|
||||||
|
Passed = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 已拒绝
|
||||||
|
/// </summary>
|
||||||
|
Declined = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 拉黑
|
||||||
|
/// </summary>
|
||||||
|
Blocked = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public enum FriendStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待处理
|
||||||
|
/// </summary>
|
||||||
|
Pending = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 已添加
|
||||||
|
/// </summary>
|
||||||
|
Added = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 已拒绝
|
||||||
|
/// </summary>
|
||||||
|
Declined = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// 已拉黑
|
||||||
|
/// </summary>
|
||||||
|
Blocked = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
|
||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public interface IFriendReposity
|
||||||
|
{
|
||||||
|
Task<Friend?> FindByIdAsync(Guid id);
|
||||||
|
Task<Friend?> FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId);
|
||||||
|
Task<IEnumerable<Friend>> FindByTargetAsync(Guid targetId);
|
||||||
|
Task<IEnumerable<Friend>> FindByOwnerAsync(Guid ownerId);
|
||||||
|
|
||||||
|
Task<Friend> CreateAsync(Friend friend);
|
||||||
|
|
||||||
|
Task<bool> CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
|
||||||
|
namespace ContactService.Domain
|
||||||
|
{
|
||||||
|
public interface IFriendRequestReposity
|
||||||
|
{
|
||||||
|
Task<bool> CreateAsync(FriendRequest friendRequest);
|
||||||
|
Task<FriendRequest?> FindByIdAsync(Guid id);
|
||||||
|
Task<IEnumerable<FriendRequest>> FindByOwnerIdAsync(Guid ownerId);
|
||||||
|
Task<IEnumerable<FriendRequest>> FindByTargetIdAsync(Guid targetId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ContactService.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
public class UserProfile
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public string NickName { get; private set; }
|
||||||
|
public string? Avatar { get; private set; }
|
||||||
|
public UserProfile() { }
|
||||||
|
|
||||||
|
public UserProfile(Guid id, string nickName, string? avatar)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
NickName = nickName;
|
||||||
|
Avatar = avatar;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure.Configs
|
||||||
|
{
|
||||||
|
public class FriendConfig : IEntityTypeConfiguration<Friend>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Friend> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("friends");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.OwnsOne(x => x.Owner, owner =>
|
||||||
|
{
|
||||||
|
owner.WithOwner(); // 🔥 关键:明确归属
|
||||||
|
|
||||||
|
owner.Property(p => p.Id).HasColumnName("OwnerId").IsRequired();
|
||||||
|
owner.Property(p => p.NickName).HasColumnName("OwnerNickName");
|
||||||
|
owner.Property(p => p.Avatar).HasColumnName("OwnerAvatarUrl");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.OwnsOne(x => x.Target, target =>
|
||||||
|
{
|
||||||
|
target.WithOwner(); // 🔥 关键
|
||||||
|
|
||||||
|
target.Property(p => p.Id).HasColumnName("TargetId").IsRequired();
|
||||||
|
target.Property(p => p.NickName).HasColumnName("TargetNickName");
|
||||||
|
target.Property(p => p.Avatar).HasColumnName("TargetAvatarUrl");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure.Configs
|
||||||
|
{
|
||||||
|
public class FriendRequestConfig : IEntityTypeConfiguration<FriendRequest>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<FriendRequest> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("friend_requests");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.HasIndex(x => new { x.OwnerId, x.TargetId });
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using IM.Infrastructure.Efcore;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure
|
||||||
|
{
|
||||||
|
public class ContactDbContext : BaseDbContext
|
||||||
|
{
|
||||||
|
public DbSet<Friend> Friends { get; private set; }
|
||||||
|
public DbSet<FriendRequest> FriendRequests { get; private set; }
|
||||||
|
public ContactDbContext(DbContextOptions options, IMediator mediator) : base(options, mediator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
|
||||||
|
|
||||||
|
modelBuilder.EnableSoftDeletionGlobalFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||||
|
<ProjectReference Include="..\ContactService.Domain\ContactService.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\Infrastructure\IM.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure
|
||||||
|
{
|
||||||
|
public class FriendReposity : IFriendReposity
|
||||||
|
{
|
||||||
|
private readonly ContactDbContext db;
|
||||||
|
|
||||||
|
public FriendReposity(ContactDbContext db)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId)
|
||||||
|
{
|
||||||
|
var exist = await db.Friends.AnyAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId);
|
||||||
|
return exist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Friend> CreateAsync(Friend friend)
|
||||||
|
{
|
||||||
|
db.Add(friend);
|
||||||
|
return friend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Friend?> FindByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return db.Friends.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Friend?> FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId)
|
||||||
|
{
|
||||||
|
return db.Friends.FirstOrDefaultAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId);
|
||||||
|
}
|
||||||
|
public async Task<IEnumerable<Friend>> FindByTargetAsync(Guid targetId)
|
||||||
|
{
|
||||||
|
return await db.Friends.Where(x => x.Target.Id == targetId).ToListAsync();
|
||||||
|
}
|
||||||
|
public async Task<IEnumerable<Friend>> FindByOwnerAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
return await db.Friends.Where(x => x.Owner.Id == ownerId).ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure
|
||||||
|
{
|
||||||
|
public class FriendRequestReposity : IFriendRequestReposity
|
||||||
|
{
|
||||||
|
private readonly ContactDbContext db;
|
||||||
|
|
||||||
|
public FriendRequestReposity(ContactDbContext db)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CreateAsync(FriendRequest friendRequest)
|
||||||
|
{
|
||||||
|
db.Add(friendRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<FriendRequest?> FindByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return db.FriendRequests.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<FriendRequest>> FindByOwnerIdAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
return await db.FriendRequests.Where(x => x.OwnerId == ownerId).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<FriendRequest>> FindByTargetIdAsync(Guid targetId)
|
||||||
|
{
|
||||||
|
return await db.FriendRequests.Where(x => x.TargetId == targetId).ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ContactService.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ContactDbContext))]
|
||||||
|
[Migration("20260413114340_InitDb")]
|
||||||
|
partial class InitDb
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Deletion")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("RemarkName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("friends", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Deletion")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("RemarkName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("TargetId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId", "TargetId");
|
||||||
|
|
||||||
|
b.ToTable("friend_requests", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("FriendId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b1.Property<string>("Avatar")
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("OwnerAvatarUrl");
|
||||||
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)")
|
||||||
|
.HasColumnName("OwnerId");
|
||||||
|
|
||||||
|
b1.Property<string>("NickName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("OwnerNickName");
|
||||||
|
|
||||||
|
b1.HasKey("FriendId");
|
||||||
|
|
||||||
|
b1.ToTable("friends");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("FriendId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("FriendId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b1.Property<string>("Avatar")
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("TargetAvatarUrl");
|
||||||
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)")
|
||||||
|
.HasColumnName("TargetId");
|
||||||
|
|
||||||
|
b1.Property<string>("NickName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("TargetNickName");
|
||||||
|
|
||||||
|
b1.HasKey("FriendId");
|
||||||
|
|
||||||
|
b1.ToTable("friends");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("FriendId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Owner")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Target")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitDb : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "friend_requests",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OwnerId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
TargetId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Description = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
State = table.Column<int>(type: "int", nullable: false),
|
||||||
|
RemarkName = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
|
||||||
|
Deletion = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true),
|
||||||
|
ModificationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_friend_requests", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "friends",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OwnerId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OwnerNickName = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
OwnerAvatarUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
TargetId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
TargetNickName = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
TargetAvatarUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
RemarkName = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4"),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
|
||||||
|
Deletion = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true),
|
||||||
|
ModificationTime = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_friends", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_friend_requests_OwnerId_TargetId",
|
||||||
|
table: "friend_requests",
|
||||||
|
columns: new[] { "OwnerId", "TargetId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "friend_requests");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "friends");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ContactService.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ContactDbContext))]
|
||||||
|
partial class ContactDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Deletion")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("RemarkName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("friends", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Deletion")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("RemarkName")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("TargetId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId", "TargetId");
|
||||||
|
|
||||||
|
b.ToTable("friend_requests", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("FriendId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b1.Property<string>("Avatar")
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("OwnerAvatarUrl");
|
||||||
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)")
|
||||||
|
.HasColumnName("OwnerId");
|
||||||
|
|
||||||
|
b1.Property<string>("NickName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("OwnerNickName");
|
||||||
|
|
||||||
|
b1.HasKey("FriendId");
|
||||||
|
|
||||||
|
b1.ToTable("friends");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("FriendId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("FriendId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b1.Property<string>("Avatar")
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("TargetAvatarUrl");
|
||||||
|
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)")
|
||||||
|
.HasColumnName("TargetId");
|
||||||
|
|
||||||
|
b1.Property<string>("NickName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("TargetNickName");
|
||||||
|
|
||||||
|
b1.HasKey("FriendId");
|
||||||
|
|
||||||
|
b1.ToTable("friends");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("FriendId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Owner")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Target")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace ContactService.Infrastructure
|
||||||
|
{
|
||||||
|
public class ModuleInit : IModuleInitializer
|
||||||
|
{
|
||||||
|
public void Initialize(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<FriendDomainService>();
|
||||||
|
services.AddScoped<FriendRequestDomainService>();
|
||||||
|
services.AddScoped<IFriendReposity, FriendReposity>();
|
||||||
|
services.AddScoped<IFriendRequestReposity, FriendRequestReposity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.Dtos
|
||||||
|
{
|
||||||
|
public class FriendRequestResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 申请人
|
||||||
|
/// </summary>
|
||||||
|
public Guid OwnerId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被申请人
|
||||||
|
/// </summary>
|
||||||
|
public Guid TargetId { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 申请附言
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑)
|
||||||
|
/// </summary>
|
||||||
|
public FriendRequestStatus State { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注
|
||||||
|
/// </summary>
|
||||||
|
public string? RemarkName { get; private set; }
|
||||||
|
public DateTimeOffset CreationTime { get; private set; }
|
||||||
|
public DateTimeOffset? Deletion { get; private set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? ModificationTime { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.Dtos
|
||||||
|
{
|
||||||
|
public class FriendResonse
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public Guid TargetId { get; private set; }
|
||||||
|
public string? Avatar { get; private set; }
|
||||||
|
public string NickName { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 好友备注名
|
||||||
|
/// </summary>
|
||||||
|
public string? RemarkName { get; private set; }
|
||||||
|
|
||||||
|
public DateTime CreateTime { get; private set; }
|
||||||
|
public DateTime? UpdateTime { get; private set; }
|
||||||
|
public FriendStatus Status { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ContactService.WebApi.Application.Dtos
|
||||||
|
{
|
||||||
|
public class UserInfoDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string NickName { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string Region { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
public DateTimeOffset CreationTime { get; set; }
|
||||||
|
public DateTimeOffset? Deletion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ContactService.Domain.Events;
|
||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
using MassTransit;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.EventHandler
|
||||||
|
{
|
||||||
|
public class FriendAddedHandler : INotificationHandler<FriendAddedDomainEvent>
|
||||||
|
{
|
||||||
|
private readonly IPublishEndpoint endpoint;
|
||||||
|
|
||||||
|
public FriendAddedHandler(IPublishEndpoint endpoint)
|
||||||
|
{
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(FriendAddedDomainEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await endpoint.Publish(new FriendAddedEvent
|
||||||
|
{
|
||||||
|
OwnerAvatar = notification.Friend.Owner.Avatar,
|
||||||
|
OwnerId = notification.Friend.Owner.Id,
|
||||||
|
OwnerNickName = notification.Friend.Owner.NickName,
|
||||||
|
TargetAvatar = notification.Friend.Target.Avatar,
|
||||||
|
TargetId = notification.Friend.Target.Id,
|
||||||
|
TargetNickName = notification.Friend.Target.NickName,
|
||||||
|
RemarkName = notification.Friend.RemarkName,
|
||||||
|
Status = notification.Friend.Status.ToString(),
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.Domain.Events;
|
||||||
|
using ContactService.Domain.ValueObjects;
|
||||||
|
using ContactService.Infrastructure;
|
||||||
|
using ContactService.WebApi.Application.IntegrationServices;
|
||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
using MassTransit;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.EventHandler
|
||||||
|
{
|
||||||
|
public class FriendRequestStatusUpdateHandler : INotificationHandler<FriendRequestStateUpdateDomainEvent>
|
||||||
|
{
|
||||||
|
private readonly FriendDomainService friendService;
|
||||||
|
private readonly ContactDbContext contactDb;
|
||||||
|
private readonly IPublishEndpoint endpoint;
|
||||||
|
private readonly IIdentityIntegrationService idService;
|
||||||
|
|
||||||
|
public FriendRequestStatusUpdateHandler(FriendDomainService friendService, ContactDbContext contactDb, IPublishEndpoint endpoint, IIdentityIntegrationService idService)
|
||||||
|
{
|
||||||
|
this.friendService = friendService;
|
||||||
|
this.contactDb = contactDb;
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.idService = idService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(FriendRequestStateUpdateDomainEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var @event = notification.Request;
|
||||||
|
if (@event.State == FriendRequestStatus.Passed)
|
||||||
|
{
|
||||||
|
var ownerInfo = await idService.FindUserByIdAsync(@event.OwnerId);
|
||||||
|
var targetInfo = await idService.FindUserByIdAsync(@event.TargetId);
|
||||||
|
|
||||||
|
if (!ownerInfo.Succeeded || !targetInfo.Succeeded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ownerProfile = new UserProfile(ownerInfo.Data.Id, ownerInfo.Data.NickName, ownerInfo.Data.Avatar);
|
||||||
|
var targetProfile = new UserProfile(targetInfo.Data.Id, targetInfo.Data.NickName, targetInfo.Data.Avatar);
|
||||||
|
|
||||||
|
await friendService.CreateAsync(ownerProfile, targetProfile, @event.RemarkName);
|
||||||
|
await friendService.CreateAsync(targetProfile, ownerProfile, notification.AcceptRemarkName);
|
||||||
|
await contactDb.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await endpoint.Publish(new FriendRequestStateUpdateEvent
|
||||||
|
{
|
||||||
|
CorrelationId = @event.TargetId,
|
||||||
|
Description = @event.Description,
|
||||||
|
OwnerId = @event.OwnerId,
|
||||||
|
RemarkName = @event.RemarkName,
|
||||||
|
State = @event.State.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.Infrastructure;
|
||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
using MassTransit;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.EventHandler
|
||||||
|
{
|
||||||
|
public class UserProfileUpdateHandler : IConsumer<UserProfileUpdateEvent>
|
||||||
|
{
|
||||||
|
private readonly ContactDbContext contactDb;
|
||||||
|
private readonly IFriendReposity reposity;
|
||||||
|
|
||||||
|
public UserProfileUpdateHandler(ContactDbContext contactDb, IFriendReposity reposity)
|
||||||
|
{
|
||||||
|
this.contactDb = contactDb;
|
||||||
|
this.reposity = reposity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<UserProfileUpdateEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
var friends = await reposity.FindByTargetAsync(@event.UserId);
|
||||||
|
|
||||||
|
foreach (var friend in friends)
|
||||||
|
{
|
||||||
|
friend.UpdateUserInfo(
|
||||||
|
new Domain.ValueObjects.UserProfile(
|
||||||
|
@event.UserId, @event.Avatar, @event.NickName));
|
||||||
|
}
|
||||||
|
|
||||||
|
await contactDb.SaveChangesAsync();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.Friend
|
||||||
|
{
|
||||||
|
public class FriendMapperConfig : Profile
|
||||||
|
{
|
||||||
|
public FriendMapperConfig()
|
||||||
|
{
|
||||||
|
CreateMap<Domain.Entities.Friend, FriendResonse>()
|
||||||
|
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||||
|
.ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Target.Avatar))
|
||||||
|
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status))
|
||||||
|
.ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => src.ModificationTime.Value.DateTime))
|
||||||
|
.ForMember(dest => dest.CreateTime, opt => opt.MapFrom(src => src.CreationTime.DateTime))
|
||||||
|
.ForMember(dest => dest.NickName, opt => opt.MapFrom(src => src.Target.NickName))
|
||||||
|
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => src.RemarkName))
|
||||||
|
.ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.Target.Id))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using ContactService.WebApi.Application.IntegrationServices;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.Friend
|
||||||
|
{
|
||||||
|
public class FriendService
|
||||||
|
{
|
||||||
|
private readonly IFriendReposity reposity;
|
||||||
|
private readonly FriendDomainService service;
|
||||||
|
private readonly IIdentityIntegrationService idService;
|
||||||
|
private readonly IMapper mapper;
|
||||||
|
|
||||||
|
public FriendService(IFriendReposity reposity, FriendDomainService service
|
||||||
|
, IIdentityIntegrationService idService, IMapper mapper
|
||||||
|
)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
this.service = service;
|
||||||
|
this.idService = idService;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<FriendResonse>>> GetFriendsByOwnerIdAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
IEnumerable<Domain.Entities.Friend> friend = await reposity.FindByOwnerAsync(ownerId);
|
||||||
|
return Result<List<FriendResonse>>.Success(mapper.Map<List<FriendResonse>>(friend.ToList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<object>> DeleteFriendAsync(Guid userId, Guid friendId)
|
||||||
|
{
|
||||||
|
var friend = await reposity.FindByIdAsync(friendId);
|
||||||
|
if (friend is null)
|
||||||
|
{
|
||||||
|
return Result<object>.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friend.Owner.Id != userId)
|
||||||
|
{
|
||||||
|
return Result<object>.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend.SoftDelete();
|
||||||
|
|
||||||
|
return Result<object>.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<object>> BlockFriendAsync(Guid userId, Guid friendId)
|
||||||
|
{
|
||||||
|
var friend = await reposity.FindByIdAsync(friendId);
|
||||||
|
if (friend is null)
|
||||||
|
{
|
||||||
|
return Result<object>.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friend.Owner.Id != userId)
|
||||||
|
{
|
||||||
|
return Result<object>.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend.Block();
|
||||||
|
return Result<object>.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<bool>> CheckFriendAsync(Guid ownerId, Guid targetId)
|
||||||
|
{
|
||||||
|
var exist = await reposity.CheckOwnerIdAndTargetIdAsync(ownerId, targetId);
|
||||||
|
return Result.Success(exist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ContactService.WebApi.Application.FriendRequest
|
||||||
|
{
|
||||||
|
public record CreateFriendRequestCommand
|
||||||
|
{
|
||||||
|
public Guid ownerId { get; private set; }
|
||||||
|
public Guid targetId { get; private set; }
|
||||||
|
public string? description { get; private set; }
|
||||||
|
public string? remarkName { get; private set; }
|
||||||
|
|
||||||
|
public CreateFriendRequestCommand(Guid ownerId, Guid targetId, string? description, string? remarkName)
|
||||||
|
{
|
||||||
|
this.ownerId = ownerId;
|
||||||
|
this.targetId = targetId;
|
||||||
|
this.description = description;
|
||||||
|
this.remarkName = remarkName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.FriendRequest
|
||||||
|
{
|
||||||
|
public class FriendRequestConfig : Profile
|
||||||
|
{
|
||||||
|
public FriendRequestConfig()
|
||||||
|
{
|
||||||
|
CreateMap<Domain.Entities.FriendRequest, FriendRequestResponse>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace ContactService.WebApi.Application.FriendRequest
|
||||||
|
{
|
||||||
|
public class FriendRequestHandleCommand
|
||||||
|
{
|
||||||
|
public Guid UserId { get; private set; }
|
||||||
|
public Guid RequestId { get; private set; }
|
||||||
|
public FriendRequestAction Action { get; private set; }
|
||||||
|
public string? RemarkName { get; set; }
|
||||||
|
|
||||||
|
public FriendRequestHandleCommand(Guid userId, Guid requestId, FriendRequestAction action, string? remarkName)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
RequestId = requestId;
|
||||||
|
Action = action;
|
||||||
|
RemarkName = remarkName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FriendRequestAction
|
||||||
|
{
|
||||||
|
Accpet = 0,
|
||||||
|
Reject = 1,
|
||||||
|
Block = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using ContactService.Domain;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.FriendRequest
|
||||||
|
{
|
||||||
|
public class FriendRequestService
|
||||||
|
{
|
||||||
|
private readonly IFriendRequestReposity reposity;
|
||||||
|
private readonly FriendRequestDomainService service;
|
||||||
|
private readonly IMapper mapper;
|
||||||
|
|
||||||
|
public FriendRequestService(IFriendRequestReposity reposity, FriendRequestDomainService service, IMapper mapper)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
this.service = service;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FriendRequestResponse>> CreateAsync(CreateFriendRequestCommand command)
|
||||||
|
{
|
||||||
|
var request = await service.CreateAsync(command.ownerId, command.targetId, command.description, command.remarkName);
|
||||||
|
if (!request.Succeeded)
|
||||||
|
{
|
||||||
|
return Result<FriendRequestResponse>.Fail(request);
|
||||||
|
}
|
||||||
|
return Result<FriendRequestResponse>.Success(mapper.Map<FriendRequestResponse>(request.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FriendRequestResponse>> UpdateStatusAsync(FriendRequestHandleCommand command)
|
||||||
|
{
|
||||||
|
var request = await reposity.FindByIdAsync(command.RequestId);
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
return Result<FriendRequestResponse>.Fail(ResultCode.FRIEND_REQUEST_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.TargetId != command.UserId)
|
||||||
|
{
|
||||||
|
return Result<FriendRequestResponse>.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (command.Action)
|
||||||
|
{
|
||||||
|
case FriendRequestAction.Accpet:
|
||||||
|
request.Accept(command.RemarkName);
|
||||||
|
break;
|
||||||
|
case FriendRequestAction.Block:
|
||||||
|
request.Block();
|
||||||
|
break;
|
||||||
|
case FriendRequestAction.Reject:
|
||||||
|
request.Reject();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return Result<FriendRequestResponse>.Fail(ResultCode.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
return Result<FriendRequestResponse>.Success(mapper.Map<FriendRequestResponse>(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<FriendRequestResponse>>> GetByOwnerIdAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
var requests = await reposity.FindByOwnerIdAsync(ownerId);
|
||||||
|
return Result<List<FriendRequestResponse>>.Success(mapper.Map<List<FriendRequestResponse>>(requests));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<FriendRequestResponse>>> GetByTargetIdAsync(Guid targetId)
|
||||||
|
{
|
||||||
|
var requests = await reposity.FindByTargetIdAsync(targetId);
|
||||||
|
return Result<List<FriendRequestResponse>>.Success(mapper.Map<List<FriendRequestResponse>>(requests));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<FriendRequestResponse>>> GetByTargetIdOrOwnerIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var requests = await reposity.FindByTargetIdAsync(id);
|
||||||
|
var requests2 = await reposity.FindByOwnerIdAsync(id);
|
||||||
|
return Result<List<FriendRequestResponse>>.Success(mapper.Map<List<FriendRequestResponse>>(requests.Concat(requests2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.IntegrationServices
|
||||||
|
{
|
||||||
|
public interface IIdentityIntegrationService
|
||||||
|
{
|
||||||
|
Task<Result<UserInfoDto>> FindUserByIdAsync(Guid id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using Grpc.Core;
|
||||||
|
using IM.Commons;
|
||||||
|
using IM.Protocols.Grpc.User;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Application.IntegrationServices
|
||||||
|
{
|
||||||
|
public class IdentityIntegrationService : IIdentityIntegrationService
|
||||||
|
{
|
||||||
|
private readonly UserInternal.UserInternalClient client;
|
||||||
|
|
||||||
|
public IdentityIntegrationService(UserInternal.UserInternalClient client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<UserInfoDto>> FindUserByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var req = new GetUserInfoRequest()
|
||||||
|
{
|
||||||
|
UserId = id.ToString()
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await client.GetUserInfoAsyncAsync(req);
|
||||||
|
return Result.Success(new UserInfoDto
|
||||||
|
{
|
||||||
|
Avatar = res.Avatar,
|
||||||
|
CreationTime = res.CreationTime.ToDateTimeOffset(),
|
||||||
|
Deletion = res.Deletion.ToDateTimeOffset(),
|
||||||
|
Description = res.Description,
|
||||||
|
Email = res.Email,
|
||||||
|
Id = Guid.Parse(res.Id),
|
||||||
|
NickName = res.NickName,
|
||||||
|
Phone = res.Phone,
|
||||||
|
Region = res.Region,
|
||||||
|
UserName = res.UserName
|
||||||
|
});
|
||||||
|
}catch(RpcException e)
|
||||||
|
{
|
||||||
|
return Result.Fail<UserInfoDto>(ResultCode.USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||||
|
<ProjectReference Include="..\ContactService.Domain\ContactService.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\ContactService.Infrastructure\ContactService.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.ASPNETCore\IM.ASPNETCore.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.InitCommon\IM.InitCommon.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.Protocols\IM.Protocols.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@ContactService.WebApi_HostAddress = http://localhost:5294
|
||||||
|
|
||||||
|
GET {{ContactService.WebApi_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
###
|
||||||
|
|
||||||
|
GET {{ContactService.WebApi_HostAddress}}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using ContactService.Infrastructure;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using ContactService.WebApi.Application.Friend;
|
||||||
|
using IM.ASPNETCore;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]/[action]")]
|
||||||
|
[ApiController]
|
||||||
|
public class FriendController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly FriendService service;
|
||||||
|
|
||||||
|
public FriendController(FriendService service)
|
||||||
|
{
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesDefaultResponseType(typeof(Result<FriendResonse>))]
|
||||||
|
public async Task<IActionResult> List()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var friends = await service.GetFriendsByOwnerIdAsync(Guid.Parse(userId));
|
||||||
|
return Ok(friends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[UnitOfWork(typeof(ContactDbContext))]
|
||||||
|
public async Task<IActionResult> Delete([FromQuery] Guid friendId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.DeleteFriendAsync(Guid.Parse(userId), friendId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[UnitOfWork(typeof(ContactDbContext))]
|
||||||
|
public async Task<IActionResult> Block([FromQuery] Guid friendId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.BlockFriendAsync(Guid.Parse(userId), friendId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> CheckFriend(Guid userId, Guid targetId)
|
||||||
|
{
|
||||||
|
return Ok(await service.CheckFriendAsync(userId, targetId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Controllers
|
||||||
|
{
|
||||||
|
public class FriendRequestAddRequest
|
||||||
|
{
|
||||||
|
public Guid TargetId { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? RemarkName { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FriendRequestAddRequestValidator : AbstractValidator<FriendRequestAddRequest>
|
||||||
|
{
|
||||||
|
public FriendRequestAddRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(r => r.TargetId)
|
||||||
|
.NotEmpty()
|
||||||
|
.NotNull()
|
||||||
|
;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using ContactService.Infrastructure;
|
||||||
|
using ContactService.WebApi.Application.Dtos;
|
||||||
|
using ContactService.WebApi.Application.FriendRequest;
|
||||||
|
using IM.ASPNETCore;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]/[action]")]
|
||||||
|
[ApiController]
|
||||||
|
public class FriendRequestController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly FriendRequestService service;
|
||||||
|
|
||||||
|
public FriendRequestController(FriendRequestService service)
|
||||||
|
{
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[UnitOfWork(typeof(ContactDbContext))]
|
||||||
|
[ProducesDefaultResponseType(typeof(Result<FriendRequestResponse?>))]
|
||||||
|
public async Task<IActionResult> Add(FriendRequestAddRequest request)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.CreateAsync(new CreateFriendRequestCommand(Guid.Parse(userId), request.TargetId, request.Description, request.RemarkName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[UnitOfWork(typeof(ContactDbContext))]
|
||||||
|
public async Task<IActionResult> Handle(FriendRequestHandleRequest request)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.UpdateStatusAsync(new FriendRequestHandleCommand(Guid.Parse(userId), request.RequestId, request.Action,request.RemarkName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> List()
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.GetByTargetIdOrOwnerIdAsync(Guid.Parse(userId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ContactService.WebApi.Application.FriendRequest;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Controllers
|
||||||
|
{
|
||||||
|
public class FriendRequestHandleRequest
|
||||||
|
{
|
||||||
|
public Guid RequestId { get; set; }
|
||||||
|
public FriendRequestAction Action { get; set; }
|
||||||
|
public string? RemarkName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FriendRequestHandleRequestValidator : AbstractValidator<FriendRequestHandleRequest>
|
||||||
|
{
|
||||||
|
public FriendRequestHandleRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(r => r.RequestId)
|
||||||
|
.NotEmpty()
|
||||||
|
.NotNull();
|
||||||
|
|
||||||
|
When(w => w.Action == FriendRequestAction.Accpet, () =>
|
||||||
|
{
|
||||||
|
RuleFor(r => r.RemarkName)
|
||||||
|
.NotEmpty()
|
||||||
|
.NotNull();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using ContactService.Infrastructure;
|
||||||
|
using IM.InitCommon;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi
|
||||||
|
{
|
||||||
|
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ContactDbContext>
|
||||||
|
{
|
||||||
|
public ContactDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
// 1. 复用你写好的配置工厂,提取连接字符串
|
||||||
|
var optionsBuilder = DbContextOptionsBuilderFactory.Create<ContactDbContext>();
|
||||||
|
|
||||||
|
// 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求!
|
||||||
|
return new ContactDbContext(optionsBuilder.Options, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY IM_API_NEW.sln ./
|
||||||
|
|
||||||
|
COPY ContactService.WebApi/ContactService.WebApi.csproj ContactService.WebApi/
|
||||||
|
COPY ContactService.Domain/ContactService.Domain.csproj ContactService.Domain/
|
||||||
|
COPY ContactService.Infrastructure/ContactService.Infrastructure.csproj ContactService.Infrastructure/
|
||||||
|
COPY DomainCommons/IM.DomainCommons.csproj DomainCommons/
|
||||||
|
COPY Infrastructure/IM.Infrastructure.csproj Infrastructure/
|
||||||
|
COPY IM.ASPNETCore/IM.ASPNETCore.csproj IM.ASPNETCore/
|
||||||
|
COPY IM.Commons/IM.Commons.csproj IM.Commons/
|
||||||
|
COPY IM.InitCommon/IM.InitCommon.csproj IM.InitCommon/
|
||||||
|
COPY IM.Jwt/IM.Jwt.csproj IM.Jwt/
|
||||||
|
COPY IM.Protocols/IM.Protocols.csproj IM.Protocols/
|
||||||
|
|
||||||
|
RUN dotnet restore ContactService.WebApi/ContactService.WebApi.csproj
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN dotnet publish ContactService.WebApi/ContactService.WebApi.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /app/publish \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "ContactService.WebApi.dll"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ContactService.WebApi.Application.Friend;
|
||||||
|
using ContactService.WebApi.Application.FriendRequest;
|
||||||
|
using ContactService.WebApi.Application.IntegrationServices;
|
||||||
|
using IM.Commons;
|
||||||
|
using IM.Protocols.Grpc.User;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi
|
||||||
|
{
|
||||||
|
public class ModuleInit : IModuleInitializer
|
||||||
|
{
|
||||||
|
public void Initialize(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<FriendService>();
|
||||||
|
services.AddScoped<FriendRequestService>();
|
||||||
|
services.AddScoped<IIdentityIntegrationService, IdentityIntegrationService>();
|
||||||
|
services.AddGrpcClient<UserInternal.UserInternalClient>((sp, o) =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptionsMonitor<GrpcOptions>>();
|
||||||
|
o.Address = new Uri(options.CurrentValue.IdentityServiceUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
using IM.InitCommon;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.ConfigureDbConfiguration();
|
||||||
|
|
||||||
|
//builder.Services.AddControllers();
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.ConfigExtraServices();
|
||||||
|
|
||||||
|
builder.Services.AddAllGrpcServer();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseAppDefault();
|
||||||
|
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.MapAllGrpcServer();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"applicationUrl": "http://localhost:5294"
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"applicationUrl": "https://localhost:7242;http://localhost:5294"
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:46536",
|
||||||
|
"sslPort": 44317
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iissettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:53560/",
|
||||||
|
"sslPort": 44389
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using ContactService.WebApi.Application.Friend;
|
||||||
|
using Grpc.Core;
|
||||||
|
using IM.Protocols.Grpc.Contact;
|
||||||
|
|
||||||
|
namespace ContactService.WebApi.Services
|
||||||
|
{
|
||||||
|
public class ContactintegrationService: ContactInternal.ContactInternalBase
|
||||||
|
{
|
||||||
|
private readonly Application.Friend.FriendService friendService;
|
||||||
|
|
||||||
|
public ContactintegrationService(FriendService friendService)
|
||||||
|
{
|
||||||
|
this.friendService = friendService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<CheckFriendshipResponse> CheckFriendship(CheckFriendshipRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var response = new CheckFriendshipResponse();
|
||||||
|
var res = await friendService.CheckFriendAsync(
|
||||||
|
Guid.Parse(request.OwnerId),
|
||||||
|
Guid.Parse(request.TargetId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if(res.Succeeded && res.Data)
|
||||||
|
{
|
||||||
|
response.Checked = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Checked = false;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public class AggregateRootEntity : BaseEntity, IAggregateRoot, ISoftDelete, IHasCreationTime, IHasDeletionTime, IHasModificationTime
|
||||||
|
{
|
||||||
|
public bool IsDeleted { get; private set; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreationTime { get; private set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public DateTimeOffset? Deletion { get; private set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? ModificationTime { get; set; }
|
||||||
|
|
||||||
|
public void SoftDelete()
|
||||||
|
{
|
||||||
|
Deletion = DateTime.Now;
|
||||||
|
IsDeleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyModified()
|
||||||
|
{
|
||||||
|
ModificationTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using MediatR;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public class BaseEntity : IEntity, IDomainEvents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 这里使用连续guid,防止数据库性能问题
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; private set; } = NewId.NextGuid();
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public List<INotification> domainEvents = [];
|
||||||
|
|
||||||
|
public void AddDomainEvent(INotification eventItem)
|
||||||
|
{
|
||||||
|
domainEvents.Add(eventItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDomainEventIfAbsent(INotification eventItem)
|
||||||
|
{
|
||||||
|
if (!domainEvents.Contains(eventItem))
|
||||||
|
{
|
||||||
|
domainEvents.Add(eventItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearDomainEvents()
|
||||||
|
{
|
||||||
|
domainEvents.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<INotification> GetDomainEvents()
|
||||||
|
{
|
||||||
|
return domainEvents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public class DomainException : Exception
|
||||||
|
{
|
||||||
|
public DomainException(string message) : base(message)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IAggregateRoot
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IDomainEvents
|
||||||
|
{
|
||||||
|
IEnumerable<INotification> GetDomainEvents();
|
||||||
|
void AddDomainEvent(INotification eventItem);
|
||||||
|
void AddDomainEventIfAbsent(INotification eventItem);
|
||||||
|
void ClearDomainEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IEntity
|
||||||
|
{
|
||||||
|
Guid Id { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IHasCreationTime
|
||||||
|
{
|
||||||
|
DateTimeOffset CreationTime { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IHasDeletionTime
|
||||||
|
{
|
||||||
|
DateTimeOffset? Deletion { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface IHasModificationTime
|
||||||
|
{
|
||||||
|
DateTimeOffset? ModificationTime { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||||
|
<PackageReference Include="NewId" Version="4.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace IM.DomainCommons
|
||||||
|
{
|
||||||
|
public interface ISoftDelete
|
||||||
|
{
|
||||||
|
bool IsDeleted { get; }
|
||||||
|
void SoftDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FileService.Domain\FileService.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||||
|
<ProjectReference Include="..\IM.InitCommon\IM.InitCommon.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FileService.Application.UploadFile;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace FileService.Application
|
||||||
|
{
|
||||||
|
public class ModueInit : IModuleInitializer
|
||||||
|
{
|
||||||
|
public void Initialize(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<UploadFileService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using FileService.Application.StorageContracts;
|
||||||
|
|
||||||
|
namespace FileService.Application.Ports
|
||||||
|
{
|
||||||
|
public interface IObjectStoragePort
|
||||||
|
{
|
||||||
|
string ProviderCode { get; }
|
||||||
|
public Task<InitiateUploadResult> InitUploadAsync(InitiateUploadCommand command,CancellationToken token);
|
||||||
|
public Task<PresignedUrl> GenerateUploadUrlAsync(GenerateUploadUrlCommand command, CancellationToken token);
|
||||||
|
public Task<CompleteUploadResult> CompleteUploadAsync(CompleteUploadCommand command, CancellationToken token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.Ports
|
||||||
|
{
|
||||||
|
public interface IObjectStorageRouter
|
||||||
|
{
|
||||||
|
IObjectStoragePort Route(string providerCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using FileService.Application.StorageContracts;
|
||||||
|
|
||||||
|
namespace FileService.Application.Ports
|
||||||
|
{
|
||||||
|
public interface IStorageRedisCache
|
||||||
|
{
|
||||||
|
Task SetAsync(UploadRuntimeCache upload);
|
||||||
|
Task DeleteAsync(string sessionId);
|
||||||
|
Task DeleteByTaskIdAsync(string taskId);
|
||||||
|
Task<UploadRuntimeCache?> GetAsync(string sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
using FileService.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace FileService.Application.StorageContracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ProviderCode">储存提供商编号</param>
|
||||||
|
/// <param name="Bucket">储存桶名称</param>
|
||||||
|
/// <param name="ObjectKey">鉴权key</param>
|
||||||
|
/// <param name="ContentType"></param>
|
||||||
|
/// <param name="ContentLength"></param>
|
||||||
|
/// <param name="Metadata"></param>
|
||||||
|
public sealed record InitiateUploadCommand(
|
||||||
|
string ProviderCode,
|
||||||
|
string Bucket,
|
||||||
|
string ObjectKey,
|
||||||
|
string ContentType,
|
||||||
|
long ContentLength,
|
||||||
|
IReadOnlyDictionary<string, string>? Metadata = null);
|
||||||
|
|
||||||
|
public sealed record InitiateUploadResult(
|
||||||
|
string UploadSessionId,
|
||||||
|
StorageLocation Location);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ProviderCode"></param>
|
||||||
|
/// <param name="Bucket"></param>
|
||||||
|
/// <param name="ObjectKey"></param>
|
||||||
|
/// <param name="UploadSessionId"></param>
|
||||||
|
/// <param name="PartNumber"></param>
|
||||||
|
/// <param name="ExpiresIn"></param>
|
||||||
|
public sealed record GenerateUploadUrlCommand(
|
||||||
|
string ProviderCode,
|
||||||
|
string Bucket,
|
||||||
|
string ObjectKey,
|
||||||
|
string UploadSessionId,
|
||||||
|
int? PartNumber,
|
||||||
|
TimeSpan ExpiresIn);
|
||||||
|
|
||||||
|
public sealed record PresignedUrl(
|
||||||
|
string Url,
|
||||||
|
IReadOnlyDictionary<string, string> Headers,
|
||||||
|
DateTimeOffset ExpiresAt);
|
||||||
|
|
||||||
|
public sealed record CompleteUploadCommand(
|
||||||
|
string ProviderCode,
|
||||||
|
string Bucket,
|
||||||
|
string Region,
|
||||||
|
string ObjectKey,
|
||||||
|
string UploadSessionId,
|
||||||
|
IReadOnlyList<UploadPart> Parts);
|
||||||
|
|
||||||
|
public sealed record UploadPart(
|
||||||
|
int PartNumber,
|
||||||
|
string ETag,
|
||||||
|
long? Size = null,
|
||||||
|
string? Checksum = null);
|
||||||
|
|
||||||
|
public sealed record CompleteUploadResult(
|
||||||
|
StorageLocation Location,
|
||||||
|
string? ETag,
|
||||||
|
long Size,
|
||||||
|
string? Checksum = null,
|
||||||
|
string? VersionId = null);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.StorageContracts
|
||||||
|
{
|
||||||
|
public sealed class UploadRuntimeCache
|
||||||
|
{
|
||||||
|
public string TaskId { get; init; } = default!;
|
||||||
|
|
||||||
|
public string ProviderCode { get; init; } = default!;
|
||||||
|
|
||||||
|
public string UploadSessionId { get; init; } = default!;
|
||||||
|
|
||||||
|
public string Bucket { get; init; } = default!;
|
||||||
|
|
||||||
|
public string Region { get; init; } = default!;
|
||||||
|
|
||||||
|
public string ObjectKey { get; init; } = default!;
|
||||||
|
|
||||||
|
public long FileSize { get; init; }
|
||||||
|
|
||||||
|
public int TotalPartCount { get; init; }
|
||||||
|
|
||||||
|
public long UploadedBytes { get; private set; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset ExpireAt { get; init; }
|
||||||
|
|
||||||
|
public Dictionary<int, UploadPart> Parts { get; init; } = new();
|
||||||
|
|
||||||
|
public void AddOrUpdatePart(UploadPart part)
|
||||||
|
{
|
||||||
|
Parts[part.PartNumber] = part;
|
||||||
|
|
||||||
|
UploadedBytes = Parts.Sum(x => x.Value.Size ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted()
|
||||||
|
{
|
||||||
|
return Parts.Count == TotalPartCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToJson()
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UploadRuntimeCache? FromJson(string json)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<UploadRuntimeCache>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FileService.Domain;
|
||||||
|
using FileService.Domain.ValueObjects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFile
|
||||||
|
{
|
||||||
|
public class FileResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OwnerId { get; set; }
|
||||||
|
public FileName FileName { get; set; }
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public ContentType ContentType { get; set; }
|
||||||
|
public FileState State { get; set; }
|
||||||
|
public StorageLocation StorageLocation { get; set; }
|
||||||
|
public CheckSum CheckSum { get; set; }
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
public DateTimeOffset Updated { get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFile
|
||||||
|
{
|
||||||
|
public class UploadFileMapperConfig:Profile
|
||||||
|
{
|
||||||
|
public UploadFileMapperConfig()
|
||||||
|
{
|
||||||
|
CreateMap<Domain.Entities.UploadFile, FileResponse>()
|
||||||
|
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime))
|
||||||
|
.ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using FileService.Domain.IReposities;
|
||||||
|
using IM.Commons;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFile
|
||||||
|
{
|
||||||
|
public class UploadFileService
|
||||||
|
{
|
||||||
|
private readonly IUploadFileReposity reposity;
|
||||||
|
private readonly IMapper mapper;
|
||||||
|
|
||||||
|
public UploadFileService(IUploadFileReposity reposity, IMapper mapper)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FileResponse>> GetFileInfoAsync(Guid id)
|
||||||
|
{
|
||||||
|
var file = await reposity.FindByIdAsync(id);
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
|
return Result.Fail<FileResponse>(ResultCode.FILE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success(mapper.Map<FileResponse>(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using FileService.Domain.ValueObjects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFileTask
|
||||||
|
{
|
||||||
|
public class TaskInitResponse
|
||||||
|
{
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
public string UploadSessionId { get; init; }
|
||||||
|
|
||||||
|
public StorageLocation StorageLocation { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using FileService.Application.Ports;
|
||||||
|
using FileService.Domain.Entities;
|
||||||
|
using FileService.Domain.IReposities;
|
||||||
|
using IM.Commons;
|
||||||
|
using IM.InitCommon;
|
||||||
|
using MassTransit.Internals;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFileTask
|
||||||
|
{
|
||||||
|
public class UploadFileTaskService
|
||||||
|
{
|
||||||
|
private readonly IUploadTaskReposity reposity;
|
||||||
|
private readonly IMapper mapper;
|
||||||
|
private readonly IObjectStorageRouter router;
|
||||||
|
private readonly IOptions<StorageOptions> options;
|
||||||
|
|
||||||
|
public UploadFileTaskService(IUploadTaskReposity reposity, IMapper mapper, IObjectStorageRouter router, IOptions<StorageOptions> options)
|
||||||
|
{
|
||||||
|
this.reposity = reposity;
|
||||||
|
this.mapper = mapper;
|
||||||
|
this.router = router;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<TaskInitResponse>> InitTaskAsync(UploadTaskInitCommand command)
|
||||||
|
{
|
||||||
|
CancellationToken cancellationToken = CancellationToken.None;
|
||||||
|
var task = command.ToUploadTask();
|
||||||
|
var storage = router.Route(options.Value.ProviderCode);
|
||||||
|
var initRes = await storage.InitUploadAsync(new StorageContracts.InitiateUploadCommand(
|
||||||
|
ProviderCode: options.Value.ProviderCode,
|
||||||
|
Bucket: options.Value.Bucket,
|
||||||
|
ObjectKey: options.Value.Endpoint,
|
||||||
|
ContentType:task.ContentType.Value,
|
||||||
|
ContentLength: command.FileSize,
|
||||||
|
null
|
||||||
|
), cancellationToken);
|
||||||
|
|
||||||
|
var res = mapper.Map<TaskInitResponse>(initRes);
|
||||||
|
res.TaskId = task.Id;
|
||||||
|
|
||||||
|
return Result.Success(res);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFileTask
|
||||||
|
{
|
||||||
|
public record UploadTaskInitCommand(
|
||||||
|
Guid UploaderId,
|
||||||
|
Guid ConversationId, string FileName,
|
||||||
|
long FileSize,string contentType,
|
||||||
|
string checkSum
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public Domain.Entities.UploadTask ToUploadTask()
|
||||||
|
{
|
||||||
|
return new Domain.Entities.UploadTask(
|
||||||
|
UploaderId,
|
||||||
|
ConversationId, FileName, FileSize, contentType,
|
||||||
|
null,new Domain.ValueObjects.CheckSum("md5", checkSum)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using FileService.Application.StorageContracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFileTask
|
||||||
|
{
|
||||||
|
public class UploadTaskMapperConfig:Profile
|
||||||
|
{
|
||||||
|
public UploadTaskMapperConfig()
|
||||||
|
{
|
||||||
|
CreateMap<InitiateUploadResult, TaskInitResponse>()
|
||||||
|
.ForMember(dest => dest.StorageLocation, opt => opt.MapFrom(src => src.Location))
|
||||||
|
.ForMember(dest => dest.UploadSessionId, opt => opt.MapFrom(src => src.UploadSessionId))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using FileService.Domain.ValueObjects;
|
||||||
|
using IM.DomainCommons;
|
||||||
|
|
||||||
|
namespace FileService.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UploadFile:AggregateRootEntity
|
||||||
|
{
|
||||||
|
public Guid OwnerId { get; private set; }
|
||||||
|
public FileName FileName { get; private set; }
|
||||||
|
public long FileSize { get;private set; }
|
||||||
|
public ContentType ContentType { get;private set; }
|
||||||
|
public FileState State { get; private set; } = FileState.Uploaded;
|
||||||
|
public StorageLocation StorageLocation { get; private set; } = new StorageLocation();
|
||||||
|
public CheckSum CheckSum { get; private set; }
|
||||||
|
|
||||||
|
private UploadFile() { }
|
||||||
|
|
||||||
|
public UploadFile(Guid ownerId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum)
|
||||||
|
{
|
||||||
|
OwnerId = ownerId;
|
||||||
|
FileName = fileName;
|
||||||
|
FileSize = fileSize;
|
||||||
|
ContentType = contentType;
|
||||||
|
StorageLocation = storageLocation ?? new StorageLocation();
|
||||||
|
CheckSum = checkSum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using FileService.Domain.Events;
|
||||||
|
using FileService.Domain.ValueObjects;
|
||||||
|
using IM.DomainCommons;
|
||||||
|
|
||||||
|
namespace FileService.Domain.Entities
|
||||||
|
{
|
||||||
|
public class UploadTask:AggregateRootEntity
|
||||||
|
{
|
||||||
|
public Guid UploaderId { get; private set; }
|
||||||
|
public Guid ConversationId { get; private set; }
|
||||||
|
public FileName FileName { get; private set; }
|
||||||
|
public long FileSize { get; private set; }
|
||||||
|
public ContentType ContentType { get; private set; }
|
||||||
|
public StorageLocation StorageLocation { get; private set; }
|
||||||
|
public UploadTaskState State { get; private set; }
|
||||||
|
public CheckSum CheckSum { get; private set; }
|
||||||
|
|
||||||
|
private UploadTask() { }
|
||||||
|
|
||||||
|
public UploadTask(Guid uploaderId, Guid conversationId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum)
|
||||||
|
{
|
||||||
|
UploaderId = uploaderId;
|
||||||
|
ConversationId = conversationId;
|
||||||
|
FileName = fileName;
|
||||||
|
FileSize = fileSize;
|
||||||
|
ContentType = contentType;
|
||||||
|
StorageLocation = storageLocation ?? new StorageLocation();
|
||||||
|
CheckSum = checkSum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartUpload()
|
||||||
|
{
|
||||||
|
State = UploadTaskState.Uploading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteUpload(StorageLocation location)
|
||||||
|
{
|
||||||
|
State = UploadTaskState.Completed;
|
||||||
|
AddDomainEvent(new UploadTaskCompletedDomainEvent(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail()
|
||||||
|
{
|
||||||
|
State = UploadTaskState.Failed;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using FileService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain.Events
|
||||||
|
{
|
||||||
|
public class UploadTaskCompletedDomainEvent(UploadTask task):INotification;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DomainCommons\IM.DomainCommons.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain
|
||||||
|
{
|
||||||
|
public enum FileState
|
||||||
|
{
|
||||||
|
Created = 0,
|
||||||
|
Uploading = 1,
|
||||||
|
Uploaded = 2,
|
||||||
|
Failed = 3,
|
||||||
|
Deleted = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using FileService.Domain.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain.IReposities
|
||||||
|
{
|
||||||
|
public interface IUploadFileReposity
|
||||||
|
{
|
||||||
|
void Create(UploadFile file);
|
||||||
|
Task<UploadFile?> FindByIdAsync(Guid id);
|
||||||
|
Task<UploadFile?> FindByCheckSumAsync(Guid uploaderId,string value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using FileService.Domain.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain.IReposities
|
||||||
|
{
|
||||||
|
public interface IUploadTaskReposity
|
||||||
|
{
|
||||||
|
void Create(UploadTask task);
|
||||||
|
Task<UploadTask?> FindByIdAsync(Guid id);
|
||||||
|
Task<UploadTask?> FindByCheckSumAsync(string value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain
|
||||||
|
{
|
||||||
|
public enum UploadTaskState
|
||||||
|
{
|
||||||
|
Created = 0,
|
||||||
|
Failed = 1,
|
||||||
|
Uploading = 2,
|
||||||
|
Uploaded = 4,
|
||||||
|
Merging = 5,
|
||||||
|
Completed = 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
public class CheckSum
|
||||||
|
{
|
||||||
|
public string Algorithm { get; }
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public CheckSum(string algorithm, string value)
|
||||||
|
{
|
||||||
|
Algorithm = algorithm;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator CheckSum((string algorithm, string value) value)
|
||||||
|
{
|
||||||
|
return new CheckSum(value.algorithm, value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FileService.Domain.ValueObjects
|
||||||
|
{
|
||||||
|
public class ContentType
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
public ContentType(string contentType)
|
||||||
|
{
|
||||||
|
Value = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator ContentType(string contentType)
|
||||||
|
{
|
||||||
|
return new ContentType(contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user