增加Shop API

This commit is contained in:
chfei committed 2026-10-29 11:34:35 +08:00
1 parent 3769193696
commit 5d017740c6
83 files changed
+4451 -696

No files matched your search

@@ -75,8 +75,19 @@ internal sealed class ShopRenewCommandHandler(UnitOfWorkExecutionContext executi
if (currentTime > expireBase)
expireBase = currentTime;
}
int newExpireTime = CalculateNewExpireTime(expireBase, request.RenewDuration);
DateTime newExpireDate;
try
{
newExpireDate = expireBase.AddMonths(request.RenewDuration);
}
catch (ArgumentOutOfRangeException)
{
throw new AppException("续费时长超出允许范围");
}
long timestamp = DateHelper.GetUnixTimestampInSeconds(newExpireDate);
if (timestamp > int.MaxValue || timestamp < 0)
throw new AppException("续费时间超出系统支持范围");
int newExpireTime = (int)timestamp;
int affected = await orm.Update<UserEntity>().Set(a => a.Points, newPoints)
.Where(a => a.Id == user.Id && a.Points == user.Points)
@@ -94,7 +105,7 @@ internal sealed class ShopRenewCommandHandler(UnitOfWorkExecutionContext executi
UserName = userName,
UserNick = user.NickName,
DeltaPoints = pointsPerMonth,
NewPoints = user.Points - (decimal)pointsPerMonth * (i + 1),
NewPoints = user.Points - pointsPerMonth * (i + 1),
ShopId = shop.ShopId,
ShopAccount = shop.ShopAccount,
Remark = remark,
@@ -127,21 +138,4 @@ internal sealed class ShopRenewCommandHandler(UnitOfWorkExecutionContext executi
cancellationToken.ThrowIfCancellationRequested();
return new ShopRenewResponse { ShopId = shopId, UserPoints = newPoints, ExpireTime‌ = newExpireTime };
}
private static int CalculateNewExpireTime(DateTime expireBase, int duration)
{
DateTime newExpireDate;
try
{
newExpireDate = expireBase.AddMonths(duration);
}
catch (ArgumentOutOfRangeException)
{
throw new AppException("续费时长超出允许范围");
}
long timestamp = DateHelper.GetUnixTimestampInSeconds(newExpireDate);
if (timestamp > int.MaxValue || timestamp < 0)
throw new AppException("续费时间超出系统支持范围");
return (int)timestamp;
}
}
@@ -12,7 +12,7 @@ using MAX.Framework.Conventions;
namespace Store.Users.Controllers;
/// <summary>用户模块接口;所有管理操作均以当前 JWT 对应的数据库账户进行授权。</summary>
/// <summary>用户模块接口;所有管理操作均以当前登录身份对应的数据库账户进行授权。</summary>
[ApiController]
[DefaultHttpPost]
[Authorize]
@@ -22,16 +22,12 @@ public class UserController(UserManagementService users, UserFundsService funds,
/// <summary>获取本人或授权范围内的用户信息,响应不包含密码。</summary>
[EndpointSummary("获取用户信息")]
public Task<UserInfo> GetUser(string? userName, CancellationToken cancellationToken)
{
return users.GetUser(userName, cancellationToken);
}
=> users.GetUser(userName, cancellationToken);
/// <summary>管理员查询全部用户;普通主账号仅查询本人及直属子账号。</summary>
[EndpointSummary("分页查询所有用户")]
public Task<UserPage<UserInfo>> GetAllUser(UserSearchRequest request, CancellationToken cancellationToken)
{
return users.Search(request, false, cancellationToken);
}
=> users.Search(request, false, cancellationToken);
/// <summary>管理员可查询所有账号的子账号,普通主账号只能查询自己的直属子账号。</summary>
/// <param name="request">管理员可通过 ParentUserName 筛选指定父账号;普通主账号仅可指定自己。</param>
@@ -39,37 +35,27 @@ public class UserController(UserManagementService users, UserFundsService funds,
[EndpointSummary("分页查询子账号")]
[EndpointDescription("管理员不传 ParentUserName 时查询全部子账号,传入时按指定父账号筛选;普通主账号只能查询自己的子账号。")]
public Task<UserPage<UserInfo>> GetSubUser(UserSearchRequest request, CancellationToken cancellationToken)
{
return users.Search(request, true, cancellationToken);
}
=> users.Search(request, true, cancellationToken);
/// <summary>管理员创建主账号。</summary>
[EndpointSummary("创建主账号")]
public Task<UserInfo> AddUser(UserCreateRequest request, CancellationToken cancellationToken)
{
return users.Create(request, false, cancellationToken);
}
=> users.Create(request, false, cancellationToken);
/// <summary>管理员可为指定主账号创建子账号,普通主账号仅可为自己创建子账号。</summary>
[EndpointSummary("创建直属子账号")]
public Task<UserInfo> AddSubUser(UserCreateRequest request, CancellationToken cancellationToken)
{
return users.Create(request, true, cancellationToken);
}
=> users.Create(request, true, cancellationToken);
/// <summary>管理员修改任意账户;普通主账号仅修改本人及直属子账号,用户名不可变更。</summary>
[EndpointSummary("修改用户资料")]
public Task<UserInfo> UpdateUser(UserUpdateRequest request, CancellationToken cancellationToken)
{
return users.Update(request, false, cancellationToken);
}
=> users.Update(request, false, cancellationToken);
/// <summary>管理员修改任意子账号,普通主账号仅修改直属子账号。</summary>
[EndpointSummary("修改直属子账号资料")]
public Task<UserInfo> UpdateSubUser(UserUpdateRequest request, CancellationToken cancellationToken)
{
return users.Update(request, true, cancellationToken);
}
=> users.Update(request, true, cancellationToken);
/// <summary>删除无资金和业务关联的账户。</summary>
[EndpointSummary("删除用户")]
@@ -90,37 +76,27 @@ public class UserController(UserManagementService users, UserFundsService funds,
/// <summary>查询授权账户的积分流水。</summary>
[EndpointSummary("查询用户积分流水")]
public Task<UserPage<UserPointsRecordEntity>> GetUserPoints(UserRecordSearchRequest request, CancellationToken cancellationToken)
{
return users.Points(request.UserName, request, cancellationToken);
}
=> users.Points(request.UserName, request, cancellationToken);
/// <summary>查询授权账户的余额流水。</summary>
[EndpointSummary("查询用户余额流水")]
public Task<UserPage<UserPaymentRecordEntity>> GetUserBalance(UserRecordSearchRequest request, CancellationToken cancellationToken)
{
return users.Balance(request.UserName, request, cancellationToken);
}
=> users.Balance(request.UserName, request, cancellationToken);
/// <summary>管理员充值积分,或主账号向直属子账号转移自己的积分。</summary>
[EndpointSummary("充值或转移用户积分")]
public Task<UserFundsResponse> UpdateUserPoints(UserFundsRequest request, CancellationToken cancellationToken)
{
return funds.Change(request, true, cancellationToken);
}
=> funds.Change(request, true, cancellationToken);
/// <summary>管理员充值或扣减余额,自动记录审计流水。</summary>
[EndpointSummary("充值或扣减用户余额")]
public Task<UserFundsResponse> UpdateUserBalance(UserFundsRequest request, CancellationToken cancellationToken)
{
return funds.Change(request, false, cancellationToken);
}
=> funds.Change(request, false, cancellationToken);
/// <summary>提交本人或管理范围内账户的充值备注。</summary>
[EndpointSummary("提交充值备注")]
public Task<long> AddUserTopUpMsg(UserTopUpMessageRequest request, CancellationToken cancellationToken)
{
return users.AddTopUpMessage(request, cancellationToken);
}
=> users.AddTopUpMessage(request, cancellationToken);
/// <summary>管理员可处理所有充值备注,普通主账号仅处理本人及直属子账号备注,不自动充值。</summary>
[EndpointSummary("处理充值备注")]
@@ -133,9 +109,7 @@ public class UserController(UserManagementService users, UserFundsService funds,
/// <summary>按管理员、主账号或子账号的授权范围分页查询充值备注。</summary>
[EndpointSummary("分页查询充值备注")]
public Task<UserPage<UserTopUpMessageEntity>> GetUserTopUpMsg(UserSearchRequest request, CancellationToken cancellationToken)
{
return users.TopUpMessages(request, cancellationToken);
}
=> users.TopUpMessages(request, cancellationToken);
/// <summary>管理员维护代理充值二维码。</summary>
[EndpointSummary("维护代理充值二维码")]
@@ -148,15 +122,10 @@ public class UserController(UserManagementService users, UserFundsService funds,
/// <summary>获取代理充值二维码。</summary>
[EndpointSummary("获取代理充值二维码")]
public Task<string?> GetTopUpQrcode(string agentCode, CancellationToken cancellationToken)
{
return users.GetTopUpQrcode(agentCode, cancellationToken);
}
=> users.GetTopUpQrcode(agentCode, cancellationToken);
/// <summary>管理员或主账号为授权账户的商铺续费,扣除店铺所属账户积分并记录实际操作者。</summary>
[EndpointSummary("使用积分为商铺续费")]
public Task<ShopRenewResponse> ShopRenew(ShopRenewRequest request, CancellationToken cancellationToken)
{
// 操作者身份、目标账户归属和消费额度统一在命令管道中检查。
return mediator.Send(new ShopRenewCommand(request), cancellationToken);
}
=> mediator.Send(new ShopRenewCommand(request), cancellationToken);
}
+2 -2
View File
@@ -1,6 +1,6 @@
# 用户模块
参考旧版 `D:/Works/KJD_WebService/KJD_WebService/BLL/WXXD/WXUserManage.cs`,沿用用户、积分、余额、充值备注等旧表。接口采用 JSON DTO 和现有 JWT 身份,不接受旧版拼接参数或客户端提供的操作者身份。
参考旧版 `D:/Works/KJD_WebService/KJD_WebService/BLL/WXXD/WXUserManage.cs`,沿用用户、积分、余额、充值备注等旧表。接口采用 JSON DTO 和现有 Bearer Token 身份,不接受旧版拼接参数或客户端提供的操作者身份。
## 文件与职责
@@ -54,4 +54,4 @@ dotnet run --project Store.API/Store.Api.csproj
连接现有 MySQL 前确认账户表用户名唯一、密码列可容纳 Identity 哈希、资金相关表支持事务。应用不自动创建或修改旧表。旧系统直接比较明文密码的登录接口不能识别新哈希,需随调用方迁移。
本次不修改或运行 Test 项目。编译和独立参数/权限边界验证不能替代真实 MySQL 的并发与表结构联调。旧文件中的 E57 令牌、销售助手 Cookie、淘宝买手不属于本模块范围。可以继续使用“代码审查”检查实现质量。
使用 `dotnet test Tests/Store.Users.Tests/Store.Users.Tests.csproj` 验证参数、权限、密码、资金和 HTTP 接口。SQLite 测试不能替代真实 MySQL 的并发与表结构联调。旧文件中的 E57 令牌、销售助手 Cookie、淘宝买手不属于本模块范围。可以继续使用“代码审查”检查实现质量。
@@ -5,13 +5,13 @@ namespace Store.Users.Repositories;
public interface IUserRepository : IBaseRepository<UserEntity>
{
Task<UserEntity> GetUser(string userName);
Task<UserEntity?> GetUser(string userName);
}
/// <summary>集中处理旧系统主子账号的有效计费配置和积分权限继承。</summary>
internal class UserRepository(IFreeSql freeSql) : BaseRepository<UserEntity>(freeSql), IUserRepository, ITransientDependency
{
public async Task<UserEntity> GetUser(string userName)
public async Task<UserEntity?> GetUser(string userName)
{
var user = await Where(u => u.UserName == userName).FirstAsync();
if (user == null || user.IsMainUser) return user;
@@ -11,9 +11,6 @@ internal static class UserAccessPolicy
public static bool Owns(UserEntity actor, UserEntity target) => actor.IsMainUser
&& string.Equals(target.ParentUserName, actor.UserName, StringComparison.OrdinalIgnoreCase);
public static bool CanRead(UserEntity actor, UserEntity target) =>
CanUseSelfService(actor, target);
/// <summary>子账号可查询本人、修改本人密码、提交充值备注和为本人店铺续费。</summary>
public static bool CanUseSelfService(UserEntity actor, UserEntity target) =>
actor.Id == target.Id || CanManage(actor, target);
@@ -30,7 +30,7 @@ public sealed partial class UserManagementService
var actor = await Actor(orm, ct);
var message = await orm.Select<UserTopUpMessageEntity>().Where(m => m.Id == request.Id).FirstAsync(ct)
?? throw new AppException("充值备注不存在");
if (!IsAdmin(actor))
if (!UserAccessPolicy.IsAdmin(actor))
{
var target = await orm.Select<UserEntity>().Where(u => u.UserName == message.UserName).FirstAsync(ct);
if (target == null || !UserAccessPolicy.CanManage(actor, target))
@@ -46,7 +46,7 @@ public sealed partial class UserManagementService
RequestValidator.Validate(request);
var actor = await Actor(orm, ct);
var select = orm.Select<UserTopUpMessageEntity>();
if (!IsAdmin(actor))
if (!UserAccessPolicy.IsAdmin(actor))
{
if (actor.IsMainUser)
select = select.Where(m => m.UserName == actor.UserName || orm.Select<UserEntity>()
@@ -63,7 +63,7 @@ public sealed partial class UserManagementService
{
RequestValidator.Validate(request);
var actor = await Actor(orm, ct);
if (!IsAdmin(actor)) throw new AppException("仅管理员可以维护充值二维码", 403);
if (!UserAccessPolicy.IsAdmin(actor)) throw new AppException("仅管理员可以维护充值二维码", 403);
await orm.InsertOrUpdate<UserTopUpQrcodeEntity>().SetSource(new UserTopUpQrcodeEntity
{
AgentCode = request.AgentCode.Trim(), QrcodeBase64 = request.QrcodeBase64
@@ -24,7 +24,6 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
?? throw new UnauthorizedAccessException("登录账户不存在");
}
private static bool IsAdmin(UserEntity actor) => UserAccessPolicy.IsAdmin(actor);
/// <summary>仅本人、直属主账号或超级管理员可以读取账户详情。</summary>
public async Task<UserInfo> GetUser(string? userName, CancellationToken ct)
{
@@ -33,7 +32,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
RequestValidator.Validate(new UserNameRequest { UserName = name });
var target = await orm.Select<UserEntity>().Where(u => u.UserName == name).FirstAsync(ct)
?? throw new AppException("用户不存在");
if (!UserAccessPolicy.CanRead(actor, target))
if (!UserAccessPolicy.CanUseSelfService(actor, target))
throw new AppException("无权访问该账户", 403);
return await query.GetUser(target.UserName);
}
@@ -43,12 +42,12 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
{
RequestValidator.Validate(request);
var actor = await Actor(orm, ct);
if (!IsAdmin(actor) && !actor.IsMainUser) throw new AppException("子账号不能管理账户列表", 403);
if (!UserAccessPolicy.IsAdmin(actor) && !actor.IsMainUser) throw new AppException("子账号不能管理账户列表", 403);
var select = orm.Select<UserEntity>();
if (subUsersOnly)
{
var parentUserName = request.ParentUserName?.Trim();
if (IsAdmin(actor))
if (UserAccessPolicy.IsAdmin(actor))
{
// 管理员省略父账号时查询所有子账号,但不混入主账号。
select = select.Where(u => u.ParentUserName != null && u.ParentUserName.Trim() != "");
@@ -63,7 +62,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
select = select.Where(u => u.ParentUserName == actor.UserName && (u.SaLevel == null || u.SaLevel != UserAdminLevel.SuperAdmin));
}
}
else if (!IsAdmin(actor))
else if (!UserAccessPolicy.IsAdmin(actor))
select = select.Where(u => u.Id == actor.Id || (u.ParentUserName == actor.UserName && (u.SaLevel == null || u.SaLevel != UserAdminLevel.SuperAdmin)));
if (!subUsersOnly && !string.IsNullOrWhiteSpace(request.ParentUserName))
{
@@ -79,7 +78,8 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
}
var total = await select.CountAsync(ct);
var items = await select.OrderByDescending(u => u.Id).Page(request.Page, request.PageSize).ToListAsync(ct);
return new UserPage<UserInfo>(total, items.Select(u => new UserMapper().ToInfo(u)).ToList());
var mapper = new UserMapper();
return new UserPage<UserInfo>(total, items.Select(mapper.ToInfo).ToList());
}
/// <summary>创建账户并初始化余额。主账号仅由管理员创建,子账号的归属由服务端指定。</summary>
@@ -94,12 +94,12 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
// 锁住主账号,使其创建子账号和删除操作串行化。
actor = await db.Select<UserEntity>().Where(u => u.Id == actor.Id).ForUpdate().FirstAsync(ct)
?? throw new AppException("操作者账户不存在");
if (subUser ? !IsAdmin(actor) && !actor.IsMainUser : !IsAdmin(actor)) throw new AppException("无权创建该账户", 403);
if (subUser ? !UserAccessPolicy.IsAdmin(actor) && !actor.IsMainUser : !UserAccessPolicy.IsAdmin(actor)) throw new AppException("无权创建该账户", 403);
var parentName = "";
if (subUser)
{
parentName = string.IsNullOrWhiteSpace(request.ParentUserName) ? actor.UserName : request.ParentUserName.Trim();
if (!IsAdmin(actor) && !string.Equals(parentName, actor.UserName, StringComparison.OrdinalIgnoreCase))
if (!UserAccessPolicy.IsAdmin(actor) && !string.Equals(parentName, actor.UserName, StringComparison.OrdinalIgnoreCase))
throw new AppException("只能在自己的账户下创建子账号", 403);
var parent = await db.Select<UserEntity>().Where(u => u.UserName == parentName).ForUpdate().FirstAsync(ct)
?? throw new AppException("父账号不存在");
@@ -186,7 +186,7 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
var name = string.IsNullOrWhiteSpace(request.UserName) ? actor.UserName : request.UserName.Trim();
var target = await orm.Select<UserEntity>().Where(u => u.UserName == name).FirstAsync(ct)
?? throw new AppException("用户不存在");
if (target.Id == actor.Id && !IsAdmin(actor))
if (target.Id == actor.Id && !UserAccessPolicy.IsAdmin(actor))
{
RequestValidator.Validate(new UserPasswordProofRequest { OldPassword = request.OldPassword! });
if (!UserPassword.Verify(target, request.OldPassword!)) throw new AppException("原密码错误");
@@ -242,18 +242,18 @@ public sealed partial class UserManagementService(IFreeSql orm, ICurrentUser cur
if (request.GroupId.HasValue && (subUser ? request.GroupId == UserGroup.MainAccount : request.GroupId != UserGroup.MainAccount))
throw new AppException("主账号必须使用主号分组,子账号不能使用主号分组");
// 普通主账号只能授予自己已有的权限,不能通过创建子账号提升权限。
if (!IsAdmin(actor) && request.Authority != null)
if (!UserAccessPolicy.IsAdmin(actor) && request.Authority != null)
{
var allowed = Split(actor.Authority);
if (Split(request.Authority).Any(a => !allowed.Contains(a))) throw new AppException("不能授予超出自身范围的权限", 403);
}
if (!IsAdmin(actor) && (request.OemId != null || request.ShopPointsPerMonth != null
if (!UserAccessPolicy.IsAdmin(actor) && (request.OemId != null || request.ShopPointsPerMonth != null
|| request.PostagePerOrder != null || request.RefundPostagePerOrder != null
|| request.GiftOemAmountPerOrder != null || request.SyncLogisticsPerOrder != null))
throw new AppException("仅管理员可以修改计费配置", 403);
if (request.SaLevel.HasValue)
{
if (!IsAdmin(actor)) throw new AppException("无权设置管理员级别", 403);
if (!UserAccessPolicy.IsAdmin(actor)) throw new AppException("无权设置管理员级别", 403);
user.SaLevel = request.SaLevel;
}
user.NickName = request.NickName?.Trim() ?? user.NickName;