feat: 完成激活码功能
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user