1
0
mirror of synced 2025-02-17 11:18:32 +01:00

Stage changes

This commit is contained in:
asesidaa 2024-11-20 01:15:21 +08:00
parent 6f41aacef3
commit bc2e30757e
34 changed files with 364 additions and 50 deletions

View File

@ -1,6 +1,8 @@
// Global using directives
global using Application.Interfaces;
global using Application.Models.Api;
global using Application.Models.Game;
global using Application.Utils;
global using Domain.Common;
global using Domain.Entities;

View File

@ -0,0 +1,39 @@
namespace Application.Handlers.Api.Auth;
public record ChangePasswordCommand(string AccessCode, string OldPassword, string NewPassword) : IRequest<ApiResult<bool>>;
public class ChangePasswordCommandHandler(ITaikoDbContext context, ILogger<ChangePasswordCommandHandler> logger)
: IRequestHandler<ChangePasswordCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
{
var card = await context.Cards.Include(card => card.Ba)
.ThenInclude(user => user!.Credential)
.FirstOrDefaultAsync(card => card.AccessCode == request.AccessCode, cancellationToken);
if (card is null)
{
return ApiResult.Failed<bool>("Invalid access code");
}
var credential = card.Ba?.Credential;
if (credential is null || credential.Password == string.Empty)
{
return ApiResult.Failed<bool>("User not registered");
}
if (!BCrypt.Net.BCrypt.Verify(request.OldPassword, credential.Password))
{
return ApiResult.Failed<bool>("Wrong old password");
}
var salt = BCrypt.Net.BCrypt.GenerateSalt();
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(request.NewPassword, salt);
credential.Password = hashedPassword;
credential.Salt = salt;
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -0,0 +1,14 @@
namespace Application.Handlers.Api.Auth;
public record GenerateOtpCommand(uint Baid) : IRequest<ApiResult<string>>;
public class GenerateOtpCommandHandler(ILogger<GenerateOtpCommandHandler> logger)
: IRequestHandler<GenerateOtpCommand, ApiResult<string>>
{
public Task<ApiResult<string>> Handle(GenerateOtpCommand request, CancellationToken cancellationToken)
{
var totp = TotpUtils.MakeTotp(request.Baid);
var otp = totp.ComputeTotp();
return Task.FromResult(ApiResult.Succeed(otp));
}
}

View File

@ -1,7 +1,5 @@
using Application.Models.Api;
namespace Application.Handlers.Api.Auth;
namespace Application.Handlers.Api;
using BCrypt.Net;
public record LoginCommand(string AccessCode, string Password): IRequest<ApiResult<string>>;
public class LoginCommandHandler(ITaikoDbContext context, IJwtTokenService jwtTokenService,
@ -25,7 +23,7 @@ public class LoginCommandHandler(ITaikoDbContext context, IJwtTokenService jwtTo
return ApiResult.Failed<string>("User not registered");
}
if (!BCrypt.Verify(request.Password, credential.Password))
if (!BCrypt.Net.BCrypt.Verify(request.Password, credential.Password))
{
return ApiResult.Failed<string>("Invalid password");
}

View File

@ -1,6 +1,4 @@
using Application.Models.Api;
namespace Application.Handlers.Api;
namespace Application.Handlers.Api.Auth;
public record RegisterCommand : IRequest<ApiResult<bool>>
{

View File

@ -0,0 +1,23 @@
namespace Application.Handlers.Api.Auth;
public record ResetPasswordCommand(uint Baid) : IRequest<ApiResult<bool>>;
// ResetPasswordCommandHandler.cs
public class ResetPasswordCommandHandler(ITaikoDbContext context, ILogger<ResetPasswordCommandHandler> logger)
: IRequestHandler<ResetPasswordCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(ResetPasswordCommand request, CancellationToken cancellationToken)
{
var credential = await context.Credentials.FirstOrDefaultAsync(c => c.Baid == request.Baid, cancellationToken);
if (credential is null)
{
return ApiResult.Failed<bool>("Credential not found");
}
credential.Password = string.Empty;
credential.Salt = string.Empty;
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -0,0 +1,26 @@
namespace Application.Handlers.Api.Cards;
public record BindAccessCodeCommand(string AccessCode, uint Baid) : IRequest<ApiResult<bool>>;
public class BindAccessCodeCommandHandler(ITaikoDbContext context, ILogger<BindAccessCodeCommandHandler> logger)
: IRequestHandler<BindAccessCodeCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(BindAccessCodeCommand request, CancellationToken cancellationToken)
{
var existingCard = await context.Cards.FirstOrDefaultAsync(c => c.AccessCode == request.AccessCode, cancellationToken);
if (existingCard != null)
{
return ApiResult.Failed<bool>("Access code already exists");
}
var newCard = new Card
{
Baid = request.Baid,
AccessCode = request.AccessCode
};
await context.Cards.AddAsync(newCard, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -0,0 +1,25 @@
namespace Application.Handlers.Api.Cards;
public record DeleteCardCommand(string AccessCode, uint Baid, bool IsAdmin) : IRequest<ApiResult<bool>>;
public class DeleteCardCommandHandler(ITaikoDbContext context, ILogger<DeleteCardCommandHandler> logger)
: IRequestHandler<DeleteCardCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(DeleteCardCommand request, CancellationToken cancellationToken)
{
var card = await context.Cards.FirstOrDefaultAsync(c => c.AccessCode == request.AccessCode, cancellationToken);
if (card == null)
{
return ApiResult.Failed<bool>("Card not found");
}
if (card.Baid != request.Baid && !request.IsAdmin)
{
return ApiResult.Failed<bool>("Unauthorized to delete card");
}
context.Cards.Remove(card);
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -0,0 +1,18 @@
using Application.Mappers;
namespace Application.Handlers.Api.Data;
public record GetDanBestDataQuery(uint Baid): IRequest<ApiResult<List<DanBestData>>>;
public class GetDanBestDataQueryHandler(ITaikoDbContext context, ILogger<GetDanBestDataQueryHandler> logger)
: IRequestHandler<GetDanBestDataQuery, ApiResult<List<DanBestData>>>
{
public async Task<ApiResult<List<DanBestData>>> Handle(GetDanBestDataQuery request, CancellationToken cancellationToken)
{
var danBestData = await context.DanScoreData.Where(datum => datum.Baid == request.Baid && datum.DanType == DanType.Normal)
.Include(datum => datum.DanStageScoreData)
.Select(d => DanDataMapper.MapToDanBestData(d))
.ToListAsync(cancellationToken);
return ApiResult.Succeed(danBestData);
}
}

View File

@ -0,0 +1,19 @@
namespace Application.Handlers.Api.User;
public record GetFavoriteSongsQuery(uint Baid): IRequest<ApiResult<List<uint>>>;
public class GetFavoriteSongsQueryHandler(ITaikoDbContext context, ILogger<GetFavoriteSongsQueryHandler> logger)
: IRequestHandler<GetFavoriteSongsQuery, ApiResult<List<uint>>>
{
public async Task<ApiResult<List<uint>>> Handle(GetFavoriteSongsQuery request, CancellationToken cancellationToken)
{
var user = await context.UserData.FindAsync([request.Baid], cancellationToken);
if (user is null)
{
return ApiResult.Failed<List<uint>>("User not found");
}
var favoriteSongs = user.FavoriteSongsArray;
return ApiResult.Succeed(favoriteSongs);
}
}

View File

@ -0,0 +1,58 @@
// File: Application/Handlers/Api/User/GetSongBestRecordsQuery.cs
using Application.Mappers;
namespace Application.Handlers.Api.User;
public record GetSongBestRecordsQuery(uint Baid) : IRequest<ApiResult<List<SongBestData>>>;
public class GetSongBestRecordsQueryHandler(ITaikoDbContext context, ILogger<GetSongBestRecordsQueryHandler> logger)
: IRequestHandler<GetSongBestRecordsQuery, ApiResult<List<SongBestData>>>
{
public async Task<ApiResult<List<SongBestData>>> Handle(GetSongBestRecordsQuery request,
CancellationToken cancellationToken)
{
var user = await context.UserData
.FirstOrDefaultAsync(d => d.Baid == request.Baid, cancellationToken);
if (user is null)
{
return ApiResult.Failed<List<SongBestData>>("User not found");
}
var songBestRecords = await context.SongBestData
.Where(datum => datum.Baid == request.Baid)
.Select(datum => SongBestDataMapper.ToSongBestData(datum))
.ToListAsync(cancellationToken);
var songPlayData = await context.SongPlayData
.Where(datum => datum.Baid == request.Baid)
.ToListAsync(cancellationToken);
foreach (var songBestData in songBestRecords)
{
var songPlayLogs = songPlayData
.Where(datum => datum.SongId == songBestData.SongId && datum.Difficulty == songBestData.Difficulty)
.ToList();
songBestData.PlayCount = songPlayLogs.Count;
songBestData.ClearCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Clear);
songBestData.FullComboCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Gold);
songBestData.PerfectCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Dondaful);
}
var favoriteSongs = user.FavoriteSongsArray.ToHashSet();
foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSongs.Contains(songBestRecord.SongId)))
{
songBestRecord.IsFavorite = true;
}
foreach (var songBestRecord in songBestRecords)
{
songBestRecord.RecentPlayData = songPlayData
.Where(datum => datum.SongId == songBestRecord.SongId && datum.Difficulty == songBestRecord.Difficulty)
.Select(SongPlayDataMapper.MapToDto)
.ToList();
}
return ApiResult.Succeed(songBestRecords);
}
}

View File

@ -0,0 +1,33 @@
using Application.Mappers;
namespace Application.Handlers.Api.User;
public record GetSongHistoryQuery(uint Baid) : IRequest<ApiResult<List<SongHistoryData>>>;
public class GetSongHistoryQueryHandler(ITaikoDbContext context, ILogger<GetSongHistoryQueryHandler> logger)
: IRequestHandler<GetSongHistoryQuery, ApiResult<List<SongHistoryData>>>
{
public async Task<ApiResult<List<SongHistoryData>>> Handle(GetSongHistoryQuery request, CancellationToken cancellationToken)
{
var user = await context.UserData
.FirstOrDefaultAsync(d => d.Baid == request.Baid, cancellationToken);
if (user is null)
{
return ApiResult.Failed<List<SongHistoryData>>("User not found");
}
var playLogs = await context.SongPlayData
.Where(datum => datum.Baid == request.Baid)
.ToListAsync(cancellationToken);
var songHistory = playLogs.Select(SongHistoryDataMapper.ToSongHistoryData).ToList();
var favoriteSongs = user.FavoriteSongsArray.ToHashSet();
foreach (var song in songHistory.Where(song => favoriteSongs.Contains(song.SongId)))
{
song.IsFavorite = true;
}
return ApiResult.Succeed(songHistory);
}
}

View File

@ -0,0 +1,33 @@
namespace Application.Handlers.Api.User;
public record UpdateFavoriteSongCommand(uint Baid, uint SongId, bool IsFavorite): IRequest<ApiResult<bool>>;
public class UpdateFavoriteSongCommandHandler(ITaikoDbContext context, ILogger<UpdateFavoriteSongCommandHandler> logger)
: IRequestHandler<UpdateFavoriteSongCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(UpdateFavoriteSongCommand request, CancellationToken cancellationToken)
{
var userDatum = await context.UserData.FindAsync([request.Baid], cancellationToken);
if (userDatum is null)
{
return ApiResult.Failed<bool>("User not found");
}
var favoriteSet = new HashSet<uint>(userDatum.FavoriteSongsArray);
if (request.IsFavorite)
{
favoriteSet.Add(request.SongId);
}
else
{
favoriteSet.Remove(request.SongId);
}
userDatum.FavoriteSongsArray = favoriteSet.ToList();
context.Update(userDatum);
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record AddMyDonEntryCommand(string AccessCode, string Name, uint Language) : IRequest<CommonMyDonEntryResponse>;
#pragma warning disable CS9113 // Parameter is unread.

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record AddTokenCountCommand(CommonAddTokenCountRequest Request) : IRequest;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record BaidQuery(string AccessCode) : IRequest<CommonBaidResponse>;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetAiDataQuery(uint Baid) : IRequest<CommonAiDataResponse>;

View File

@ -1,5 +1,4 @@
using Application.Mappers;
using Application.Models.Game;
namespace Application.Handlers.Game;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetDanScoreQuery(uint Baid, uint Type, uint[] DanIds) : IRequest<CommonDanScoreDataResponse>;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetFolderQuery(uint[] FolderIds) : IRequest<CommonGetFolderResponse>;

View File

@ -1,5 +1,4 @@
using System.Collections.Immutable;
using Application.Models.Game;
using Domain.Settings;
using Microsoft.Extensions.Options;

View File

@ -1,5 +1,4 @@
using Application.Models.Game;
using Swan.Formatters;
using Swan.Formatters;
namespace Application.Handlers.Game;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetShopFolderQuery : IRequest<CommonGetShopFolderResponse>;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetSongIntroductionQuery(uint[] SetIds) : IRequest<CommonGetSongIntroductionResponse>;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record GetTokenCountQuery(uint Baid) : IRequest<CommonGetTokenCountResponse>;

View File

@ -1,6 +1,4 @@
using Application.Models.Game;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record PurchaseSongCommand(uint Baid, uint SongNo, uint Type, uint TokenId, uint Price) : IRequest<CommonSongPurchaseResponse>;

View File

@ -1,7 +1,4 @@
using Application.Models.Game;
using SharedProject.Utils;
namespace Application.Handlers.Game;
namespace Application.Handlers.Game;
public record UpdatePlayResultCommand(uint Baid, CommonPlayResultData PlayResultData) : IRequest<uint>;

View File

@ -1,5 +1,4 @@
using System.Buffers.Binary;
using Application.Models.Game;
using Domain.Settings;
using Microsoft.Extensions.Options;

View File

@ -1,15 +1,14 @@
using Application.Models.Game;
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
public static partial class AiScoreMapper
{
#pragma warning disable RMG020
[MapProperty(nameof(AiScoreDatum.AiSectionScoreData), nameof(CommonAiScoreResponse.AryBestSectionDatas))]
public static partial CommonAiScoreResponse MapToCommonAiScoreResponse(AiScoreDatum datum);
#pragma warning restore RMG020
public static CommonAiScoreResponse MapAsSuccess(AiScoreDatum datum)
{

View File

@ -0,0 +1,20 @@
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper]
public static partial class DanDataMapper
{
[MapProperty(nameof(DanScoreDatum.DanStageScoreData), nameof(DanBestData.DanBestStageDataList))]
[MapperIgnoreSource(nameof(DanScoreDatum.Baid))]
[MapperIgnoreSource(nameof(DanScoreDatum.DanType))]
[MapperIgnoreSource(nameof(DanScoreDatum.ArrivalSongCount))]
[MapperIgnoreSource(nameof(DanScoreDatum.Ba))]
public static partial DanBestData MapToDanBestData(DanScoreDatum datum);
[MapperIgnoreSource(nameof(datum.Baid))]
[MapperIgnoreSource(nameof(datum.DanId))]
[MapperIgnoreSource(nameof(datum.DanType))]
[MapperIgnoreSource(nameof(datum.Parent))]
public static partial DanBestStageData MapToDanBestStageData(DanStageScoreDatum datum);
}

View File

@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
[SuppressMessage("Mapper", "RMG012:Source member was not found for target member")]
public static partial class SongBestDataMapper
{
public static partial SongBestData ToSongBestData(SongBestDatum datum);
}

View File

@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
[SuppressMessage("Mapper", "RMG012:Source member was not found for target member")]
public static partial class SongHistoryDataMapper
{
public static partial SongHistoryData ToSongHistoryData(SongPlayDatum datum);
}

View File

@ -0,0 +1,12 @@
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper]
public static partial class SongPlayDataMapper
{
[MapperIgnoreSource(nameof(entity.Id))]
[MapperIgnoreSource(nameof(entity.Baid))]
[MapperIgnoreSource(nameof(entity.Ba))]
public static partial SongPlayDatumDto MapToDto(SongPlayDatum entity);
}

View File

@ -1,4 +1,4 @@
namespace SharedProject.Utils;
namespace Application.Utils;
public static class ValueHelpers
{