添加项目文件。

This commit is contained in:
西街长安 2026-04-30 21:08:28 +08:00
parent c60f5fe117
commit 720ef957d4
378 changed files with 14843 additions and 0 deletions

View File

@ -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>

View File

@ -0,0 +1,6 @@
@ConnectorService_HostAddress = http://localhost:5100
GET {{ConnectorService_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -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());
}
}
}

View File

@ -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"]

View File

@ -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!
};
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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();
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
using IM.Commons;
namespace ConnectorService.Services
{
public interface IConversationIntergrationService
{
Task<List<string>> GetUserStreamKeysAsync(Guid userId);
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,7 @@
using ContactService.Domain.Entities;
using MediatR;
namespace ContactService.Domain.Events
{
public record FriendAddedDomainEvent(Friend Friend) : INotification;
}

View File

@ -0,0 +1,7 @@
using ContactService.Domain.Entities;
using MediatR;
namespace ContactService.Domain.Events
{
public record FriendBlockDomainEvent(Friend Friend) : INotification;
}

View File

@ -0,0 +1,7 @@
using ContactService.Domain.Entities;
using MediatR;
namespace ContactService.Domain.Events
{
public record FriendRequestCreatedDomainEvent(FriendRequest Request) : INotification;
}

View File

@ -0,0 +1,7 @@
using ContactService.Domain.Entities;
using MediatR;
namespace ContactService.Domain.Events
{
public record FriendRequestStateUpdateDomainEvent(FriendRequest Request,string? AcceptRemarkName = default) : INotification;
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
};
}

View File

@ -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");
});
}
}
}

View File

@ -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 });
}
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}
}

View File

@ -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>();
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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()
});
}
}
}

View File

@ -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();
}
}
}

View File

@ -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))
;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>();
}
}
}

View File

@ -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
}
}

View File

@ -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)));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
@ContactService.WebApi_HostAddress = http://localhost:5294
GET {{ContactService.WebApi_HostAddress}}/weatherforecast/
Accept: application/json
###
GET {{ContactService.WebApi_HostAddress}}

View File

@ -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));
}
}
}

View File

@ -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()
;
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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();
});
}
}
}

View File

@ -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);
}
}
}

View File

@ -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"]

View File

@ -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);
});
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,10 @@
namespace IM.DomainCommons
{
public class DomainException : Exception
{
public DomainException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,6 @@
namespace IM.DomainCommons
{
public interface IAggregateRoot
{
}
}

View File

@ -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();
}
}

7
DomainCommons/IEntity.cs Normal file
View File

@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IEntity
{
Guid Id { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasCreationTime
{
DateTimeOffset CreationTime { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasDeletionTime
{
DateTimeOffset? Deletion { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasModificationTime
{
DateTimeOffset? ModificationTime { get; }
}
}

View File

@ -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>

View File

@ -0,0 +1,8 @@
namespace IM.DomainCommons
{
public interface ISoftDelete
{
bool IsDeleted { get; }
void SoftDelete();
}
}

View File

@ -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>

View File

@ -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>();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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))
;
}
}
}

View File

@ -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));
}
}
}

View 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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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)
);
}
}
}

View File

@ -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))
;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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,
}
}

View File

@ -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);
}
}
}

View File

@ -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