Compare commits

...

13 Commits

Author SHA1 Message Date
chenchun
998d97b669 fix: 修复审计日志问题 2024-10-21 16:58:19 +08:00
chenchun
453d95a460 feat: 新增验证 2024-10-21 10:39:16 +08:00
chenchun
d55545849a fix: 修复人数访问 2024-10-18 13:14:51 +08:00
橙子
22ba44c271 feat: 完善部门编号字段 2024-10-15 23:26:18 +08:00
橙子
ae2cc7ad9b feat: 优化访问数量统计,采用本地缓存+分布式缓存+数据库 2024-10-15 23:07:12 +08:00
橙子
c880f32d33 !70 修复菜单编辑新增bug
Merge pull request !70 from fuxing168/Fyun168
2024-10-15 10:38:30 +00:00
志福
7b20b68b6a 修复菜单管理里面,编辑后或者新增没有增加routerName,会导致主菜单导航时无法显示 2024-10-15 18:31:14 +08:00
橙子
974f264272 !67 修复字段名驼峰转下划线导致查询日志列表失败的兼容问题
Merge pull request !67 from 凤凰/abp
2024-10-15 01:26:04 +00:00
凤凰
bcbb2b5139 fix: 修复字段名驼峰转下划线导致查询日志列表失败的兼容问题 2024-10-15 00:25:43 +08:00
橙子
e09aaa2dc7 !65 增加表单编辑器
Merge pull request !65 from 李大饼/abp
2024-10-14 08:58:56 +00:00
橙子
e3178d7579 !66 Ruoyi是否启用验证码由后台appsettings.json决定
Merge pull request !66 from Po/abp
2024-10-14 08:57:55 +00:00
Po
b59dfbc3fd Ruoyi是否启用验证码由后台appsettings.json决定 2024-10-14 16:49:33 +08:00
simiyu
0f21688b3c feat:添加表单生成器 2024-10-14 15:54:47 +08:00
23 changed files with 208 additions and 39 deletions

View File

@@ -109,6 +109,7 @@ namespace Yi.Framework.AuditLogging.Domain.Entities
public virtual string? Url { get; protected set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public virtual string? Exceptions { get; protected set; }
public virtual string? Comments { get; protected set; }

View File

@@ -4,7 +4,9 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Yi.Framework.Bbs.Domain.Shared.Caches;
using Yi.Framework.Bbs.Domain.Shared.Etos;
namespace Yi.Framework.Bbs.Application.Extensions;
@@ -14,6 +16,29 @@ namespace Yi.Framework.Bbs.Application.Extensions;
/// 需考虑一致性问题,又不能上锁影响性能
/// </summary>
public class AccessLogMiddleware : IMiddleware, ITransientDependency
{
private static int _accessLogNumber = 0;
internal static void ResetAccessLogNumber()
{
_accessLogNumber = 0;
}
internal static int GetAccessLogNumber()
{
return _accessLogNumber;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
Interlocked.Increment(ref _accessLogNumber);
}
}
public class AccessLogResetEventHandler : ILocalEventHandler<AccessLogResetArgs>,
ITransientDependency
{
/// <summary>
/// 缓存前缀
@@ -40,12 +65,27 @@ public class AccessLogMiddleware : IMiddleware, ITransientDependency
}
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
//该事件由job定时10秒触发
public async Task HandleEventAsync(AccessLogResetArgs eventData)
{
await next(context);
if (EnableRedisCache)
{
await RedisClient.IncrByAsync($"{CacheKeyPrefix}:{AccessLogCacheConst.Key}:{DateTime.Now.Date}", 1);
//分布式锁
if (await RedisClient.SetNxAsync("AccessLogLock",true,TimeSpan.FromSeconds(5)))
{
//自增长数
var incrNumber= AccessLogMiddleware.GetAccessLogNumber();
//立即重置,开始计算,方式丢失
AccessLogMiddleware.ResetAccessLogNumber();
if (incrNumber>0)
{
await RedisClient.IncrByAsync(
$"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date:yyyyMMdd}", incrNumber);
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
using FreeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Quartz;
using Volo.Abp.BackgroundWorkers.Quartz;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EventBus.Local;
using Yi.Framework.Bbs.Domain.Entities;
using Yi.Framework.Bbs.Domain.Shared.Caches;
using Yi.Framework.Bbs.Domain.Shared.Enums;
using Yi.Framework.Bbs.Domain.Shared.Etos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.Bbs.Application.Jobs;
public class AccessLogCacheJob : QuartzBackgroundWorkerBase
{
private readonly ILocalEventBus _localEventBus;
public AccessLogCacheJob(ILocalEventBus localEventBus)
{
_localEventBus = localEventBus;
JobDetail = JobBuilder.Create<AccessLogCacheJob>().WithIdentity(nameof(AccessLogCacheJob))
.Build();
//每10秒执行一次将本地缓存转入redis防止丢数据
Trigger = TriggerBuilder.Create().WithIdentity(nameof(AccessLogCacheJob))
.WithSimpleSchedule((schedule) => { schedule.WithInterval(TimeSpan.FromSeconds(10)).RepeatForever();; })
.Build();
}
public override async Task Execute(IJobExecutionContext context)
{
await _localEventBus.PublishAsync(new AccessLogResetArgs());
}
}

View File

@@ -62,7 +62,7 @@ public class AccessLogStoreJob : QuartzBackgroundWorkerBase
{
//当天的访问量
var number =
await RedisClient.GetAsync<long>($"{CacheKeyPrefix}:{AccessLogCacheConst.Key}:{DateTime.Now.Date}");
await RedisClient.GetAsync<long>($"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date:yyyyMMdd}");
var entity = await _repository._DbQueryable.Where(x => x.AccessLogType == AccessLogTypeEnum.Request)
@@ -81,7 +81,7 @@ public class AccessLogStoreJob : QuartzBackgroundWorkerBase
}
//删除前一天的缓存
await RedisClient.DelAsync($"{CacheKeyPrefix}:{AccessLogCacheConst.Key}:{DateTime.Now.Date.AddDays(-1)}");
await RedisClient.DelAsync($"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date.AddDays(-1):yyyyMMdd}");
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Yi.Framework.Bbs.Domain.Shared.Etos;
public class AccessLogResetArgs
{
}

View File

@@ -4,5 +4,7 @@
{
public Guid Uuid { get; set; } = Guid.Empty;
public byte[] Img { get; set; }
public bool IsEnableCaptcha { get; set; }
}
}

View File

@@ -3,5 +3,9 @@
public class PhoneCaptchaImageDto
{
public string Phone { get; set; }
public string? Uuid { get; set; }
public string? Code { get; set; }
}
}

View File

@@ -20,7 +20,7 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Menu
public string? Remark { get; set; }
public string? Component { get; set; }
public string? Query { get; set; }
public string? RouterName { get; set; }
public int OrderNum { get; set; }
//public List<MenuEntity>? Children { get; set; }
}

View File

@@ -24,6 +24,8 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Menu
public int OrderNum { get; set; }
public string? RouterName { get; set; }
//public List<MenuEntity>? Children { get; set; }
}
}

View File

@@ -82,12 +82,12 @@ namespace Yi.Framework.Rbac.Application.Services
/// 校验图片登录验证码,无需和账号绑定
/// </summary>
[AllowAnonymous]
private void ValidationImageCaptcha(LoginInputVo input)
private void ValidationImageCaptcha(string? uuid,string? code )
{
if (_rbacOptions.EnableCaptcha)
{
//登录不想要验证码 ,可不校验
if (!_captcha.Validate(input.Uuid, input.Code))
if (!_captcha.Validate(uuid, code))
{
throw new UserFriendlyException("验证码错误");
}
@@ -109,7 +109,7 @@ namespace Yi.Framework.Rbac.Application.Services
}
//校验验证码
ValidationImageCaptcha(input);
ValidationImageCaptcha(input.Uuid,input.Code);
UserAggregateRoot user = new();
//校验
@@ -157,7 +157,8 @@ namespace Yi.Framework.Rbac.Application.Services
{
var uuid = _guidGenerator.Create();
var captcha = _captcha.Generate(uuid.ToString());
return new CaptchaImageDto { Img = captcha.Bytes, Uuid = uuid };
var enableCaptcha = _rbacOptions.EnableCaptcha;
return new CaptchaImageDto { Img = captcha.Bytes, Uuid = uuid,IsEnableCaptcha= enableCaptcha };
}
/// <summary>
@@ -198,12 +199,15 @@ namespace Yi.Framework.Rbac.Application.Services
}
/// <summary>
/// 手机验证码
/// 手机验证码-需通过图形验证码
/// </summary>
/// <returns></returns>
private async Task<object> PostCaptchaPhoneAsync(ValidationPhoneTypeEnum validationPhoneType,
PhoneCaptchaImageDto input)
{
//验证uuid 和 验证码
ValidationImageCaptcha(input.Uuid,input.Code);
await ValidationPhone(input.Phone);
//注册的手机号验证,是不能已经注册过的

View File

@@ -20,12 +20,12 @@ namespace Yi.Framework.Rbac.Application.Services.RecordLog
public override async Task<PagedResultDto<LoginLogGetListOutputDto>> GetListAsync(LoginLogGetListInputVo input)
{
RefAsync<int> total = 0;
if (input.Sorting.IsNullOrWhiteSpace())
input.Sorting = $"{nameof(LoginLogAggregateRoot.CreationTime)} Desc";
//if (input.Sorting.IsNullOrWhiteSpace())
// input.Sorting = $"{nameof(LoginLogAggregateRoot.CreationTime)} Desc";
var entities = await _repository._DbQueryable.WhereIF(!string.IsNullOrEmpty(input.LoginIp), x => x.LoginIp.Contains(input.LoginIp!))
.WhereIF(!string.IsNullOrEmpty(input.LoginUser), x => x.LoginUser!.Contains(input.LoginUser!))
.WhereIF(input.StartTime is not null && input.EndTime is not null, x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
.OrderBy(input.Sorting)
.OrderByDescending(it => it.CreationTime) //降序
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
return new PagedResultDto<LoginLogGetListOutputDto>(total, await MapToGetListOutputDtosAsync(entities));
}

View File

@@ -24,12 +24,12 @@ namespace Yi.Framework.Rbac.Application.Services.RecordLog
public override async Task<PagedResultDto<OperationLogGetListOutputDto>> GetListAsync(OperationLogGetListInputVo input)
{
RefAsync<int> total = 0;
if (input.Sorting.IsNullOrWhiteSpace())
input.Sorting = $"{nameof(OperationLogEntity.CreationTime)} Desc";
//if (input.Sorting.IsNullOrWhiteSpace())
// input.Sorting = $"{nameof(OperationLogEntity.CreationTime)} Desc";
var entities = await _repository._DbQueryable.WhereIF(!string.IsNullOrEmpty(input.OperUser), x => x.OperUser.Contains(input.OperUser!))
.WhereIF(input.OperType is not null, x => x.OperType == input.OperType)
.WhereIF(input.StartTime is not null && input.EndTime is not null, x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
.OrderBy(input.Sorting)
.OrderByDescending(it => it.CreationTime) //降序
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
return new PagedResultDto<OperationLogGetListOutputDto>(total, await MapToGetListOutputDtosAsync(entities));
}

View File

@@ -63,12 +63,12 @@ namespace Yi.Framework.Rbac.Domain.Entities
/// <summary>
/// 部门名称
///</summary>
public string DeptName { get; set; } = string.Empty;
public string DeptName { get; set; }
/// <summary>
/// 部门编码
///</summary>
[SugarColumn(ColumnName = "DeptCode")]
public string DeptCode { get; set; } = string.Empty;
public string DeptCode { get; set; }
/// <summary>
/// 负责人
///</summary>

View File

@@ -41,7 +41,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot shenzhenDept = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "SZ",
DeptName = "深圳总公司",
OrderNum = 100,
IsDeleted = false,
@@ -52,7 +52,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot jiangxiDept = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "JX",
DeptName = "江西总公司",
OrderNum = 100,
IsDeleted = false,
@@ -64,7 +64,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot szDept1 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "YF",
DeptName = "研发部门",
OrderNum = 100,
IsDeleted = false,
@@ -74,7 +74,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot szDept2 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "SC",
DeptName = "市场部门",
OrderNum = 100,
IsDeleted = false,
@@ -84,7 +84,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot szDept3 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "CS",
DeptName = "测试部门",
OrderNum = 100,
IsDeleted = false,
@@ -94,7 +94,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot szDept4 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "CW",
DeptName = "财务部门",
OrderNum = 100,
IsDeleted = false,
@@ -104,7 +104,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot szDept5 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "YW",
DeptName = "运维部门",
OrderNum = 100,
IsDeleted = false,
@@ -115,7 +115,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot jxDept1 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "SC",
DeptName = "市场部门",
OrderNum = 100,
IsDeleted = false,
@@ -126,7 +126,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
DeptAggregateRoot jxDept2 = new DeptAggregateRoot(_guidGenerator.Create())
{
DeptCode = "CW2",
DeptName = "财务部门",
OrderNum = 100,
IsDeleted = false,

View File

@@ -228,6 +228,21 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
};
entities.Add(swagger);
//表单构建
MenuAggregateRoot builder = new MenuAggregateRoot(_guidGenerator.Create(), tool.Id)
{
MenuName = "表单生成器",
MenuType = MenuTypeEnum.Menu,
Router = "build",
IsShow = true,
IsLink = false,
MenuIcon = "form",
Component = "tool/build/index",
IsCache = true,
OrderNum = 101,
IsDeleted = false,
};
entities.Add(builder);
// //ERP
// MenuAggregateRoot erp = new MenuAggregateRoot(_guidGenerator.Create())

View File

@@ -91,13 +91,28 @@ defineExpose({ getRef });
<re-col
v-show="newFormInline.menuType !== 2"
:value="24"
:xs="24"
:sm="24"
:xs="12"
:sm="12"
>
<el-form-item label="菜单图标">
<IconSelect v-model="newFormInline.menuIcon" class="w-full" />
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType !== 2"
:value="24"
:xs="12"
:sm="12"
>
<el-form-item label="路由名称">
<el-input
v-model="newFormInline.routerName"
clearable
placeholder="请输入菜单名称"
class="w-full"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="菜单名称" prop="menuName">
<el-input

View File

@@ -89,7 +89,7 @@ export function useMenu() {
{
label: "显示",
prop: "isShow",
formatter: ({ isShow }) => (isShow ? "" : ""),
formatter: ({ isShow }) => (isShow ? "" : ""),
width: 100
},
{

View File

@@ -32,7 +32,8 @@
"typeface-roboto": "^1.1.13",
"vue": "3.4.21",
"vue-cropper": "1.0.3",
"vue-router": "4.3.0"
"vue-router": "4.3.0",
"vform3-builds": "^3.0.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",

View File

@@ -11,6 +11,8 @@ import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import VForm3 from 'vform3-builds'
import 'vform3-builds/dist/designer.style.css' //引入VForm3样式
// 注册指令
@@ -77,5 +79,6 @@ app.use(ElementPlus, {
// 支持 large、default、small
size: Cookies.get('size') || 'default'
})
app.use(VForm3)
app.mount('#app')

View File

@@ -113,7 +113,7 @@ const loginRules = {
const codeUrl = ref("");
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
const captchaEnabled = ref(false);
// 注册开关
const register = ref(false);
const redirect = ref(undefined);
@@ -157,7 +157,7 @@ function handleLogin() {
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
captchaEnabled.value = res.data.isEnableCaptcha === undefined ? true : res.data.isEnableCaptcha;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.data.img;
loginForm.value.uuid = res.data.uuid;

View File

@@ -142,7 +142,7 @@ function handleRegister() {
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
captchaEnabled.value = res.data.isEnableCaptcha === undefined ? true : res.data.isEnableCaptcha;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
registerForm.value.uuid = res.uuid;

View File

@@ -9,6 +9,14 @@
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="部门编号" prop="deptCode">
<el-input
v-model="queryParams.deptCode"
placeholder="请输入部门编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="state">
<el-select v-model="queryParams.state" placeholder="部门状态" clearable>
<el-option
@@ -55,6 +63,7 @@
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
<el-table-column prop="deptCode" label="部门编号" width="200"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
<el-table-column prop="state" label="状态" width="100">
<template #default="scope">
@@ -112,6 +121,11 @@
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门编号" prop="deptCode">
<el-input v-model="form.deptCode" placeholder="部门编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
@@ -173,11 +187,13 @@ const refreshTable = ref(true);
const data = reactive({
form: {},
queryParams: {
deptCode:undefined,
deptName: undefined,
state: undefined
},
rules: {
parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
deptCode: [{ required: true, message: "部门编号不能为空", trigger: "blur" }],
deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],

View File

@@ -1,3 +1,25 @@
<template>
<div> 表单构建 <svg-icon icon-class="build" /> </div>
<div ref="box">
<v-form-designer ></v-form-designer>
</div>
</template>
<script setup lang="ts">
import {ref,onMounted} from "vue";
const box = ref<Element>()
onMounted(() =>
{
box.value?.firstChild?.classList.add("not-margin")
box.value?.firstChild?.children[0].remove()
})
</script>
<style lang="scss" scoped>
div {
margin: 0; /* 如果页面出现垂直滚动条则加入此行CSS以消除之 */
}
.not-margin{
margin: 0 !important;
}
</style>