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.Shops.Repositories; using Store.Users.Contracts; using Store.Users.Contracts.Dtos; using Store.Users.Contracts.Enums; using Store.Users.Entities; using YourNamespace; namespace Store.Shops.Services; /// 微信店铺业务;账号身份来自服务端,普通子账号只能访问主账号明确分配的店铺。 public sealed partial class ShopService(IFreeSql db, ICurrentUser currentUser, IUserQueryService users, PddTokenClient tokens, Microsoft.Extensions.Configuration.IConfiguration configuration, IShopRepository shops) : ITransientDependency { /// 读取当前数据库账户,避免相信客户端传来的身份或管理员标志。 private async Task Actor() { if (!currentUser.IsAuthenticated || string.IsNullOrWhiteSpace(currentUser.UserName)) throw new UnauthorizedAccessException("请先登录"); return await users.GetUser(currentUser.UserName); } /// 校验管理员专属入口。 private static void RequireAdmin(UserInfo actor) { if (actor.SaLevel != UserAdminLevel.SuperAdmin) throw new AppException("仅管理员可执行此操作"); } /// 主账号数据归属;子账号使用可信数据库记录中的父账号。 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; } /// 本人店铺或直属主账号分配的店铺;完整匹配逗号分隔账号,避免前缀越权。 private static Expression> 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)); } /// 为关联表查询提供当前用户可见店铺集合,不扩大管理员普通入口的范围。 private Task> VisibleIds(UserInfo actor, CancellationToken ct) => db.Select().Where(Scope(actor)).Where(s => s.ShopId != "").Distinct().ToListAsync(s => s.ShopId, ct); /// 关联数据操作必须先核对店铺归属;使用事务时传入同一工作单元。 private static async Task RequireShop(IFreeSql orm, UserInfo actor, string shopId, CancellationToken ct) { if (!await orm.Select().Where(Scope(actor)).Where(s => s.ShopId == shopId).AnyAsync(ct)) throw new AppException("店铺不存在或无操作权限"); } /// 统一分页边界;DTO只声明基础必填规则。 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"); } /// 批量资源必须明确指定,禁止空数组退化为全表操作。 private static void Ids(string[] ids) { if (ids.Length == 0 || ids.Any(string.IsNullOrWhiteSpace)) throw new ValidationException("编号数组不能为空且不能包含空编号"); } /// 分配账号必须是店铺主账号的直属子账号。 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("只能分配所属主账号的直属子账号"); } } /// 普通或管理店铺列表;选品模式按两张选品表筛选并维护每月共享额度。 public async Task> 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(); 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().Any(p => p.ShopId == s.ShopId && p.OperateSubSaName == request.OperateSubSaName) || db.Select().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 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; 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().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); } /// 查询同一登录名的授权店铺,供客户端判断新增或更新。 public async Task> ByLogin(LoginRequest request, CancellationToken ct) { RequestValidator.Validate(request); var actor = await Actor(); var rows = await db.Select().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName) .OrderBy(s => s.Id).ToListAsync(ct); return rows.Select(new ShopDetailsMapper().Map).ToList(); } /// 批量读取授权店铺Cookie;对重复店铺ID按最新记录确定结果。 public async Task> Cookies(ShopIdsRequest request, CancellationToken ct) { RequestValidator.Validate(request); Ids(request.ShopIds); var actor = await Actor(); var rows = await db.Select().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(); } /// 新增或修改微信店铺;新增重复时报错,不使用微店的覆盖导入行为。 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().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().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(); } /// 更新登录名下全部授权店铺的Cookie与状态,禁止跨账号同名更新。 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().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().Where(Scope(actor)).Where(s => s.LoginName == request.LoginName).AnyAsync(ct)) throw new AppException("店铺不存在或无操作权限"); } /// 删除所属账号的店铺记录;不删除独立的续费记录。 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().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("店铺不存在或无删除权限"); } }