Files
2026-10-29 11:34:35 +08:00

195 lines
11 KiB
C#

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("店铺不存在或无删除权限");
}
}