diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateInput.cs
new file mode 100644
index 00000000..dc9a4070
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateInput.cs
@@ -0,0 +1,34 @@
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
+
+///
+/// 批量生成激活码输入
+///
+public class ActivationCodeCreateInput
+{
+ ///
+ /// 商品类型
+ ///
+ public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
+
+ ///
+ /// 数量
+ ///
+ public int Count { get; set; } = 1;
+
+ ///
+ /// 是否允许多人各使用一次
+ ///
+ public bool IsReusable { get; set; }
+
+ ///
+ /// 是否限制同类型只能兑换一次
+ ///
+ public bool IsSameTypeOnce { get; set; }
+
+ ///
+ /// 备注
+ ///
+ public string? Remark { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateOutput.cs
new file mode 100644
index 00000000..4f734b0a
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateOutput.cs
@@ -0,0 +1,24 @@
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
+
+///
+/// 批量生成激活码输出
+///
+public class ActivationCodeCreateOutput
+{
+ ///
+ /// 商品类型
+ ///
+ public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
+
+ ///
+ /// 数量
+ ///
+ public int Count { get; set; }
+
+ ///
+ /// 激活码列表
+ ///
+ public List Codes { get; set; } = new();
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemInput.cs
new file mode 100644
index 00000000..6cad77e7
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemInput.cs
@@ -0,0 +1,12 @@
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
+
+///
+/// 激活码兑换输入
+///
+public class ActivationCodeRedeemInput
+{
+ ///
+ /// 激活码
+ ///
+ public string Code { get; set; } = string.Empty;
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemOutput.cs
new file mode 100644
index 00000000..0ec53184
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemOutput.cs
@@ -0,0 +1,24 @@
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
+
+///
+/// 激活码兑换输出
+///
+public class ActivationCodeRedeemOutput
+{
+ ///
+ /// 商品类型
+ ///
+ public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
+
+ ///
+ /// 商品名称
+ ///
+ public string PackageName { get; set; } = string.Empty;
+
+ ///
+ /// 内容描述
+ ///
+ public string Content { get; set; } = string.Empty;
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IActivationCodeService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IActivationCodeService.cs
new file mode 100644
index 00000000..9f887a0d
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IActivationCodeService.cs
@@ -0,0 +1,20 @@
+using Volo.Abp.Application.Services;
+using Yi.Framework.AiHub.Application.Contracts.Dtos.ActivationCode;
+
+namespace Yi.Framework.AiHub.Application.Contracts.IServices;
+
+///
+/// 激活码服务接口
+///
+public interface IActivationCodeService : IApplicationService
+{
+ ///
+ /// 批量生成激活码
+ ///
+ Task CreateBatchAsync(ActivationCodeCreateInput input);
+
+ ///
+ /// 兑换激活码
+ ///
+ Task RedeemAsync(ActivationCodeRedeemInput input);
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ActivationCodeService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ActivationCodeService.cs
new file mode 100644
index 00000000..2ca825f5
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ActivationCodeService.cs
@@ -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;
+
+///
+/// 激活码服务
+///
+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;
+ }
+
+ ///
+ /// 批量生成激活码
+ ///
+ [Authorize]
+ [HttpPost("activationCode/Batch")]
+ public async Task 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()
+ };
+ }
+
+ ///
+ /// 兑换激活码
+ ///
+ [Authorize]
+ [HttpPost("activationCode/Redeem")]
+ public async Task 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
+ };
+ }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ActivationCodeGoodsTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ActivationCodeGoodsTypeEnum.cs
new file mode 100644
index 00000000..7a07b11f
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ActivationCodeGoodsTypeEnum.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Reflection;
+
+namespace Yi.Framework.AiHub.Domain.Shared.Enums;
+
+///
+/// 激活码商品特性
+///
+[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;
+ }
+}
+
+///
+/// 激活码商品类型
+///
+public enum ActivationCodeGoodsTypeEnum
+{
+ ///
+ /// 48.90【意心Ai会员1月+2000w 尊享Token】新人首单组合包(推荐)
+ ///
+ [ActivationCodeGoods(48.90, 20000000, 1, true, "意心Ai会员1月+2000w 尊享Token", "新人首单组合包(推荐)")]
+ Vip1MonthPlus2000W = 1,
+
+ ///
+ /// 1.00【10w 尊享Token】测试体验包
+ ///
+ [ActivationCodeGoods(1.00, 100000, 0, false, "10w 尊享Token", "测试体验包")]
+ Premium10W = 2,
+
+ ///
+ /// 9.90【1000w 尊享Token】意心会员首单回馈包
+ ///
+ [ActivationCodeGoods(9.90, 10000000, 0, false, "1000w 尊享Token", "意心会员首单回馈包")]
+ Premium1000W = 3,
+
+ ///
+ /// 22.90【意心Ai会员1月】特价包
+ ///
+ [ActivationCodeGoods(22.90, 0, 1, false, "意心Ai会员1月", "特价包")]
+ Vip1Month = 4,
+
+ ///
+ /// 138.90【5000w 尊享Token】特价包
+ ///
+ [ActivationCodeGoods(138.90, 50000000, 0, false, "5000w 尊享Token", "特价包")]
+ Premium5000W = 5,
+
+ ///
+ /// 198.90【1亿 尊享Token】特价包
+ ///
+ [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();
+ }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeAggregateRoot.cs
new file mode 100644
index 00000000..3142f2b0
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeAggregateRoot.cs
@@ -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;
+
+///
+/// 激活码
+///
+[SugarTable("Ai_ActivationCode")]
+[SugarIndex($"index_{nameof(Code)}", nameof(Code), OrderByType.Asc, true)]
+[SugarIndex($"index_{nameof(GoodsType)}", nameof(GoodsType), OrderByType.Asc)]
+public class ActivationCodeAggregateRoot : FullAuditedAggregateRoot
+{
+ ///
+ /// 激活码(唯一)
+ ///
+ [SugarColumn(Length = 50)]
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// 商品类型
+ ///
+ public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
+
+ ///
+ /// 是否允许多人各使用一次
+ ///
+ public bool IsReusable { get; set; }
+
+ ///
+ /// 是否限制同类型只能兑换一次
+ ///
+ public bool IsSameTypeOnce { get; set; }
+
+ ///
+ /// 已使用次数
+ ///
+ public int UsedCount { get; set; }
+
+ ///
+ /// 备注
+ ///
+ [SugarColumn(Length = 500, IsNullable = true)]
+ public string? Remark { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeRecordAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeRecordAggregateRoot.cs
new file mode 100644
index 00000000..b24d6df0
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeRecordAggregateRoot.cs
@@ -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;
+
+///
+/// 激活码使用记录
+///
+[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
+{
+ ///
+ /// 激活码Id
+ ///
+ public Guid ActivationCodeId { get; set; }
+
+ ///
+ /// 激活码内容
+ ///
+ [SugarColumn(Length = 50)]
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// 用户Id
+ ///
+ public Guid UserId { get; set; }
+
+ ///
+ /// 商品类型
+ ///
+ public ActivationCodeGoodsTypeEnum GoodsType { get; set; }
+
+ ///
+ /// 兑换时间
+ ///
+ public DateTime RedeemTime { get; set; }
+
+ ///
+ /// 备注
+ ///
+ [SugarColumn(Length = 500, IsNullable = true)]
+ public string? Remark { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ActivationCodeManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ActivationCodeManager.cs
new file mode 100644
index 00000000..9b6cf322
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ActivationCodeManager.cs
@@ -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!;
+}
+
+///
+/// 激活码管理器
+///
+public class ActivationCodeManager : DomainService
+{
+ private readonly ISqlSugarRepository _activationCodeRepository;
+ private readonly ISqlSugarRepository _activationCodeRecordRepository;
+ private readonly ILogger _logger;
+
+ public ActivationCodeManager(
+ ISqlSugarRepository activationCodeRepository,
+ ISqlSugarRepository activationCodeRecordRepository,
+ ILogger logger)
+ {
+ _activationCodeRepository = activationCodeRepository;
+ _activationCodeRecordRepository = activationCodeRecordRepository;
+ _logger = logger;
+ }
+
+ public async Task> CreateBatchAsync(ActivationCodeGoodsTypeEnum goodsType,
+ int count, bool isReusable, bool isSameTypeOnce, string? remark)
+ {
+ if (count <= 0)
+ {
+ throw new UserFriendlyException("生成数量必须大于0");
+ }
+
+ var entities = new List();
+ 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 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 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();
+ }
+}