feat(saas): implement marketplace and tenant onboarding

This commit is contained in:
2026-07-29 13:58:59 +08:00
parent 76606029e2
commit 6db200a2fc
145 changed files with 16862 additions and 94529 deletions

View File

@@ -3,6 +3,7 @@ using Serilog;
using Tiku.Api.Logging;
using Tiku.Api.Middleware;
using Tiku.Api.Options;
using Tiku.Api.Security;
namespace Tiku.Api.Configuration;
@@ -35,6 +36,7 @@ public static class ApplicationBuilderExtensions
app.UseRateLimiter();
app.UseMiddleware<CurrentPrincipalMiddleware>();
app.UseAuthorization();
app.UseMiddleware<SaasFeatureAccessMiddleware>();
app.MapControllers();
return app;

View File

@@ -6,15 +6,27 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 资产访问签名查询参数。
/// </summary>
public sealed class AssetAccessQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 有效期秒数。
/// </summary>
[Range(1, 7200)]
public int? ExpiresInSeconds { get; set; }
}
/// <summary>
/// 资产访问签名响应。
/// </summary>
public sealed record AssetAccessResponseDto(
ContentAssetAccessSummaryDto Item,
AssetAccessPrincipalDto Access,
@@ -34,6 +46,9 @@ public sealed record AssetAccessResponseDto(
}
}
/// <summary>
/// 内容资产访问摘要。
/// </summary>
public sealed record ContentAssetAccessSummaryDto(
Guid Id,
ContentAssetType AssetType,
@@ -60,11 +75,17 @@ public sealed record ContentAssetAccessSummaryDto(
}
}
/// <summary>
/// 资产访问Principal请求 DTO。
/// </summary>
public sealed record AssetAccessPrincipalDto(
Guid? UserId,
bool IsMember,
bool HasSvip);
/// <summary>
/// SignedStorageUrl请求 DTO。
/// </summary>
public sealed record SignedStorageUrlDto(
string Provider,
string? Bucket,

View File

@@ -3,39 +3,84 @@ using Tiku.Application.Assets;
namespace Tiku.Api.Contracts;
/// <summary>
/// 资产查询参数。
/// </summary>
public sealed class AssetQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 资产 ID。
/// </summary>
public Guid? AssetId { get; set; }
/// <summary>
/// 资产类型。
/// </summary>
[StringLength(50)]
public string? AssetType { get; set; }
/// <summary>
/// 分类。
/// </summary>
[StringLength(100)]
public string? Category { get; set; }
/// <summary>
/// 资产键。
/// </summary>
[StringLength(200)]
public string? AssetKey { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 是否包含锁定资源。
/// </summary>
public bool IncludeLocked { get; set; }
/// <summary>
/// 是否包含停用数据。
/// </summary>
public bool IncludeInactive { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }

View File

@@ -5,64 +5,130 @@ using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
/// <summary>
/// 创建资产上传签名请求。
/// </summary>
public sealed class AssetUploadSignDto
{
/// <summary>
/// 资产 ID。
/// </summary>
public Guid? AssetId { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 资产键。
/// </summary>
[StringLength(200)]
public string? AssetKey { get; set; }
/// <summary>
/// 标题。
/// </summary>
[StringLength(300)]
public string? Title { get; set; }
/// <summary>
/// 分类。
/// </summary>
[StringLength(100)]
public string? Category { get; set; }
/// <summary>
/// 说明。
/// </summary>
[StringLength(1000)]
public string? Description { get; set; }
/// <summary>
/// 文件名。
/// </summary>
[Required]
[StringLength(500)]
public string FileName { get; set; } = string.Empty;
/// <summary>
/// MIME 类型。
/// </summary>
[Required]
[StringLength(200)]
public string MimeType { get; set; } = string.Empty;
/// <summary>
/// 文件大小,单位为字节。
/// </summary>
[Range(0, long.MaxValue)]
public long? FileSizeBytes { get; set; }
/// <summary>
/// SHA-256 校验值。
/// </summary>
[RegularExpression("^[A-Fa-f0-9]{64}$")]
public string? ChecksumSha256 { get; set; }
/// <summary>
/// 资产类型。
/// </summary>
[StringLength(50)]
public string? AssetType { get; set; }
/// <summary>
/// 可见性。
/// </summary>
[StringLength(50)]
public string? Visibility { get; set; }
/// <summary>
/// 是否公开。
/// </summary>
public bool? IsPublic { get; set; }
/// <summary>
/// 服务提供方。
/// </summary>
[StringLength(50)]
public string? Provider { get; set; }
/// <summary>
/// 存储桶。
/// </summary>
[StringLength(200)]
public string? Bucket { get; set; }
/// <summary>
/// 对象存储键。
/// </summary>
[StringLength(1000)]
public string? ObjectKey { get; set; }
/// <summary>
/// 有效期秒数。
/// </summary>
[Range(1, 3600)]
public int? ExpiresInSeconds { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public AssetUploadSignCommand ToCommand()
@@ -92,17 +158,32 @@ public sealed class AssetUploadSignDto
}
}
/// <summary>
/// 确认资产上传请求。
/// </summary>
public sealed class AssetUploadConfirmDto
{
/// <summary>
/// 资产 ID。
/// </summary>
[Required]
public Guid AssetId { get; set; }
/// <summary>
/// MIME 类型。
/// </summary>
[StringLength(200)]
public string? MimeType { get; set; }
/// <summary>
/// 文件大小,单位为字节。
/// </summary>
[Range(0, long.MaxValue)]
public long? FileSizeBytes { get; set; }
/// <summary>
/// SHA-256 校验值。
/// </summary>
[RegularExpression("^[A-Fa-f0-9]{64}$")]
public string? ChecksumSha256 { get; set; }
@@ -112,34 +193,118 @@ public sealed class AssetUploadConfirmDto
}
}
/// <summary>
/// 新增或更新内容资产请求。
/// </summary>
public sealed class UpsertAssetDto
{
/// <summary>
/// 资产 ID。
/// </summary>
public Guid? AssetId { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 资产键。
/// </summary>
public string? AssetKey { get; set; }
/// <summary>
/// 标题。
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 分类。
/// </summary>
public string? Category { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 文件名。
/// </summary>
public string? FileName { get; set; }
/// <summary>
/// CDN 地址。
/// </summary>
public string? CdnUrl { get; set; }
/// <summary>
/// 是否公开。
/// </summary>
public bool? IsPublic { get; set; }
/// <summary>
/// 资产类型。
/// </summary>
public string? AssetType { get; set; }
/// <summary>
/// 可见性。
/// </summary>
public string? Visibility { get; set; }
/// <summary>
/// 状态。
/// </summary>
public string? Status { get; set; }
/// <summary>
/// 服务提供方。
/// </summary>
public string? Provider { get; set; }
/// <summary>
/// 存储桶。
/// </summary>
public string? Bucket { get; set; }
/// <summary>
/// 对象存储键。
/// </summary>
public string? ObjectKey { get; set; }
/// <summary>
/// MIME 类型。
/// </summary>
public string? MimeType { get; set; }
/// <summary>
/// 文件大小,单位为字节。
/// </summary>
public long? FileSizeBytes { get; set; }
/// <summary>
/// SHA-256 校验值。
/// </summary>
public string? ChecksumSha256 { get; set; }
/// <summary>
/// 预览地址。
/// </summary>
public string? PreviewUrl { get; set; }
/// <summary>
/// 预览对象存储键。
/// </summary>
public string? PreviewObjectKey { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 访问规则。
/// </summary>
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertAssetCommand ToCommand()
@@ -175,11 +340,20 @@ public sealed class UpsertAssetDto
}
}
/// <summary>
/// 创建资产访问签名请求。
/// </summary>
public sealed class AssetAccessSignDto
{
/// <summary>
/// 资产 ID。
/// </summary>
[Required]
public Guid AssetId { get; set; }
/// <summary>
/// 有效期秒数。
/// </summary>
[Range(60, 3600)]
public int? ExpiresInSeconds { get; set; }
@@ -189,31 +363,64 @@ public sealed class AssetAccessSignDto
}
}
/// <summary>
/// 资产管理查询参数。
/// </summary>
public sealed class AssetManagementQueryDto
{
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 资产类型。
/// </summary>
[StringLength(50)]
public string? AssetType { get; set; }
/// <summary>
/// 分类。
/// </summary>
[StringLength(100)]
public string? Category { get; set; }
/// <summary>
/// 上传状态。
/// </summary>
[StringLength(50)]
public string? UploadStatus { get; set; }
/// <summary>
/// 安全扫描状态。
/// </summary>
[StringLength(50)]
public string? SecurityScanStatus { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
@@ -233,11 +440,23 @@ public sealed class AssetManagementQueryDto
}
}
/// <summary>
/// 资产事件查询参数。
/// </summary>
public sealed class AssetEventQueryDto
{
/// <summary>
/// 资产 ID。
/// </summary>
public Guid? AssetId { get; set; }
/// <summary>
/// 用户 ID。
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
@@ -247,17 +466,32 @@ public sealed class AssetEventQueryDto
}
}
/// <summary>
/// 导入任务查询参数。
/// </summary>
public sealed class ImportJobQueryDto
{
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 导入Type。
/// </summary>
[StringLength(50)]
public string? ImportType { get; set; }
/// <summary>
/// 来源格式。
/// </summary>
[StringLength(50)]
public string? SourceFormat { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }

View File

@@ -6,32 +6,38 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Api.Contracts;
/// <summary>
/// 手机号密码登录请求
/// PasswordLogin请求 DTO
/// </summary>
public sealed class PasswordLoginDto
{
/// <summary>
/// 认证域。
/// </summary>
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略
/// 租户编码
/// </summary>
[StringLength(100)]
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
public string? TenantCode { get; set; }
/// <summary>
/// 账号标识。tenant 可使用手机号platform 可使用邮箱或用户名。
/// 账号标识。
/// </summary>
[StringLength(320)]
[Description("账号标识。tenant 可使用手机号platform 可使用邮箱或用户名。")]
public string? Identifier { get; set; }
/// <summary>
/// 兼容手机号字段;新客户端应使用 identifier。
/// </summary>
[StringLength(32)]
[Description("兼容手机号字段;新客户端应使用 identifier。")]
public string? Phone { get; set; }
/// <summary>
/// 用户密码。
/// 密码。
/// </summary>
[Required]
[StringLength(128, MinimumLength = 8)]
@@ -40,21 +46,24 @@ public sealed class PasswordLoginDto
}
/// <summary>
/// 短信验证码登录请求
/// SmsLogin请求 DTO
/// </summary>
public sealed class SmsLoginDto
{
/// <summary>
/// 认证域。
/// </summary>
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略
/// 租户编码
/// </summary>
[StringLength(100)]
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
public string? TenantCode { get; set; }
/// <summary>
/// 中国大陆手机号。
/// 手机号。
/// </summary>
[Required]
[StringLength(32)]
@@ -62,7 +71,7 @@ public sealed class SmsLoginDto
public string Phone { get; set; } = string.Empty;
/// <summary>
/// 收到的短信验证码。
/// 码。
/// </summary>
[Required]
[StringLength(12, MinimumLength = 4)]
@@ -70,38 +79,56 @@ public sealed class SmsLoginDto
public string Code { get; set; } = string.Empty;
}
/// <summary>
/// 发送短信验证码请求。
/// </summary>
public sealed class SendSmsCodeDto
{
/// <summary>
/// 认证域。
/// </summary>
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 手机号。
/// </summary>
[Required]
[StringLength(32)]
public string Phone { get; set; } = string.Empty;
/// <summary>
/// 设备 ID。
/// </summary>
[StringLength(256)]
public string? DeviceId { get; set; }
}
/// <summary>
/// OAuth code 登录请求
/// O认证编码请求 DTO
/// </summary>
public sealed class OAuthCodeDto
{
/// <summary>
/// 认证域。
/// </summary>
[Required]
public AuthRealm? Realm { get; set; }
/// <summary>
/// 平台控制域名登录时使用的租户代码;自定义域名登录可省略
/// 租户编码
/// </summary>
[StringLength(100)]
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
public string? TenantCode { get; set; }
/// <summary>
/// OAuth 平台返回的一次性授权 code
/// 编码
/// </summary>
[Required]
[StringLength(512)]
@@ -109,13 +136,13 @@ public sealed class OAuthCodeDto
public string Code { get; set; } = string.Empty;
/// <summary>
/// 客户端可提供的公开用户资料,不允许包含 token/secret。当前后端先保留模板字段后续按业务需要逐步使用
/// 公开用户资料
/// </summary>
[Description("客户端可提供的公开用户资料,不允许包含 token/secret。当前后端先保留模板字段后续按业务需要逐步使用。")]
public Dictionary<string, object?>? Profile { get; set; }
/// <summary>
/// 微信用户资料语言,例如 zh_CN。当前微信小程序登录不会使用该字段
/// 语言
/// </summary>
[StringLength(20)]
[Description("微信用户资料语言,例如 zh_CN。当前微信小程序登录不会使用该字段。")]
@@ -123,12 +150,12 @@ public sealed class OAuthCodeDto
}
/// <summary>
/// refresh token 请求
/// 刷新会话请求 DTO
/// </summary>
public sealed class RefreshSessionDto
{
/// <summary>
/// refresh token。服务端只存储哈希明文只返回给客户端一次
/// 刷新令牌
/// </summary>
[Required]
[StringLength(2048)]
@@ -137,7 +164,7 @@ public sealed class RefreshSessionDto
}
/// <summary>
/// 登录成功后的用户、租户成员和令牌信息
/// Authenticated用户请求 DTO
/// </summary>
public sealed class AuthenticatedUserDto
{
@@ -157,19 +184,22 @@ public sealed class AuthenticatedUserDto
public string? Email { get; init; }
/// <summary>
/// 用户显示名称。
/// 名称。
/// </summary>
public string? Name { get; init; }
/// <summary>
/// 认证域。
/// </summary>
public AuthRealm Realm { get; init; }
/// <summary>
/// 当前登录租户成员摘要。
/// 租户成员摘要。
/// </summary>
public TenantMembershipSummary? Tenant { get; init; }
/// <summary>
/// access token 和 refresh token
/// 访问令牌和刷新令牌
/// </summary>
public AuthTokenPair Tokens { get; init; } = default!;
@@ -188,11 +218,26 @@ public sealed class AuthenticatedUserDto
}
}
/// <summary>
/// 登录认证结果。
/// </summary>
public sealed class AuthenticationResultDto
{
/// <summary>
/// 状态。
/// </summary>
public AuthenticationStatus Status { get; init; }
/// <summary>
/// 用户信息。
/// </summary>
public AuthenticatedUserDto? User { get; init; }
/// <summary>
/// 挑战令牌。
/// </summary>
public string? ChallengeToken { get; init; }
/// <summary>
/// 挑战令牌过期时间。
/// </summary>
public DateTimeOffset? ChallengeExpiresAt { get; init; }
public static AuthenticationResultDto FromApplication(AuthenticationResult result) => new()
@@ -204,12 +249,21 @@ public sealed class AuthenticationResultDto
};
}
/// <summary>
/// 首次登录必改密码请求。
/// </summary>
public sealed class RequiredPasswordChangeDto
{
/// <summary>
/// 挑战令牌。
/// </summary>
[Required]
[StringLength(2048)]
public string ChallengeToken { get; set; } = string.Empty;
/// <summary>
/// 新密码。
/// </summary>
[Required]
[StringLength(128, MinimumLength = 8)]
public string NewPassword { get; set; } = string.Empty;

View File

@@ -3,11 +3,26 @@ using Tiku.Application.Jobs;
namespace Tiku.Api.Contracts;
/// <summary>
/// 创建租户后台任务请求。
/// </summary>
public sealed class CreateBackgroundJobDto
{
/// <summary>
/// 任务类型。
/// </summary>
public string JobType { get; set; } = string.Empty;
/// <summary>
/// 任务载荷。
/// </summary>
public JsonElement Payload { get; set; }
/// <summary>
/// 计划运行时间。
/// </summary>
public DateTimeOffset? RunAfter { get; set; }
/// <summary>
/// 最大重试次数。
/// </summary>
public int MaxRetries { get; set; } = 3;
public CreateBackgroundJobCommand ToCommand(Guid tenantId)

View File

@@ -4,13 +4,34 @@ using Tiku.Domain.Operations;
namespace Tiku.Api.Contracts;
/// <summary>
/// 创建或更新后台角色请求。
/// </summary>
public sealed class UpsertBackofficeRoleDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 编码。
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 名称。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 状态。
/// </summary>
public BackendRoleStatus Status { get; set; } = BackendRoleStatus.Active;
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 数据范围配置。
/// </summary>
public JsonElement? DataScope { get; set; }
public UpsertBackofficeRoleCommand ToCommand()
@@ -19,9 +40,18 @@ public sealed class UpsertBackofficeRoleDto
}
}
/// <summary>
/// 替换角色权限绑定请求。
/// </summary>
public sealed class ReplaceRoleBindingsDto
{
/// <summary>
/// 权限编码列表。
/// </summary>
public IReadOnlyCollection<string> PermissionCodes { get; set; } = [];
/// <summary>
/// 菜单编码列表。
/// </summary>
public IReadOnlyCollection<string> MenuCodes { get; set; } = [];
public ReplaceRoleBindingsCommand ToCommand(Guid roleId)
@@ -30,8 +60,14 @@ public sealed class ReplaceRoleBindingsDto
}
}
/// <summary>
/// 替换用户角色请求。
/// </summary>
public sealed class ReplaceUserRolesDto
{
/// <summary>
/// 角色 ID 列表。
/// </summary>
public IReadOnlyCollection<Guid> RoleIds { get; set; } = [];
public ReplaceUserRolesCommand ToCommand(Guid userId)

View File

@@ -3,10 +3,13 @@ using Tiku.Application.Catalog;
namespace Tiku.Api.Contracts;
/// <summary>
/// 目录查询参数。
/// </summary>
public sealed class CatalogQueryDto
{
/// <summary>
/// 租户编码。未登录公开查询时使用;已登录时优先使用当前 token 的租户。
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
@@ -17,7 +20,7 @@ public sealed class CatalogQueryDto
public Guid? RegionId { get; set; }
/// <summary>
/// 功能模块 ID
/// ModuleId
/// </summary>
public Guid? ModuleId { get; set; }
@@ -44,24 +47,24 @@ public sealed class CatalogQueryDto
public Guid? SubjectId { get; set; }
/// <summary>
/// 导航节点 ID。
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 名称关键
/// 关键
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 类型过滤,例如 cultural、professional、chapter、paper
/// 类型。
/// </summary>
[StringLength(50)]
public string? Type { get; set; }
/// <summary>
/// 返回数上限。
/// 返回数上限。
/// </summary>
[Range(1, 2000)]
public int? Limit { get; set; }

View File

@@ -3,25 +3,49 @@ using Tiku.Application.Commerce;
namespace Tiku.Api.Contracts;
/// <summary>
/// 创建交易订单请求 DTO。
/// </summary>
public sealed class CreateCommerceOrderDto
{
/// <summary>
/// 套餐 ID。
/// </summary>
[Required]
public Guid PlanId { get; set; }
/// <summary>
/// 数量。
/// </summary>
[Range(1, 99)]
public int Quantity { get; set; } = 1;
/// <summary>
/// 支付方式。
/// </summary>
[StringLength(50)]
public string? PayMethod { get; set; }
/// <summary>
/// 支付渠道。
/// </summary>
[StringLength(50)]
public string? PayProvider { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 优惠券码。
/// </summary>
[StringLength(100)]
public string? CouponCode { get; set; }
/// <summary>
/// 优惠券领取记录 ID。
/// </summary>
public Guid? CouponRedemptionId { get; set; }
public CreateCommerceOrderCommand ToCommand()
@@ -37,35 +61,65 @@ public sealed class CreateCommerceOrderDto
}
}
/// <summary>
/// 交易订单查询参数。
/// </summary>
public sealed class CommerceOrderQueryDto
{
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 100)]
public int? Limit { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
}
/// <summary>
/// 创建交易支付请求 DTO。
/// </summary>
public sealed class CreateCommercePaymentDto
{
/// <summary>
/// 订单号。
/// </summary>
[Required]
[StringLength(100)]
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 方式。
/// </summary>
[Required]
[StringLength(50)]
public string Method { get; set; } = string.Empty;
/// <summary>
/// 微信或支付渠道 openid。
/// </summary>
[StringLength(255)]
public string? OpenId { get; set; }
/// <summary>
/// 支付完成返回地址。
/// </summary>
[StringLength(2048)]
public string? ReturnUrl { get; set; }
/// <summary>
/// 支付取消返回地址。
/// </summary>
[StringLength(2048)]
public string? QuitUrl { get; set; }
@@ -81,11 +135,20 @@ public sealed class CreateCommercePaymentDto
}
}
/// <summary>
/// 交易优惠券查询参数。
/// </summary>
public sealed class CommerceCouponQueryDto
{
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 100)]
public int? Limit { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
@@ -95,8 +158,14 @@ public sealed class CommerceCouponQueryDto
}
}
/// <summary>
/// 领取交易优惠券请求 DTO。
/// </summary>
public sealed class ClaimCommerceCouponDto
{
/// <summary>
/// 优惠券码。
/// </summary>
[Required]
[StringLength(100)]
public string CouponCode { get; set; } = string.Empty;
@@ -107,19 +176,37 @@ public sealed class ClaimCommerceCouponDto
}
}
/// <summary>
/// 校验交易优惠券请求 DTO。
/// </summary>
public sealed class CheckCommerceCouponDto
{
/// <summary>
/// 优惠券码。
/// </summary>
[StringLength(100)]
public string? CouponCode { get; set; }
/// <summary>
/// 优惠券领取记录 ID。
/// </summary>
public Guid? CouponRedemptionId { get; set; }
/// <summary>
/// 套餐 ID。
/// </summary>
[Required]
public Guid PlanId { get; set; }
/// <summary>
/// 数量。
/// </summary>
[Range(1, 99)]
public int Quantity { get; set; } = 1;
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
public CheckCommerceCouponCommand ToCommand()

View File

@@ -5,132 +5,294 @@ using Tiku.Application.Growth;
namespace Tiku.Api.Contracts;
/// <summary>
/// 保存佣金配置请求。
/// </summary>
public sealed class UpdateCommissionSettingsDto
{
/// <summary>
/// 默认佣金比例。
/// </summary>
[Range(0, 1)]
public decimal? DefaultRate { get; set; }
/// <summary>
/// 最低结算金额,单位为分。
/// </summary>
[Range(0, int.MaxValue)]
public int? MinSettlementCents { get; set; }
/// <summary>
/// 结算周期。
/// </summary>
[StringLength(50)]
public string? SettlementCycle { get; set; }
/// <summary>
/// 配置内容。
/// </summary>
public JsonElement? Config { get; set; }
public UpdateCommissionSettingsCommand ToCommand() => new(DefaultRate, MinSettlementCents, SettlementCycle, Config);
}
/// <summary>
/// 调整成员佣金比例请求。
/// </summary>
public sealed class UpdateMemberCommissionRateDto
{
/// <summary>
/// 用户 ID。
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// 成员佣金比例。
/// </summary>
[Range(0, 1)]
public decimal? CommissionRate { get; set; }
/// <summary>
/// 佣金扩展配置。
/// </summary>
public JsonElement? CommissionConfig { get; set; }
public UpdateMemberCommissionRateCommand ToCommand() => new(UserId, CommissionRate, CommissionConfig);
}
/// <summary>
/// 佣金统计周期查询参数。
/// </summary>
public class CommissionPeriodQueryDto
{
/// <summary>
/// 开始日期,格式为 yyyy-MM-dd。
/// </summary>
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? StartDate { get; set; }
/// <summary>
/// 结束日期,格式为 yyyy-MM-dd。
/// </summary>
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? EndDate { get; set; }
/// <summary>
/// 推荐人用户 ID。
/// </summary>
public Guid? ReferrerUserId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
public CommissionPeriodQuery ToQuery() => new(ParseDate(StartDate), ParseDate(EndDate), ReferrerUserId, Limit);
protected static DateOnly? ParseDate(string? value) => string.IsNullOrWhiteSpace(value) ? null : DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
}
/// <summary>
/// 佣金结算单查询参数。
/// </summary>
public sealed class CommissionSettlementsQueryDto
{
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 推荐人用户 ID。
/// </summary>
public Guid? ReferrerUserId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
public CommissionSettlementQuery ToQuery() => new(Status, ReferrerUserId, Limit);
}
/// <summary>
/// 生成佣金结算单请求 DTO。
/// </summary>
public sealed class GenerateCommissionSettlementDto
{
/// <summary>
/// 开始日期,格式为 yyyy-MM-dd。
/// </summary>
[Required]
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string StartDate { get; set; } = string.Empty;
/// <summary>
/// 结束日期,格式为 yyyy-MM-dd。
/// </summary>
[Required]
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string EndDate { get; set; } = string.Empty;
/// <summary>
/// 推荐人用户 ID。
/// </summary>
[Required]
public Guid ReferrerUserId { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Remark { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public GenerateCommissionSettlementCommand ToCommand() => new(Parse(StartDate), Parse(EndDate), ReferrerUserId, Status, Remark, Metadata);
private static DateOnly Parse(string value) => DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
}
/// <summary>
/// 更新佣金结算单状态请求 DTO。
/// </summary>
public sealed class UpdateCommissionSettlementStatusDto
{
/// <summary>
/// 结算单 ID。
/// </summary>
[Required]
public Guid SettlementId { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 审核备注。
/// </summary>
[StringLength(1000)]
public string? ReviewNote { get; set; }
/// <summary>
/// 支付方式。
/// </summary>
[StringLength(100)]
public string? PaymentMethod { get; set; }
/// <summary>
/// 收款账号。
/// </summary>
[StringLength(200)]
public string? PaymentAccount { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public UpdateCommissionSettlementStatusCommand ToCommand() => new(SettlementId, Status, ReviewNote, PaymentMethod, PaymentAccount, Metadata);
}
/// <summary>
/// 佣金结算单导出查询参数。
/// </summary>
public sealed class CommissionSettlementExportQueryDto
{
/// <summary>
/// 结算单 ID。
/// </summary>
[Required]
public Guid SettlementId { get; set; }
/// <summary>
/// 导出格式。
/// </summary>
[StringLength(10)]
public string? Format { get; set; }
}
/// <summary>
/// 佣金结算单凭证查询参数。
/// </summary>
public sealed class CommissionSettlementProofQueryDto
{
/// <summary>
/// 结算单 ID。
/// </summary>
[Required]
public Guid SettlementId { get; set; }
}
/// <summary>
/// 创建佣金结算凭证请求。
/// </summary>
public sealed class CreateCommissionProofDto
{
/// <summary>
/// 结算单 ID。
/// </summary>
[Required]
public Guid SettlementId { get; set; }
/// <summary>
/// 凭证类型。
/// </summary>
[StringLength(50)]
public string? ProofType { get; set; }
/// <summary>
/// 标题。
/// </summary>
[StringLength(200)]
public string? Title { get; set; }
/// <summary>
/// 说明。
/// </summary>
[StringLength(2000)]
public string? Description { get; set; }
/// <summary>
/// 资产 ID。
/// </summary>
public Guid? AssetId { get; set; }
/// <summary>
/// 外部链接。
/// </summary>
[StringLength(2048)]
public string? ExternalUrl { get; set; }
/// <summary>
/// 金额,单位为分。
/// </summary>
[Range(0, int.MaxValue)]
public int? AmountCents { get; set; }
/// <summary>
/// 支付方式。
/// </summary>
[StringLength(100)]
public string? PaymentMethod { get; set; }
/// <summary>
/// 收款账号。
/// </summary>
[StringLength(200)]
public string? PaymentAccount { get; set; }
/// <summary>
/// 支付时间。
/// </summary>
public DateTimeOffset? PaidAt { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public CreateCommissionProofCommand ToCommand() => new(SettlementId, ProofType, Title, Description, AssetId, ExternalUrl, AmountCents, PaymentMethod, PaymentAccount, PaidAt, Metadata);
}
/// <summary>
/// 更新佣金结算凭证状态请求。
/// </summary>
public sealed class UpdateCommissionProofStatusDto
{
/// <summary>
/// 凭证 ID。
/// </summary>
[Required]
public Guid ProofId { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 审核备注。
/// </summary>
[StringLength(1000)]
public string? ReviewNote { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public UpdateCommissionProofStatusCommand ToCommand() => new(ProofId, Status, ReviewNote, Metadata);
}

View File

@@ -7,37 +7,76 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 内容管理查询参数。
/// </summary>
public sealed class ContentManagementQueryDto
{
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 父节点 ID传 root 表示根节点。
/// </summary>
[StringLength(64)]
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
public string? ParentId { get; set; }
/// <summary>
/// 入口类型。
/// </summary>
[StringLength(50)]
public string? EntryType { get; set; }
/// <summary>
/// 题集类型。
/// </summary>
[StringLength(50)]
public string? CollectionType { get; set; }
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; }
/// <summary>
/// 标记类型。
/// </summary>
[StringLength(50)]
public string? MarkerType { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 是否包含停用数据。
/// </summary>
public bool IncludeInactive { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 1000)]
public int? Limit { get; set; }
@@ -59,43 +98,88 @@ public sealed class ContentManagementQueryDto
}
}
/// <summary>
/// 新增或更新内容条目请求 DTO。
/// </summary>
public sealed class UpsertContentEntryDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
[StringLength(64)]
public string? LegacyId { get; set; }
/// <summary>
/// 入口键。
/// </summary>
[StringLength(100)]
public string? EntryKey { get; set; }
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(300)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 入口类型。
/// </summary>
[StringLength(50)]
public string? EntryType { get; set; }
/// <summary>
/// 图标。
/// </summary>
[StringLength(100)]
public string? Icon { get; set; }
/// <summary>
/// 路由地址。
/// </summary>
[StringLength(500)]
public string? Route { get; set; }
/// <summary>
/// 说明。
/// </summary>
[StringLength(2000)]
public string? Description { get; set; }
/// <summary>
/// 可见性。
/// </summary>
[StringLength(50)]
public string? Visibility { get; set; }
/// <summary>
/// 访问规则。
/// </summary>
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 布局配置。
/// </summary>
public JsonElement LayoutConfig { get; set; } = JsonDefaults.Object();
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
public UpsertContentEntryCommand ToCommand()
@@ -118,45 +202,96 @@ public sealed class UpsertContentEntryDto
}
}
/// <summary>
/// 新增或更新内容节点请求 DTO。
/// </summary>
public sealed class UpsertContentNodeDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
[Required]
public Guid EntryId { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 父节点 ID。
/// </summary>
public Guid? ParentId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
[StringLength(64)]
public string? LegacyId { get; set; }
/// <summary>
/// 节点键。
/// </summary>
[StringLength(100)]
public string? NodeKey { get; set; }
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(300)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 节点Type。
/// </summary>
[StringLength(50)]
public string? NodeType { get; set; }
/// <summary>
/// 标记类型。
/// </summary>
[StringLength(50)]
public string? MarkerType { get; set; }
/// <summary>
/// 标记配置。
/// </summary>
public JsonElement MarkerConfig { get; set; } = JsonDefaults.Object();
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 是否可选择。
/// </summary>
public bool? IsSelectable { get; set; }
/// <summary>
/// 是否叶子节点。
/// </summary>
public bool? IsLeaf { get; set; }
/// <summary>
/// 访问规则。
/// </summary>
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertContentNodeCommand ToCommand()
@@ -181,48 +316,105 @@ public sealed class UpsertContentNodeDto
}
}
/// <summary>
/// 新增或更新题目题集请求 DTO。
/// </summary>
public sealed class UpsertQuestionCollectionDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 题库 ID。
/// </summary>
public Guid? QuestionBankId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
[StringLength(64)]
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(300)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 题集类型。
/// </summary>
[StringLength(50)]
public string? CollectionType { get; set; }
/// <summary>
/// 来源类型。
/// </summary>
[StringLength(50)]
public string? SourceType { get; set; }
/// <summary>
/// 筛选条件。
/// </summary>
public JsonElement Filters { get; set; } = JsonDefaults.Object();
/// <summary>
/// 总分。
/// </summary>
public decimal? TotalScore { get; set; }
/// <summary>
/// 时长,单位为分钟。
/// </summary>
public int? DurationMinutes { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 访问规则。
/// </summary>
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertQuestionCollectionCommand ToCommand()
@@ -249,23 +441,47 @@ public sealed class UpsertQuestionCollectionDto
}
}
/// <summary>
/// 题集题目请求 DTO。
/// </summary>
public sealed class CollectionQuestionDto
{
/// <summary>
/// 题目 ID。
/// </summary>
[Required]
public Guid QuestionId { get; set; }
/// <summary>
/// 来源。
/// </summary>
[Required]
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
/// <summary>
/// 分段键。
/// </summary>
[StringLength(100)]
public string? SectionKey { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 分数。
/// </summary>
public decimal? Score { get; set; }
/// <summary>
/// 是否必填。
/// </summary>
public bool? Required { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CollectionQuestionCommand ToCommand()
@@ -280,11 +496,20 @@ public sealed class CollectionQuestionDto
}
}
/// <summary>
/// 替换题集Items请求 DTO。
/// </summary>
public sealed class ReplaceCollectionItemsDto
{
/// <summary>
/// 题集 ID。
/// </summary>
[Required]
public Guid CollectionId { get; set; }
/// <summary>
/// 题目列表。
/// </summary>
public IReadOnlyCollection<CollectionQuestionDto> Questions { get; set; } = [];
public ReplaceCollectionItemsCommand ToCommand()
@@ -295,48 +520,105 @@ public sealed class ReplaceCollectionItemsDto
}
}
/// <summary>
/// 新增或更新练习Blueprint请求 DTO。
/// </summary>
public sealed class UpsertPracticeBlueprintDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
[StringLength(64)]
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(300)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; }
/// <summary>
/// 组卷方式。
/// </summary>
[StringLength(50)]
public string? AssemblyType { get; set; }
/// <summary>
/// 题目数量上限。
/// </summary>
public int? QuestionLimit { get; set; }
/// <summary>
/// 时长,单位为分钟。
/// </summary>
public int? DurationMinutes { get; set; }
/// <summary>
/// 总分。
/// </summary>
public decimal? TotalScore { get; set; }
/// <summary>
/// 及格分。
/// </summary>
public decimal? PassScore { get; set; }
/// <summary>
/// 分段配置。
/// </summary>
public JsonElement Sections { get; set; } = JsonDefaults.Array();
/// <summary>
/// 规则配置。
/// </summary>
public JsonElement Rules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 访问规则。
/// </summary>
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
public UpsertPracticeBlueprintCommand ToCommand()
@@ -363,12 +645,21 @@ public sealed class UpsertPracticeBlueprintDto
}
}
/// <summary>
/// 导入Template查询参数。
/// </summary>
public sealed class ImportTemplateQueryDto
{
/// <summary>
/// 导入Type。
/// </summary>
[Required]
[StringLength(50)]
public string ImportType { get; set; } = string.Empty;
/// <summary>
/// 导出格式。
/// </summary>
[StringLength(10)]
public string? Format { get; set; }
}

View File

@@ -3,42 +3,87 @@ using Tiku.Application.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 内容Navigation查询参数。
/// </summary>
public sealed class ContentNavigationQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 父节点 ID传 root 表示根节点。
/// </summary>
[StringLength(64)]
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
public string? ParentId { get; set; }
/// <summary>
/// 入口类型。
/// </summary>
[StringLength(50)]
public string? EntryType { get; set; }
/// <summary>
/// 题集类型。
/// </summary>
[StringLength(50)]
public string? CollectionType { get; set; }
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; }
/// <summary>
/// 标记类型。
/// </summary>
[StringLength(50)]
public string? MarkerType { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 是否包含隐藏数据。
/// </summary>
public bool IncludeHidden { get; set; }
/// <summary>
/// 是否包含停用数据。
/// </summary>
public bool IncludeInactive { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 1000)]
public int? Limit { get; set; }

View File

@@ -4,35 +4,71 @@ using Tiku.Application.Growth;
namespace Tiku.Api.Contracts;
/// <summary>
/// 新增或更新CRM配置请求 DTO。
/// </summary>
public sealed class UpsertCrmConfigDto
{
/// <summary>
/// 是否启用。
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 访问地址。
/// </summary>
[StringLength(2048)]
public string? Url { get; set; }
/// <summary>
/// 密钥引用。
/// </summary>
[StringLength(300)]
public string? SecretRef { get; set; }
/// <summary>
/// 密钥内容。
/// </summary>
public string? Secret { get; set; }
/// <summary>
/// 表单名称。
/// </summary>
[StringLength(200)]
public string? FormName { get; set; }
/// <summary>
/// 考试类型。
/// </summary>
[StringLength(100)]
public string? ExamType { get; set; }
/// <summary>
/// 超时时长,单位为秒。
/// </summary>
[Range(1, 120)]
public int? TimeoutSeconds { get; set; }
/// <summary>
/// 延迟秒数。
/// </summary>
[Range(0, 86400)]
public int? DelaySeconds { get; set; }
/// <summary>
/// 分配模式。
/// </summary>
[StringLength(50)]
public string? AssignmentMode { get; set; }
/// <summary>
/// 分配池。
/// </summary>
public JsonElement? AssignmentPool { get; set; }
/// <summary>
/// 分配配置。
/// </summary>
public JsonElement? AssignmentConfig { get; set; }
public UpsertCrmConfigCommand ToCommand()
@@ -52,16 +88,31 @@ public sealed class UpsertCrmConfigDto
}
}
/// <summary>
/// CRM队列查询参数。
/// </summary>
public sealed class CrmQueueQueryDto
{
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 队列任务 ID。
/// </summary>
public Guid? QueueId { get; set; }
/// <summary>
/// 来源。
/// </summary>
[StringLength(200)]
public string? Source { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
@@ -71,10 +122,19 @@ public sealed class CrmQueueQueryDto
}
}
/// <summary>
/// CRM队列日志查询参数。
/// </summary>
public sealed class CrmQueueLogQueryDto
{
/// <summary>
/// 队列任务 ID。
/// </summary>
public Guid? QueueId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
@@ -84,17 +144,32 @@ public sealed class CrmQueueLogQueryDto
}
}
/// <summary>
/// CRM队列Action请求 DTO。
/// </summary>
public sealed class CrmQueueActionDto
{
/// <summary>
/// 队列任务 ID。
/// </summary>
[Required]
public Guid QueueId { get; set; }
/// <summary>
/// 操作。
/// </summary>
[StringLength(50)]
public string? Action { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(500)]
public string? Note { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public CrmQueueActionCommand ToCommand()

View File

@@ -5,28 +5,73 @@ using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
/// <summary>
/// 管理侧内容查询参数。
/// </summary>
public sealed class DirectContentQueryDto
{
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 父节点 ID。
/// </summary>
public Guid? ParentId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 章节 ID。
/// </summary>
public Guid? ChapterId { get; set; }
/// <summary>
/// 单元 ID。
/// </summary>
public Guid? UnitId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 专业 ID。
/// </summary>
public Guid? MajorId { get; set; }
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(200)]
public string? Keyword { get; set; }
/// <summary>
/// 年份。
/// </summary>
[Range(1900, 3000)]
public int? Year { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 1000)]
public int? Limit { get; set; }
@@ -50,37 +95,121 @@ public sealed class DirectContentQueryDto
}
}
/// <summary>
/// 管理侧题目写入请求。
/// </summary>
public sealed class DirectQuestionWriteDto
{
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 题库 ID。
/// </summary>
public Guid? QuestionBankId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 主题集 ID。
/// </summary>
public Guid? PrimaryCollectionId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 类型。
/// </summary>
public string? Type { get; set; }
/// <summary>
/// 类型显示名。
/// </summary>
public string? TypeLabel { get; set; }
/// <summary>
/// 难度。
/// </summary>
[Range(1, 5)]
public int? Difficulty { get; set; }
/// <summary>
/// 标签列表。
/// </summary>
public JsonElement Tags { get; set; } = JsonDefaults.Array();
/// <summary>
/// 内容。
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 选项配置。
/// </summary>
public JsonElement Options { get; set; } = JsonDefaults.Array();
/// <summary>
/// 正确选项索引。
/// </summary>
public int? CorrectOptionIndex { get; set; }
/// <summary>
/// 正确选项索引列表。
/// </summary>
public JsonElement CorrectOptionIndices { get; set; } = JsonDefaults.Array();
/// <summary>
/// 文字答案。
/// </summary>
public string? AnswerText { get; set; }
/// <summary>
/// 解析。
/// </summary>
public string? Explanation { get; set; }
/// <summary>
/// 子题列表。
/// </summary>
public JsonElement SubQuestions { get; set; } = JsonDefaults.Array();
/// <summary>
/// 代码语言。
/// </summary>
public string? CodeLang { get; set; }
/// <summary>
/// 代码模板。
/// </summary>
public string? CodeTemplate { get; set; }
/// <summary>
/// 媒体地址。
/// </summary>
public string? MediaUrl { get; set; }
/// <summary>
/// 状态。
/// </summary>
public string? Status { get; set; }
/// <summary>
/// 考试标记配置。
/// </summary>
public JsonElement ExamMarkers { get; set; } = JsonDefaults.Object();
/// <summary>
/// 来源哈希。
/// </summary>
public string? SourceHash { get; set; }
/// <summary>
/// 是否创建题目版本。
/// </summary>
public bool? CreateVersion { get; set; }
public QuestionWriteCommand ToCommand(bool createVersionDefault)
@@ -116,18 +245,54 @@ public sealed class DirectQuestionWriteDto
}
}
/// <summary>
/// 管理侧词汇单元写入请求。
/// </summary>
public sealed class DirectVocabularyUnitDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 单词数量。
/// </summary>
public int? WordCount { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VocabularyUnitCommand ToCommand()
@@ -136,22 +301,70 @@ public sealed class DirectVocabularyUnitDto
}
}
/// <summary>
/// 管理侧词汇单词写入请求。
/// </summary>
public sealed class DirectVocabularyWordDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 单元 ID。
/// </summary>
public Guid? UnitId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 单词。
/// </summary>
public required string Word { get; set; }
/// <summary>
/// 音标。
/// </summary>
public string? Phonetic { get; set; }
/// <summary>
/// 释义。
/// </summary>
public string? Meaning { get; set; }
/// <summary>
/// 例句。
/// </summary>
public string? Example { get; set; }
/// <summary>
/// 例句翻译。
/// </summary>
public string? ExampleTranslation { get; set; }
/// <summary>
/// 难度。
/// </summary>
public int? Difficulty { get; set; }
/// <summary>
/// 标签列表。
/// </summary>
public JsonElement Tags { get; set; } = JsonDefaults.Array();
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VocabularyWordCommand ToCommand()
@@ -175,22 +388,70 @@ public sealed class DirectVocabularyWordDto
}
}
/// <summary>
/// 管理侧知识手册科目写入请求。
/// </summary>
public sealed class DirectHandbookSubjectDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 专业 ID。
/// </summary>
public Guid? MajorId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 类型。
/// </summary>
public string? Type { get; set; }
/// <summary>
/// 图标。
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 颜色。
/// </summary>
public string? Color { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookSubjectCommand ToCommand()
@@ -214,17 +475,50 @@ public sealed class DirectHandbookSubjectDto
}
}
/// <summary>
/// 管理侧知识手册章节写入请求。
/// </summary>
public sealed class DirectHandbookChapterDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookChapterCommand ToCommand()
@@ -233,19 +527,58 @@ public sealed class DirectHandbookChapterDto
}
}
/// <summary>
/// 管理侧知识手册条目写入请求。
/// </summary>
public sealed class DirectHandbookEntryDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 章节 ID。
/// </summary>
public Guid? ChapterId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 标题。
/// </summary>
public required string Title { get; set; }
/// <summary>
/// 摘要。
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// 内容。
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 标签列表。
/// </summary>
public JsonElement Tags { get; set; } = JsonDefaults.Array();
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public HandbookEntryCommand ToCommand()
@@ -254,13 +587,34 @@ public sealed class DirectHandbookEntryDto
}
}
/// <summary>
/// 管理侧院校写入请求。
/// </summary>
public sealed class DirectSchoolDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 专业考试日期。
/// </summary>
public string? ProfessionalExamDate { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public SchoolCommand ToCommand()
@@ -269,16 +623,46 @@ public sealed class DirectSchoolDto
}
}
/// <summary>
/// 管理侧专业写入请求。
/// </summary>
public sealed class DirectMajorDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 名称。
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 备考建议。
/// </summary>
public string? StudyTips { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
public MajorCommand ToCommand()
@@ -287,22 +671,70 @@ public sealed class DirectMajorDto
}
}
/// <summary>
/// 管理侧分数线字段写入请求。
/// </summary>
public sealed class DirectScorelineFieldDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 字段键。
/// </summary>
public required string FieldKey { get; set; }
/// <summary>
/// 字段名称。
/// </summary>
public required string FieldName { get; set; }
/// <summary>
/// 字段类型。
/// </summary>
public string? FieldType { get; set; }
/// <summary>
/// 单位。
/// </summary>
public string? Unit { get; set; }
/// <summary>
/// 是否可筛选。
/// </summary>
public bool? IsFilter { get; set; }
/// <summary>
/// 是否必填。
/// </summary>
public bool? IsRequired { get; set; }
/// <summary>
/// 是否可见。
/// </summary>
public bool? IsVisible { get; set; }
/// <summary>
/// 是否参与趋势展示。
/// </summary>
public bool? IsTrend { get; set; }
/// <summary>
/// 选项配置。
/// </summary>
public JsonElement Options { get; set; } = JsonDefaults.Array();
/// <summary>
/// 占位提示。
/// </summary>
public string? Placeholder { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
public ScorelineFieldCommand ToCommand()
@@ -326,19 +758,49 @@ public sealed class DirectScorelineFieldDto
}
}
/// <summary>
/// 管理侧分数线记录写入请求。
/// </summary>
public sealed class DirectScorelineRecordDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 专业 ID。
/// </summary>
public Guid? MajorId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 年份。
/// </summary>
[Range(1900, 3000)]
public int Year { get; set; }
/// <summary>
/// 院校名称。
/// </summary>
public string? SchoolName { get; set; }
/// <summary>
/// 专业名称。
/// </summary>
public string? MajorName { get; set; }
/// <summary>
/// 动态字段值。
/// </summary>
public JsonElement FieldValues { get; set; } = JsonDefaults.Object();
public ScorelineRecordCommand ToCommand()
@@ -356,21 +818,66 @@ public sealed class DirectScorelineRecordDto
}
}
/// <summary>
/// 管理侧视频写入请求。
/// </summary>
public sealed class DirectVideoDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 标题。
/// </summary>
public required string Title { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 视频地址。
/// </summary>
public string? VideoUrl { get; set; }
/// <summary>
/// 缩略图地址。
/// </summary>
public string? ThumbnailUrl { get; set; }
/// <summary>
/// 时长,单位为秒。
/// </summary>
public int? DurationSeconds { get; set; }
/// <summary>
/// 知识标签。
/// </summary>
public JsonElement KnowledgeTags { get; set; } = JsonDefaults.Array();
/// <summary>
/// 是否通用视频。
/// </summary>
public bool? IsGeneral { get; set; }
/// <summary>
/// 难度。
/// </summary>
public int? Difficulty { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public VideoExplanationCommand ToCommand()
@@ -393,13 +900,34 @@ public sealed class DirectVideoDto
}
}
/// <summary>
/// 管理侧题目视频绑定请求。
/// </summary>
public sealed class DirectQuestionVideoDto
{
/// <summary>
/// 题目 ID。
/// </summary>
public Guid QuestionId { get; set; }
/// <summary>
/// 视频 ID。
/// </summary>
public Guid VideoId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 视频类型。
/// </summary>
public string? VideoType { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public QuestionVideoCommand ToCommand()
@@ -408,28 +936,94 @@ public sealed class DirectQuestionVideoDto
}
}
/// <summary>
/// 管理侧运营内容写入请求。
/// </summary>
public sealed class DirectOperationContentDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 历史系统 ID。
/// </summary>
public string? LegacyId { get; set; }
/// <summary>
/// 标题。
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 副标题。
/// </summary>
public string? Subtitle { get; set; }
/// <summary>
/// 内容。
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 问题。
/// </summary>
public string? Question { get; set; }
/// <summary>
/// 答案。
/// </summary>
public string? Answer { get; set; }
/// <summary>
/// 链接地址。
/// </summary>
public string? Link { get; set; }
/// <summary>
/// 按钮文案。
/// </summary>
public string? ButtonText { get; set; }
/// <summary>
/// 按钮链接。
/// </summary>
public string? ButtonLink { get; set; }
/// <summary>
/// 背景颜色。
/// </summary>
public string? BackgroundColor { get; set; }
/// <summary>
/// 边框颜色。
/// </summary>
public string? BorderColor { get; set; }
/// <summary>
/// 考试名称。
/// </summary>
public string? ExamName { get; set; }
/// <summary>
/// 考试时间。
/// </summary>
public DateTimeOffset? ExamAt { get; set; }
/// <summary>
/// 考试类型。
/// </summary>
public string? ExamType { get; set; }
/// <summary>
/// 说明。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 显示顺序。
/// </summary>
public int? Order { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public OperationContentCommand ToCommand()
@@ -459,27 +1053,90 @@ public sealed class DirectOperationContentDto
}
}
/// <summary>
/// 管理侧内容导入请求。
/// </summary>
public sealed class DirectImportDto
{
/// <summary>
/// 来源格式。
/// </summary>
public string? SourceFormat { get; set; }
/// <summary>
/// 来源名称。
/// </summary>
public string? SourceName { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 题库 ID。
/// </summary>
public Guid? QuestionBankId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 是否异步执行。
/// </summary>
public bool? Async { get; set; }
/// <summary>
/// 数据项列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Items { get; set; }
/// <summary>
/// 单元数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Units { get; set; }
/// <summary>
/// 单词数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Words { get; set; }
/// <summary>
/// 科目数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Subjects { get; set; }
/// <summary>
/// 条目数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Entries { get; set; }
/// <summary>
/// 字段数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Fields { get; set; }
/// <summary>
/// 院校数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Schools { get; set; }
/// <summary>
/// 专业数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Majors { get; set; }
/// <summary>
/// 记录数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Records { get; set; }
/// <summary>
/// 视频数据列表。
/// </summary>
public IReadOnlyCollection<JsonElement>? Videos { get; set; }
public SimpleImportCommand ToCommand(string importType, bool dryRun)
@@ -513,7 +1170,13 @@ public sealed class DirectImportDto
}
}
/// <summary>
/// 管理侧内容导入任务查询请求。
/// </summary>
public sealed class DirectImportJobDto
{
/// <summary>
/// 任务 ID。
/// </summary>
public Guid JobId { get; set; }
}

View File

@@ -6,6 +6,9 @@ namespace Tiku.Api.Contracts;
/// <param name="Status">服务状态,例如 ok。</param>
/// <param name="Service">服务名称。</param>
/// <param name="CheckedAt">检查时间。</param>
/// <summary>
/// 健康检查Response请求 DTO。
/// </summary>
public sealed record HealthResponseDto(
string Status,
string Service,

View File

@@ -7,14 +7,26 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 学习Limit查询参数。
/// </summary>
public sealed class LearningLimitQueryDto
{
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 单元 ID。
/// </summary>
public Guid? UnitId { get; set; }
public LearningLimitFilter ToFilter()
@@ -23,18 +35,36 @@ public sealed class LearningLimitQueryDto
}
}
/// <summary>
/// 练习会话查询参数。
/// </summary>
public sealed class PracticeSessionQueryDto
{
/// <summary>
/// 练习会话 ID。
/// </summary>
public Guid? PracticeSessionId { get; set; }
/// <summary>
/// 练习蓝图 ID。
/// </summary>
public Guid? BlueprintId { get; set; }
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
@@ -49,33 +79,69 @@ public sealed class PracticeSessionQueryDto
}
}
/// <summary>
/// 创建练习会话请求 DTO。
/// </summary>
public sealed class CreatePracticeSessionDto
{
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; }
/// <summary>
/// 目标类型。
/// </summary>
[StringLength(50)]
public string? TargetType { get; set; }
/// <summary>
/// 目标 ID。
/// </summary>
public Guid? TargetId { get; set; }
/// <summary>
/// 练习蓝图 ID。
/// </summary>
public Guid? BlueprintId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 题目数量上限。
/// </summary>
[Range(1, 500)]
public int? QuestionLimit { get; set; }
/// <summary>
/// 时长,单位为分钟。
/// </summary>
[Range(1, 1440)]
public int? DurationMinutes { get; set; }
/// <summary>
/// 总分。
/// </summary>
[Range(typeof(decimal), "0", "99999")]
public decimal? TotalScore { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public PracticeSessionCommand ToCommand()
@@ -95,8 +161,14 @@ public sealed class CreatePracticeSessionDto
}
}
/// <summary>
/// 提交练习会话请求 DTO。
/// </summary>
public sealed class SubmitPracticeSessionDto
{
/// <summary>
/// 练习会话 ID。
/// </summary>
[Required]
public Guid PracticeSessionId { get; set; }
@@ -106,16 +178,31 @@ public sealed class SubmitPracticeSessionDto
}
}
/// <summary>
/// 提交答案请求 DTO。
/// </summary>
public sealed class SubmitAnswerDto
{
/// <summary>
/// 会话题目 ID。
/// </summary>
[Required]
public Guid SessionQuestionId { get; set; }
/// <summary>
/// 已选选项。
/// </summary>
public IReadOnlyCollection<string>? SelectedOptions { get; set; }
/// <summary>
/// 文字答案。
/// </summary>
[StringLength(10000)]
public string? AnswerText { get; set; }
/// <summary>
/// 主观题自评是否正确。
/// </summary>
public bool? SelfJudgedCorrect { get; set; }
public SubmitAnswerCommand ToCommand()
@@ -128,14 +215,26 @@ public sealed class SubmitAnswerDto
}
}
/// <summary>
/// 题目Action请求 DTO。
/// </summary>
public sealed class QuestionActionDto
{
/// <summary>
/// 题目 ID。
/// </summary>
[Required]
public Guid QuestionId { get; set; }
/// <summary>
/// 来源。
/// </summary>
[Required]
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
/// <summary>
/// 是否收藏。
/// </summary>
public bool? Favorite { get; set; }
public QuestionActionCommand ToCommand()
@@ -144,20 +243,38 @@ public sealed class QuestionActionDto
}
}
/// <summary>
/// 单词Progress请求 DTO。
/// </summary>
public sealed class WordProgressDto
{
/// <summary>
/// 单词 ID。
/// </summary>
[Required]
public Guid WordId { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 答对数量变化。
/// </summary>
[Range(0, 100)]
public int? CorrectDelta { get; set; }
/// <summary>
/// 答错数量变化。
/// </summary>
[Range(0, 100)]
public int? WrongDelta { get; set; }
/// <summary>
/// 下次复习时间。
/// </summary>
public DateTimeOffset? NextReviewAt { get; set; }
public WordProgressCommand ToCommand()
@@ -171,13 +288,25 @@ public sealed class WordProgressDto
}
}
/// <summary>
/// 收藏单词请求 DTO。
/// </summary>
public sealed class FavoriteWordDto
{
/// <summary>
/// 单词 ID。
/// </summary>
[Required]
public Guid WordId { get; set; }
/// <summary>
/// 是否收藏。
/// </summary>
public bool? Favorite { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Note { get; set; }
@@ -187,14 +316,26 @@ public sealed class FavoriteWordDto
}
}
/// <summary>
/// 单词Review请求 DTO。
/// </summary>
public sealed class WordReviewDto
{
/// <summary>
/// 单词 ID。
/// </summary>
[Required]
public Guid WordId { get; set; }
/// <summary>
/// 结果。
/// </summary>
[StringLength(50)]
public string? Result { get; set; }
/// <summary>
/// 下次复习时间。
/// </summary>
public DateTimeOffset? NextReviewAt { get; set; }
public WordReviewCommand ToCommand()

View File

@@ -9,88 +9,201 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Api.Contracts;
/// <summary>
/// 平台管理端查询参数。
/// </summary>
public sealed class PlatformAdminQueryDto
{
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
/// <summary>
/// 搜索关键字。
/// </summary>
[StringLength(200)]
public string? Search { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
public PlatformAdminQuery ToQuery() => new(Status, Search, Limit);
}
/// <summary>
/// 创建平台租户请求 DTO。
/// </summary>
public sealed class CreatePlatformTenantDto
{
/// <summary>
/// 短编码。
/// </summary>
[Required]
[StringLength(100)]
public string Slug { get; set; } = string.Empty;
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(200)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 法定名称。
/// </summary>
[StringLength(300)]
public string? LegalName { get; set; }
/// <summary>
/// 状态。
/// </summary>
public TenantStatus Status { get; set; } = TenantStatus.Active;
/// <summary>
/// 账务状态。
/// </summary>
public BillingStatus BillingStatus { get; set; } = BillingStatus.Trial;
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CreatePlatformTenantCommand ToCommand() => new(Slug, Name, LegalName, Status, BillingStatus, Metadata);
/// <summary>
/// 租户负责人邮箱。
/// </summary>
[EmailAddress, StringLength(320)]
public string? OwnerEmail { get; set; }
/// <summary>
/// 租户负责人手机号。
/// </summary>
[Phone, StringLength(32)]
public string? OwnerPhone { get; set; }
/// <summary>
/// 租户负责人姓名。
/// </summary>
[Required, StringLength(100)]
public string OwnerName { get; set; } = string.Empty;
/// <summary>
/// 临时密码。
/// </summary>
[Required, StringLength(200, MinimumLength = 12)]
public string TemporaryPassword { get; set; } = string.Empty;
public CreatePlatformTenantCommand ToCommand() => new(
Slug, Name, LegalName, Status, BillingStatus, Metadata,
OwnerEmail, OwnerPhone, OwnerName, TemporaryPassword);
}
/// <summary>
/// 更新平台租户状态请求 DTO。
/// </summary>
public sealed class UpdatePlatformTenantStatusDto
{
/// <summary>
/// 租户 ID。
/// </summary>
[Required]
public Guid TenantId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public TenantStatus Status { get; set; } = TenantStatus.Active;
/// <summary>
/// 账务状态。
/// </summary>
public BillingStatus BillingStatus { get; set; } = BillingStatus.Active;
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
public UpdatePlatformTenantStatusCommand ToCommand() => new(TenantId, Status, BillingStatus, Reason);
}
/// <summary>
/// 新增或更新平台租户账务资料请求 DTO。
/// </summary>
public sealed class UpsertPlatformTenantBillingProfileDto
{
/// <summary>
/// 租户 ID。
/// </summary>
[Required]
public Guid TenantId { get; set; }
/// <summary>
/// 账务名称。
/// </summary>
[StringLength(300)]
public string? BillingName { get; set; }
/// <summary>
/// 税号。
/// </summary>
[StringLength(100)]
public string? TaxId { get; set; }
/// <summary>
/// 联系人姓名。
/// </summary>
[StringLength(100)]
public string? ContactName { get; set; }
/// <summary>
/// 联系人手机号。
/// </summary>
[StringLength(32)]
public string? ContactPhone { get; set; }
/// <summary>
/// 联系人邮箱。
/// </summary>
[StringLength(320)]
public string? ContactEmail { get; set; }
/// <summary>
/// 账务地址。
/// </summary>
[StringLength(1000)]
public string? BillingAddress { get; set; }
/// <summary>
/// 发票抬头。
/// </summary>
[StringLength(300)]
public string? InvoiceTitle { get; set; }
public TenantInvoiceTitleType? InvoiceType { get; set; }
/// <summary>
/// 发票类型。
/// </summary>
public TenantBillingInvoiceTitleType? InvoiceType { get; set; }
/// <summary>
/// 开户银行。
/// </summary>
[StringLength(300)]
public string? BankName { get; set; }
/// <summary>
/// 脱敏银行账号。
/// </summary>
[StringLength(100)]
public string? BankAccountMasked { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertPlatformTenantBillingProfileCommand ToCommand() => new(
@@ -108,143 +221,180 @@ public sealed class UpsertPlatformTenantBillingProfileDto
Metadata);
}
public sealed class UpsertPlatformSubscriptionDto
{
[Required]
public Guid TenantId { get; set; }
[Required]
[StringLength(100)]
public string PlanCode { get; set; } = string.Empty;
public TenantSubscriptionStatus Status { get; set; } = TenantSubscriptionStatus.Active;
public DateTimeOffset? StartsAt { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
[StringLength(32)]
public string? BillingCycle { get; set; }
[Range(0, int.MaxValue)]
public int AmountCents { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertPlatformSubscriptionCommand ToCommand() => new(
TenantId,
PlanCode,
Status,
StartsAt,
ExpiresAt,
BillingCycle,
AmountCents,
Metadata);
}
public sealed class ReplacePlatformPlanModulesDto
{
[Required]
public IReadOnlyCollection<string> ModuleCodes { get; set; } = [];
public ReplacePlatformPlanModulesCommand ToCommand(string planCode) => new(planCode, ModuleCodes);
}
public sealed class UpsertPlatformTenantModuleOverrideDto
{
public TenantModuleOverrideMode Mode { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
[Required]
[StringLength(1000, MinimumLength = 1)]
public string Reason { get; set; } = string.Empty;
public UpsertPlatformTenantModuleOverrideCommand ToCommand(Guid tenantId, string moduleCode) =>
new(tenantId, moduleCode, Mode, ExpiresAt, Reason);
}
/// <summary>
/// 新增或更新平台Staff请求 DTO。
/// </summary>
public sealed class UpsertPlatformStaffDto
{
/// <summary>
/// 用户 ID。
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// 邮箱。
/// </summary>
[StringLength(320)]
public string? Email { get; set; }
/// <summary>
/// 手机号。
/// </summary>
[StringLength(32)]
public string? Phone { get; set; }
/// <summary>
/// 名称。
/// </summary>
[StringLength(200)]
public string? Name { get; set; }
/// <summary>
/// 状态。
/// </summary>
public UserStatus Status { get; set; } = UserStatus.Active;
/// <summary>
/// 角色 ID 列表。
/// </summary>
public IReadOnlyCollection<Guid> RoleIds { get; set; } = [];
public UpsertPlatformStaffCommand ToCommand() => new(UserId, Email, Phone, Name, Status, RoleIds);
}
/// <summary>
/// 更新平台Staff状态请求 DTO。
/// </summary>
public sealed class UpdatePlatformStaffStatusDto
{
/// <summary>
/// 用户 ID。
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public UserStatus Status { get; set; } = UserStatus.Active;
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
public UpdatePlatformStaffStatusCommand ToCommand() => new(UserId, Status, Reason);
}
/// <summary>
/// 更新平台审计Alert状态请求 DTO。
/// </summary>
public sealed class UpdatePlatformAuditAlertStatusDto
{
/// <summary>
/// 告警 ID。
/// </summary>
[Required]
public Guid AlertId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public PlatformAuditAlertStatus Status { get; set; } = PlatformAuditAlertStatus.Acknowledged;
/// <summary>
/// 处理备注。
/// </summary>
[StringLength(1000)]
public string? ResolutionNote { get; set; }
public UpdatePlatformAuditAlertStatusCommand ToCommand() => new(AlertId, Status, ResolutionNote);
}
public sealed class UpsertPlatformDunningChannelDto
/// <summary>
/// 新增或更新平台DunningChannel请求 DTO。
/// </summary>
public sealed class UpsertPlatformBillingDunningChannelDto
{
/// <summary>
/// 通知渠道 ID。
/// </summary>
public Guid? ChannelId { get; set; }
/// <summary>
/// 渠道编码。
/// </summary>
[Required]
[StringLength(100)]
public string ChannelCode { get; set; } = string.Empty;
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(200)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 说明。
/// </summary>
[StringLength(1000)]
public string? Description { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool Enabled { get; set; } = true;
public PlatformDunningProvider Provider { get; set; } = PlatformDunningProvider.Generic;
/// <summary>
/// 服务提供方。
/// </summary>
public PlatformBillingDunningProvider Provider { get; set; } = PlatformBillingDunningProvider.Generic;
/// <summary>
/// Webhook 地址。
/// </summary>
[Required]
[StringLength(2048)]
public string WebhookUrl { get; set; } = string.Empty;
/// <summary>
/// 密钥引用。
/// </summary>
[StringLength(300)]
public string? SecretRef { get; set; }
/// <summary>
/// 提醒类型列表。
/// </summary>
public IReadOnlyCollection<string> ReminderTypes { get; set; } = ["overdue", "final_notice"];
/// <summary>
/// 提醒渠道列表。
/// </summary>
public IReadOnlyCollection<string> ReminderChannels { get; set; } = ["internal"];
/// <summary>
/// 最低提醒级别。
/// </summary>
[Range(1, 20)]
public int MinReminderLevel { get; set; } = 1;
/// <summary>
/// 租户 ID 列表。
/// </summary>
public IReadOnlyCollection<Guid> TenantIds { get; set; } = [];
/// <summary>
/// 超时时长,单位为秒。
/// </summary>
[Range(1, 60)]
public int TimeoutSeconds { get; set; } = 10;
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertPlatformDunningChannelCommand ToCommand() => new(
public UpsertPlatformBillingDunningChannelCommand ToCommand() => new(
ChannelId,
ChannelCode,
Name,
@@ -261,24 +411,42 @@ public sealed class UpsertPlatformDunningChannelDto
Metadata);
}
public sealed class DisablePlatformDunningChannelDto
/// <summary>
/// 禁用平台DunningChannel请求 DTO。
/// </summary>
public sealed class DisablePlatformBillingDunningChannelDto
{
/// <summary>
/// 通知渠道 ID。
/// </summary>
[Required]
public Guid ChannelId { get; set; }
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
public DisablePlatformDunningChannelCommand ToCommand() => new(ChannelId, Reason);
public DisablePlatformBillingDunningChannelCommand ToCommand() => new(ChannelId, Reason);
}
public sealed class RetryPlatformDunningEventDto
/// <summary>
/// 重试平台Dunning事件请求 DTO。
/// </summary>
public sealed class RetryPlatformBillingDunningEventDto
{
/// <summary>
/// 事件 ID。
/// </summary>
[Required]
public Guid EventId { get; set; }
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
public RetryPlatformDunningEventCommand ToCommand() => new(EventId, Reason);
public RetryPlatformBillingDunningEventCommand ToCommand() => new(EventId, Reason);
}

View File

@@ -3,14 +3,26 @@ using Tiku.Application.Points;
namespace Tiku.Api.Contracts;
/// <summary>
/// 积分查询参数。
/// </summary>
public sealed class PointQueryDto
{
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
public PointLimitQuery ToQuery()
@@ -19,15 +31,27 @@ public sealed class PointQueryDto
}
}
/// <summary>
/// 领取积分任务请求 DTO。
/// </summary>
public sealed class ClaimPointTaskDto
{
/// <summary>
/// 任务键。
/// </summary>
[Required]
[StringLength(100)]
public string TaskKey { get; set; } = string.Empty;
/// <summary>
/// 来源类型。
/// </summary>
[StringLength(100)]
public string? SourceType { get; set; }
/// <summary>
/// 来源 ID。
/// </summary>
public Guid? SourceId { get; set; }
public ClaimPointTaskCommand ToCommand()
@@ -36,8 +60,14 @@ public sealed class ClaimPointTaskDto
}
}
/// <summary>
/// 创建积分兑换订单请求 DTO。
/// </summary>
public sealed class CreatePointExchangeOrderDto
{
/// <summary>
/// 兑换项 ID。
/// </summary>
[Required]
public Guid ItemId { get; set; }

View File

@@ -4,47 +4,107 @@ using Tiku.Application.Profile;
namespace Tiku.Api.Contracts;
/// <summary>
/// 资料查询参数。
/// </summary>
public sealed class ProfileQueryDto
{
/// <summary>
/// 最近记录数量上限。
/// </summary>
[Range(1, 50)]
public int? RecentLimit { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 20)]
public int? Limit { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
/// <summary>
/// 类型。
/// </summary>
[StringLength(50)]
public string? Type { get; set; }
/// <summary>
/// 分类。
/// </summary>
[StringLength(50)]
public string? Category { get; set; }
/// <summary>
/// 是否包含锁定资源。
/// </summary>
public bool IncludeLocked { get; set; }
/// <summary>
/// 来源类型。
/// </summary>
[StringLength(100)]
public string? SourceType { get; set; }
/// <summary>
/// 开始时间。
/// </summary>
public DateTimeOffset? From { get; set; }
/// <summary>
/// 结束时间。
/// </summary>
public DateTimeOffset? To { get; set; }
}
/// <summary>
/// 更新资料请求 DTO。
/// </summary>
public sealed class UpdateProfileDto
{
/// <summary>
/// 名称。
/// </summary>
[StringLength(100)]
public string? Name { get; set; }
/// <summary>
/// 头像预设。
/// </summary>
[StringLength(32)]
public string? AvatarPreset { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 已选择院校 ID。
/// </summary>
public Guid? SelectedSchoolId { get; set; }
/// <summary>
/// 已选择专业 ID。
/// </summary>
public Guid? SelectedMajorId { get; set; }
/// <summary>
/// 统计数据。
/// </summary>
public JsonElement? Stats { get; set; }
/// <summary>
/// 进度数据。
/// </summary>
public JsonElement? Progress { get; set; }
/// <summary>
/// 模块选择数据。
/// </summary>
public JsonElement? ModuleSelections { get; set; }
/// <summary>
/// 最近活动数据。
/// </summary>
public JsonElement? RecentActivities { get; set; }
public UpdateProfileCommand ToCommand()
@@ -62,13 +122,22 @@ public sealed class UpdateProfileDto
}
}
/// <summary>
/// 通知状态请求 DTO。
/// </summary>
public sealed class NotificationStatusDto
{
/// <summary>
/// 通知 ID 列表。
/// </summary>
[Required]
[MinLength(1)]
[MaxLength(100)]
public IReadOnlyCollection<Guid> NotificationIds { get; set; } = [];
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
@@ -78,30 +147,60 @@ public sealed class NotificationStatusDto
}
}
/// <summary>
/// 提交反馈请求 DTO。
/// </summary>
public sealed class SubmitFeedbackDto
{
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 类型。
/// </summary>
[StringLength(50)]
public string? Type { get; set; }
/// <summary>
/// 分类。
/// </summary>
[StringLength(100)]
public string? Category { get; set; }
/// <summary>
/// 标题。
/// </summary>
[StringLength(200)]
public string? Title { get; set; }
/// <summary>
/// 说明。
/// </summary>
[Required]
[StringLength(5000)]
public string Description { get; set; } = string.Empty;
/// <summary>
/// 优先级。
/// </summary>
[StringLength(32)]
public string? Priority { get; set; }
/// <summary>
/// 联系方式。
/// </summary>
[StringLength(200)]
public string? Contact { get; set; }
/// <summary>
/// 附件列表。
/// </summary>
public JsonElement Attachments { get; set; } = JsonSerializer.SerializeToElement(Array.Empty<object>());
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonSerializer.SerializeToElement(new { });
public SubmitFeedbackCommand ToCommand()

View File

@@ -4,40 +4,82 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 题目题库查询参数。
/// </summary>
public sealed class QuestionBankQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 题库 ID。
/// </summary>
public Guid? QuestionBankId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 分类 ID。
/// </summary>
public Guid? CategoryId { get; set; }
/// <summary>
/// 节点 ID。
/// </summary>
public Guid? NodeId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 题集 ID。
/// </summary>
public Guid? CollectionId { get; set; }
/// <summary>
/// 来源。
/// </summary>
public QuestionSource? Source { get; set; }
/// <summary>
/// 类型。
/// </summary>
[StringLength(50)]
public string? Type { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 题目 ID 列表,支持逗号分隔;最多取前 300 个
/// 题目 ID 列表。
/// </summary>
public string? QuestionIds { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }

View File

@@ -5,17 +5,32 @@ using Tiku.Application.Growth;
namespace Tiku.Api.Contracts;
/// <summary>
/// 推荐租户查询参数。
/// </summary>
public sealed class ReferralTenantQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
}
/// <summary>
/// 推荐邀请码请求 DTO。
/// </summary>
public sealed class ReferralInviteDto
{
/// <summary>
/// 渠道。
/// </summary>
[StringLength(100)]
public string? Channel { get; set; }
/// <summary>
/// 落地页路径。
/// </summary>
[StringLength(2048)]
public string? LandingPath { get; set; }
@@ -25,11 +40,20 @@ public sealed class ReferralInviteDto
}
}
/// <summary>
/// 解析推荐请求 DTO。
/// </summary>
public sealed class ResolveReferralDto
{
/// <summary>
/// 编码。
/// </summary>
[StringLength(100)]
public string? Code { get; set; }
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
@@ -39,22 +63,43 @@ public sealed class ResolveReferralDto
}
}
/// <summary>
/// 记录推荐事件请求 DTO。
/// </summary>
public sealed class TrackReferralEventDto
{
/// <summary>
/// 推荐码。
/// </summary>
[Required]
[StringLength(100)]
public string RefCode { get; set; } = string.Empty;
/// <summary>
/// 事件类型。
/// </summary>
[StringLength(100)]
public string? EventType { get; set; }
/// <summary>
/// 来源。
/// </summary>
[StringLength(100)]
public string? Source { get; set; }
/// <summary>
/// 目标用户 ID。
/// </summary>
public Guid? TargetUserId { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
@@ -64,15 +109,27 @@ public sealed class TrackReferralEventDto
}
}
/// <summary>
/// 绑定推荐请求 DTO。
/// </summary>
public sealed class BindReferralDto
{
/// <summary>
/// 推荐码。
/// </summary>
[Required]
[StringLength(100)]
public string RefCode { get; set; } = string.Empty;
/// <summary>
/// 来源。
/// </summary>
[StringLength(100)]
public string? Source { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public BindReferralCommand ToCommand()
@@ -81,20 +138,38 @@ public sealed class BindReferralDto
}
}
/// <summary>
/// 推荐二维码请求 DTO。
/// </summary>
public sealed class ReferralQrcodeDto
{
/// <summary>
/// 页码。
/// </summary>
[StringLength(500)]
public string? Page { get; set; }
/// <summary>
/// 场景参数。
/// </summary>
[StringLength(128)]
public string? Scene { get; set; }
/// <summary>
/// 服务提供方。
/// </summary>
[StringLength(100)]
public string? Provider { get; set; }
/// <summary>
/// 二维码地址。
/// </summary>
[StringLength(2048)]
public string? QrcodeUrl { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public ReferralQrcodeCommand ToCommand()
@@ -103,10 +178,19 @@ public sealed class ReferralQrcodeDto
}
}
/// <summary>
/// 推荐统计查询参数。
/// </summary>
public sealed class ReferralStatsQueryDto
{
/// <summary>
/// 推荐人用户 ID。
/// </summary>
public Guid? ReferrerUserId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }
@@ -116,19 +200,37 @@ public sealed class ReferralStatsQueryDto
}
}
/// <summary>
/// 推荐Conversion查询参数。
/// </summary>
public sealed class ReferralConversionQueryDto
{
/// <summary>
/// 推荐人用户 ID。
/// </summary>
public Guid? ReferrerUserId { get; set; }
/// <summary>
/// 开始日期,格式为 yyyy-MM-dd。
/// </summary>
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? StartDate { get; set; }
/// <summary>
/// 结束日期,格式为 yyyy-MM-dd。
/// </summary>
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? EndDate { get; set; }
/// <summary>
/// 天数。
/// </summary>
[Range(1, 365)]
public int? Days { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 100)]
public int? Limit { get; set; }
@@ -150,19 +252,37 @@ public sealed class ReferralConversionQueryDto
}
}
/// <summary>
/// 人工绑定推荐请求 DTO。
/// </summary>
public sealed class ManualBindReferralDto
{
/// <summary>
/// 学生用户 ID。
/// </summary>
[Required]
public Guid StudentUserId { get; set; }
/// <summary>
/// 推荐人用户 ID。
/// </summary>
[Required]
public Guid ReferrerUserId { get; set; }
/// <summary>
/// 来源。
/// </summary>
[StringLength(100)]
public string? Source { get; set; }
/// <summary>
/// 是否强制执行。
/// </summary>
public bool Force { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public ManualBindReferralCommand ToCommand()
@@ -171,8 +291,14 @@ public sealed class ManualBindReferralDto
}
}
/// <summary>
/// 推荐团队查询参数。
/// </summary>
public sealed class ReferralTeamQueryDto
{
/// <summary>
/// 上级成员用户 ID。
/// </summary>
public Guid? LeaderUserId { get; set; }
public ReferralTeamQuery ToQuery()
@@ -181,19 +307,37 @@ public sealed class ReferralTeamQueryDto
}
}
/// <summary>
/// 新增或更新推荐团队请求 DTO。
/// </summary>
public sealed class UpsertReferralTeamDto
{
/// <summary>
/// 成员用户 ID。
/// </summary>
[Required]
public Guid MemberUserId { get; set; }
/// <summary>
/// 上级成员用户 ID。
/// </summary>
public Guid? LeaderUserId { get; set; }
/// <summary>
/// 关系类型。
/// </summary>
[StringLength(50)]
public string? RelationType { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(50)]
public string? Status { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement? Metadata { get; set; }
public UpsertReferralTeamCommand ToCommand()

View File

@@ -0,0 +1,151 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Tiku.Application.PlatformBilling;
using Tiku.Domain.Platform;
namespace Tiku.Api.Contracts;
/// <summary>
/// 新增或更新SaaS功能请求 DTO。
/// </summary>
public sealed record UpsertSaasFeatureDto(
Guid? Id,
[Required, MaxLength(120)] string Code,
[Required, MaxLength(200)] string Name,
[Required, MaxLength(100)] string Category,
[MaxLength(1000)] string? Description,
[Range(0, int.MaxValue)] int ReferencePriceCents,
[Required, MaxLength(10)] string Currency,
SaasFeatureStatus Status,
int SortOrder)
{
public UpsertSaasFeatureCommand ToCommand() => new(Id, Code, Name, Category, Description, ReferencePriceCents, Currency, Status, SortOrder);
}
/// <summary>
/// 新增或更新SaaS套餐请求 DTO。
/// </summary>
public sealed record UpsertSaasOfferingDto(
Guid? Id,
[Required, MaxLength(120)] string Code,
[Required, MaxLength(200)] string Name,
SaasOfferingType Type,
SaasOfferingStatus Status,
[MaxLength(1000)] string? Description,
int SortOrder)
{
public UpsertSaasOfferingCommand ToCommand() => new(Id, Code, Name, Type, Status, Description, SortOrder);
}
/// <summary>
/// 新增或更新 SaaS 功能额度定义。
/// </summary>
public sealed record UpsertSaasFeatureLimitDto(
Guid? Id,
[Required, MaxLength(120)] string MetricCode,
[Required, MaxLength(120)] string FeatureCode,
[Required, MaxLength(200)] string Name,
[Required, MaxLength(50)] string Unit,
SaasFeatureLimitKind Kind,
[Range(1, 100)] int WarningPercent,
bool IsHardLimit)
{
public UpsertSaasFeatureLimitCommand ToCommand() => new(
Id, MetricCode, FeatureCode, Name, Unit, Kind, WarningPercent, IsHardLimit);
}
/// <summary>
/// 新增或更新SaaS套餐版本请求 DTO。
/// </summary>
public sealed record UpsertSaasOfferingVersionDto(
Guid? Id,
Guid OfferingId,
PlatformBillingCycle BillingCycle,
[Range(0, int.MaxValue)] int OriginalAmountCents,
[Range(0, int.MaxValue)] int AmountCents,
[Required, MaxLength(10)] string Currency,
DateTimeOffset? EffectiveAt,
IReadOnlyCollection<string>? FeatureCodes,
IReadOnlyDictionary<string, long>? Limits,
JsonElement Metadata)
{
public UpsertSaasOfferingVersionCommand ToCommand() => new(
Id, OfferingId, BillingCycle, OriginalAmountCents, AmountCents, Currency, EffectiveAt,
FeatureCodes ?? [], Limits ?? new Dictionary<string, long>(), Metadata);
}
/// <summary>
/// 创建平台账务报价请求 DTO。
/// </summary>
public sealed record CreatePlatformBillingQuoteDto(
Guid BaseOfferingVersionId,
IReadOnlyCollection<Guid>? AddOnOfferingVersionIds,
PlatformBillingOrderPurpose Purpose,
[Required, MaxLength(200)] string IdempotencyKey)
{
public CreatePlatformBillingQuoteCommand ToCommand() => new(BaseOfferingVersionId, AddOnOfferingVersionIds ?? [], Purpose, IdempotencyKey);
}
/// <summary>
/// 创建平台账务订单请求 DTO。
/// </summary>
public sealed record CreatePlatformBillingOrderDto(Guid QuoteId, [Required, MaxLength(200)] string IdempotencyKey)
{
public CreatePlatformBillingOrderCommand ToCommand() => new(QuoteId, IdempotencyKey);
}
/// <summary>
/// 创建平台账务支付请求 DTO。
/// </summary>
public sealed record CreatePlatformBillingPaymentDto(
[Required, MaxLength(50)] string Provider,
[Required, MaxLength(50)] string Method,
[Required, MaxLength(200)] string IdempotencyKey,
string? OpenId,
string? ReturnUrl,
string? QuitUrl)
{
public CreatePlatformBillingPaymentCommand ToCommand(string orderNo) =>
new(orderNo, Provider, Method, IdempotencyKey, OpenId, ReturnUrl, QuitUrl);
}
/// <summary>
/// 变更租户订阅请求 DTO。
/// </summary>
public sealed record ChangeTenantSubscriptionDto(
Guid BaseOfferingVersionId,
IReadOnlyCollection<Guid>? AddOnOfferingVersionIds,
[Required, MaxLength(200)] string IdempotencyKey)
{
public ChangeTenantSubscriptionCommand ToCommand() => new(BaseOfferingVersionId, AddOnOfferingVersionIds ?? []);
}
/// <summary>
/// Idempotent租户账务请求 DTO。
/// </summary>
public sealed record IdempotentTenantBillingDto([Required, MaxLength(200)] string IdempotencyKey);
/// <summary>
/// 确认人工平台支付请求 DTO。
/// </summary>
public sealed record ConfirmManualPlatformPaymentDto(
Guid PaymentId,
string? ProviderTradeNo,
DateTimeOffset? PaidAt,
[Required, MaxLength(1000)] string Reason)
{
public ConfirmManualPaymentCommand ToCommand() => new(PaymentId, ProviderTradeNo, PaidAt, Reason);
}
/// <summary>
/// 新增或更新租户功能覆盖规则请求 DTO。
/// </summary>
public sealed record UpsertTenantFeatureOverrideDto(
Guid TenantId,
[Required, MaxLength(120)] string FeatureCode,
TenantFeatureOverrideMode Mode,
DateTimeOffset? ExpiresAt,
[Required, MaxLength(1000)] string Reason)
{
public UpsertTenantFeatureOverrideCommand ToCommand() => new(TenantId, FeatureCode, Mode, ExpiresAt, Reason);
}

View File

@@ -3,27 +3,57 @@ using Tiku.Application.Scoreline;
namespace Tiku.Api.Contracts;
/// <summary>
/// 分数线查询参数。
/// </summary>
public sealed class ScorelineQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 院校 ID。
/// </summary>
public Guid? SchoolId { get; set; }
/// <summary>
/// 专业 ID。
/// </summary>
public Guid? MajorId { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 年份。
/// </summary>
[Range(1900, 3000)]
public int? Year { get; set; }
/// <summary>
/// 页码。
/// </summary>
[Range(1, 10000)]
public int? Page { get; set; }
/// <summary>
/// 每页数量。
/// </summary>
[Range(1, 200)]
public int? PageSize { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 2000)]
public int? Limit { get; set; }

View File

@@ -3,28 +3,61 @@ using Tiku.Application.StudyContent;
namespace Tiku.Api.Contracts;
/// <summary>
/// Study内容查询参数。
/// </summary>
public sealed class StudyContentQueryDto
{
/// <summary>
/// 租户编码。
/// </summary>
[StringLength(100)]
public string? TenantCode { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 单元 ID。
/// </summary>
public Guid? UnitId { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 章节 ID。
/// </summary>
public Guid? ChapterId { get; set; }
/// <summary>
/// 内容入口 ID。
/// </summary>
public Guid? EntryId { get; set; }
/// <summary>
/// 内容节点 ID。
/// </summary>
public Guid? ContentNodeId { get; set; }
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 是否包含正文内容。
/// </summary>
public bool IncludeContent { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 2000)]
public int? Limit { get; set; }

View File

@@ -7,17 +7,41 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Contracts;
/// <summary>
/// 创建分类节点请求 DTO。
/// </summary>
public sealed class CreateTaxonomyNodeDto
{
/// <summary>
/// 父节点 ID。
/// </summary>
public Guid? ParentId { get; set; }
/// <summary>
/// 父级来源。
/// </summary>
public QuestionSource? ParentSource { get; set; }
/// <summary>
/// 节点Type。
/// </summary>
[Required]
public TaxonomyNodeType NodeType { get; set; }
/// <summary>
/// 编码。
/// </summary>
[Required, StringLength(100)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 名称。
/// </summary>
[Required, StringLength(300)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CreateTaxonomyNodeCommand ToCommand() => new(

File diff suppressed because it is too large Load Diff

View File

@@ -7,41 +7,83 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Api.Contracts;
/// <summary>
/// 租户交易查询参数。
/// </summary>
public sealed class TenantCommerceQueryDto
{
/// <summary>
/// 服务提供方。
/// </summary>
[StringLength(50)]
public string? Provider { get; set; }
/// <summary>
/// 状态。
/// </summary>
[StringLength(32)]
public string? Status { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
/// <summary>
/// 用户 ID。
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
}
/// <summary>
/// 新增或更新支付账号请求 DTO。
/// </summary>
public sealed class UpsertPaymentAccountDto
{
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 模式。
/// </summary>
[StringLength(50)]
public string? Mode { get; set; } = "TenantCollect";
/// <summary>
/// 显示名称。
/// </summary>
[StringLength(200)]
public string? DisplayName { get; set; }
/// <summary>
/// 状态。
/// </summary>
public TenantExternalProviderStatus Status { get; set; } = TenantExternalProviderStatus.Disabled;
/// <summary>
/// 密钥引用。
/// </summary>
[StringLength(300)]
public string? SecretRef { get; set; }
/// <summary>
/// 优先级。
/// </summary>
public int? Priority { get; set; }
/// <summary>
/// 公开配置内容。
/// </summary>
public JsonElement ConfigPublic { get; set; } = JsonDefaults.Object();
public UpsertPaymentAccountCommand ToCommand()
@@ -50,25 +92,49 @@ public sealed class UpsertPaymentAccountDto
}
}
/// <summary>
/// 新增或更新租户Secret请求 DTO。
/// </summary>
public sealed class UpsertTenantSecretDto
{
/// <summary>
/// 用途。
/// </summary>
[Required]
[StringLength(80)]
public string Purpose { get; set; } = "payment";
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 密钥键。
/// </summary>
[Required]
[StringLength(120)]
public string SecretKey { get; set; } = string.Empty;
/// <summary>
/// 密钥引用。
/// </summary>
[StringLength(300)]
public string SecretRef { get; set; } = string.Empty;
/// <summary>
/// 状态。
/// </summary>
public TenantSecretStatus Status { get; set; } = TenantSecretStatus.Active;
/// <summary>
/// 密钥载荷。
/// </summary>
public JsonElement SecretPayload { get; set; } = JsonDefaults.Object();
/// <summary>
/// 过期时间。
/// </summary>
public DateTimeOffset? ExpiresAt { get; set; }
public UpsertTenantSecretCommand ToCommand()
@@ -84,32 +150,62 @@ public sealed class UpsertTenantSecretDto
}
}
/// <summary>
/// 创建编码批次请求 DTO。
/// </summary>
public sealed class CreateCodeBatchDto
{
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(200)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 总数量。
/// </summary>
[Range(1, 1000)]
public int TotalCount { get; set; }
/// <summary>
/// 天数。
/// </summary>
[Range(1, 3650)]
public int Days { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 售卖类型。
/// </summary>
[StringLength(50)]
public string? SaleType { get; set; }
/// <summary>
/// 渠道。
/// </summary>
[StringLength(100)]
public string? Channel { get; set; }
/// <summary>
/// 默认单价,单位为分。
/// </summary>
[Range(0, int.MaxValue)]
public int? DefaultUnitPriceCents { get; set; }
/// <summary>
/// 成本价,单位为分。
/// </summary>
[Range(0, int.MaxValue)]
public int? CostPriceCents { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Remark { get; set; }
@@ -128,15 +224,27 @@ public sealed class CreateCodeBatchDto
}
}
/// <summary>
/// 核销激活码编码请求 DTO。
/// </summary>
public sealed class RedeemActivationCodeDto
{
/// <summary>
/// 编码。
/// </summary>
[Required]
[StringLength(100)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 用户 ID。
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
public RedeemActivationCodeCommand ToCommand()
@@ -145,34 +253,76 @@ public sealed class RedeemActivationCodeDto
}
}
/// <summary>
/// 新增或更新积分任务请求 DTO。
/// </summary>
public sealed class UpsertPointTaskDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 任务键。
/// </summary>
[Required]
[StringLength(100)]
public string TaskKey { get; set; } = string.Empty;
/// <summary>
/// 标题。
/// </summary>
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
/// <summary>
/// 说明。
/// </summary>
[StringLength(1000)]
public string? Description { get; set; }
/// <summary>
/// 任务类型。
/// </summary>
public PointActivityTaskType TaskType { get; set; } = PointActivityTaskType.Manual;
/// <summary>
/// 状态。
/// </summary>
public PointActivityTaskStatus Status { get; set; } = PointActivityTaskStatus.Active;
/// <summary>
/// 积分数量。
/// </summary>
[Range(1, int.MaxValue)]
public int Points { get; set; }
/// <summary>
/// 每用户最多领取次数。
/// </summary>
[Range(1, 1000)]
public int MaxClaimsPerUser { get; set; } = 1;
/// <summary>
/// 开始时间。
/// </summary>
public DateTimeOffset? StartsAt { get; set; }
/// <summary>
/// 结束时间。
/// </summary>
public DateTimeOffset? EndsAt { get; set; }
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 规则配置。
/// </summary>
public JsonElement Rules { get; set; } = JsonDefaults.Object();
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertPointTaskCommand ToCommand()
@@ -194,38 +344,86 @@ public sealed class UpsertPointTaskDto
}
}
/// <summary>
/// 新增或更新积分兑换Item请求 DTO。
/// </summary>
public sealed class UpsertPointExchangeItemDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 地区 ID。
/// </summary>
public Guid? RegionId { get; set; }
/// <summary>
/// 兑换项键。
/// </summary>
[Required]
[StringLength(100)]
public string ItemKey { get; set; } = string.Empty;
/// <summary>
/// 名称。
/// </summary>
[Required]
[StringLength(200)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 说明。
/// </summary>
[StringLength(1000)]
public string? Description { get; set; }
/// <summary>
/// 兑换项类型。
/// </summary>
public PointExchangeItemType ItemType { get; set; } = PointExchangeItemType.Entitlement;
/// <summary>
/// 状态。
/// </summary>
public PointExchangeItemStatus Status { get; set; } = PointExchangeItemStatus.Active;
/// <summary>
/// 所需积分。
/// </summary>
[Range(1, int.MaxValue)]
public int PointsCost { get; set; }
/// <summary>
/// 库存数量。
/// </summary>
[Range(0, int.MaxValue)]
public int? Stock { get; set; }
/// <summary>
/// 天数。
/// </summary>
[Range(0, 3650)]
public int? Days { get; set; }
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 开始时间。
/// </summary>
public DateTimeOffset? StartsAt { get; set; }
/// <summary>
/// 结束时间。
/// </summary>
public DateTimeOffset? EndsAt { get; set; }
/// <summary>
/// 履约载荷。
/// </summary>
public JsonElement FulfillmentPayload { get; set; } = JsonDefaults.Object();
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public UpsertPointExchangeItemCommand ToCommand()
@@ -249,20 +447,41 @@ public sealed class UpsertPointExchangeItemDto
}
}
/// <summary>
/// 创建退款Request请求 DTO。
/// </summary>
public sealed class CreateRefundRequestDto
{
/// <summary>
/// 订单 ID。
/// </summary>
[Required]
public Guid OrderId { get; set; }
/// <summary>
/// 支付记录 ID。
/// </summary>
public Guid? PaymentId { get; set; }
/// <summary>
/// 金额,单位为分。
/// </summary>
[Range(1, int.MaxValue)]
public int AmountCents { get; set; }
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
/// <summary>
/// 权益动作。
/// </summary>
public RefundEntitlementAction EntitlementAction { get; set; } = RefundEntitlementAction.RevokeOnSuccess;
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CreateRefundRequestCommand ToCommand()
@@ -271,16 +490,31 @@ public sealed class CreateRefundRequestDto
}
}
/// <summary>
/// 更新退款状态请求 DTO。
/// </summary>
public sealed class UpdateRefundStatusDto
{
/// <summary>
/// 退款申请 ID。
/// </summary>
[Required]
public Guid RefundRequestId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public CommerceRefundStatus Status { get; set; }
/// <summary>
/// 原因。
/// </summary>
[StringLength(1000)]
public string? Reason { get; set; }
/// <summary>
/// 渠道退款单号。
/// </summary>
[StringLength(200)]
public string? ProviderRefundNo { get; set; }
@@ -290,23 +524,47 @@ public sealed class UpdateRefundStatusDto
}
}
/// <summary>
/// 创建对账批次请求 DTO。
/// </summary>
public sealed class CreateReconciliationBatchDto
{
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 账单日期。
/// </summary>
public DateOnly BillDate { get; set; }
/// <summary>
/// 账单类型。
/// </summary>
public ReconciliationBillType BillType { get; set; } = ReconciliationBillType.Combined;
/// <summary>
/// 来源。
/// </summary>
public ReconciliationSource Source { get; set; } = ReconciliationSource.ManualUpload;
/// <summary>
/// 来源名称。
/// </summary>
[StringLength(200)]
public string? SourceName { get; set; }
/// <summary>
/// 来源哈希。
/// </summary>
[Required]
[StringLength(200)]
public string SourceHash { get; set; } = string.Empty;
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CreateReconciliationBatchCommand ToCommand()
@@ -315,17 +573,35 @@ public sealed class CreateReconciliationBatchDto
}
}
/// <summary>
/// 更新对账Issue请求 DTO。
/// </summary>
public sealed class UpdateReconciliationIssueDto
{
/// <summary>
/// 工单 ID。
/// </summary>
[Required]
public Guid IssueId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public ReconciliationIssueStatus Status { get; set; } = ReconciliationIssueStatus.Investigating;
/// <summary>
/// 处理类型。
/// </summary>
public ReconciliationResolutionType ResolutionType { get; set; } = ReconciliationResolutionType.None;
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Note { get; set; }
/// <summary>
/// 分配处理人。
/// </summary>
public Guid? AssignedTo { get; set; }
public UpdateReconciliationIssueCommand ToCommand()
@@ -334,19 +610,37 @@ public sealed class UpdateReconciliationIssueDto
}
}
/// <summary>
/// 预览对账导入请求 DTO。
/// </summary>
public sealed class PreviewReconciliationImportDto
{
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 账单日期。
/// </summary>
public DateOnly BillDate { get; set; }
/// <summary>
/// 账单类型。
/// </summary>
public ReconciliationBillType BillType { get; set; } = ReconciliationBillType.Combined;
/// <summary>
/// 来源名称。
/// </summary>
[Required]
[StringLength(300)]
public string SourceName { get; set; } = string.Empty;
/// <summary>
/// 数据行列表。
/// </summary>
public JsonElement Rows { get; set; } = JsonDefaults.Array();
public PreviewReconciliationImportCommand ToPreviewCommand() => new(Provider, BillDate, BillType, SourceName, Rows);
@@ -354,29 +648,68 @@ public sealed class PreviewReconciliationImportDto
public ImportReconciliationCommand ToImportCommand() => new(Provider, BillDate, BillType, SourceName, Rows);
}
/// <summary>
/// 创建调账凭证请求 DTO。
/// </summary>
public sealed class CreateAdjustmentVoucherDto
{
/// <summary>
/// 工单 ID。
/// </summary>
public Guid? IssueId { get; set; }
/// <summary>
/// 批次 ID。
/// </summary>
public Guid? BatchId { get; set; }
/// <summary>
/// 兑换项 ID。
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// 订单 ID。
/// </summary>
public Guid? OrderId { get; set; }
/// <summary>
/// 支付记录 ID。
/// </summary>
public Guid? PaymentId { get; set; }
/// <summary>
/// 退款申请 ID。
/// </summary>
public Guid? RefundRequestId { get; set; }
/// <summary>
/// 方向。
/// </summary>
public CommerceAdjustmentDirection Direction { get; set; } = CommerceAdjustmentDirection.IncreaseRevenue;
/// <summary>
/// 金额,单位为分。
/// </summary>
[Range(1, int.MaxValue)]
public int AmountCents { get; set; }
/// <summary>
/// 币种。
/// </summary>
[StringLength(10)]
public string? Currency { get; set; }
/// <summary>
/// 原因。
/// </summary>
[Required]
[StringLength(1000)]
public string Reason { get; set; } = string.Empty;
/// <summary>
/// 证明资产键。
/// </summary>
[StringLength(500)]
public string? ProofAssetKey { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public CreateAdjustmentVoucherCommand ToCommand() => new(
@@ -394,56 +727,110 @@ public sealed class CreateAdjustmentVoucherDto
Metadata);
}
/// <summary>
/// 更新调账凭证状态请求 DTO。
/// </summary>
public sealed class UpdateAdjustmentVoucherStatusDto
{
/// <summary>
/// 凭证 ID。
/// </summary>
[Required]
public Guid VoucherId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public CommerceAdjustmentVoucherStatus Status { get; set; } = CommerceAdjustmentVoucherStatus.PendingReview;
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Note { get; set; }
public UpdateAdjustmentVoucherStatusCommand ToCommand() => new(VoucherId, Status, Note);
}
/// <summary>
/// RequestProviderBill任务请求 DTO。
/// </summary>
public sealed class RequestProviderBillJobDto
{
/// <summary>
/// 服务提供方。
/// </summary>
[Required]
[StringLength(50)]
public string Provider { get; set; } = string.Empty;
/// <summary>
/// 账单日期。
/// </summary>
public DateOnly BillDate { get; set; }
/// <summary>
/// 账单类型。
/// </summary>
public ReconciliationBillType BillType { get; set; } = ReconciliationBillType.Combined;
/// <summary>
/// 计划运行时间。
/// </summary>
public DateTimeOffset? RunAfter { get; set; }
public RequestProviderBillJobCommand ToCommand() => new(Provider, BillDate, BillType, RunAfter);
}
/// <summary>
/// 退款通知请求 DTO。
/// </summary>
public sealed class RefundNotificationDto
{
/// <summary>
/// 退款单号。
/// </summary>
[Required]
[StringLength(100)]
public string RefundNo { get; set; } = string.Empty;
/// <summary>
/// 渠道退款单号。
/// </summary>
[StringLength(200)]
public string? ProviderRefundNo { get; set; }
/// <summary>
/// 状态。
/// </summary>
public CommerceRefundStatus Status { get; set; } = CommerceRefundStatus.Succeeded;
/// <summary>
/// 事件 ID。
/// </summary>
[StringLength(200)]
public string? EventId { get; set; }
/// <summary>
/// 任务载荷。
/// </summary>
public JsonElement Payload { get; set; } = JsonDefaults.Object();
public RefundNotificationCommand ToCommand(string provider) => new(provider, RefundNo, ProviderRefundNo, Status, EventId, Payload);
}
/// <summary>
/// 更新积分兑换订单状态请求 DTO。
/// </summary>
public sealed class UpdatePointExchangeOrderStatusDto
{
/// <summary>
/// 订单 ID。
/// </summary>
[Required]
public Guid OrderId { get; set; }
/// <summary>
/// 状态。
/// </summary>
public PointExchangeOrderStatus Status { get; set; }
public UpdatePointExchangeOrderStatusCommand ToCommand()
@@ -452,29 +839,62 @@ public sealed class UpdatePointExchangeOrderStatusDto
}
}
/// <summary>
/// 新增或更新租户优惠券请求 DTO。
/// </summary>
public sealed class UpsertTenantCouponDto
{
/// <summary>
/// ID。
/// </summary>
public Guid? Id { get; set; }
/// <summary>
/// 编码。
/// </summary>
[Required]
[StringLength(100)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 套餐 ID。
/// </summary>
public Guid? PlanId { get; set; }
/// <summary>
/// 优惠类型。
/// </summary>
public DiscountType DiscountType { get; set; } = DiscountType.Fixed;
/// <summary>
/// 优惠值。
/// </summary>
[Range(typeof(decimal), "0.01", "999999")]
public decimal DiscountValue { get; set; }
/// <summary>
/// 有效期开始时间。
/// </summary>
public DateTimeOffset? ValidFrom { get; set; }
/// <summary>
/// 有效期结束时间。
/// </summary>
public DateTimeOffset? ValidTo { get; set; }
/// <summary>
/// 最大使用次数。
/// </summary>
[Range(1, int.MaxValue)]
public int? MaxUses { get; set; }
/// <summary>
/// 来源。
/// </summary>
[StringLength(50)]
public string? Source { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(1000)]
public string? Remark { get; set; }

View File

@@ -12,14 +12,14 @@ namespace Tiku.Api.Contracts;
public sealed class TenantResolveQueryDto
{
/// <summary>
/// 要解析的访问域名,例如 student.example.com。本地开发也可以直接传 localhost
/// 访问域名
/// </summary>
[StringLength(253)]
[Description("要解析的访问域名,例如 student.example.com。本地开发也可以直接传 localhost。")]
public string? Host { get; set; }
/// <summary>
/// 租户编码;本地开发或无独立域名时使用,例如 master
/// 租户编码。
/// </summary>
[StringLength(100)]
[Description("租户编码;本地开发或无独立域名时使用,例如 master。")]
@@ -34,6 +34,9 @@ public sealed class TenantResolveQueryDto
/// <param name="Features">面向前端公开的功能开关。</param>
/// <param name="AdminFeatures">面向管理端公开的功能开关。</param>
/// <param name="PublicConfig">可公开的租户配置,不包含密钥或内部配置。</param>
/// <summary>
/// 租户解析Response请求 DTO。
/// </summary>
public sealed record TenantResolveResponseDto(
PublicTenantDto Tenant,
PublicTenantBrandingDto Branding,
@@ -50,6 +53,9 @@ public sealed record TenantResolveResponseDto(
/// <param name="Status">租户状态。</param>
/// <param name="Mode">租户模式。</param>
/// <param name="Host">当前匹配到的访问域名。</param>
/// <summary>
/// 公开租户请求 DTO。
/// </summary>
public sealed record PublicTenantDto(
Guid Id,
string Slug,
@@ -70,6 +76,9 @@ public sealed record PublicTenantDto(
/// <param name="ServiceAccountName">公众号或服务号名称。</param>
/// <param name="Theme">公开主题配置。</param>
/// <param name="PublicAssets">公开资源配置。</param>
/// <summary>
/// 公开租户品牌请求 DTO。
/// </summary>
public sealed record PublicTenantBrandingDto(
string? BrandName,
string? ShortName,

View File

@@ -5,12 +5,30 @@ using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
/// <summary>
/// 保存租户前端配置草稿请求。
/// </summary>
public sealed class SaveTenantFrontendConfigDraftDto
{
/// <summary>
/// 品牌配置。
/// </summary>
public JsonElement Branding { get; set; } = JsonDefaults.Object();
/// <summary>
/// 主题配置。
/// </summary>
public JsonElement Theme { get; set; } = JsonDefaults.Object();
/// <summary>
/// 功能配置。
/// </summary>
public JsonElement Features { get; set; } = JsonDefaults.Object();
/// <summary>
/// 导航配置。
/// </summary>
public JsonElement Navigation { get; set; } = JsonDefaults.Array();
/// <summary>
/// 首页模块配置。
/// </summary>
public JsonElement HomeModules { get; set; } = JsonDefaults.Array();
public TenantFrontendConfigDraft ToDraft() => new(
@@ -21,8 +39,14 @@ public sealed class SaveTenantFrontendConfigDraftDto
HomeModules);
}
/// <summary>
/// 发布租户前端配置请求。
/// </summary>
public sealed class PublishTenantFrontendConfigDto
{
/// <summary>
/// 期望配置版本。
/// </summary>
[Range(1, int.MaxValue)]
public int ExpectedVersion { get; set; }
}

View File

@@ -4,47 +4,92 @@ using Tiku.Application.Assets;
namespace Tiku.Api.Contracts;
/// <summary>
/// 视频Search查询参数。
/// </summary>
public sealed class VideoSearchQueryDto
{
/// <summary>
/// 关键字。
/// </summary>
[StringLength(100)]
public string? Keyword { get; set; }
/// <summary>
/// 科目 ID。
/// </summary>
public Guid? SubjectId { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 200)]
public int? Limit { get; set; }
public VideoSearchQuery ToQuery() => new(Keyword, SubjectId, Limit);
}
/// <summary>
/// 视频Play请求 DTO。
/// </summary>
public sealed class VideoPlayDto
{
/// <summary>
/// 视频 ID。
/// </summary>
[Required]
public Guid VideoId { get; set; }
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
public VideoPlayCommand ToCommand() => new(VideoId, QuestionId);
}
/// <summary>
/// 视频Progress请求 DTO。
/// </summary>
public sealed class VideoProgressDto
{
/// <summary>
/// 视频 ID。
/// </summary>
[Required]
public Guid VideoId { get; set; }
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 播放位置,单位为秒。
/// </summary>
[Range(0, int.MaxValue)]
public int PositionSeconds { get; set; }
/// <summary>
/// 时长,单位为秒。
/// </summary>
[Range(0, int.MaxValue)]
public int? DurationSeconds { get; set; }
/// <summary>
/// 已观看时长,单位为秒。
/// </summary>
[Range(0, int.MaxValue)]
public int? WatchedSeconds { get; set; }
/// <summary>
/// 是否已完成。
/// </summary>
public bool? IsCompleted { get; set; }
/// <summary>
/// 扩展元数据。
/// </summary>
public JsonElement Metadata { get; set; } = JsonSerializer.SerializeToElement(new { });
public VideoProgressCommand ToCommand()
@@ -60,13 +105,25 @@ public sealed class VideoProgressDto
}
}
/// <summary>
/// 题目视频查询参数。
/// </summary>
public sealed class QuestionVideoQueryDto
{
/// <summary>
/// 题目 ID。
/// </summary>
public Guid? QuestionId { get; set; }
/// <summary>
/// 题目 ID 列表。
/// </summary>
[MaxLength(100)]
public IReadOnlyCollection<Guid>? QuestionIds { get; set; }
/// <summary>
/// 返回数量上限。
/// </summary>
[Range(1, 500)]
public int? Limit { get; set; }

View File

@@ -146,6 +146,7 @@ public sealed class CatalogController(
}
[HttpGet("question-collections")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询可用题集")]
[ProducesResponseType<CatalogList<QuestionCollectionCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -159,6 +160,7 @@ public sealed class CatalogController(
}
[HttpGet("question-collections/questions")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询题集内题目")]
[ProducesResponseType<CatalogList<CollectionQuestionCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
@@ -173,6 +175,7 @@ public sealed class CatalogController(
}
[HttpGet("practice-blueprints")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询练习蓝图")]
[ProducesResponseType<CatalogList<PracticeBlueprintCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -186,6 +189,7 @@ public sealed class CatalogController(
}
[HttpGet("question-banks")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询题库列表")]
[ProducesResponseType<CatalogList<QuestionBankCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -199,6 +203,7 @@ public sealed class CatalogController(
}
[HttpGet("questions")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询已发布题目")]
[EndpointDescription("支持按题库、科目、分类、模块节点、内容入口、内容节点、题集或题目 ID 列表筛选。")]
[ProducesResponseType<CatalogList<QuestionCatalogItem>>(StatusCodes.Status200OK)]
@@ -213,6 +218,7 @@ public sealed class CatalogController(
}
[HttpGet("questions/{questionId:guid}")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询题目详情")]
[ProducesResponseType<QuestionCatalogItem>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -227,6 +233,7 @@ public sealed class CatalogController(
}
[HttpGet("questions/{questionId:guid}/versions")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询题目版本")]
[ProducesResponseType<CatalogList<QuestionVersionCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -241,6 +248,7 @@ public sealed class CatalogController(
}
[HttpGet("vocabulary-units")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("查询词汇单元")]
[ProducesResponseType<CatalogList<VocabularyUnitCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -254,6 +262,7 @@ public sealed class CatalogController(
}
[HttpGet("vocabulary-words")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("查询词汇单词")]
[ProducesResponseType<CatalogList<VocabularyWordCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -267,6 +276,7 @@ public sealed class CatalogController(
}
[HttpGet("handbook-subjects")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询知识手册科目")]
[ProducesResponseType<CatalogList<HandbookSubjectCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -280,6 +290,7 @@ public sealed class CatalogController(
}
[HttpGet("handbook-chapters")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询知识手册章节")]
[ProducesResponseType<CatalogList<HandbookChapterCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -293,6 +304,7 @@ public sealed class CatalogController(
}
[HttpGet("handbook-entries")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询知识手册条目")]
[ProducesResponseType<CatalogList<HandbookEntryCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -345,6 +357,7 @@ public sealed class CatalogController(
}
[HttpGet("video-explanations")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[EndpointSummary("查询视频讲解")]
[ProducesResponseType<CatalogList<VideoExplanationCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -358,6 +371,7 @@ public sealed class CatalogController(
}
[HttpGet("question-videos")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[EndpointSummary("查询题目关联视频")]
[ProducesResponseType<CatalogList<QuestionVideoCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -371,6 +385,7 @@ public sealed class CatalogController(
}
[HttpGet("banners")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("查询首页横幅")]
[ProducesResponseType<CatalogList<BannerCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -384,6 +399,7 @@ public sealed class CatalogController(
}
[HttpGet("faqs")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("查询常见问题")]
[ProducesResponseType<CatalogList<FaqCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -397,6 +413,7 @@ public sealed class CatalogController(
}
[HttpGet("announcements")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("查询公告")]
[ProducesResponseType<CatalogList<AnnouncementCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -410,6 +427,7 @@ public sealed class CatalogController(
}
[HttpGet("exam-dates")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("查询考试日期")]
[ProducesResponseType<CatalogList<ExamDateCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -423,6 +441,7 @@ public sealed class CatalogController(
}
[HttpGet("products")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[EndpointSummary("查询可购买产品")]
[ProducesResponseType<CatalogList<ProductCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
@@ -436,6 +455,7 @@ public sealed class CatalogController(
}
[HttpGet("svip-plans")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[EndpointSummary("查询 SVIP 套餐")]
[ProducesResponseType<CatalogList<SvipPlanCatalogItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]

View File

@@ -13,6 +13,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[Produces("application/json")]
[Route("api/commerce")]
public sealed class CommerceController(

View File

@@ -8,6 +8,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = BackendPermissions.TenantCommissionManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
[Produces("application/json")]
[Route("api/commission")]
public sealed class CommissionController(

View File

@@ -8,6 +8,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Crm)]
[Produces("application/json")]
[Route("api/crm")]
public sealed class CrmController(

View File

@@ -8,6 +8,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[Produces("application/json")]
[Route("api/learning")]
public sealed class LearningController(

View File

@@ -37,8 +37,8 @@ public sealed class PlatformAdminController(
[HttpPost("tenants")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("创建平台租户")]
[ProducesResponseType<PlatformTenantItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantItem>> CreateTenant(
[ProducesResponseType<PlatformTenantProvisioningResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantProvisioningResult>> CreateTenant(
CreatePlatformTenantDto request,
CancellationToken cancellationToken)
{
@@ -78,55 +78,6 @@ public sealed class PlatformAdminController(
return Ok(await platformAdminService.UpsertTenantBillingProfileAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("plans")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询平台 SaaS 套餐")]
[ProducesResponseType<PlatformPlanList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformPlanList>> Plans(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetPlansAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPut("plans/{planCode}/modules")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("替换 SaaS 套餐模块权益")]
[ProducesResponseType<PlatformPlanModuleEntitlements>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformPlanModuleEntitlements>> ReplacePlanModules(
string planCode,
ReplacePlatformPlanModulesDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.ReplacePlanModulesAsync(
ResolveActor(), request.ToCommand(planCode), cancellationToken));
}
[HttpPost("subscriptions")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("创建或调整租户订阅")]
[ProducesResponseType<PlatformTenantSubscriptionItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantSubscriptionItem>> UpsertSubscription(
UpsertPlatformSubscriptionDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertSubscriptionAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPut("tenants/{tenantId:guid}/module-overrides/{moduleCode}")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("设置租户模块覆盖")]
[ProducesResponseType<PlatformTenantModuleOverrideItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformTenantModuleOverrideItem>> UpsertTenantModuleOverride(
Guid tenantId,
string moduleCode,
UpsertPlatformTenantModuleOverrideDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertTenantModuleOverrideAsync(
ResolveActor(), request.ToCommand(tenantId, moduleCode), cancellationToken));
}
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
[EndpointSummary("查询租户域名状态")]
@@ -215,70 +166,70 @@ public sealed class PlatformAdminController(
return Ok(await platformAdminService.UpdateAuditAlertStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("dunning-notification-channels")]
[HttpGet("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知渠道")]
[ProducesResponseType<PlatformDunningChannelList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningChannelList>> DunningChannels(
[ProducesResponseType<PlatformBillingDunningChannelList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelList>> BillingDunningChannels(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetDunningChannelsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
return Ok(await platformAdminService.GetBillingDunningChannelsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpPut("dunning-notification-channels")]
[HttpPut("saas/dunning/channels")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("创建或更新平台催缴通知渠道")]
[ProducesResponseType<PlatformDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningChannelItem>> UpsertDunningChannel(
UpsertPlatformDunningChannelDto request,
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> UpsertBillingDunningChannel(
UpsertPlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.UpsertDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
return Ok(await platformAdminService.UpsertBillingDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("dunning-notification-channels/disable")]
[HttpPost("saas/dunning/channels/disable")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("禁用平台催缴通知渠道")]
[ProducesResponseType<PlatformDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningChannelItem>> DisableDunningChannel(
DisablePlatformDunningChannelDto request,
[ProducesResponseType<PlatformBillingDunningChannelItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningChannelItem>> DisableBillingDunningChannel(
DisablePlatformBillingDunningChannelDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.DisableDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
return Ok(await platformAdminService.DisableBillingDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpGet("dunning-notification-events")]
[HttpGet("saas/dunning/events")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件")]
[ProducesResponseType<PlatformDunningEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningEventList>> DunningEvents(
[ProducesResponseType<PlatformBillingDunningEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventList>> BillingDunningEvents(
[FromQuery] PlatformAdminQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetDunningEventsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
return Ok(await platformAdminService.GetBillingDunningEventsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
}
[HttpGet("dunning-notification-events/detail")]
[HttpGet("saas/dunning/events/detail")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("查询平台催缴通知事件详情")]
[ProducesResponseType<PlatformDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningEventItem>> DunningEventDetail(
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> BillingDunningEventDetail(
[FromQuery] Guid eventId,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.GetDunningEventDetailAsync(ResolveActor(), eventId, cancellationToken));
return Ok(await platformAdminService.GetBillingDunningEventDetailAsync(ResolveActor(), eventId, cancellationToken));
}
[HttpPost("dunning-notification-events/retry")]
[HttpPost("saas/dunning/events/retry")]
[Authorize(Policy = BackendPermissions.PlatformBillingNotification)]
[EndpointSummary("重新标记平台催缴通知事件待发送")]
[ProducesResponseType<PlatformDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformDunningEventItem>> RetryDunningEvent(
RetryPlatformDunningEventDto request,
[ProducesResponseType<PlatformBillingDunningEventItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<PlatformBillingDunningEventItem>> RetryBillingDunningEvent(
RetryPlatformBillingDunningEventDto request,
CancellationToken cancellationToken)
{
return Ok(await platformAdminService.RetryDunningEventAsync(ResolveActor(), request.ToCommand(), cancellationToken));
return Ok(await platformAdminService.RetryBillingDunningEventAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
private PlatformAdminActor ResolveActor()

View File

@@ -0,0 +1,45 @@
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Application.PlatformBilling;
namespace Tiku.Api.Controllers;
[ApiController]
[Route("api/platform-billing/callbacks")]
public sealed class PlatformBillingCallbackController(
IPlatformBillingNotificationService notificationService) : ControllerBase
{
[AllowAnonymous]
[HttpPost("{provider}")]
public async Task<IActionResult> Notify(string provider, CancellationToken cancellationToken)
{
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync(cancellationToken);
Request.Body.Position = 0;
JsonElement body;
try
{
body = string.IsNullOrWhiteSpace(rawBody)
? JsonDocument.Parse("{}").RootElement.Clone()
: JsonDocument.Parse(rawBody).RootElement.Clone();
}
catch (JsonException)
{
body = JsonDocument.Parse("{}").RootElement.Clone();
}
var normalizedProvider = provider.Trim().ToLowerInvariant().Replace('-', '_');
await notificationService.ProcessAsync(new PlatformBillingNotification(
normalizedProvider,
Request.Headers.ToDictionary(value => value.Key, value => value.Value.ToString(), StringComparer.OrdinalIgnoreCase),
rawBody,
body), cancellationToken);
if (normalizedProvider is "alipay" or "ali_pay")
{
return Content("success", "text/plain", Encoding.UTF8);
}
return Ok(new { code = "SUCCESS", message = "成功" });
}
}

View File

@@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Route("api/platform-admin/saas")]
[Authorize(Policy = TikuPolicies.PlatformBackofficeBootstrap)]
public sealed class PlatformSaasController(
ISaasCatalogAdminService catalogService,
IPlatformBillingAdminService billingService,
ICurrentUser currentUser) : ControllerBase
{
[HttpGet("catalog")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasCatalogSnapshot> Catalog(CancellationToken cancellationToken) =>
catalogService.GetCatalogAsync(Actor(), cancellationToken);
[HttpPut("features")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasFeature> UpsertFeature(UpsertSaasFeatureDto request, CancellationToken cancellationToken) =>
catalogService.UpsertFeatureAsync(Actor(), request.ToCommand(), cancellationToken);
[HttpPut("feature-limits")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasFeatureLimitDefinition> UpsertFeatureLimit(
UpsertSaasFeatureLimitDto request,
CancellationToken cancellationToken) =>
catalogService.UpsertLimitDefinitionAsync(Actor(), request.ToCommand(), cancellationToken);
[HttpPut("offerings")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasOffering> UpsertOffering(UpsertSaasOfferingDto request, CancellationToken cancellationToken) =>
catalogService.UpsertOfferingAsync(Actor(), request.ToCommand(), cancellationToken);
[HttpPut("offering-versions")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasOfferingVersionItem> UpsertVersion(UpsertSaasOfferingVersionDto request, CancellationToken cancellationToken) =>
catalogService.UpsertDraftVersionAsync(Actor(), request.ToCommand(), cancellationToken);
[HttpPost("offering-versions/{versionId:guid}/publish")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasOfferingVersionItem> PublishVersion(Guid versionId, CancellationToken cancellationToken) =>
catalogService.PublishVersionAsync(Actor(), versionId, cancellationToken);
[HttpPost("offering-versions/{versionId:guid}/clone")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasOfferingVersionItem> CloneVersion(Guid versionId, CancellationToken cancellationToken) =>
catalogService.CloneVersionAsync(Actor(), versionId, cancellationToken);
[HttpPost("offering-versions/{versionId:guid}/retire")]
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
public Task<SaasOfferingVersionItem> RetireVersion(Guid versionId, CancellationToken cancellationToken) =>
catalogService.RetireVersionAsync(Actor(), versionId, cancellationToken);
[HttpGet("orders")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<IReadOnlyCollection<PlatformBillingOrder>> Orders(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
billingService.GetOrdersAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
[HttpGet("payments")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<IReadOnlyCollection<PlatformBillingPayment>> Payments(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
billingService.GetPaymentsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
[HttpGet("refunds")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<IReadOnlyCollection<PlatformBillingRefund>> Refunds(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
billingService.GetRefundsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
[HttpGet("invoices")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
billingService.GetInvoicesAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
[HttpGet("subscriptions")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<IReadOnlyCollection<TenantSaasSubscription>> Subscriptions(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
billingService.GetSubscriptionsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
[HttpPost("payments/manual/confirm")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<PlatformBillingPayment> ConfirmManualPayment(ConfirmManualPlatformPaymentDto request, CancellationToken cancellationToken) =>
billingService.ConfirmManualPaymentAsync(Actor(), request.ToCommand(), cancellationToken);
[HttpPut("tenant-feature-overrides")]
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
public Task<TenantFeatureOverride> UpsertFeatureOverride(UpsertTenantFeatureOverrideDto request, CancellationToken cancellationToken) =>
billingService.UpsertFeatureOverrideAsync(Actor(), request.ToCommand(), cancellationToken);
private SaasCatalogActor Actor() => currentUser.UserId is { } userId
? new SaasCatalogActor(userId)
: throw new PlatformBillingException("Platform actor was not resolved.", "platform_access_denied");
}

View File

@@ -8,6 +8,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[Produces("application/json")]
[Route("api/points")]
public sealed class PointsController(

View File

@@ -108,6 +108,7 @@ public sealed class ProfileController(
}
[HttpPost("check-in")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[EndpointSummary("每日签到")]
[ProducesResponseType<CheckInResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<CheckInResult>> CheckIn(CancellationToken cancellationToken)
@@ -116,6 +117,7 @@ public sealed class ProfileController(
}
[HttpGet("score-events")]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[EndpointSummary("查询积分流水")]
[ProducesResponseType<ProfileScoreEventList>(StatusCodes.Status200OK)]
public async Task<ActionResult<ProfileScoreEventList>> ScoreEvents(

View File

@@ -9,6 +9,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[Produces("application/json")]
[Route("api/questions/videos")]
public sealed class QuestionVideosController(

View File

@@ -11,6 +11,7 @@ using Tiku.Infrastructure.Persistence;
namespace Tiku.Api.Controllers;
[ApiController]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
[Produces("application/json")]
[Route("api/referral")]
public sealed class ReferralController(

View File

@@ -12,6 +12,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[AllowAnonymous]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[Produces("application/json")]
[Route("api/scoreline")]
public sealed class ScorelineController(

View File

@@ -15,6 +15,7 @@ public sealed class TaxonomyController(
ITaxonomyService taxonomyService) : ControllerBase
{
[HttpGet]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
[EndpointSummary("查询租户分类节点")]
public Task<IReadOnlyCollection<TaxonomyNodeItem>> List(CancellationToken cancellationToken)
{
@@ -22,6 +23,7 @@ public sealed class TaxonomyController(
}
[HttpPost]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[EndpointSummary("创建租户分类节点")]
public Task<TaxonomyNodeItem> Create(

View File

@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Api.Controllers;
[ApiController]
[Route("api/tenant-billing")]
[Authorize(Policy = BackendPermissions.TenantBillingManage)]
public sealed class TenantBillingController(
ITenantBillingService billingService,
ICurrentAccessContext accessContext) : ControllerBase
{
[HttpGet("catalog")]
public async Task<TenantBillingCatalog> Catalog(CancellationToken cancellationToken) =>
await billingService.GetCatalogAsync(await ActorAsync(cancellationToken), cancellationToken);
[HttpPost("quotes")]
public async Task<PlatformBillingQuoteView> Quote(CreatePlatformBillingQuoteDto request, CancellationToken cancellationToken) =>
await billingService.CreateQuoteAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
[HttpPost("orders")]
public async Task<PlatformBillingOrderView> CreateOrder(CreatePlatformBillingOrderDto request, CancellationToken cancellationToken) =>
await billingService.CreateOrderAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
[HttpPost("orders/{orderNo}/payments")]
public async Task<PlatformBillingPaymentView> CreatePayment(string orderNo, CreatePlatformBillingPaymentDto request, CancellationToken cancellationToken) =>
await billingService.CreatePaymentAsync(await ActorAsync(cancellationToken), request.ToCommand(orderNo), cancellationToken);
[HttpGet("orders")]
public async Task<IReadOnlyCollection<PlatformBillingOrderView>> Orders(int limit = 100, CancellationToken cancellationToken = default) =>
await billingService.GetOrdersAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
[HttpGet("orders/{orderNo}")]
public async Task<PlatformBillingOrderView> Order(string orderNo, CancellationToken cancellationToken) =>
await billingService.GetOrderAsync(await ActorAsync(cancellationToken), orderNo, cancellationToken);
[HttpGet("subscription")]
public async Task<TenantSubscriptionView?> Subscription(CancellationToken cancellationToken) =>
await billingService.GetSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
[HttpPost("subscription/change")]
public async Task<PlatformBillingOrderView> Change(ChangeTenantSubscriptionDto request, CancellationToken cancellationToken) =>
await billingService.ChangeSubscriptionAsync(await ActorAsync(cancellationToken), request.ToCommand(), request.IdempotencyKey, cancellationToken);
[HttpPost("subscription/renew")]
public async Task<PlatformBillingOrderView> Renew(IdempotentTenantBillingDto request, CancellationToken cancellationToken) =>
await billingService.RenewSubscriptionAsync(await ActorAsync(cancellationToken), request.IdempotencyKey, cancellationToken);
[HttpPost("subscription/cancel")]
public async Task<TenantSubscriptionView> Cancel(CancellationToken cancellationToken) =>
await billingService.CancelSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
[HttpGet("usage")]
public async Task<IReadOnlyCollection<FeatureQuotaSnapshot>> Usage(CancellationToken cancellationToken) =>
await billingService.GetUsageAsync(await ActorAsync(cancellationToken), cancellationToken);
[HttpGet("invoices")]
public async Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(int limit = 100, CancellationToken cancellationToken = default) =>
await billingService.GetInvoicesAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
private async Task<TenantBillingActor> ActorAsync(CancellationToken cancellationToken)
{
var access = await accessContext.GetAsync(cancellationToken);
return access.UserId is { } userId && access.TenantId is { } tenantId && access.IsCurrentTenantMember
? new TenantBillingActor(userId, tenantId)
: throw new PlatformBillingException("Tenant billing actor was not resolved.", "tenant_access_denied");
}
}

View File

@@ -10,6 +10,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = BackendPermissions.TenantCommerceOperate)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
[Produces("application/json")]
[Route("api/tenant-commerce")]
public sealed class TenantCommerceController(

View File

@@ -235,6 +235,20 @@ public sealed class TenantContentController(
cancellationToken));
}
[HttpDelete("assets/{assetId:guid}")]
[EndpointSummary("归档内容资产")]
[ProducesResponseType<ContentManagementResult<ContentAssetManagementItem>>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ContentManagementResult<ContentAssetManagementItem>>> ArchiveAsset(
Guid assetId,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.ArchiveAssetAsync(
ResolveActor(),
assetId,
cancellationToken));
}
[HttpPost("assets/sign-download")]
[EndpointSummary("签发管理侧资产下载地址")]
[ProducesResponseType<AssetManagementSignedAccessResult>(StatusCodes.Status200OK)]

View File

@@ -12,7 +12,6 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[Produces("application/json")]
[Route("api/tenant-content")]
public sealed class TenantContentDirectController(
@@ -22,7 +21,9 @@ public sealed class TenantContentDirectController(
ITenantContext currentTenant) : ControllerBase
{
[HttpPost("questions")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
[EndpointSummary("创建题目及首个版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
@@ -33,7 +34,9 @@ public sealed class TenantContentDirectController(
}
[HttpPatch("questions")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
[EndpointSummary("更新题目并可选择创建新版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
@@ -44,6 +47,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("vocabulary-units")]
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("查询管理侧词汇单元")]
[ProducesResponseType<CatalogList<VocabularyUnit>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VocabularyUnit>>> GetVocabularyUnits(
@@ -54,6 +59,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("vocabulary-units")]
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("新增或更新词汇单元")]
[ProducesResponseType<ContentManagementResult<VocabularyUnit>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VocabularyUnit>>> UpsertVocabularyUnit(
@@ -64,7 +71,9 @@ public sealed class TenantContentDirectController(
}
[HttpGet("vocabulary-words")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("查询管理侧词汇")]
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
@@ -75,7 +84,9 @@ public sealed class TenantContentDirectController(
}
[HttpPut("vocabulary-words")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
[EndpointSummary("新增或更新词汇")]
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
@@ -86,6 +97,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("handbook-subjects")]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询管理侧知识手册科目")]
[ProducesResponseType<CatalogList<HandbookSubject>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookSubject>>> GetHandbookSubjects(
@@ -96,6 +109,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("handbook-subjects")]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("新增或更新知识手册科目")]
[ProducesResponseType<ContentManagementResult<HandbookSubject>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookSubject>>> UpsertHandbookSubject(
@@ -106,7 +121,9 @@ public sealed class TenantContentDirectController(
}
[HttpGet("handbook-chapters")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询管理侧知识手册章节")]
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
@@ -117,7 +134,9 @@ public sealed class TenantContentDirectController(
}
[HttpPut("handbook-chapters")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("新增或更新知识手册章节")]
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
@@ -128,7 +147,9 @@ public sealed class TenantContentDirectController(
}
[HttpGet("handbook-entries")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("查询管理侧知识手册条目")]
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
@@ -139,7 +160,9 @@ public sealed class TenantContentDirectController(
}
[HttpPut("handbook-entries")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
[EndpointSummary("新增或更新知识手册条目")]
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
@@ -150,6 +173,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/schools")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询管理侧分数线院校")]
[ProducesResponseType<CatalogList<School>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<School>>> GetSchools(
@@ -160,6 +185,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("scoreline/schools")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("新增或更新分数线院校")]
[ProducesResponseType<ContentManagementResult<School>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<School>>> UpsertSchool(
@@ -170,6 +197,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/majors")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询管理侧分数线专业")]
[ProducesResponseType<CatalogList<Major>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<Major>>> GetMajors(
@@ -180,6 +209,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("scoreline/majors")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("新增或更新分数线专业")]
[ProducesResponseType<ContentManagementResult<Major>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<Major>>> UpsertMajor(
@@ -190,6 +221,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/fields")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询管理侧分数线字段")]
[ProducesResponseType<CatalogList<ScorelineField>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ScorelineField>>> GetScorelineFields(
@@ -200,6 +233,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("scoreline/fields")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("新增或更新动态分数线字段")]
[ProducesResponseType<ContentManagementResult<ScorelineField>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<ScorelineField>>> UpsertScorelineField(
@@ -210,6 +245,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/records")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询管理侧分数线记录")]
[ProducesResponseType<CatalogList<ScorelineRecord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ScorelineRecord>>> GetScorelineRecords(
@@ -220,6 +257,8 @@ public sealed class TenantContentDirectController(
}
[HttpPut("scoreline/records")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("新增或更新分数线记录")]
[ProducesResponseType<ContentManagementResult<ScorelineRecord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<ScorelineRecord>>> UpsertScorelineRecord(
@@ -230,6 +269,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/years")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询分数线年份")]
[ProducesResponseType<CatalogList<int>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<int>>> GetScorelineYears(
@@ -240,6 +281,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("scoreline/trend")]
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
[EndpointSummary("查询分数线趋势摘要")]
[ProducesResponseType<CatalogList<ScorelineTrendItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ScorelineTrendItem>>> GetScorelineTrend(
@@ -250,7 +293,9 @@ public sealed class TenantContentDirectController(
}
[HttpGet("videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[EndpointSummary("查询租户视频解析")]
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
@@ -261,7 +306,9 @@ public sealed class TenantContentDirectController(
}
[HttpPut("videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[EndpointSummary("新增或更新视频解析")]
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
@@ -272,7 +319,9 @@ public sealed class TenantContentDirectController(
}
[HttpPost("question-videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[EndpointSummary("绑定题目与解析视频")]
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
@@ -283,7 +332,9 @@ public sealed class TenantContentDirectController(
}
[HttpGet("operations/{kind}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("查询运营内容")]
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
@@ -295,7 +346,9 @@ public sealed class TenantContentDirectController(
}
[HttpPut("operations/{kind}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
[EndpointSummary("新增或更新运营内容")]
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
@@ -307,7 +360,9 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/preview/{importType}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")]
[EndpointSummary("预览内容导入数据")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
@@ -319,7 +374,9 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/{importType}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")]
[EndpointSummary("执行或排队内容导入")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
[ProducesResponseType<BackgroundJobItem>(StatusCodes.Status202Accepted)]
@@ -359,7 +416,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("imports/detail")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[EndpointSummary("查询内容导入任务详情")]
[ProducesResponseType<ContentImportJobDetail>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentImportJobDetail>> GetImportDetail(
@@ -370,7 +428,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("imports/issues")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[EndpointSummary("查询内容导入问题明细")]
[ProducesResponseType<CatalogList<ContentImportIssueModel>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentImportIssueModel>>> GetImportIssues(
@@ -381,7 +440,8 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/post-check")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[EndpointSummary("执行内容导入后完整性检查")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> RunImportPostCheck(
@@ -392,7 +452,8 @@ public sealed class TenantContentDirectController(
}
[HttpGet("imports/post-check")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
[EndpointSummary("查询内容导入后检查状态")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> GetImportPostCheck(

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
namespace Tiku.Api.Controllers;
[ApiController]
[Route("api/tenant-onboarding")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
public sealed class TenantOnboardingController(
ITenantOnboardingService onboardingService,
ICurrentAccessContext accessContext) : ControllerBase
{
[HttpGet("status")]
public async Task<TenantOnboardingStatus> Status(CancellationToken cancellationToken)
{
var access = await accessContext.GetAsync(cancellationToken);
if (access.TenantId is not { } tenantId || !access.IsCurrentTenantMember)
{
throw new TenantExternalProviderException("Tenant onboarding access is denied.", "tenant_access_denied");
}
return await onboardingService.GetStatusAsync(tenantId, cancellationToken);
}
}

View File

@@ -9,6 +9,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
[Produces("application/json")]
[Route("api/videos")]
public sealed class VideosController(

View File

@@ -8,6 +8,7 @@ using Tiku.Application.Content;
using Tiku.Application.Growth;
using Tiku.Application.Points;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.PlatformBilling;
using Tiku.Application.QuestionBanks;
using Tiku.Application.Storage;
using Tiku.Infrastructure.Content;
@@ -56,6 +57,18 @@ public sealed class ExceptionHandlingMiddleware(
return;
}
if (exception is FeatureAccessException featureAccessException)
{
await WriteProblemAsync(
context,
featureAccessException.Message,
featureAccessException.Code == "feature_quota_exhausted"
? StatusCodes.Status409Conflict
: StatusCodes.Status403Forbidden,
featureAccessException.Code);
return;
}
if (exception is TenantContextConflictException)
{
await WriteProblemAsync(
@@ -262,6 +275,16 @@ public sealed class ExceptionHandlingMiddleware(
return;
}
if (exception is PlatformBillingException platformBillingException)
{
await WriteProblemAsync(
context,
platformBillingException.Message,
PlatformBillingStatusCode(platformBillingException.Code),
platformBillingException.Code);
return;
}
if (exception is CommerceException commerceException)
{
await WriteProblemAsync(
@@ -518,6 +541,22 @@ public sealed class ExceptionHandlingMiddleware(
};
}
private static int PlatformBillingStatusCode(string code)
{
return code switch
{
"tenant_not_found" => StatusCodes.Status404NotFound,
"platform_billing_public_url_missing" or "payment_provider_not_configured" or
"payment_secret_not_configured" => StatusCodes.Status503ServiceUnavailable,
"saas_offering_version_immutable" or "saas_offering_version_status_invalid" or
"platform_billing_quote_expired" or "platform_billing_order_status_invalid" or
"platform_billing_payment_status_invalid" or "platform_billing_payment_amount_mismatch" =>
StatusCodes.Status409Conflict,
_ when code.EndsWith("_not_found", StringComparison.Ordinal) => StatusCodes.Status404NotFound,
_ => StatusCodes.Status400BadRequest
};
}
private static int PointStatusCode(string code)
{
return code switch

View File

@@ -76,7 +76,7 @@ internal sealed class CurrentAccessAuthorizationHandler(ICurrentAccessContext ac
internal sealed class TenantPermissionAuthorizationHandler(
ICurrentAccessContext accessContext,
ICapabilityAccessEvaluator capabilityAccessEvaluator,
IFeatureAccessService featureAccessService,
IHttpContextAccessor httpContextAccessor) :
AuthorizationHandler<TenantPermissionRequirement>
{
@@ -90,24 +90,18 @@ internal sealed class TenantPermissionAuthorizationHandler(
}
var access = await accessContext.GetAsync();
var moduleCode = ResolveModuleCode(requirement.PermissionCode);
var operation = IsSafeMethod(httpContextAccessor.HttpContext?.Request.Method)
? CapabilityOperation.Read
: CapabilityOperation.Write;
? FeatureAccessOperation.Read
: FeatureAccessOperation.Write;
if (access.TenantId is { } tenantId &&
access.HasTenantPermission(requirement.PermissionCode) &&
await capabilityAccessEvaluator.IsAllowedAsync(tenantId, moduleCode, operation))
(await featureAccessService.FilterPermissionCodesAsync(
tenantId, [requirement.PermissionCode], operation)).Contains(requirement.PermissionCode))
{
context.Succeed(requirement);
}
}
private static string ResolveModuleCode(string permissionCode)
{
var parts = permissionCode.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length >= 2 ? parts[1].ToLowerInvariant() : permissionCode.ToLowerInvariant();
}
private static bool IsSafeMethod(string? method) =>
method is not null &&
(HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method));
@@ -197,7 +191,7 @@ public static class AccessAuthorizationServiceCollectionExtensions
public static IServiceCollection AddTikuRbacAuthorization(this IServiceCollection services)
{
services.AddHttpContextAccessor();
services.TryAddScoped<ICapabilityAccessEvaluator, CompatibilityCapabilityAccessEvaluator>();
services.TryAddScoped<IFeatureAccessService, CompatibilityFeatureAccessService>();
services.AddScoped<IAuthorizationHandler, CurrentAccessAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, CurrentPlatformAccessAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, TenantPermissionAuthorizationHandler>();
@@ -241,6 +235,13 @@ public static class AccessAuthorizationServiceCollectionExtensions
new CurrentTenantMemberRequirement(),
new TenantPermissionRequirement(BackendPermissions.TenantContentManage),
new AllDataScopeRequirement()));
options.AddPolicy(
TikuPolicies.TenantAllDataScope,
policy => policy
.RequireAuthenticatedUser()
.AddRequirements(
new CurrentTenantMemberRequirement(),
new AllDataScopeRequirement()));
options.AddPolicy(
TikuPolicies.TenantCommerceOperateAllScope,
policy => policy
@@ -275,17 +276,21 @@ public static class AccessAuthorizationServiceCollectionExtensions
}
}
internal sealed class CompatibilityCapabilityAccessEvaluator : ICapabilityAccessEvaluator
internal sealed class CompatibilityFeatureAccessService : IFeatureAccessService
{
public Task<bool> IsAllowedAsync(
Guid tenantId,
string moduleCode,
CapabilityOperation operation,
CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<FeatureAccessDecision> EvaluateAsync(Guid tenantId, string featureCode, FeatureAccessOperation operation, CancellationToken cancellationToken = default) =>
Task.FromResult(new FeatureAccessDecision(true, null, featureCode, operation));
public Task<IReadOnlySet<string>> GetEnabledModulesAsync(
Guid tenantId,
CapabilityOperation operation = CapabilityOperation.Read,
CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlySet<string>>(new HashSet<string>(StringComparer.Ordinal));
public Task<IReadOnlySet<string>> GetEnabledFeaturesAsync(Guid tenantId, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlySet<string>>(new HashSet<string>(SaasFeatureCatalog.All, StringComparer.Ordinal));
public Task<IReadOnlySet<string>> FilterPermissionCodesAsync(Guid tenantId, IEnumerable<string> permissionCodes, FeatureAccessOperation operation = FeatureAccessOperation.Read, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlySet<string>>(permissionCodes.ToHashSet(StringComparer.Ordinal));
public Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetQuotaSummaryAsync(Guid tenantId, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyCollection<FeatureQuotaSnapshot>>([]);
public Task<bool> TryConsumeQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task ReleaseQuotaAsync(Guid tenantId, string metricCode, long amount, CancellationToken cancellationToken = default) => Task.CompletedTask;
}

View File

@@ -8,6 +8,7 @@ namespace Tiku.Api.Security;
public sealed record EndpointAuthorizationMetadata(
string Realm,
string? Module,
IReadOnlyList<string> RequiredFeatures,
string? Permission,
CapabilityOperation Operation,
bool RequiresAllDataScope,
@@ -43,7 +44,20 @@ internal sealed class EndpointAuthorizationMetadataConvention : IApplicationMode
policies.Any(policy => policy.StartsWith("tenant", StringComparison.Ordinal))
? "tenant"
: "authenticated";
var module = permission is null ? null : ResolveModule(permission);
var module = permission is null ? null : PermissionModuleCatalog.ResolvePermissionModuleCode(permission);
var requiredFeatures = controller.Attributes.OfType<RequireSaasFeatureAttribute>()
.Concat(action.Attributes.OfType<RequireSaasFeatureAttribute>())
.Select(attribute => attribute.FeatureCode)
.Concat(module is not null &&
PermissionModuleCatalog.RequiredFeatures.TryGetValue(module, out var moduleFeature) &&
moduleFeature is not null
? [moduleFeature]
: [])
.Distinct(StringComparer.Ordinal)
.Order(StringComparer.Ordinal)
.Concat(action.Attributes.OfType<RequireSaasFeatureFromRouteAttribute>()
.Select(attribute => $"route:{attribute.RouteValueName}"))
.ToArray();
var httpMethods = action.Attributes.OfType<HttpMethodAttribute>()
.SelectMany(attribute => attribute.HttpMethods)
.Distinct(StringComparer.OrdinalIgnoreCase)
@@ -55,9 +69,11 @@ internal sealed class EndpointAuthorizationMetadataConvention : IApplicationMode
var metadata = new EndpointAuthorizationMetadata(
realm,
module,
requiredFeatures,
permission,
operation,
policies.Contains(TikuPolicies.TenantContentManageAllScope, StringComparer.Ordinal) ||
policies.Contains(TikuPolicies.TenantAllDataScope, StringComparer.Ordinal) ||
policies.Contains(TikuPolicies.TenantCommerceOperateAllScope, StringComparer.Ordinal),
$"{string.Join(',', httpMethods.Order(StringComparer.Ordinal))}:{route}");
foreach (var selector in action.Selectors)
@@ -68,12 +84,6 @@ internal sealed class EndpointAuthorizationMetadataConvention : IApplicationMode
}
}
private static string ResolveModule(string permission)
{
var parts = permission.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length >= 2 ? parts[1].ToLowerInvariant() : permission.ToLowerInvariant();
}
private static bool IsSafeMethod(string method) =>
HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method);
}

View File

@@ -0,0 +1,113 @@
using Microsoft.AspNetCore.Mvc;
using Tiku.Application.Security;
namespace Tiku.Api.Security;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public sealed class RequireSaasFeatureAttribute(string featureCode) : Attribute
{
public string FeatureCode { get; } = featureCode;
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public sealed class RequireSaasFeatureFromRouteAttribute(string routeValueName) : Attribute
{
public string RouteValueName { get; } = routeValueName;
}
public sealed class SaasFeatureAccessMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(
HttpContext context,
ITenantContext tenantContext,
IFeatureAccessService featureAccessService)
{
var requirements = context.GetEndpoint()?.Metadata.GetOrderedMetadata<RequireSaasFeatureAttribute>();
var routeRequirements = context.GetEndpoint()?.Metadata.GetOrderedMetadata<RequireSaasFeatureFromRouteAttribute>();
if ((requirements is null || requirements.Count == 0) &&
(routeRequirements is null || routeRequirements.Count == 0))
{
await next(context);
return;
}
if (!tenantContext.IsResolved || tenantContext.IsSystem || tenantContext.TenantId is not { } tenantId)
{
// Some platform-hosted public entry points resolve their tenant from a
// signed code inside the application service. They must keep their
// existing 404 semantics when no request tenant has been established.
await next(context);
return;
}
var operation = HttpMethods.IsGet(context.Request.Method) ||
HttpMethods.IsHead(context.Request.Method) ||
HttpMethods.IsOptions(context.Request.Method)
? FeatureAccessOperation.Read
: FeatureAccessOperation.Write;
foreach (var requirement in (requirements ?? []).DistinctBy(value => value.FeatureCode, StringComparer.Ordinal))
{
var decision = await featureAccessService.EvaluateAsync(
tenantId,
requirement.FeatureCode,
operation,
context.RequestAborted);
if (!decision.Allowed)
{
await WriteForbiddenAsync(
context,
decision.DenialCode ?? "feature_not_available",
$"The SaaS feature '{requirement.FeatureCode}' is not available.");
return;
}
}
foreach (var requirement in routeRequirements ?? [])
{
var routeValue = context.Request.RouteValues[requirement.RouteValueName]?.ToString();
var featureCode = ResolveRouteFeature(routeValue);
if (featureCode is null)
{
await WriteForbiddenAsync(context, "feature_route_value_invalid", "The requested content module is not available.");
return;
}
var decision = await featureAccessService.EvaluateAsync(
tenantId,
featureCode,
operation,
context.RequestAborted);
if (!decision.Allowed)
{
await WriteForbiddenAsync(
context,
decision.DenialCode ?? "feature_not_available",
$"The SaaS feature '{featureCode}' is not available.");
return;
}
}
await next(context);
}
private static Task WriteForbiddenAsync(HttpContext context, string code, string title)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Title = title,
Instance = context.Request.Path,
Extensions =
{
["code"] = code,
["traceId"] = context.TraceIdentifier
}
});
}
internal static string? ResolveRouteFeature(string? routeValue)
{
return SaasFeatureCatalog.ResolveContentImportFeature(routeValue);
}
}

View File

@@ -86,9 +86,9 @@
['P-08', 'PlatformAdminBillingController_confirmPayment', 'POST', '/api/platform-admin/invoices/payments/manual-confirm'],
['P-08', 'PlatformAdminBillingController_processOverdue', 'POST', '/api/platform-admin/invoices/process-overdue'],
['P-08', 'PlatformAdminBillingController_reminders', 'GET', '/api/platform-admin/invoices/reminders'],
['P-08', 'PlatformAdminDunningChannelsController_channels', 'GET', '/api/platform-admin/dunning-notification-channels'],
['P-08', 'PlatformAdminDunningChannelsController_upsertChannel', 'PUT', '/api/platform-admin/dunning-notification-channels'],
['P-08', 'PlatformAdminDunningEventsController_events', 'GET', '/api/platform-admin/dunning-notification-events'],
['P-08', 'PlatformAdminDunningChannelsController_channels', 'GET', '/api/platform-admin/saas/dunning/channels'],
['P-08', 'PlatformAdminDunningChannelsController_upsertChannel', 'PUT', '/api/platform-admin/saas/dunning/channels'],
['P-08', 'PlatformAdminDunningEventsController_events', 'GET', '/api/platform-admin/saas/dunning/events'],
['P-09', 'PlatformAdminQuestionBanksController_banks', 'GET', '/api/platform-admin/question-banks'],
['P-09', 'PlatformAdminQuestionBanksController_grants', 'GET', '/api/platform-admin/question-bank-grants'],
['P-09', 'PlatformAdminQuestionBanksController_upsertGrant', 'PUT', '/api/platform-admin/question-bank-grants'],