feat: add referral management endpoints

This commit is contained in:
xiong
2026-07-26 20:22:09 +08:00
parent 9fff468bd7
commit ecfb57bd30
5 changed files with 724 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.Json;
using Tiku.Application.Growth;
@@ -101,3 +102,102 @@ public sealed class ReferralQrcodeDto
return new ReferralQrcodeCommand(Page, Scene, Provider, QrcodeUrl, Metadata);
}
}
public sealed class ReferralStatsQueryDto
{
public Guid? ReferrerUserId { get; set; }
[Range(1, 500)]
public int? Limit { get; set; }
public ReferralStatsQuery ToQuery()
{
return new ReferralStatsQuery(ReferrerUserId, Limit);
}
}
public sealed class ReferralConversionQueryDto
{
public Guid? ReferrerUserId { get; set; }
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? StartDate { get; set; }
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
public string? EndDate { get; set; }
[Range(1, 365)]
public int? Days { get; set; }
[Range(1, 100)]
public int? Limit { get; set; }
public ReferralConversionQuery ToQuery()
{
return new ReferralConversionQuery(
ReferrerUserId,
ParseDate(StartDate),
ParseDate(EndDate),
Days,
Limit);
}
private static DateOnly? ParseDate(string? value)
{
return string.IsNullOrWhiteSpace(value)
? null
: DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
}
}
public sealed class ManualBindReferralDto
{
[Required]
public Guid StudentUserId { get; set; }
[Required]
public Guid ReferrerUserId { get; set; }
[StringLength(100)]
public string? Source { get; set; }
public bool Force { get; set; }
public JsonElement? Metadata { get; set; }
public ManualBindReferralCommand ToCommand()
{
return new ManualBindReferralCommand(StudentUserId, ReferrerUserId, Source, Force, Metadata);
}
}
public sealed class ReferralTeamQueryDto
{
public Guid? LeaderUserId { get; set; }
public ReferralTeamQuery ToQuery()
{
return new ReferralTeamQuery(LeaderUserId);
}
}
public sealed class UpsertReferralTeamDto
{
[Required]
public Guid MemberUserId { get; set; }
public Guid? LeaderUserId { get; set; }
[StringLength(50)]
public string? RelationType { get; set; }
[StringLength(50)]
public string? Status { get; set; }
public JsonElement? Metadata { get; set; }
public UpsertReferralTeamCommand ToCommand()
{
return new UpsertReferralTeamCommand(MemberUserId, LeaderUserId, RelationType, Status, Metadata);
}
}

View File

@@ -92,6 +92,104 @@ public sealed class ReferralController(
cancellationToken));
}
[HttpGet("stats")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("查询推荐人个人统计")]
[ProducesResponseType<ReferralStatsItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralStatsItem>> Stats(
[FromQuery] ReferralStatsQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await referralService.GetStatsAsync(
ResolveAdminActor(),
query.ToQuery(),
cancellationToken));
}
[HttpGet("sales-stats")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("查询销售推荐统计排行")]
[ProducesResponseType<ReferralList<ReferralStatsItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralStatsItem>>> SalesStats(
[FromQuery] ReferralStatsQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await referralService.GetSalesStatsAsync(
ResolveAdminActor(),
query.ToQuery(),
cancellationToken));
}
[HttpGet("conversion-report")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("查询推荐转化报告")]
[ProducesResponseType<ReferralConversionReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralConversionReport>> ConversionReport(
[FromQuery] ReferralConversionQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await referralService.GetConversionReportAsync(
ResolveAdminActor(),
query.ToQuery(),
cancellationToken));
}
[HttpGet("sales-clients")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("查询推荐人名下客户")]
[ProducesResponseType<ReferralList<ReferralLeadItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralLeadItem>>> SalesClients(
[FromQuery] ReferralStatsQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await referralService.GetClientsAsync(
ResolveAdminActor(),
query.ToQuery(),
cancellationToken));
}
[HttpPost("manual-bind")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("人工调整学生推荐归属")]
[ProducesResponseType<ReferralBindResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralBindResult>> ManualBind(
ManualBindReferralDto request,
CancellationToken cancellationToken)
{
return Ok(await referralService.ManualBindAsync(
ResolveAdminActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("team")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("查询推荐团队成员")]
[ProducesResponseType<ReferralList<ReferralTeamItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralTeamItem>>> Team(
[FromQuery] ReferralTeamQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await referralService.GetTeamAsync(
ResolveAdminActor(),
query.ToQuery(),
cancellationToken));
}
[HttpPut("team")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[EndpointSummary("新增或更新推荐团队关系")]
[ProducesResponseType<ReferralTeamItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralTeamItem>> UpsertTeam(
UpsertReferralTeamDto request,
CancellationToken cancellationToken)
{
return Ok(await referralService.UpsertTeamAsync(
ResolveAdminActor(),
request.ToCommand(),
cancellationToken));
}
private ReferralActor ResolveUserActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
@@ -102,6 +200,16 @@ public sealed class ReferralController(
return new ReferralActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
private ReferralAdminActor ResolveAdminActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
{
throw new ReferralException("Referral admin actor was not resolved.", "referral_access_denied");
}
return new ReferralAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
}
private async Task<Guid> ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken)
{
if (currentTenant.TenantId.HasValue)