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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.13.0.120203">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.14.0.120626">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -15,7 +15,8 @@ public static class ISelectExtensions
where TQuery : DataAbstraction, new()
{
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;

View File

@ -62,6 +62,23 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
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>
@ -85,7 +102,8 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
whereExp ??= a => a.Id.Equals(newValue.Id);
var update = BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql);
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();
@ -106,8 +124,8 @@ public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicReposit
TEntity newValue //
, List<string> includeFields = null //
, List<string> excludeFields = null //
, Expression<Func<TEntity, bool>> whereExp = null //
, string whereSql = null //
, Expression<Func<TEntity, bool>> whereExp = null //
, string whereSql = null //
, bool ignoreVersion = false)
{
// 默认匹配主键

View File

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

View File

@ -3,14 +3,23 @@ namespace NetAdmin.Domain;
/// <summary>
/// 数据基类
/// </summary>
public abstract record DataAbstraction
public abstract record DataAbstraction : IValidatableObject
{
/// <summary>
/// 是否已验证
/// </summary>
protected bool HasValidated { get; set; }
/// <summary>
/// 如果数据校验失败,抛出异常
/// </summary>
/// <exception cref="NetAdminValidateException">NetAdminValidateException</exception>
public void ThrowIfInvalid()
{
if (HasValidated) {
return;
}
var validationResult = this.TryValidate();
if (!validationResult.IsValid) {
throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( //
@ -45,4 +54,19 @@ public abstract record DataAbstraction
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>
public record CreateDepositOrderReq : Sys_DepositOrder, IValidatableObject
public record CreateDepositOrderReq : Sys_DepositOrder
{
/// <inheritdoc cref="Sys_DepositOrder.ActualPayAmount" />
public override long ActualPayAmount { get; init; }
@ -25,7 +25,7 @@ public record CreateDepositOrderReq : Sys_DepositOrder, IValidatableObject
public override int ToPointRate { get; init; }
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{
if (PaymentMode != PaymentModes.USDT) {
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>
public record CreateDeptReq : Sys_Dept
{
/// <inheritdoc cref="IFieldEnabled.Enabled" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override bool Enabled { get; init; }
/// <inheritdoc />
public override bool Enabled { get; init; } = true;
/// <inheritdoc cref="Sys_Dept.Name" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ namespace NetAdmin.Domain.Dto.Sys.WalletTrade;
/// <summary>
/// 请求:创建钱包交易
/// </summary>
public record CreateWalletTradeReq : Sys_WalletTrade, IValidatableObject
public record CreateWalletTradeReq : Sys_WalletTrade
{
/// <inheritdoc cref="Sys_WalletTrade.Amount" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
@ -39,7 +39,7 @@ public record CreateWalletTradeReq : Sys_WalletTrade, IValidatableObject
}
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
protected override IEnumerable<ValidationResult> ValidateInternal(ValidationContext validationContext)
{
var tradeDirection = TradeType.Attr<TradeAttribute>().Direction;
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_INDEX_PREFIX = "idx_{tablename}_";
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_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_FRONT_APP_SET_HOME_GRID = "APP_SET_HOME_GRID";
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_UP_AND_LOWER_NUMBER = """^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$""";
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 RGXL_CHINESE_NAME

View File

@ -5,7 +5,7 @@ namespace NetAdmin.Infrastructure.Utils;
/// </summary>
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
static PhoneNumberHelper()
@ -21,7 +21,8 @@ public static class PhoneNumberHelper
.OrderBy(x => x.Item1)
.ThenByDescending(x => x.x.Attr<CountryInfoAttribute>().IsPreferred)
.DistinctBy(x => x.Item1)
.OrderByDescending(x => x.Item1.Length);
.OrderByDescending(x => x.Item1.Length)
.ToImmutableList();
}
/// <summary>
@ -29,7 +30,11 @@ public static class PhoneNumberHelper
/// </summary>
public static CountryCodes? PhoneNumberToCountryCode(string phoneNumber)
{
return _countryList.FirstOrDefault(x => phoneNumber.Replace("+", string.Empty).Trim().StartsWith(x.CallingCode, StringComparison.Ordinal))
.CountryCode;
phoneNumber = phoneNumber.Trim();
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>
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>
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>
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 />
public async Task<int> ReceivedConfirmationAsync(ReceivedConfirmationReq req)
public async Task<int> ReceivedConfirmationAsync(JobReq req)
{
req.ThrowIfInvalid();
var ret = 0;
@ -165,7 +165,7 @@ public sealed class DepositOrderService(BasicRepository<Sys_DepositOrder, long>
})
.ConfigureAwait(false)).ToList();
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);
foreach (var apiItem in apiResult.TokenTransfers.Where(x => x.TokenInfo.TokenAbbr == "USDT" && x.Confirmed && x.ContractRet == "SUCCESS" &&
x.FinalResult == "SUCCESS")) {

View File

@ -104,6 +104,12 @@ public sealed class DeptService(BasicRepository<Sys_Dept, long> rpo) //
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 />
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)
{
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
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)
{
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)
.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);
}

View File

@ -23,6 +23,12 @@ public sealed class UserRoleService(BasicRepository<Sys_UserRole, long> rpo) //
return ret;
}
/// <inheritdoc />
public Task<int> BulkDeleteByUserIdAsync(long userId)
{
return Rpo.DeleteAsync(a => a.UserId == userId);
}
/// <inheritdoc />
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.Contexts;
using NetAdmin.Domain.DbMaps.Sys;
using NetAdmin.Domain.Dto.Sys.Dept;
using NetAdmin.Domain.Dto.Sys.User;
using NetAdmin.Domain.Dto.Sys.UserInvite;
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.Events.Sys;
using NetAdmin.Domain.Extensions;
using Yitter.IdGenerator;
namespace NetAdmin.SysComponent.Application.Services.Sys;
@ -108,8 +110,10 @@ public sealed class UserService(
req.ThrowIfInvalid();
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);
// 分表
@ -134,6 +138,9 @@ public sealed class UserService(
// 邀请表
_ = 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();
await eventPublisher.PublishAsync(new UserCreatedEvent(ret.Adapt<UserInfoRsp>())).ConfigureAwait(false);
@ -477,7 +484,9 @@ public sealed class UserService(
.IncludeMany(a => a.Roles, OtherIncludes)
.ToOneAsync()
.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)
{

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7"/>
<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>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -21,9 +21,21 @@ public static class ServiceCollectionExtensions
(Startup.Args.SyncStructure ? FreeSqlInitMethods.SyncStructure : FreeSqlInitMethods.None) |
(Startup.Args.InsertSeedData ? FreeSqlInitMethods.InsertSeedData : FreeSqlInitMethods.None), freeSql => {
// 数据权限过滤器
// 本人
_ = 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);
// 本部门
_ = 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>
<el-table-column v-bind="$attrs">
<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
v-if="$TOOL.getNestedProperty(row, $attrs.nestProp)"
:src="getAvatar(row, $attrs.nestProp)"
@click="click($TOOL.getNestedProperty(row, $attrs.prop))"
size="small"
style="cursor: pointer" />
<div>
<div style="line-height: 1.2rem">
<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>
<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>
<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">
<el-tabs v-model="tabId" :class="{ 'hide-tabs': !tabs || !tabs[mode] || tabs[mode].length === 0 }">
<el-tab-pane :label="$t(`基本信息`)" name="basic">
@ -35,12 +41,14 @@
v-else-if="typeof form[i] === `boolean` || item.isBoolean"
v-model="form[i]"
:disabled="item.disabled?.includes(mode)" />
<component
v-bind="item.detail?.props"
v-else-if="item.detail?.vModelValue"
v-model:value="form[i]"
:disabled="item.disabled?.includes(mode)"
:is="item.detail?.is ?? `el-input`" />
<template v-else-if="item.detail?.vModelValue">
<component
v-bind="item.detail?.props"
v-if="this.opened"
v-model:value="form[i]"
:disabled="item.disabled?.includes(mode)"
:is="item.detail.is" />
</template>
<component
v-bind="item.detail?.props"
v-else
@ -52,7 +60,7 @@
</el-form>
</el-tab-pane>
<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-tabs>
</div>
@ -66,12 +74,20 @@
<script>
export default {
components: {},
watch: {
mode(n) {
if (this.dialogFullScreen.includes(n) && !this.$refs.dialog.isFullscreen) {
this.$refs.dialog.setFullscreen()
}
},
},
data() {
return {
mode: '',
opened: false,
tabId: `basic`,
rules: {},
visible: false,
mode: `add`,
loading: false,
form: {},
titleMap: {
@ -115,11 +131,14 @@ export default {
if (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.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.opened = true
return this
},
@ -134,7 +153,7 @@ export default {
}
const method = this.mode === `add` ? this.$API[this.entityName].create : this.$API[this.entityName].edit
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.visible = false
this.$message.success(this.$t(`操作成功`))
@ -155,7 +174,7 @@ export default {
entityName: { type: String },
summary: { type: String },
columns: { type: Array },
dialogFullScreen: { type: Boolean },
dialogFullScreen: { type: Array },
tabs: { type: Array },
},
}

View File

@ -45,21 +45,29 @@
<!-- 表格主体-->
<el-main class="nopadding">
<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-menus="Object.keys(this.columns)"
:context-opers="this.operations"
:default-sort="{ prop: `id`, order: `descending` }"
:export-api="$API[entityName].export"
:params="query"
:query-api="$API[entityName].pagedQuery"
:vue="this"
@data-change="onDataChange"
@selection-change="onSelectionChange"
ref="table"
remote-filter
remote-sort
row-key="id"
stripe>
ref="table">
<el-table-column type="selection" width="50" />
<template v-for="(item, i) in columns" :key="i">
<component
@ -160,6 +168,10 @@ export default {
},
},
created() {
for (const f of this.dyFilters) {
this.query.dynamicFilter.filters.push(f)
}
const searchFields = []
this.searchControls = []
for (const item in this.columns) {
@ -274,7 +286,7 @@ export default {
// ---------------------------- ↓ 搜索栏事件 ----------------------------
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) {
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()
},
// ---------------------------- 搜索栏事件 ↑ ----------------------------
@ -352,7 +371,13 @@ export default {
loading?.close()
},
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) {
try {
@ -420,8 +445,19 @@ export default {
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: {
dyFilters: { type: Array, default: [] },
keywords: { type: String },
entityName: { type: String },
summary: { type: String },
@ -431,8 +467,9 @@ export default {
operations: { type: Array, default: [`view`, `add`, `edit`, `del`] },
rowButtons: { type: Array, default: [] },
rightButtons: { type: Array, default: [] },
dialogFullScreen: { type: Boolean },
dialogFullScreen: { type: Array, default: [] },
tabs: { type: Array },
tableProps: { type: Object, default: {} },
},
watch: {},
}

View File

@ -634,4 +634,8 @@ textarea {
* {
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
? ['*/*/*']
: preloads[1]?.data?.roles
?.map((x) => x.apiIds.join(','))
?.map((x) => (x.apiIds ?? []).join(','))
?.join(',')
.split(',')
},

View File

@ -251,6 +251,59 @@ tool.objCopy = function (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) {
if (!path) return null

View File

@ -2,7 +2,7 @@
<common-page :title="$t('注册新账号')">
<el-steps :active="stepActive" finish-status="success" simple>
<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-steps>
<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>
<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 < (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('下一步')
}}</el-button>
<el-button
v-if="stepActive === (this.config.registerInviteRequired ? 1 : 0)"
v-if="stepActive === (this.config.registerMobileRequired ? 1 : 0)"
:loading="loading"
@click="save"
size="large"

View File

@ -1,202 +1,79 @@
<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">
<sc-statistic :title="$t('总数')" :value="statistics.total" group-separator />
</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: 'user.userName' },
],
],
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">
<el-dropdown v-show="this.selection.length > 0">
<el-button type="primary">
{{ $t('批量操作') }}
<el-icon>
<el-icon-arrow-down />
</el-icon>
</el-button>
<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>
<na-table-page
:columns="{
'user.userName': {
label: $t(`邀请关系`),
show: [`list`],
width: 300,
},
id: {
headerAlign: `center`,
is: `na-col-user`,
clickOpenDialog: this.$GLOBAL.hasApiPermission(`api/sys/user/get`),
nestProp: `user.userName`,
nestProp2: `id`,
width: 170,
label: $t(`新用户`),
show: [`list`],
},
createdTime: {
label: $t(`注册时间`),
align: `right`,
show: [`list`],
width: 170,
},
ownerId: {
headerAlign: `center`,
is: `na-col-user`,
clickOpenDialog: this.$GLOBAL.hasApiPermission(`api/sys/user/get`),
nestProp: `owner.userName`,
nestProp2: `ownerId`,
width: 170,
label: $t(`邀请人`),
show: [`list`],
},
}"
:dy-filters="dyFilters"
:operations="operations"
:row-buttons="[{ title: $t(`更改邀请人`), click: modifyInviter }]"
:summary="$t(`号码明细`)"
:table-props="{
queryApi: $API.sys_userinvite.query,
defaultExpandAll: true,
hidePagination: true,
}"
entity-name="sys_userinvite" />
<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>
import { defineAsyncComponent } from 'vue'
import table from '@/config/table'
import naColOperation from '@/config/na-col-operation'
const naColAvatar = defineAsyncComponent(() => import('@/components/na-col-avatar'))
const setInviterDialog = defineAsyncComponent(() => import('./set-inviter'))
export default {
components: {
naColAvatar,
},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
setInviterDialog,
},
created() {},
data() {
return {
statistics: {
total: '...',
},
dialog: {},
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
keywords: this.keywords,
},
selection: [],
dyFilters: [],
operations: [],
}
},
inject: ['reload'],
methods: {
async setCommissionRatio() {
let loading
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()
modifyInviter(row) {
// this.dialog.setInviter = { data: row }
},
},
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',
})
}
this.onReset()
props: {
row: { type: Object },
},
props: ['keywords'],
watch: {},
}
</script>
<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"
prop="dataScope"
sortable="custom"
width="120" />
width="180" />
<el-table-column :label="$t('显示仪表板')" align="center" prop="displayDashboard" sortable="custom" width="120">
<template #default="{ row }">
<el-switch v-model="row.displayDashboard" @change="changeDisplayDashboard($event, row)" />

View File

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