refactor(profile): lock question feedback to v2 delivery
This commit is contained in:
@@ -159,9 +159,9 @@ public sealed class NotificationStatusDto
|
||||
public sealed class SubmitFeedbackDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 本次练习中实际交付的会话题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
public Guid? SessionQuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
@@ -213,7 +213,7 @@ public sealed class SubmitFeedbackDto
|
||||
public SubmitFeedbackCommand ToCommand()
|
||||
{
|
||||
return new SubmitFeedbackCommand(
|
||||
QuestionId,
|
||||
SessionQuestionId,
|
||||
Type,
|
||||
Category,
|
||||
Title,
|
||||
@@ -223,4 +223,4 @@ public sealed class SubmitFeedbackDto
|
||||
Attachments,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ internal sealed class ContentExceptionProblemDetailsMapper : ExceptionProblemDet
|
||||
"profile_access_denied" => StatusCodes.Status403Forbidden,
|
||||
"profile_user_not_found" or "check_in_task_not_found" or "region_not_found" or "school_not_found" or
|
||||
"major_not_found" => StatusCodes.Status404NotFound,
|
||||
_ when code.EndsWith("_not_found", StringComparison.Ordinal) => StatusCodes.Status404NotFound,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
|
||||
|
||||
@@ -126,7 +126,11 @@ public sealed record FeedbackQuery(string? Status = null, string? Type = null, i
|
||||
|
||||
public sealed record FeedbackItem(
|
||||
Guid Id,
|
||||
Guid? QuestionId,
|
||||
Guid? SessionQuestionId,
|
||||
Guid? QuestionAssetOwnerTenantId,
|
||||
Guid? QuestionAssetId,
|
||||
Guid? QuestionRevisionId,
|
||||
Guid? QuestionPlacementId,
|
||||
string? Type,
|
||||
string? Title,
|
||||
string? Category,
|
||||
@@ -140,7 +144,7 @@ public sealed record FeedbackItem(
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record SubmitFeedbackCommand(
|
||||
Guid? QuestionId,
|
||||
Guid? SessionQuestionId,
|
||||
string? Type,
|
||||
string? Category,
|
||||
string? Title,
|
||||
@@ -199,4 +203,4 @@ public interface IProfileService
|
||||
|
||||
Task<ProfileScoreEventList> GetScoreEventsAsync(ProfileActor actor, ProfileScoreEventQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +610,11 @@ public sealed record TenantAdminFeedbackItem(
|
||||
Guid? UserId,
|
||||
string? UserName,
|
||||
string? UserPhone,
|
||||
Guid? QuestionId,
|
||||
Guid? SessionQuestionId,
|
||||
Guid? QuestionAssetOwnerTenantId,
|
||||
Guid? QuestionAssetId,
|
||||
Guid? QuestionRevisionId,
|
||||
Guid? QuestionPlacementId,
|
||||
ReportType? Type,
|
||||
string? Title,
|
||||
string? Category,
|
||||
@@ -629,4 +633,4 @@ public sealed record TenantAdminFeedbackItem(
|
||||
public sealed class TenantAdminDirectException(string message, string code) : Exception(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ public sealed class ExamDate : AuditableTenantEntity
|
||||
|
||||
public sealed class Report : AuditableTenantEntity
|
||||
{
|
||||
public Guid? QuestionId { get; set; }
|
||||
public Guid? SessionQuestionId { get; set; }
|
||||
public Guid? QuestionAssetOwnerTenantId { get; set; }
|
||||
public Guid? QuestionAssetId { get; set; }
|
||||
public Guid? QuestionRevisionId { get; set; }
|
||||
public Guid? QuestionPlacementId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? HandledBy { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
@@ -4,7 +4,6 @@ using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Configurations;
|
||||
@@ -50,11 +49,48 @@ internal sealed class ReportConfiguration : IEntityTypeConfiguration<Report>
|
||||
builder.Property(entity => entity.Metadata).IsJson("{}");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.CreatedAt });
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.QuestionId, entity.CreatedAt })
|
||||
.HasFilter("question_id is not null");
|
||||
builder.HasIndex(entity => new { entity.TenantId, entity.SessionQuestionId, entity.CreatedAt })
|
||||
.HasDatabaseName("ix_reports_session_question")
|
||||
.HasFilter("session_question_id is not null");
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
entity.QuestionAssetOwnerTenantId,
|
||||
entity.QuestionAssetId,
|
||||
entity.QuestionRevisionId
|
||||
}).HasDatabaseName("ix_reports_question_revision");
|
||||
builder.HasIndex(entity => new
|
||||
{
|
||||
entity.QuestionAssetOwnerTenantId,
|
||||
entity.QuestionPlacementId
|
||||
}).HasDatabaseName("ix_reports_question_placement");
|
||||
|
||||
builder.HasOne<Question>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
|
||||
builder.HasOne<PracticeSessionQuestion>().WithMany()
|
||||
.HasForeignKey(entity => new { entity.TenantId, Id = entity.SessionQuestionId })
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionAsset>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
TenantId = entity.QuestionAssetOwnerTenantId,
|
||||
Id = entity.QuestionAssetId
|
||||
})
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionRevision>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
TenantId = entity.QuestionAssetOwnerTenantId,
|
||||
QuestionAssetId = entity.QuestionAssetId,
|
||||
Id = entity.QuestionRevisionId
|
||||
})
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionAssetId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<QuestionPlacement>().WithMany()
|
||||
.HasForeignKey(entity => new
|
||||
{
|
||||
TenantId = entity.QuestionAssetOwnerTenantId,
|
||||
Id = entity.QuestionPlacementId
|
||||
})
|
||||
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany()
|
||||
|
||||
26511
Tiku.Infrastructure/Persistence/Migrations/20260806024912_MigrateFeedbackEvidenceV2.Designer.cs
generated
Normal file
26511
Tiku.Infrastructure/Persistence/Migrations/20260806024912_MigrateFeedbackEvidenceV2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MigrateFeedbackEvidenceV2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_reports_questions_tenant_id_question_id",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_reports_tenant_id_question_id_created_at",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "question_id",
|
||||
table: "reports",
|
||||
newName: "session_question_id");
|
||||
|
||||
// V1 question identifiers do not identify a delivered V2 session question.
|
||||
migrationBuilder.Sql("UPDATE reports SET session_question_id = NULL;");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "question_asset_id",
|
||||
table: "reports",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "question_asset_owner_tenant_id",
|
||||
table: "reports",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "question_placement_id",
|
||||
table: "reports",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "question_revision_id",
|
||||
table: "reports",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_question_placement",
|
||||
table: "reports",
|
||||
columns: new[] { "question_asset_owner_tenant_id", "question_placement_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_question_revision",
|
||||
table: "reports",
|
||||
columns: new[] { "question_asset_owner_tenant_id", "question_asset_id", "question_revision_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_session_question",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "session_question_id", "created_at" },
|
||||
filter: "session_question_id is not null");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_reports_practice_session_questions_tenant_id_session_questi~",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "session_question_id" },
|
||||
principalTable: "practice_session_questions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_reports_question_assets_question_asset_owner_tenant_id_ques~",
|
||||
table: "reports",
|
||||
columns: new[] { "question_asset_owner_tenant_id", "question_asset_id" },
|
||||
principalTable: "question_assets",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_reports_question_placements_question_asset_owner_tenant_id_~",
|
||||
table: "reports",
|
||||
columns: new[] { "question_asset_owner_tenant_id", "question_placement_id" },
|
||||
principalTable: "question_placements",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_reports_question_revisions_question_asset_owner_tenant_id_q~",
|
||||
table: "reports",
|
||||
columns: new[] { "question_asset_owner_tenant_id", "question_asset_id", "question_revision_id" },
|
||||
principalTable: "question_revisions",
|
||||
principalColumns: new[] { "tenant_id", "question_asset_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_reports_practice_session_questions_tenant_id_session_questi~",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_reports_question_assets_question_asset_owner_tenant_id_ques~",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_reports_question_placements_question_asset_owner_tenant_id_~",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_reports_question_revisions_question_asset_owner_tenant_id_q~",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_reports_question_placement",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_reports_question_revision",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_reports_session_question",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "question_asset_id",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "question_asset_owner_tenant_id",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "question_placement_id",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "question_revision_id",
|
||||
table: "reports");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "session_question_id",
|
||||
table: "reports",
|
||||
newName: "question_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_tenant_id_question_id_created_at",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "question_id", "created_at" },
|
||||
filter: "question_id is not null");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_reports_questions_tenant_id_question_id",
|
||||
table: "reports",
|
||||
columns: new[] { "tenant_id", "question_id" },
|
||||
principalTable: "questions",
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13243,14 +13243,30 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<Guid?>("QuestionId")
|
||||
b.Property<Guid?>("QuestionAssetId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("question_id");
|
||||
.HasColumnName("question_asset_id");
|
||||
|
||||
b.Property<Guid?>("QuestionAssetOwnerTenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("question_asset_owner_tenant_id");
|
||||
|
||||
b.Property<Guid?>("QuestionPlacementId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("question_placement_id");
|
||||
|
||||
b.Property<Guid?>("QuestionRevisionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("question_revision_id");
|
||||
|
||||
b.Property<string>("Resolution")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("resolution");
|
||||
|
||||
b.Property<Guid?>("SessionQuestionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("session_question_id");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
@@ -13293,13 +13309,19 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_reports_user_id");
|
||||
|
||||
b.HasIndex("QuestionAssetOwnerTenantId", "QuestionPlacementId")
|
||||
.HasDatabaseName("ix_reports_question_placement");
|
||||
|
||||
b.HasIndex("TenantId", "LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_reports_tenant_id_legacy_id");
|
||||
|
||||
b.HasIndex("TenantId", "QuestionId", "CreatedAt")
|
||||
.HasDatabaseName("ix_reports_tenant_id_question_id_created_at")
|
||||
.HasFilter("question_id is not null");
|
||||
b.HasIndex("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId")
|
||||
.HasDatabaseName("ix_reports_question_revision");
|
||||
|
||||
b.HasIndex("TenantId", "SessionQuestionId", "CreatedAt")
|
||||
.HasDatabaseName("ix_reports_session_question")
|
||||
.HasFilter("session_question_id is not null");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "CreatedAt")
|
||||
.HasDatabaseName("ix_reports_tenant_id_status_created_at");
|
||||
@@ -24797,12 +24819,33 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_reports_users_user_id");
|
||||
|
||||
b.HasOne("Tiku.Domain.QuestionBanks.Question", null)
|
||||
b.HasOne("Tiku.Domain.Content.QuestionAsset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "QuestionId")
|
||||
.HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.HasConstraintName("fk_reports_questions_tenant_id_question_id");
|
||||
.HasConstraintName("fk_reports_question_assets_question_asset_owner_tenant_id_ques~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Content.QuestionPlacement", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("QuestionAssetOwnerTenantId", "QuestionPlacementId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.HasConstraintName("fk_reports_question_placements_question_asset_owner_tenant_id_~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Learning.PracticeSessionQuestion", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId", "SessionQuestionId")
|
||||
.HasPrincipalKey("TenantId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.HasConstraintName("fk_reports_practice_session_questions_tenant_id_session_questi~");
|
||||
|
||||
b.HasOne("Tiku.Domain.Content.QuestionRevision", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("QuestionAssetOwnerTenantId", "QuestionAssetId", "QuestionRevisionId")
|
||||
.HasPrincipalKey("TenantId", "QuestionAssetId", "Id")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.HasConstraintName("fk_reports_question_revisions_question_asset_owner_tenant_id_q~");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tiku.Domain.Learning.ReportStatusEvent", b =>
|
||||
|
||||
@@ -9,7 +9,6 @@ using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -253,14 +252,32 @@ public sealed class ProfileService(IIdentityPersistence identityPersistence,
|
||||
{
|
||||
await EnsureProfileAsync(actor, cancellationToken);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(command.Description);
|
||||
await AssertReferenceAsync<Question>(actor.TenantId, command.QuestionId, "question_not_found",
|
||||
cancellationToken);
|
||||
PracticeSessionQuestion? sessionQuestion = null;
|
||||
if (command.SessionQuestionId.HasValue)
|
||||
{
|
||||
sessionQuestion = await (
|
||||
from question in learningPersistence.PracticeSessionQuestions.AsNoTracking()
|
||||
join session in learningPersistence.PracticeSessions.AsNoTracking()
|
||||
on new { question.TenantId, Id = question.PracticeSessionId }
|
||||
equals new { session.TenantId, session.Id }
|
||||
where question.TenantId == actor.TenantId &&
|
||||
question.Id == command.SessionQuestionId.Value &&
|
||||
session.UserId == actor.UserId
|
||||
select question)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
if (sessionQuestion is null)
|
||||
throw new ProfileException("Session question was not found.", "session_question_not_found");
|
||||
}
|
||||
|
||||
var feedback = new Report
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
UserId = actor.UserId,
|
||||
QuestionId = command.QuestionId,
|
||||
SessionQuestionId = sessionQuestion?.Id,
|
||||
QuestionAssetOwnerTenantId = sessionQuestion?.QuestionOwnerTenantId,
|
||||
QuestionAssetId = sessionQuestion?.QuestionAssetId,
|
||||
QuestionRevisionId = sessionQuestion?.QuestionRevisionId,
|
||||
QuestionPlacementId = sessionQuestion?.QuestionPlacementId,
|
||||
Type = ParseReportType(command.Type ?? "other"),
|
||||
Title = string.IsNullOrWhiteSpace(command.Title) ? null : command.Title.Trim(),
|
||||
Category = string.IsNullOrWhiteSpace(command.Category) ? null : command.Category.Trim(),
|
||||
@@ -573,7 +590,11 @@ public sealed class ProfileService(IIdentityPersistence identityPersistence,
|
||||
{
|
||||
return new FeedbackItem(
|
||||
report.Id,
|
||||
report.QuestionId,
|
||||
report.SessionQuestionId,
|
||||
report.QuestionAssetOwnerTenantId,
|
||||
report.QuestionAssetId,
|
||||
report.QuestionRevisionId,
|
||||
report.QuestionPlacementId,
|
||||
report.Type.HasValue ? ToWire(report.Type.Value) : null,
|
||||
report.Title,
|
||||
report.Category,
|
||||
|
||||
@@ -291,7 +291,11 @@ internal abstract partial class TenantAdminServiceBase
|
||||
report.UserId,
|
||||
user?.Name ?? user?.UserName,
|
||||
user?.Phone,
|
||||
report.QuestionId,
|
||||
report.SessionQuestionId,
|
||||
report.QuestionAssetOwnerTenantId,
|
||||
report.QuestionAssetId,
|
||||
report.QuestionRevisionId,
|
||||
report.QuestionPlacementId,
|
||||
report.Type,
|
||||
report.Title,
|
||||
report.Category,
|
||||
|
||||
@@ -184,6 +184,14 @@ public sealed class ProfileEndpointTests
|
||||
Priority = "normal"
|
||||
});
|
||||
var feedbackJson = await ReadJsonAsync(feedbackResponse);
|
||||
var forgedQuestionFeedback = await client.PostAsJsonAsync(
|
||||
"/api/student/profile/feedbacks",
|
||||
new SubmitFeedbackDto
|
||||
{
|
||||
SessionQuestionId = Guid.NewGuid(),
|
||||
Type = "suggestion",
|
||||
Description = "伪造会话题目引用"
|
||||
});
|
||||
var feedbacksResponse = await client.GetAsync("/api/student/profile/feedbacks?type=suggestion");
|
||||
var feedbacksJson = await ReadJsonAsync(feedbacksResponse);
|
||||
|
||||
@@ -196,6 +204,7 @@ public sealed class ProfileEndpointTests
|
||||
Assert.True(badgesJson.RootElement.GetProperty("items").EnumerateArray().Single().GetProperty("isUnlocked")
|
||||
.GetBoolean());
|
||||
Assert.Equal(HttpStatusCode.OK, feedbackResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, forgedQuestionFeedback.StatusCode);
|
||||
Assert.Equal("suggestion", feedbackJson.RootElement.GetProperty("type").GetString());
|
||||
Assert.Equal(HttpStatusCode.OK, feedbacksResponse.StatusCode);
|
||||
Assert.Single(feedbacksJson.RootElement.EnumerateArray());
|
||||
@@ -303,4 +312,4 @@ public sealed class ProfileEndpointTests
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonDocument.ParseAsync(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
- 首次决策:2026-08-05
|
||||
- 最近核对:2026-08-06
|
||||
|
||||
原版查询方式与 V2 的差异见 [V2 题库与原版 tiku-web 查询架构对比](content-domain-v2-vs-legacy-tiku-web.md)。
|
||||
|
||||
## 先说结论
|
||||
|
||||
V2 题库不是取消树形结构,而是把**编辑结构**和**查询结构**分开:
|
||||
@@ -467,6 +469,10 @@ PracticeSessionQuestion
|
||||
└─ Position 本次会话中的顺序
|
||||
```
|
||||
|
||||
题目纠错/举报也必须提交 `SessionQuestionId`,服务端只接受当前学生真实交付过的会话题目,并把
|
||||
Asset、Revision 与 Placement 证据复制到反馈记录。通用产品建议可以不关联题目;客户端不能直接
|
||||
提交任意题目资产 ID 来探测或举报未获授权的内容。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
OLD["学生开始练习\n锁定 Revision 1 和评分规则 1"]
|
||||
|
||||
Reference in New Issue
Block a user