using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Tiku.Api.Contracts; using Tiku.Application.Growth; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Api.Controllers; [ApiController] [Tags("学生端-推荐增长")] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)] [Produces("application/json")] [Route("api/referral")] public sealed class ReferralController( IReferralService referralService, ICurrentUser currentUser, ITenantContext currentTenant, ITenantContextInitializer tenantInitializer, ITenantDirectory tenantDirectory) : ControllerBase { [HttpPost("invite-code")] [Authorize(Policy = TikuPolicies.CurrentTenantMember)] [EndpointSummary("生成或查询当前成员邀请码")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> InviteCode( ReferralInviteDto request, CancellationToken cancellationToken) { return Ok(await referralService.GetOrCreateInviteCodeAsync( ResolveUserActor(), request.ToCommand(), cancellationToken)); } [HttpPost("resolve")] [AllowAnonymous] [EndpointSummary("解析推荐邀请码")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Resolve( ResolveReferralDto request, CancellationToken cancellationToken) { return Ok(await referralService.ResolveAsync( new ReferralActor(await ResolveTenantIdAsync(request.TenantCode, cancellationToken), currentUser.UserId), request.ToCommand(), cancellationToken)); } [HttpPost("track-event")] [AllowAnonymous] [EndpointSummary("记录推荐行为")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> TrackEvent( TrackReferralEventDto request, CancellationToken cancellationToken) { return Ok(await referralService.TrackEventAsync( new ReferralActor(await ResolveTenantIdAsync(request.TenantCode, cancellationToken), currentUser.UserId), request.ToCommand(), HttpContext.Connection.RemoteIpAddress?.ToString(), Request.Headers.UserAgent.FirstOrDefault(), cancellationToken)); } [HttpPost("bind")] [Authorize(Policy = TikuPolicies.CurrentTenantMember)] [EndpointSummary("绑定当前用户推荐归属")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Bind( BindReferralDto request, CancellationToken cancellationToken) { return Ok(await referralService.BindAsync( ResolveUserActor(), request.ToCommand(), cancellationToken)); } [HttpPost("qrcode")] [Authorize(Policy = TikuPolicies.CurrentTenantMember)] [EndpointSummary("生成或查询推广二维码")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Qrcode( ReferralQrcodeDto request, CancellationToken cancellationToken) { return Ok(await referralService.GetOrCreateQrcodeAsync( ResolveUserActor(), request.ToCommand(), cancellationToken)); } [HttpGet("stats")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("查询推荐人个人统计")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Stats( [FromQuery] ReferralStatsQueryDto query, CancellationToken cancellationToken) { return Ok(await referralService.GetStatsAsync( ResolveAdminActor(), query.ToQuery(), cancellationToken)); } [HttpGet("sales-stats")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("查询销售推荐统计排行")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> SalesStats( [FromQuery] ReferralStatsQueryDto query, CancellationToken cancellationToken) { return Ok(await referralService.GetSalesStatsAsync( ResolveAdminActor(), query.ToQuery(), cancellationToken)); } [HttpGet("conversion-report")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("查询推荐转化报告")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ConversionReport( [FromQuery] ReferralConversionQueryDto query, CancellationToken cancellationToken) { return Ok(await referralService.GetConversionReportAsync( ResolveAdminActor(), query.ToQuery(), cancellationToken)); } [HttpGet("sales-clients")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("查询推荐人名下客户")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> SalesClients( [FromQuery] ReferralStatsQueryDto query, CancellationToken cancellationToken) { return Ok(await referralService.GetClientsAsync( ResolveAdminActor(), query.ToQuery(), cancellationToken)); } [HttpPost("manual-bind")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("人工调整学生推荐归属")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ManualBind( ManualBindReferralDto request, CancellationToken cancellationToken) { return Ok(await referralService.ManualBindAsync( ResolveAdminActor(), request.ToCommand(), cancellationToken)); } [HttpGet("team")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("查询推荐团队成员")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> Team( [FromQuery] ReferralTeamQueryDto query, CancellationToken cancellationToken) { return Ok(await referralService.GetTeamAsync( ResolveAdminActor(), query.ToQuery(), cancellationToken)); } [HttpPut("team")] [Tags("租户端-推荐增长")] [Authorize(Policy = BackendPermissions.TenantCrmManage)] [EndpointSummary("新增或更新推荐团队关系")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> 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) { throw new ReferralException("Current referral actor was not resolved.", "referral_access_denied"); } 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 ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken) { if (currentTenant.TenantId.HasValue) { return currentTenant.TenantId.Value; } var resolvedTenantCode = tenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(resolvedTenantCode)) { throw new TenantNotFoundException(); } var tenant = await tenantDirectory.FindByCodeAsync(resolvedTenantCode.Trim(), cancellationToken) ?? throw new TenantNotFoundException(); tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode); return tenant.TenantId; } }