Compare commits
No commits in common. "fb466b35ea31ca7b742352f459884bac10e039a0" and "96503a593dd25b0580566b99899df992261432d1" have entirely different histories.
fb466b35ea
...
96503a593d
@ -123,7 +123,6 @@
|
|||||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
|
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
|
||||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
|
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
|
||||||
<!-- Testing packages -->
|
<!-- Testing packages -->
|
||||||
<PackageVersion Include="FluentAssertions" Version="6.12.2" />
|
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
|
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
|
||||||
<PackageVersion Include="Testcontainers.MySql" Version="$(TestcontainersVersion)" />
|
<PackageVersion Include="Testcontainers.MySql" Version="$(TestcontainersVersion)" />
|
||||||
|
|||||||
@ -86,7 +86,7 @@ docker run --restart unless-stopped --name netcorepal-rabbitmq -p 5672:5672 -p 1
|
|||||||
|
|
||||||
启动后,可以通过以下地址访问各个服务:
|
启动后,可以通过以下地址访问各个服务:
|
||||||
|
|
||||||
- **Redis**: `81.68.223.70:16379,password=sl52788542`
|
- **Redis**: `localhost:6379`
|
||||||
- **MySQL**: `localhost:3306` (root/123456)
|
- **MySQL**: `localhost:3306` (root/123456)
|
||||||
- **RabbitMQ AMQP**: `localhost:5672` (guest/guest)
|
- **RabbitMQ AMQP**: `localhost:5672` (guest/guest)
|
||||||
- **RabbitMQ 管理界面**: http://localhost:15672 (guest/guest)
|
- **RabbitMQ 管理界面**: http://localhost:15672 (guest/guest)
|
||||||
|
|||||||
@ -132,10 +132,10 @@ Update your `appsettings.Development.json` with these connection strings:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Redis": "81.68.223.70:16379,password=sl52788542,defaultDatabase=0",
|
"Redis": "localhost:6379,defaultDatabase=0",
|
||||||
"MySql": "Server=localhost;Port=3306;Database=abctemplate;Uid=root;Pwd=123456;",
|
"MySql": "Server=localhost;Port=3306;Database=abctemplate;Uid=root;Pwd=123456;",
|
||||||
"SqlServer": "Server=localhost,1433;Database=abctemplate;User Id=sa;Password=Test123456!;TrustServerCertificate=true;",
|
"SqlServer": "Server=localhost,1433;Database=abctemplate;User Id=sa;Password=Test123456!;TrustServerCertificate=true;",
|
||||||
"PostgreSQL": "Host=localhost;Port=15432;Database=abctemplate;Username=postgres;Password=123456;"
|
"PostgreSQL": "Host=localhost;Port=5432;Database=abctemplate;Username=postgres;Password=123456;"
|
||||||
},
|
},
|
||||||
"RabbitMQ": {
|
"RabbitMQ": {
|
||||||
"HostName": "localhost",
|
"HostName": "localhost",
|
||||||
|
|||||||
@ -214,7 +214,7 @@ function Start-Infrastructure {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "📋 Service Summary:" -ForegroundColor Cyan
|
Write-Host "📋 Service Summary:" -ForegroundColor Cyan
|
||||||
Write-Host "==================="
|
Write-Host "==================="
|
||||||
Write-Host "✅ Redis: 81.68.223.70:16379,password=sl52788542"
|
Write-Host "✅ Redis: localhost:6379"
|
||||||
|
|
||||||
if ($Postgres) {
|
if ($Postgres) {
|
||||||
Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)"
|
Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)"
|
||||||
|
|||||||
@ -146,7 +146,7 @@ main() {
|
|||||||
echo
|
echo
|
||||||
echo "📋 Service Summary:"
|
echo "📋 Service Summary:"
|
||||||
echo "==================="
|
echo "==================="
|
||||||
echo "✅ Redis: 81.68.223.70:16379,password=sl52788542"
|
echo "✅ Redis: localhost:6379"
|
||||||
echo "✅ MySQL: localhost:3306 (root/123456)"
|
echo "✅ MySQL: localhost:3306 (root/123456)"
|
||||||
echo "✅ RabbitMQ: localhost:5672 (guest/guest)"
|
echo "✅ RabbitMQ: localhost:5672 (guest/guest)"
|
||||||
echo "📊 RabbitMQ Management UI: http://localhost:15672"
|
echo "📊 RabbitMQ Management UI: http://localhost:15672"
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public class AddPointsCommand : IRequest<AddPointsResponse>
|
|||||||
|
|
||||||
public class AddPointsResponse
|
public class AddPointsResponse
|
||||||
{
|
{
|
||||||
public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
|
public long AccountId { get; set; }
|
||||||
public long MemberId { get; set; }
|
public long MemberId { get; set; }
|
||||||
public int AddedPoints { get; set; }
|
public int AddedPoints { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public class DeductPointsCommand : IRequest<DeductPointsResponse>
|
|||||||
|
|
||||||
public class DeductPointsResponse
|
public class DeductPointsResponse
|
||||||
{
|
{
|
||||||
public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
|
public long AccountId { get; set; }
|
||||||
public long MemberId { get; set; }
|
public long MemberId { get; set; }
|
||||||
public int DeductedPoints { get; set; }
|
public int DeductedPoints { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
|
|||||||
@ -31,7 +31,6 @@ public class ProcessExpiredPointsCommandHandler : IRequestHandler<ProcessExpired
|
|||||||
var deduction = PointsTransaction.Create(
|
var deduction = PointsTransaction.Create(
|
||||||
transaction.PointsAccountId,
|
transaction.PointsAccountId,
|
||||||
transaction.MemberId,
|
transaction.MemberId,
|
||||||
transaction.TenantId,
|
|
||||||
-transaction.Points,
|
-transaction.Points,
|
||||||
"Expired",
|
"Expired",
|
||||||
$"来源交易: {transaction.Id}",
|
$"来源交易: {transaction.Id}",
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
using Fengling.Member.Domain.Events.Points;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Events;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分变动事件处理器 - 负责将积分变动持久化到数据库
|
|
||||||
/// </summary>
|
|
||||||
public class PointsChangedEventHandler : INotificationHandler<PointsChangedEvent>
|
|
||||||
{
|
|
||||||
private readonly IPointsHistoryRepository _historyRepository;
|
|
||||||
private readonly ILogger<PointsChangedEventHandler> _logger;
|
|
||||||
|
|
||||||
public PointsChangedEventHandler(
|
|
||||||
IPointsHistoryRepository historyRepository,
|
|
||||||
ILogger<PointsChangedEventHandler> logger)
|
|
||||||
{
|
|
||||||
_historyRepository = historyRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(PointsChangedEvent notification, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Handling PointsChangedEvent for MemberId={MemberId}, AccountId={AccountId}, ChangedPoints={ChangedPoints}",
|
|
||||||
notification.MemberId, notification.AccountId, notification.ChangedPoints);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 幂等性检查:通过 SourceId 检查是否已处理
|
|
||||||
if (!string.IsNullOrEmpty(notification.SourceId))
|
|
||||||
{
|
|
||||||
var exists = await _historyRepository.ExistsBySourceIdAsync(
|
|
||||||
notification.SourceId, cancellationToken);
|
|
||||||
|
|
||||||
if (exists)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"PointsChangedEvent already processed, skipping. MemberId={MemberId}, SourceId={SourceId}",
|
|
||||||
notification.MemberId, notification.SourceId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定交易类型分类
|
|
||||||
var transactionTypeCategory = notification.ChangedPoints >= 0
|
|
||||||
? PointsTransactionType.Earn
|
|
||||||
: PointsTransactionType.Deduct;
|
|
||||||
|
|
||||||
// 创建积分明细记录(使用 factory 方法)
|
|
||||||
// 注意:PointsAccountId 需要根据业务需求从缓存或数据库获取
|
|
||||||
var transaction = PointsTransaction.Create(
|
|
||||||
pointsAccountId: notification.AccountId,
|
|
||||||
memberId: notification.MemberId,
|
|
||||||
tenantId: notification.TenantId,
|
|
||||||
points: notification.ChangedPoints,
|
|
||||||
transactionType: notification.TransactionType,
|
|
||||||
sourceId: notification.SourceId ?? string.Empty,
|
|
||||||
typeCategory: transactionTypeCategory,
|
|
||||||
remark: notification.Remark);
|
|
||||||
|
|
||||||
await _historyRepository.AddAsync(transaction, cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"PointsChangedEvent processed successfully. MemberId={MemberId}, Points={Points}, TotalPoints={TotalPoints}",
|
|
||||||
notification.MemberId, notification.ChangedPoints, notification.NewBalance);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex,
|
|
||||||
"Error handling PointsChangedEvent. MemberId={MemberId}, AccountId={AccountId}",
|
|
||||||
notification.MemberId, notification.AccountId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,7 +11,6 @@
|
|||||||
<PackageReference Include="FluentValidation.AspNetCore" />
|
<PackageReference Include="FluentValidation.AspNetCore" />
|
||||||
<PackageReference Include="MediatR" />
|
<PackageReference Include="MediatR" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<PackageReference Include="StackExchange.Redis" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
|
<ProjectReference Include="..\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
|
||||||
|
|||||||
@ -1,99 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 基于 Redis 的分布式锁实现
|
|
||||||
/// </summary>
|
|
||||||
public class CodeDistributedLock : ICodeDistributedLock
|
|
||||||
{
|
|
||||||
private readonly IConnectionMultiplexer _redis;
|
|
||||||
private readonly ILogger<CodeDistributedLock> _logger;
|
|
||||||
|
|
||||||
private const string LockKeyPrefix = "points:lock:";
|
|
||||||
private readonly TimeSpan _defaultTtl = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
public CodeDistributedLock(IConnectionMultiplexer redis, ILogger<CodeDistributedLock> logger)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<LockAcquisitionResult> AcquireAsync(string codeId, TimeSpan? ttl = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{LockKeyPrefix}{codeId}";
|
|
||||||
var lockValue = Guid.NewGuid().ToString("N");
|
|
||||||
var effectiveTtl = ttl ?? _defaultTtl;
|
|
||||||
|
|
||||||
// 使用 SET NX EX 原子操作获取锁
|
|
||||||
var acquired = await db.StringSetAsync(
|
|
||||||
key,
|
|
||||||
lockValue,
|
|
||||||
effectiveTtl,
|
|
||||||
When.NotExists);
|
|
||||||
|
|
||||||
if (acquired)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Acquired lock for code {CodeId}, lock value: {LockValue}", codeId, lockValue);
|
|
||||||
return LockAcquisitionResult.Succeeded(lockValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取锁失败,检查是否超时
|
|
||||||
var existingValue = await db.StringGetAsync(key);
|
|
||||||
if (existingValue.IsNullOrEmpty)
|
|
||||||
{
|
|
||||||
// 锁已过期,重试获取
|
|
||||||
return LockAcquisitionResult.Failed("Lock expired, please retry");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("Failed to acquire lock for code {CodeId}, already held", codeId);
|
|
||||||
return LockAcquisitionResult.Failed("Lock is held by another process");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error acquiring lock for code {CodeId}", codeId);
|
|
||||||
return LockAcquisitionResult.Failed($"Error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ReleaseAsync(string codeId, string lockValue)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{LockKeyPrefix}{codeId}";
|
|
||||||
|
|
||||||
// 使用 Lua 脚本确保原子性:只有持有锁的进程才能释放
|
|
||||||
var luaScript = @"
|
|
||||||
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
||||||
return redis.call('DEL', KEYS[1])
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
";
|
|
||||||
|
|
||||||
var result = await db.ScriptEvaluateAsync(luaScript,
|
|
||||||
new RedisKey[] { key },
|
|
||||||
new RedisValue[] { lockValue });
|
|
||||||
|
|
||||||
var deleted = (int)result;
|
|
||||||
if (deleted > 0)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Released lock for code {CodeId}, lock value: {LockValue}", codeId, lockValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Failed to release lock for code {CodeId}, lock value: {LockValue}, may be expired or held by another process",
|
|
||||||
codeId, lockValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error releasing lock for code {CodeId}", codeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分布式锁获取结果
|
|
||||||
/// </summary>
|
|
||||||
public class LockAcquisitionResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public string? LockValue { get; set; }
|
|
||||||
public string? ErrorMessage { get; set; }
|
|
||||||
|
|
||||||
public static LockAcquisitionResult Succeeded(string lockValue) => new()
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
LockValue = lockValue
|
|
||||||
};
|
|
||||||
|
|
||||||
public static LockAcquisitionResult Failed(string errorMessage) => new()
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
ErrorMessage = errorMessage
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分布式锁接口
|
|
||||||
/// </summary>
|
|
||||||
public interface ICodeDistributedLock
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取分布式锁
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="codeId">码ID</param>
|
|
||||||
/// <param name="ttl">锁过期时间,默认10秒</param>
|
|
||||||
/// <returns>获取结果</returns>
|
|
||||||
Task<LockAcquisitionResult> AcquireAsync(string codeId, TimeSpan? ttl = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 释放分布式锁
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="codeId">码ID</param>
|
|
||||||
/// <param name="lockValue">锁值</param>
|
|
||||||
Task ReleaseAsync(string codeId, string lockValue);
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分账户缓存模型
|
|
||||||
/// </summary>
|
|
||||||
public class PointsAccountCacheModel
|
|
||||||
{
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
public long TenantId { get; set; }
|
|
||||||
public int TotalPoints { get; set; }
|
|
||||||
public int FrozenPoints { get; set; }
|
|
||||||
public int AvailablePoints => TotalPoints - FrozenPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分账户缓存接口
|
|
||||||
/// </summary>
|
|
||||||
public interface IPointsAccountCache
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取缓存的积分账户
|
|
||||||
/// </summary>
|
|
||||||
Task<PointsAccountCacheModel?> GetAsync(long memberId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置积分账户缓存
|
|
||||||
/// </summary>
|
|
||||||
Task SetAsync(long memberId, PointsAccountCacheModel account, TimeSpan? ttl = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 增加积分
|
|
||||||
/// </summary>
|
|
||||||
Task<int> AddPointsAsync(long memberId, int points);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 扣减积分
|
|
||||||
/// </summary>
|
|
||||||
Task<(bool Success, int RemainingPoints)> DeductPointsAsync(long memberId, int points);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 冻结积分
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> FreezePointsAsync(long memberId, int points);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解冻积分
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> UnfreezePointsAsync(long memberId, int points);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 移除缓存
|
|
||||||
/// </summary>
|
|
||||||
Task RemoveAsync(long memberId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查积分码是否已处理(幂等性检查)
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> IsCodeProcessedAsync(string codeId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记积分码已处理
|
|
||||||
/// </summary>
|
|
||||||
Task MarkCodeProcessedAsync(string codeId, TimeSpan? ttl = null);
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
using Fengling.Member.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分处理请求
|
|
||||||
/// </summary>
|
|
||||||
public class PointsProcessRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 码ID(用于幂等性检查和分布式锁)
|
|
||||||
/// </summary>
|
|
||||||
public string CodeId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 会员ID
|
|
||||||
/// </summary>
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 交易类型
|
|
||||||
/// </summary>
|
|
||||||
public string TransactionType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分数(增加或扣减)
|
|
||||||
/// </summary>
|
|
||||||
public int Points { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 扣减类型:true=增加积分,false=扣减积分
|
|
||||||
/// </summary>
|
|
||||||
public bool IsAddition { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 来源ID
|
|
||||||
/// </summary>
|
|
||||||
public string? SourceId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 备注
|
|
||||||
/// </summary>
|
|
||||||
public string? Remark { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分处理结果
|
|
||||||
/// </summary>
|
|
||||||
public class PointsProcessResult
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public int Points { get; set; }
|
|
||||||
public int TotalPoints { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? ErrorCode { get; set; }
|
|
||||||
|
|
||||||
public static PointsProcessResult Succeeded(int points, int totalPoints, string message = "处理成功") => new()
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Points = points,
|
|
||||||
TotalPoints = totalPoints,
|
|
||||||
Message = message
|
|
||||||
};
|
|
||||||
|
|
||||||
public static PointsProcessResult Failed(string message, string? errorCode = null) => new()
|
|
||||||
{
|
|
||||||
Success = false,
|
|
||||||
Points = 0,
|
|
||||||
TotalPoints = 0,
|
|
||||||
Message = message,
|
|
||||||
ErrorCode = errorCode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分处理服务接口
|
|
||||||
/// </summary>
|
|
||||||
public interface IPointsProcessingService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 处理积分(增加或扣减)
|
|
||||||
/// </summary>
|
|
||||||
Task<PointsProcessResult> ProcessAsync(PointsProcessRequest request, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取积分余额
|
|
||||||
/// </summary>
|
|
||||||
Task<PointsAccountCacheModel?> GetBalanceAsync(long memberId);
|
|
||||||
}
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Redis 积分账户缓存实现
|
|
||||||
/// </summary>
|
|
||||||
public class PointsAccountCache : IPointsAccountCache
|
|
||||||
{
|
|
||||||
private readonly IConnectionMultiplexer _redis;
|
|
||||||
private readonly ILogger<PointsAccountCache> _logger;
|
|
||||||
private readonly TimeSpan _defaultTtl = TimeSpan.FromMinutes(5);
|
|
||||||
|
|
||||||
private const string AccountKeyPrefix = "points:account:";
|
|
||||||
private const string CodeKeyPrefix = "points:code:";
|
|
||||||
|
|
||||||
public PointsAccountCache(IConnectionMultiplexer redis, ILogger<PointsAccountCache> logger)
|
|
||||||
{
|
|
||||||
_redis = redis;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PointsAccountCacheModel?> GetAsync(long memberId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{AccountKeyPrefix}{memberId}";
|
|
||||||
var value = await db.StringGetAsync(key);
|
|
||||||
|
|
||||||
if (value.IsNullOrEmpty)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return Deserialize(value!);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error getting points account from cache for member {MemberId}", memberId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetAsync(long memberId, PointsAccountCacheModel account, TimeSpan? ttl = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{AccountKeyPrefix}{memberId}";
|
|
||||||
var value = Serialize(account);
|
|
||||||
await db.StringSetAsync(key, value, ttl ?? _defaultTtl);
|
|
||||||
_logger.LogDebug("Set points account cache for member {MemberId}", memberId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error setting points account cache for member {MemberId}", memberId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> AddPointsAsync(long memberId, int points)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{AccountKeyPrefix}{memberId}";
|
|
||||||
|
|
||||||
// 使用 Redis 原子操作
|
|
||||||
var newTotal = await db.StringIncrementAsync(key, points);
|
|
||||||
|
|
||||||
// 如果是首次设置,需要初始化结构
|
|
||||||
if (newTotal == points)
|
|
||||||
{
|
|
||||||
var account = new PointsAccountCacheModel
|
|
||||||
{
|
|
||||||
MemberId = memberId,
|
|
||||||
TotalPoints = points,
|
|
||||||
FrozenPoints = 0
|
|
||||||
};
|
|
||||||
await SetAsync(memberId, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Added {Points} points to member {MemberId}, new total: {Total}",
|
|
||||||
points, memberId, newTotal);
|
|
||||||
|
|
||||||
return (int)newTotal;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error adding points for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool Success, int RemainingPoints)> DeductPointsAsync(long memberId, int points)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{AccountKeyPrefix}{memberId}";
|
|
||||||
|
|
||||||
// 使用 Lua 脚本保证原子性
|
|
||||||
var luaScript = @"
|
|
||||||
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
|
|
||||||
local points = tonumber(ARGV[1])
|
|
||||||
if current >= points then
|
|
||||||
local newValue = redis.call('DECRBY', KEYS[1], points)
|
|
||||||
return newValue
|
|
||||||
else
|
|
||||||
return -1
|
|
||||||
end
|
|
||||||
";
|
|
||||||
|
|
||||||
var result = await db.ScriptEvaluateAsync(luaScript,
|
|
||||||
new RedisKey[] { key },
|
|
||||||
new RedisValue[] { points });
|
|
||||||
|
|
||||||
var value = (int)result;
|
|
||||||
|
|
||||||
if (value < 0)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Insufficient points for member {MemberId}, available: {Available}, requested: {Requested}",
|
|
||||||
memberId, await db.StringGetAsync(key), points);
|
|
||||||
return (false, (int)(await db.StringGetAsync(key)));
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Deducted {Points} points from member {MemberId}, remaining: {Remaining}",
|
|
||||||
points, memberId, value);
|
|
||||||
|
|
||||||
return (true, value);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deducting points for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> FreezePointsAsync(long memberId, int points)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var account = await GetAsync(memberId);
|
|
||||||
if (account == null || account.AvailablePoints < points)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
account.FrozenPoints += points;
|
|
||||||
await SetAsync(memberId, account);
|
|
||||||
|
|
||||||
_logger.LogInformation("Frozen {Points} points for member {MemberId}", points, memberId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error freezing points for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UnfreezePointsAsync(long memberId, int points)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var account = await GetAsync(memberId);
|
|
||||||
if (account == null || account.FrozenPoints < points)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
account.FrozenPoints -= points;
|
|
||||||
await SetAsync(memberId, account);
|
|
||||||
|
|
||||||
_logger.LogInformation("Unfrozen {Points} points for member {MemberId}", points, memberId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error unfreezing points for member {MemberId}", memberId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveAsync(long memberId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{AccountKeyPrefix}{memberId}";
|
|
||||||
await db.KeyDeleteAsync(key);
|
|
||||||
_logger.LogDebug("Removed points account cache for member {MemberId}", memberId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error removing points account cache for member {MemberId}", memberId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsCodeProcessedAsync(string codeId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{CodeKeyPrefix}{codeId}";
|
|
||||||
return await db.KeyExistsAsync(key);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error checking code processed status for code {CodeId}", codeId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task MarkCodeProcessedAsync(string codeId, TimeSpan? ttl = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var key = $"{CodeKeyPrefix}{codeId}";
|
|
||||||
// 码的处理标记保留 24 小时
|
|
||||||
await db.StringSetAsync(key, "1", ttl ?? TimeSpan.FromHours(24));
|
|
||||||
_logger.LogDebug("Marked code {CodeId} as processed", codeId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error marking code processed for code {CodeId}", codeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Serialize(PointsAccountCacheModel account)
|
|
||||||
{
|
|
||||||
return $"{account.MemberId}|{account.TenantId}|{account.TotalPoints}|{account.FrozenPoints}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PointsAccountCacheModel? Deserialize(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var parts = value.Split('|');
|
|
||||||
if (parts.Length != 4)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new PointsAccountCacheModel
|
|
||||||
{
|
|
||||||
MemberId = long.Parse(parts[0]),
|
|
||||||
TenantId = long.Parse(parts[1]),
|
|
||||||
TotalPoints = int.Parse(parts[2]),
|
|
||||||
FrozenPoints = int.Parse(parts[3])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
using Fengling.Member.Domain.Events.Points;
|
|
||||||
using Fengling.Member.Infrastructure;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分处理服务实现
|
|
||||||
/// 使用分布式锁 + Redis 缓存实现高性能积分处理
|
|
||||||
/// </summary>
|
|
||||||
public class PointsProcessingService : IPointsProcessingService
|
|
||||||
{
|
|
||||||
private readonly IPointsAccountCache _cache;
|
|
||||||
private readonly ICodeDistributedLock _lock;
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
private readonly ITenantAccessor _tenantAccessor;
|
|
||||||
private readonly ILogger<PointsProcessingService> _logger;
|
|
||||||
|
|
||||||
public PointsProcessingService(
|
|
||||||
IPointsAccountCache cache,
|
|
||||||
ICodeDistributedLock distributedLock,
|
|
||||||
IMediator mediator,
|
|
||||||
ITenantAccessor tenantAccessor,
|
|
||||||
ILogger<PointsProcessingService> logger)
|
|
||||||
{
|
|
||||||
_cache = cache;
|
|
||||||
_lock = distributedLock;
|
|
||||||
_mediator = mediator;
|
|
||||||
_tenantAccessor = tenantAccessor;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PointsProcessResult> ProcessAsync(PointsProcessRequest request, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// 1. 幂等性检查 - 检查码是否已处理
|
|
||||||
if (await _cache.IsCodeProcessedAsync(request.CodeId))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Code {CodeId} already processed", request.CodeId);
|
|
||||||
return PointsProcessResult.Failed("码已处理", "CODE_ALREADY_PROCESSED");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 获取分布式锁
|
|
||||||
var lockResult = await _lock.AcquireAsync(request.CodeId);
|
|
||||||
if (!lockResult.Success)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Failed to acquire lock for code {CodeId}: {Error}", request.CodeId, lockResult.ErrorMessage);
|
|
||||||
return PointsProcessResult.Failed("处理中,请稍后重试", "LOCK_FAILED");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 3. 双重检查幂等性(在获取锁后再次检查)
|
|
||||||
if (await _cache.IsCodeProcessedAsync(request.CodeId))
|
|
||||||
{
|
|
||||||
return PointsProcessResult.Failed("码已处理", "CODE_ALREADY_PROCESSED");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 从缓存获取账户,不存在则创建
|
|
||||||
var account = await _cache.GetAsync(request.MemberId);
|
|
||||||
var tenantId = _tenantAccessor.GetTenantId() ?? 0;
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
account = new PointsAccountCacheModel
|
|
||||||
{
|
|
||||||
MemberId = request.MemberId,
|
|
||||||
TenantId = tenantId,
|
|
||||||
TotalPoints = 0,
|
|
||||||
FrozenPoints = 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 处理积分
|
|
||||||
int newTotal;
|
|
||||||
if (request.IsAddition)
|
|
||||||
{
|
|
||||||
newTotal = await _cache.AddPointsAsync(request.MemberId, request.Points);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var deductResult = await _cache.DeductPointsAsync(request.MemberId, request.Points);
|
|
||||||
if (!deductResult.Success)
|
|
||||||
{
|
|
||||||
return PointsProcessResult.Failed("积分不足", "INSUFFICIENT_POINTS");
|
|
||||||
}
|
|
||||||
newTotal = deductResult.RemainingPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 标记码已处理
|
|
||||||
await _cache.MarkCodeProcessedAsync(request.CodeId);
|
|
||||||
|
|
||||||
// 7. 发布领域事件(异步持久化到数据库)
|
|
||||||
var domainEvent = new PointsChangedEvent(
|
|
||||||
PointsAccountId.New(), // AccountId - 缓存中暂无ID,事件消费者处理
|
|
||||||
request.MemberId,
|
|
||||||
tenantId,
|
|
||||||
request.IsAddition ? request.Points : -request.Points,
|
|
||||||
newTotal,
|
|
||||||
request.TransactionType,
|
|
||||||
request.SourceId,
|
|
||||||
request.Remark);
|
|
||||||
|
|
||||||
await _mediator.Publish(domainEvent, ct);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Points processed successfully: MemberId={MemberId}, CodeId={CodeId}, Points={Points}, IsAddition={IsAddition}, NewTotal={Total}",
|
|
||||||
request.MemberId, request.CodeId, request.Points, request.IsAddition, newTotal);
|
|
||||||
|
|
||||||
return PointsProcessResult.Succeeded(request.Points, newTotal);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error processing points for member {MemberId}, code {CodeId}", request.MemberId, request.CodeId);
|
|
||||||
return PointsProcessResult.Failed("处理失败", "PROCESS_ERROR");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// 8. 释放锁
|
|
||||||
await _lock.ReleaseAsync(request.CodeId, lockResult.LockValue!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PointsAccountCacheModel?> GetBalanceAsync(long memberId)
|
|
||||||
{
|
|
||||||
return await _cache.GetAsync(memberId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Fengling.Member.Application.Dtos;
|
using Fengling.Member.Application.Dtos;
|
||||||
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
||||||
using Fengling.Member.Domain.Aggregates.PointsRuleModel.Enums;
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel.Enums;
|
||||||
using Fengling.Member.Infrastructure.Repositories;
|
using Fengling.Member.Domain.Repositories;
|
||||||
using NetCorePal.Extensions.Domain;
|
using NetCorePal.Extensions.Domain;
|
||||||
|
|
||||||
namespace Fengling.Member.Application.Services;
|
namespace Fengling.Member.Application.Services;
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分历史仓储接口
|
|
||||||
/// </summary>
|
|
||||||
public interface IPointsHistoryRepository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 检查 SourceId 是否已存在(幂等性检查)
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> ExistsBySourceIdAsync(string sourceId, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取会员的积分明细(分页)
|
|
||||||
/// </summary>
|
|
||||||
Task<IEnumerable<PointsTransaction>> GetByMemberIdAsync(
|
|
||||||
long memberId,
|
|
||||||
int page = 1,
|
|
||||||
int pageSize = 20,
|
|
||||||
CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 统计会员的积分明细数量
|
|
||||||
/// </summary>
|
|
||||||
Task<int> CountByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取积分明细
|
|
||||||
/// </summary>
|
|
||||||
Task<PointsTransaction?> GetByIdAsync(long id, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加积分明细
|
|
||||||
/// </summary>
|
|
||||||
Task AddAsync(PointsTransaction transaction, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@ -1,18 +1,8 @@
|
|||||||
using Fengling.Member.Domain.Events.Points;
|
using Fengling.Member.Domain.Events.Points;
|
||||||
using NetCorePal.Extensions.Domain;
|
|
||||||
using NetCorePal.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Domain.Aggregates.PointsModel;
|
namespace Fengling.Member.Domain.Aggregates.PointsModel;
|
||||||
|
|
||||||
public partial record PointsAccountId : IGuidStronglyTypedId
|
public class PointsAccount : Entity<long>, IAggregateRoot
|
||||||
{
|
|
||||||
public static PointsAccountId New() => new PointsAccountId(Guid.NewGuid());
|
|
||||||
public Guid Value => this;
|
|
||||||
public static implicit operator Guid(PointsAccountId id) => id.Value;
|
|
||||||
public static implicit operator PointsAccountId(Guid value) => new PointsAccountId(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|
||||||
{
|
{
|
||||||
public long MemberId { get; private set; }
|
public long MemberId { get; private set; }
|
||||||
public long TenantId { get; private set; }
|
public long TenantId { get; private set; }
|
||||||
@ -22,8 +12,6 @@ public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|||||||
public int Version { get; private set; } = 1;
|
public int Version { get; private set; } = 1;
|
||||||
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
|
||||||
public DateTime? UpdatedAt { get; private set; }
|
public DateTime? UpdatedAt { get; private set; }
|
||||||
public Deleted Deleted { get; private set; } = new();
|
|
||||||
public RowVersion RowVersion { get; private set; } = new(0);
|
|
||||||
|
|
||||||
private readonly List<PointsTransaction> _transactions = new();
|
private readonly List<PointsTransaction> _transactions = new();
|
||||||
public IReadOnlyCollection<PointsTransaction> Transactions => _transactions.AsReadOnly();
|
public IReadOnlyCollection<PointsTransaction> Transactions => _transactions.AsReadOnly();
|
||||||
@ -60,7 +48,6 @@ public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|||||||
var transaction = PointsTransaction.Create(
|
var transaction = PointsTransaction.Create(
|
||||||
Id,
|
Id,
|
||||||
MemberId,
|
MemberId,
|
||||||
TenantId,
|
|
||||||
points,
|
points,
|
||||||
transactionType,
|
transactionType,
|
||||||
sourceId,
|
sourceId,
|
||||||
@ -73,7 +60,7 @@ public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
Version++;
|
Version++;
|
||||||
|
|
||||||
AddDomainEvent(new PointsChangedEvent(Id, MemberId, TenantId, points, TotalPoints, transactionType, sourceId, remark));
|
AddDomainEvent(new PointsChangedEvent(Id, MemberId, points, TotalPoints, transactionType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DeductPoints(int points, string transactionType, string sourceId, string? remark = null)
|
public bool DeductPoints(int points, string transactionType, string sourceId, string? remark = null)
|
||||||
@ -87,7 +74,6 @@ public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|||||||
var transaction = PointsTransaction.Create(
|
var transaction = PointsTransaction.Create(
|
||||||
Id,
|
Id,
|
||||||
MemberId,
|
MemberId,
|
||||||
TenantId,
|
|
||||||
points,
|
points,
|
||||||
transactionType,
|
transactionType,
|
||||||
sourceId,
|
sourceId,
|
||||||
@ -100,7 +86,7 @@ public class PointsAccount : Entity<PointsAccountId>, IAggregateRoot
|
|||||||
UpdatedAt = DateTime.UtcNow;
|
UpdatedAt = DateTime.UtcNow;
|
||||||
Version++;
|
Version++;
|
||||||
|
|
||||||
AddDomainEvent(new PointsChangedEvent(Id, MemberId, TenantId, -points, TotalPoints, transactionType, sourceId, remark));
|
AddDomainEvent(new PointsChangedEvent(Id, MemberId, -points, TotalPoints, transactionType));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,8 @@ namespace Fengling.Member.Domain.Aggregates.PointsModel;
|
|||||||
|
|
||||||
public class PointsTransaction : Entity<long>
|
public class PointsTransaction : Entity<long>
|
||||||
{
|
{
|
||||||
public PointsAccountId PointsAccountId { get; private set; } = PointsAccountId.New();
|
public long PointsAccountId { get; private set; }
|
||||||
public long MemberId { get; private set; }
|
public long MemberId { get; private set; }
|
||||||
public long TenantId { get; private set; }
|
|
||||||
public int Points { get; private set; }
|
public int Points { get; private set; }
|
||||||
public string TransactionType { get; private set; } = string.Empty;
|
public string TransactionType { get; private set; } = string.Empty;
|
||||||
public string SourceId { get; private set; } = string.Empty;
|
public string SourceId { get; private set; } = string.Empty;
|
||||||
@ -23,9 +22,8 @@ public class PointsTransaction : Entity<long>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static PointsTransaction Create(
|
public static PointsTransaction Create(
|
||||||
PointsAccountId pointsAccountId,
|
long pointsAccountId,
|
||||||
long memberId,
|
long memberId,
|
||||||
long tenantId,
|
|
||||||
int points,
|
int points,
|
||||||
string transactionType,
|
string transactionType,
|
||||||
string sourceId,
|
string sourceId,
|
||||||
@ -36,7 +34,6 @@ public class PointsTransaction : Entity<long>
|
|||||||
{
|
{
|
||||||
PointsAccountId = pointsAccountId,
|
PointsAccountId = pointsAccountId,
|
||||||
MemberId = memberId,
|
MemberId = memberId,
|
||||||
TenantId = tenantId,
|
|
||||||
Points = points,
|
Points = points,
|
||||||
TransactionType = transactionType,
|
TransactionType = transactionType,
|
||||||
SourceId = sourceId,
|
SourceId = sourceId,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Fengling.Member.Domain.Events.Member;
|
using Fengling.Member.Domain.Events.Member;
|
||||||
using NetCorePal.Extensions.Domain;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Domain.Aggregates.Users;
|
namespace Fengling.Member.Domain.Aggregates.Users;
|
||||||
|
|
||||||
@ -24,8 +23,6 @@ public class MemberEntity : Entity<MemberId>, IAggregateRoot
|
|||||||
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
|
||||||
public DateTime? UpdatedAt { get; private set; }
|
public DateTime? UpdatedAt { get; private set; }
|
||||||
public int Version { get; private set; } = 1;
|
public int Version { get; private set; } = 1;
|
||||||
public Deleted Deleted { get; private set; } = new();
|
|
||||||
public RowVersion RowVersion { get; private set; } = new(0);
|
|
||||||
|
|
||||||
private readonly List<MemberTag> _tags = new();
|
private readonly List<MemberTag> _tags = new();
|
||||||
public IReadOnlyCollection<MemberTag> Tags => _tags.AsReadOnly();
|
public IReadOnlyCollection<MemberTag> Tags => _tags.AsReadOnly();
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Domain.Events.Points;
|
namespace Fengling.Member.Domain.Events.Points;
|
||||||
|
|
||||||
public record PointsChangedEvent(
|
public record PointsChangedEvent(
|
||||||
PointsAccountId AccountId,
|
long AccountId,
|
||||||
long MemberId,
|
long MemberId,
|
||||||
long TenantId,
|
|
||||||
int ChangedPoints,
|
int ChangedPoints,
|
||||||
int NewBalance,
|
int NewBalance,
|
||||||
string TransactionType,
|
string TransactionType
|
||||||
string? SourceId,
|
|
||||||
string? Remark
|
|
||||||
) : IDomainEvent;
|
) : IDomainEvent;
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
||||||
|
|
||||||
|
namespace Fengling.Member.Domain.Repositories;
|
||||||
|
|
||||||
|
public interface IPointsRuleConditionRepository
|
||||||
|
{
|
||||||
|
Task<List<PointsRuleCondition>> GetByRuleIdAsync(PointsRuleId ruleId);
|
||||||
|
Task AddAsync(PointsRuleCondition condition);
|
||||||
|
Task DeleteByRuleIdAsync(PointsRuleId ruleId);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
||||||
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel.Enums;
|
||||||
|
|
||||||
|
namespace Fengling.Member.Domain.Repositories;
|
||||||
|
|
||||||
|
public interface IPointsRuleRepository
|
||||||
|
{
|
||||||
|
Task<PointsRule?> GetByIdAsync(PointsRuleId id);
|
||||||
|
Task<PointsRule?> GetByCodeAsync(string code);
|
||||||
|
Task<List<PointsRule>> GetActiveRulesAsync();
|
||||||
|
Task<List<PointsRule>> GetActiveRulesByDimensionAsync(DimensionType dimensionType, string dimensionValue);
|
||||||
|
Task AddAsync(PointsRule rule);
|
||||||
|
Task UpdateAsync(PointsRule rule);
|
||||||
|
Task DeleteAsync(PointsRuleId id);
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ public class DesignTimeApplicationDbContextFactory: IDesignTimeDbContextFactory<
|
|||||||
services.AddDbContext<ApplicationDbContext>(options =>
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
{
|
{
|
||||||
// change connectionstring if you want to run command “dotnet ef database update”
|
// change connectionstring if you want to run command “dotnet ef database update”
|
||||||
options.UseNpgsql("Host=192.168.100.10;Database=fengling_member;Username=movingsam;Password=sl52788542",
|
options.UseNpgsql("Host=any;Database=any;Username=any;Password=any",
|
||||||
b =>
|
b =>
|
||||||
{
|
{
|
||||||
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);
|
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);
|
||||||
|
|||||||
@ -14,8 +14,7 @@ public class PointsAccountEntityTypeConfiguration : IEntityTypeConfiguration<Poi
|
|||||||
|
|
||||||
builder.Property(p => p.Id)
|
builder.Property(p => p.Id)
|
||||||
.HasColumnName("id")
|
.HasColumnName("id")
|
||||||
.UseGuidVersion7ValueGenerator()
|
.UseIdentityColumn();
|
||||||
.HasComment("积分账户标识");
|
|
||||||
|
|
||||||
builder.Property(p => p.MemberId)
|
builder.Property(p => p.MemberId)
|
||||||
.HasColumnName("user_id")
|
.HasColumnName("user_id")
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
namespace Fengling.Member.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 租户访问器接口
|
|
||||||
/// 用于从不同来源获取当前请求的 TenantId
|
|
||||||
/// - HTTP 请求:从 JWT Claim 获取
|
|
||||||
/// - 消息队列消费者:从消息 Header 获取
|
|
||||||
/// </summary>
|
|
||||||
public interface ITenantAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前租户ID
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>租户ID,如果无法获取则返回 null</returns>
|
|
||||||
long? GetTenantId();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 尝试获取当前租户ID
|
|
||||||
/// </summary>
|
|
||||||
bool TryGetTenantId(out long tenantId);
|
|
||||||
}
|
|
||||||
140
src/Fengling.Member.Infrastructure/Migrations/20260122054728_Init.Designer.cs
generated
Normal file
140
src/Fengling.Member.Infrastructure/Migrations/20260122054728_Init.Designer.cs
generated
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Fengling.Member.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260122054728_Init")]
|
||||||
|
partial class Init
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Instance")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLockTime")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("CAPLock", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.ToTable("CAPPublishedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(400)
|
||||||
|
.HasColumnType("character varying(400)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.ToTable("CAPReceivedMessage", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Init : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPLock",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Instance = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
LastLockTime = table.Column<DateTime>(type: "TIMESTAMP", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPLock", x => x.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPPublishedMessage",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Retries = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
|
||||||
|
StatusName = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPReceivedMessage",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
|
||||||
|
Group = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Retries = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
|
||||||
|
StatusName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpiresAt_StatusName",
|
||||||
|
table: "CAPPublishedMessage",
|
||||||
|
columns: new[] { "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Version_ExpiresAt_StatusName",
|
||||||
|
table: "CAPPublishedMessage",
|
||||||
|
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpiresAt_StatusName1",
|
||||||
|
table: "CAPReceivedMessage",
|
||||||
|
columns: new[] { "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Version_ExpiresAt_StatusName1",
|
||||||
|
table: "CAPReceivedMessage",
|
||||||
|
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPLock");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPPublishedMessage");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPReceivedMessage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
429
src/Fengling.Member.Infrastructure/Migrations/20260205051658_AddMemberAndPointsEntities.Designer.cs
generated
Normal file
429
src/Fengling.Member.Infrastructure/Migrations/20260205051658_AddMemberAndPointsEntities.Designer.cs
generated
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Fengling.Member.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260205051658_AddMemberAndPointsEntities")]
|
||||||
|
partial class AddMemberAndPointsEntities
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("FrozenPoints")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("frozen_points");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<int>("TotalPoints")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("points");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("version");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_points_account_memberid");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId", "TenantId")
|
||||||
|
.HasDatabaseName("idx_points_account_member_tenant");
|
||||||
|
|
||||||
|
b.ToTable("mka_integraldetails", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("Points")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("PointsAccountId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Remark")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TransactionType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("TransactionTypeCategory")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PointsAccountId");
|
||||||
|
|
||||||
|
b.ToTable("PointsTransaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("OpenId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("open_id");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("phone_number");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<string>("UnionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("union_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("version");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OpenId")
|
||||||
|
.HasDatabaseName("idx_member_openid");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_member_tenantid");
|
||||||
|
|
||||||
|
b.HasIndex("UnionId")
|
||||||
|
.HasDatabaseName("idx_member_unionid");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "PhoneNumber")
|
||||||
|
.HasDatabaseName("idx_member_tenant_phone");
|
||||||
|
|
||||||
|
b.ToTable("fls_member", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("member_id");
|
||||||
|
|
||||||
|
b.Property<string>("TagId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.Property<string>("TagName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("tag_name");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TagId")
|
||||||
|
.HasDatabaseName("idx_membertag_tagid");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId", "TagId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_membertag_member_tag");
|
||||||
|
|
||||||
|
b.ToTable("fls_member_tag", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("AuthorizedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("authorized_at");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_login_at");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("member_id");
|
||||||
|
|
||||||
|
b.Property<string>("OpenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("open_id");
|
||||||
|
|
||||||
|
b.Property<string>("UnionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("union_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId")
|
||||||
|
.HasDatabaseName("idx_wechat_auth_memberid");
|
||||||
|
|
||||||
|
b.HasIndex("OpenId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_wechat_auth_openid");
|
||||||
|
|
||||||
|
b.HasIndex("UnionId")
|
||||||
|
.HasDatabaseName("idx_wechat_auth_unionid");
|
||||||
|
|
||||||
|
b.ToTable("fls_wechat_authorization", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Instance")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLockTime")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("CAPLock", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.ToTable("CAPPublishedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(400)
|
||||||
|
.HasColumnType("character varying(400)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.ToTable("CAPReceivedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("PointsAccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
|
||||||
|
.WithMany("Tags")
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
|
||||||
|
.WithMany("WechatAuthorizations")
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Tags");
|
||||||
|
|
||||||
|
b.Navigation("WechatAuthorizations");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,208 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMemberAndPointsEntities : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fls_member",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
tenant_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
phone_number = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||||
|
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_fls_member", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "mka_integraldetails",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
tenant_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
frozen_points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||||
|
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_mka_integraldetails", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fls_member_tag",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
member_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
tag_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
tag_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_fls_member_tag", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_fls_member_tag_fls_member_member_id",
|
||||||
|
column: x => x.member_id,
|
||||||
|
principalTable: "fls_member",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fls_wechat_authorization",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
member_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
authorized_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
last_login_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_fls_wechat_authorization", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_fls_wechat_authorization_fls_member_member_id",
|
||||||
|
column: x => x.member_id,
|
||||||
|
principalTable: "fls_member",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PointsTransaction",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
PointsAccountId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
MemberId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Points = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TransactionType = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SourceId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TransactionTypeCategory = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Remark = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PointsTransaction", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PointsTransaction_mka_integraldetails_PointsAccountId",
|
||||||
|
column: x => x.PointsAccountId,
|
||||||
|
principalTable: "mka_integraldetails",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_member_openid",
|
||||||
|
table: "fls_member",
|
||||||
|
column: "open_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_member_tenant_phone",
|
||||||
|
table: "fls_member",
|
||||||
|
columns: new[] { "tenant_id", "phone_number" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_member_tenantid",
|
||||||
|
table: "fls_member",
|
||||||
|
column: "tenant_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_member_unionid",
|
||||||
|
table: "fls_member",
|
||||||
|
column: "union_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_membertag_member_tag",
|
||||||
|
table: "fls_member_tag",
|
||||||
|
columns: new[] { "member_id", "tag_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_membertag_tagid",
|
||||||
|
table: "fls_member_tag",
|
||||||
|
column: "tag_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_wechat_auth_memberid",
|
||||||
|
table: "fls_wechat_authorization",
|
||||||
|
column: "member_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_wechat_auth_openid",
|
||||||
|
table: "fls_wechat_authorization",
|
||||||
|
column: "open_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_wechat_auth_unionid",
|
||||||
|
table: "fls_wechat_authorization",
|
||||||
|
column: "union_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_points_account_member_tenant",
|
||||||
|
table: "mka_integraldetails",
|
||||||
|
columns: new[] { "user_id", "tenant_id" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_points_account_memberid",
|
||||||
|
table: "mka_integraldetails",
|
||||||
|
column: "user_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PointsTransaction_PointsAccountId",
|
||||||
|
table: "PointsTransaction",
|
||||||
|
column: "PointsAccountId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fls_member_tag");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fls_wechat_authorization");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PointsTransaction");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fls_member");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "mka_integraldetails");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
598
src/Fengling.Member.Infrastructure/Migrations/20260209110951_AddPointsRuleSystem.Designer.cs
generated
Normal file
598
src/Fengling.Member.Infrastructure/Migrations/20260209110951_AddPointsRuleSystem.Designer.cs
generated
Normal file
@ -0,0 +1,598 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Fengling.Member.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260209110951_AddPointsRuleSystem")]
|
||||||
|
partial class AddPointsRuleSystem
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("FrozenPoints")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("frozen_points");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<int>("TotalPoints")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("points");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("version");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_points_account_memberid");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId", "TenantId")
|
||||||
|
.HasDatabaseName("idx_points_account_member_tenant");
|
||||||
|
|
||||||
|
b.ToTable("mka_integraldetails", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpireAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("Points")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("PointsAccountId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Remark")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TransactionType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("TransactionTypeCategory")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PointsAccountId");
|
||||||
|
|
||||||
|
b.ToTable("PointsTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasComment("规则标识");
|
||||||
|
|
||||||
|
b.Property<int>("BasePoints")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("基础积分");
|
||||||
|
|
||||||
|
b.Property<int>("CalculationMode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("计算模式");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasComment("规则编码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EffectiveFrom")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("生效开始时间");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EffectiveTo")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("生效结束时间");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否启用");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasComment("规则名称");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("优先级");
|
||||||
|
|
||||||
|
b.Property<int>("RuleType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("规则类型");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.Property<int>("ValidityDays")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("有效期天数");
|
||||||
|
|
||||||
|
b.Property<decimal?>("WeightFactor")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("numeric(18,4)")
|
||||||
|
.HasComment("权重因子");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.ToTable("PointsRules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRuleCondition", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasComment("条件标识");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<int>("DimensionType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("维度类型");
|
||||||
|
|
||||||
|
b.Property<string>("DimensionValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasComment("维度值");
|
||||||
|
|
||||||
|
b.Property<string>("Operator")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasComment("操作符");
|
||||||
|
|
||||||
|
b.Property<Guid>("RuleId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasComment("关联规则标识");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RuleId", "DimensionType");
|
||||||
|
|
||||||
|
b.ToTable("PointsRuleConditions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("OpenId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("open_id");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("phone_number");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("tenant_id");
|
||||||
|
|
||||||
|
b.Property<string>("UnionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("union_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("version");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OpenId")
|
||||||
|
.HasDatabaseName("idx_member_openid");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.HasDatabaseName("idx_member_tenantid");
|
||||||
|
|
||||||
|
b.HasIndex("UnionId")
|
||||||
|
.HasDatabaseName("idx_member_unionid");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "PhoneNumber")
|
||||||
|
.HasDatabaseName("idx_member_tenant_phone");
|
||||||
|
|
||||||
|
b.ToTable("fls_member", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("member_id");
|
||||||
|
|
||||||
|
b.Property<string>("TagId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.Property<string>("TagName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("tag_name");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TagId")
|
||||||
|
.HasDatabaseName("idx_membertag_tagid");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId", "TagId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_membertag_member_tag");
|
||||||
|
|
||||||
|
b.ToTable("fls_member_tag", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.OAuthAuthorization", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AccessToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AuthorizedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("OpenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Provider")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("TokenExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("UnionId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId");
|
||||||
|
|
||||||
|
b.ToTable("OAuthAuthorization");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("AuthorizedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("authorized_at");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_login_at");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("member_id");
|
||||||
|
|
||||||
|
b.Property<string>("OpenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("open_id");
|
||||||
|
|
||||||
|
b.Property<string>("UnionId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("union_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MemberId")
|
||||||
|
.HasDatabaseName("idx_wechat_auth_memberid");
|
||||||
|
|
||||||
|
b.HasIndex("OpenId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx_wechat_auth_openid");
|
||||||
|
|
||||||
|
b.HasIndex("UnionId")
|
||||||
|
.HasDatabaseName("idx_wechat_auth_unionid");
|
||||||
|
|
||||||
|
b.ToTable("fls_wechat_authorization", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Instance")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLockTime")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("CAPLock", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("character varying(40)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.ToTable("CAPPublishedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(400)
|
||||||
|
.HasColumnType("character varying(400)");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.ToTable("CAPReceivedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("PointsAccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRuleCondition", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", null)
|
||||||
|
.WithMany("Conditions")
|
||||||
|
.HasForeignKey("RuleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
|
||||||
|
.WithMany("Tags")
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.OAuthAuthorization", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
|
||||||
|
.WithMany("OAuthAuthorizations")
|
||||||
|
.HasForeignKey("MemberId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Conditions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OAuthAuthorizations");
|
||||||
|
|
||||||
|
b.Navigation("Tags");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPointsRuleSystem : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_fls_wechat_authorization_fls_member_member_id",
|
||||||
|
table: "fls_wechat_authorization");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_PointsTransaction_mka_integraldetails_PointsAccountId",
|
||||||
|
table: "PointsTransaction");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_PointsTransaction",
|
||||||
|
table: "PointsTransaction");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "PointsTransaction",
|
||||||
|
newName: "PointsTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_PointsTransaction_PointsAccountId",
|
||||||
|
table: "PointsTransactions",
|
||||||
|
newName: "IX_PointsTransactions_PointsAccountId");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ExpireAt",
|
||||||
|
table: "PointsTransactions",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_PointsTransactions",
|
||||||
|
table: "PointsTransactions",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OAuthAuthorization",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
MemberId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Provider = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
OpenId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UnionId = table.Column<string>(type: "text", nullable: true),
|
||||||
|
AccessToken = table.Column<string>(type: "text", nullable: true),
|
||||||
|
RefreshToken = table.Column<string>(type: "text", nullable: true),
|
||||||
|
TokenExpiredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
AuthorizedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
LastLoginAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OAuthAuthorization", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OAuthAuthorization_fls_member_MemberId",
|
||||||
|
column: x => x.MemberId,
|
||||||
|
principalTable: "fls_member",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PointsRules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "规则标识"),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false, comment: "规则名称"),
|
||||||
|
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "规则编码"),
|
||||||
|
RuleType = table.Column<int>(type: "integer", nullable: false, comment: "规则类型"),
|
||||||
|
BasePoints = table.Column<int>(type: "integer", nullable: false, comment: "基础积分"),
|
||||||
|
WeightFactor = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: true, comment: "权重因子"),
|
||||||
|
ValidityDays = table.Column<int>(type: "integer", nullable: false, comment: "有效期天数"),
|
||||||
|
Priority = table.Column<int>(type: "integer", nullable: false, comment: "优先级"),
|
||||||
|
CalculationMode = table.Column<int>(type: "integer", nullable: false, comment: "计算模式"),
|
||||||
|
IsActive = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用"),
|
||||||
|
EffectiveFrom = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "生效开始时间"),
|
||||||
|
EffectiveTo = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "生效结束时间"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PointsRules", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PointsRuleConditions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "条件标识"),
|
||||||
|
RuleId = table.Column<Guid>(type: "uuid", nullable: false, comment: "关联规则标识"),
|
||||||
|
DimensionType = table.Column<int>(type: "integer", nullable: false, comment: "维度类型"),
|
||||||
|
DimensionValue = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false, comment: "维度值"),
|
||||||
|
Operator = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true, comment: "操作符"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PointsRuleConditions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PointsRuleConditions_PointsRules_RuleId",
|
||||||
|
column: x => x.RuleId,
|
||||||
|
principalTable: "PointsRules",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OAuthAuthorization_MemberId",
|
||||||
|
table: "OAuthAuthorization",
|
||||||
|
column: "MemberId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PointsRuleConditions_RuleId_DimensionType",
|
||||||
|
table: "PointsRuleConditions",
|
||||||
|
columns: new[] { "RuleId", "DimensionType" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PointsRules_Code",
|
||||||
|
table: "PointsRules",
|
||||||
|
column: "Code",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PointsRules_IsActive",
|
||||||
|
table: "PointsRules",
|
||||||
|
column: "IsActive");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_PointsTransactions_mka_integraldetails_PointsAccountId",
|
||||||
|
table: "PointsTransactions",
|
||||||
|
column: "PointsAccountId",
|
||||||
|
principalTable: "mka_integraldetails",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_PointsTransactions_mka_integraldetails_PointsAccountId",
|
||||||
|
table: "PointsTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OAuthAuthorization");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PointsRuleConditions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PointsRules");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_PointsTransactions",
|
||||||
|
table: "PointsTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExpireAt",
|
||||||
|
table: "PointsTransactions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "PointsTransactions",
|
||||||
|
newName: "PointsTransaction");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_PointsTransactions_PointsAccountId",
|
||||||
|
table: "PointsTransaction",
|
||||||
|
newName: "IX_PointsTransaction_PointsAccountId");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_PointsTransaction",
|
||||||
|
table: "PointsTransaction",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_fls_wechat_authorization_fls_member_member_id",
|
||||||
|
table: "fls_wechat_authorization",
|
||||||
|
column: "member_id",
|
||||||
|
principalTable: "fls_member",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_PointsTransaction_mka_integraldetails_PointsAccountId",
|
||||||
|
table: "PointsTransaction",
|
||||||
|
column: "PointsAccountId",
|
||||||
|
principalTable: "mka_integraldetails",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
namespace Fengling.Member.Infrastructure.Migrations
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20260217154010_Init")]
|
[Migration("20260209163416_ChangeMemberIdToGuid")]
|
||||||
partial class Init
|
partial class ChangeMemberIdToGuid
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@ -27,18 +27,17 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<long>("Id")
|
||||||
.HasColumnType("uuid")
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnName("id")
|
.HasColumnType("bigint")
|
||||||
.HasComment("积分账户标识");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
b.Property<bool>("Deleted")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("FrozenPoints")
|
b.Property<int>("FrozenPoints")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
@ -49,10 +48,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasColumnName("user_id");
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
b.Property<int>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasColumnName("tenant_id");
|
.HasColumnName("tenant_id");
|
||||||
@ -106,8 +101,8 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
b.Property<int>("Points")
|
b.Property<int>("Points")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<Guid>("PointsAccountId")
|
b.Property<long>("PointsAccountId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("Remark")
|
b.Property<string>("Remark")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@ -116,9 +111,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("TransactionType")
|
b.Property<string>("TransactionType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@ -253,9 +245,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
b.Property<bool>("Deleted")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("OpenId")
|
b.Property<string>("OpenId")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
@ -266,10 +255,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(20)")
|
.HasColumnType("character varying(20)")
|
||||||
.HasColumnName("phone_number");
|
.HasColumnName("phone_number");
|
||||||
|
|
||||||
b.Property<int>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ChangeMemberIdToGuid : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "MemberId",
|
||||||
|
table: "OAuthAuthorization",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(long),
|
||||||
|
oldType: "bigint");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "member_id",
|
||||||
|
table: "fls_member_tag",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(long),
|
||||||
|
oldType: "bigint");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "id",
|
||||||
|
table: "fls_member",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
comment: "会员标识",
|
||||||
|
oldClrType: typeof(long),
|
||||||
|
oldType: "bigint")
|
||||||
|
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<long>(
|
||||||
|
name: "MemberId",
|
||||||
|
table: "OAuthAuthorization",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<long>(
|
||||||
|
name: "member_id",
|
||||||
|
table: "fls_member_tag",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<long>(
|
||||||
|
name: "id",
|
||||||
|
table: "fls_member",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid",
|
||||||
|
oldComment: "会员标识")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,390 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Fengling.Member.Infrastructure.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Init : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "CAPLock",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Key = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
|
||||||
Instance = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
|
||||||
LastLockTime = table.Column<DateTime>(type: "TIMESTAMP", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_CAPLock", x => x.Key);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "CAPPublishedMessage",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
|
||||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
|
||||||
Content = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Retries = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
|
|
||||||
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
|
|
||||||
StatusName = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "CAPReceivedMessage",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Version = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
|
||||||
Name = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
|
|
||||||
Group = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
|
||||||
Content = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Retries = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
Added = table.Column<DateTime>(type: "TIMESTAMP", nullable: false),
|
|
||||||
ExpiresAt = table.Column<DateTime>(type: "TIMESTAMP", nullable: true),
|
|
||||||
StatusName = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "fls_member",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false, comment: "会员标识"),
|
|
||||||
tenant_id = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
phone_number = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
|
||||||
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
|
||||||
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
|
||||||
status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
|
||||||
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
||||||
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
|
||||||
Deleted = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
RowVersion = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_fls_member", x => x.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "fls_wechat_authorization",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
member_id = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
open_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
|
||||||
union_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
|
||||||
authorized_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
last_login_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_fls_wechat_authorization", x => x.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "mka_integraldetails",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false, comment: "积分账户标识"),
|
|
||||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
tenant_id = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
|
||||||
frozen_points = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
|
||||||
version = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
|
||||||
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
||||||
Deleted = table.Column<bool>(type: "boolean", nullable: false),
|
|
||||||
RowVersion = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_mka_integraldetails", x => x.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PointsRules",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "规则标识"),
|
|
||||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false, comment: "规则名称"),
|
|
||||||
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "规则编码"),
|
|
||||||
RuleType = table.Column<int>(type: "integer", nullable: false, comment: "规则类型"),
|
|
||||||
BasePoints = table.Column<int>(type: "integer", nullable: false, comment: "基础积分"),
|
|
||||||
WeightFactor = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: true, comment: "权重因子"),
|
|
||||||
ValidityDays = table.Column<int>(type: "integer", nullable: false, comment: "有效期天数"),
|
|
||||||
Priority = table.Column<int>(type: "integer", nullable: false, comment: "优先级"),
|
|
||||||
CalculationMode = table.Column<int>(type: "integer", nullable: false, comment: "计算模式"),
|
|
||||||
IsActive = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用"),
|
|
||||||
EffectiveFrom = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "生效开始时间"),
|
|
||||||
EffectiveTo = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "生效结束时间"),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间"),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "更新时间")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PointsRules", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "fls_member_tag",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
member_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
tag_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
|
||||||
tag_name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
|
||||||
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_fls_member_tag", x => x.id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_fls_member_tag_fls_member_member_id",
|
|
||||||
column: x => x.member_id,
|
|
||||||
principalTable: "fls_member",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "OAuthAuthorization",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
MemberId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
Provider = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
OpenId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
UnionId = table.Column<string>(type: "text", nullable: true),
|
|
||||||
AccessToken = table.Column<string>(type: "text", nullable: true),
|
|
||||||
RefreshToken = table.Column<string>(type: "text", nullable: true),
|
|
||||||
TokenExpiredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
||||||
AuthorizedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
LastLoginAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_OAuthAuthorization", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_OAuthAuthorization_fls_member_MemberId",
|
|
||||||
column: x => x.MemberId,
|
|
||||||
principalTable: "fls_member",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PointsTransactions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
PointsAccountId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
MemberId = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
TenantId = table.Column<long>(type: "bigint", nullable: false),
|
|
||||||
Points = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
TransactionType = table.Column<string>(type: "text", nullable: false),
|
|
||||||
SourceId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TransactionTypeCategory = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Remark = table.Column<string>(type: "text", nullable: true),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
ExpireAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PointsTransactions", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PointsTransactions_mka_integraldetails_PointsAccountId",
|
|
||||||
column: x => x.PointsAccountId,
|
|
||||||
principalTable: "mka_integraldetails",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PointsRuleConditions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "条件标识"),
|
|
||||||
RuleId = table.Column<Guid>(type: "uuid", nullable: false, comment: "关联规则标识"),
|
|
||||||
DimensionType = table.Column<int>(type: "integer", nullable: false, comment: "维度类型"),
|
|
||||||
DimensionValue = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false, comment: "维度值"),
|
|
||||||
Operator = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true, comment: "操作符"),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PointsRuleConditions", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PointsRuleConditions_PointsRules_RuleId",
|
|
||||||
column: x => x.RuleId,
|
|
||||||
principalTable: "PointsRules",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpiresAt_StatusName",
|
|
||||||
table: "CAPPublishedMessage",
|
|
||||||
columns: new[] { "ExpiresAt", "StatusName" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Version_ExpiresAt_StatusName",
|
|
||||||
table: "CAPPublishedMessage",
|
|
||||||
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpiresAt_StatusName1",
|
|
||||||
table: "CAPReceivedMessage",
|
|
||||||
columns: new[] { "ExpiresAt", "StatusName" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Version_ExpiresAt_StatusName1",
|
|
||||||
table: "CAPReceivedMessage",
|
|
||||||
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_member_openid",
|
|
||||||
table: "fls_member",
|
|
||||||
column: "open_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_member_tenant_phone",
|
|
||||||
table: "fls_member",
|
|
||||||
columns: new[] { "tenant_id", "phone_number" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_member_tenantid",
|
|
||||||
table: "fls_member",
|
|
||||||
column: "tenant_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_member_unionid",
|
|
||||||
table: "fls_member",
|
|
||||||
column: "union_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_membertag_member_tag",
|
|
||||||
table: "fls_member_tag",
|
|
||||||
columns: new[] { "member_id", "tag_id" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_membertag_tagid",
|
|
||||||
table: "fls_member_tag",
|
|
||||||
column: "tag_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_wechat_auth_memberid",
|
|
||||||
table: "fls_wechat_authorization",
|
|
||||||
column: "member_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_wechat_auth_openid",
|
|
||||||
table: "fls_wechat_authorization",
|
|
||||||
column: "open_id",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_wechat_auth_unionid",
|
|
||||||
table: "fls_wechat_authorization",
|
|
||||||
column: "union_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_points_account_member_tenant",
|
|
||||||
table: "mka_integraldetails",
|
|
||||||
columns: new[] { "user_id", "tenant_id" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "idx_points_account_memberid",
|
|
||||||
table: "mka_integraldetails",
|
|
||||||
column: "user_id",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_OAuthAuthorization_MemberId",
|
|
||||||
table: "OAuthAuthorization",
|
|
||||||
column: "MemberId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PointsRuleConditions_RuleId_DimensionType",
|
|
||||||
table: "PointsRuleConditions",
|
|
||||||
columns: new[] { "RuleId", "DimensionType" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PointsRules_Code",
|
|
||||||
table: "PointsRules",
|
|
||||||
column: "Code",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PointsRules_IsActive",
|
|
||||||
table: "PointsRules",
|
|
||||||
column: "IsActive");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PointsTransactions_PointsAccountId",
|
|
||||||
table: "PointsTransactions",
|
|
||||||
column: "PointsAccountId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "CAPLock");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "CAPPublishedMessage");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "CAPReceivedMessage");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "fls_member_tag");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "fls_wechat_authorization");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "OAuthAuthorization");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PointsRuleConditions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PointsTransactions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "fls_member");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PointsRules");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "mka_integraldetails");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,18 +24,17 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<long>("Id")
|
||||||
.HasColumnType("uuid")
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnName("id")
|
.HasColumnType("bigint")
|
||||||
.HasComment("积分账户标识");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
b.Property<bool>("Deleted")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("FrozenPoints")
|
b.Property<int>("FrozenPoints")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
@ -46,10 +45,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasColumnName("user_id");
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
b.Property<int>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasColumnName("tenant_id");
|
.HasColumnName("tenant_id");
|
||||||
@ -103,8 +98,8 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
b.Property<int>("Points")
|
b.Property<int>("Points")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<Guid>("PointsAccountId")
|
b.Property<long>("PointsAccountId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("Remark")
|
b.Property<string>("Remark")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@ -113,9 +108,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
|
||||||
.HasColumnType("bigint");
|
|
||||||
|
|
||||||
b.Property<string>("TransactionType")
|
b.Property<string>("TransactionType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@ -250,9 +242,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("created_at");
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
b.Property<bool>("Deleted")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<string>("OpenId")
|
b.Property<string>("OpenId")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
@ -263,10 +252,6 @@ namespace Fengling.Member.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(20)")
|
.HasColumnType("character varying(20)")
|
||||||
.HasColumnName("phone_number");
|
.HasColumnName("phone_number");
|
||||||
|
|
||||||
b.Property<int>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using Fengling.Member.Domain.Aggregates.PointsModel;
|
|||||||
|
|
||||||
namespace Fengling.Member.Infrastructure.Repositories;
|
namespace Fengling.Member.Infrastructure.Repositories;
|
||||||
|
|
||||||
public interface IPointsAccountRepository : IRepository<PointsAccount, PointsAccountId>
|
public interface IPointsAccountRepository : IRepository<PointsAccount, long>
|
||||||
{
|
{
|
||||||
Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
|
Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default);
|
||||||
Task<PointsAccount?> GetByMemberIdAndTenantIdAsync(long memberId, long tenantId, CancellationToken cancellationToken = default);
|
Task<PointsAccount?> GetByMemberIdAndTenantIdAsync(long memberId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
@ -12,7 +12,7 @@ public interface IPointsAccountRepository : IRepository<PointsAccount, PointsAcc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class PointsAccountRepository(ApplicationDbContext context) :
|
public class PointsAccountRepository(ApplicationDbContext context) :
|
||||||
RepositoryBase<PointsAccount, PointsAccountId, ApplicationDbContext>(context), IPointsAccountRepository
|
RepositoryBase<PointsAccount, long, ApplicationDbContext>(context), IPointsAccountRepository
|
||||||
{
|
{
|
||||||
public async Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
|
public async Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Infrastructure.Repositories;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 积分历史仓储实现
|
|
||||||
/// </summary>
|
|
||||||
public class PointsHistoryRepository : IPointsHistoryRepository
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public PointsHistoryRepository(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ExistsBySourceIdAsync(string sourceId, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _context.PointsTransactions
|
|
||||||
.AnyAsync(t => t.SourceId == sourceId, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<PointsTransaction>> GetByMemberIdAsync(
|
|
||||||
long memberId,
|
|
||||||
int page = 1,
|
|
||||||
int pageSize = 20,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _context.PointsTransactions
|
|
||||||
.Where(t => t.MemberId == memberId)
|
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> CountByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _context.PointsTransactions
|
|
||||||
.CountAsync(t => t.MemberId == memberId, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PointsTransaction?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _context.PointsTransactions.FindAsync(new object[] { id }, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddAsync(PointsTransaction transaction, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
_context.PointsTransactions.Add(transaction);
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
||||||
|
using Fengling.Member.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Fengling.Member.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class PointsRuleConditionRepository : IPointsRuleConditionRepository
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public PointsRuleConditionRepository(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<PointsRuleCondition>> GetByRuleIdAsync(PointsRuleId ruleId)
|
||||||
|
{
|
||||||
|
return await _context.PointsRuleConditions
|
||||||
|
.Where(x => x.RuleId == ruleId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(PointsRuleCondition condition)
|
||||||
|
{
|
||||||
|
await _context.PointsRuleConditions.AddAsync(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteByRuleIdAsync(PointsRuleId ruleId)
|
||||||
|
{
|
||||||
|
var conditions = await GetByRuleIdAsync(ruleId);
|
||||||
|
_context.PointsRuleConditions.RemoveRange(conditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,27 +5,25 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace Fengling.Member.Infrastructure.Repositories;
|
namespace Fengling.Member.Infrastructure.Repositories;
|
||||||
|
|
||||||
public interface IPointsRuleRepository : IRepository<PointsRule, PointsRuleId>
|
public class PointsRuleRepository : IPointsRuleRepository
|
||||||
{
|
{
|
||||||
Task<PointsRule?> GetByIdAsync(PointsRuleId id);
|
private readonly ApplicationDbContext _context;
|
||||||
Task<PointsRule?> GetByCodeAsync(string code);
|
|
||||||
Task<List<PointsRule>> GetActiveRulesAsync();
|
public PointsRuleRepository(ApplicationDbContext context)
|
||||||
Task<List<PointsRule>> GetActiveRulesByDimensionAsync(DimensionType dimensionType, string dimensionValue);
|
{
|
||||||
}
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
public class PointsRuleRepository(ApplicationDbContext context)
|
|
||||||
: RepositoryBase<PointsRule, PointsRuleId, ApplicationDbContext>(context), IPointsRuleRepository
|
|
||||||
{
|
|
||||||
public async Task<PointsRule?> GetByIdAsync(PointsRuleId id)
|
public async Task<PointsRule?> GetByIdAsync(PointsRuleId id)
|
||||||
{
|
{
|
||||||
return await DbContext.PointsRules
|
return await _context.PointsRules
|
||||||
.Include(x => x.Conditions)
|
.Include(x => x.Conditions)
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PointsRule?> GetByCodeAsync(string code)
|
public async Task<PointsRule?> GetByCodeAsync(string code)
|
||||||
{
|
{
|
||||||
return await DbContext.PointsRules
|
return await _context.PointsRules
|
||||||
.Include(x => x.Conditions)
|
.Include(x => x.Conditions)
|
||||||
.FirstOrDefaultAsync(x => x.Code == code);
|
.FirstOrDefaultAsync(x => x.Code == code);
|
||||||
}
|
}
|
||||||
@ -33,7 +31,7 @@ public class PointsRuleRepository(ApplicationDbContext context)
|
|||||||
public async Task<List<PointsRule>> GetActiveRulesAsync()
|
public async Task<List<PointsRule>> GetActiveRulesAsync()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return await DbContext.PointsRules
|
return await _context.PointsRules
|
||||||
.Include(x => x.Conditions)
|
.Include(x => x.Conditions)
|
||||||
.Where(x => x.IsActive
|
.Where(x => x.IsActive
|
||||||
&& x.EffectiveFrom <= now
|
&& x.EffectiveFrom <= now
|
||||||
@ -47,7 +45,7 @@ public class PointsRuleRepository(ApplicationDbContext context)
|
|||||||
string dimensionValue)
|
string dimensionValue)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
return await DbContext.PointsRules
|
return await _context.PointsRules
|
||||||
.Include(x => x.Conditions)
|
.Include(x => x.Conditions)
|
||||||
.Where(x => x.IsActive
|
.Where(x => x.IsActive
|
||||||
&& x.EffectiveFrom <= now
|
&& x.EffectiveFrom <= now
|
||||||
@ -56,4 +54,24 @@ public class PointsRuleRepository(ApplicationDbContext context)
|
|||||||
.OrderByDescending(x => x.Priority)
|
.OrderByDescending(x => x.Priority)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(PointsRule rule)
|
||||||
|
{
|
||||||
|
await _context.PointsRules.AddAsync(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(PointsRule rule)
|
||||||
|
{
|
||||||
|
_context.PointsRules.Update(rule);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(PointsRuleId id)
|
||||||
|
{
|
||||||
|
var rule = await GetByIdAsync(id);
|
||||||
|
if (rule != null)
|
||||||
|
{
|
||||||
|
_context.PointsRules.Remove(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
Unable to retrieve project metadata. Ensure it's an SDK-style project.
|
|
||||||
25
src/Fengling.Member.Web/Endpoints/HelloEndpoint.cs
Normal file
25
src/Fengling.Member.Web/Endpoints/HelloEndpoint.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using FastEndpoints.Swagger;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using NetCorePal.Extensions.Dto;
|
||||||
|
|
||||||
|
namespace Fengling.Member.Web.Endpoints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hello
|
||||||
|
/// </summary>
|
||||||
|
public class HelloEndpoint : EndpointWithoutRequest<ResponseData<string>>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Tags("Hello");
|
||||||
|
Description(b => b.AutoTagOverride("Hello"));
|
||||||
|
Get("/api/hello");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Send.OkAsync("hello".AsResponseData(), cancellation: ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,445 +0,0 @@
|
|||||||
using FastEndpoints;
|
|
||||||
using Fengling.Member.Domain.Aggregates.Users;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Fengling.Member.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Web.Endpoints.v1;
|
|
||||||
|
|
||||||
public class QueryMembersEndpoint : Endpoint<QueryMembersRequest, QueryMembersResponse>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public QueryMembersEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Get("/api/v1/members");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "查询会员列表";
|
|
||||||
s.Description = "分页查询会员列表,支持按手机号、状态筛选";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(QueryMembersRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var query = _dbContext.Members
|
|
||||||
.Where(m => m.TenantId == req.TenantId);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(req.PhoneNumber))
|
|
||||||
{
|
|
||||||
query = query.Where(m => m.PhoneNumber != null && m.PhoneNumber.Contains(req.PhoneNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(req.Status))
|
|
||||||
{
|
|
||||||
query = query.Where(m => m.Status.Value == req.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(req.OpenId))
|
|
||||||
{
|
|
||||||
query = query.Where(m => m.OpenId != null && m.OpenId.Contains(req.OpenId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(ct);
|
|
||||||
var members = await query
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.OrderByDescending(m => m.CreatedAt)
|
|
||||||
.Skip((req.Page - 1) * req.PageSize)
|
|
||||||
.Take(req.PageSize)
|
|
||||||
.Select(m => new MemberDto(
|
|
||||||
Id: m.Id.Value,
|
|
||||||
TenantId: m.TenantId,
|
|
||||||
PhoneNumber: m.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: m.OpenId ?? string.Empty,
|
|
||||||
UnionId: m.UnionId,
|
|
||||||
Status: m.Status.Value,
|
|
||||||
StatusDesc: m.Status.Description,
|
|
||||||
Tags: m.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: m.CreatedAt,
|
|
||||||
UpdatedAt: m.UpdatedAt
|
|
||||||
))
|
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
Response = new QueryMembersResponse(
|
|
||||||
Items: members,
|
|
||||||
Total: totalCount,
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QueryMembersRequest
|
|
||||||
{
|
|
||||||
[QueryParam]
|
|
||||||
public long TenantId { get; set; } = 1;
|
|
||||||
|
|
||||||
[QueryParam]
|
|
||||||
public string? PhoneNumber { get; set; }
|
|
||||||
|
|
||||||
[QueryParam]
|
|
||||||
public string? Status { get; set; }
|
|
||||||
|
|
||||||
[QueryParam]
|
|
||||||
public string? OpenId { get; set; }
|
|
||||||
|
|
||||||
[QueryParam]
|
|
||||||
public int Page { get; set; } = 1;
|
|
||||||
|
|
||||||
[QueryParam]
|
|
||||||
public int PageSize { get; set; } = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record QueryMembersResponse(
|
|
||||||
List<MemberDto> Items,
|
|
||||||
int Total,
|
|
||||||
int Page,
|
|
||||||
int PageSize
|
|
||||||
);
|
|
||||||
|
|
||||||
public record MemberDto(
|
|
||||||
Guid Id,
|
|
||||||
long TenantId,
|
|
||||||
string PhoneNumber,
|
|
||||||
string OpenId,
|
|
||||||
string? UnionId,
|
|
||||||
string Status,
|
|
||||||
string StatusDesc,
|
|
||||||
List<MemberTagDto> Tags,
|
|
||||||
DateTime CreatedAt,
|
|
||||||
DateTime? UpdatedAt
|
|
||||||
);
|
|
||||||
|
|
||||||
public record MemberTagDto(
|
|
||||||
string TagId,
|
|
||||||
string TagName
|
|
||||||
);
|
|
||||||
|
|
||||||
public class GetMemberEndpoint : Endpoint<GetMemberRequest, MemberDto>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public GetMemberEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Get("/api/v1/members/{Id}");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "获取会员详情";
|
|
||||||
s.Description = "根据ID获取会员详细信息";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(GetMemberRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
Response = new MemberDto(
|
|
||||||
Id: member.Id.Value,
|
|
||||||
TenantId: member.TenantId,
|
|
||||||
PhoneNumber: member.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: member.OpenId ?? string.Empty,
|
|
||||||
UnionId: member.UnionId,
|
|
||||||
Status: member.Status.Value,
|
|
||||||
StatusDesc: member.Status.Description,
|
|
||||||
Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: member.CreatedAt,
|
|
||||||
UpdatedAt: member.UpdatedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GetMemberRequest([FromRoute] Guid Id);
|
|
||||||
|
|
||||||
public class UpdateMemberEndpoint : Endpoint<UpdateMemberRequest, MemberDto>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public UpdateMemberEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Put("/api/v1/members/{Id}");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "更新会员";
|
|
||||||
s.Description = "更新会员基本信息";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(UpdateMemberRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(req.PhoneNumber))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
member.BindPhoneNumber(req.PhoneNumber);
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
AddError(ex.Message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
Response = new MemberDto(
|
|
||||||
Id: member.Id.Value,
|
|
||||||
TenantId: member.TenantId,
|
|
||||||
PhoneNumber: member.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: member.OpenId ?? string.Empty,
|
|
||||||
UnionId: member.UnionId,
|
|
||||||
Status: member.Status.Value,
|
|
||||||
StatusDesc: member.Status.Description,
|
|
||||||
Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: member.CreatedAt,
|
|
||||||
UpdatedAt: member.UpdatedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record UpdateMemberRequest(
|
|
||||||
[FromRoute] Guid Id,
|
|
||||||
string? PhoneNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
public class UpdateMemberStatusEndpoint : Endpoint<UpdateMemberStatusRequest, MemberDto>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public UpdateMemberStatusEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Put("/api/v1/members/{Id}/status");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "更新会员状态";
|
|
||||||
s.Description = "冻结/解冻/停用会员";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(UpdateMemberStatusRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (req.Action.ToLower())
|
|
||||||
{
|
|
||||||
case "freeze":
|
|
||||||
member.Freeze();
|
|
||||||
break;
|
|
||||||
case "unfreeze":
|
|
||||||
member.Unfreeze();
|
|
||||||
break;
|
|
||||||
case "deactivate":
|
|
||||||
member.Deactivate();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
AddError("无效的操作类型");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
Response = new MemberDto(
|
|
||||||
Id: member.Id.Value,
|
|
||||||
TenantId: member.TenantId,
|
|
||||||
PhoneNumber: member.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: member.OpenId ?? string.Empty,
|
|
||||||
UnionId: member.UnionId,
|
|
||||||
Status: member.Status.Value,
|
|
||||||
StatusDesc: member.Status.Description,
|
|
||||||
Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: member.CreatedAt,
|
|
||||||
UpdatedAt: member.UpdatedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record UpdateMemberStatusRequest
|
|
||||||
{
|
|
||||||
[FromRoute]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string Action { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DeleteMemberEndpoint : Endpoint<DeleteMemberRequest>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public DeleteMemberEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Delete("/api/v1/members/{Id}");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "删除会员";
|
|
||||||
s.Description = "软删除会员";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(DeleteMemberRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
member.Deactivate();
|
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DeleteMemberRequest([FromRoute] Guid Id);
|
|
||||||
|
|
||||||
public class AddMemberTagEndpoint : Endpoint<AddMemberTagRequest, MemberDto>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public AddMemberTagEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/v1/members/{Id}/tags");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "添加会员标签";
|
|
||||||
s.Description = "为会员添加标签";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(AddMemberTagRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
member.AddTag(req.TagId, req.TagName);
|
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
Response = new MemberDto(
|
|
||||||
Id: member.Id.Value,
|
|
||||||
TenantId: member.TenantId,
|
|
||||||
PhoneNumber: member.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: member.OpenId ?? string.Empty,
|
|
||||||
UnionId: member.UnionId,
|
|
||||||
Status: member.Status.Value,
|
|
||||||
StatusDesc: member.Status.Description,
|
|
||||||
Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: member.CreatedAt,
|
|
||||||
UpdatedAt: member.UpdatedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record AddMemberTagRequest
|
|
||||||
{
|
|
||||||
[FromRoute]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string TagId { get; set; } = string.Empty;
|
|
||||||
public string? TagName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RemoveMemberTagEndpoint : Endpoint<RemoveMemberTagRequest, MemberDto>
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _dbContext;
|
|
||||||
|
|
||||||
public RemoveMemberTagEndpoint(ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
_dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Delete("/api/v1/members/{Id}/tags/{TagId}");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "移除会员标签";
|
|
||||||
s.Description = "移除会员的指定标签";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(RemoveMemberTagRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var member = await _dbContext.Members
|
|
||||||
.Include(m => m.Tags)
|
|
||||||
.FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct);
|
|
||||||
|
|
||||||
if (member == null)
|
|
||||||
{
|
|
||||||
ThrowError("会员不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
member.RemoveTag(req.TagId);
|
|
||||||
await _dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
Response = new MemberDto(
|
|
||||||
Id: member.Id.Value,
|
|
||||||
TenantId: member.TenantId,
|
|
||||||
PhoneNumber: member.PhoneNumber ?? string.Empty,
|
|
||||||
OpenId: member.OpenId ?? string.Empty,
|
|
||||||
UnionId: member.UnionId,
|
|
||||||
Status: member.Status.Value,
|
|
||||||
StatusDesc: member.Status.Description,
|
|
||||||
Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(),
|
|
||||||
CreatedAt: member.CreatedAt,
|
|
||||||
UpdatedAt: member.UpdatedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record RemoveMemberTagRequest([FromRoute] Guid Id, [FromRoute] string TagId);
|
|
||||||
@ -2,8 +2,6 @@ using FastEndpoints;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Fengling.Member.Application.Commands.Points;
|
using Fengling.Member.Application.Commands.Points;
|
||||||
using Fengling.Member.Application.Services;
|
|
||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Web.Endpoints.v1;
|
namespace Fengling.Member.Web.Endpoints.v1;
|
||||||
|
|
||||||
@ -62,202 +60,9 @@ public class AddPointsRequest
|
|||||||
|
|
||||||
public class AddPointsResponse
|
public class AddPointsResponse
|
||||||
{
|
{
|
||||||
public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
|
public long AccountId { get; set; }
|
||||||
public long MemberId { get; set; }
|
public long MemberId { get; set; }
|
||||||
public int AddedPoints { get; set; }
|
public int AddedPoints { get; set; }
|
||||||
public int TotalPoints { get; set; }
|
public int TotalPoints { get; set; }
|
||||||
public DateTime TransactionAt { get; set; }
|
public DateTime TransactionAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetPointsBalanceEndpoint : Endpoint<GetPointsBalanceRequest, GetPointsBalanceResponse>
|
|
||||||
{
|
|
||||||
private readonly IPointsProcessingService _pointsService;
|
|
||||||
|
|
||||||
public GetPointsBalanceEndpoint(IPointsProcessingService pointsService)
|
|
||||||
{
|
|
||||||
_pointsService = pointsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Get("/api/v1/members/{MemberId}/points/balance");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "获取积分余额";
|
|
||||||
s.Description = "获取会员的当前积分余额";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(GetPointsBalanceRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var account = await _pointsService.GetBalanceAsync(req.MemberId);
|
|
||||||
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
Response = new GetPointsBalanceResponse
|
|
||||||
{
|
|
||||||
MemberId = req.MemberId,
|
|
||||||
TotalPoints = 0,
|
|
||||||
FrozenPoints = 0,
|
|
||||||
AvailablePoints = 0
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Response = new GetPointsBalanceResponse
|
|
||||||
{
|
|
||||||
MemberId = account.MemberId,
|
|
||||||
TotalPoints = account.TotalPoints,
|
|
||||||
FrozenPoints = account.FrozenPoints,
|
|
||||||
AvailablePoints = account.TotalPoints - account.FrozenPoints
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPointsBalanceRequest
|
|
||||||
{
|
|
||||||
[FromRoute]
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPointsBalanceResponse
|
|
||||||
{
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
public int TotalPoints { get; set; }
|
|
||||||
public int FrozenPoints { get; set; }
|
|
||||||
public int AvailablePoints { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPointsHistoryEndpoint(IPointsHistoryRepository historyRepository)
|
|
||||||
: Endpoint<GetPointsHistoryRequest, GetPointsHistoryResponse>
|
|
||||||
{
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Get("/api/v1/members/{MemberId}/points/history");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "获取积分明细";
|
|
||||||
s.Description = "获取会员的积分变动历史";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(GetPointsHistoryRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var transactions = await historyRepository.GetByMemberIdAsync(
|
|
||||||
req.MemberId,
|
|
||||||
req.Page > 0 ? req.Page : 1,
|
|
||||||
req.PageSize > 0 && req.PageSize <= 100 ? req.PageSize : 20,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
var totalCount = await historyRepository.CountByMemberIdAsync(req.MemberId, ct);
|
|
||||||
|
|
||||||
Response = new GetPointsHistoryResponse
|
|
||||||
{
|
|
||||||
MemberId = req.MemberId,
|
|
||||||
Page = req.Page > 0 ? req.Page : 1,
|
|
||||||
PageSize = req.PageSize > 0 ? req.PageSize : 20,
|
|
||||||
TotalCount = totalCount,
|
|
||||||
Transactions = transactions.Select(t => new PointsTransactionDto
|
|
||||||
{
|
|
||||||
Id = t.Id,
|
|
||||||
Points = t.Points,
|
|
||||||
TransactionType = t.TransactionType,
|
|
||||||
SourceId = t.SourceId,
|
|
||||||
Remark = t.Remark,
|
|
||||||
CreatedAt = t.CreatedAt
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPointsHistoryRequest
|
|
||||||
{
|
|
||||||
[FromRoute]
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
[QueryParam]
|
|
||||||
public int Page { get; set; } = 1;
|
|
||||||
[QueryParam]
|
|
||||||
public int PageSize { get; set; } = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetPointsHistoryResponse
|
|
||||||
{
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
public int Page { get; set; }
|
|
||||||
public int PageSize { get; set; }
|
|
||||||
public int TotalCount { get; set; }
|
|
||||||
public List<PointsTransactionDto> Transactions { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PointsTransactionDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public int Points { get; set; }
|
|
||||||
public string TransactionType { get; set; } = string.Empty;
|
|
||||||
public string SourceId { get; set; } = string.Empty;
|
|
||||||
public string? Remark { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProcessPointsEndpoint : Endpoint<ProcessPointsRequest, ProcessPointsResponse>
|
|
||||||
{
|
|
||||||
private readonly IPointsProcessingService _pointsService;
|
|
||||||
|
|
||||||
public ProcessPointsEndpoint(IPointsProcessingService pointsService)
|
|
||||||
{
|
|
||||||
_pointsService = pointsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/v1/points/process");
|
|
||||||
Summary(s =>
|
|
||||||
{
|
|
||||||
s.Summary = "处理积分";
|
|
||||||
s.Description = "增加或扣减会员积分,支持幂等性";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(ProcessPointsRequest req, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var result = await _pointsService.ProcessAsync(new PointsProcessRequest
|
|
||||||
{
|
|
||||||
CodeId = req.CodeId,
|
|
||||||
MemberId = req.MemberId,
|
|
||||||
TransactionType = req.TransactionType,
|
|
||||||
Points = req.Points,
|
|
||||||
IsAddition = req.IsAddition,
|
|
||||||
SourceId = req.SourceId,
|
|
||||||
Remark = req.Remark
|
|
||||||
}, ct);
|
|
||||||
|
|
||||||
Response = new ProcessPointsResponse
|
|
||||||
{
|
|
||||||
Success = result.Success,
|
|
||||||
Points = result.Points,
|
|
||||||
TotalPoints = result.TotalPoints,
|
|
||||||
Message = result.Message,
|
|
||||||
ErrorCode = result.ErrorCode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProcessPointsRequest
|
|
||||||
{
|
|
||||||
public string CodeId { get; set; } = string.Empty;
|
|
||||||
public long MemberId { get; set; }
|
|
||||||
public string TransactionType { get; set; } = string.Empty;
|
|
||||||
public int Points { get; set; }
|
|
||||||
public bool IsAddition { get; set; } = true;
|
|
||||||
public string? SourceId { get; set; }
|
|
||||||
public string? Remark { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ProcessPointsResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; set; }
|
|
||||||
public int Points { get; set; }
|
|
||||||
public int TotalPoints { get; set; }
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
public string? ErrorCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ using Fengling.Member.Application.Commands.Points;
|
|||||||
using Fengling.Member.Application.Dtos;
|
using Fengling.Member.Application.Dtos;
|
||||||
using Fengling.Member.Application.Dtos.PointsRule;
|
using Fengling.Member.Application.Dtos.PointsRule;
|
||||||
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
|
||||||
using Fengling.Member.Infrastructure.Repositories;
|
using Fengling.Member.Domain.Repositories;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Fengling.Member.Web.Endpoints.v1;
|
namespace Fengling.Member.Web.Endpoints.v1;
|
||||||
@ -60,10 +60,14 @@ public class QueryPointsRulesEndpoint : Endpoint<EmptyRequest, List<PointsRuleDt
|
|||||||
public class CreatePointsRuleEndpoint : Endpoint<CreatePointsRuleRequest, CreatePointsRuleResponse>
|
public class CreatePointsRuleEndpoint : Endpoint<CreatePointsRuleRequest, CreatePointsRuleResponse>
|
||||||
{
|
{
|
||||||
private readonly IPointsRuleRepository _ruleRepository;
|
private readonly IPointsRuleRepository _ruleRepository;
|
||||||
|
private readonly IPointsRuleConditionRepository _conditionRepository;
|
||||||
|
|
||||||
public CreatePointsRuleEndpoint(IPointsRuleRepository ruleRepository)
|
public CreatePointsRuleEndpoint(
|
||||||
|
IPointsRuleRepository ruleRepository,
|
||||||
|
IPointsRuleConditionRepository conditionRepository)
|
||||||
{
|
{
|
||||||
_ruleRepository = ruleRepository;
|
_ruleRepository = ruleRepository;
|
||||||
|
_conditionRepository = conditionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@ -88,6 +92,8 @@ public class CreatePointsRuleEndpoint : Endpoint<CreatePointsRuleRequest, Create
|
|||||||
req.CalculationMode,
|
req.CalculationMode,
|
||||||
req.WeightFactor);
|
req.WeightFactor);
|
||||||
|
|
||||||
|
await _ruleRepository.AddAsync(rule);
|
||||||
|
|
||||||
foreach (var condition in req.Conditions)
|
foreach (var condition in req.Conditions)
|
||||||
{
|
{
|
||||||
var ruleCondition = PointsRuleCondition.Create(
|
var ruleCondition = PointsRuleCondition.Create(
|
||||||
@ -95,10 +101,9 @@ public class CreatePointsRuleEndpoint : Endpoint<CreatePointsRuleRequest, Create
|
|||||||
condition.DimensionType,
|
condition.DimensionType,
|
||||||
condition.DimensionValue,
|
condition.DimensionValue,
|
||||||
condition.Operator);
|
condition.Operator);
|
||||||
rule.AddCondition(ruleCondition);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _ruleRepository.AddAsync(rule, ct);
|
await _conditionRepository.AddAsync(ruleCondition);
|
||||||
|
}
|
||||||
|
|
||||||
var response = new CreatePointsRuleResponse(
|
var response = new CreatePointsRuleResponse(
|
||||||
Id: rule.Id,
|
Id: rule.Id,
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
using Fengling.Member.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Fengling.Member.Web;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP 上下文租户访问器
|
|
||||||
/// 从 JWT Claim 中获取 tenant_id
|
|
||||||
/// </summary>
|
|
||||||
public class HttpContextTenantAccessor : ITenantAccessor
|
|
||||||
{
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
private const string TenantIdClaimType = "tenant_id";
|
|
||||||
|
|
||||||
public HttpContextTenantAccessor(IHttpContextAccessor httpContextAccessor)
|
|
||||||
{
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long? GetTenantId()
|
|
||||||
{
|
|
||||||
var context = _httpContextAccessor.HttpContext;
|
|
||||||
if (context?.User == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var tenantIdClaim = context.User.FindFirst(TenantIdClaimType);
|
|
||||||
if (tenantIdClaim == null || string.IsNullOrEmpty(tenantIdClaim.Value))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (long.TryParse(tenantIdClaim.Value, out var tenantId))
|
|
||||||
return tenantId;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetTenantId(out long tenantId)
|
|
||||||
{
|
|
||||||
var result = GetTenantId();
|
|
||||||
tenantId = result ?? 0;
|
|
||||||
return result.HasValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -22,14 +22,12 @@ using NetCorePal.Extensions.CodeAnalysis;
|
|||||||
using Fengling.Member.Infrastructure.Repositories;
|
using Fengling.Member.Infrastructure.Repositories;
|
||||||
using Fengling.Member.Application.Services;
|
using Fengling.Member.Application.Services;
|
||||||
using Fengling.Member.Application.Commands.Points;
|
using Fengling.Member.Application.Commands.Points;
|
||||||
using Fengling.Member.Domain.Aggregates.PointsModel;
|
|
||||||
using Fengling.Member.Web.Endpoints.v1;
|
using Fengling.Member.Web.Endpoints.v1;
|
||||||
using Fengling.Member.Web;
|
|
||||||
using Fengling.Member.Domain.Repositories;
|
using Fengling.Member.Domain.Repositories;
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.Enrich.WithClientIp()
|
.Enrich.WithClientIp()
|
||||||
.WriteTo.Console()
|
.WriteTo.Console(new JsonFormatter())
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -54,10 +52,6 @@ try
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddScoped<IPointsProcessingService, PointsProcessingService>();
|
|
||||||
builder.Services.AddScoped<IPointsAccountCache, PointsAccountCache>();
|
|
||||||
builder.Services.AddScoped<ICodeDistributedLock, CodeDistributedLock>();
|
|
||||||
|
|
||||||
|
|
||||||
#region 身份认证
|
#region 身份认证
|
||||||
|
|
||||||
@ -68,19 +62,18 @@ try
|
|||||||
builder.Services.AddDataProtection()
|
builder.Services.AddDataProtection()
|
||||||
.PersistKeysToStackExchangeRedis("DataProtection-Keys");
|
.PersistKeysToStackExchangeRedis("DataProtection-Keys");
|
||||||
|
|
||||||
// 配置JWT认证
|
// 配置JWT认证
|
||||||
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
|
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
|
||||||
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration
|
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
|
||||||
{ JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
options.RequireHttpsMetadata = false;
|
options.RequireHttpsMetadata = false;
|
||||||
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
|
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
|
||||||
options.TokenValidationParameters.ValidateAudience = true;
|
options.TokenValidationParameters.ValidateAudience = true;
|
||||||
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
|
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
|
||||||
options.TokenValidationParameters.ValidateIssuer = true;
|
options.TokenValidationParameters.ValidateIssuer = true;
|
||||||
});
|
});
|
||||||
builder.Services.AddNetCorePalJwt().AddRedisStore();
|
builder.Services.AddNetCorePalJwt().AddRedisStore();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -124,7 +117,6 @@ try
|
|||||||
{
|
{
|
||||||
options.EnableSensitiveDataLogging();
|
options.EnableSensitiveDataLogging();
|
||||||
}
|
}
|
||||||
|
|
||||||
options.EnableDetailedErrors();
|
options.EnableDetailedErrors();
|
||||||
});
|
});
|
||||||
builder.Services.AddUnitOfWork<ApplicationDbContext>();
|
builder.Services.AddUnitOfWork<ApplicationDbContext>();
|
||||||
@ -151,12 +143,7 @@ try
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
builder.Services.AddMediatR(cfg =>
|
builder.Services.AddMediatR(cfg =>
|
||||||
cfg.RegisterServicesFromAssemblies(
|
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())
|
||||||
Assembly.GetExecutingAssembly(),
|
|
||||||
typeof(Fengling.Member.Application.Commands.Member.RegisterMemberCommand).Assembly,
|
|
||||||
typeof(Fengling.Member.Domain.Aggregates.Users.MemberEntity).Assembly,
|
|
||||||
typeof(Fengling.Member.Infrastructure.Repositories.MemberRepository).Assembly
|
|
||||||
)
|
|
||||||
.AddCommandLockBehavior()
|
.AddCommandLockBehavior()
|
||||||
.AddKnownExceptionValidationBehavior()
|
.AddKnownExceptionValidationBehavior()
|
||||||
.AddUnitOfWorkBehaviors());
|
.AddUnitOfWorkBehaviors());
|
||||||
@ -197,20 +184,12 @@ try
|
|||||||
|
|
||||||
#region Points Rule Services
|
#region Points Rule Services
|
||||||
|
|
||||||
builder.Services.AddScoped<IPointsProcessingService, PointsProcessingService>();
|
builder.Services.AddScoped<IPointsRuleRepository, PointsRuleRepository>();
|
||||||
builder.Services.AddScoped<IPointsHistoryRepository, PointsHistoryRepository>();
|
builder.Services.AddScoped<IPointsRuleConditionRepository, PointsRuleConditionRepository>();
|
||||||
builder.Services.AddScoped<IPointsAccountCache, PointsAccountCache>();
|
|
||||||
builder.Services.AddScoped<PointsRuleMatcher>();
|
builder.Services.AddScoped<PointsRuleMatcher>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Tenant Accessor
|
|
||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
|
||||||
builder.Services.AddScoped<ITenantAccessor, HttpContextTenantAccessor>();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"PostgreSQL": "Host=192.168.100.10;Database=fengling_member;Username=movingsam;Password=sl52788542",
|
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
|
||||||
"Redis": "81.68.223.70:16379,password=sl52788542"
|
"Redis": "localhost:6379"
|
||||||
},
|
},
|
||||||
"RabbitMQ": {
|
"RabbitMQ": {
|
||||||
"HostName": "localhost",
|
"HostName": "localhost",
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
|
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456",
|
||||||
"Redis": "81.68.223.70:16379,password=sl52788542"
|
"Redis": "localhost:6379"
|
||||||
},
|
},
|
||||||
"RabbitMQ": {
|
"RabbitMQ": {
|
||||||
"HostName": "localhost",
|
"HostName": "localhost",
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="Moq" />
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Fengling.Member.Domain\Fengling.Member.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
2
test/Fengling.Member.Domain.Tests/GlobalUsings.cs
Normal file
2
test/Fengling.Member.Domain.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
global using Xunit;
|
||||||
|
global using NetCorePal.Extensions.Primitives;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="Moq" />
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Fengling.Member.Infrastructure\Fengling.Member.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
8
test/Fengling.Member.Web.Tests/AssemblyInfo.cs
Normal file
8
test/Fengling.Member.Web.Tests/AssemblyInfo.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//Ordering Tests In Collections see:https://fast-endpoints.com/docs/integration-unit-testing#ordering-tests-in-collections
|
||||||
|
// [assembly: EnableAdvancedTesting]
|
||||||
|
|
||||||
|
// can capture standard output and standard error
|
||||||
|
// [assembly: CaptureConsole]
|
||||||
|
|
||||||
|
// will capture output from Debug and Trace
|
||||||
|
// [assembly: CaptureTrace]
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="appsettings.Development.json" />
|
||||||
|
<None Remove="appsettings.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Include="Moq" />
|
||||||
|
<PackageReference Include="Testcontainers" />
|
||||||
|
<PackageReference Include="Testcontainers.PostgreSql" />
|
||||||
|
<PackageReference Include="Testcontainers.RabbitMq" />
|
||||||
|
<PackageReference Include="Testcontainers.Redis" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis" />
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="FastEndpoints.Testing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Fengling.Member.Web\Fengling.Member.Web.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
54
test/Fengling.Member.Web.Tests/Fixtures/WebAppFixture.cs
Normal file
54
test/Fengling.Member.Web.Tests/Fixtures/WebAppFixture.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using Testcontainers.PostgreSql;
|
||||||
|
using Testcontainers.RabbitMq;
|
||||||
|
using Testcontainers.Redis;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Fengling.Member.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Fengling.Member.Web.Tests.Fixtures;
|
||||||
|
|
||||||
|
public class WebAppFixture : AppFixture<Program>
|
||||||
|
{
|
||||||
|
private RedisContainer _redisContainer = null!;
|
||||||
|
private RabbitMqContainer _rabbitMqContainer = null!;
|
||||||
|
private PostgreSqlContainer _databaseContainer = null!;
|
||||||
|
|
||||||
|
protected override async ValueTask PreSetupAsync()
|
||||||
|
{
|
||||||
|
_redisContainer = new RedisBuilder()
|
||||||
|
.WithCommand("--databases", "1024").Build();
|
||||||
|
_rabbitMqContainer = new RabbitMqBuilder()
|
||||||
|
.WithUsername("guest").WithPassword("guest").Build();
|
||||||
|
_databaseContainer = new PostgreSqlBuilder()
|
||||||
|
.WithUsername("postgres").WithPassword("123456")
|
||||||
|
.WithEnvironment("TZ", "Asia/Shanghai")
|
||||||
|
.WithDatabase("postgres").Build();
|
||||||
|
|
||||||
|
var tasks = new List<Task> { _redisContainer.StartAsync() };
|
||||||
|
tasks.Add(_rabbitMqContainer.StartAsync());
|
||||||
|
tasks.Add(_databaseContainer.StartAsync());
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
await CreateVisualHostAsync("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureApp(IWebHostBuilder a)
|
||||||
|
{
|
||||||
|
a.UseSetting("ConnectionStrings:Redis",
|
||||||
|
_redisContainer.GetConnectionString());
|
||||||
|
a.UseSetting("ConnectionStrings:PostgreSQL",
|
||||||
|
_databaseContainer.GetConnectionString());
|
||||||
|
a.UseSetting("RabbitMQ:Port", _rabbitMqContainer.GetMappedPublicPort(5672).ToString());
|
||||||
|
a.UseSetting("RabbitMQ:UserName", "guest");
|
||||||
|
a.UseSetting("RabbitMQ:Password", "guest");
|
||||||
|
a.UseSetting("RabbitMQ:VirtualHost", "/");
|
||||||
|
a.UseSetting("RabbitMQ:HostName", _rabbitMqContainer.Hostname);
|
||||||
|
a.UseEnvironment("Development");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateVisualHostAsync(string visualHost)
|
||||||
|
{
|
||||||
|
await _rabbitMqContainer.ExecAsync(["rabbitmqctl", "add_vhost", visualHost]);
|
||||||
|
await _rabbitMqContainer.ExecAsync(["rabbitmqctl", "set_permissions", "-p", visualHost, "guest", ".*", ".*", ".*"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Fengling.Member.Web.Tests.Fixtures;
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public class WebAppTestCollection : TestCollection<WebAppFixture>
|
||||||
|
{
|
||||||
|
public const string Name = nameof(WebAppTestCollection);
|
||||||
|
}
|
||||||
9
test/Fengling.Member.Web.Tests/GlobalUsings.cs
Normal file
9
test/Fengling.Member.Web.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
global using Xunit;
|
||||||
|
global using Fengling.Member.Web.Tests.Fixtures;
|
||||||
|
global using FastEndpoints.Testing;
|
||||||
|
global using FastEndpoints;
|
||||||
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
|
global using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
global using NetCorePal.Extensions.NewtonsoftJson;
|
||||||
|
global using Microsoft.Extensions.Configuration;
|
||||||
|
global using Microsoft.Extensions.Logging;
|
||||||
5
test/Fengling.Member.Web.Tests/xunit.runner.json
Normal file
5
test/Fengling.Member.Web.Tests/xunit.runner.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"parallelizeAssembly": true,
|
||||||
|
"parallelizeTestCollections": false,
|
||||||
|
"diagnosticMessages": false
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user