feat: 财务管理

This commit is contained in:
tk
2025-06-26 17:35:19 +08:00
committed by nsnail
parent a202595687
commit 94d0b7028f
63 changed files with 2544 additions and 72 deletions

View File

@ -17,7 +17,6 @@
以什么结束
作业名称
作业状态
信息
倒序排序
全部数据
@ -45,6 +44,7 @@
字典内容导出
宕机
客户端IP
小于
小于等于
小学
@ -71,6 +71,8 @@
接口路径
插入种子数据
操作系统
支出
收入
数据范围
文档内容
文档内容导出
@ -99,6 +101,7 @@
用户代理
用户名
用户导出
用户钱包导出
电子邮箱
登录
@ -111,11 +114,14 @@
站内信导出
等于
等待发送
管理员充值
管理员扣费
管理模块
系统模块
绑定手机号码
结果非预期
群众
自助充值
自定义
范围
菜单
@ -128,16 +134,17 @@
请求方式
请求日志导出
调试
追踪
追踪标识
身份证
运行
追踪
追踪标识
通知
邮箱号
部门名称
部门导出
配置导出
重设密码
钱包交易导出
链接
错误
随机排序

View File

@ -5,6 +5,8 @@ XML注释文件不存在
中文姓名
事务已回滚
事务已提交
交易失败
交易金额不正确
人机校验请求不能为空
人机验证未通过
作业名称不能为空
@ -108,6 +110,7 @@ XML注释文件不存在
部门可见
部门名称不能为空
配置文件初始化完毕
钱包余额不足
键值不能为空
键名称不能为空
随机延时结束时间不正确

View File

@ -26,7 +26,7 @@
"Id": 373837957840901,
"Name": "sys/user",
"ParentId": 373837917724677,
"Path": "/sys/user",
"Path": "/power/user",
"Sort": 100,
"Title": "用户管理",
"Type": 1
@ -37,7 +37,7 @@
"Id": 373838018527237,
"Name": "sys/role",
"ParentId": 373837917724677,
"Path": "/sys/role",
"Path": "/power/role",
"Sort": 99,
"Title": "角色管理",
"Type": 1
@ -48,7 +48,7 @@
"Id": 373838045605893,
"Name": "sys/dept",
"ParentId": 373837917724677,
"Path": "/sys/dept",
"Path": "/power/dept",
"Sort": 98,
"Title": "部门管理",
"Type": 1
@ -59,18 +59,50 @@
"Id": 373838070898693,
"Name": "sys/menu",
"ParentId": 373837917724677,
"Path": "/sys/menu",
"Path": "/power/menu",
"Sort": 97,
"Title": "菜单管理",
"Type": 1
},
// ------------------------------ 财务管理 ------------------------------
{
"Icon": "el-icon-money",
"Id": 690906994118665,
"Name": "finance",
"Path": "/finance",
"Sort": 99,
"Title": "财务管理",
"Type": 1
},
{
"Component": "sys/wallet",
"Icon": "el-icon-wallet",
"Id": 690907673255942,
"Name": "sys/wallet",
"ParentId": 690906994118665,
"Path": "/finance/wallet",
"Sort": 100,
"Title": "钱包管理",
"Type": 1
},
{
"Component": "sys/trade",
"Icon": "el-icon-calendar",
"Id": 690907673255943,
"Name": "sys/trade",
"ParentId": 690906994118665,
"Path": "/finance/trade",
"Sort": 99,
"Title": "交易流水",
"Type": 1
},
// ------------------------------ 系统管理 ------------------------------
{
"Icon": "sc-icon-App",
"Id": 485278637670422,
"Name": "sys",
"Path": "/sys",
"Sort": 99,
"Sort": 98,
"Title": "系统管理",
"Type": 1
},
@ -80,7 +112,7 @@
"Id": 380415005847557,
"Name": "sys/config",
"ParentId": 485278637670422,
"Path": "/sys/config",
"Path": "/system/config",
"Sort": 100,
"Title": "系统设置",
"Type": 1
@ -91,7 +123,7 @@
"Id": 510067557638158,
"Name": "sys/job",
"ParentId": 485278637670422,
"Path": "/sys/job",
"Path": "/system/job",
"Sort": 99,
"Title": "计划作业",
"Type": 1
@ -102,7 +134,7 @@
"Id": 375315654221829,
"Name": "sys/dic",
"ParentId": 485278637670422,
"Path": "/sys/dic",
"Path": "/system/dic",
"Sort": 98,
"Title": "字典管理",
"Type": 1
@ -113,7 +145,7 @@
"Id": 482779610341392,
"Name": "sys/msg",
"ParentId": 485278637670422,
"Path": "/sys/msg",
"Path": "/system/msg",
"Sort": 97,
"Title": "消息管理",
"Type": 1,
@ -124,7 +156,7 @@
"Id": 397880678895621,
"Name": "sys/api",
"ParentId": 485278637670422,
"Path": "/sys/api",
"Path": "/system/api",
"Sort": 96,
"Title": "接口管理",
"Type": 1
@ -135,7 +167,7 @@
"Id": 374911555702789,
"Name": "sys/cache",
"ParentId": 485278637670422,
"Path": "/sys/cache",
"Path": "/system/cache",
"Sort": 95,
"Title": "缓存管理",
"Type": 1
@ -146,7 +178,7 @@
"Id": 616214756757512,
"Name": "archive",
"Path": "/archive",
"Sort": 98,
"Sort": 97,
"Title": "档案管理",
"Type": 1
},
@ -167,7 +199,7 @@
"Id": 374792687640581,
"Name": "log",
"Path": "/log",
"Sort": 97,
"Sort": 96,
"Title": "日志管理",
"Type": 1
},
@ -177,7 +209,7 @@
"Id": 485285246504976,
"Name": "sys/log/operation",
"ParentId": 374792687640581,
"Path": "/sys/log/operation",
"Path": "/log/operation",
"Sort": 100,
"Title": "操作日志",
"Type": 1,
@ -188,7 +220,7 @@
"Id": 485285246504970,
"Name": "sys/log/login",
"ParentId": 374792687640581,
"Path": "/sys/log/login",
"Path": "/log/login",
"Sort": 99,
"Title": "登录日志",
"Type": 1,
@ -199,7 +231,7 @@
"Id": 373838105399301,
"Name": "dev",
"Path": "/dev",
"Sort": 96,
"Sort": 95,
"Title": "开发管理",
"Type": 1
},

View File

@ -58,5 +58,25 @@
{
"ApiId": "api/sys/login.log/export",
"RoleId": 371729946431493,
},
{
"ApiId": "api/sys/user.wallet/paged.query",
"RoleId": 371729946431493,
},
{
"ApiId": "api/sys/user.wallet/get",
"RoleId": 371729946431493,
},
{
"ApiId": "api/sys/wallet.trade/paged.query",
"RoleId": 371729946431493,
},
{
"ApiId": "api/sys/wallet.trade/count.by",
"RoleId": 371729946431493,
},
{
"ApiId": "api/sys/wallet.trade/get",
"RoleId": 371729946431493,
}
]

View File

@ -6,5 +6,25 @@
{
"MenuId": 374967228141573,
"RoleId": 371729946431493
},
{
"MenuId": 690906994118665,
"RoleId": 371729946431493
},
{
"MenuId": 690907673255942,
"RoleId": 371729946431493
},
{
"MenuId": 374792687640581,
"RoleId": 371729946431493
},
{
"MenuId": 485285246504970,
"RoleId": 371729946431493
},
{
"MenuId": 690907673255943,
"RoleId": 371729946431493
}
]

View File

@ -0,0 +1,12 @@
[
{
"Id": 370942943322181,
"OwnerDeptId": 372119301627909,
"OwnerId": 370942943322181,
},
{
"Id": 560217289236492,
"OwnerDeptId": 372119301627909,
"OwnerId": 560217289236492,
}
]

View File

@ -3,7 +3,7 @@
"devDependencies": {
"cz-git": "^1.11.2",
"commitizen": "^4.3.1",
"prettier": "^3.5.3",
"prettier": "^3.6.1",
"standard-version": "^9.5.0"
},
"config": {

View File

@ -10,7 +10,7 @@ public abstract class ServiceBase<TLogger> : ServiceBase
/// </summary>
protected ServiceBase() //
{
Logger = App.GetService<ILogger<TLogger>>();
Logger = S<ILogger<TLogger>>();
}
/// <summary>
@ -29,7 +29,7 @@ public abstract class ServiceBase : IScoped, IService
/// </summary>
protected ServiceBase()
{
UserToken = App.GetService<ContextUserToken>();
UserToken = S<ContextUserToken>();
ServiceId = Guid.NewGuid();
}
@ -38,4 +38,19 @@ public abstract class ServiceBase : IScoped, IService
/// <inheritdoc />
public ContextUserToken UserToken { get; set; }
/// <summary>
/// 获取服务
/// </summary>
#pragma warning disable RCS1036
#pragma warning restore RCS1036
// ReSharper disable once MemberCanBeMadeStatic.Global
#pragma warning disable CA1822, S2325
protected T S<T>()
#pragma warning restore S2325, CA1822
where T : class
{
return App.GetService<T>();
}
}

View File

@ -42,8 +42,9 @@ public sealed class ExampleService(BasicRepository<Tpl_Example, long> rpo) //
.ToDictionaryAsync(a => a.Count())
.ConfigureAwait(false);
return ret.Select(x => new KeyValuePair<IImmutableDictionary<string, string>, int>(
req.RequiredFields.ToImmutableDictionary(y => y, y => typeof(Tpl_Example).GetProperty(y)!.GetValue(x.Key)!.ToString())
req.RequiredFields.ToImmutableDictionary(y => y, y => typeof(Tpl_Example).GetProperty(y)!.GetValue(x.Key)?.ToString())
, x.Value))
.Where(x => x.Key.Any(y => !y.Value.NullOrEmpty()))
.OrderByDescending(x => x.Value);
}

View File

@ -1,17 +1,17 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 有者字段接口
/// 有者字段接口
/// </summary>
public interface IFieldOwner
{
/// <summary>
/// 有者部门编号
/// 有者部门编号
/// </summary>
long? OwnerDeptId { get; init; }
/// <summary>
/// 有者用户编号
/// 有者用户编号
/// </summary>
long? OwnerId { get; init; }
}

View File

@ -40,7 +40,7 @@ public record Sys_DocCatalog : VersionEntity, IFieldOwner
public virtual string Name { get; init; }
/// <summary>
/// 有者
/// 有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
@ -48,7 +48,7 @@ public record Sys_DocCatalog : VersionEntity, IFieldOwner
public Sys_User Owner { get; init; }
/// <summary>
/// 有者部门编号
/// 有者部门编号
/// </summary>
[Column]
[CsvIgnore]
@ -56,7 +56,7 @@ public record Sys_DocCatalog : VersionEntity, IFieldOwner
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 有者用户编号
/// 有者用户编号
/// </summary>
[Column]
[CsvIgnore]

View File

@ -41,7 +41,7 @@ public record Sys_DocContent : VersionEntity, IFieldEnabled, IFieldOwner
public virtual bool Enabled { get; init; }
/// <summary>
/// 有者
/// 有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
@ -49,7 +49,7 @@ public record Sys_DocContent : VersionEntity, IFieldEnabled, IFieldOwner
public Sys_User Owner { get; init; }
/// <summary>
/// 有者部门编号
/// 有者部门编号
/// </summary>
[Column]
[CsvIgnore]
@ -57,7 +57,7 @@ public record Sys_DocContent : VersionEntity, IFieldEnabled, IFieldOwner
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 有者用户编号
/// 有者用户编号
/// </summary>
[Column]
[CsvIgnore]

View File

@ -35,7 +35,7 @@ public record Sys_JobRecord : LiteImmutableEntity
public int HttpStatusCode { get; init; }
/// <summary>
/// 有者信息
/// 有者信息
/// </summary>
[CsvIgnore]
[JsonIgnore]

View File

@ -66,7 +66,7 @@ public record Sys_LoginLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFiel
public virtual string LoginUserName { get; protected init; }
/// <summary>
/// 有者
/// 有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
@ -74,7 +74,7 @@ public record Sys_LoginLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFiel
public Sys_User Owner { get; init; }
/// <summary>
/// 有者部门编号
/// 有者部门编号
/// </summary>
[Column]
[CsvIgnore]
@ -82,7 +82,7 @@ public record Sys_LoginLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFiel
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 有者用户编号
/// 有者用户编号
/// </summary>
[Column]
[CsvIgnore]

View File

@ -76,7 +76,7 @@ public record Sys_RequestLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFi
public virtual int HttpStatusCode { get; init; }
/// <summary>
/// 有者
/// 有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
@ -84,7 +84,7 @@ public record Sys_RequestLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFi
public Sys_User Owner { get; init; }
/// <summary>
/// 有者部门编号
/// 有者部门编号
/// </summary>
[Column]
[CsvIgnore]
@ -92,7 +92,7 @@ public record Sys_RequestLog : SimpleEntity, IFieldCreatedTime, IFieldOwner, IFi
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 有者用户编号
/// 有者用户编号
/// </summary>
[Column]
[CsvIgnore]

View File

@ -0,0 +1,72 @@
namespace NetAdmin.Domain.DbMaps.Sys;
/// <summary>
/// 用户钱包表
/// </summary>
[Table(Name = Chars.FLG_DB_TABLE_NAME_PREFIX + nameof(Sys_UserWallet))]
public record Sys_UserWallet : LiteVersionEntity, IFieldOwner
{
/// <summary>
/// 可用余额
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long AvailableBalance { get; init; }
/// <summary>
/// 冻结余额
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long FrozenBalance { get; init; }
/// <summary>
/// 所有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
[Navigate(nameof(OwnerId))]
public Sys_User Owner { get; init; }
/// <summary>
/// 所有者部门编号
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 所有者用户编号
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long? OwnerId { get; init; }
/// <summary>
/// 总余额
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long TotalBalance { get; init; }
/// <summary>
/// 总支出
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long TotalExpenditure { get; init; }
/// <summary>
/// 总收入
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long TotalIncome { get; init; }
}

View File

@ -0,0 +1,80 @@
namespace NetAdmin.Domain.DbMaps.Sys;
/// <summary>
/// 钱包交易表
/// </summary>
[Table(Name = Chars.FLG_DB_TABLE_NAME_PREFIX + nameof(Sys_WalletTrade))]
public record Sys_WalletTrade : ImmutableEntity, IFieldOwner, IFieldSummary
{
/// <summary>
/// 交易金额
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long Amount { get; init; }
/// <summary>
/// 交易前余额
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long BalanceBefore { get; init; }
/// <summary>
/// 业务订单号
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long? BusinessOrderNumber { get; init; }
/// <summary>
/// 所有者
/// </summary>
[CsvIgnore]
[JsonIgnore]
[Navigate(nameof(OwnerId))]
public Sys_User Owner { get; init; }
/// <summary>
/// 所有者部门编号
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long? OwnerDeptId { get; init; }
/// <summary>
/// 所有者用户编号
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual long? OwnerId { get; init; }
/// <summary>
/// 备注
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_255)]
[CsvIgnore]
[JsonIgnore]
public virtual string Summary { get; init; }
/// <summary>
/// 交易方向
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual TradeDirections TradeDirection { get; init; }
/// <summary>
/// 交易类型
/// </summary>
[Column]
[CsvIgnore]
[JsonIgnore]
public virtual TradeTypes TradeType { get; init; }
}

View File

@ -0,0 +1,8 @@
using NetAdmin.Domain.DbMaps.Sys;
namespace NetAdmin.Domain.Dto.Sys.UserWallet;
/// <summary>
/// 请求:创建用户钱包
/// </summary>
public record CreateUserWalletReq : Sys_UserWallet;

View File

@ -0,0 +1,15 @@
namespace NetAdmin.Domain.Dto.Sys.UserWallet;
/// <summary>
/// 请求:编辑用户钱包
/// </summary>
public record EditUserWalletReq : CreateUserWalletReq
{
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <inheritdoc cref="IFieldVersion.Version" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Version { get; init; }
}

View File

@ -0,0 +1,21 @@
namespace NetAdmin.Domain.Dto.Sys.UserWallet;
/// <summary>
/// 请求:查询用户钱包
/// </summary>
public sealed record QueryUserWalletReq : Sys_UserWallet
{
/// <summary>
/// 部门编号
/// </summary>
public long DeptId { get; init; }
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <summary>
/// 角色编号
/// </summary>
public long RoleId { get; init; }
}

View File

@ -0,0 +1,56 @@
using NetAdmin.Domain.Dto.Sys.User;
namespace NetAdmin.Domain.Dto.Sys.UserWallet;
/// <summary>
/// 响应:查询用户钱包
/// </summary>
public record QueryUserWalletRsp : Sys_UserWallet
{
/// <inheritdoc cref="Sys_UserWallet.AvailableBalance" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long AvailableBalance { get; init; }
/// <inheritdoc cref="IFieldCreatedTime.CreatedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override DateTime CreatedTime { get; init; }
/// <inheritdoc cref="Sys_UserWallet.FrozenBalance" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long FrozenBalance { get; init; }
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <inheritdoc cref="IFieldModifiedTime.ModifiedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override DateTime? ModifiedTime { get; init; }
/// <inheritdoc cref="Sys_UserWallet.Owner" />
public new virtual QueryUserRsp Owner { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerDeptId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? OwnerDeptId { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? OwnerId { get; init; }
/// <inheritdoc cref="Sys_UserWallet.TotalBalance" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long TotalBalance { get; init; }
/// <inheritdoc cref="Sys_UserWallet.TotalExpenditure" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long TotalExpenditure { get; init; }
/// <inheritdoc cref="Sys_UserWallet.TotalIncome" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long TotalIncome { get; init; }
/// <inheritdoc cref="IFieldVersion.Version" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Version { get; init; }
}

View File

@ -0,0 +1,53 @@
namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary>
/// 请求:创建钱包交易
/// </summary>
public record CreateWalletTradeReq : Sys_WalletTrade, IValidatableObject
{
private readonly TradeTypes _tradeType;
/// <inheritdoc cref="Sys_WalletTrade.Amount" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Amount { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.BusinessOrderNumber" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? BusinessOrderNumber { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[Required]
[UserId]
[Range(1, long.MaxValue)]
public override long? OwnerId { get; init; }
/// <inheritdoc cref="IFieldSummary.Summary" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string Summary { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.TradeDirection" />
public override TradeDirections TradeDirection { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.TradeType" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[EnumDataType(typeof(TradeTypes))]
public override TradeTypes TradeType {
get => _tradeType;
init {
_tradeType = value;
TradeDirection = value.Attr<TradeAttribute>().Direction;
}
}
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var tradeDirection = TradeType.Attr<TradeAttribute>().Direction;
if (Amount == 0 || (tradeDirection == TradeDirections.Income && Amount < 0) || (tradeDirection == TradeDirections.Expense && Amount > 0)) {
yield return new ValidationResult(Ln., [nameof(Amount)]);
}
yield return ValidationResult.Success;
}
}

View File

@ -0,0 +1,11 @@
namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary>
/// 请求:编辑钱包交易
/// </summary>
public record EditWalletTradeReq : CreateWalletTradeReq
{
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
}

View File

@ -0,0 +1,13 @@
using NetAdmin.Domain.DbMaps.Sys;
namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary>
/// 请求:查询钱包交易
/// </summary>
public sealed record QueryWalletTradeReq : Sys_WalletTrade
{
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
}

View File

@ -0,0 +1,61 @@
using NetAdmin.Domain.Dto.Sys.User;
namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary>
/// 响应:查询钱包交易
/// </summary>
public record QueryWalletTradeRsp : Sys_WalletTrade
{
/// <inheritdoc cref="Sys_WalletTrade.Amount" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Amount { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.BalanceBefore" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long BalanceBefore { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.BusinessOrderNumber" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? BusinessOrderNumber { get; init; }
/// <inheritdoc cref="IFieldCreatedTime.CreatedTime" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override DateTime CreatedTime { get; init; }
/// <inheritdoc cref="IFieldCreatedUser.CreatedUserId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? CreatedUserId { get; init; }
/// <inheritdoc cref="IFieldCreatedUser.CreatedUserName" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string CreatedUserName { get; init; }
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.Owner" />
[CsvIgnore]
public new virtual QueryUserRsp Owner { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerDeptId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? OwnerDeptId { get; init; }
/// <inheritdoc cref="IFieldOwner.OwnerId" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override long? OwnerId { get; init; }
/// <inheritdoc cref="IFieldSummary.Summary" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string Summary { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.TradeDirection" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override TradeDirections TradeDirection { get; init; }
/// <inheritdoc cref="Sys_WalletTrade.TradeType" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override TradeTypes TradeType { get; init; }
}

View File

@ -0,0 +1,13 @@
namespace NetAdmin.Infrastructure.Attributes;
/// <summary>
/// 交易方向特性
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Enum)]
public sealed class TradeAttribute : Attribute
{
/// <summary>
/// 交易方向
/// </summary>
public TradeDirections Direction { get; init; }
}

View File

@ -0,0 +1,22 @@
namespace NetAdmin.Infrastructure.Enums;
/// <summary>
/// 交易方向
/// </summary>
[Export]
public enum TradeDirections
{
/// <summary>
/// 收入
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.收入))]
Income = 4
,
/// <summary>
/// 支出
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.支出))]
Expense = 5
}

View File

@ -0,0 +1,33 @@
namespace NetAdmin.Infrastructure.Enums;
/// <summary>
/// 交易类型
/// </summary>
[Export]
public enum TradeTypes
{
/// <summary>
/// 管理员充值
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.管理员充值))]
[Trade(Direction = TradeDirections.Income)]
AdminDeposit = 1
,
/// <summary>
/// 管理员扣费
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.管理员扣费))]
[Trade(Direction = TradeDirections.Expense)]
AdminDeduct = 2
,
/// <summary>
/// 自助充值
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.自助充值))]
[Trade(Direction = TradeDirections.Income)]
SelfDeposit = 3
}

View File

@ -0,0 +1,12 @@
using NetAdmin.Domain.Dto.Sys.UserWallet;
namespace NetAdmin.SysComponent.Application.Modules.Sys;
/// <summary>
/// 用户钱包模块
/// </summary>
public interface IUserWalletModule : ICrudModule<CreateUserWalletReq, QueryUserWalletRsp // 创建类型
, EditUserWalletReq // 编辑类型
, QueryUserWalletReq, QueryUserWalletRsp // 查询类型
, DelReq // 删除类型
>;

View File

@ -0,0 +1,12 @@
using NetAdmin.Domain.Dto.Sys.WalletTrade;
namespace NetAdmin.SysComponent.Application.Modules.Sys;
/// <summary>
/// 钱包交易模块
/// </summary>
public interface IWalletTradeModule : ICrudModule<CreateWalletTradeReq, QueryWalletTradeRsp // 创建类型
, EditWalletTradeReq // 编辑类型
, QueryWalletTradeReq, QueryWalletTradeRsp // 查询类型
, DelReq // 删除类型
>;

View File

@ -0,0 +1,6 @@
namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
/// <summary>
/// 用户钱包服务
/// </summary>
public interface IUserWalletService : IService, IUserWalletModule;

View File

@ -0,0 +1,6 @@
namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
/// <summary>
/// 钱包交易服务
/// </summary>
public interface IWalletTradeService : IService, IWalletTradeModule;

View File

@ -16,7 +16,7 @@ public sealed class ToolsService : ServiceBase<IToolsService>, IToolsService
public async Task<object[][]> ExecuteSqlAsync(ExecuteSqlReq req)
{
req.ThrowIfInvalid();
var cmd = App.GetService<IFreeSql>().Ado.CommandFluent(req.Sql).CommandTimeout(req.TimeoutSecs).ExecuteArrayAsync();
var cmd = S<IFreeSql>().Ado.CommandFluent(req.Sql).CommandTimeout(req.TimeoutSecs).ExecuteArrayAsync();
return req.WaitResult ? await cmd.ConfigureAwait(false) : null;
}

View File

@ -175,7 +175,7 @@ public sealed class UserProfileService(BasicRepository<Sys_UserProfile, long> rp
// 默认仪表版
if (req.AppConfig == "[]") {
req.AppConfig = BuildAppConfig(App.GetService<ContextUserInfo>().Roles.ToDictionary(x => x.Id, x => x.DashboardLayout));
req.AppConfig = BuildAppConfig(S<ContextUserInfo>().Roles.ToDictionary(x => x.Id, x => x.DashboardLayout));
}
return UpdateAsync(req, [nameof(req.AppConfig)], null, a => a.Id == UserToken.Id, null, true);

View File

@ -3,6 +3,7 @@ using NetAdmin.Domain.Contexts;
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.User;
using NetAdmin.Domain.Dto.Sys.UserProfile;
using NetAdmin.Domain.Dto.Sys.UserWallet;
using NetAdmin.Domain.Dto.Sys.VerifyCode;
using NetAdmin.Domain.Events.Sys;
using NetAdmin.Domain.Extensions;
@ -13,6 +14,7 @@ namespace NetAdmin.SysComponent.Application.Services.Sys;
public sealed class UserService(
BasicRepository<Sys_User, long> rpo //
, IUserProfileService userProfileService //
, IUserWalletService userWalletService //
, IVerifyCodeService verifyCodeService //
, IEventPublisher eventPublisher) //
: RepositoryService<Sys_User, long, IUserService>(rpo), IUserService
@ -112,6 +114,14 @@ public sealed class UserService(
, AppConfig = appConfig
})
.ConfigureAwait(false);
// 钱包表
_ = await userWalletService.CreateAsync(new CreateUserWalletReq() with //
{
Id = dbUser.Id, OwnerId = dbUser.Id, OwnerDeptId = dbUser.DeptId
})
.ConfigureAwait(false);
var userList = await QueryAsync(new QueryReq<QueryUserReq> { Filter = new QueryUserReq { Id = dbUser.Id } }).ConfigureAwait(false);
// 发布用户创建事件

View File

@ -0,0 +1,145 @@
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.UserWallet;
using NetAdmin.Domain.Extensions;
namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IUserWalletService" />
public sealed class UserWalletService(BasicRepository<Sys_UserWallet, long> rpo) //
: RepositoryService<Sys_UserWallet, long, IUserWalletService>(rpo), IUserWalletService
{
/// <inheritdoc />
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
req.ThrowIfInvalid();
var ret = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in req.Items) {
ret += await DeleteAsync(item).ConfigureAwait(false);
}
return ret;
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryUserWalletReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req).WithNoLockNoWait().CountAsync();
}
/// <inheritdoc />
public async Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryUserWalletReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req with { Order = Orders.None })
.WithNoLockNoWait()
.GroupBy(req.GetToListExp<Sys_UserWallet>())
.ToDictionaryAsync(a => a.Count())
.ConfigureAwait(false);
return ret.Select(x => new KeyValuePair<IImmutableDictionary<string, string>, int>(
req.RequiredFields.ToImmutableDictionary(
y => y, y => typeof(Sys_UserWallet).GetProperty(y)!.GetValue(x.Key)?.ToString()), x.Value))
.Where(x => x.Key.Any(y => !y.Value.NullOrEmpty()))
.OrderByDescending(x => x.Value);
}
/// <inheritdoc />
public async Task<QueryUserWalletRsp> CreateAsync(CreateUserWalletReq req)
{
req.ThrowIfInvalid();
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
return ret.Adapt<QueryUserWalletRsp>();
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
req.ThrowIfInvalid();
return Rpo.DeleteAsync(a => a.Id == req.Id);
}
/// <inheritdoc />
public async Task<QueryUserWalletRsp> EditAsync(EditUserWalletReq req)
{
req.ThrowIfInvalid();
#if DBTYPE_SQLSERVER
return (await UpdateReturnListAsync(req).ConfigureAwait(false)).FirstOrDefault()?.Adapt<QueryUserWalletRsp>();
#else
return await UpdateAsync(req).ConfigureAwait(false) > 0 ? await GetAsync(new QueryUserWalletReq { Id = req.Id }).ConfigureAwait(false) : null;
#endif
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryUserWalletReq> req)
{
req.ThrowIfInvalid();
return ExportAsync<QueryUserWalletReq, QueryUserWalletRsp>(QueryInternal, req, Ln.);
}
/// <inheritdoc />
public async Task<QueryUserWalletRsp> GetAsync(QueryUserWalletReq req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(new QueryReq<QueryUserWalletReq> { Filter = req, Order = Orders.None }).ToOneAsync().ConfigureAwait(false);
return ret.Adapt<QueryUserWalletRsp>();
}
/// <inheritdoc />
public async Task<PagedQueryRsp<QueryUserWalletRsp>> PagedQueryAsync(PagedQueryReq<QueryUserWalletReq> req)
{
req.ThrowIfInvalid();
var list = await QueryInternal(req)
.Include(a => a.Owner)
.Page(req.Page, req.PageSize)
.WithNoLockNoWait()
.Count(out var total)
.ToListAsync(req)
.ConfigureAwait(false);
return new PagedQueryRsp<QueryUserWalletRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryUserWalletRsp>>());
}
/// <inheritdoc />
public async Task<IEnumerable<QueryUserWalletRsp>> QueryAsync(QueryReq<QueryUserWalletReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req).WithNoLockNoWait().Take(req.Count).ToListAsync(req).ConfigureAwait(false);
return ret.Adapt<IEnumerable<QueryUserWalletRsp>>();
}
private ISelect<Sys_UserWallet> QueryInternal(QueryReq<QueryUserWalletReq> req)
{
IEnumerable<long> deptIds = null;
if (req.Filter?.DeptId > 0) {
deptIds = Rpo.Orm.Select<Sys_Dept>().Where(a => a.Id == req.Filter.DeptId).AsTreeCte().ToList(a => a.Id);
}
return QueryInternal(req, deptIds);
}
private ISelect<Sys_UserWallet> QueryInternal(QueryReq<QueryUserWalletReq> req, IEnumerable<long> deptIds)
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter)
.WhereIf(req.Filter?.Id > 0, a => a.Id == req.Filter.Id)
.WhereIf(deptIds != null, a => deptIds.Contains(a.Owner.DeptId))
.WhereIf( //
req.Filter?.RoleId > 0, a => a.Owner.Roles.Any(b => b.Id == req.Filter.RoleId));
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (req.Order) {
case Orders.None:
return ret;
case Orders.Random:
return ret.OrderByRandom();
}
ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id);
}
return ret;
}
}

View File

@ -0,0 +1,145 @@
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.UserWallet;
using NetAdmin.Domain.Dto.Sys.WalletTrade;
using NetAdmin.Domain.Extensions;
namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IWalletTradeService" />
public sealed class WalletTradeService(BasicRepository<Sys_WalletTrade, long> rpo) //
: RepositoryService<Sys_WalletTrade, long, IWalletTradeService>(rpo), IWalletTradeService
{
/// <inheritdoc />
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
req.ThrowIfInvalid();
var ret = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in req.Items) {
ret += await DeleteAsync(item).ConfigureAwait(false);
}
return ret;
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryWalletTradeReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req).WithNoLockNoWait().CountAsync();
}
/// <inheritdoc />
public async Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryWalletTradeReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req with { Order = Orders.None })
.WithNoLockNoWait()
.GroupBy(req.GetToListExp<Sys_WalletTrade>())
.ToDictionaryAsync(a => a.Count())
.ConfigureAwait(false);
return ret.Select(x => new KeyValuePair<IImmutableDictionary<string, string>, int>(
req.RequiredFields.ToImmutableDictionary(
y => y, y => typeof(Sys_WalletTrade).GetProperty(y)!.GetValue(x.Key)?.ToString()), x.Value))
.Where(x => x.Key.Any(y => !y.Value.NullOrEmpty()))
.OrderByDescending(x => x.Value);
}
/// <inheritdoc />
public async Task<QueryWalletTradeRsp> CreateAsync(CreateWalletTradeReq req)
{
req.ThrowIfInvalid();
var userWalletService = S<IUserWalletService>();
var wallet = await userWalletService.GetAsync(new QueryUserWalletReq { Id = req.OwnerId!.Value }).ConfigureAwait(false);
if (wallet.AvailableBalance + req.Amount < 0) {
throw new NetAdminInvalidOperationException(Ln.);
}
_ = await userWalletService.EditAsync(wallet.Adapt<EditUserWalletReq>() with {
AvailableBalance = wallet.AvailableBalance + req.Amount
, TotalBalance = wallet.TotalBalance + req.Amount
})
.ConfigureAwait(false) ?? throw new NetAdminUnexpectedException(Ln.);
var ret = await Rpo.InsertAsync(req with { BalanceBefore = wallet.AvailableBalance, OwnerDeptId = wallet.OwnerDeptId }).ConfigureAwait(false);
return ret.Adapt<QueryWalletTradeRsp>();
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
req.ThrowIfInvalid();
return Rpo.DeleteAsync(a => a.Id == req.Id);
}
/// <inheritdoc />
public async Task<QueryWalletTradeRsp> EditAsync(EditWalletTradeReq req)
{
req.ThrowIfInvalid();
#if DBTYPE_SQLSERVER
return (await UpdateReturnListAsync(req).ConfigureAwait(false)).FirstOrDefault()?.Adapt<QueryWalletTradeRsp>();
#else
return await UpdateAsync(req).ConfigureAwait(false) > 0
? await GetAsync(new QueryWalletTradeReq { Id = req.Id }).ConfigureAwait(false)
: null;
#endif
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryWalletTradeReq> req)
{
req.ThrowIfInvalid();
return ExportAsync<QueryWalletTradeReq, QueryWalletTradeRsp>(QueryInternal, req, Ln.);
}
/// <inheritdoc />
public async Task<QueryWalletTradeRsp> GetAsync(QueryWalletTradeReq req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(new QueryReq<QueryWalletTradeReq> { Filter = req, Order = Orders.None }).ToOneAsync().ConfigureAwait(false);
return ret.Adapt<QueryWalletTradeRsp>();
}
/// <inheritdoc />
public async Task<PagedQueryRsp<QueryWalletTradeRsp>> PagedQueryAsync(PagedQueryReq<QueryWalletTradeReq> req)
{
req.ThrowIfInvalid();
var list = await QueryInternal(req)
.Include(a => a.Owner)
.Page(req.Page, req.PageSize)
.WithNoLockNoWait()
.Count(out var total)
.ToListAsync(req)
.ConfigureAwait(false);
return new PagedQueryRsp<QueryWalletTradeRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryWalletTradeRsp>>());
}
/// <inheritdoc />
public async Task<IEnumerable<QueryWalletTradeRsp>> QueryAsync(QueryReq<QueryWalletTradeReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req).WithNoLockNoWait().Take(req.Count).ToListAsync(req).ConfigureAwait(false);
return ret.Adapt<IEnumerable<QueryWalletTradeRsp>>();
}
private ISelect<Sys_WalletTrade> QueryInternal(QueryReq<QueryWalletTradeReq> req)
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter);
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (req.Order) {
case Orders.None:
return ret;
case Orders.Random:
return ret.OrderByRandom();
}
ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id);
}
return ret;
}
}

View File

@ -0,0 +1,6 @@
namespace NetAdmin.SysComponent.Cache.Sys.Dependency;
/// <summary>
/// 用户钱包缓存
/// </summary>
public interface IUserWalletCache : ICache<IDistributedCache, IUserWalletService>, IUserWalletModule;

View File

@ -0,0 +1,6 @@
namespace NetAdmin.SysComponent.Cache.Sys.Dependency;
/// <summary>
/// 钱包交易缓存
/// </summary>
public interface IWalletTradeCache : ICache<IDistributedCache, IWalletTradeService>, IWalletTradeModule;

View File

@ -0,0 +1,68 @@
using NetAdmin.Domain.Dto.Sys.UserWallet;
namespace NetAdmin.SysComponent.Cache.Sys;
/// <inheritdoc cref="IUserWalletCache" />
public sealed class UserWalletCache(IDistributedCache cache, IUserWalletService service)
: DistributedCache<IUserWalletService>(cache, service), IScoped, IUserWalletCache
{
/// <inheritdoc />
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Service.BulkDeleteAsync(req);
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryUserWalletReq> req)
{
return Service.CountAsync(req);
}
/// <inheritdoc />
public Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryUserWalletReq> req)
{
return Service.CountByAsync(req);
}
/// <inheritdoc />
public Task<QueryUserWalletRsp> CreateAsync(CreateUserWalletReq req)
{
return Service.CreateAsync(req);
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
return Service.DeleteAsync(req);
}
/// <inheritdoc />
public Task<QueryUserWalletRsp> EditAsync(EditUserWalletReq req)
{
return Service.EditAsync(req);
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryUserWalletReq> req)
{
return Service.ExportAsync(req);
}
/// <inheritdoc />
public Task<QueryUserWalletRsp> GetAsync(QueryUserWalletReq req)
{
return Service.GetAsync(req);
}
/// <inheritdoc />
public Task<PagedQueryRsp<QueryUserWalletRsp>> PagedQueryAsync(PagedQueryReq<QueryUserWalletReq> req)
{
return Service.PagedQueryAsync(req);
}
/// <inheritdoc />
public Task<IEnumerable<QueryUserWalletRsp>> QueryAsync(QueryReq<QueryUserWalletReq> req)
{
return Service.QueryAsync(req);
}
}

View File

@ -0,0 +1,68 @@
using NetAdmin.Domain.Dto.Sys.WalletTrade;
namespace NetAdmin.SysComponent.Cache.Sys;
/// <inheritdoc cref="IWalletTradeCache" />
public sealed class WalletTradeCache(IDistributedCache cache, IWalletTradeService service)
: DistributedCache<IWalletTradeService>(cache, service), IScoped, IWalletTradeCache
{
/// <inheritdoc />
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Service.BulkDeleteAsync(req);
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryWalletTradeReq> req)
{
return Service.CountAsync(req);
}
/// <inheritdoc />
public Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryWalletTradeReq> req)
{
return Service.CountByAsync(req);
}
/// <inheritdoc />
public Task<QueryWalletTradeRsp> CreateAsync(CreateWalletTradeReq req)
{
return Service.CreateAsync(req);
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
return Service.DeleteAsync(req);
}
/// <inheritdoc />
public Task<QueryWalletTradeRsp> EditAsync(EditWalletTradeReq req)
{
return Service.EditAsync(req);
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryWalletTradeReq> req)
{
return Service.ExportAsync(req);
}
/// <inheritdoc />
public Task<QueryWalletTradeRsp> GetAsync(QueryWalletTradeReq req)
{
return Service.GetAsync(req);
}
/// <inheritdoc />
public Task<PagedQueryRsp<QueryWalletTradeRsp>> PagedQueryAsync(PagedQueryReq<QueryWalletTradeReq> req)
{
return Service.PagedQueryAsync(req);
}
/// <inheritdoc />
public Task<IEnumerable<QueryWalletTradeRsp>> QueryAsync(QueryReq<QueryWalletTradeReq> req)
{
return Service.QueryAsync(req);
}
}

View File

@ -0,0 +1,97 @@
using NetAdmin.Domain.Dto.Sys.UserWallet;
namespace NetAdmin.SysComponent.Host.Controllers.Sys;
/// <summary>
/// 用户钱包服务
/// </summary>
[ApiDescriptionSettings(nameof(Sys), Module = nameof(Sys))]
[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
public sealed class UserWalletController(IUserWalletCache cache) : ControllerBase<IUserWalletCache, IUserWalletService>(cache), IUserWalletModule
{
/// <summary>
/// 批量删除用户钱包
/// </summary>
[Transaction]
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Cache.BulkDeleteAsync(req);
}
/// <summary>
/// 用户钱包计数
/// </summary>
public Task<long> CountAsync(QueryReq<QueryUserWalletReq> req)
{
return Cache.CountAsync(req);
}
/// <summary>
/// 用户钱包分组计数
/// </summary>
public Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryUserWalletReq> req)
{
return Cache.CountByAsync(req);
}
/// <summary>
/// 创建用户钱包
/// </summary>
[Transaction]
public Task<QueryUserWalletRsp> CreateAsync(CreateUserWalletReq req)
{
return Cache.CreateAsync(req);
}
/// <summary>
/// 删除用户钱包
/// </summary>
[Transaction]
public Task<int> DeleteAsync(DelReq req)
{
return Cache.DeleteAsync(req);
}
/// <summary>
/// 编辑用户钱包
/// </summary>
[Transaction]
public Task<QueryUserWalletRsp> EditAsync(EditUserWalletReq req)
{
return Cache.EditAsync(req);
}
/// <summary>
/// 导出用户钱包
/// </summary>
[NonAction]
public Task<IActionResult> ExportAsync(QueryReq<QueryUserWalletReq> req)
{
return Cache.ExportAsync(req);
}
/// <summary>
/// 获取单个用户钱包
/// </summary>
public Task<QueryUserWalletRsp> GetAsync(QueryUserWalletReq req)
{
return Cache.GetAsync(req);
}
/// <summary>
/// 分页查询用户钱包
/// </summary>
public Task<PagedQueryRsp<QueryUserWalletRsp>> PagedQueryAsync(PagedQueryReq<QueryUserWalletReq> req)
{
return Cache.PagedQueryAsync(req);
}
/// <summary>
/// 查询用户钱包
/// </summary>
[NonAction]
public Task<IEnumerable<QueryUserWalletRsp>> QueryAsync(QueryReq<QueryUserWalletReq> req)
{
return Cache.QueryAsync(req);
}
}

View File

@ -0,0 +1,97 @@
using NetAdmin.Domain.Dto.Sys.WalletTrade;
namespace NetAdmin.SysComponent.Host.Controllers.Sys;
/// <summary>
/// 钱包交易服务
/// </summary>
[ApiDescriptionSettings(nameof(Sys), Module = nameof(Sys))]
[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
public sealed class WalletTradeController(IWalletTradeCache cache) : ControllerBase<IWalletTradeCache, IWalletTradeService>(cache), IWalletTradeModule
{
/// <summary>
/// 批量删除钱包交易
/// </summary>
[Transaction]
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Cache.BulkDeleteAsync(req);
}
/// <summary>
/// 钱包交易计数
/// </summary>
public Task<long> CountAsync(QueryReq<QueryWalletTradeReq> req)
{
return Cache.CountAsync(req);
}
/// <summary>
/// 钱包交易分组计数
/// </summary>
public Task<IOrderedEnumerable<KeyValuePair<IImmutableDictionary<string, string>, int>>> CountByAsync(QueryReq<QueryWalletTradeReq> req)
{
return Cache.CountByAsync(req);
}
/// <summary>
/// 创建钱包交易
/// </summary>
[Transaction]
public Task<QueryWalletTradeRsp> CreateAsync(CreateWalletTradeReq req)
{
return Cache.CreateAsync(req);
}
/// <summary>
/// 删除钱包交易
/// </summary>
[Transaction]
public Task<int> DeleteAsync(DelReq req)
{
return Cache.DeleteAsync(req);
}
/// <summary>
/// 编辑钱包交易
/// </summary>
[Transaction]
public Task<QueryWalletTradeRsp> EditAsync(EditWalletTradeReq req)
{
return Cache.EditAsync(req);
}
/// <summary>
/// 导出钱包交易
/// </summary>
[NonAction]
public Task<IActionResult> ExportAsync(QueryReq<QueryWalletTradeReq> req)
{
return Cache.ExportAsync(req);
}
/// <summary>
/// 获取单个钱包交易
/// </summary>
public Task<QueryWalletTradeRsp> GetAsync(QueryWalletTradeReq req)
{
return Cache.GetAsync(req);
}
/// <summary>
/// 分页查询钱包交易
/// </summary>
public Task<PagedQueryRsp<QueryWalletTradeRsp>> PagedQueryAsync(PagedQueryReq<QueryWalletTradeReq> req)
{
return Cache.PagedQueryAsync(req);
}
/// <summary>
/// 查询钱包交易
/// </summary>
[NonAction]
public Task<IEnumerable<QueryWalletTradeRsp>> QueryAsync(QueryReq<QueryWalletTradeReq> req)
{
return Cache.QueryAsync(req);
}
}

View File

@ -45,21 +45,21 @@ public sealed class OperationLogger : IEventSubscriber
// 插入登录日志
if (log.ApiPathCrc32 == Chars.FLG_PATH_API_SYS_USER_LOGIN_BY_PWD.Crc32()) {
_ = await App.GetService<ILoginLogCache>().CreateAsync(log.Adapt<CreateLoginLogReq>()).ConfigureAwait(false);
_ = await S<ILoginLogCache>().CreateAsync(log.Adapt<CreateLoginLogReq>()).ConfigureAwait(false);
}
}
// 如果首尾日期不一致,要分别插入不同的日期分表
if (inserts[0].CreatedTime.Date != inserts[^1].CreatedTime.Date) {
foreach (var dayInserts in inserts.GroupBy(x => x.CreatedTime.Date)) {
await App.GetService<IFreeSql>()
await S<IFreeSql>()
.Insert<Sys_RequestLog>(dayInserts.Select(x => x))
.ExecuteSqlBulkCopyAsync(tableName: $"{nameof(Sys_RequestLog)}_{dayInserts.Key:yyyyMMdd}")
.ConfigureAwait(false);
}
}
else {
await App.GetService<IFreeSql>()
await S<IFreeSql>()
.Insert<Sys_RequestLog>(inserts)
.ExecuteSqlBulkCopyAsync(tableName: $"{nameof(Sys_RequestLog)}_{inserts[0].CreatedTime:yyyyMMdd}")
.ConfigureAwait(false);

View File

@ -149,7 +149,7 @@ public sealed class SqlAuditor : ISingleton
}
/// <summary>
/// 设置有者
/// 设置有者
/// </summary>
private static void SetOwner(AuditValueEventArgs e, ContextUserInfo userInfo)
{

View File

@ -10,8 +10,8 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"ace-builds": "1.42.0",
"aieditor": "1.3.9",
"ace-builds": "1.43.0",
"aieditor": "1.4.0",
"axios": "1.10.0",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
@ -23,8 +23,8 @@
"nprogress": "0.2.0",
"sortablejs": "1.15.6",
"vkbeautify": "0.99.3",
"vue": "3.5.16",
"vue-i18n": "11.1.6",
"vue": "3.5.17",
"vue-i18n": "11.1.7",
"vue-router": "4.5.1",
"vue3-ace-editor": "2.2.4",
"vue3-json-viewer": "2.4.0",
@ -32,12 +32,12 @@
"vuex": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.2.4",
"prettier": "3.5.3",
"@vitejs/plugin-vue": "6.0.0",
"prettier": "3.6.1",
"prettier-plugin-organize-attributes": "1.0.0",
"sass": "1.89.2",
"terser": "5.43.0",
"vite": "6.3.5"
"terser": "5.43.1",
"vite": "7.0.0"
},
"browserslist": [
"> 1%",

View File

@ -0,0 +1,95 @@
/**
* 用户钱包服务
* @module @/api/sys/user.wallet
*/
import config from '@/config'
import http from '@/utils/request'
export default {
/**
* 批量删除用户钱包
*/
bulkDelete: {
url: `${config.API_URL}/api/sys/user.wallet/bulk.delete`,
name: `批量删除用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 用户钱包计数
*/
count: {
url: `${config.API_URL}/api/sys/user.wallet/count`,
name: `用户钱包计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 用户钱包分组计数
*/
countBy: {
url: `${config.API_URL}/api/sys/user.wallet/count.by`,
name: `用户钱包分组计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 创建用户钱包
*/
create: {
url: `${config.API_URL}/api/sys/user.wallet/create`,
name: `创建用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 删除用户钱包
*/
delete: {
url: `${config.API_URL}/api/sys/user.wallet/delete`,
name: `删除用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 编辑用户钱包
*/
edit: {
url: `${config.API_URL}/api/sys/user.wallet/edit`,
name: `编辑用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 获取单个用户钱包
*/
get: {
url: `${config.API_URL}/api/sys/user.wallet/get`,
name: `获取单个用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询用户钱包
*/
pagedQuery: {
url: `${config.API_URL}/api/sys/user.wallet/paged.query`,
name: `分页查询用户钱包`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
}

View File

@ -0,0 +1,95 @@
/**
* 钱包交易服务
* @module @/api/sys/wallet.trade
*/
import config from '@/config'
import http from '@/utils/request'
export default {
/**
* 批量删除钱包交易
*/
bulkDelete: {
url: `${config.API_URL}/api/sys/wallet.trade/bulk.delete`,
name: `批量删除钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 钱包交易计数
*/
count: {
url: `${config.API_URL}/api/sys/wallet.trade/count`,
name: `钱包交易计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 钱包交易分组计数
*/
countBy: {
url: `${config.API_URL}/api/sys/wallet.trade/count.by`,
name: `钱包交易分组计数`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 创建钱包交易
*/
create: {
url: `${config.API_URL}/api/sys/wallet.trade/create`,
name: `创建钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 删除钱包交易
*/
delete: {
url: `${config.API_URL}/api/sys/wallet.trade/delete`,
name: `删除钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 编辑钱包交易
*/
edit: {
url: `${config.API_URL}/api/sys/wallet.trade/edit`,
name: `编辑钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 获取单个钱包交易
*/
get: {
url: `${config.API_URL}/api/sys/wallet.trade/get`,
name: `获取单个钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询钱包交易
*/
pagedQuery: {
url: `${config.API_URL}/api/sys/wallet.trade/paged.query`,
name: `分页查询钱包交易`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
}

View File

@ -60,6 +60,9 @@ export default {
hasPermission: function (p) {
return this.permissions.includes('*/*/*') || this.permissions.some((a) => a === p)
},
hasApiPermission: function (p) {
return this.apiPermissions.includes('*/*/*') || this.apiPermissions.some((a) => a === p)
},
}
app.use(JsonViewer)

View File

@ -6,7 +6,7 @@
<el-button
@click="
() => {
this.$router.push({ path: '/sys/job', query: { view: 'fail' } })
this.$router.push({ path: '/system/job', query: { view: 'fail' } })
this.$emit('closed')
}
"
@ -22,7 +22,7 @@
<el-button
@click="
() => {
this.$router.push({ path: '/sys/job' })
this.$router.push({ path: '/system/job' })
this.$emit('closed')
}
"

View File

@ -624,6 +624,6 @@ export default {
链接: 'Link',
框架: 'IFrame',
按钮: 'Button',
倒序排序:'Sort-Descending',
顺序排序:'Sort-Ascending',
倒序排序: 'Sort-Descending',
顺序排序: 'Sort-Ascending',
}

View File

@ -622,6 +622,6 @@ export default {
链接: '链接',
框架: '框架',
按钮: '按钮',
倒序排序:'倒序排序',
顺序排序:'顺序排序',
倒序排序: '倒序排序',
顺序排序: '顺序排序',
}

View File

@ -51,5 +51,12 @@ export default {
global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0
? ['*/*/*']
: tool.recursiveFindProperty(preloads[0]?.data, 'type', 'button').map((x) => x.tag)
global.apiPermissions =
global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0
? ['*/*/*']
: preloads[1]?.data?.roles
?.map((x) => x.apiIds.join(','))
?.join(',')
.split(',')
},
}

View File

@ -1,6 +1,6 @@
<template>
<el-card :header="$t('登录日志')" shadow="never">
<login-log :keywords="$GLOBAL.user.id" :show-filter="false"></login-log>
<login-log :ownerId="$GLOBAL.user.id.toString()" :show-filter="false"></login-log>
</el-card>
</template>

View File

@ -74,10 +74,19 @@
<na-search
:controls="[
{
type: 'input',
field: ['root', 'keywords'],
placeholder: $t('日志编号 / 登录名 / 客户端IP'),
type: 'select-input',
field: [
'dy',
[
{ label: $t('日志编号'), key: 'id' },
{ label: $t('用户编号'), key: 'owner.id' },
{ label: $t('登录名'), key: 'loginUserName' },
{ label: $t('客户端IP'), key: 'createdClientIp' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
]"
:vue="this"
@ -150,7 +159,15 @@ export default {
naInfo,
},
computed: {},
created() {},
created() {
if (this.ownerId) {
this.query.dynamicFilter.filters.push({
field: 'owner.id',
operator: 'eq',
value: this.ownerId,
})
}
},
data() {
return {
statistics: {
@ -227,8 +244,11 @@ export default {
this.$refs.search.search()
},
onReset() {
if (!this.showFilter) return
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
if (this.showFilter) {
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
}
this.$refs.search.selectInputKey = 'id'
if (this.ownerId) this.$refs.search.selectInputKey = 'owner.id'
},
//搜索
async onSearch(form) {
@ -255,6 +275,13 @@ export default {
)
}
if (typeof form.dy.id === 'string' && form.dy.id.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy.id,
})
}
if (typeof form.dy.errorCode === 'string' && form.dy.errorCode.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'errorCode',
@ -279,6 +306,15 @@ export default {
})
}
if (typeof form.dy['owner.id'] === 'string' && form.dy['owner.id'].trim() !== '') {
this.$refs.search.selectInputKey = 'owner.id'
this.query.dynamicFilter.filters.push({
field: 'owner.id',
operator: 'eq',
value: form.dy['owner.id'],
})
}
await this.$refs.table.upData()
},
@ -295,6 +331,15 @@ export default {
},
},
async mounted() {
if (this.ownerId) {
this.$refs.search.selectInputKey = 'owner.id'
this.$refs.search.form.dy['owner.id'] = this.ownerId
this.$refs.search.keeps.push({
field: 'owner.id',
value: this.ownerId,
type: 'dy',
})
}
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
@ -303,8 +348,9 @@ export default {
type: 'root',
})
}
this.onReset()
},
props: { keywords: { type: String }, showFilter: { type: Boolean, default: true } },
props: { keywords: { type: String }, showFilter: { type: Boolean, default: true }, ownerId: { type: String } },
watch: {},
}
</script>

View File

@ -67,7 +67,7 @@
config: { props: { label: 'userName', value: 'id' } },
placeholder: '用户',
style: 'width:15rem',
condition: () => $GLOBAL.hasPermission('sys/log/operation/user'),
condition: () => $GLOBAL.hasApiPermission('api/sys/user/query'),
},
{
multiple: true,

View File

@ -0,0 +1,323 @@
<template>
<el-container>
<el-header v-loading="statistics.total === '...'" class="el-header-statistics">
<el-row :gutter="15">
<el-col :lg="24">
<el-card shadow="never">
<scStatistic :title="$t('总数')" :value="statistics.total" group-separator></scStatistic>
</el-card>
</el-col>
</el-row>
</el-header>
<el-header class="el-header-select-filter">
<scSelectFilter
:data="[
{
title: $t('交易方向'),
key: 'tradeDirection',
options: [
{ label: '全部', value: '' },
...Object.entries(this.$GLOBAL.enums.tradeDirections).map((x) => {
return {
value: x[0],
label: x[1][1],
badge: this.statistics.tradeDirection?.find((y) => y.key.tradeDirection.toLowerCase() === x[0].toLowerCase())
?.value,
}
}),
],
},
{
title: $t('交易类型'),
key: 'tradeType',
options: [
{ label: '全部', value: '' },
...Object.entries(this.$GLOBAL.enums.tradeTypes).map((x) => {
return {
value: x[0],
label: x[1][1],
badge: this.statistics.tradeType?.find((y) => y.key.tradeType.toLowerCase() === x[0].toLowerCase())?.value,
}
}),
],
},
]"
:label-width="15"
@on-change="filterChange"
ref="selectFilter"></scSelectFilter>
</el-header>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'select-input',
field: [
'dy',
[
{ label: $t('交易编号'), key: 'id' },
{ label: $t('用户名'), key: 'owner.userName' },
{ label: $t('用户编号'), key: 'ownerId' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
]"
:vue="this"
@reset="onReset"
@search="onSearch"
dateFormat="YYYY-MM-DD HH:mm:ss"
dateType="datetimerange"
dateValueFormat="YYYY-MM-DD HH:mm:ss"
ref="search" />
</div>
<div class="right-panel"></div>
</el-header>
<el-main class="nopadding">
<scTable
:context-menus="[
'id',
'ownerId',
'createdTime',
'tradeType',
'amount',
'balanceBefore',
'summary',
'owner.userName',
'tradeDirection',
]"
:context-multi="{ id: ['createdTime'], ownerId: ['owner.userName'] }"
:context-opers="['view']"
:default-sort="{ prop: 'id', order: 'descending' }"
:export-api="$API.sys_wallettrade.export"
:params="query"
:query-api="$API.sys_wallettrade.pagedQuery"
:vue="this"
@data-change="getStatistics"
@selection-change="
(items) => {
selection = items
}
"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<naColId :label="$t('交易编号')" prop="id" sortable="custom" width="170" />
<naColUser
:clickOpenDialog="$GLOBAL.hasApiPermission('api/sys/user/get')"
:label="$t('所属用户')"
header-align="center"
nestProp="owner.userName"
nestProp2="ownerId"
prop="ownerId"
sortable="custom"
width="170"></naColUser>
<naColIndicator
:label="$t('交易方向')"
:options="
Object.entries(this.$GLOBAL.enums.tradeDirections).map((x) => {
return { value: x[0], text: `${x[1][1]}`, type: x[1][2], pulse: x[1][3] === 'true' }
})
"
align="center"
prop="tradeDirection"
sortable="custom" />
<naColIndicator
:label="$t('交易类型')"
:options="
Object.entries(this.$GLOBAL.enums.tradeTypes).map((x) => {
return { value: x[0], text: `${x[1][1]}`, type: x[1][2], pulse: x[1][3] === 'true' }
})
"
align="center"
prop="tradeType"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.balanceBefore)"
:label="$t('交易前余额')"
align="right"
prop="balanceBefore"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.amount)"
:label="$t('发生金额')"
align="right"
prop="amount"
sortable="custom" />
<el-table-column :label="$t('交易后余额')" align="right">
<template #default="{ row }">
{{ $TOOL.groupSeparator(row.balanceBefore + row.amount) }}
</template>
</el-table-column>
<el-table-column :label="$t('备注')" min-width="100" prop="summary" show-overflow-tooltip sortable="custom" />
<naColOperation :buttons="[naColOperation.buttons[0]]" :vue="this" width="50" />
</scTable>
</el-main>
</el-container>
<save-dialog
v-if="dialog.save"
@closed="dialog.save = null"
@mounted="$refs.saveDialog.open(dialog.save)"
@success="(data, mode) => $refs.table.refresh()"
ref="saveDialog"></save-dialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
const naColUser = defineAsyncComponent(() => import('@/components/naColUser'))
const saveDialog = defineAsyncComponent(() => import('./save.vue'))
export default {
components: {
naColUser,
saveDialog,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
async created() {
if (this.ownerId) {
this.query.dynamicFilter.filters.push({ field: 'ownerId', operator: 'eq', value: this.ownerId })
}
},
data() {
return {
statistics: {
total: '...',
},
dialog: {},
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
}
},
inject: ['reload'],
methods: {
filterChange(data) {
Object.entries(data).forEach(([key, value]) => {
this.$refs.search.form.dy[key] = value === 'true' ? true : value === 'false' ? false : value
})
this.$refs.search.search()
},
async getStatistics() {
this.statistics.total = this.$refs.table?.total
const res = await Promise.all([
this.$API.sys_wallettrade.countBy.post({
dynamicFilter: {
filters: this.query.dynamicFilter.filters,
},
requiredFields: ['TradeDirection'],
}),
this.$API.sys_wallettrade.countBy.post({
dynamicFilter: {
filters: this.query.dynamicFilter.filters,
},
requiredFields: ['TradeType'],
}),
])
this.statistics.tradeDirection = res[0].data
this.statistics.tradeType = res[1].data
},
//重置
onReset() {
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = ['']))
if (this.ownerId) {
this.$refs.search.selectInputKey = 'ownerId'
}
},
//搜索
async onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime.map((x) => x.replace(/ 00:00:00$/, '')),
})
}
if (typeof form.dy['owner.userName'] === 'string' && form.dy['owner.userName'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.userName',
operator: 'eq',
value: form.dy['owner.userName'],
})
}
if (typeof form.dy['ownerId'] === 'string' && form.dy['ownerId'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'ownerId',
operator: 'eq',
value: form.dy['ownerId'],
})
}
if (typeof form.dy['id'] === 'string' && form.dy['id'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy['id'],
})
}
if (typeof form.dy['tradeType'] === 'string' && form.dy['tradeType'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'tradeType',
operator: 'eq',
value: form.dy['tradeType'],
})
}
if (typeof form.dy['tradeDirection'] === 'string' && form.dy['tradeDirection'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'tradeDirection',
operator: 'eq',
value: form.dy['tradeDirection'],
})
}
await this.$refs.table.upData()
},
},
async mounted() {
if (this.ownerId) {
this.$refs.search.selectInputKey = 'ownerId'
this.$refs.search.form.dy.ownerId = this.ownerId
this.$refs.search.keeps.push({
field: 'ownerId',
value: this.ownerId,
type: 'dy',
})
}
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
field: 'keywords',
value: this.keywords,
type: 'root',
})
}
this.onReset()
},
props: ['keywords', 'ownerId'],
watch: {},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,119 @@
<template>
<scDialog v-model="visible" :title="`${titleMap[mode]}${form?.id ?? '...'}`" @closed="$emit('closed')" destroy-on-close full-screen>
<div v-loading="loading">
<el-tabs v-model="tabId" @tab-change="tabChange" tab-position="top">
<el-tab-pane :label="$t('基本信息')" name="basic">
<el-form :disabled="mode === 'view'" :model="form" :rules="rules" label-width="15rem" ref="dialogForm">
<el-form-item :label="$t('唯一编码')" prop="id">
<el-input v-model="form.id" clearable />
</el-form-item>
<el-form-item :label="$t('交易方向')" prop="tradeDirection">
<el-input v-model="form.tradeDirection" clearable />
</el-form-item>
<el-form-item :label="$t('交易类型')" prop="tradeType">
<el-input v-model="form.tradeType" clearable />
</el-form-item>
<el-form-item :label="$t('交易前余额')" prop="balanceBefore">
<el-input v-model="form.balanceBefore" clearable />
</el-form-item>
<el-form-item :label="$t('交易金额')" prop="amount">
<el-input v-model="form.amount" clearable />
</el-form-item>
<el-form-item :label="$t('交易后余额')">
<el-input :value="form.balanceBefore + form.amount" clearable />
</el-form-item>
<el-form-item :label="$t('所有者部门编号')" prop="ownerDeptId">
<el-input v-model="form.ownerDeptId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户编号')" prop="ownerId">
<el-input v-model="form.ownerId" clearable />
</el-form-item>
<el-form-item :label="$t('创建者用户编号')" prop="createdUserId">
<el-input v-model="form.createdUserId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户名')" prop="createdUserName">
<el-input v-model="form.createdUserName" clearable />
</el-form-item>
<el-form-item :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<JsonViewer
:expand-depth="5"
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'dark' : 'light'"
:value="form"
copyable
expanded
sort></JsonViewer>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
export default {
components: {},
data() {
return {
//表单数据
form: {},
loading: true,
mode: 'add',
//验证规则
rules: {},
tabId: 'basic',
titleMap: {
add: this.$t('新增交易'),
edit: this.$t('编辑交易'),
view: this.$t('查看交易'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.visible = true
if (data.mode === 'add') {
this.loading = false
return this
}
this.loading = true
this.mode = data.mode
if (data.row?.id) {
const res = await this.$API.sys_wallettrade.get.post({ id: data.row.id })
if (res.data) {
Object.assign(this.form, res.data)
this.loading = false
return this
}
}
this.$message.error(`未找到该数据`)
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
//
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>

View File

@ -116,8 +116,8 @@
<naColAvatar :label="$t('用户名')" prop="userName" width="170" />
<el-table-column :label="$t('手机号 / 邮箱')" align="right" prop="mobile" sortable="custom" width="250">
<template #default="{ row }">
<p>{{ row.mobile }}</p>
<p>{{ row.email }}</p>
<p>{{ row.mobile ?? '-' }}</p>
<p>{{ row.email ?? '-' }}</p>
</template>
</el-table-column>
<naColTags

View File

@ -7,7 +7,7 @@
destroy-on-close
full-screen>
<el-form
:disabled="mode === 'view' && tabId !== 'log'"
:disabled="mode === 'view' && tabId !== 'log' && tabId !== 'wallet' && tabId !== 'trade'"
:model="form"
:rules="rules"
label-position="right"
@ -234,6 +234,12 @@
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('钱包信息')" name="wallet">
<wallet v-if="tabId === 'wallet'" :id="form.id.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('交易流水')" name="trade">
<trade v-if="tabId === 'trade'" :ownerId="form.id.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('操作日志')" name="log">
<log v-if="tabId === 'log'" :owner-id="form.id"></log>
</el-tab-pane>
@ -259,12 +265,14 @@
import { defineAsyncComponent } from 'vue'
const log = defineAsyncComponent(() => import('@/views/sys/log/operation'))
const trade = defineAsyncComponent(() => import('@/views/sys/trade'))
const wallet = defineAsyncComponent(() => import('@/views/sys/wallet'))
const naArea = defineAsyncComponent(() => import('@/components/naArea'))
const naDept = defineAsyncComponent(() => import('@/components/naDept'))
const scUpload = defineAsyncComponent(() => import('@/components/scUpload'))
const scSelect = defineAsyncComponent(() => import('@/components/scSelect'))
export default {
components: { log, naArea, naDept, scUpload, scSelect },
components: { log, naArea, naDept, scUpload, scSelect, trade, wallet },
data() {
return {
//表单数据

View File

@ -0,0 +1,315 @@
<template>
<el-container>
<el-header v-loading="statistics.total === '...'" class="el-header-statistics">
<el-row :gutter="15">
<el-col :lg="24">
<el-card shadow="never">
<scStatistic :title="$t('总数')" :value="statistics.total" group-separator></scStatistic>
</el-card>
</el-col>
</el-row>
</el-header>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'select-input',
field: [
'dy',
[
{ label: $t('用户编号'), key: 'id' },
{ label: $t('用户名'), key: 'owner.userName' },
{ label: $t('电子邮箱'), key: 'owner.email' },
{ label: $t('手机号'), key: 'owner.mobile' },
],
],
placeholder: $t('匹配内容'),
style: 'width:25rem',
selectStyle: 'width:8rem',
},
{
type: 'remote-select',
field: ['filter', 'deptId'],
api: $API.sys_dept.query,
config: { props: { label: 'name', value: 'id' } },
placeholder: $t('所属部门'),
style: 'width:15rem',
condition: () => $GLOBAL.hasApiPermission('api/sys/dept/query'),
},
{
type: 'remote-select',
field: ['filter', 'roleId'],
api: $API.sys_role.query,
config: { props: { label: 'name', value: 'id' } },
placeholder: $t('所属角色'),
style: 'width:15rem',
condition: () => $GLOBAL.hasApiPermission('api/sys/dept/query'),
},
]"
:vue="this"
@reset="onReset"
@search="onSearch"
dateFormat="YYYY-MM-DD HH:mm:ss"
dateType="datetimerange"
dateValueFormat="YYYY-MM-DD HH:mm:ss"
ref="search" />
</div>
<div class="right-panel"></div>
</el-header>
<el-main class="nopadding">
<scTable
:context-menus="[
'id',
'ownerId',
'owner.userName',
'createdTime',
'totalBalance',
'availableBalance',
'frozenBalance',
'totalIncome',
'totalExpenditure',
'modifiedTime',
]"
:context-multi="{ id: ['createdTime'], ownerId: ['owner.userName'] }"
:context-opers="['view']"
:default-sort="{ prop: 'id', order: 'descending' }"
:export-api="$API.sys_userwallet.export"
:params="query"
:query-api="$API.sys_userwallet.pagedQuery"
:vue="this"
@data-change="getStatistics"
@selection-change="
(items) => {
selection = items
}
"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<naColId :label="$t('钱包编号')" prop="id" sortable="custom" width="170" />
<naColUser
:clickOpenDialog="$GLOBAL.hasApiPermission('api/sys/user/get')"
:label="$t('所属用户')"
nestProp="owner.userName"
nestProp2="ownerId"
prop="ownerId"
sortable="custom"
width="170"></naColUser>
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalBalance)"
:label="$t('总余额')"
align="right"
prop="totalBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.availableBalance)"
:label="$t('可用余额')"
align="right"
prop="availableBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.frozenBalance)"
:label="$t('冻结余额')"
align="right"
prop="frozenBalance"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalIncome)"
:label="$t('总收入')"
align="right"
prop="totalIncome"
sortable="custom" />
<el-table-column
:formatter="(row) => $TOOL.groupSeparator(row.totalExpenditure)"
:label="$t('总支出')"
align="right"
prop="totalExpenditure"
sortable="custom" />
<el-table-column v-tim :label="$t('最后交易时间')" align="right" prop="modifiedTime" sortable="custom" width="150">
<template #default="{ row }">
<span v-if="row.modifiedTime" v-time.tip="row.modifiedTime" :title="row.modifiedTime"></span>
</template>
</el-table-column>
<naColOperation
:buttons="[
naColOperation.buttons[0],
{
icon: 'el-icon-plus',
title: $t('新建交易'),
click: async (row, vue) => {
vue.dialog.trade = { row }
},
condition: () => {
return $GLOBAL.hasApiPermission('api/sys/wallet.trade/create')
},
},
]"
:vue="this"
width="120" />
</scTable>
</el-main>
</el-container>
<trade-dialog
v-if="dialog.trade"
@closed="dialog.trade = null"
@mounted="$refs.tradeDialog.open(dialog.trade)"
@success="(data, mode) => $refs.table.refresh()"
ref="tradeDialog"></trade-dialog>
<save-dialog
v-if="dialog.save"
@closed="dialog.save = null"
@mounted="$refs.saveDialog.open(dialog.save)"
@success="(data, mode) => $refs.table.refresh()"
ref="saveDialog"></save-dialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
const tradeDialog = defineAsyncComponent(() => import('./trade.vue'))
const saveDialog = defineAsyncComponent(() => import('./save.vue'))
const naColUser = defineAsyncComponent(() => import('@/components/naColUser'))
export default {
components: {
tradeDialog,
saveDialog,
naColUser,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
async created() {
if (this.roleId) {
this.query.filter.roleId = this.roleId
}
if (this.deptId) {
this.query.filter.deptId = this.deptId
}
if (this.id) {
this.query.dynamicFilter.filters.push({ field: 'id', operator: 'eq', value: this.id })
}
},
data() {
return {
statistics: {
total: '...',
},
dialog: {},
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
}
},
inject: ['reload'],
methods: {
async getStatistics() {
this.statistics.total = this.$refs.table?.total
},
//重置
onReset() {
if (this.id) {
this.$refs.search.selectInputKey = 'id'
}
},
//搜索
async onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime.map((x) => x.replace(/ 00:00:00$/, '')),
})
}
if (typeof form.dy['owner.userName'] === 'string' && form.dy['owner.userName'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.userName',
operator: 'eq',
value: form.dy['owner.userName'],
})
}
if (typeof form.dy['owner.email'] === 'string' && form.dy['owner.email'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.email',
operator: 'eq',
value: form.dy['owner.email'],
})
}
if (typeof form.dy['owner.mobile'] === 'string' && form.dy['owner.mobile'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'owner.mobile',
operator: 'eq',
value: form.dy['owner.mobile'],
})
}
if (typeof form.dy['id'] === 'string' && form.dy['id'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy['id'],
})
}
await this.$refs.table.upData()
},
},
async mounted() {
if (this.keywords) {
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
field: 'keywords',
value: this.keywords,
type: 'root',
})
}
if (this.id) {
this.$refs.search.selectInputKey = 'id'
this.$refs.search.form.dy.id = this.id
this.$refs.search.keeps.push({
field: 'id',
value: this.id,
type: 'dy',
})
}
if (this.roleId) {
this.$refs.search.form.filter.roleId = this.roleId
this.$refs.search.keeps.push({
field: 'roleId',
value: this.roleId,
type: 'filter',
})
}
if (this.deptId) {
this.$refs.search.form.filter.deptId = this.deptId
this.$refs.search.keeps.push({
field: 'deptId',
value: this.deptId,
type: 'filter',
})
}
this.onReset()
},
props: ['keywords', 'roleId', 'deptId', 'id'],
watch: {},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,124 @@
<template>
<scDialog v-model="visible" :title="`${titleMap[mode]}${form?.id ?? '...'}`" @closed="$emit('closed')" destroy-on-close full-screen>
<div v-loading="loading">
<el-tabs v-model="tabId" @tab-change="tabChange" tab-position="top">
<el-tab-pane :label="$t('基本信息')" name="basic">
<el-form :disabled="mode === 'view'" :model="form" :rules="rules" label-width="15rem" ref="dialogForm">
<el-form-item :label="$t('唯一编码')" prop="id">
<el-input v-model="form.id" clearable />
</el-form-item>
<el-form-item :label="$t('总余额')" prop="totalBalance">
<el-input v-model="form.totalBalance" clearable />
</el-form-item>
<el-form-item :label="$t('可用余额')" prop="availableBalance">
<el-input v-model="form.availableBalance" clearable />
</el-form-item>
<el-form-item :label="$t('冻结余额')" prop="frozenBalance">
<el-input v-model="form.frozenBalance" clearable />
</el-form-item>
<el-form-item :label="$t('总收入')" prop="totalIncome">
<el-input v-model="form.totalIncome" clearable />
</el-form-item>
<el-form-item :label="$t('总支出')" prop="totalExpenditure">
<el-input v-model="form.totalExpenditure" clearable />
</el-form-item>
<el-form-item :label="$t('所有者部门编号')" prop="ownerDeptId">
<el-input v-model="form.ownerDeptId" clearable />
</el-form-item>
<el-form-item :label="$t('所有者用户编号')" prop="ownerId">
<el-input v-model="form.ownerId" clearable />
</el-form-item>
<el-form-item :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable />
</el-form-item>
<el-form-item :label="$t('修改时间')" prop="modifiedTime">
<el-input v-model="form.modifiedTime" clearable />
</el-form-item>
<el-form-item :label="$t('数据版本')" prop="version">
<el-input v-model="form.version" :disabled="mode === 'edit'" clearable />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('交易流水')" name="trade">
<trade v-if="tabId === 'trade'" :ownerId="form.ownerId.toString()" />
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<JsonViewer
:expand-depth="5"
:theme="this.$TOOL.data.get('APP_SET_DARK') || this.$CONFIG.APP_SET_DARK ? 'dark' : 'light'"
:value="form"
copyable
expanded
sort></JsonViewer>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const trade = defineAsyncComponent(() => import('@/views/sys/trade'))
export default {
components: { trade },
data() {
return {
//表单数据
form: {},
loading: true,
mode: 'add',
//验证规则
rules: {},
tabId: 'basic',
titleMap: {
add: this.$t('新增钱包'),
edit: this.$t('编辑钱包'),
view: this.$t('查看钱包'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.visible = true
if (data.mode === 'add') {
this.loading = false
return this
}
this.loading = true
this.mode = data.mode
if (data.row?.id) {
const res = await this.$API.sys_userwallet.get.post({ id: data.row.id })
if (res.data) {
Object.assign(this.form, res.data)
this.loading = false
return this
}
}
this.$message.error(`未找到该数据`)
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
//
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,91 @@
<template>
<scDialog v-model="visible" :title="$t('新建交易')" @closed="$emit('closed')" append-to-body destroy-on-close>
<el-form :model="form" :rules="rules" label-position="right" label-width="12rem" ref="dialogForm" style="height: 100%">
<el-form-item>
<el-descriptions border column="1">
<el-descriptions-item :label="$t('用户名')">
<b>{{ row.owner.userName }}</b>
</el-descriptions-item>
<el-descriptions-item :label="$t('用户编号')">{{ row.owner.id }}</el-descriptions-item>
<el-descriptions-item :label="$t('可用余额')">{{ $TOOL.groupSeparator(row.availableBalance) }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
<el-form-item :label="$t('交易类型')" prop="tradeType">
<el-select v-model="form.tradeType" clearable filterable>
<el-option v-for="(item, i) in $GLOBAL.enums.tradeTypes" :key="i" :label="item[1]" :value="i" />
</el-select>
</el-form-item>
<el-form-item :label="$t('交易金额')" prop="amount">
<el-input-number v-model="form.amount" :max="999999999" :min="-999999999" precision="0" style="width: 15rem"></el-input-number>
</el-form-item>
<el-form-item :label="$t('备注')" prop="summary">
<el-input v-model="form.summary" rows="3" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">{{ $t('取消') }}</el-button>
<el-button v-if="mode !== 'view'" :disabled="loading" :loading="loading" @click="submit" type="primary">{{ $t('保存') }}</el-button>
</template>
</scDialog>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {},
data() {
return {
//表单数据
form: {},
row: {},
loading: true,
//验证规则
rules: {
tradeType: [{ required: true, message: this.$t('请选择交易类型') }],
amount: [{ required: true, message: this.$t('请输入交易金额') }],
},
tabId: '0',
titleMap: {
add: this.$t('新增用户'),
edit: this.$t('编辑用户'),
view: this.$t('查看用户'),
},
visible: false,
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.row = data.row
this.form.ownerId = data.row.id
this.visible = true
this.loading = false
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
this.loading = true
try {
const res = await this.$API.sys_wallettrade.create.post(this.form)
this.$emit('success', res.data, this.mode)
this.visible = false
this.$message.success(this.$t('操作成功'))
} catch {}
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped></style>