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; /// 店铺业务入口;所有调用路径均从当前身份计算数据范围。 public sealed partial class ShopManagementService( IFreeSql db, ICurrentUser currentUser, IUserQueryService users, IConfiguration configuration, PddTokenClient e57) : ITransientDependency { private async Task 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> 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() .Where(a => db.Select().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> 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() .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().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().Where(q => q.ShopId == group.Key) .Set(q => q.AddProductMonth, month).Set(q => q.AddProductRemaining, 300).ExecuteAffrowsAsync(ct); } await orm.Update().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().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().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().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().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() .Where(s => s.ShopId == request.ShopId && s.UserName == actor.UserName).ExecuteAffrowsAsync(ct); if (affected == 0) throw new AppException("店铺不存在或无删除权限"); } }