feat: 完成激活码功能

This commit is contained in:
chenchun
2025-12-19 13:50:30 +08:00
parent 8021ca9eff
commit 75c208dafc
10 changed files with 560 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
/// <summary>
/// 批量生成激活码输入
/// </summary>
public class ActivationCodeCreateInput
{
/// <summary>
/// 商品类型
/// </summary>
public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 数量
/// </summary>
public int Count { get; set; } = 1;
/// <summary>
/// 是否允许多人各使用一次
/// </summary>
public bool IsReusable { get; set; }
/// <summary>
/// 是否限制同类型只能兑换一次
/// </summary>
public bool IsSameTypeOnce { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,24 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
/// <summary>
/// 批量生成激活码输出
/// </summary>
public class ActivationCodeCreateOutput
{
/// <summary>
/// 商品类型
/// </summary>
public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 数量
/// </summary>
public int Count { get; set; }
/// <summary>
/// 激活码列表
/// </summary>
public List<string> Codes { get; set; } = new();
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
/// <summary>
/// 激活码兑换输入
/// </summary>
public class ActivationCodeRedeemInput
{
/// <summary>
/// 激活码
/// </summary>
public string Code { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,24 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
/// <summary>
/// 激活码兑换输出
/// </summary>
public class ActivationCodeRedeemOutput
{
/// <summary>
/// 商品类型
/// </summary>
public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 商品名称
/// </summary>
public string PackageName { get; set; } = string.Empty;
/// <summary>
/// 内容描述
/// </summary>
public string Content { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,20 @@
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 激活码服务接口
/// </summary>
public interface IActivationCodeService : IApplicationService
{
/// <summary>
/// 批量生成激活码
/// </summary>
Task<ActivationCodeCreateOutput> CreateBatchAsync(ActivationCodeCreateInput input);
/// <summary>
/// 兑换激活码
/// </summary>
Task<ActivationCodeRedeemOutput> RedeemAsync(ActivationCodeRedeemInput input);
}

View File

@@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 激活码服务
/// </summary>
public class ActivationCodeService : ApplicationService, IActivationCodeService
{
private readonly ActivationCodeManager _activationCodeManager;
private readonly IRechargeService _rechargeService;
private readonly PremiumPackageManager _premiumPackageManager;
public ActivationCodeService(
ActivationCodeManager activationCodeManager,
IRechargeService rechargeService,
PremiumPackageManager premiumPackageManager)
{
_activationCodeManager = activationCodeManager;
_rechargeService = rechargeService;
_premiumPackageManager = premiumPackageManager;
}
/// <summary>
/// 批量生成激活码
/// </summary>
[Authorize]
[HttpPost("activationCode/Batch")]
public async Task<ActivationCodeCreateOutput> CreateBatchAsync(ActivationCodeCreateInput input)
{
var entities = await _activationCodeManager.CreateBatchAsync(
input.GoodsType,
input.Count,
input.IsReusable,
input.IsSameTypeOnce,
input.Remark);
return new ActivationCodeCreateOutput
{
GoodsType = input.GoodsType,
Count = input.Count,
Codes = entities.Select(x => x.Code).ToList()
};
}
/// <summary>
/// 兑换激活码
/// </summary>
[Authorize]
[HttpPost("activationCode/Redeem")]
public async Task<ActivationCodeRedeemOutput> RedeemAsync(ActivationCodeRedeemInput input)
{
var userId = CurrentUser.GetId();
var redeemContext = await _activationCodeManager.RedeemAsync(userId, input.Code);
var goodsType = redeemContext.ActivationCode.GoodsType;
var goods = redeemContext.Goods;
var packageName = redeemContext.PackageName;
var totalAmount = goods.Price;
if (goods.TokenAmount > 0)
{
await _premiumPackageManager.CreatePremiumPackageAsync(
userId,
goods.TokenAmount,
packageName,
totalAmount,
"激活码兑换",
expireMonths: null,
isCreateRechargeRecord: !goods.IsCombo);
}
if (goods.VipMonths > 0)
{
await _rechargeService.RechargeVipAsync(new RechargeCreateInput
{
UserId = userId,
RechargeAmount = totalAmount,
Content = packageName,
Months = goods.VipMonths,
Remark = "激活码兑换",
ContactInfo = null
});
}
return new ActivationCodeRedeemOutput
{
GoodsType = goodsType,
PackageName = packageName,
Content = packageName
};
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Reflection;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
/// <summary>
/// 激活码商品特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class ActivationCodeGoodsAttribute : Attribute
{
public decimal Price { get; }
public long TokenAmount { get; }
public int VipMonths { get; }
public bool IsCombo { get; }
public string DisplayName { get; }
public string Content { get; }
public ActivationCodeGoodsAttribute(
double price,
long tokenAmount,
int vipMonths,
bool isCombo,
string displayName,
string content)
{
Price = (decimal)price;
TokenAmount = tokenAmount;
VipMonths = vipMonths;
IsCombo = isCombo;
DisplayName = displayName;
Content = content;
}
}
/// <summary>
/// 激活码商品类型
/// </summary>
public enum ActivationCodeGoodsTypeEnum
{
/// <summary>
/// 48.90【意心Ai会员1月+2000w 尊享Token】新人首单组合包推荐
/// </summary>
[ActivationCodeGoods(48.90, 20000000, 1, true, "意心Ai会员1月+2000w 尊享Token", "新人首单组合包(推荐)")]
Vip1MonthPlus2000W = 1,
/// <summary>
/// 1.00【10w 尊享Token】测试体验包
/// </summary>
[ActivationCodeGoods(1.00, 100000, 0, false, "10w 尊享Token", "测试体验包")]
Premium10W = 2,
/// <summary>
/// 9.90【1000w 尊享Token】意心会员首单回馈包
/// </summary>
[ActivationCodeGoods(9.90, 10000000, 0, false, "1000w 尊享Token", "意心会员首单回馈包")]
Premium1000W = 3,
/// <summary>
/// 22.90【意心Ai会员1月】特价包
/// </summary>
[ActivationCodeGoods(22.90, 0, 1, false, "意心Ai会员1月", "特价包")]
Vip1Month = 4,
/// <summary>
/// 138.90【5000w 尊享Token】特价包
/// </summary>
[ActivationCodeGoods(138.90, 50000000, 0, false, "5000w 尊享Token", "特价包")]
Premium5000W = 5,
/// <summary>
/// 198.90【1亿 尊享Token】特价包
/// </summary>
[ActivationCodeGoods(198.90, 100000000, 0, false, "1亿 尊享Token", "特价包")]
Premium1Yi = 6
}
public static class ActivationCodeGoodsTypeEnumExtensions
{
public static ActivationCodeGoodsAttribute? GetGoods(this ActivationCodeGoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
return fieldInfo?.GetCustomAttribute<ActivationCodeGoodsAttribute>();
}
}

View File

@@ -0,0 +1,46 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 激活码
/// </summary>
[SugarTable("Ai_ActivationCode")]
[SugarIndex($"index_{nameof(Code)}", nameof(Code), OrderByType.Asc, true)]
[SugarIndex($"index_{nameof(GoodsType)}", nameof(GoodsType), OrderByType.Asc)]
public class ActivationCodeAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 激活码(唯一)
/// </summary>
[SugarColumn(Length = 50)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 商品类型
/// </summary>
public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 是否允许多人各使用一次
/// </summary>
public bool IsReusable { get; set; }
/// <summary>
/// 是否限制同类型只能兑换一次
/// </summary>
public bool IsSameTypeOnce { get; set; }
/// <summary>
/// 已使用次数
/// </summary>
public int UsedCount { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,50 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 激活码使用记录
/// </summary>
[SugarTable("Ai_ActivationCodeRecord")]
[SugarIndex($"index_{nameof(UserId)}_{nameof(ActivationCodeId)}",
nameof(UserId), OrderByType.Asc,
nameof(ActivationCodeId), OrderByType.Asc, true)]
[SugarIndex($"index_{nameof(UserId)}_{nameof(GoodsType)}",
nameof(UserId), OrderByType.Asc,
nameof(GoodsType), OrderByType.Asc)]
public class ActivationCodeRecordAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 激活码Id
/// </summary>
public Guid ActivationCodeId { get; set; }
/// <summary>
/// 激活码内容
/// </summary>
[SugarColumn(Length = 50)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 用户Id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 兑换时间
/// </summary>
public DateTime RedeemTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,164 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class ActivationCodeRedeemContext
{
public ActivationCodeAggregateRoot ActivationCode { get; set; } = default!;
public string PackageName { get; set; } = string.Empty;
public ActivationCodeGoodsAttribute Goods { get; set; } = default!;
}
/// <summary>
/// 激活码管理器
/// </summary>
public class ActivationCodeManager : DomainService
{
private readonly ISqlSugarRepository<ActivationCodeAggregateRoot, Guid> _activationCodeRepository;
private readonly ISqlSugarRepository<ActivationCodeRecordAggregateRoot, Guid> _activationCodeRecordRepository;
private readonly ILogger<ActivationCodeManager> _logger;
public ActivationCodeManager(
ISqlSugarRepository<ActivationCodeAggregateRoot, Guid> activationCodeRepository,
ISqlSugarRepository<ActivationCodeRecordAggregateRoot, Guid> activationCodeRecordRepository,
ILogger<ActivationCodeManager> logger)
{
_activationCodeRepository = activationCodeRepository;
_activationCodeRecordRepository = activationCodeRecordRepository;
_logger = logger;
}
public async Task<List<ActivationCodeAggregateRoot>> CreateBatchAsync(ActivationCodeGoodsTypeEnum goodsType,
int count, bool isReusable, bool isSameTypeOnce, string? remark)
{
if (count <= 0)
{
throw new UserFriendlyException("生成数量必须大于0");
}
var entities = new List<ActivationCodeAggregateRoot>();
for (var i = 0; i < count; i++)
{
var code = await GenerateUniqueActivationCodeAsync();
entities.Add(new ActivationCodeAggregateRoot
{
Code = code,
GoodsType = goodsType,
IsReusable = isReusable,
IsSameTypeOnce = isSameTypeOnce,
UsedCount = 0,
Remark = remark
});
}
await _activationCodeRepository.InsertRangeAsync(entities);
return entities;
}
public async Task<ActivationCodeRedeemContext> RedeemAsync(Guid userId, string code)
{
if (string.IsNullOrWhiteSpace(code))
{
throw new UserFriendlyException("激活码不能为空");
}
var trimmedCode = code.Trim();
var activationCode = await _activationCodeRepository._DbQueryable
.Where(x => x.Code == trimmedCode)
.FirstAsync();
if (activationCode == null)
{
throw new UserFriendlyException("激活码不存在");
}
var hasUsedCurrentCode = await _activationCodeRecordRepository._DbQueryable
.Where(x => x.UserId == userId && x.ActivationCodeId == activationCode.Id)
.AnyAsync();
if (hasUsedCurrentCode)
{
throw new UserFriendlyException("该激活码已被你使用过");
}
if (!activationCode.IsReusable && activationCode.UsedCount > 0)
{
throw new UserFriendlyException("该激活码已被使用");
}
if (activationCode.IsSameTypeOnce)
{
var hasUsedSameType = await _activationCodeRecordRepository._DbQueryable
.Where(x => x.UserId == userId && x.GoodsType == activationCode.GoodsType)
.AnyAsync();
if (hasUsedSameType)
{
throw new UserFriendlyException("该类型激活码每个用户只能兑换一次");
}
}
var goods = activationCode.GoodsType.GetGoods();
if (goods == null)
{
throw new UserFriendlyException("激活码商品类型无效");
}
var packageName = string.IsNullOrWhiteSpace(goods.Content)
? goods.DisplayName
: $"{goods.DisplayName} {goods.Content}";
activationCode.UsedCount += 1;
await _activationCodeRepository.UpdateAsync(activationCode);
var record = new ActivationCodeRecordAggregateRoot
{
ActivationCodeId = activationCode.Id,
Code = activationCode.Code,
UserId = userId,
GoodsType = activationCode.GoodsType,
RedeemTime = DateTime.Now,
Remark = "激活码兑换"
};
await _activationCodeRecordRepository.InsertAsync(record);
_logger.LogInformation("用户 {UserId} 兑换激活码 {Code} 成功", userId, activationCode.Code);
return new ActivationCodeRedeemContext
{
ActivationCode = activationCode,
PackageName = packageName,
Goods = goods
};
}
private async Task<string> GenerateUniqueActivationCodeAsync()
{
string code;
do
{
code = GenerateActivationCode();
} while (await _activationCodeRepository._DbQueryable.AnyAsync(x => x.Code == code));
return code;
}
private string GenerateActivationCode()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var builder = new StringBuilder(16);
for (var i = 0; i < 16; i++)
{
builder.Append(chars[random.Next(chars.Length)]);
}
return builder.ToString();
}
}