添加项目文件。

This commit is contained in:
2026-04-30 21:08:28 +08:00
parent c60f5fe117
commit cc017b6495
327 changed files with 12860 additions and 0 deletions
+20
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>
+6
View File
@@ -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,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!
};
}
}
}
+53
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);
}
}
}
+19
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);
});
}
}
}
+42
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();
}
}
}
@@ -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"
}
}
}
+9
View File
@@ -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>
+54
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;
}
}
}
@@ -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
}
}
+22
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
}
}
+16
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);
}
}
@@ -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);
}
}
}
+24
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);
});
}
}
}
+42
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();
}
}
}
@@ -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"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+24
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;
}
}
}
+40
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;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace IM.DomainCommons
{
public class DomainException : Exception
{
public DomainException(string message) : base(message)
{
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace IM.DomainCommons
{
public interface IAggregateRoot
{
}
}
+12
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
View File
@@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IEntity
{
Guid Id { get; }
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasCreationTime
{
DateTimeOffset CreationTime { get; }
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasDeletionTime
{
DateTimeOffset? Deletion { get; }
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace IM.DomainCommons
{
public interface IHasModificationTime
{
DateTimeOffset? ModificationTime { get; }
}
}
+14
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>
+8
View File
@@ -0,0 +1,8 @@
namespace IM.DomainCommons
{
public interface ISoftDelete
{
bool IsDeleted { get; }
void SoftDelete();
}
}
+43
View File
@@ -0,0 +1,43 @@
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; }
public StorageLocation? StorageLocation { get; private set; }
public CheckSum CheckSum { get; private set; }
private UploadFile() { }
public UploadFile(Guid ownerId, string fileName, string contentType)
{
OwnerId = ownerId;
FileName = fileName;
ContentType = contentType;
State = FileState.Created;
}
public void StartUpload()
{
State = FileState.Uploading;
}
public void CompleteUpload(StorageLocation storage, CheckSum checkSum)
{
StorageLocation = storage;
CheckSum = checkSum;
State = FileState.Uploaded;
}
public void Fail()
{
}
}
}
@@ -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>
+17
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
}
}
@@ -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);
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileService.Domain.ValueObjects
{
public class FileName
{
public string Value { get; }
public FileName(string value)
{
if(value.Length > 20)
{
throw new ArgumentException("文件名超出长度");
}
Value = value;
}
public override string ToString()
{
return Value;
}
public static implicit operator FileName(string value)
{
return new FileName(value);
}
}
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileService.Domain.ValueObjects
{
public class StorageLocation
{
public string StorageProvider { get; private set; }
public string Bucket { get; private set; }
public string ObjectKey { get; private set; }
public string? Region { get; private set; }
public StorageLocation(string storageProvider, string bucket, string objectKey, string? region = null)
{
this.StorageProvider = storageProvider;
this.Bucket = bucket;
this.ObjectKey = objectKey;
this.Region = region;
}
}
}
+116
View File
@@ -0,0 +1,116 @@
using GroupService.Domain.Enums;
using GroupService.Domain.Events;
using IM.DomainCommons;
namespace GroupService.Domain.Entities
{
public class Group : AggregateRootEntity
{
/// <summary>
/// 群聊名称
/// </summary>
public string Name { get; private set; } = "新建群聊";
/// <summary>
/// 群主
/// </summary>
public Guid GroupMaster { get; private set; }
/// <summary>
/// 群权限
/// (0:需管理员同意,1:任意人可加群,2:不允许任何人加入)
/// </summary>
public GroupAuthorityType Authority { get; private set; } = GroupAuthorityType.REQUIRE_CONSENT;
/// <summary>
/// 全员禁言(false允许发言,true全员禁言)
/// </summary>
public bool AllMembersBanned { get; private set; } = false;
/// <summary>
/// 群聊状态
/// (1:正常,2:封禁)
/// </summary>
public GroupState Status { get; private set; } = GroupState.Normal;
/// <summary>
/// 群公告
/// </summary>
public string Announcement { get; private set; } = string.Empty;
/// <summary>
/// 群头像
/// </summary>
public string? Avatar { get; private set; } = "https://ts1.tc.mm.bing.net/th/id/OIP-C.XiB9NHwI57WPmotCbXuwawAAAA?rs=1&pid=ImgDetMain&o=7&rm=3";
public long MaxSequenceId { get; private set; } = 0;
public string LastMessage { get; private set; } = string.Empty;
public string LastSenderName { get; private set; } = string.Empty;
private Group() { }
public Group(Guid groupMaster, string name = "新建群聊")
{
Name = name;
GroupMaster = groupMaster;
AddDomainEvent(new GroupCreateDomainEvent(this));
}
public void setAllMembersBanned(bool isBanned)
{
if (isBanned == AllMembersBanned) return;
AllMembersBanned = isBanned;
AddDomainEvent(new AllMembersBannedDomainEvent(this));
}
public void Ban()
{
Status = GroupState.Blocked;
ModificationTime = DateTime.Now;
AddDomainEvent(new GroupBlockedDomainEvent(this));
}
public void Update(string? name, GroupAuthorityType? groupAuthority, string? announcement, string? avatar)
{
bool isChanged = false;
if (name != null)
{
Name = name;
isChanged = true;
}
if (groupAuthority != null)
{
Authority = groupAuthority.Value;
isChanged = true;
}
if (announcement != null)
{
Announcement = announcement;
isChanged = true;
}
if (avatar != null)
{
Avatar = avatar;
isChanged = true;
}
if (isChanged)
{
ModificationTime = DateTime.Now;
AddDomainEvent(new GroupUpdateDomainEvent(this));
}
}
public void UpdateLastMsg(long maxSequenceId, string lastMsg, string lastSenderName)
{
MaxSequenceId = maxSequenceId;
LastMessage = lastMsg;
LastSenderName = lastSenderName;
}
}
}
@@ -0,0 +1,62 @@
using GroupService.Domain.Enums;
using GroupService.Domain.Events;
using GroupService.Domain.ValueObjects;
using IM.DomainCommons;
namespace GroupService.Domain.Entities
{
public class GroupInvitation : AggregateRootEntity
{
/// <summary>
/// 群聊编号
/// </summary>
public Guid GroupId { get; private set; }
public GroupProfile GroupProfile { get; private set; }
/// <summary>
/// 被邀请用户
/// </summary>
public Guid UserId { get; private set; }
public UserProfile UserProfile { get; private set; }
/// <summary>
/// 邀请用户
/// </summary>
public Guid OperatorId { get; private set; }
public UserProfile OperatorProfile { get; private set; }
/// <summary>
/// 当前状态(0:待被邀请人同意
/// 1:被邀请人已同意)
/// </summary>
public GroupInvitationState State { get; private set; } = GroupInvitationState.Pending;
private GroupInvitation() { }
public GroupInvitation(Guid groupId, GroupProfile groupProfile, Guid userId, UserProfile userProfile, Guid operatorId, UserProfile operatorProfile)
{
GroupId = groupId;
GroupProfile = groupProfile;
UserId = userId;
UserProfile = userProfile;
OperatorId = operatorId;
OperatorProfile = operatorProfile;
AddDomainEvent(new GroupInvitationCreateDomainEvent(this));
}
public void Accept()
{
if (State != GroupInvitationState.Pending) return;
State = GroupInvitationState.Passed;
ModificationTime = DateTime.Now;
AddDomainEvent(new GroupInvitationAcceptDomainEvent(this));
}
public void Reject()
{
if (State != GroupInvitationState.Pending) return;
State = GroupInvitationState.Reject;
ModificationTime = DateTime.Now;
}
}
}
@@ -0,0 +1,74 @@
using GroupService.Domain.Enums;
using GroupService.Domain.Events;
using GroupService.Domain.ValueObjects;
using IM.DomainCommons;
namespace GroupService.Domain.Entities
{
public class GroupJoinRequest : AggregateRootEntity
{
/// <summary>
/// 群聊编号
///
/// </summary>
public Guid GroupId { get; private set; }
public GroupProfile GroupProfile { get; private set; }
/// <summary>
/// 申请人
/// </summary>
public Guid UserId { get; private set; }
public UserProfile UserProfile { get; private set; }
public Guid? OperatorId { get; private set; }
public string? OperatorName { get; private set; }
public string? OperatorAvatar { get; private set; }
/// <summary>
/// 申请状态(0:待管理员同意,1:已拒绝,2:已同意)
/// </summary>
public GroupJoinRequestState State { get; private set; } = GroupJoinRequestState.Pending;
/// <summary>
/// 入群附言
/// </summary>
public string Description { get; private set; } = "申请入群";
private GroupJoinRequest() { }
public GroupJoinRequest(Guid userId, UserProfile user, Guid groupId, GroupProfile group, string? desc)
{
UserId = userId;
GroupId = groupId;
GroupProfile = group;
UserProfile = user;
if (desc is not null && desc.Length > 20)
{
throw new DomainException("入群描述不可超过20字符");
}
Description = desc ?? Description;
}
public void Approve(Guid id, string nickName, string avatar)
{
if (State != GroupJoinRequestState.Pending) return;
State = GroupJoinRequestState.Passed;
OperatorAvatar = avatar;
OperatorId = id;
OperatorName = nickName;
ModificationTime = DateTime.Now;
AddDomainEvent(new GroupJoinRequestPassedDomainEvent(this));
}
public void Decline(Guid id, string nickName, string avatar)
{
if (State != GroupJoinRequestState.Pending) return;
State = GroupJoinRequestState.Declined;
OperatorAvatar = avatar;
OperatorId = id;
OperatorName = nickName;
ModificationTime = DateTime.Now;
AddDomainEvent(new GroupJoinRequestDeclinedDomainEvent(this));
}
}
}
@@ -0,0 +1,53 @@
using GroupService.Domain.Enums;
using GroupService.Domain.Events;
using IM.DomainCommons;
namespace GroupService.Domain.Entities
{
public class GroupMember : AggregateRootEntity
{
/// <summary>
/// 用户编号
/// </summary>
public Guid UserId { get; private set; }
public string GroupNickName { get; private set; }
public string? Avatar { get; private set; }
/// <summary>
/// 群聊编号
/// </summary>
public Guid GroupId { get; private set; }
/// <summary>
/// 成员角色(0:普通成员,1:管理员,2:群主)
/// </summary>
public GroupMemberRole Role { get; private set; }
private GroupMember() { }
public GroupMember(Guid userId, Guid groupId, string groupNickname, string? avatar, GroupMemberRole role = GroupMemberRole.Normal)
{
UserId = userId;
GroupId = groupId;
Role = role;
GroupNickName = groupNickname;
Avatar = avatar;
AddDomainEvent(new GroupMemberJoinedDomainEvent(this));
}
public void UpdateRole(GroupMemberRole role)
{
if (role == Role) return;
Role = role;
}
public void UpdateAvatar(string avatar)
{
Avatar = avatar;
}
public void UpdateGroupNickname(string nickname)
{
GroupNickName = nickname;
}
}
}
@@ -0,0 +1,18 @@
namespace GroupService.Domain.Enums
{
public enum GroupAuthorityType
{
/// <summary>
/// 需管理员同意
/// </summary>
REQUIRE_CONSENT = 0,
/// <summary>
/// 任何人可加入
/// </summary>
ANYONE_CAN_JOIN = 1,
/// <summary>
/// 不允许加入
/// </summary>
NOT_ALLOWED_TO_JOIN = 2
}
}
@@ -0,0 +1,18 @@
namespace GroupService.Domain.Enums
{
public enum GroupInvitationState
{
/// <summary>
/// 待处理
/// </summary>
Pending = 0,
/// <summary>
/// 已同意
/// </summary>
Passed = 1,
/// <summary>
/// 拒绝
/// </summary>
Reject = 2
}
}
@@ -0,0 +1,18 @@
namespace GroupService.Domain.Enums
{
public enum GroupJoinRequestState
{
/// <summary>
/// 待管理员处理
/// </summary>
Pending = 0,
/// <summary>
/// 已拒绝
/// </summary>
Declined = 1,
/// <summary>
/// 已同意
/// </summary>
Passed = 2
}
}
@@ -0,0 +1,18 @@
namespace GroupService.Domain.Enums
{
public enum GroupMemberRole
{
/// <summary>
/// 普通成员
/// </summary>
Normal = 0,
/// <summary>
/// 管理员
/// </summary>
Administrator = 1,
/// <summary>
/// 群主
/// </summary>
Master = 2
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace GroupService.Domain.Enums
{
public enum GroupState
{
/// <summary>
/// 正常
/// </summary>
Normal = 1,
/// <summary>
/// 封禁
/// </summary>
Blocked = 2
}
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record AllMembersBannedDomainEvent(Group Group) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupBlockedDomainEvent(Group Group) : INotification;
}
@@ -0,0 +1,6 @@
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupCreateDomainEvent(Domain.Entities.Group Group) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupInvitationAcceptDomainEvent(GroupInvitation Invitation) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupInvitationCreateDomainEvent(GroupInvitation Invitation) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupJoinRequestDeclinedDomainEvent(GroupJoinRequest Request) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupJoinRequestPassedDomainEvent(GroupJoinRequest Request) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupMemberJoinedDomainEvent(GroupMember Member) : INotification;
}
@@ -0,0 +1,7 @@
using GroupService.Domain.Entities;
using MediatR;
namespace GroupService.Domain.Events
{
public record GroupUpdateDomainEvent(Group Group) : INotification;
}
@@ -0,0 +1,30 @@
using GroupService.Domain.Entities;
using GroupService.Domain.Enums;
using GroupService.Domain.IReposities;
using IM.Commons;
namespace GroupService.Domain
{
public class GroupMemberDomainService
{
private readonly IGroupMemberReposity reposity;
public GroupMemberDomainService(IGroupMemberReposity reposity)
{
this.reposity = reposity;
}
public async Task<Result<GroupMember>> CreateAsync(
Guid userId, Guid groupId, string nickName, string? avatar, GroupMemberRole role = GroupMemberRole.Normal)
{
var exist = await reposity.CheckMemberExistAsync(groupId, userId);
if (exist)
{
return Result<GroupMember>.Fail(ResultCode.ALREADY_IN_GROUP);
}
var groupMember = new GroupMember(userId, groupId, nickName, avatar, role);
groupMember = reposity.Create(groupMember);
return Result<GroupMember>.Success(groupMember);
}
}
}

Some files were not shown because too many files have changed in this diff Show More