From 75c208dafc8ab85c800e1e3a346aec7d1eb58f7d Mon Sep 17 00:00:00 2001 From: chenchun Date: Fri, 19 Dec 2025 13:50:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActivationCodeCreateInput.cs | 34 ++++ .../ActivationCodeCreateOutput.cs | 24 +++ .../ActivationCodeRedeemInput.cs | 12 ++ .../ActivationCodeRedeemOutput.cs | 24 +++ .../IServices/IActivationCodeService.cs | 20 +++ .../Services/ActivationCodeService.cs | 101 +++++++++++ .../Enums/ActivationCodeGoodsTypeEnum.cs | 85 +++++++++ .../Entities/ActivationCodeAggregateRoot.cs | 46 +++++ .../ActivationCodeRecordAggregateRoot.cs | 50 ++++++ .../Managers/ActivationCodeManager.cs | 164 ++++++++++++++++++ 10 files changed, 560 insertions(+) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateInput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeCreateOutput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemInput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/ActivationCode/ActivationCodeRedeemOutput.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IActivationCodeService.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ActivationCodeService.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ActivationCodeGoodsTypeEnum.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeAggregateRoot.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/ActivationCodeRecordAggregateRoot.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/ActivationCodeManager.cs 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(); + } +}