feat: 框架代码同步

This commit is contained in:
tk
2025-07-19 09:59:44 +08:00
committed by nsnail
parent 4f6d465602
commit e99cb2aff9
40 changed files with 437 additions and 265 deletions

View File

@ -68,6 +68,7 @@ XML注释文件不存在
消息内容不能为空 消息内容不能为空
父节点不存在 父节点不存在
用户不存在 用户不存在
用户名不符合要求
用户名不能为空 用户名不能为空
用户名不能是手机号码 用户名不能是手机号码
用户名或密码错误 用户名或密码错误

View File

@ -23,7 +23,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.13.0.120203"> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.14.0.120626">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -15,7 +15,8 @@ public static class ISelectExtensions
where TQuery : DataAbstraction, new() where TQuery : DataAbstraction, new()
{ {
if (req.IgnoreOwner) { if (req.IgnoreOwner) {
me = me.DisableGlobalFilter(Chars.FLG_FREE_SQL_GLOBAL_FILTER_DATA); me = me.DisableGlobalFilter(Chars.FLG_FREE_SQL_GLOBAL_FILTER_SELF, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT
, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD);
} }
return me; return me;

View File

@ -62,6 +62,23 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false); return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false);
} }
/// <summary>
/// 唯一索引冲突处理
/// </summary>
protected static async Task OnUniqueIndexConflictAsync(Func<Task> actionTry, Func<Task> actionCatch = null)
{
try {
await actionTry().ConfigureAwait(false);
}
catch (Exception ex) when (ex.Message.Contains(Chars.FLG_DB_EXCEPTION_PRIMARY_KEY_CONFLICT) ||
ex.Message.Contains(Chars.FLG_DB_EXCEPTION_UNIQUE_CONSTRAINT_CONFLICT) ||
ex.Message.Contains(Chars.FLG_DB_EXCEPTION_IDX)) {
if (actionCatch != null) {
await actionCatch().ConfigureAwait(false);
}
}
}
/// <summary> /// <summary>
/// 更新实体 /// 更新实体
/// </summary> /// </summary>
@ -85,7 +102,8 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
whereExp ??= a => a.Id.Equals(newValue.Id); whereExp ??= a => a.Id.Equals(newValue.Id);
var update = BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql); var update = BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql);
if (disableGlobalDataFilter) { if (disableGlobalDataFilter) {
update = update.DisableGlobalFilter(nameof(Chars.FLG_FREE_SQL_GLOBAL_FILTER_DATA)); update = update.DisableGlobalFilter(Chars.FLG_FREE_SQL_GLOBAL_FILTER_SELF, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT
, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD);
} }
return update.ExecuteEffectsAsync(); return update.ExecuteEffectsAsync();
@ -106,8 +124,8 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
TEntity newValue // TEntity newValue //
, List<string> includeFields = null // , List<string> includeFields = null //
, List<string> excludeFields = null // , List<string> excludeFields = null //
, Expression<Func<TEntity, bool>> whereExp = null // , Expression<Func<TEntity, bool>> whereExp = null //
, string whereSql = null // , string whereSql = null //
, bool ignoreVersion = false) , bool ignoreVersion = false)
{ {
// 默认匹配主键 // 默认匹配主键

View File

@ -19,7 +19,7 @@ public sealed class UserNameAttribute : RegexAttribute
public override bool IsValid(object value) public override bool IsValid(object value)
{ {
if (!base.IsValid(value)) { if (!base.IsValid(value)) {
ErrorMessageResourceName = nameof(Ln.4); ErrorMessageResourceName = nameof(Ln.);
return false; return false;
} }

View File

@ -3,14 +3,23 @@ namespace NetAdmin.Domain;
/// <summary> /// <summary>
/// 数据基类 /// 数据基类
/// </summary> /// </summary>
public abstract record DataAbstraction public abstract record DataAbstraction : IValidatableObject
{ {
/// <summary>
/// 是否已验证
/// </summary>
protected bool HasValidated { get; set; }
/// <summary> /// <summary>
/// 如果数据校验失败,抛出异常 /// 如果数据校验失败,抛出异常
/// </summary> /// </summary>
/// <exception cref="NetAdminValidateException">NetAdminValidateException</exception> /// <exception cref="NetAdminValidateException">NetAdminValidateException</exception>
public void ThrowIfInvalid() public void ThrowIfInvalid()
{ {
if (HasValidated) {
return;
}
var validationResult = this.TryValidate(); var validationResult = this.TryValidate();
if (!validationResult.IsValid) { if (!validationResult.IsValid) {
throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( // throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( //
@ -45,4 +54,19 @@ public abstract record DataAbstraction
property.SetValue(this, s); property.SetValue(this, s);
} }
} }
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
HasValidated = true;
return ValidateInternal(validationContext);
}
/// <summary>
/// 内部验证
/// </summary>
protected virtual IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{
yield return ValidationResult.Success;
}
} }

View File

@ -0,0 +1,22 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 工作批请求
/// </summary>
public record JobReq : DataAbstraction
{
/// <summary>
/// 处理数量
/// </summary>
public int? Count { get; init; }
/// <summary>
/// n秒以前
/// </summary>
public int? SecondsAgo { get; init; }
/// <summary>
/// 直到n秒前
/// </summary>
public int? UntilSecondsAgo { get; init; }
}

View File

@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.DepositOrder;
/// <summary> /// <summary>
/// 请求:创建充值订单 /// 请求:创建充值订单
/// </summary> /// </summary>
public record CreateDepositOrderReq : Sys_DepositOrder, IValidatableObject public record CreateDepositOrderReq : Sys_DepositOrder
{ {
/// <inheritdoc cref="Sys_DepositOrder.ActualPayAmount" /> /// <inheritdoc cref="Sys_DepositOrder.ActualPayAmount" />
public override long ActualPayAmount { get; init; } public override long ActualPayAmount { get; init; }
@ -25,7 +25,7 @@ public record CreateDepositOrderReq : Sys_DepositOrder, IValidatableObject
public override int ToPointRate { get; init; } public override int ToPointRate { get; init; }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{ {
if (PaymentMode != PaymentModes.USDT) { if (PaymentMode != PaymentModes.USDT) {
yield return new ValidationResult(Ln., [nameof(PaymentMode)]); yield return new ValidationResult(Ln., [nameof(PaymentMode)]);

View File

@ -1,12 +0,0 @@
namespace NetAdmin.Domain.Dto.Sys.DepositOrder;
/// <summary>
/// 请求:到账确认
/// </summary>
public record ReceivedConfirmationReq : DataAbstraction
{
/// <summary>
/// 读取前n条记录
/// </summary>
public int ReadRecordCount { get; init; }
}

View File

@ -5,9 +5,8 @@ namespace NetAdmin.Domain.Dto.Sys.Dept;
/// </summary> /// </summary>
public record CreateDeptReq : Sys_Dept public record CreateDeptReq : Sys_Dept
{ {
/// <inheritdoc cref="IFieldEnabled.Enabled" /> /// <inheritdoc />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)] public override bool Enabled { get; init; } = true;
public override bool Enabled { get; init; }
/// <inheritdoc cref="Sys_Dept.Name" /> /// <inheritdoc cref="Sys_Dept.Name" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonIgnore(Condition = JsonIgnoreCondition.Never)]

View File

@ -5,7 +5,7 @@ namespace NetAdmin.Domain.Dto.Sys.Role;
/// <summary> /// <summary>
/// 请求:创建角色 /// 请求:创建角色
/// </summary> /// </summary>
public record CreateRoleReq : Sys_Role, IValidatableObject public record CreateRoleReq : Sys_Role
{ {
/// <summary> /// <summary>
/// 角色-接口映射 /// 角色-接口映射
@ -58,7 +58,7 @@ public record CreateRoleReq : Sys_Role, IValidatableObject
public override string Summary { get; init; } public override string Summary { get; init; }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{ {
if (validationContext.MemberName != null) { if (validationContext.MemberName != null) {
DashboardLayout = JsonSerializer.Serialize(JsonDocument.Parse(DashboardLayout)); DashboardLayout = JsonSerializer.Serialize(JsonDocument.Parse(DashboardLayout));

View File

@ -27,6 +27,11 @@ public record QueryUserRsp : Sys_User
/// <inheritdoc cref="Sys_User.Dept" /> /// <inheritdoc cref="Sys_User.Dept" />
public new virtual QueryDeptRsp Dept { get; init; } public new virtual QueryDeptRsp Dept { get; init; }
/// <summary>
/// 本部门以及子部门编号
/// </summary>
public List<long?> DeptIds { get; init; }
/// <inheritdoc cref="Sys_User.Email" /> /// <inheritdoc cref="Sys_User.Email" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public override string Email { get; init; } public override string Email { get; init; }

View File

@ -6,7 +6,7 @@ namespace NetAdmin.Domain.Dto.Sys.VerifyCode;
/// <summary> /// <summary>
/// 请求:发送验证码 /// 请求:发送验证码
/// </summary> /// </summary>
public sealed record SendVerifyCodeReq : Sys_VerifyCode, IValidatableObject public sealed record SendVerifyCodeReq : Sys_VerifyCode
{ {
/// <inheritdoc cref="Sys_VerifyCode.DestDevice" /> /// <inheritdoc cref="Sys_VerifyCode.DestDevice" />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@ -35,7 +35,7 @@ public sealed record SendVerifyCodeReq : Sys_VerifyCode, IValidatableObject
public VerifyCaptchaReq VerifyCaptchaReq { get; init; } public VerifyCaptchaReq VerifyCaptchaReq { get; init; }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{ {
ValidationResult validationResult; ValidationResult validationResult;
switch (DeviceType) { switch (DeviceType) {

View File

@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary> /// <summary>
/// 请求:创建钱包交易 /// 请求:创建钱包交易
/// </summary> /// </summary>
public record CreateWalletTradeReq : Sys_WalletTrade, IValidatableObject public record CreateWalletTradeReq : Sys_WalletTrade
{ {
/// <inheritdoc cref="Sys_WalletTrade.Amount" /> /// <inheritdoc cref="Sys_WalletTrade.Amount" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
@ -39,7 +39,7 @@ public record CreateWalletTradeReq : Sys_WalletTrade, IValidatableObject
} }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{ {
var tradeDirection = TradeType.Attr<TradeAttribute>().Direction; var tradeDirection = TradeType.Attr<TradeAttribute>().Direction;
if (Amount == 0 || (tradeDirection == TradeDirections.Income && Amount < 0) || (tradeDirection == TradeDirections.Expense && Amount > 0)) { if (Amount == 0 || (tradeDirection == TradeDirections.Income && Amount < 0) || (tradeDirection == TradeDirections.Expense && Amount > 0)) {

View File

@ -46,10 +46,11 @@ public static class Chars
public const string FLG_DB_FIELD_TYPE_VARCHAR_7 = "varchar(7)"; public const string FLG_DB_FIELD_TYPE_VARCHAR_7 = "varchar(7)";
public const string FLG_DB_INDEX_PREFIX = "idx_{tablename}_"; public const string FLG_DB_INDEX_PREFIX = "idx_{tablename}_";
public const string FLG_DB_TABLE_NAME_PREFIX = ""; public const string FLG_DB_TABLE_NAME_PREFIX = "";
public const string FLG_FREE_SQL_GLOBAL_FILTER_DATA = nameof(FLG_FREE_SQL_GLOBAL_FILTER_DATA); public const string FLG_FREE_SQL_GLOBAL_FILTER_SELF = nameof(FLG_FREE_SQL_GLOBAL_FILTER_SELF);
public const string FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD = nameof(FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD);
public const string FLG_FREE_SQL_GLOBAL_FILTER_DEPT = nameof(FLG_FREE_SQL_GLOBAL_FILTER_DEPT);
public const string FLG_FREE_SQL_GLOBAL_FILTER_DELETE = nameof(FLG_FREE_SQL_GLOBAL_FILTER_DELETE); public const string FLG_FREE_SQL_GLOBAL_FILTER_DELETE = nameof(FLG_FREE_SQL_GLOBAL_FILTER_DELETE);
public const string FLG_FREE_SQL_GLOBAL_FILTER_MEMBER = nameof(FLG_FREE_SQL_GLOBAL_FILTER_MEMBER); public const string FLG_FREE_SQL_GLOBAL_FILTER_MEMBER = nameof(FLG_FREE_SQL_GLOBAL_FILTER_MEMBER);
public const string FLG_FREE_SQL_GLOBAL_FILTER_SELF = nameof(FLG_FREE_SQL_GLOBAL_FILTER_SELF);
public const string FLG_FREE_SQL_GLOBAL_FILTER_TENANT = nameof(FLG_FREE_SQL_GLOBAL_FILTER_TENANT); public const string FLG_FREE_SQL_GLOBAL_FILTER_TENANT = nameof(FLG_FREE_SQL_GLOBAL_FILTER_TENANT);
public const string FLG_FRONT_APP_SET_HOME_GRID = "APP_SET_HOME_GRID"; public const string FLG_FRONT_APP_SET_HOME_GRID = "APP_SET_HOME_GRID";
public const string FLG_HTTP_HEADER_KEY_ACCESS_TOKEN = "ACCESS-TOKEN"; public const string FLG_HTTP_HEADER_KEY_ACCESS_TOKEN = "ACCESS-TOKEN";
@ -108,7 +109,7 @@ public static class Chars
public const string RGX_TELEPHONE = """^((\d{3,4}\-)|)\d{7,8}(|([-\u8f6c]{1}\d{1,5}))$"""; public const string RGX_TELEPHONE = """^((\d{3,4}\-)|)\d{7,8}(|([-\u8f6c]{1}\d{1,5}))$""";
public const string RGX_UP_AND_LOWER_NUMBER = """^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$"""; public const string RGX_UP_AND_LOWER_NUMBER = """^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$""";
public const string RGX_URL = """^(https?|ftp):\/\/[^\s/$.?#].[^\s]*\.[^\s]{2,}$"""; public const string RGX_URL = """^(https?|ftp):\/\/[^\s/$.?#].[^\s]*\.[^\s]{2,}$""";
public const string RGX_USERNAME = "^[a-zA-Z0-9_]{4,16}$"; public const string RGX_USERNAME = """^[\u4e00-\u9fa5a-zA-Z0-9_-]{2,16}$""";
public const string RGX_VERIFY_CODE = """^\d{4}$"""; public const string RGX_VERIFY_CODE = """^\d{4}$""";
public const string RGXL_CHINESE_NAME public const string RGXL_CHINESE_NAME

View File

@ -5,7 +5,7 @@ namespace NetAdmin.Infrastructure.Utils;
/// </summary> /// </summary>
public static class PhoneNumberHelper public static class PhoneNumberHelper
{ {
private static readonly IEnumerable<(string CallingCode, CountryCodes CountryCode)> _countryList; private static readonly ImmutableList<(string CallingCode, CountryCodes CountryCode)> _countryList;
#pragma warning disable S3963 #pragma warning disable S3963
static PhoneNumberHelper() static PhoneNumberHelper()
@ -21,7 +21,8 @@ public static class PhoneNumberHelper
.OrderBy(x => x.Item1) .OrderBy(x => x.Item1)
.ThenByDescending(x => x.x.Attr<CountryInfoAttribute>().IsPreferred) .ThenByDescending(x => x.x.Attr<CountryInfoAttribute>().IsPreferred)
.DistinctBy(x => x.Item1) .DistinctBy(x => x.Item1)
.OrderByDescending(x => x.Item1.Length); .OrderByDescending(x => x.Item1.Length)
.ToImmutableList();
} }
/// <summary> /// <summary>
@ -29,7 +30,11 @@ public static class PhoneNumberHelper
/// </summary> /// </summary>
public static CountryCodes? PhoneNumberToCountryCode(string phoneNumber) public static CountryCodes? PhoneNumberToCountryCode(string phoneNumber)
{ {
return _countryList.FirstOrDefault(x => phoneNumber.Replace("+", string.Empty).Trim().StartsWith(x.CallingCode, StringComparison.Ordinal)) phoneNumber = phoneNumber.Trim();
.CountryCode; if (phoneNumber.StartsWith('+')) {
phoneNumber = phoneNumber[1..];
}
return _countryList.FirstOrDefault(x => phoneNumber.StartsWith(x.CallingCode, StringComparison.Ordinal)).CountryCode;
} }
} }

View File

@ -24,5 +24,5 @@ public interface IDepositOrderModule : ICrudModule<CreateDepositOrderReq, QueryD
/// <summary> /// <summary>
/// 到账确认 /// 到账确认
/// </summary> /// </summary>
Task<int> ReceivedConfirmationAsync(ReceivedConfirmationReq req); Task<int> ReceivedConfirmationAsync(JobReq req);
} }

View File

@ -3,4 +3,10 @@ namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
/// <summary> /// <summary>
/// 部门服务 /// 部门服务
/// </summary> /// </summary>
public interface IDeptService : IService, IDeptModule; public interface IDeptService : IService, IDeptModule
{
/// <summary>
/// 获取所有子部门编号
/// </summary>
Task<IEnumerable<long>> GetChildDeptIdsAsync(long deptId);
}

View File

@ -3,4 +3,10 @@ namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
/// <summary> /// <summary>
/// 用户角-色映射服务 /// 用户角-色映射服务
/// </summary> /// </summary>
public interface IUserRoleService : IService, IUserRoleModule; public interface IUserRoleService : IService, IUserRoleModule
{
/// <summary>
/// 通过用户id删除
/// </summary>
Task<int> BulkDeleteByUserIdAsync(long userId);
}

View File

@ -146,7 +146,7 @@ public sealed class DepositOrderService(BasicRepository<Sys_DepositOrder, long>
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<int> ReceivedConfirmationAsync(ReceivedConfirmationReq req) public async Task<int> ReceivedConfirmationAsync(JobReq req)
{ {
req.ThrowIfInvalid(); req.ThrowIfInvalid();
var ret = 0; var ret = 0;
@ -165,7 +165,7 @@ public sealed class DepositOrderService(BasicRepository<Sys_DepositOrder, long>
}) })
.ConfigureAwait(false)).ToList(); .ConfigureAwait(false)).ToList();
var apiResult = await S<ITronScanClient>() var apiResult = await S<ITronScanClient>()
.TransfersAsync(S<IOptions<TronScanOptions>>().Value.Token, req.ReadRecordCount, config.Trc20ReceiptAddress) .TransfersAsync(S<IOptions<TronScanOptions>>().Value.Token, req.Count!.Value, config.Trc20ReceiptAddress)
.ConfigureAwait(false); .ConfigureAwait(false);
foreach (var apiItem in apiResult.TokenTransfers.Where(x => x.TokenInfo.TokenAbbr == "USDT" && x.Confirmed && x.ContractRet == "SUCCESS" && foreach (var apiItem in apiResult.TokenTransfers.Where(x => x.TokenInfo.TokenAbbr == "USDT" && x.Confirmed && x.ContractRet == "SUCCESS" &&
x.FinalResult == "SUCCESS")) { x.FinalResult == "SUCCESS")) {

View File

@ -104,6 +104,12 @@ public sealed class DeptService(BasicRepository<Sys_Dept, long> rpo) //
return ret.Adapt<QueryDeptRsp>(); return ret.Adapt<QueryDeptRsp>();
} }
/// <inheritdoc />
public async Task<IEnumerable<long>> GetChildDeptIdsAsync(long deptId)
{
return await Rpo.Where(a => a.Id == deptId).AsTreeCte().ToListAsync(a => a.Id).ConfigureAwait(false);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<PagedQueryRsp<QueryDeptRsp>> PagedQueryAsync(PagedQueryReq<QueryDeptReq> req) public Task<PagedQueryRsp<QueryDeptRsp>> PagedQueryAsync(PagedQueryReq<QueryDeptReq> req)
{ {

View File

@ -122,7 +122,9 @@ public sealed class DicCatalogService(BasicRepository<Sys_DicCatalog, long> rpo)
private ISelect<Sys_DicCatalog> QueryInternal(QueryReq<QueryDicCatalogReq> req) private ISelect<Sys_DicCatalog> QueryInternal(QueryReq<QueryDicCatalogReq> req)
{ {
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter); var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter)
.WhereIf(req.Filter?.Id > 0, a => a.Id == req.Filter.Id)
.WhereIf(req.Filter?.Code?.Length > 0, a => a.Code == req.Filter.Code);
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (req.Order) { switch (req.Order) {

View File

@ -83,9 +83,15 @@ public sealed class UserInviteService(BasicRepository<Sys_UserInvite, long> rpo)
public Task<List<long>> GetAssociatedUserIdAsync(long userId) public Task<List<long>> GetAssociatedUserIdAsync(long userId)
{ {
return Rpo.Orm.Select<Sys_UserInvite>() return Rpo.Orm.Select<Sys_UserInvite>()
.DisableGlobalFilter(Chars.FLG_FREE_SQL_GLOBAL_FILTER_DATA) .DisableGlobalFilter(Chars.FLG_FREE_SQL_GLOBAL_FILTER_SELF, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT
, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD)
.Where(a => a.Id == userId) .Where(a => a.Id == userId)
.AsTreeCte(up: true, disableGlobalFilters: [Chars.FLG_FREE_SQL_GLOBAL_FILTER_DATA]) .AsTreeCte( //
up: true
, disableGlobalFilters: [
Chars.FLG_FREE_SQL_GLOBAL_FILTER_SELF, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT
, Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD
])
.ToListAsync(a => a.Id); .ToListAsync(a => a.Id);
} }

View File

@ -23,6 +23,12 @@ public sealed class UserRoleService(BasicRepository<Sys_UserRole, long> rpo) //
return ret; return ret;
} }
/// <inheritdoc />
public Task<int> BulkDeleteByUserIdAsync(long userId)
{
return Rpo.DeleteAsync(a => a.UserId == userId);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryUserRoleReq> req) public Task<long> CountAsync(QueryReq<QueryUserRoleReq> req)
{ {

View File

@ -2,6 +2,7 @@ using NetAdmin.Application.Extensions;
using NetAdmin.Domain.Attributes.DataValidation; using NetAdmin.Domain.Attributes.DataValidation;
using NetAdmin.Domain.Contexts; using NetAdmin.Domain.Contexts;
using NetAdmin.Domain.DbMaps.Sys; using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.Dept;
using NetAdmin.Domain.Dto.Sys.User; using NetAdmin.Domain.Dto.Sys.User;
using NetAdmin.Domain.Dto.Sys.UserInvite; using NetAdmin.Domain.Dto.Sys.UserInvite;
using NetAdmin.Domain.Dto.Sys.UserProfile; using NetAdmin.Domain.Dto.Sys.UserProfile;
@ -9,6 +10,7 @@ using NetAdmin.Domain.Dto.Sys.UserWallet;
using NetAdmin.Domain.Dto.Sys.VerifyCode; using NetAdmin.Domain.Dto.Sys.VerifyCode;
using NetAdmin.Domain.Events.Sys; using NetAdmin.Domain.Events.Sys;
using NetAdmin.Domain.Extensions; using NetAdmin.Domain.Extensions;
using Yitter.IdGenerator;
namespace NetAdmin.SysComponent.Application.Services.Sys; namespace NetAdmin.SysComponent.Application.Services.Sys;
@ -108,8 +110,10 @@ public sealed class UserService(
req.ThrowIfInvalid(); req.ThrowIfInvalid();
var roles = await CreateEditCheckAsync(req).ConfigureAwait(false); var roles = await CreateEditCheckAsync(req).ConfigureAwait(false);
var newDeptId = YitIdHelper.NextId();
// 主表 // 主表
var entity = req.Adapt<Sys_User>(); var entity = req.Adapt<Sys_User>() with { DeptId = newDeptId };
var dbUser = await Rpo.InsertAsync(entity).ConfigureAwait(false); var dbUser = await Rpo.InsertAsync(entity).ConfigureAwait(false);
// 分表 // 分表
@ -134,6 +138,9 @@ public sealed class UserService(
// 邀请表 // 邀请表
_ = await userInviteService.CreateAsync((req.Invite ?? new CreateUserInviteReq()) with { Id = dbUser.Id }).ConfigureAwait(false); _ = await userInviteService.CreateAsync((req.Invite ?? new CreateUserInviteReq()) with { Id = dbUser.Id }).ConfigureAwait(false);
// 创建一个用户自己的部门
_ = S<IDeptService>().CreateAsync(new CreateDeptReq { Id = newDeptId, Name = $"{req.UserName}的部门", ParentId = req.Invite?.OwnerDeptId ?? 0 });
// 发布用户创建事件 // 发布用户创建事件
var ret = userList.First(); var ret = userList.First();
await eventPublisher.PublishAsync(new UserCreatedEvent(ret.Adapt<UserInfoRsp>())).ConfigureAwait(false); await eventPublisher.PublishAsync(new UserCreatedEvent(ret.Adapt<UserInfoRsp>())).ConfigureAwait(false);
@ -477,7 +484,9 @@ public sealed class UserService(
.IncludeMany(a => a.Roles, OtherIncludes) .IncludeMany(a => a.Roles, OtherIncludes)
.ToOneAsync() .ToOneAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
return dbUser.Adapt<UserInfoRsp>();
var deptIds = await S<IDeptService>().GetChildDeptIdsAsync(dbUser.DeptId).ConfigureAwait(false);
return dbUser.Adapt<UserInfoRsp>() with { DeptIds = deptIds.ToList().ConvertAll(x => (long?)x) };
static void OtherIncludes(ISelect<Sys_Role> select) static void OtherIncludes(ISelect<Sys_Role> select)
{ {

View File

@ -79,7 +79,7 @@ public sealed class DepositOrderCache(IDistributedCache cache, IDepositOrderServ
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<int> ReceivedConfirmationAsync(ReceivedConfirmationReq req) public Task<int> ReceivedConfirmationAsync(JobReq req)
{ {
return Service.ReceivedConfirmationAsync(req); return Service.ReceivedConfirmationAsync(req);
} }

View File

@ -115,7 +115,7 @@ public sealed class DepositOrderController(IDepositOrderCache cache)
/// <summary> /// <summary>
/// 到账确认 /// 到账确认
/// </summary> /// </summary>
public Task<int> ReceivedConfirmationAsync(ReceivedConfirmationReq req) public Task<int> ReceivedConfirmationAsync(JobReq req)
{ {
return Cache.ReceivedConfirmationAsync(req); return Cache.ReceivedConfirmationAsync(req);
} }

View File

@ -5,7 +5,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7"/> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7"/>
<PackageReference Include="xunit" Version="2.9.3"/> <PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -21,9 +21,21 @@ public static class ServiceCollectionExtensions
(Startup.Args.SyncStructure ? FreeSqlInitMethods.SyncStructure : FreeSqlInitMethods.None) | (Startup.Args.SyncStructure ? FreeSqlInitMethods.SyncStructure : FreeSqlInitMethods.None) |
(Startup.Args.InsertSeedData ? FreeSqlInitMethods.InsertSeedData : FreeSqlInitMethods.None), freeSql => { (Startup.Args.InsertSeedData ? FreeSqlInitMethods.InsertSeedData : FreeSqlInitMethods.None), freeSql => {
// 数据权限过滤器 // 数据权限过滤器
// 本人
_ = freeSql.GlobalFilter.ApplyOnlyIf<IFieldOwner>( // _ = freeSql.GlobalFilter.ApplyOnlyIf<IFieldOwner>( //
Chars.FLG_FREE_SQL_GLOBAL_FILTER_DATA, () => ContextUserInfo.Create()?.Roles.All(x => x.DataScope == DataScopes.Self) ?? false Chars.FLG_FREE_SQL_GLOBAL_FILTER_SELF, () => ContextUserInfo.Create()?.Roles.All(x => x.DataScope == DataScopes.Self) ?? false
, a => a.OwnerId == ContextUserInfo.Create().Id); , a => a.OwnerId == ContextUserInfo.Create().Id);
// 本部门
_ = freeSql.GlobalFilter.ApplyOnlyIf<IFieldOwner>( //
Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT, () => ContextUserInfo.Create()?.Roles.All(x => x.DataScope == DataScopes.Dept) ?? false
, a => ContextUserInfo.Create().DeptId == a.OwnerDeptId);
// 本部门和子部门
_ = freeSql.GlobalFilter.ApplyOnlyIf<IFieldOwner>( //
Chars.FLG_FREE_SQL_GLOBAL_FILTER_DEPT_WITH_CHILD
, () => ContextUserInfo.Create()?.Roles.All(x => x.DataScope == DataScopes.DeptWithChild) ?? false
, a => ContextUserInfo.Create().DeptIds.Contains(a.OwnerDeptId));
}); });
} }

View File

@ -1,16 +1,22 @@
<template> <template>
<el-table-column v-bind="$attrs"> <el-table-column v-bind="$attrs">
<template #default="{ row }"> <template #default="{ row }">
<div :style="{ display: $TOOL.getNestedProperty(row, $attrs.prop) ? 'flex' : 'none' }" class="el-table-column-avatar"> <div
:style="{
display: $TOOL.getNestedProperty(row, $attrs.prop) ? 'flex' : 'none',
' justify-content': 'center',
'align-items': 'center',
gap: '0.5rem',
}">
<el-avatar <el-avatar
v-if="$TOOL.getNestedProperty(row, $attrs.nestProp)" v-if="$TOOL.getNestedProperty(row, $attrs.nestProp)"
:src="getAvatar(row, $attrs.nestProp)" :src="getAvatar(row, $attrs.nestProp)"
@click="click($TOOL.getNestedProperty(row, $attrs.prop))" @click="click($TOOL.getNestedProperty(row, $attrs.prop))"
size="small" size="small"
style="cursor: pointer" /> style="cursor: pointer" />
<div> <div style="line-height: 1.2rem">
<p>{{ $TOOL.getNestedProperty(row, $attrs.nestProp) }}</p> <p>{{ $TOOL.getNestedProperty(row, $attrs.nestProp) }}</p>
<p v-if="$attrs.nestProp2">{{ $TOOL.getNestedProperty(row, $attrs.nestProp2) }}</p> <p v-if="$attrs.nestProp2" style="color: var(--el-color-info-light-3)">{{ $TOOL.getNestedProperty(row, $attrs.nestProp2) }}</p>
</div> </div>
</div> </div>
<save-dialog v-if="dialog.save" @closed="dialog.save = null" @mounted="$refs.saveDialog.open(dialog.save)" ref="saveDialog" /> <save-dialog v-if="dialog.save" @closed="dialog.save = null" @mounted="$refs.saveDialog.open(dialog.save)" ref="saveDialog" />

View File

@ -1,5 +1,11 @@
<template> <template>
<sc-dialog v-model="visible" :full-screen="dialogFullScreen" :title="titleMap[mode]" @closed="$emit(`closed`)" destroy-on-close> <sc-dialog
v-model="visible"
:full-screen="dialogFullScreen.includes(mode)"
:title="titleMap[mode]"
@closed="$emit(`closed`)"
destroy-on-close
ref="dialog">
<div v-loading="loading"> <div v-loading="loading">
<el-tabs v-model="tabId" :class="{ 'hide-tabs': !tabs || !tabs[mode] || tabs[mode].length === 0 }"> <el-tabs v-model="tabId" :class="{ 'hide-tabs': !tabs || !tabs[mode] || tabs[mode].length === 0 }">
<el-tab-pane :label="$t(`基本信息`)" name="basic"> <el-tab-pane :label="$t(`基本信息`)" name="basic">
@ -35,12 +41,14 @@
v-else-if="typeof form[i] === `boolean` || item.isBoolean" v-else-if="typeof form[i] === `boolean` || item.isBoolean"
v-model="form[i]" v-model="form[i]"
:disabled="item.disabled?.includes(mode)" /> :disabled="item.disabled?.includes(mode)" />
<component <template v-else-if="item.detail?.vModelValue">
v-bind="item.detail?.props" <component
v-else-if="item.detail?.vModelValue" v-bind="item.detail?.props"
v-model:value="form[i]" v-if="this.opened"
:disabled="item.disabled?.includes(mode)" v-model:value="form[i]"
:is="item.detail?.is ?? `el-input`" /> :disabled="item.disabled?.includes(mode)"
:is="item.detail.is" />
</template>
<component <component
v-bind="item.detail?.props" v-bind="item.detail?.props"
v-else v-else
@ -52,7 +60,7 @@
</el-form> </el-form>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-bind="item" v-for="(item, i) in tabs[mode]" v-if="tabs" :key="i" :name="item.name"> <el-tab-pane v-bind="item" v-for="(item, i) in tabs[mode]" v-if="tabs" :key="i" :name="item.name">
<component v-if="item.name === tabId" :is="item.component" :ref="item.ref" @closed="paneClosed" /> <component v-bind="item.props" v-if="item.name === tabId" :is="item.component" :row="form" @closed="paneClosed" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -66,12 +74,20 @@
<script> <script>
export default { export default {
components: {}, components: {},
watch: {
mode(n) {
if (this.dialogFullScreen.includes(n) && !this.$refs.dialog.isFullscreen) {
this.$refs.dialog.setFullscreen()
}
},
},
data() { data() {
return { return {
mode: '',
opened: false,
tabId: `basic`, tabId: `basic`,
rules: {}, rules: {},
visible: false, visible: false,
mode: `add`,
loading: false, loading: false,
form: {}, form: {},
titleMap: { titleMap: {
@ -115,11 +131,14 @@ export default {
if (data.row?.id) { if (data.row?.id) {
const res = await this.$API[this.entityName].get.post({ id: data.row.id }) const res = await this.$API[this.entityName].get.post({ id: data.row.id })
Object.assign(this.form, res.data) Object.assign(this.form, this.$TOOL.nestedToDotNotation(res.data))
this.titleMap.edit = this.$t(`编辑{summary}: {id}`, { summary: this.summary, id: this.form.id }) this.titleMap.edit = this.$t(`编辑{summary}: {id}`, { summary: this.summary, id: this.form.id })
this.titleMap.view = this.$t(`查看{summary}: {id}`, { summary: this.summary, id: this.form.id }) this.titleMap.view = this.$t(`查看{summary}: {id}`, { summary: this.summary, id: this.form.id })
} else {
Object.assign(this.form, this.$TOOL.nestedToDotNotation(data.row))
} }
this.loading = false this.loading = false
this.opened = true
return this return this
}, },
@ -134,7 +153,7 @@ export default {
} }
const method = this.mode === `add` ? this.$API[this.entityName].create : this.$API[this.entityName].edit const method = this.mode === `add` ? this.$API[this.entityName].create : this.$API[this.entityName].edit
try { try {
const res = await method.post(this.form) const res = await method.post(this.$TOOL.dotNotationToNested(this.form))
this.$emit(`success`, res.data, this.mode) this.$emit(`success`, res.data, this.mode)
this.visible = false this.visible = false
this.$message.success(this.$t(`操作成功`)) this.$message.success(this.$t(`操作成功`))
@ -155,7 +174,7 @@ export default {
entityName: { type: String }, entityName: { type: String },
summary: { type: String }, summary: { type: String },
columns: { type: Array }, columns: { type: Array },
dialogFullScreen: { type: Boolean }, dialogFullScreen: { type: Array },
tabs: { type: Array }, tabs: { type: Array },
}, },
} }

View File

@ -45,21 +45,29 @@
<!-- 表格主体--> <!-- 表格主体-->
<el-main class="nopadding"> <el-main class="nopadding">
<sc-table <sc-table
v-bind="
Object.assign(
{
queryApi: $API[entityName].pagedQuery,
exportApi: $API[entityName].export,
remoteFilter: true,
remoteSort: true,
rowKey: `id`,
stripe: true,
pageSize: 20,
defaultSort: { prop: `id`, order: `descending` },
},
tableProps,
)
"
:context-extra="this.table.menu.extra" :context-extra="this.table.menu.extra"
:context-menus="Object.keys(this.columns)" :context-menus="Object.keys(this.columns)"
:context-opers="this.operations" :context-opers="this.operations"
:default-sort="{ prop: `id`, order: `descending` }"
:export-api="$API[entityName].export"
:params="query" :params="query"
:query-api="$API[entityName].pagedQuery"
:vue="this" :vue="this"
@data-change="onDataChange" @data-change="onDataChange"
@selection-change="onSelectionChange" @selection-change="onSelectionChange"
ref="table" ref="table">
remote-filter
remote-sort
row-key="id"
stripe>
<el-table-column type="selection" width="50" /> <el-table-column type="selection" width="50" />
<template v-for="(item, i) in columns" :key="i"> <template v-for="(item, i) in columns" :key="i">
<component <component
@ -160,6 +168,10 @@ export default {
}, },
}, },
created() { created() {
for (const f of this.dyFilters) {
this.query.dynamicFilter.filters.push(f)
}
const searchFields = [] const searchFields = []
this.searchControls = [] this.searchControls = []
for (const item in this.columns) { for (const item in this.columns) {
@ -274,7 +286,7 @@ export default {
// ---------------------------- ↓ 搜索栏事件 ---------------------------- // ---------------------------- ↓ 搜索栏事件 ----------------------------
async onReset() { async onReset() {
Object.entries(this.$refs.selectFilter.selected).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = [``])) Object.entries(this.$refs.selectFilter?.selected ?? []).forEach(([key, _]) => (this.$refs.selectFilter.selected[key] = [``]))
}, },
async onSearch(form) { async onSearch(form) {
if (Array.isArray(form.dy.createdTime)) { if (Array.isArray(form.dy.createdTime)) {
@ -299,6 +311,13 @@ export default {
}) })
} }
for (const item of this.dyFilters) {
const exists = this.query.dynamicFilter.filters.find((x) => x.field === item.field)
if (!exists) {
this.query.dynamicFilter.filters.push(item)
}
}
await this.$refs.table.upData() await this.$refs.table.upData()
}, },
// ---------------------------- 搜索栏事件 ↑ ---------------------------- // ---------------------------- 搜索栏事件 ↑ ----------------------------
@ -352,7 +371,13 @@ export default {
loading?.close() loading?.close()
}, },
async onAddClick() { async onAddClick() {
this.dialog.detail = { mode: `add` } const row = {}
for (const i in this.columns) {
if (this.columns[i].default) {
Object.assign(row, Object.fromEntries([[i, this.columns[i].default]]))
}
}
this.dialog.detail = { mode: `add`, row }
}, },
async onSwitchChange(row, method) { async onSwitchChange(row, method) {
try { try {
@ -420,8 +445,19 @@ export default {
type: `root`, type: `root`,
}) })
} }
for (const f of this.dyFilters) {
this.$refs.search.selectInputKey = f.field
this.$refs.search.form.dy[f.field] = f.value
this.$refs.search.keeps.push({
field: f.field,
value: f.value,
type: 'dy',
})
}
}, },
props: { props: {
dyFilters: { type: Array, default: [] },
keywords: { type: String }, keywords: { type: String },
entityName: { type: String }, entityName: { type: String },
summary: { type: String }, summary: { type: String },
@ -431,8 +467,9 @@ export default {
operations: { type: Array, default: [`view`, `add`, `edit`, `del`] }, operations: { type: Array, default: [`view`, `add`, `edit`, `del`] },
rowButtons: { type: Array, default: [] }, rowButtons: { type: Array, default: [] },
rightButtons: { type: Array, default: [] }, rightButtons: { type: Array, default: [] },
dialogFullScreen: { type: Boolean }, dialogFullScreen: { type: Array, default: [] },
tabs: { type: Array }, tabs: { type: Array },
tableProps: { type: Object, default: {} },
}, },
watch: {}, watch: {},
} }

View File

@ -634,4 +634,8 @@ textarea {
* { * {
padding: 0 !important; padding: 0 !important;
} }
}
.ace_placeholder {
color: var(--el-text-color-secondary);
} }

View File

@ -55,7 +55,7 @@ export default {
global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0 global.user?.roles.findIndex((x) => x.ignorePermissionControl) >= 0
? ['*/*/*'] ? ['*/*/*']
: preloads[1]?.data?.roles : preloads[1]?.data?.roles
?.map((x) => x.apiIds.join(',')) ?.map((x) => (x.apiIds ?? []).join(','))
?.join(',') ?.join(',')
.split(',') .split(',')
}, },

View File

@ -251,6 +251,59 @@ tool.objCopy = function (obj) {
return JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj))
} }
/* 带点属性转嵌套对象 */
tool.dotNotationToNested = function (obj) {
const result = {}
for (const key in obj) {
if (key.includes('.')) {
// 处理带点的键
const keys = key.split('.')
let current = result
for (let i = 0; i < keys.length; i++) {
const part = keys[i]
// 如果是最后一个部分,设置值
if (i === keys.length - 1) {
current[part] = obj[key]
} else {
// 如果不是最后一个部分,确保对象存在
current[part] = current[part] || {}
current = current[part]
}
}
} else {
// 直接复制不带点的键
result[key] = obj[key]
}
}
return result
}
/* 将嵌套对象转带点属性 */
tool.nestedToDotNotation = function (obj, prefix = '', result = {}) {
// 处理 null 或 undefined 的情况
if (obj === null || obj === undefined) {
return result
}
for (const key in obj) {
// 更安全的属性检查方式
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const newKey = prefix ? `${prefix}.${key}` : key
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
// 递归处理嵌套对象
this.nestedToDotNotation(obj[key], newKey, result)
} else {
// 基本类型或数组,直接赋值
result[newKey] = obj[key]
}
}
}
return result
}
/* 获取嵌套属性 */ /* 获取嵌套属性 */
tool.getNestedProperty = function (obj, path) { tool.getNestedProperty = function (obj, path) {
if (!path) return null if (!path) return null

View File

@ -2,7 +2,7 @@
<common-page :title="$t('注册新账号')"> <common-page :title="$t('注册新账号')">
<el-steps :active="stepActive" finish-status="success" simple> <el-steps :active="stepActive" finish-status="success" simple>
<el-step :title="$t('填写账号')" /> <el-step :title="$t('填写账号')" />
<el-step v-if="config.registerInviteRequired" :title="$t('验证手机')" /> <el-step v-if="config.registerMobileRequired" :title="$t('验证手机')" />
<el-step :title="$t('注册成功')" /> <el-step :title="$t('注册成功')" />
</el-steps> </el-steps>
<el-form v-if="stepActive === 0" :model="form" :rules="rules" @keyup.enter="next" label-width="15rem" ref="stepForm_0" size="large"> <el-form v-if="stepActive === 0" :model="form" :rules="rules" @keyup.enter="next" label-width="15rem" ref="stepForm_0" size="large">
@ -49,11 +49,11 @@
</div> </div>
<el-form size="large" style="text-align: center"> <el-form size="large" style="text-align: center">
<el-button v-if="stepActive > 0 && stepActive < 2" @click="pre" size="large">{{ $t('上一步') }}</el-button> <el-button v-if="stepActive > 0 && stepActive < 2" @click="pre" size="large">{{ $t('上一步') }}</el-button>
<el-button v-if="stepActive < (this.config.registerInviteRequired ? 1 : 0)" @click="next" size="large" type="primary">{{ <el-button v-if="stepActive < (this.config.registerMobileRequired ? 1 : 0)" @click="next" size="large" type="primary">{{
$t('下一步') $t('下一步')
}}</el-button> }}</el-button>
<el-button <el-button
v-if="stepActive === (this.config.registerInviteRequired ? 1 : 0)" v-if="stepActive === (this.config.registerMobileRequired ? 1 : 0)"
:loading="loading" :loading="loading"
@click="save" @click="save"
size="large" size="large"

View File

@ -1,202 +1,79 @@
<template> <template>
<el-container> <na-table-page
<el-header v-loading="statistics.total === '...'" class="el-header-statistics"> :columns="{
<el-row :gutter="15"> 'user.userName': {
<el-col :lg="24"> label: $t(`邀请关系`),
<el-card shadow="never"> show: [`list`],
<sc-statistic :title="$t('总数')" :value="statistics.total" group-separator /> width: 300,
</el-card> },
</el-col> id: {
</el-row> headerAlign: `center`,
</el-header> is: `na-col-user`,
<el-header> clickOpenDialog: this.$GLOBAL.hasApiPermission(`api/sys/user/get`),
<div class="left-panel"> nestProp: `user.userName`,
<na-search nestProp2: `id`,
:controls="[ width: 170,
{ label: $t(`新用户`),
type: 'select-input', show: [`list`],
field: [ },
'dy', createdTime: {
[ label: $t(`注册时间`),
{ label: $t('用户编号'), key: 'id' }, align: `right`,
{ label: $t('用户名'), key: 'user.userName' }, show: [`list`],
], width: 170,
], },
placeholder: $t('匹配内容'), ownerId: {
style: 'width:25rem', headerAlign: `center`,
selectStyle: 'width:8rem', is: `na-col-user`,
}, clickOpenDialog: this.$GLOBAL.hasApiPermission(`api/sys/user/get`),
]" nestProp: `owner.userName`,
:vue="this" nestProp2: `ownerId`,
@reset="onReset" width: 170,
@search="onSearch" label: $t(`邀请人`),
dateFormat="YYYY-MM-DD HH:mm:ss" show: [`list`],
dateType="datetimerange" },
dateValueFormat="YYYY-MM-DD HH:mm:ss" }"
ref="search" /> :dy-filters="dyFilters"
</div> :operations="operations"
<div class="right-panel"> :row-buttons="[{ title: $t(`更改邀请人`), click: modifyInviter }]"
<el-dropdown v-show="this.selection.length > 0"> :summary="$t(`号码明细`)"
<el-button type="primary"> :table-props="{
{{ $t('批量操作') }} queryApi: $API.sys_userinvite.query,
<el-icon> defaultExpandAll: true,
<el-icon-arrow-down /> hidePagination: true,
</el-icon> }"
</el-button> entity-name="sys_userinvite" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setCommissionRatio">设置返佣比率</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="nopadding">
<sc-table
:context-menus="['id', 'user.userName', 'createdTime', 'commissionRatio']"
:context-opers="[]"
:default-sort="{ prop: 'sort', order: 'descending' }"
:params="query"
:query-api="$API.sys_userinvite.query"
:vue="this"
@data-change="getStatistics"
@selection-change="
(items) => {
selection = items
}
"
default-expand-all
hidePagination
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
<el-table-column type="selection" width="50" />
<el-table-column :label="$t('用户编号')" prop="id" sortable="custom" />
<na-col-avatar :label="$t('用户名')" prop="user.userName" />
<el-table-column
:formatter="(row) => `${(row.commissionRatio / 100).toFixed(2)}%`"
:label="$t('返佣比率')"
align="right"
prop="commissionRatio"
sortable="custom" />
<el-table-column :label="$t('注册时间')" align="right" prop="createdTime" sortable="custom" />
</sc-table>
</el-main>
</el-container>
</template>
<set-inviter-dialog
v-if="dialog.setInviter"
@closed="dialog.setInviter = null"
@mounted="$refs.setInviterDialog.open(dialog.setInviter)"
@success="(data, mode) => table.handleUpdate($refs.table, data, mode)"
ref="setInviterDialog"></set-inviter-dialog>
</template>
<script> <script>
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import table from '@/config/table' const setInviterDialog = defineAsyncComponent(() => import('./set-inviter'))
import naColOperation from '@/config/na-col-operation'
const naColAvatar = defineAsyncComponent(() => import('@/components/na-col-avatar'))
export default { export default {
components: { components: {
naColAvatar, setInviterDialog,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
}, },
created() {}, created() {},
data() { data() {
return { return {
statistics: {
total: '...',
},
dialog: {}, dialog: {},
loading: false, dyFilters: [],
query: { operations: [],
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
} }
}, },
inject: ['reload'],
methods: { methods: {
async setCommissionRatio() { modifyInviter(row) {
let loading // this.dialog.setInviter = { data: row }
try {
const prompt = await this.$prompt(this.$t('1 代表 0.01%'), this.$t('设置返佣比率'), {
inputPattern: /^[0-9]\d*$/,
inputErrorMessage: this.$t('返佣比率不正确'),
})
loading = this.$loading()
const res = await Promise.all(
this.selection.map((x) => this.$API.sys_userinvite.setCommissionRatio.post(Object.assign(x, { commissionRatio: prompt.value }))),
)
this.$message.success(
this.$t(`操作成功 {count}/{total} 项`, {
count: this.selection.length,
total: res.map((x) => x.data ?? 0).reduce((a, b) => a + b, 0),
}),
)
this.$refs.table.refresh()
} catch {
//
}
loading?.close()
},
async getStatistics() {
this.statistics.total = this.$refs.table?.tableData?.length
},
//重置
onReset() {},
//搜索
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['id'] === 'string' && form.dy['id'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'id',
operator: 'eq',
value: form.dy['id'],
})
}
if (typeof form.dy['user.userName'] === 'string' && form.dy['user.userName'].trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'user.userName',
operator: 'eq',
value: form.dy['user.userName'],
})
}
await this.$refs.table.upData()
}, },
}, },
async mounted() { props: {
if (this.keywords) { row: { type: Object },
this.$refs.search.form.root.keywords = this.keywords
this.$refs.search.keeps.push({
field: 'keywords',
value: this.keywords,
type: 'root',
})
}
this.onReset()
}, },
props: ['keywords'],
watch: {},
} }
</script> </script>
<style scoped /> <style scoped />

View File

@ -0,0 +1,59 @@
<template>
<sc-dialog v-model="visible" :title="$t(`修改邀请人`)" @closed="$emit('closed')" destroy-on-close>
<el-form :model="form" label-position="right" label-width="12rem" ref="form">
<el-form-item :label="$t(`用户`)">
<el-input v-model="form.user.userName" disabled placeholder="placeholder"></el-input>
</el-form-item>
<el-form-item :label="$t(`邀请人`)">
<sc-select
v-model="form.ownerId"
:config="{ props: { label: `userName`, value: `id` } }"
:query-api="$API.sys_user.query"
clearable
filterable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button :disabled="loading" :loading="loading" @click="submit" type="primary">保存</el-button>
</template>
</sc-dialog>
</template>
<script>
export default {
components: {},
data() {
return {
loading: true,
visible: false,
form: {},
}
},
emits: ['success', 'closed', 'mounted'],
methods: {
//显示
async open(data) {
this.visible = true
this.loading = true
Object.assign(this.form, data.data)
this.loading = false
return this
},
//表单提交方法
async submit() {
this.loading = true
this.$API.sys_userinvite.edit.post(this.form)
this.$emit('success')
this.visible = false
this.loading = false
},
},
mounted() {
this.$emit('mounted')
},
}
</script>
<style scoped />

View File

@ -158,7 +158,7 @@
align="center" align="center"
prop="dataScope" prop="dataScope"
sortable="custom" sortable="custom"
width="120" /> width="180" />
<el-table-column :label="$t('显示仪表板')" align="center" prop="displayDashboard" sortable="custom" width="120"> <el-table-column :label="$t('显示仪表板')" align="center" prop="displayDashboard" sortable="custom" width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-switch v-model="row.displayDashboard" @change="changeDisplayDashboard($event, row)" /> <el-switch v-model="row.displayDashboard" @change="changeDisplayDashboard($event, row)" />

View File

@ -295,7 +295,7 @@ export default {
userName: [ userName: [
{ {
required: true, required: true,
message: '4位以上字母、数字或下划线', message: '2位以上中文数字或字母',
pattern: this.$GLOBAL.chars.RGX_USERNAME, pattern: this.$GLOBAL.chars.RGX_USERNAME,
}, },
], ],