Compare commits

..

10 Commits

Author SHA1 Message Date
movingsam
fb466b35ea debug 2026-02-19 21:40:22 +08:00
movingsam
489f02cb5f refactor: clean up Member module and update Console
- Remove redundant PointsRule repositories (use single PointsRuleRepository)
- Clean up Member migrations and consolidate to single Init migration
- Update Console frontend API and components for Tenant
- Add H5LinkService for member H5 integration
2026-02-18 23:34:40 +08:00
movingsam
daf8bc9e24 feat(member): add member management module with frontend and backend
- Add MemberQueryEndpoints with CRUD operations and status management
- Add frontend member management pages (list, detail, points, tags)
- Add frontend routes and API client for member module
- Move completed docs to docs/completed/
2026-02-17 16:29:34 +08:00
movingsam
73f84a1237 fix: resolve strongly typed ID compilation errors
- Change PointsAccountId to IGuidStronglyTypedId
- Update PointsTransaction to use PointsAccountId
- Update Domain Events and Command responses
- Fix Member and Activity endpoint references
- Remove duplicate PointsChangedEvent in Application layer
2026-02-17 15:42:07 +08:00
movingsam
ab3d755f63 refactor: change PointsAccountId to Guid-based strongly typed ID 2026-02-17 15:30:10 +08:00
movingsam
f3a0601257 fix: remove IdGenerator usage - let EF SnowFlakeValueGenerator handle ID generation 2026-02-16 22:40:07 -08:00
movingsam
eb1d4ac4f7 refactor: apply CleanDDD strongly typed ID and add Deleted/RowVersion
- Convert CampaignId to partial record implementing IGuidStronglyTypedId
- Add PointsAccountId as IInt64StronglyTypedId with Snowflake ID generation
- Add Deleted and RowVersion to MemberEntity and PointsAccount
- Update PointsAccountEntityTypeConfiguration to use SnowFlakeValueGenerator

BREAKING CHANGE: PointsAccount now uses PointsAccountId (long) instead of plain long
2026-02-16 22:03:04 -08:00
sam
92247346dd refactor: major project restructuring and cleanup
Changes:

- Remove deprecated Fengling.Activity and YarpGateway.Admin projects

- Add points processing services with distributed lock support

- Update Vben frontend with gateway management pages

- Add gateway config controller and database listener

- Update routing to use header-mixed-nav layout

- Add comprehensive test suites for Member services

- Add YarpGateway integration tests

- Update package versions in Directory.Packages.props

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-15 10:34:07 +08:00
sam
087948a8e1 docs: reorganize documentation structure
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-15 10:32:23 +08:00
sam
c3ae2b618c chore: update gitignore to allow Vben packages directory 2026-02-13 21:34:52 +08:00
60 changed files with 2095 additions and 2123 deletions

View File

@ -123,6 +123,7 @@
<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)" />

View File

@ -86,7 +86,7 @@ docker run --restart unless-stopped --name netcorepal-rabbitmq -p 5672:5672 -p 1
启动后,可以通过以下地址访问各个服务: 启动后,可以通过以下地址访问各个服务:
- **Redis**: `localhost:6379` - **Redis**: `81.68.223.70:16379,password=sl52788542`
- **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)

View File

@ -132,10 +132,10 @@ Update your `appsettings.Development.json` with these connection strings:
```json ```json
{ {
"ConnectionStrings": { "ConnectionStrings": {
"Redis": "localhost:6379,defaultDatabase=0", "Redis": "81.68.223.70:16379,password=sl52788542,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=5432;Database=abctemplate;Username=postgres;Password=123456;" "PostgreSQL": "Host=localhost;Port=15432;Database=abctemplate;Username=postgres;Password=123456;"
}, },
"RabbitMQ": { "RabbitMQ": {
"HostName": "localhost", "HostName": "localhost",

View File

@ -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: localhost:6379" Write-Host "✅ Redis: 81.68.223.70:16379,password=sl52788542"
if ($Postgres) { if ($Postgres) {
Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)" Write-Host "✅ PostgreSQL: localhost:5432 (postgres/123456)"

View File

@ -146,7 +146,7 @@ main() {
echo echo
echo "📋 Service Summary:" echo "📋 Service Summary:"
echo "===================" echo "==================="
echo "✅ Redis: localhost:6379" echo "✅ Redis: 81.68.223.70:16379,password=sl52788542"
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"

View File

@ -16,7 +16,7 @@ public class AddPointsCommand : IRequest<AddPointsResponse>
public class AddPointsResponse public class AddPointsResponse
{ {
public long AccountId { get; set; } public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
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; }

View File

@ -16,7 +16,7 @@ public class DeductPointsCommand : IRequest<DeductPointsResponse>
public class DeductPointsResponse public class DeductPointsResponse
{ {
public long AccountId { get; set; } public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
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; }

View File

@ -31,6 +31,7 @@ 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}",

View File

@ -0,0 +1,78 @@
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;
}
}
}

View File

@ -11,6 +11,7 @@
<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" />

View File

@ -0,0 +1,99 @@
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);
}
}
}

View File

@ -0,0 +1,44 @@
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);
}

View File

@ -0,0 +1,64 @@
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);
}

View File

@ -0,0 +1,89 @@
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);
}

View File

@ -0,0 +1,248 @@
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])
};
}
}

View File

@ -0,0 +1,127 @@
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);
}
}

View File

@ -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.Domain.Repositories; using Fengling.Member.Infrastructure.Repositories;
using NetCorePal.Extensions.Domain; using NetCorePal.Extensions.Domain;
namespace Fengling.Member.Application.Services; namespace Fengling.Member.Application.Services;

View File

@ -0,0 +1,38 @@
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);
}

View File

@ -1,8 +1,18 @@
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 class PointsAccount : Entity<long>, IAggregateRoot public partial record PointsAccountId : IGuidStronglyTypedId
{
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; }
@ -12,6 +22,8 @@ public class PointsAccount : Entity<long>, 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();
@ -48,6 +60,7 @@ public class PointsAccount : Entity<long>, IAggregateRoot
var transaction = PointsTransaction.Create( var transaction = PointsTransaction.Create(
Id, Id,
MemberId, MemberId,
TenantId,
points, points,
transactionType, transactionType,
sourceId, sourceId,
@ -60,7 +73,7 @@ public class PointsAccount : Entity<long>, IAggregateRoot
UpdatedAt = DateTime.UtcNow; UpdatedAt = DateTime.UtcNow;
Version++; Version++;
AddDomainEvent(new PointsChangedEvent(Id, MemberId, points, TotalPoints, transactionType)); AddDomainEvent(new PointsChangedEvent(Id, MemberId, TenantId, points, TotalPoints, transactionType, sourceId, remark));
} }
public bool DeductPoints(int points, string transactionType, string sourceId, string? remark = null) public bool DeductPoints(int points, string transactionType, string sourceId, string? remark = null)
@ -74,6 +87,7 @@ public class PointsAccount : Entity<long>, IAggregateRoot
var transaction = PointsTransaction.Create( var transaction = PointsTransaction.Create(
Id, Id,
MemberId, MemberId,
TenantId,
points, points,
transactionType, transactionType,
sourceId, sourceId,
@ -86,7 +100,7 @@ public class PointsAccount : Entity<long>, IAggregateRoot
UpdatedAt = DateTime.UtcNow; UpdatedAt = DateTime.UtcNow;
Version++; Version++;
AddDomainEvent(new PointsChangedEvent(Id, MemberId, -points, TotalPoints, transactionType)); AddDomainEvent(new PointsChangedEvent(Id, MemberId, TenantId, -points, TotalPoints, transactionType, sourceId, remark));
return true; return true;
} }

View File

@ -2,8 +2,9 @@ namespace Fengling.Member.Domain.Aggregates.PointsModel;
public class PointsTransaction : Entity<long> public class PointsTransaction : Entity<long>
{ {
public long PointsAccountId { get; private set; } public PointsAccountId PointsAccountId { get; private set; } = PointsAccountId.New();
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;
@ -22,8 +23,9 @@ public class PointsTransaction : Entity<long>
} }
public static PointsTransaction Create( public static PointsTransaction Create(
long pointsAccountId, PointsAccountId pointsAccountId,
long memberId, long memberId,
long tenantId,
int points, int points,
string transactionType, string transactionType,
string sourceId, string sourceId,
@ -34,6 +36,7 @@ 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,

View File

@ -1,5 +1,6 @@
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;
@ -23,6 +24,8 @@ 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();

View File

@ -1,9 +1,14 @@
using Fengling.Member.Domain.Aggregates.PointsModel;
namespace Fengling.Member.Domain.Events.Points; namespace Fengling.Member.Domain.Events.Points;
public record PointsChangedEvent( public record PointsChangedEvent(
long AccountId, PointsAccountId 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;

View File

@ -1,10 +0,0 @@
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);
}

View File

@ -1,15 +0,0 @@
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);
}

View File

@ -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=any;Database=any;Username=any;Password=any", options.UseNpgsql("Host=192.168.100.10;Database=fengling_member;Username=movingsam;Password=sl52788542",
b => b =>
{ {
b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName); b.MigrationsAssembly(typeof(DesignTimeApplicationDbContextFactory).Assembly.FullName);

View File

@ -14,7 +14,8 @@ public class PointsAccountEntityTypeConfiguration : IEntityTypeConfiguration<Poi
builder.Property(p => p.Id) builder.Property(p => p.Id)
.HasColumnName("id") .HasColumnName("id")
.UseIdentityColumn(); .UseGuidVersion7ValueGenerator()
.HasComment("积分账户标识");
builder.Property(p => p.MemberId) builder.Property(p => p.MemberId)
.HasColumnName("user_id") .HasColumnName("user_id")

View File

@ -0,0 +1,21 @@
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);
}

View File

@ -1,140 +0,0 @@
// <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
}
}
}

View File

@ -1,101 +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.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");
}
}
}

View File

@ -1,429 +0,0 @@
// <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
}
}
}

View File

@ -1,208 +0,0 @@
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");
}
}
}

View File

@ -1,598 +0,0 @@
// <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
}
}
}

View File

@ -1,206 +0,0 @@
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);
}
}
}

View File

@ -1,72 +0,0 @@
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);
}
}
}

View File

@ -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("20260209163416_ChangeMemberIdToGuid")] [Migration("20260217154010_Init")]
partial class ChangeMemberIdToGuid partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -27,17 +27,18 @@ 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<long>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .HasColumnType("uuid")
.HasColumnType("bigint") .HasColumnName("id")
.HasColumnName("id"); .HasComment("积分账户标识");
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")
@ -48,6 +49,10 @@ 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");
@ -101,8 +106,8 @@ namespace Fengling.Member.Infrastructure.Migrations
b.Property<int>("Points") b.Property<int>("Points")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<long>("PointsAccountId") b.Property<Guid>("PointsAccountId")
.HasColumnType("bigint"); .HasColumnType("uuid");
b.Property<string>("Remark") b.Property<string>("Remark")
.HasColumnType("text"); .HasColumnType("text");
@ -111,6 +116,9 @@ 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");
@ -245,6 +253,9 @@ 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)")
@ -255,6 +266,10 @@ 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)

View File

@ -0,0 +1,390 @@
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");
}
}
}

View File

@ -24,17 +24,18 @@ 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<long>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .HasColumnType("uuid")
.HasColumnType("bigint") .HasColumnName("id")
.HasColumnName("id"); .HasComment("积分账户标识");
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")
@ -45,6 +46,10 @@ 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");
@ -98,8 +103,8 @@ namespace Fengling.Member.Infrastructure.Migrations
b.Property<int>("Points") b.Property<int>("Points")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<long>("PointsAccountId") b.Property<Guid>("PointsAccountId")
.HasColumnType("bigint"); .HasColumnType("uuid");
b.Property<string>("Remark") b.Property<string>("Remark")
.HasColumnType("text"); .HasColumnType("text");
@ -108,6 +113,9 @@ 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");
@ -242,6 +250,9 @@ 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)")
@ -252,6 +263,10 @@ 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)

View File

@ -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, long> public interface IPointsAccountRepository : IRepository<PointsAccount, PointsAccountId>
{ {
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, long>
} }
public class PointsAccountRepository(ApplicationDbContext context) : public class PointsAccountRepository(ApplicationDbContext context) :
RepositoryBase<PointsAccount, long, ApplicationDbContext>(context), IPointsAccountRepository RepositoryBase<PointsAccount, PointsAccountId, ApplicationDbContext>(context), IPointsAccountRepository
{ {
public async Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default) public async Task<PointsAccount?> GetByMemberIdAsync(long memberId, CancellationToken cancellationToken = default)
{ {

View File

@ -0,0 +1,54 @@
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);
}
}

View File

@ -1,32 +0,0 @@
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);
}
}

View File

@ -5,25 +5,27 @@ using Microsoft.EntityFrameworkCore;
namespace Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Infrastructure.Repositories;
public class PointsRuleRepository : IPointsRuleRepository public interface IPointsRuleRepository : IRepository<PointsRule, PointsRuleId>
{ {
private readonly ApplicationDbContext _context; Task<PointsRule?> GetByIdAsync(PointsRuleId id);
Task<PointsRule?> GetByCodeAsync(string code);
public PointsRuleRepository(ApplicationDbContext context) Task<List<PointsRule>> GetActiveRulesAsync();
{ 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 _context.PointsRules return await DbContext.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 _context.PointsRules return await DbContext.PointsRules
.Include(x => x.Conditions) .Include(x => x.Conditions)
.FirstOrDefaultAsync(x => x.Code == code); .FirstOrDefaultAsync(x => x.Code == code);
} }
@ -31,7 +33,7 @@ public class PointsRuleRepository : IPointsRuleRepository
public async Task<List<PointsRule>> GetActiveRulesAsync() public async Task<List<PointsRule>> GetActiveRulesAsync()
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return await _context.PointsRules return await DbContext.PointsRules
.Include(x => x.Conditions) .Include(x => x.Conditions)
.Where(x => x.IsActive .Where(x => x.IsActive
&& x.EffectiveFrom <= now && x.EffectiveFrom <= now
@ -45,7 +47,7 @@ public class PointsRuleRepository : IPointsRuleRepository
string dimensionValue) string dimensionValue)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return await _context.PointsRules return await DbContext.PointsRules
.Include(x => x.Conditions) .Include(x => x.Conditions)
.Where(x => x.IsActive .Where(x => x.IsActive
&& x.EffectiveFrom <= now && x.EffectiveFrom <= now
@ -54,24 +56,4 @@ public class PointsRuleRepository : IPointsRuleRepository
.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);
}
}
} }

View File

@ -0,0 +1 @@
Unable to retrieve project metadata. Ensure it's an SDK-style project.

View File

@ -1,25 +0,0 @@
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);
}
}

View File

@ -0,0 +1,445 @@
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);

View File

@ -2,6 +2,8 @@ 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;
@ -60,9 +62,202 @@ public class AddPointsRequest
public class AddPointsResponse public class AddPointsResponse
{ {
public long AccountId { get; set; } public PointsAccountId AccountId { get; set; } = PointsAccountId.New();
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; }
}

View File

@ -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.Domain.Repositories; using Fengling.Member.Infrastructure.Repositories;
using MediatR; using MediatR;
namespace Fengling.Member.Web.Endpoints.v1; namespace Fengling.Member.Web.Endpoints.v1;
@ -60,14 +60,10 @@ 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( public CreatePointsRuleEndpoint(IPointsRuleRepository ruleRepository)
IPointsRuleRepository ruleRepository,
IPointsRuleConditionRepository conditionRepository)
{ {
_ruleRepository = ruleRepository; _ruleRepository = ruleRepository;
_conditionRepository = conditionRepository;
} }
public override void Configure() public override void Configure()
@ -92,8 +88,6 @@ 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(
@ -101,10 +95,11 @@ public class CreatePointsRuleEndpoint : Endpoint<CreatePointsRuleRequest, Create
condition.DimensionType, condition.DimensionType,
condition.DimensionValue, condition.DimensionValue,
condition.Operator); condition.Operator);
rule.AddCondition(ruleCondition);
await _conditionRepository.AddAsync(ruleCondition);
} }
await _ruleRepository.AddAsync(rule, ct);
var response = new CreatePointsRuleResponse( var response = new CreatePointsRuleResponse(
Id: rule.Id, Id: rule.Id,
Name: rule.Name, Name: rule.Name,

View File

@ -0,0 +1,43 @@
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;
}
}

View File

@ -22,12 +22,14 @@ 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(new JsonFormatter()) .WriteTo.Console()
.CreateLogger(); .CreateLogger();
try try
{ {
@ -52,28 +54,33 @@ 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
var redis = await ConnectionMultiplexer.ConnectAsync(builder.Configuration.GetConnectionString("Redis")!); var redis = await ConnectionMultiplexer.ConnectAsync(builder.Configuration.GetConnectionString("Redis")!);
builder.Services.AddSingleton<IConnectionMultiplexer>(_ => redis); builder.Services.AddSingleton<IConnectionMultiplexer>(_ => redis);
// DataProtection - use custom extension that resolves IConnectionMultiplexer from DI // DataProtection - use custom extension that resolves IConnectionMultiplexer from DI
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 { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" }; var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration
{ JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
builder.Services.AddAuthentication().AddJwtBearer(options =>
{ builder.Services.AddAuthentication().AddJwtBearer(options =>
options.RequireHttpsMetadata = false; {
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience; options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidateAudience = true; options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer; options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidateIssuer = true; options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
}); options.TokenValidationParameters.ValidateIssuer = true;
});
builder.Services.AddNetCorePalJwt().AddRedisStore(); builder.Services.AddNetCorePalJwt().AddRedisStore();
#endregion #endregion
@ -117,6 +124,7 @@ try
{ {
options.EnableSensitiveDataLogging(); options.EnableSensitiveDataLogging();
} }
options.EnableDetailedErrors(); options.EnableDetailedErrors();
}); });
builder.Services.AddUnitOfWork<ApplicationDbContext>(); builder.Services.AddUnitOfWork<ApplicationDbContext>();
@ -143,7 +151,12 @@ try
#endregion #endregion
builder.Services.AddMediatR(cfg => builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()) cfg.RegisterServicesFromAssemblies(
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());
@ -184,12 +197,20 @@ try
#region Points Rule Services #region Points Rule Services
builder.Services.AddScoped<IPointsRuleRepository, PointsRuleRepository>(); builder.Services.AddScoped<IPointsProcessingService, PointsProcessingService>();
builder.Services.AddScoped<IPointsRuleConditionRepository, PointsRuleConditionRepository>(); builder.Services.AddScoped<IPointsHistoryRepository, PointsHistoryRepository>();
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();
@ -226,7 +247,7 @@ try
#endregion #endregion
app.MapMetrics(); // 通过 /metrics 访问指标 app.MapMetrics(); // 通过 /metrics 访问指标
// Code analysis endpoint // Code analysis endpoint
app.MapGet("/code-analysis", () => app.MapGet("/code-analysis", () =>
{ {
@ -236,7 +257,7 @@ try
); );
return Results.Content(html, "text/html; charset=utf-8"); return Results.Content(html, "text/html; charset=utf-8");
}); });
app.UseHangfireDashboard("/hangfire"); app.UseHangfireDashboard("/hangfire");
RecurringJob.AddOrUpdate<IMediator>( RecurringJob.AddOrUpdate<IMediator>(
@ -259,4 +280,4 @@ finally
public partial class Program public partial class Program
#pragma warning restore S1118 #pragma warning restore S1118
{ {
} }

View File

@ -6,8 +6,8 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456", "PostgreSQL": "Host=192.168.100.10;Database=fengling_member;Username=movingsam;Password=sl52788542",
"Redis": "localhost:6379" "Redis": "81.68.223.70:16379,password=sl52788542"
}, },
"RabbitMQ": { "RabbitMQ": {
"HostName": "localhost", "HostName": "localhost",

View File

@ -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": "localhost:6379" "Redis": "81.68.223.70:16379,password=sl52788542"
}, },
"RabbitMQ": { "RabbitMQ": {
"HostName": "localhost", "HostName": "localhost",

View File

@ -1,29 +0,0 @@
<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>

View File

@ -1,2 +0,0 @@
global using Xunit;
global using NetCorePal.Extensions.Primitives;

View File

@ -1,29 +0,0 @@
<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>

View File

@ -1 +0,0 @@
global using Xunit;

View File

@ -1,8 +0,0 @@
//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]

View File

@ -1,41 +0,0 @@
<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>

View File

@ -1,54 +0,0 @@
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", ".*", ".*", ".*"
]);
}
}

View File

@ -1,7 +0,0 @@
namespace Fengling.Member.Web.Tests.Fixtures;
[CollectionDefinition(Name)]
public class WebAppTestCollection : TestCollection<WebAppFixture>
{
public const string Name = nameof(WebAppTestCollection);
}

View File

@ -1,9 +0,0 @@
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;

View File

@ -1,5 +0,0 @@
{
"parallelizeAssembly": true,
"parallelizeTestCollections": false,
"diagnosticMessages": false
}