增加Shop API
This commit is contained in:
1 parent
3769193696
commit
5d017740c6
83 files changed
+4451
-696
No files matched your search
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>e57 店铺令牌查询;固定上游地址,失败重试一次,保留证书验证。</summary>
|
||||
public sealed class PddTokenClient(HttpClient http) : IDisposable
|
||||
{
|
||||
/// <summary>按拼多多商城ID获取授权资料。</summary>
|
||||
public Task<JsonElement> GetToken(string mallId, CancellationToken ct)
|
||||
=> GetResult("info/forPdd?mallId=" + Uri.EscapeDataString(mallId), "data", ct);
|
||||
|
||||
/// <summary>按微信AppId获取单店授权资料。</summary>
|
||||
public Task<JsonElement> GetShopToken(string appId, CancellationToken ct)
|
||||
=> GetResult("info?appId=" + Uri.EscapeDataString(appId), "data", ct);
|
||||
|
||||
/// <summary>按部署配置中的e57账号读取微信令牌列表。</summary>
|
||||
public Task<JsonElement> GetTokens(string userName, CancellationToken ct)
|
||||
=> GetResult("list?userName=" + Uri.EscapeDataString(userName), "list", ct);
|
||||
|
||||
/// <summary>解析上游业务状态,只返回指定数据节点,不泄露原始错误页面。</summary>
|
||||
private async Task<JsonElement> GetResult(string path, string property, CancellationToken ct)
|
||||
{
|
||||
var url = "https://po.e57.cn/prod-api/api/sys-api/system/open/api/shop/" + path;
|
||||
for (var attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await http.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) continue;
|
||||
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(ct), cancellationToken: ct);
|
||||
var root = json.RootElement;
|
||||
if (root.ValueKind == JsonValueKind.Object
|
||||
&& root.TryGetProperty("code", out var code) && code.ValueKind == JsonValueKind.Number
|
||||
&& code.TryGetInt32(out var value) && value == 0
|
||||
&& root.TryGetProperty(property, out var data))
|
||||
return data.Clone();
|
||||
}
|
||||
catch (HttpRequestException) { /* 上游网络失败按旧接口约定重试一次。 */ }
|
||||
catch (JsonException) { /* 非 JSON 上游响应按业务失败处理,不返回原始页面。 */ }
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested) { /* HttpClient 超时。 */ }
|
||||
}
|
||||
throw new AppException("获取店铺令牌失败,请稍后重试");
|
||||
}
|
||||
|
||||
/// <summary>随容器关闭释放共享HTTP客户端。</summary>
|
||||
public void Dispose() => http.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopManagementService
|
||||
{
|
||||
public async Task<IReadOnlyList<PddClientInfo>> GetPddClients()
|
||||
{
|
||||
await Actor();
|
||||
var clients = configuration.GetSection("Shops:PddClients").Get<List<PddClientInfo>>() ?? [];
|
||||
if (clients.Count == 0 || clients.Any(c => string.IsNullOrWhiteSpace(c.ClientId)
|
||||
|| string.IsNullOrWhiteSpace(c.ClientSecret) || string.IsNullOrWhiteSpace(c.RedirectUri)))
|
||||
throw new AppException("尚未完整配置拼多多应用信息");
|
||||
return clients;
|
||||
}
|
||||
|
||||
public async Task<JsonElement> GetPddToken(PddTokenRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
if (!await db.Select<PddShopEntity>().Where(s => s.MallId == request.MallId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无读取令牌权限");
|
||||
return await e57.GetToken(request.MallId, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Security;
|
||||
using MAX.Framework.Validation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
/// <summary>店铺业务入口;所有调用路径均从当前身份计算数据范围。</summary>
|
||||
public sealed partial class ShopManagementService(
|
||||
IFreeSql db, ICurrentUser currentUser, IUserQueryService users,
|
||||
IConfiguration configuration, PddTokenClient e57) : ITransientDependency
|
||||
{
|
||||
private async Task<UserInfo> Actor()
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName))
|
||||
throw new UnauthorizedAccessException("请先登录");
|
||||
return await users.GetUser(currentUser.UserName);
|
||||
}
|
||||
|
||||
private static void ValidatePage(int page, int size)
|
||||
{
|
||||
if (page < 1 || size < 1 || size > 500 || (long)(page - 1) * size > int.MaxValue)
|
||||
throw new ValidationException("页码必须大于0,每页数量必须为1至500,分页偏移不能超限");
|
||||
}
|
||||
|
||||
public async Task<ShopPage<ShopClientInfo>> GetShopClientInfo(ShopClientSearchRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
ValidatePage(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
// 授权凭据即使由管理员请求,也必须属于本人或明确分配给本人的店铺。
|
||||
var query = db.Select<ShopApiAuthEntity>()
|
||||
.Where(a => db.Select<PddShopEntity>().Any(s => s.ShopId == a.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))))
|
||||
.WhereIf(request.ShopIds != null, a => request.ShopIds!.Contains(a.ShopId));
|
||||
var total = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
var mapper = new ShopMapper();
|
||||
return new(rows.Select(mapper.ToClientInfo).ToList(), total);
|
||||
}
|
||||
|
||||
public async Task<ShopPage<ShopInfo>> GetAdminShops(ShopSearchRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
ValidatePage(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
if (actor.SaLevel != UserAdminLevel.SuperAdmin)
|
||||
throw new AppException("仅管理员可查询全部店铺");
|
||||
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<PddShopEntity>()
|
||||
.Where(s => s.Cookie == "" || s.ShopId != "")
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName)
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var total = await query.CountAsync(ct);
|
||||
var shops = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
var mapper = new ShopMapper();
|
||||
var result = shops.Select(mapper.ToInfo).ToList();
|
||||
// shortcut: 旧库只保存月份;需要跨年区分同月额度时升级为年月字段。
|
||||
var month = DateTime.Now.Month;
|
||||
// 同店铺可能有多条登录记录,按 shop_id 加锁并只重置一次共享额度。
|
||||
foreach (var group in result.Where(s => s.ShopId != "").GroupBy(s => s.ShopId).OrderBy(g => g.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var quota = await orm.Select<ShopProductQuotaEntity>().Where(q => q.ShopId == group.Key).ForUpdate().FirstAsync(ct);
|
||||
if (quota == null)
|
||||
{
|
||||
quota = new() { ShopId = group.Key, ShopAccount = group.First().ShopAccount, AddProductMonth = month, AddProductRemaining = 300 };
|
||||
await orm.Insert(quota).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else if (quota.AddProductMonth != month)
|
||||
{
|
||||
quota.AddProductMonth = month;
|
||||
quota.AddProductRemaining = 300;
|
||||
await orm.Update<ShopProductQuotaEntity>().Where(q => q.ShopId == group.Key)
|
||||
.Set(q => q.AddProductMonth, month).Set(q => q.AddProductRemaining, 300).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
await orm.Update<PddShopEntity>().Where(s => s.ShopId == group.Key)
|
||||
.Set(s => s.AddProductMonth, quota.AddProductMonth)
|
||||
.Set(s => s.AddProductRemaining, quota.AddProductRemaining).ExecuteAffrowsAsync(ct);
|
||||
foreach (var shop in group)
|
||||
{
|
||||
shop.AddProductMonth = quota.AddProductMonth;
|
||||
shop.AddProductRemaining = quota.AddProductRemaining;
|
||||
}
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new(result, total);
|
||||
}
|
||||
|
||||
public async Task SaveWeidian(WeidianSaveRequest request, bool create, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.LoginName.Length > 255)
|
||||
throw new ValidationException("店铺登录名不能超过255个字符");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
using var work = db.CreateUnitOfWork();
|
||||
// 锁定匹配范围,避免同一账号并发导入产生重复记录;需要 InnoDB。
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<WeidianShopEntity>().Where(s => s.LoginName == request.LoginName)
|
||||
.Where(s => s.UserName == name || (!create && ("," + s.SubUserName + ",").Contains(member)))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId);
|
||||
var matches = await query.ForUpdate().ToListAsync(ct);
|
||||
if (matches.Count > 1)
|
||||
throw new AppException("登录名对应多个店铺,请指定店铺ID");
|
||||
var shop = matches.SingleOrDefault();
|
||||
if (shop == null)
|
||||
{
|
||||
if (!create) throw new AppException("店铺不存在或无操作权限");
|
||||
await orm.Insert(new WeidianShopEntity
|
||||
{
|
||||
LoginName = request.LoginName, UserName = name, ShopId = request.ShopId ?? "",
|
||||
LoginPass = request.LoginPass ?? "", Cookie = request.Cookie ?? "", Cookie2 = request.Cookie2 ?? "",
|
||||
WdToken = request.WdToken ?? "", MallId = request.MallId ?? "", ShopAccount = request.ShopAccount ?? "",
|
||||
SubUserName = request.SubUserName ?? "", GroupName = request.GroupName ?? "", Notes = request.Notes ?? "",
|
||||
LoginStatus = request.LoginStatus ?? "", AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (request.SubUserName != null && shop.UserName != name)
|
||||
throw new AppException("只有店铺所有者可以修改分配账号");
|
||||
var update = orm.Update<WeidianShopEntity>().Where(s => s.Id == shop.Id)
|
||||
.Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.WdToken != null) update.Set(s => s.WdToken, request.WdToken);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.MallId != null) update.Set(s => s.MallId, request.MallId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
public async Task UpdateWeidianCookie(WeidianCookieRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (string.IsNullOrEmpty(request.Cookie) && string.IsNullOrEmpty(request.LoginStatus))
|
||||
throw new ValidationException("Cookie和登录状态不能全为空");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var scope = db.Select<WeidianShopEntity>().Where(s => s.ShopId == request.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member)));
|
||||
if (!await scope.AnyAsync(ct)) throw new AppException("店铺不存在或无操作权限");
|
||||
var update = db.Update<WeidianShopEntity>().Where(s => s.ShopId == request.ShopId
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member)))
|
||||
.Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.WdToken != null) update.Set(s => s.WdToken, request.WdToken);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteWeidian(ShopIdRequest request, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var affected = await db.Delete<WeidianShopEntity>()
|
||||
.Where(s => s.ShopId == request.ShopId && s.UserName == actor.UserName).ExecuteAffrowsAsync(ct);
|
||||
if (affected == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>保存当前账号的店铺售后地址;同账号同店铺更新原记录。</summary>
|
||||
public async Task SaveDefaultAddress(AddressRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<DefaultAddressEntity>()
|
||||
.Where(a => a.UserId == actor.UserName && a.ShopId == request.ShopId).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new DefaultAddressEntity
|
||||
{
|
||||
UserId = actor.UserName, ShopId = request.ShopId, ProvinceName = request.ProvinceName,
|
||||
CityName = request.CityName, CountyName = request.CountyName, DetailInfo = request.DetailInfo,
|
||||
Receiver = request.Receiver, TelNumber = request.TelNumber, AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<DefaultAddressEntity>().Where(a => a.Id == row.Id)
|
||||
.Set(a => a.ProvinceName, request.ProvinceName).Set(a => a.CityName, request.CityName)
|
||||
.Set(a => a.CountyName, request.CountyName).Set(a => a.DetailInfo, request.DetailInfo)
|
||||
.Set(a => a.Receiver, request.Receiver).Set(a => a.TelNumber, request.TelNumber)
|
||||
.Set(a => a.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>按当前账号和可见店铺范围查询默认地址。</summary>
|
||||
public async Task<ShopPage<DefaultAddressInfo>> DefaultAddresses(AddressSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<DefaultAddressEntity>().Where(a => a.UserId == actor.UserName && ids.Contains(a.ShopId))
|
||||
.WhereIf(request.ShopIds != null, a => request.ShopIds!.Contains(a.ShopId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>删除本人指定店铺的默认地址,不影响其他账号保存的地址。</summary>
|
||||
public async Task DeleteDefaultAddresses(ShopIdsRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
if (request.ShopIds.Except(ids).Any()) throw new AppException("包含无权访问的店铺");
|
||||
await db.Delete<DefaultAddressEntity>().Where(a => a.UserId == actor.UserName && request.ShopIds.Contains(a.ShopId)).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>以店铺及订单为键保存解密地址,禁止跨店覆盖。</summary>
|
||||
public async Task SaveOrderAddress(OrderAddressRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OrderAddressEntity>().Where(a => a.ShopId == request.ShopId && a.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new OrderAddressEntity { ShopId = request.ShopId, OrderId = request.OrderId,
|
||||
OrderAddress = request.OrderAddress, AddTime = DateTime.Now, MdyTime = DateTime.Now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<OrderAddressEntity>().Where(a => a.Id == row.Id).Set(a => a.OrderAddress, request.OrderAddress)
|
||||
.Set(a => a.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>查询订单解密地址,先限制当前用户可见店铺,再按订单过滤。</summary>
|
||||
public async Task<ShopPage<OrderAddressInfo>> OrderAddresses(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.OrderIds == null) throw new ValidationException("订单编号不能为空");
|
||||
Ids(request.OrderIds);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OrderAddressEntity>().Where(a => ids.Contains(a.ShopId) && request.OrderIds.Contains(a.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>采购备注按可信主账号归属保存,直属子账号共享主账号的备注。</summary>
|
||||
public async Task SaveBuyNote(NoteRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var owner = MainName(await Actor());
|
||||
if (request.MainUserName != null && request.MainUserName != owner) throw new AppException("无权修改其他主账号备注");
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var row = await orm.Select<BuyUrlNoteEntity>().Where(n => n.UserName == owner && n.SpuCode == request.SpuCode).ForUpdate().FirstAsync(ct);
|
||||
if (row == null)
|
||||
await orm.Insert(new BuyUrlNoteEntity { UserName = owner, SpuCode = request.SpuCode,
|
||||
BuyUrlNotes = request.BuyUrlNotes, AddTime = DateTime.Now, MdyTime = DateTime.Now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<BuyUrlNoteEntity>().Where(n => n.Id == row.Id).Set(n => n.BuyUrlNotes, request.BuyUrlNotes)
|
||||
.Set(n => n.MdyTime, DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询当前主账号的采购备注。</summary>
|
||||
public async Task<ShopPage<BuyUrlNoteInfo>> BuyNotes(NoteSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
Ids(request.SpuCodes);
|
||||
var owner = MainName(await Actor());
|
||||
if (request.MainUserName != null && request.MainUserName != owner) throw new AppException("无权查询其他主账号备注");
|
||||
var query = db.Select<BuyUrlNoteEntity>().Where(n => n.UserName == owner && request.SpuCodes.Contains(n.SpuCode));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(n => n.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using YourNamespace;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>选品列表跨月时重置300条额度,并同步店铺表;同店多条登录记录只处理一次。</summary>
|
||||
private async Task RefreshQuotas(List<ShopDetailsInfo> shops, CancellationToken ct)
|
||||
{
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
// shortcut: 旧表仅保存月份;需要区分跨年同月额度时升级为年月字段。
|
||||
var month = DateTime.Now.Month;
|
||||
foreach (var group in shops.Where(s => !string.IsNullOrEmpty(s.ShopId)).GroupBy(s => s.ShopId).OrderBy(g => g.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var quota = await orm.Select<ShopQuotaEntity>().Where(q => q.ShopId == group.Key).ForUpdate().FirstAsync(ct);
|
||||
if (quota == null)
|
||||
{
|
||||
quota = new() { ShopId = group.Key, ShopAccount = group.First().ShopAccount, AddProductMonth = month, AddProductRemaining = 300 };
|
||||
await orm.Insert(quota).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else if (quota.AddProductMonth != month)
|
||||
{
|
||||
quota.AddProductMonth = month;
|
||||
quota.AddProductRemaining = 300;
|
||||
await orm.Update<ShopQuotaEntity>().Where(q => q.ShopId == group.Key)
|
||||
.Set(q => q.AddProductMonth, month).Set(q => q.AddProductRemaining, 300).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
await orm.Update<ShopEntity>().Where(s => s.ShopId == group.Key)
|
||||
.Set(s => s.AddProductMonth, (byte)quota.AddProductMonth)
|
||||
.Set(s => s.AddProductRemaining, quota.AddProductRemaining).ExecuteAffrowsAsync(ct);
|
||||
foreach (var item in group)
|
||||
{
|
||||
item.AddProductMonth = (byte)quota.AddProductMonth;
|
||||
item.AddProductRemaining = quota.AddProductRemaining;
|
||||
}
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>管理员设置明确指定店铺的共享额度,同步列表读取的店铺字段。</summary>
|
||||
public async Task UpdateQuota(QuotaRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
if (request.RemainCount < 0) throw new ValidationException("剩余额度不能小于0");
|
||||
RequireAdmin(await Actor());
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var existing = await orm.Select<ShopQuotaEntity>().Where(q => request.ShopIds.Contains(q.ShopId)).ForUpdate().ToListAsync(ct);
|
||||
if (request.ShopIds.Except(existing.Select(q => q.ShopId)).Any()) throw new AppException("部分店铺尚未建立额度记录");
|
||||
await orm.Update<ShopQuotaEntity>().Where(q => request.ShopIds.Contains(q.ShopId))
|
||||
.Set(q => q.AddProductRemaining, request.RemainCount).ExecuteAffrowsAsync(ct);
|
||||
await orm.Update<ShopEntity>().Where(s => request.ShopIds.Contains(s.ShopId))
|
||||
.Set(s => s.AddProductRemaining, request.RemainCount).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>管理员按记录ID或登录名分配选品操作员,空账号表示解除分配。</summary>
|
||||
public async Task Assign(AssignRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.DbIds is not { Length: > 0 } && request.ShopLoginNames is not { Length: > 0 })
|
||||
throw new ValidationException("请选择店铺记录或登录名");
|
||||
if (request.DbIds?.Any(id => id <= 0) == true) throw new ValidationException("记录ID必须大于0");
|
||||
if (request.ShopLoginNames != null) Ids(request.ShopLoginNames);
|
||||
RequireAdmin(await Actor());
|
||||
if (request.OperateSubSaName != "") await users.GetUser(request.OperateSubSaName);
|
||||
var update = db.Update<ShopEntity>();
|
||||
if (request.DbIds is { Length: > 0 }) update.Where(s => request.DbIds.Contains(s.Id));
|
||||
else update.Where(s => request.ShopLoginNames!.Contains(s.LoginName));
|
||||
if (await update.Set(s => s.OperateSubSaName, request.OperateSubSaName)
|
||||
.Set(s => s.AddSubSaTime, DateTime.Now).ExecuteAffrowsAsync(ct) == 0)
|
||||
throw new AppException("未找到指定店铺");
|
||||
}
|
||||
|
||||
/// <summary>查询授权店铺对应的ERP商户余额,不能仅凭外部商户ID读取他人余额。</summary>
|
||||
public async Task<ShopPage<ErpAccountInfo>> ErpMerchants(ErpRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.CorporateAccountUserId == null && request.ManageUserId == null)
|
||||
throw new ValidationException("必须指定公司账号或管理账号");
|
||||
if (request.CorporateAccountUserId <= 0 || request.ManageUserId <= 0)
|
||||
throw new ValidationException("ERP账号ID必须大于0");
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var linked = await db.Select<ErpShopEntity>().Where(s => ids.Contains(s.AppKey))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.AppKey == request.ShopId).ToListAsync(ct);
|
||||
var managers = new List<long>();
|
||||
if (request.ManageUserId.HasValue)
|
||||
{
|
||||
if (!linked.Any(s => s.ManageUserId == request.ManageUserId)) throw new AppException("无权查询该ERP管理账号");
|
||||
managers.Add(request.ManageUserId.Value);
|
||||
}
|
||||
if (request.CorporateAccountUserId.HasValue)
|
||||
{
|
||||
if (!linked.Any(s => s.CorporateAccountUserId == request.CorporateAccountUserId))
|
||||
throw new AppException("无权查询该ERP公司账号");
|
||||
managers.AddRange(await db.Select<ErpGroupEntity>()
|
||||
.Where(g => g.CorporateAccountUserId == request.CorporateAccountUserId)
|
||||
.ToListAsync(g => g.ManageUserId, ct));
|
||||
}
|
||||
var query = db.Select<ErpAccountEntity>().Where(a => managers.Contains(a.UserId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(a => a.Balance).OrderBy(a => a.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>保存授权店铺的物流优化结果;订单已关联其他店铺时拒绝覆盖。</summary>
|
||||
public async Task SaveOptimizeResult(OptimizeResultRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OptimizeResultEntity>().Where(r => r.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单属于其他店铺,不能覆盖记录");
|
||||
if (row == null)
|
||||
await orm.Insert(new OptimizeResultEntity
|
||||
{
|
||||
ShopId = request.ShopId, OrderId = request.OrderId, OptimizeRst = request.OptimizeRst!,
|
||||
WaybillId = request.WaybillId ?? "", DeliveryName = request.DeliveryName ?? "",
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
{
|
||||
var update = orm.Update<OptimizeResultEntity>().Where(r => r.Id == row.Id)
|
||||
.Set(r => r.OptimizeRst, request.OptimizeRst!).Set(r => r.MdyTime, DateTime.Now);
|
||||
if (request.WaybillId != null) update.Set(r => r.WaybillId, request.WaybillId);
|
||||
if (request.DeliveryName != null) update.Set(r => r.DeliveryName, request.DeliveryName);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询可见店铺的物流优化结果,统计与数据使用相同范围。</summary>
|
||||
public async Task<ShopPage<OptimizeResultInfo>> OptimizeResults(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OptimizeResultEntity>().Where(r => ids.Contains(r.ShopId))
|
||||
.WhereIf(request.OrderIds != null, r => request.OrderIds!.Contains(r.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>保存授权店铺的物流优化备注;订单已关联其他店铺时拒绝覆盖。</summary>
|
||||
public async Task SaveOptimizeRemark(OptimizeRemarkRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<OptimizeRemarkEntity>().Where(r => r.OrderId == request.OrderId).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单属于其他店铺,不能覆盖记录");
|
||||
if (row == null)
|
||||
await orm.Insert(new OptimizeRemarkEntity
|
||||
{
|
||||
ShopId = request.ShopId, OrderId = request.OrderId, Remark = request.Remark!,
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
{
|
||||
var update = orm.Update<OptimizeRemarkEntity>().Where(r => r.Id == row.Id)
|
||||
.Set(r => r.Remark, request.Remark!).Set(r => r.MdyTime, DateTime.Now);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>分页查询可见店铺的物流优化备注,统计与数据使用相同范围。</summary>
|
||||
public async Task<ShopPage<OptimizeRemarkInfo>> OptimizeRemarks(OrderSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var ids = await VisibleIds(actor, ct);
|
||||
var query = db.Select<OptimizeRemarkEntity>().Where(r => ids.Contains(r.ShopId))
|
||||
.WhereIf(request.OrderIds != null, r => request.OrderIds!.Contains(r.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(r => r.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>查询微信业务下的拼多多店铺,保持与 bxx.shop 数据隔离。</summary>
|
||||
public async Task<ShopPage<ShopInfo>> PddShops(ShopSearchRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var query = db.Select<PddLoginShopEntity>().Where(s => s.UserName == name || ("," + s.SubUserName + ",").Contains(member))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName)
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var count = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>导入拼多多店铺,重复导入更新资料;已有Cookie只能由专用登录更新接口修改。</summary>
|
||||
public async Task SavePdd(PddSaveRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
await ValidateAssignments(request.SubUserName, name);
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var rows = await orm.Select<PddLoginShopEntity>().Where(s => s.UserName == name && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ForUpdate().ToListAsync(ct);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
await orm.Insert(new PddLoginShopEntity
|
||||
{
|
||||
UserName = name, LoginName = request.LoginName, LoginPass = request.LoginPass ?? "",
|
||||
Cookie = request.Cookie ?? "", Cookie2 = request.Cookie2 ?? "", AntiContents = request.AntiContents ?? "",
|
||||
ShopId = request.ShopId ?? "", MallId = request.MallId ?? "", ShopAccount = request.ShopAccount ?? "",
|
||||
SubUserName = request.SubUserName ?? "", GroupName = request.GroupName ?? "", Notes = request.Notes ?? "",
|
||||
LoginStatus = request.LoginStatus ?? "", AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ids = rows.Select(s => s.Id).ToArray();
|
||||
var update = orm.Update<PddLoginShopEntity>().Where(s => ids.Contains(s.Id)).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.AntiContents != null) update.Set(s => s.AntiContents, request.AntiContents);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.MallId != null) update.Set(s => s.MallId, request.MallId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>刷新本人或已分配拼多多店铺的Cookie、状态及拼多多用户ID。</summary>
|
||||
public async Task UpdatePddCookies(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.Cookie == null && request.Cookie2 == null && request.LoginStatus == null && request.LoginStatus2 == null && request.PddId == null)
|
||||
throw new ValidationException("至少提供一个登录状态字段");
|
||||
var actor = await Actor();
|
||||
var name = actor.UserName;
|
||||
var member = "," + name + ",";
|
||||
var update = db.Update<PddLoginShopEntity>().Where(s => s.LoginName == request.LoginName
|
||||
&& (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
if (request.LoginStatus2 != null) update.Set(s => s.LoginStatus2, request.LoginStatus2);
|
||||
if (request.PddId != null) update.Set(s => s.PddId, request.PddId);
|
||||
if (await update.ExecuteAffrowsAsync(ct) == 0 && !await db.Select<PddLoginShopEntity>()
|
||||
.Where(s => s.LoginName == request.LoginName && (s.UserName == name || ("," + s.SubUserName + ",").Contains(member))).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>删除当前账号自己创建的拼多多店铺,分配账号不能删除。</summary>
|
||||
public async Task DeletePdd(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var count = await db.Delete<PddLoginShopEntity>().Where(s => s.UserName == actor.UserName && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ExecuteAffrowsAsync(ct);
|
||||
if (count == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>获取当前用户授权店铺的微信令牌,外部请求不在数据库事务中执行。</summary>
|
||||
public async Task<JsonElement> ShopToken(ShopIdRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
await RequireShop(db, actor, request.ShopId, ct);
|
||||
return await tokens.GetShopToken(request.ShopId, ct);
|
||||
}
|
||||
|
||||
/// <summary>管理员读取共享令牌列表;缓存有效期60分钟,配置按缓存ID指定e57账号。</summary>
|
||||
public async Task<TokensInfo> TokenList(TokenRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.DbId <= 0) throw new ValidationException("缓存记录ID必须大于0");
|
||||
RequireAdmin(await Actor());
|
||||
var cached = await db.Select<TokenEntity>().Where(t => t.Id == request.DbId).FirstAsync(ct);
|
||||
if (cached != null && cached.MdyTime.AddMinutes(60) > DateTime.Now)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(cached.Tokens);
|
||||
if (json.RootElement.ValueKind == JsonValueKind.Array) return new(json.RootElement.Clone(), cached.MdyTime);
|
||||
}
|
||||
catch (JsonException) { /* 旧缓存损坏时重新向上游获取,不返回无法解析的数据。 */ }
|
||||
}
|
||||
var account = configuration[$"Shops:E57WxAccounts:{request.DbId}"];
|
||||
if (string.IsNullOrWhiteSpace(account)) throw new AppException("尚未配置该缓存分组的e57微信账号");
|
||||
var list = await tokens.GetTokens(account, ct);
|
||||
if (list.ValueKind != JsonValueKind.Array) throw new AppException("e57返回的令牌列表格式无效");
|
||||
var now = DateTime.Now;
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var existing = await orm.Select<TokenEntity>().Where(t => t.Id == request.DbId).ForUpdate().FirstAsync(ct);
|
||||
if (existing == null)
|
||||
await orm.Insert(new TokenEntity { Id = request.DbId, Tokens = list.GetRawText(), MdyTime = now }).ExecuteAffrowsAsync(ct);
|
||||
else
|
||||
await orm.Update<TokenEntity>().Where(t => t.Id == request.DbId)
|
||||
.Set(t => t.Tokens, list.GetRawText()).Set(t => t.MdyTime, now).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new(list, now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
|
||||
namespace Store.Shops.Services;
|
||||
|
||||
public sealed partial class ShopService
|
||||
{
|
||||
/// <summary>查询本人运单;跨账号按订单查询仅供管理员使用且必须指定订单。</summary>
|
||||
public async Task<ShopPage<WaybillInfo>> Waybills(WaybillSearchRequest request, bool allUsers, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
if (request.Status?.Any(s => s is < 0 or > 2) == true) throw new ValidationException("运单状态只能为0、1、2");
|
||||
var actor = await Actor();
|
||||
if (allUsers)
|
||||
{
|
||||
RequireAdmin(actor);
|
||||
if (request.OrderIds == null) throw new ValidationException("订单编号不能为空");
|
||||
Ids(request.OrderIds);
|
||||
}
|
||||
var query = db.Select<WaybillEntity>().WhereIf(!allUsers, w => w.UserName == actor.UserName)
|
||||
.WhereIf(request.Status != null, w => request.Status!.Contains(w.Status))
|
||||
.WhereIf(request.OrderIds != null, w => request.OrderIds!.Contains(w.OrderId));
|
||||
var count = request.NotCount ? -1 : await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(w => w.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count);
|
||||
}
|
||||
|
||||
/// <summary>导入运单;揽收时间或绑定订单至少提供一个,运单号全库去重。</summary>
|
||||
public async Task AddWaybill(WaybillAddRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.WaybillPickupTime == null && string.IsNullOrWhiteSpace(request.BindOrderId))
|
||||
throw new ValidationException("揽收时间和绑定订单不能同时为空");
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
if (await orm.Select<WaybillEntity>().Where(w => w.WaybillId == request.WaybillId).ForUpdate().AnyAsync(ct))
|
||||
throw new AppException("运单号已存在");
|
||||
await orm.Insert(new WaybillEntity
|
||||
{
|
||||
UserName = actor.UserName, DeliveryId = request.DeliveryId, DeliveryName = request.DeliveryName,
|
||||
WaybillId = request.WaybillId, BindOrderId = request.BindOrderId ?? "", WaybillPickupTime = request.WaybillPickupTime,
|
||||
Status = 0, AddTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>只删除本人尚未使用的运单,已预占或使用的运单保持不变。</summary>
|
||||
public async Task DeleteWaybills(WaybillDeleteRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.WaybillIds);
|
||||
var actor = await Actor();
|
||||
await db.Delete<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& request.WaybillIds.Contains(w.WaybillId)).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>预占或直接使用运单;先复用同订单记录,再按绑定订单及次日揽收条件选择。</summary>
|
||||
public async Task<WaybillInfo> SelectWaybill(WaybillSelectRequest request, bool useImmediately, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.OrderTime?.Date >= DateTime.MaxValue.Date.AddDays(-2))
|
||||
throw new ValidationException("订单时间超出可匹配范围");
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<WaybillEntity>()
|
||||
.Where(w => w.UserName == actor.UserName && w.OrderId == request.OrderId && (w.Status == 1 || w.Status == 2))
|
||||
.OrderBy(w => w.Id).ForUpdate().FirstAsync(ct);
|
||||
if (row != null && !string.IsNullOrEmpty(row.ShopId) && row.ShopId != request.ShopId)
|
||||
throw new AppException("订单已绑定其他店铺的运单");
|
||||
if (row == null)
|
||||
{
|
||||
row = await orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& w.BindOrderId == request.OrderId && (w.OrderId == "" || w.OrderId == null))
|
||||
.OrderBy("RAND()").ForUpdate().FirstAsync(ct);
|
||||
if (row == null && (request.OrderTime.HasValue || useImmediately))
|
||||
{
|
||||
var available = orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
|
||||
&& (w.OrderId == "" || w.OrderId == null) && (w.BindOrderId == "" || w.BindOrderId == null));
|
||||
if (!useImmediately)
|
||||
{
|
||||
var start = request.OrderTime!.Value.Date.AddDays(1);
|
||||
var end = start.AddDays(1);
|
||||
available.Where(w => w.WaybillPickupTime >= start && w.WaybillPickupTime < end);
|
||||
}
|
||||
row = await available.OrderBy("RAND()").ForUpdate().FirstAsync(ct);
|
||||
}
|
||||
if (row == null) throw new AppException("没有可用的运单号");
|
||||
}
|
||||
if (row.Status != 2)
|
||||
{
|
||||
var status = useImmediately ? 2 : 1;
|
||||
var useTime = useImmediately ? DateTime.Now : (DateTime?)null;
|
||||
// 行锁与状态条件一起保护分配;请求重试不会重复占用第二张运单。
|
||||
var affected = await orm.Update<WaybillEntity>().Where(w => w.Id == row.Id && w.Status == row.Status)
|
||||
.Set(w => w.OrderId, request.OrderId).Set(w => w.ShopId, request.ShopId)
|
||||
.Set(w => w.Status, status).Set(w => w.UseTime, useTime).ExecuteAffrowsAsync(ct);
|
||||
if (affected == 0 && row.Status == 0) throw new AppException("运单已被占用,请重试");
|
||||
row.OrderId = request.OrderId;
|
||||
row.ShopId = request.ShopId;
|
||||
row.Status = status;
|
||||
row.UseTime = useTime;
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return new ShopDetailsMapper().Map(row);
|
||||
}
|
||||
|
||||
/// <summary>仅对本人的预占运单确认使用或释放;不能释放已正式使用的运单。</summary>
|
||||
public async Task UseWaybill(WaybillUseRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
await RequireShop(orm, actor, request.ShopId, ct);
|
||||
var row = await orm.Select<WaybillEntity>().Where(w => w.UserName == actor.UserName
|
||||
&& w.OrderId == request.OrderId && w.WaybillId == request.WaybillId)
|
||||
.ForUpdate().FirstAsync(ct);
|
||||
if (row == null || (row.ShopId != "" && row.ShopId != null && row.ShopId != request.ShopId))
|
||||
throw new AppException("运单不存在或不属于指定店铺订单");
|
||||
if (!request.IsRollback && row.Status == 2) return;
|
||||
if (row.Status != 1) throw new AppException("只有预占运单可以确认或释放");
|
||||
var changed = await orm.Update<WaybillEntity>().Where(w => w.Id == row.Id && w.Status == 1)
|
||||
.Set(w => w.ShopId, request.IsRollback ? "" : request.ShopId)
|
||||
.Set(w => w.OrderId, request.IsRollback ? "" : request.OrderId)
|
||||
.Set(w => w.Status, request.IsRollback ? 0 : 2)
|
||||
.Set(w => w.UseTime, request.IsRollback ? null : (DateTime?)DateTime.Now).ExecuteAffrowsAsync(ct);
|
||||
if (changed != 1) throw new AppException("运单状态已变化,请重新查询");
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq.Expressions;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Security;
|
||||
using MAX.Framework.Validation;
|
||||
using Store.Shops.Dtos;
|
||||
using Store.Shops.Entities;
|
||||
using Store.Shops.Mappers;
|
||||
using Store.Users.Contracts;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
using Store.Users.Contracts.Enums;
|
||||
using Store.Users.Entities;
|
||||
using YourNamespace;
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>读取当前数据库账户,避免相信客户端传来的身份或管理员标志。</summary>
|
||||
private async Task<UserInfo> Actor()
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName))
|
||||
throw new UnauthorizedAccessException("请先登录");
|
||||
return await users.GetUser(currentUser.UserName);
|
||||
}
|
||||
|
||||
/// <summary>校验管理员专属入口。</summary>
|
||||
private static void RequireAdmin(UserInfo actor)
|
||||
{
|
||||
if (actor.SaLevel != UserAdminLevel.SuperAdmin) throw new AppException("仅管理员可执行此操作");
|
||||
}
|
||||
|
||||
/// <summary>主账号数据归属;子账号使用可信数据库记录中的父账号。</summary>
|
||||
private static string MainName(UserInfo actor)
|
||||
{
|
||||
if (actor.GroupId == UserGroup.MainAccount || actor.SaLevel == UserAdminLevel.SuperAdmin)
|
||||
return actor.UserName;
|
||||
if (string.IsNullOrWhiteSpace(actor.ParentUserName)) throw new AppException("子账号缺少所属主账号");
|
||||
return actor.ParentUserName;
|
||||
}
|
||||
|
||||
/// <summary>本人店铺或直属主账号分配的店铺;完整匹配逗号分隔账号,避免前缀越权。</summary>
|
||||
private static Expression<Func<ShopEntity, bool>> Scope(UserInfo actor)
|
||||
{
|
||||
var owner = MainName(actor);
|
||||
var main = owner == actor.UserName;
|
||||
var member = "," + actor.UserName + ",";
|
||||
return s => s.UserName == owner && (main || ("," + s.SubUserName + ",").Contains(member));
|
||||
}
|
||||
|
||||
/// <summary>为关联表查询提供当前用户可见店铺集合,不扩大管理员普通入口的范围。</summary>
|
||||
private Task<List<string>> VisibleIds(UserInfo actor, CancellationToken ct)
|
||||
=> db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.ShopId != "").Distinct().ToListAsync(s => s.ShopId, ct);
|
||||
|
||||
/// <summary>关联数据操作必须先核对店铺归属;使用事务时传入同一工作单元。</summary>
|
||||
private static async Task RequireShop(IFreeSql orm, UserInfo actor, string shopId, CancellationToken ct)
|
||||
{
|
||||
if (!await orm.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.ShopId == shopId).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>统一分页边界;DTO只声明基础必填规则。</summary>
|
||||
private static void Page(int page, int size)
|
||||
{
|
||||
if (page < 1 || size is < 1 or > 500 || (long)(page - 1) * size > int.MaxValue)
|
||||
throw new ValidationException("分页参数无效,每页数量必须为1至500");
|
||||
}
|
||||
|
||||
/// <summary>批量资源必须明确指定,禁止空数组退化为全表操作。</summary>
|
||||
private static void Ids(string[] ids)
|
||||
{
|
||||
if (ids.Length == 0 || ids.Any(string.IsNullOrWhiteSpace))
|
||||
throw new ValidationException("编号数组不能为空且不能包含空编号");
|
||||
}
|
||||
|
||||
/// <summary>分配账号必须是店铺主账号的直属子账号。</summary>
|
||||
private async Task ValidateAssignments(string? names, string owner)
|
||||
{
|
||||
if (names == null || names == "") return;
|
||||
foreach (var name in names.Split(','))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || name != name.Trim())
|
||||
throw new ValidationException("分配账号须使用逗号分隔,不能包含空项或首尾空格");
|
||||
var child = await users.GetUser(name);
|
||||
if (child.ParentUserName != owner || child.SaLevel == UserAdminLevel.SuperAdmin)
|
||||
throw new AppException("只能分配所属主账号的直属子账号");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>普通或管理店铺列表;选品模式按两张选品表筛选并维护每月共享额度。</summary>
|
||||
public async Task<ShopPage<ShopDetailsInfo>> Search(ShopQueryRequest request, bool admin = false,
|
||||
bool selection = false, CancellationToken ct = default)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Page(request.Page, request.PageSize);
|
||||
var actor = await Actor();
|
||||
if (admin || selection) RequireAdmin(actor);
|
||||
var query = db.Select<ShopEntity>();
|
||||
if (!admin && !selection) query.Where(Scope(actor));
|
||||
else query.Where(s => s.Cookie == "" || s.ShopId != "");
|
||||
if (selection && !string.IsNullOrEmpty(request.OperateSubSaName))
|
||||
query.Where(s => db.Select<SelectionEntity>().Any(p => p.ShopId == s.ShopId && p.OperateSubSaName == request.OperateSubSaName)
|
||||
|| db.Select<ThirdSelectionEntity>().Any(p => p.ShopId == s.ShopId && p.OperateSubSaName == request.OperateSubSaName));
|
||||
else query.WhereIf(!string.IsNullOrEmpty(request.OperateSubSaName), s => s.OperateSubSaName == request.OperateSubSaName);
|
||||
query.WhereIf(!string.IsNullOrEmpty(request.SearchId), s => s.ShopId.Contains(request.SearchId!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchUserName), s => s.UserName.Contains(request.SearchUserName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchLoginName), s => s.LoginName.Contains(request.SearchLoginName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchName), s => s.ShopAccount.Contains(request.SearchName!))
|
||||
.WhereIf(!string.IsNullOrEmpty(request.SearchNotes), s => s.Notes.Contains(request.SearchNotes!))
|
||||
.WhereIf(request.ShopIds != null, s => request.ShopIds!.Contains(s.ShopId))
|
||||
.WhereIf(request.ShopNames != null, s => request.ShopNames!.Contains(s.ShopAccount))
|
||||
.WhereIf(request.ShopNotes != null, s => request.ShopNotes!.Contains(s.Notes));
|
||||
var total = await query.CountAsync(ct);
|
||||
var rows = await query.OrderByDescending(s => s.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
|
||||
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 paid = actor.Authority?.Contains("充值用户") == true;
|
||||
if (!admin && !string.IsNullOrEmpty(actor.ParentUserName))
|
||||
paid |= (await users.GetUser(actor.ParentUserName)).Authority?.Contains("充值用户") == true;
|
||||
var now = DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.ExpireTime = item.NeverExpires ? 1893427200 : expires.FirstOrDefault(e => e.ShopId == item.ShopId)?.ExpireTime ?? 0;
|
||||
item.IsPointShopExpire = !admin && paid && !item.NeverExpires && item.ExpireTime < now;
|
||||
}
|
||||
if (request.IsGetErpShopId)
|
||||
{
|
||||
var erp = await db.Select<ErpShopEntity>().Where(e => ids.Contains(e.AppKey)).OrderBy(e => e.Id).ToListAsync(ct);
|
||||
foreach (var item in result)
|
||||
{
|
||||
var linked = erp.FirstOrDefault(e => e.AppKey == item.ShopId);
|
||||
item.ErpShopId = linked?.Id;
|
||||
item.ErpCorporateAccountUserId = linked?.CorporateAccountUserId;
|
||||
item.ErpManageUserId = linked?.ManageUserId;
|
||||
}
|
||||
}
|
||||
if (selection) await RefreshQuotas(result, ct);
|
||||
return new(result, total);
|
||||
}
|
||||
|
||||
/// <summary>查询同一登录名的授权店铺,供客户端判断新增或更新。</summary>
|
||||
public async Task<IReadOnlyList<ShopDetailsInfo>> ByLogin(LoginRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
var rows = await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName)
|
||||
.OrderBy(s => s.Id).ToListAsync(ct);
|
||||
return rows.Select(new ShopDetailsMapper().Map).ToList();
|
||||
}
|
||||
|
||||
/// <summary>批量读取授权店铺Cookie;对重复店铺ID按最新记录确定结果。</summary>
|
||||
public async Task<IReadOnlyList<ShopCookieInfo>> Cookies(ShopIdsRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
Ids(request.ShopIds);
|
||||
var actor = await Actor();
|
||||
var rows = await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => request.ShopIds.Contains(s.ShopId))
|
||||
.OrderByDescending(s => s.Id).ToListAsync(ct);
|
||||
if (request.ShopIds.Except(rows.Select(s => s.ShopId)).Any()) throw new AppException("包含无权访问的店铺");
|
||||
return rows.DistinctBy(s => s.ShopId).Select(s => new ShopCookieInfo(s.ShopId, s.Cookie)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>新增或修改微信店铺;新增重复时报错,不使用微店的覆盖导入行为。</summary>
|
||||
public async Task Save(ShopSaveRequest request, bool create, bool admin, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
if (admin) RequireAdmin(actor);
|
||||
if (admin && string.IsNullOrWhiteSpace(request.ShopUserName)) throw new ValidationException("管理员操作必须指定店铺所属账号");
|
||||
var owner = admin ? (await users.GetUser(request.ShopUserName!)).UserName : MainName(actor);
|
||||
if (create && !admin && owner != actor.UserName) throw new AppException("只有主账号可新增店铺");
|
||||
if (request.SubUserName != null && !admin && owner != actor.UserName) throw new AppException("子账号不能修改店铺分配关系");
|
||||
await ValidateAssignments(request.SubUserName, owner);
|
||||
using var work = db.CreateUnitOfWork();
|
||||
work.IsolationLevel = System.Data.IsolationLevel.Serializable;
|
||||
work.GetOrBeginTransaction();
|
||||
var orm = work.Orm;
|
||||
var query = orm.Select<ShopEntity>().Where(s => s.UserName == owner && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId);
|
||||
if (!admin) query.Where(Scope(actor));
|
||||
var rows = await query.ForUpdate().ToListAsync(ct);
|
||||
if (create)
|
||||
{
|
||||
if (rows.Count != 0) throw new AppException("店铺已存在");
|
||||
await orm.Insert(new ShopEntity
|
||||
{
|
||||
UserName = owner, LoginName = request.LoginName, LoginPass = request.LoginPass ?? "",
|
||||
ShopId = request.ShopId ?? "", ShopAccount = request.ShopAccount ?? "", Cookie = request.Cookie ?? "",
|
||||
Cookie2 = request.Cookie2 ?? "", AntiContents = request.AntiContents ?? "", SubUserName = request.SubUserName ?? "",
|
||||
GroupName = request.GroupName ?? "", Notes = request.Notes ?? "", Openid = "", CompanyName = "",
|
||||
LoginStatus = request.LoginStatus ?? "", LoginStatus2 = "", OperateSubSaName = "",
|
||||
AddTime = DateTime.Now, MdyTime = DateTime.Now
|
||||
}).ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rows.Count == 0) throw new AppException("店铺不存在或无操作权限");
|
||||
var ids = rows.Select(s => s.Id).ToArray();
|
||||
var update = orm.Update<ShopEntity>().Where(s => ids.Contains(s.Id)).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.LoginPass != null) update.Set(s => s.LoginPass, request.LoginPass);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.AntiContents != null) update.Set(s => s.AntiContents, request.AntiContents);
|
||||
if (request.NewShopId != null) update.Set(s => s.ShopId, request.NewShopId);
|
||||
if (request.ShopAccount != null) update.Set(s => s.ShopAccount, request.ShopAccount);
|
||||
if (request.SubUserName != null) update.Set(s => s.SubUserName, request.SubUserName);
|
||||
if (request.GroupName != null) update.Set(s => s.GroupName, request.GroupName);
|
||||
if (request.Notes != null) update.Set(s => s.Notes, request.Notes);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
await update.ExecuteAffrowsAsync(ct);
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
}
|
||||
|
||||
/// <summary>更新登录名下全部授权店铺的Cookie与状态,禁止跨账号同名更新。</summary>
|
||||
public async Task UpdateCookies(CookieRequest request, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
if (request.Cookie == null && request.Cookie2 == null && request.LoginStatus == null && request.LoginStatus2 == null && request.Openid == null)
|
||||
throw new ValidationException("至少提供一个登录状态字段");
|
||||
var actor = await Actor();
|
||||
var update = db.Update<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName).Set(s => s.MdyTime, DateTime.Now);
|
||||
if (request.Cookie != null) update.Set(s => s.Cookie, request.Cookie);
|
||||
if (request.Cookie2 != null) update.Set(s => s.Cookie2, request.Cookie2);
|
||||
if (request.LoginStatus != null) update.Set(s => s.LoginStatus, request.LoginStatus);
|
||||
if (request.LoginStatus2 != null) update.Set(s => s.LoginStatus2, request.LoginStatus2);
|
||||
if (request.Openid != null) update.Set(s => s.Openid, request.Openid);
|
||||
if (await update.ExecuteAffrowsAsync(ct) == 0
|
||||
&& !await db.Select<ShopEntity>().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName).AnyAsync(ct))
|
||||
throw new AppException("店铺不存在或无操作权限");
|
||||
}
|
||||
|
||||
/// <summary>删除所属账号的店铺记录;不删除独立的续费记录。</summary>
|
||||
public async Task Delete(LoginRequest request, bool admin, CancellationToken ct)
|
||||
{
|
||||
RequestValidator.Validate(request);
|
||||
var actor = await Actor();
|
||||
if (admin) RequireAdmin(actor);
|
||||
if (admin && string.IsNullOrWhiteSpace(request.ShopUserName)) throw new ValidationException("必须指定店铺所属账号");
|
||||
var owner = admin ? request.ShopUserName! : actor.UserName;
|
||||
var deleted = await db.Delete<ShopEntity>().Where(s => s.UserName == owner && s.LoginName == request.LoginName)
|
||||
.WhereIf(!string.IsNullOrEmpty(request.ShopId), s => s.ShopId == request.ShopId).ExecuteAffrowsAsync(ct);
|
||||
if (deleted == 0) throw new AppException("店铺不存在或无删除权限");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user