暂时提交
This commit is contained in:
1 parent
5d017740c6
commit
b35f816a87
7 files changed
+96
-2
No files matched your search
@@ -8,6 +8,14 @@
|
||||
模块内部类型、文件及方法统一不带平台前缀,后续平台通过模块区分。核心服务为 `ShopService`;扩展查询、详情响应和映射分别为 `ShopQueryRequest`、`ShopDetailsInfo`、`ShopDetailsMapper`。`PddLoginShopEntity` 表示导入的拼多多登录记录,`PddShopEntity` 保留 bxx 店铺映射。
|
||||
原带平台标记的 Action 统一为 `SavePddShop`、`UpdatePddShopLogin`、`DeletePddShop`、`GetPddShops`、`GetTokens`、`GetShopToken`,客户端需同步更新路由。数据库映射、上游协议及已部署的 `Shops:E57WxAccounts` 配置键保持兼容。
|
||||
|
||||
## 仓储查询入口
|
||||
|
||||
当前仓储不使用内存缓存或 Redis。单店到期时间经 `ShopQueryService → IShopRepository.GetExpireTime` 读取;列表到期信息经 `ShopService → IShopRepository.GetExpireTimes` 一次批量查询数据库,不逐店查询。
|
||||
|
||||
空集合返回空结果,列表缺失到期记录按 0 处理,永久有效店铺由 Service 覆盖到期时间。列表的数据范围和跨月额度重置仍在 Service。续费沿用原有事务内 ORM 读写,不包含缓存失效管道。
|
||||
|
||||
本次只集中上述公共查询,没有将模块内全部 SQL 迁入 Repository。后续确需缓存时,再按查询的实时性要求实现,并覆盖事务提交后的失效路径。
|
||||
|
||||
## 调用方式
|
||||
|
||||
所有接口使用当前宿主认证,路由为 `POST /api/v1/shop/{Action}`,Action采用PascalCase业务命名。
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace Store.Shops.Repositories
|
||||
/// <param name="ShopId"></param>
|
||||
/// <returns></returns>
|
||||
DateTime GetExpireTime(string ShopId);
|
||||
|
||||
/// <summary>批量读取店铺到期记录;未找到的店铺不返回记录,供列表补充到期信息。</summary>
|
||||
Task<List<ShopExpireEntity>> GetExpireTimes(string[] shopIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
internal class ShopRepository : BaseRepository<ShopEntity>, IShopRepository, ITransientDependency
|
||||
{
|
||||
@@ -27,6 +30,10 @@ namespace Store.Shops.Repositories
|
||||
_freeSql = freeSql;
|
||||
}
|
||||
|
||||
/// <summary>集中列表使用的到期查询,保持一次批量读取;续费事务仍使用事务内 ORM。</summary>
|
||||
public Task<List<ShopExpireEntity>> GetExpireTimes(string[] shopIds, CancellationToken cancellationToken = default)
|
||||
=> _freeSql.Select<ShopExpireEntity>().Where(e => shopIds.Contains(e.ShopId)).ToListAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户商铺过期时间
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using Store.Shops.Repositories;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
@@ -17,7 +18,8 @@ namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>微信店铺业务;账号身份来自服务端,普通子账号只能访问主账号明确分配的店铺。</summary>
|
||||
public sealed partial class ShopService(IFreeSql db, ICurrentUser currentUser, IUserQueryService users,
|
||||
PddTokenClient tokens, Microsoft.Extensions.Configuration.IConfiguration configuration) : ITransientDependency
|
||||
PddTokenClient tokens, Microsoft.Extensions.Configuration.IConfiguration configuration,
|
||||
IShopRepository shops) : ITransientDependency
|
||||
{
|
||||
/// <summary>读取当前数据库账户,避免相信客户端传来的身份或管理员标志。</summary>
|
||||
private async Task<UserInfo> Actor()
|
||||
@@ -118,7 +120,7 @@ public sealed partial class ShopService(IFreeSql db, ICurrentUser currentUser, I
|
||||
var mapper = new ShopDetailsMapper();
|
||||
var result = rows.Select(mapper.Map).ToList();
|
||||
var ids = rows.Select(s => s.ShopId).Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
var expires = await db.Select<ShopExpireEntity>().Where(e => ids.Contains(e.ShopId)).ToListAsync(ct);
|
||||
var expires = await shops.GetExpireTimes(ids, ct);
|
||||
var paid = actor.Authority?.Contains("充值用户") == true;
|
||||
if (!admin && !string.IsNullOrEmpty(actor.ParentUserName))
|
||||
paid |= (await users.GetUser(actor.ParentUserName)).Authority?.Contains("充值用户") == true;
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
- 文件:`Repositories/UserRepository.cs`:子账号的有效 OEM、费用和积分用户权限继承。
|
||||
- 文件:`Services/UserPassword.cs`:兼容旧明文密码;新建和修改密码保存带盐哈希。
|
||||
|
||||
## 查询入口与后续缓存接入
|
||||
|
||||
公共用户查询已经通过 `IUserQueryService → UserQueryService → IUserRepository.GetUser` 集中读取,本次保留现有实现。登录凭据检查、资金操作和事务内的账户读取仍直接访问数据库,不为缓存准备迁移事务写入。
|
||||
|
||||
`GetUser` 返回主子账号配置合并后的结果,并被 Shops 等模块用于权限判断,不能直接给整个结果加缓存。后续只缓存允许延迟的资料时,应将展示查询与实时授权读取明确分开;若缓存继承配置,还需在主账号变更时失效关联子账号数据。密码、权限、余额与积分不得因接入展示缓存而改用旧状态。
|
||||
|
||||
店铺续费写入位于 Users,其到期记录由 Shops 的 Repository 直接查询数据库。当前不使用仓储缓存,不注册缓存提供程序或续费缓存失效管道;续费仍由原有事务管道统一提交或回滚。
|
||||
|
||||
## 权限规则
|
||||
|
||||
身份从登录上下文和数据库读取,管理员为 `SaLevel == UserAdminLevel.SuperAdmin`;普通主账号必须 `GroupId == UserGroup.MainAccount` 且无父账号。父账号缺失的子账号不会自动获得主账号权限。
|
||||
|
||||
@@ -54,6 +54,7 @@ public sealed class ShopHttpTests : IAsyncLifetime
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
builder.Services.AddSingleton(context.Db);
|
||||
builder.Services.AddSingleton(context.Repository);
|
||||
builder.Services.AddSingleton<IUserQueryService>(context);
|
||||
builder.Services.AddSingleton(context.Configuration);
|
||||
builder.Services.AddSingleton(context.Gateway);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using Store.Shops.Services;
|
||||
using Store.Users.Entities;
|
||||
using YourNamespace;
|
||||
|
||||
namespace Store.Shops.Tests;
|
||||
|
||||
/// <summary>验证公共到期查询集中后仍保留批量范围和列表到期规则。</summary>
|
||||
public sealed class ShopQueryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Expirations_ReadRequestedShopsInOneQuery_AndEmptyInputReturnsNothing()
|
||||
{
|
||||
using var context = new ShopTestContext();
|
||||
await context.Db.Insert(new[]
|
||||
{
|
||||
new ShopExpireEntity { ShopId = "first", ExpireTime = 100 },
|
||||
new ShopExpireEntity { ShopId = "second", ExpireTime = 200 },
|
||||
new ShopExpireEntity { ShopId = "outside", ExpireTime = 300 }
|
||||
}).ExecuteAffrowsAsync();
|
||||
var queries = 0;
|
||||
context.Db.Aop.CurdBefore += (_, _) => queries++;
|
||||
|
||||
var rows = await context.Repository.GetExpireTimes(["first", "second", "first", "missing"]);
|
||||
|
||||
Assert.Equal(1, queries);
|
||||
Assert.Equal(new[] { "first", "second" }, rows.Select(r => r.ShopId).Order().ToArray());
|
||||
Assert.Equal(100, rows.Single(r => r.ShopId == "first").ExpireTime);
|
||||
Assert.Equal(200, rows.Single(r => r.ShopId == "second").ExpireTime);
|
||||
Assert.Empty(await context.Repository.GetExpireTimes([]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_PreservesMissingAndPermanentExpiration_AndReadsUpdatedValue()
|
||||
{
|
||||
using var context = new ShopTestContext { UserName = "admin" };
|
||||
await context.Db.Insert(new[]
|
||||
{
|
||||
new ShopEntity { ShopId = "dated", UserName = "owner" },
|
||||
new ShopEntity { ShopId = "missing", UserName = "owner" },
|
||||
new ShopEntity { ShopId = "permanent", UserName = "owner", NeverExpires = true }
|
||||
}).ExecuteAffrowsAsync();
|
||||
await context.Db.Insert(new ShopExpireEntity { ShopId = "dated", ExpireTime = 100 }).ExecuteAffrowsAsync();
|
||||
var service = new ShopService(context.Db, context, context, context.Gateway,
|
||||
context.Configuration, context.Repository);
|
||||
|
||||
var page = await service.Search(new(), admin: true);
|
||||
|
||||
Assert.Equal(3, page.Total);
|
||||
Assert.Equal(100, page.Items.Single(s => s.ShopId == "dated").ExpireTime);
|
||||
Assert.Equal(0, page.Items.Single(s => s.ShopId == "missing").ExpireTime);
|
||||
Assert.Equal(1893427200, page.Items.Single(s => s.ShopId == "permanent").ExpireTime);
|
||||
|
||||
await context.Db.Update<ShopExpireEntity>().Where(e => e.ShopId == "dated")
|
||||
.Set(e => e.ExpireTime, 200).ExecuteAffrowsAsync();
|
||||
var updated = await service.Search(new() { ShopIds = ["dated"] }, admin: true);
|
||||
Assert.Equal(200, Assert.Single(updated.Items).ExpireTime);
|
||||
Assert.Empty((await service.Search(new() { Page = 2 }, admin: true)).Items);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using MAX.Framework.Security;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Services;
|
||||
using Store.Shops.Repositories;
|
||||
using Store.Users.Entities;
|
||||
using YourNamespace;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
@@ -28,6 +31,7 @@ internal sealed class ShopTestContext : IDisposable, ICurrentUser, IUserQuerySer
|
||||
public StubHttp Handler { get; } = new();
|
||||
public PddTokenClient Gateway { get; }
|
||||
public ShopManagementService Service { get; }
|
||||
public IShopRepository Repository { get; }
|
||||
public IConfiguration Configuration { get; } = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Shops:PddClients:0:ClientId"] = "test-client",
|
||||
@@ -45,6 +49,11 @@ internal sealed class ShopTestContext : IDisposable, ICurrentUser, IUserQuerySer
|
||||
Db.CodeFirst.ConfigEntity<ShopApiAuthEntity>(e => e.Name("shop_auth"));
|
||||
Db.CodeFirst.ConfigEntity<ShopProductQuotaEntity>(e => e.Name("shop_quota"));
|
||||
Db.CodeFirst.SyncStructure(typeof(PddShopEntity), typeof(WeidianShopEntity), typeof(ShopApiAuthEntity), typeof(ShopProductQuotaEntity));
|
||||
Db.CodeFirst.ConfigEntity<ShopEntity>(e => e.Name("shops"));
|
||||
Db.CodeFirst.ConfigEntity<ShopExpireEntity>(e => e.Name("shop_expirations"));
|
||||
Db.CodeFirst.SyncStructure(typeof(ShopEntity), typeof(ShopExpireEntity));
|
||||
Repository = (IShopRepository)Activator.CreateInstance(
|
||||
typeof(ShopService).Assembly.GetType("Store.Shops.Repositories.ShopRepository", true)!, Db)!;
|
||||
Gateway = new(new HttpClient(Handler));
|
||||
Service = new(Db, this, this, Configuration, Gateway);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user