forked from xiongyuxing/tiku-backend.net
feat: add scoreline dynamic records
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Identity;
|
||||
@@ -174,6 +175,56 @@ public sealed class DirectContentEndpointTests
|
||||
Assert.True(dbContext.Questions.Single(item => item.Id == questionId).HasVideoExplanation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tenant_admin_can_upsert_scoreline_fields_and_records()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var seed = await SeedAdminAsync(factory);
|
||||
var regionId = Guid.NewGuid();
|
||||
var schoolId = Guid.NewGuid();
|
||||
var majorId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new Region { Id = regionId, TenantId = seed.TenantId, Name = "四川" },
|
||||
new School { Id = schoolId, TenantId = seed.TenantId, RegionId = regionId, Name = "美术学院" },
|
||||
new Major { Id = majorId, TenantId = seed.TenantId, RegionId = regionId, SchoolId = schoolId, Name = "视觉传达" });
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client, seed);
|
||||
|
||||
var fieldResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-content/scoreline/fields",
|
||||
new DirectScorelineFieldDto
|
||||
{
|
||||
RegionId = regionId,
|
||||
FieldKey = "cultureScore",
|
||||
FieldName = "文化分",
|
||||
FieldType = "number",
|
||||
IsFilter = true,
|
||||
IsTrend = true
|
||||
});
|
||||
var recordResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-content/scoreline/records",
|
||||
new DirectScorelineRecordDto
|
||||
{
|
||||
RegionId = regionId,
|
||||
SchoolId = schoolId,
|
||||
MajorId = majorId,
|
||||
Year = 2026,
|
||||
SchoolName = "美术学院",
|
||||
MajorName = "视觉传达",
|
||||
FieldValues = JsonSerializer.SerializeToElement(new { cultureScore = 420, rank = "A" })
|
||||
});
|
||||
|
||||
var fieldsResponse = await client.GetAsync($"/api/tenant-content/scoreline/fields?regionId={regionId}");
|
||||
var recordsResponse = await client.GetAsync($"/api/tenant-content/scoreline/records?regionId={regionId}&year=2026");
|
||||
var fieldsJson = await ReadJsonAsync(fieldsResponse);
|
||||
var recordsJson = await ReadJsonAsync(recordsResponse);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, fieldResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, recordResponse.StatusCode);
|
||||
Assert.Single(fieldsJson.RootElement.GetProperty("items").EnumerateArray());
|
||||
Assert.Single(recordsJson.RootElement.GetProperty("items").EnumerateArray());
|
||||
}
|
||||
|
||||
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedAdminAsync(ApiTestFactory factory)
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
|
||||
97
Tiku.IntegrationTests/Api/ScorelineEndpointTests.cs
Normal file
97
Tiku.IntegrationTests/Api/ScorelineEndpointTests.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class ScorelineEndpointTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Public_scoreline_records_support_dynamic_filters()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var tenantId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
var schoolId = Guid.NewGuid();
|
||||
var majorId = Guid.NewGuid();
|
||||
var tenantCode = tenantId.ToString("N");
|
||||
await factory.SeedAsync(
|
||||
new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
Slug = tenantCode,
|
||||
Name = "Test Tenant",
|
||||
Status = TenantStatus.Active,
|
||||
Metadata = JsonDefaults.Object()
|
||||
},
|
||||
new Region { Id = regionId, TenantId = tenantId, Name = "四川" },
|
||||
new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "美术学院" },
|
||||
new Major { Id = majorId, TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, Name = "视觉传达" },
|
||||
new ScorelineField
|
||||
{
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
FieldKey = "cultureScore",
|
||||
FieldName = "文化分",
|
||||
FieldType = "number",
|
||||
IsFilter = true,
|
||||
IsTrend = true
|
||||
},
|
||||
new ScorelineRecord
|
||||
{
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
SchoolId = schoolId,
|
||||
MajorId = majorId,
|
||||
Year = 2026,
|
||||
SchoolName = "美术学院",
|
||||
MajorName = "视觉传达",
|
||||
FieldValues = JsonSerializer.SerializeToElement(new { cultureScore = 420, scoreType = "统考" })
|
||||
},
|
||||
new ScorelineRecord
|
||||
{
|
||||
TenantId = tenantId,
|
||||
RegionId = regionId,
|
||||
SchoolId = schoolId,
|
||||
MajorId = majorId,
|
||||
Year = 2025,
|
||||
SchoolName = "美术学院",
|
||||
MajorName = "视觉传达",
|
||||
FieldValues = JsonSerializer.SerializeToElement(new { cultureScore = 360, scoreType = "统考" })
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync(
|
||||
$"/api/scoreline/records?tenantCode={tenantCode}®ionId={regionId}&min.cultureScore=400");
|
||||
var json = await ReadJsonAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(1, json.RootElement.GetProperty("total").GetInt32());
|
||||
var item = json.RootElement.GetProperty("items").EnumerateArray().Single();
|
||||
Assert.Equal(2026, item.GetProperty("year").GetInt32());
|
||||
Assert.Equal(420, item.GetProperty("fieldValues").GetProperty("cultureScore").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Public_scoreline_without_tenant_returns_not_found()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/api/scoreline/fields");
|
||||
var json = await ReadJsonAsync(response);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal("tenant_not_found", json.RootElement.GetProperty("code").GetString());
|
||||
}
|
||||
|
||||
private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
|
||||
{
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonDocument.ParseAsync(stream);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Growth;
|
||||
@@ -30,7 +31,7 @@ public sealed class PersistenceModelTests
|
||||
.Select(entity => entity.GetTableName())
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(124, tableNames.Count);
|
||||
Assert.Equal(126, tableNames.Count);
|
||||
Assert.Contains("tenants", tableNames);
|
||||
Assert.Contains("tenant_settings", tableNames);
|
||||
Assert.Contains("content_entries", tableNames);
|
||||
@@ -135,6 +136,8 @@ public sealed class PersistenceModelTests
|
||||
Assert.Contains("pb_import_issues", tableNames);
|
||||
Assert.Contains("questions", tableNames);
|
||||
Assert.Contains("question_versions", tableNames);
|
||||
Assert.Contains("scoreline_fields", tableNames);
|
||||
Assert.Contains("scoreline_records", tableNames);
|
||||
Assert.Contains("answer_records", tableNames);
|
||||
}
|
||||
|
||||
@@ -277,6 +280,41 @@ public sealed class PersistenceModelTests
|
||||
properties => Assert.Contains("TenantId", properties));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(ScorelineField), nameof(ScorelineField.Options), "'[]'::jsonb")]
|
||||
[InlineData(typeof(ScorelineRecord), nameof(ScorelineRecord.FieldValues), "'{}'::jsonb")]
|
||||
public void Scoreline_json_properties_are_mapped_to_jsonb(
|
||||
Type entityType,
|
||||
string propertyName,
|
||||
string expectedDefaultValueSql)
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var property = context.Model.FindEntityType(entityType)!
|
||||
.FindProperty(propertyName)!;
|
||||
|
||||
Assert.Equal("jsonb", property.GetColumnType());
|
||||
Assert.Equal(expectedDefaultValueSql, property.GetDefaultValueSql());
|
||||
Assert.Equal(typeof(System.Text.Json.JsonElement), property.ClrType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scoreline_relations_include_tenant_in_foreign_keys()
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
var foreignKeys = context.Model.FindEntityType(typeof(ScorelineRecord))!
|
||||
.GetForeignKeys()
|
||||
.Where(foreignKey => foreignKey.Properties.Count > 1)
|
||||
.Select(foreignKey => foreignKey.Properties
|
||||
.Select(property => property.Name)
|
||||
.ToArray());
|
||||
|
||||
Assert.All(
|
||||
foreignKeys,
|
||||
properties => Assert.Contains("TenantId", properties));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Word_user_join_tables_have_tenant_scoped_unique_indexes()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user