1
0
mirror of synced 2024-11-23 22:41:01 +01:00

Stage changes

This commit is contained in:
asesidaa 2024-11-20 20:05:53 +08:00
parent bc2e30757e
commit 5668aad9bc
47 changed files with 477 additions and 104 deletions

View File

@ -10,6 +10,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="Riok.Mapperly" Version="4.1.1-next.0" />
<PackageReference Include="Swan.Core" Version="7.0.0-beta.2" />

View File

@ -0,0 +1,16 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(
configuration => configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
return services;
}
}

View File

@ -8,6 +8,8 @@ global using Domain.Common;
global using Domain.Entities;
global using Domain.Enums;
global using Domain.Models;
global using Domain.Models.Base;
global using Domain.Models.GameData;
global using MediatR;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Logging;

View File

@ -0,0 +1,19 @@
namespace Application.Handlers.Api.User;
public record DeleteUserCommand(uint Baid) : IRequest<ApiResult<bool>>;
public class DeleteUserCommandHandler(ITaikoDbContext context) : IRequestHandler<DeleteUserCommand, ApiResult<bool>>
{
public async Task<ApiResult<bool>> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
{
var userDatum = await context.UserData.FindAsync([request.Baid], cancellationToken);
if (userDatum == null)
{
return ApiResult.Failed<bool>("User not found.");
}
context.UserData.Remove(userDatum);
await context.SaveChangesAsync(cancellationToken);
return ApiResult.Succeed(true);
}
}

View File

@ -0,0 +1,128 @@
namespace Application.Handlers.Api.User;
using LeaderBoard = PaginatedResult<SongLeaderboardEntry>;
public record GetSongLeaderboardQuery(uint SongId, Difficulty Difficulty, int Baid, int Page, int Limit) : IRequest<ApiResult<LeaderBoard>>;
public class GetSongLeaderboardQueryHandler(ITaikoDbContext context, ILogger<GetSongLeaderboardQueryHandler> logger)
: IRequestHandler<GetSongLeaderboardQuery, ApiResult<LeaderBoard>>
{
public async Task<ApiResult<LeaderBoard>> Handle(GetSongLeaderboardQuery request, CancellationToken cancellationToken)
{
var totalScores = await context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty)
.CountAsync(cancellationToken);
var totalPages = (totalScores + request.Limit - 1) / request.Limit;
var scores = await context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty)
.Select(x => new
{
x.Baid,
x.BestScore,
x.BestRate,
x.BestCrown,
x.BestScoreRank,
Rank = context.SongBestData.Count(y => y.SongId == request.SongId && y.Difficulty == request.Difficulty && y.BestScore > x.BestScore) + 1
})
.OrderByDescending(x => x.BestScore)
.ThenByDescending(x => x.BestRate)
.ThenByDescending(x => x.BestCrown)
.Skip((request.Page - 1) * request.Limit)
.Take(request.Limit)
.ToListAsync(cancellationToken);
var userIds = scores.Select(x => x.Baid).Distinct().ToList();
var users = await context.UserData
.Where(x => userIds.Contains(x.Baid))
.ToDictionaryAsync(x => x.Baid, cancellationToken);
var leaderboard = scores.Select(score =>
{
var user = users.GetValueOrDefault(score.Baid);
return new SongLeaderboardEntry
{
Rank = score.Rank,
Baid = score.Baid,
UserName = user?.MyDonName,
BestScore = score.BestScore,
BestRate = score.BestRate,
BestCrown = score.BestCrown,
BestScoreRank = score.BestScoreRank
};
}).ToList();
var userLeaderboardEntry = context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty && x.Baid == request.Baid)
.Select(x => new SongLeaderboardEntry
{
Baid = x.Baid,
BestScore = x.BestScore,
BestRate = x.BestRate,
BestCrown = x.BestCrown,
BestScoreRank = x.BestScoreRank,
Rank = context.SongBestData.Count(y => y.SongId == request.SongId && y.Difficulty == request.Difficulty && y.BestScore > x.BestScore) + 1
})
.FirstOrDefault();
/*foreach (var score in scores)
{
var user = await context.UserData
.Where(x => x.Baid == score.Baid)
.FirstOrDefaultAsync(cancellationToken);
var rank = await context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty && x.BestScore > score.BestScore)
.CountAsync(cancellationToken);
leaderboard.Add(new SongLeaderboardEntry
{
Rank = rank + 1,
Baid = score.Baid,
UserName = user?.MyDonName,
BestScore = score.BestScore,
BestRate = score.BestRate,
BestCrown = score.BestCrown,
BestScoreRank = score.BestScoreRank
});
}*/
/*SongLeaderboardEntry? userBestScore = null;
if (request.Baid != 0)
{
var score = await context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty && x.Baid == request.Baid)
.FirstOrDefaultAsync(cancellationToken);
if (score != null)
{
var user = await context.UserData
.Where(x => x.Baid == request.Baid)
.FirstOrDefaultAsync(cancellationToken);
var rank = await context.SongBestData
.Where(x => x.SongId == request.SongId && x.Difficulty == request.Difficulty && x.BestScore > score.BestScore)
.CountAsync(cancellationToken);
userBestScore = new SongLeaderboardEntry
{
Rank = rank + 1,
Baid = score.Baid,
UserName = user?.MyDonName,
BestScore = score.BestScore,
BestRate = score.BestRate,
BestCrown = score.BestCrown,
BestScoreRank = score.BestScoreRank
};
}
}*/
return ApiResult.Succeed(new LeaderBoard
{
Data = leaderboard,
Current = userLeaderboardEntry,
CurrentPage = request.Page,
TotalPages = totalPages,
TotalCount = totalScores
});
}
}

View File

@ -0,0 +1,25 @@
namespace Application.Handlers.Api.User;
public record GetUserQuery(uint Baid) : IRequest<ApiResult<Domain.Models.User>>;
public class GetUserQueryHandler(ITaikoDbContext context) : IRequestHandler<GetUserQuery, ApiResult<Domain.Models.User>>
{
public async Task<ApiResult<Domain.Models.User>> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
var userDatum = await context.UserData.Include(datum => datum.Cards)
.Where(datum => datum.Baid == request.Baid)
.FirstOrDefaultAsync(cancellationToken);
if (userDatum == null)
{
return ApiResult.Failed<Domain.Models.User>("User not found.");
}
return ApiResult.Succeed(new Domain.Models.User
{
Baid = userDatum.Baid,
AccessCodes = userDatum.Cards.Select(card => card.AccessCode).ToList(),
IsAdmin = userDatum.IsAdmin
});
}
}

View File

@ -0,0 +1,58 @@
using Application.Mappers;
namespace Application.Handlers.Api.User;
using Users = PaginatedResult<Domain.Models.User>;
public record GetUsersQuery(int Page, int Limit, string? SearchTerm) : IRequest<ApiResult<Users>>;
public class GetUsersQueryHandler(ITaikoDbContext context) : IRequestHandler<GetUsersQuery, ApiResult<Users>>
{
public async Task<ApiResult<Users>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
{
var users = new List<Domain.Models.User>();
var cardEntries = await context.Cards.ToListAsync(cancellationToken);
var userEntriesQuery = context.UserData.AsQueryable();
if (!string.IsNullOrEmpty(request.SearchTerm))
{
var lowerCaseSearchTerm = request.SearchTerm.ToLower();
userEntriesQuery = userEntriesQuery.Where(user =>
user.Baid.ToString() == lowerCaseSearchTerm ||
user.MyDonName.Contains(lowerCaseSearchTerm, StringComparison.CurrentCultureIgnoreCase) ||
context.Cards.Any(card => card.Baid == user.Baid &&
card.AccessCode.Contains(lowerCaseSearchTerm, StringComparison.CurrentCultureIgnoreCase)));
}
var totalUsers = await userEntriesQuery.CountAsync(cancellationToken);
var totalPages = (totalUsers + request.Limit - 1) / request.Limit;
var userEntries = await userEntriesQuery
.OrderBy(user => user.Baid)
.Skip((request.Page - 1) * request.Limit)
.Take(request.Limit)
.ToListAsync(cancellationToken);
foreach (var user in userEntries)
{
var userSetting = UserSettingMapper.MapToUserSetting(user);
users.Add(new Domain.Models.User
{
Baid = user.Baid,
AccessCodes = cardEntries.Where(card => card.Baid == user.Baid).Select(card => card.AccessCode).ToList(),
IsAdmin = user.IsAdmin,
UserSetting = userSetting
});
}
return ApiResult.Succeed( new Users
{
Data = users,
CurrentPage = request.Page,
TotalPages = totalPages,
TotalCount = totalUsers
});
}
}

View File

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

View File

@ -1,4 +1,5 @@
using System.Collections.Immutable;
using Domain.Models.GameData;
namespace Application.Interfaces;

View File

@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
namespace Application.Mappers;
[Mapper(AutoUserMappings = false)]
public static partial class UserSettingMapper
{
[SuppressMessage("Mapper", "RMG020:Source member is not mapped to any target member")]
[MapProperty(nameof(UserDatum.TitleFlgArray), nameof(UserSetting.UnlockedTitle))]
[MapProperty(nameof(UserDatum.OptionSetting), nameof(UserSetting.PlaySetting), Use = nameof(ShortToPlaySetting))]
[MapProperty(nameof(UserDatum.UnlockedKigurumi), nameof(UserSetting.UnlockedKigurumi), Use = nameof(FixUnlock))]
[MapProperty(nameof(UserDatum.UnlockedBody), nameof(UserSetting.UnlockedBody), Use = nameof(FixUnlock))]
[MapProperty(nameof(UserDatum.UnlockedFace), nameof(UserSetting.UnlockedFace), Use = nameof(FixUnlock))]
[MapProperty(nameof(UserDatum.UnlockedHead), nameof(UserSetting.UnlockedHead), Use = nameof(FixUnlock))]
[MapProperty(nameof(UserDatum.UnlockedPuchi), nameof(UserSetting.UnlockedPuchi), Use = nameof(FixUnlock))]
public static partial UserSetting MapToUserSetting(UserDatum user);
public static PlaySetting ShortToPlaySetting(short option)
{
return PlaySettingConverter.ShortToPlaySetting(option);
}
public static List<uint> FixUnlock(List<uint> unlock)
{
if (!unlock.Contains(0))
{
unlock.Add(0);
}
return unlock;
}
}

View File

@ -1,4 +1,6 @@
namespace Application.Models.Game;
using Domain.Models.GameData;
namespace Application.Models.Game;
public class CommonGetFolderResponse
{

View File

@ -1,4 +1,6 @@
namespace Application.Models.Game;
using Domain.Models.GameData;
namespace Application.Models.Game;
public class CommonGetShopFolderResponse
{

View File

@ -1,4 +1,6 @@
namespace Application.Models.Game;
using Domain.Models.GameData;
namespace Application.Models.Game;
public class CommonGetSongIntroductionResponse
{

View File

@ -0,0 +1,43 @@
using System.Collections.Specialized;
namespace Application.Utils;
public static class PlaySettingConverter
{
public static PlaySetting ShortToPlaySetting(short input)
{
var bits = new BitVector32(input);
var speedSection = BitVector32.CreateSection(15);
var vanishSection = BitVector32.CreateSection(1, speedSection);
var inverseSection = BitVector32.CreateSection(1, vanishSection);
var randomSection = BitVector32.CreateSection(2, inverseSection);
var randomType = (RandomType)bits[randomSection];
randomType.Throw().IfOutOfRange();
var result = new PlaySetting
{
Speed = (uint)bits[speedSection],
IsVanishOn = bits[vanishSection] == 1,
IsInverseOn = bits[inverseSection] == 1,
RandomType = randomType
};
return result;
}
public static short PlaySettingToShort(PlaySetting setting)
{
var bits = new BitVector32();
var speedSection = BitVector32.CreateSection(15);
var vanishSection = BitVector32.CreateSection(1, speedSection);
var inverseSection = BitVector32.CreateSection(1, vanishSection);
var randomSection = BitVector32.CreateSection(2, inverseSection);
bits[speedSection] = (int)setting.Speed;
bits[vanishSection] = setting.IsVanishOn ? 1 : 0;
bits[inverseSection] = setting.IsInverseOn ? 1 : 0;
bits[randomSection] = (int)setting.RandomType;
return (short)bits.Data;
}
}

View File

@ -0,0 +1,11 @@
namespace Domain.Models.Base;
public class PaginatedResult<T>
{
public List<T> Data { get; set; } = [];
public T? Current;
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int TotalCount { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Domain.Models.GameData;
namespace Domain.Models;

View File

@ -1,4 +1,4 @@
namespace Domain.Models;
namespace Domain.Models.GameData;
public class Costume
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class DonCosRewardEntry
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class DonCosRewards
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class EventFolderData : IVerupNo
{

View File

@ -1,4 +1,4 @@
namespace Domain.Models;
namespace Domain.Models.GameData;
public interface IVerupNo
{

View File

@ -1,6 +1,6 @@
using Domain.Enums;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class MusicDetail
{

View File

@ -1,7 +1,7 @@
using System.Text.Json.Serialization;
using Domain.Enums;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class MusicInfoEntry
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class MusicInfos
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class MusicOrder
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class MusicOrderEntry
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class NeiroEntry
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class Neiros
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class QRCodeData
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class ShopFolderData
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class ShougouEntry
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class Shougous
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class SongIntroductionData : IVerupNo
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class WordList
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Domain.Models;
namespace Domain.Models.GameData;
public class WordListEntry
{

View File

@ -1,27 +1,10 @@
using Domain.Enums;
namespace Domain.Models;
namespace Domain.Models;
public class SongLeaderboard
{
public int Rank { get; set; }
public uint Baid { get; set; }
public uint BestScore { get; set; }
public uint BestRate { get; set; }
public CrownType BestCrown { get; set; }
public ScoreRank BestScoreRank { get; set; }
public uint GoodCount { get; set; }
public uint OkCount { get; set; }
public uint MissCount { get; set; }
public uint ComboCount { get; set; }
public uint HitCount { get; set; }
public uint DrumrollCount { get; set; }
public string? UserName { get; set; }
public List<SongLeaderboardEntry> LeaderboardData { get; set; } = [];
public SongLeaderboardEntry? UserScore { get; set; }
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public int TotalScores { get; set; }
}

View File

@ -0,0 +1,27 @@
using Domain.Enums;
namespace Domain.Models;
public class SongLeaderboardEntry
{
public int Rank { get; set; }
public uint Baid { get; set; }
public uint BestScore { get; set; }
public uint BestRate { get; set; }
public CrownType BestCrown { get; set; }
public ScoreRank BestScoreRank { get; set; }
public uint GoodCount { get; set; }
public uint OkCount { get; set; }
public uint MissCount { get; set; }
public uint ComboCount { get; set; }
public uint HitCount { get; set; }
public uint DrumrollCount { get; set; }
public string? UserName { get; set; }
}

View File

@ -64,5 +64,5 @@ public class UserSetting
public uint ColorLimb { get; set; }
public DateTime LastPlayDateTime { get; set; }
public DateTime LastPlayDatetime { get; set; }
}

View File

@ -0,0 +1,34 @@
using Application.Interfaces;
using Domain.Common;
using Infrastructure.Persistence;
using Infrastructure.Services;
using Infrastructure.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IJwtTokenService, JwtTokenService>();
services.AddSingleton<IGameDataService, GameDataService>();
services.AddDbContext<TaikoDbContext>(option =>
{
var dbName = configuration["DbFileName"];
if (string.IsNullOrEmpty(dbName))
{
dbName = Constants.DefaultDbName;
}
var path = Path.Combine(PathHelper.GetRootPath(), dbName);
option.UseSqlite($"Data Source={path}");
});
services.AddScoped<ITaikoDbContext, TaikoDbContext>(provider =>
provider.GetService<TaikoDbContext>() ?? throw new InvalidOperationException());
return services;
}
}

View File

@ -15,6 +15,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
<PackageReference Include="Throw" Version="1.4.0" />
</ItemGroup>

View File

@ -5,6 +5,7 @@ using System.Text.Json;
using Application.Interfaces;
using Domain.Common;
using Domain.Models;
using Domain.Models.GameData;
using Domain.Settings;
using Infrastructure.Utils;
using Microsoft.Extensions.Options;

View File

@ -0,0 +1,35 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Application.Interfaces;
using Domain.Settings;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Infrastructure.Services;
public class JwtTokenService(IOptions<AuthSettings> options) : IJwtTokenService
{
public string GenerateToken(uint baid, bool isAdmin)
{
var authSettings = options.Value;
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.JwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new(ClaimTypes.Name, baid.ToString()),
new(ClaimTypes.Role, isAdmin ? "Admin" : "User")
};
var token = new JwtSecurityToken(
issuer: authSettings.JwtIssuer,
audience: authSettings.JwtAudience,
expires: DateTime.UtcNow.AddHours(24),
signingCredentials: credentials,
claims: claims
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

View File

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Server.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

View File

@ -10,4 +10,8 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

View File

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

View File

@ -1,12 +0,0 @@
namespace Server;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

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