mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
33 Commits
v3.8.0last
...
v3.8.0last
| Author | SHA1 | Date | |
|---|---|---|---|
| c8e33d43cb | |||
| 6c6af870ae | |||
| 79107599a6 | |||
| b6e8b37aee | |||
| 195ddd0421 | |||
| 9ddad931ff | |||
| fc05fe1aff | |||
| 7e325f68ca | |||
| f74da23d06 | |||
| e279915ba2 | |||
| bb6f077a95 | |||
| 7a031e6135 | |||
| 157877f9a6 | |||
| 25a71fa66c | |||
| fdc02fa68a | |||
| ec3c34969a | |||
| 5a215525d5 | |||
| 450b93d916 | |||
| 2d62bad2a9 | |||
| 0b10096f1c | |||
| 431ddb8fcb | |||
| ddf0f61ae5 | |||
| 4042579167 | |||
| bd5fda5968 | |||
| fdbd9c30ac | |||
| b8b4d3f29d | |||
| 78212aa7c0 | |||
| 6dc3c6af2a | |||
| b7a6812140 | |||
| 7efc51e30e | |||
| bd83b994bc | |||
| 8fb81f331c | |||
| fb188a83a1 |
34
README.md
34
README.md
@ -68,14 +68,6 @@ JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼
|
||||
|
||||
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 在线演示 : [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
|
||||
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
||||
- QQ交流群 : ⑩716488839、⑨808791225(满)、其他(满)
|
||||
|
||||
|
||||
|
||||
@ -87,6 +79,29 @@ JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼
|
||||
|
||||
|
||||
|
||||
|
||||
在线体验
|
||||
-----------------------------------
|
||||
|
||||
> JeecgBoot vs 敲敲云
|
||||
> - JeecgBoot是低代码产品拥有很多低代码能力,比如流程设计、表单设计、大屏设计,代码生成器,适合半开发模式(开发+低代码结合),也可以集成零代码的应用管理模块;
|
||||
> - 敲敲云是零代码产品,完全不写代码,通过配置搭建业务系统,其在jeecgboot基础上研发而成,删除了online、代码生成、OA等很多需要编码的功能,只保留了应用管理和聊天、流程、日程、文件四个标准OA功能
|
||||
|
||||
|
||||
- JeecgBoot低代码: https://boot3.jeecg.com
|
||||
- 敲敲云零代码:https://app.qiaoqiaoyun.com
|
||||
- APP演示: http://jeecg.com/appIndex
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc) | [低代码初体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
||||
- QQ交流群 : ⑩716488839、⑨808791225(满)、其他(满)
|
||||
|
||||
|
||||
|
||||
AIGC应用平台介绍
|
||||
-----------------------------------
|
||||
|
||||
@ -105,10 +120,7 @@ JeecgBoot 平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
##### 在线体验
|
||||
|
||||
- JeecgBoot演示: https://boot3.jeecg.com
|
||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||
|
||||
##### Dify `VS` JEECG AI
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -36,7 +36,6 @@ public class RestUtil {
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
private static String getPath() {
|
||||
if (path == null) {
|
||||
path = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("server.servlet.context-path");
|
||||
|
||||
@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
@ -17,6 +18,12 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlInjectionUtil {
|
||||
|
||||
/**
|
||||
* sql注入黑名单数据库名
|
||||
*/
|
||||
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
|
||||
|
||||
/**
|
||||
* 默认—sql注入关键词
|
||||
*/
|
||||
@ -168,6 +175,27 @@ public class SqlInjectionUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在SQL注入关键词字符串
|
||||
*
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("AlibabaUndefineMagicConstant")
|
||||
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
|
||||
// 需要匹配的,sql注入关键词
|
||||
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
|
||||
for (String matchingText : matchingTexts) {
|
||||
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
|
||||
for (String checkText : checkTexts) {
|
||||
if (sql.contains(checkText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* sql注入过滤处理,遇到注入关键字抛异常
|
||||
*
|
||||
@ -208,6 +236,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
@ -244,6 +280,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.jeecg.common.util.encryption;
|
||||
|
||||
import org.apache.shiro.lang.codec.Base64;
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
@ -3,6 +3,8 @@ package org.jeecg.common.util.security;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
@ -66,6 +68,8 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
if(flag == false){
|
||||
return false;
|
||||
}
|
||||
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
|
||||
|
||||
for (QueryTable table : list) {
|
||||
String name = table.getName();
|
||||
String fieldRule = ruleMap.get(name);
|
||||
@ -81,6 +85,16 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
}
|
||||
|
||||
}
|
||||
// 判断是否调用了黑名单数据库
|
||||
String dbName = table.getDbName();
|
||||
if (oConvertUtils.isNotEmpty(dbName)) {
|
||||
dbName = dbName.toLowerCase().trim();
|
||||
if (xssTableSet.contains(dbName)) {
|
||||
flag = false;
|
||||
log.warn("sql黑名单校验,数据库【" + dbName + "】禁止查询");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回黑名单校验结果(不合法直接抛出异常)
|
||||
@ -135,6 +149,8 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
* 查询的表的信息
|
||||
*/
|
||||
protected class QueryTable {
|
||||
//数据库名
|
||||
private String dbName;
|
||||
//表名
|
||||
private String name;
|
||||
//表的别名
|
||||
@ -158,6 +174,14 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
this.fields.add(field);
|
||||
}
|
||||
|
||||
public String getDbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setDbName(String dbName) {
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@ -11,17 +11,16 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@ -59,6 +58,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
@Autowired(required = false)
|
||||
private PrometheusMeterRegistry prometheusMeterRegistry;
|
||||
|
||||
/**
|
||||
* meterRegistryPostProcessor
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
@Qualifier("meterRegistryPostProcessor")
|
||||
private BeanPostProcessor meterRegistryPostProcessor;
|
||||
|
||||
/**
|
||||
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
|
||||
*/
|
||||
@ -147,12 +154,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
|
||||
|
||||
/**
|
||||
* 解决metrics端点不显示jvm信息的问题(zyf)
|
||||
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
* @param event
|
||||
* @author chenrui
|
||||
* @date 2025/5/26 16:46
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnBean(name = "meterRegistryPostProcessor")
|
||||
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
|
||||
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||
@EventListener
|
||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||
if(null != meterRegistryPostProcessor){
|
||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
springdoc.auto-tag-classes: false
|
||||
springdoc.packages-to-scan: org.jeecg
|
||||
springdoc.default-flat-param-object: true
|
||||
@ -24,6 +24,12 @@
|
||||
<artifactId>hibernate-re</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AI大模型管理 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<!-- 企业微信/钉钉 api -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
|
||||
@ -19,6 +19,21 @@ public class CustomInMemoryHttpTraceRepository extends InMemoryHttpTraceReposito
|
||||
return super.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* for [issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309
|
||||
* @param trace
|
||||
* @author chenrui
|
||||
* @date 2025/6/4 19:38
|
||||
*/
|
||||
@Override
|
||||
public void add(HttpTrace trace) {
|
||||
// 只有当请求不是OPTIONS方法,并且URI不包含httptrace时才记录数据
|
||||
if (!"OPTIONS".equals(trace.getRequest().getMethod()) &&
|
||||
!trace.getRequest().getUri().toString().contains("httptrace")) {
|
||||
super.add(trace);
|
||||
}
|
||||
}
|
||||
|
||||
public List<HttpTrace> findAll(String query) {
|
||||
List<HttpTrace> allTrace = super.findAll();
|
||||
if (null != allTrace && !allTrace.isEmpty()) {
|
||||
|
||||
@ -162,7 +162,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
|
||||
String method = openApi.getRequestMethod();
|
||||
String appkey = request.getHeader("appkey");
|
||||
OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey);
|
||||
SysUser systemUser = sysUserService.getById(openApiAuth.getSystemUserId());
|
||||
SysUser systemUser = sysUserService.getUserByName(openApiAuth.getCreateBy());
|
||||
String token = this.getToken(systemUser.getUsername(), systemUser.getPassword());
|
||||
httpHeaders.put("X-Access-Token", Lists.newArrayList(token));
|
||||
httpHeaders.put("Content-Type",Lists.newArrayList("application/json"));
|
||||
|
||||
@ -1,35 +1,22 @@
|
||||
package org.jeecg.modules.openapi.controller;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/openapi/permission")
|
||||
public class OpenApiPermissionController extends JeecgController<OpenApiPermission, OpenApiPermissionService> {
|
||||
|
||||
@PostMapping("add")
|
||||
public Result add(@RequestBody OpenApiPermission openApiPermission) {
|
||||
List<String> list = Arrays.asList(openApiPermission.getApiId().split(","));
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
list.forEach(l->{
|
||||
OpenApiPermission saveApiPermission = new OpenApiPermission();
|
||||
saveApiPermission.setApiId(l);
|
||||
saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId());
|
||||
service.save(saveApiPermission);
|
||||
});
|
||||
}
|
||||
service.add(openApiPermission);
|
||||
return Result.ok("保存成功");
|
||||
}
|
||||
@GetMapping("/list")
|
||||
public Result list( String apiAuthId) {
|
||||
return Result.ok(service.list(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId,apiAuthId)));
|
||||
@GetMapping("/getOpenApi")
|
||||
public Result<?> getOpenApi( String apiAuthId) {
|
||||
return service.getOpenApi(apiAuthId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,4 +97,9 @@ public class OpenApi implements Serializable {
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 历史已选接口
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String ifCheckBox = "0";
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package org.jeecg.modules.openapi.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.openapi.entity.OpenApi;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,4 +12,8 @@ import java.util.List;
|
||||
*/
|
||||
public interface OpenApiPermissionService extends IService<OpenApiPermission> {
|
||||
List<OpenApiPermission> findByAuthId(String authId);
|
||||
|
||||
Result<?> getOpenApi(String apiAuthId);
|
||||
|
||||
void add(OpenApiPermission openApiPermission);
|
||||
}
|
||||
|
||||
@ -1,22 +1,67 @@
|
||||
package org.jeecg.modules.openapi.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.openapi.entity.OpenApi;
|
||||
import org.jeecg.modules.openapi.entity.OpenApiPermission;
|
||||
import org.jeecg.modules.openapi.mapper.OpenApiPermissionMapper;
|
||||
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
|
||||
import org.jeecg.modules.openapi.service.OpenApiService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @date 2024/12/19 17:44
|
||||
*/
|
||||
@Service
|
||||
public class OpenApiPermissionServiceImpl extends ServiceImpl<OpenApiPermissionMapper, OpenApiPermission> implements OpenApiPermissionService {
|
||||
@Resource
|
||||
private OpenApiService openApiService;
|
||||
@Override
|
||||
public List<OpenApiPermission> findByAuthId(String authId) {
|
||||
return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiPermission.class).eq(OpenApiPermission::getApiAuthId, authId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> getOpenApi(String apiAuthId) {
|
||||
List<OpenApi> openApis = openApiService.list();
|
||||
if (CollectionUtil.isEmpty(openApis)) {
|
||||
return Result.error("接口不存在");
|
||||
}
|
||||
List<OpenApiPermission> openApiPermissions = baseMapper.selectList(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, apiAuthId));
|
||||
if (CollectionUtil.isNotEmpty(openApiPermissions)) {
|
||||
Map<String, OpenApi> openApiMap = openApis.stream().collect(Collectors.toMap(OpenApi::getId, o -> o));
|
||||
for (OpenApiPermission openApiPermission : openApiPermissions) {
|
||||
OpenApi openApi = openApiMap.get(openApiPermission.getApiId());
|
||||
if (openApi!=null) {
|
||||
openApi.setIfCheckBox("1");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.ok(openApis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(OpenApiPermission openApiPermission) {
|
||||
this.remove(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, openApiPermission.getApiAuthId()));
|
||||
List<String> list = Arrays.asList(openApiPermission.getApiId().split(","));
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
list.forEach(l->{
|
||||
if (StrUtil.isNotEmpty(l)){
|
||||
OpenApiPermission saveApiPermission = new OpenApiPermission();
|
||||
saveApiPermission.setApiId(l);
|
||||
saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId());
|
||||
this.save(saveApiPermission);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,13 +25,6 @@
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- AI大模型管理 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- flyway 数据库自动升级 -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
|
||||
@ -40,8 +40,8 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
|
||||
String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path"));
|
||||
log.info("\n----------------------------------------------------------\n\t" +
|
||||
"Application Jeecg-Boot is running! Access URLs:\n\t" +
|
||||
"Local: \t\thttp://localhost:" + port + path + "/\n\t" +
|
||||
"External: \thttp://" + ip + ":" + port + path + "/\n\t" +
|
||||
"Local: \t\thttp://localhost:" + port + path + "/doc.html\n\t" +
|
||||
"External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" +
|
||||
"Swagger文档: \thttp://" + ip + ":" + port + path + "/doc.html\n" +
|
||||
"----------------------------------------------------------");
|
||||
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
package org.jeecg.modules.openapi.test;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.client.methods.*;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
|
||||
|
||||
public class SampleOpenApiTest {
|
||||
private final String base_url = "http://localhost:8080/jeecg-boot";
|
||||
private final String appKey = "ak-pFjyNHWRsJEFWlu6";
|
||||
private final String searchKey = "4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS";
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
// 根据部门ID查询用户
|
||||
String url = base_url+"/openapi/call/TEwcXBlr?id=6d35e179cd814e3299bd588ea7daed3f";
|
||||
JSONObject header = genTimestampAndSignature();
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
// 设置请求头
|
||||
httpGet.setHeader("Content-Type", "application/json");
|
||||
httpGet.setHeader("appkey",appKey);
|
||||
httpGet.setHeader("signature",header.get("signature").toString());
|
||||
httpGet.setHeader("timestamp",header.get("timestamp").toString());
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault();
|
||||
CloseableHttpResponse response = httpClient.execute(httpGet);) {
|
||||
// 获取响应状态码
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
System.out.println("[debug] 响应状态码: " + statusCode);
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
System.out.println(entity);
|
||||
// 获取响应内容
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
System.out.println("[debug] 响应内容: " + responseBody);
|
||||
|
||||
// 解析JSON响应
|
||||
JSONObject res = JSON.parseObject(responseBody);
|
||||
//错误日志判断
|
||||
if(res.containsKey("success")){
|
||||
Boolean success = res.getBoolean("success");
|
||||
if(success){
|
||||
System.out.println("[info] 调用成功: " + res.toJSONString());
|
||||
}else{
|
||||
System.out.println("[error] 调用失败: " + res.getString("message"));
|
||||
}
|
||||
}else{
|
||||
System.out.println("[error] 调用失败: " + res.getString("message"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private JSONObject genTimestampAndSignature(){
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
jsonObject.put("timestamp",timestamp);
|
||||
jsonObject.put("signature", md5(appKey + searchKey + timestamp));
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成md5
|
||||
* @param sourceStr
|
||||
* @return
|
||||
*/
|
||||
protected String md5(String sourceStr) {
|
||||
String result = "";
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
md.update(sourceStr.getBytes("utf-8"));
|
||||
byte[] hash = md.digest();
|
||||
int i;
|
||||
StringBuffer buf = new StringBuffer(32);
|
||||
for (int offset = 0; offset < hash.length; offset++) {
|
||||
i = hash[offset];
|
||||
if (i < 0) {
|
||||
i += 256;
|
||||
}
|
||||
if (i < 16) {
|
||||
buf.append("0");
|
||||
}
|
||||
buf.append(Integer.toHexString(i));
|
||||
}
|
||||
result = buf.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("sign签名错误", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -56,8 +56,8 @@
|
||||
<dm8.version>8.1.1.49</dm8.version>
|
||||
|
||||
<!-- 积木报表-->
|
||||
<jimureport-spring-boot-starter.version>1.9.5.1</jimureport-spring-boot-starter.version>
|
||||
<minidao.version>1.10.7</minidao.version>
|
||||
<jimureport-spring-boot-starter.version>2.0.0</jimureport-spring-boot-starter.version>
|
||||
<minidao.version>1.10.10</minidao.version>
|
||||
<!-- 持久层 -->
|
||||
<mybatis-plus.version>3.5.3.2</mybatis-plus.version>
|
||||
<dynamic-datasource-spring-boot-starter.version>4.1.3</dynamic-datasource-spring-boot-starter.version>
|
||||
@ -68,7 +68,7 @@
|
||||
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
|
||||
<aliyun.oss.version>3.11.2</aliyun.oss.version>
|
||||
<!-- shiro -->
|
||||
<shiro.version>2.0.4</shiro.version>
|
||||
<shiro.version>1.13.0</shiro.version>
|
||||
<shiro-redis.version>3.2.3</shiro-redis.version>
|
||||
<java-jwt.version>4.5.0</java-jwt.version>
|
||||
<codegenerate.version>1.4.9</codegenerate.version>
|
||||
|
||||
1
jeecgboot-vue3/.gitignore
vendored
1
jeecgboot-vue3/.gitignore
vendored
@ -2,7 +2,6 @@ node_modules
|
||||
.DS_Store
|
||||
.github
|
||||
dist
|
||||
.npmrc
|
||||
.cache
|
||||
|
||||
tests/server/static
|
||||
|
||||
2
jeecgboot-vue3/.npmrc
Normal file
2
jeecgboot-vue3/.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
@ -78,8 +78,9 @@
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-types": "^5.1.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vxe-table": "4.6.17",
|
||||
"vxe-table-plugin-antd": "4.0.7",
|
||||
"vxe-table": "4.13.31",
|
||||
"vxe-table-plugin-antd": "4.0.8",
|
||||
"vxe-pc-ui": "4.6.12",
|
||||
"xe-utils": "3.5.26",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a-upload name="file" :showUploadList="false" :customRequest="(file) => onClick(file)">
|
||||
<Button :type="type" :class="getButtonClass">
|
||||
<Button :type="type" :class="getButtonClass" :disabled="props.disabled">
|
||||
<template #default="data">
|
||||
<Icon :icon="preIcon" v-if="preIcon" :size="iconSize" />
|
||||
<slot v-bind="data || {}"></slot>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
//下拉框选项值
|
||||
const selectOptions = ref<SelectValue>([]);
|
||||
//下拉框选中值
|
||||
let selectValues = reactive<object>({
|
||||
let selectValues = reactive<any>({
|
||||
value: [],
|
||||
change: false,
|
||||
});
|
||||
@ -74,7 +74,15 @@
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
props.value && initValue();
|
||||
if (props.value) {
|
||||
initValue();
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20250604---for:【issues/8233】resetFields时无法重置
|
||||
if (selectValues.value?.length) {
|
||||
selectValues.value = [];
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20250604---for:【issues/8233】resetFields时无法重置
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Ref } from 'vue';
|
||||
import { inject, reactive, ref, computed, unref, watch, nextTick } from 'vue';
|
||||
import { TreeActionType } from '/@/components/Tree';
|
||||
import { listToTree } from '/@/utils/common/compUtils';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export function useTreeBiz(treeRef, getList, props, realProps, emit) {
|
||||
//接收下拉框选项
|
||||
@ -22,7 +23,7 @@ export function useTreeBiz(treeRef, getList, props, realProps, emit) {
|
||||
const getCheckStrictly = computed(() => (realProps.multiple ? props.checkStrictly : true));
|
||||
// 是否是首次加载回显,只有首次加载,才会显示 loading
|
||||
let isFirstLoadEcho = true;
|
||||
|
||||
let prevSelectValues = [];
|
||||
/**
|
||||
* 监听selectValues变化
|
||||
*/
|
||||
@ -32,12 +33,17 @@ export function useTreeBiz(treeRef, getList, props, realProps, emit) {
|
||||
if(!values){
|
||||
return;
|
||||
}
|
||||
if (openModal.value == false && values.length > 0) {
|
||||
// update-begin--author:liaozhiyang---date:20250604---for:【issues/8232】代码设置JSelectDept组件值没翻译
|
||||
if (values.length > 0) {
|
||||
// 防止多次请求
|
||||
if (isEqual(values, prevSelectValues)) return;
|
||||
prevSelectValues = values;
|
||||
loadingEcho.value = isFirstLoadEcho;
|
||||
isFirstLoadEcho = false;
|
||||
onLoadData(null, values.join(',')).finally(() => {
|
||||
loadingEcho.value = false;
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20250604---for:【issues/8232】代码设置JSelectDept组件值没翻译
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
|
||||
@ -222,7 +222,7 @@
|
||||
// update-end--author:sunjianlei---date:220230630---for:【QQYUN-5571】自封装选择列,解决数据行选择卡顿问题
|
||||
);
|
||||
|
||||
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef, slots);
|
||||
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef, slots, getPaginationInfo);
|
||||
|
||||
const { customRow } = useCustomRow(getProps, {
|
||||
setSelectedRowKeys,
|
||||
|
||||
@ -15,7 +15,8 @@ export function useTableScroll(
|
||||
columnsRef: ComputedRef<BasicColumn[]>,
|
||||
rowSelectionRef: ComputedRef<TableRowSelection<any> | null>,
|
||||
getDataSourceRef: ComputedRef<Recordable[]>,
|
||||
slots: Slots
|
||||
slots: Slots,
|
||||
getPaginationInfo: ComputedRef<any>
|
||||
) {
|
||||
const tableHeightRef: Ref<Nullable<number>> = ref(null);
|
||||
|
||||
@ -144,6 +145,27 @@ export function useTableScroll(
|
||||
setHeight(height);
|
||||
|
||||
bodyEl!.style.height = `${height}px`;
|
||||
// update-begin--author:liaozhiyang---date:20240609---for【issues/8374】分页始终显示在底部
|
||||
if (maxHeight === undefined) {
|
||||
if (unref(getPaginationInfo) && unref(getDataSourceRef).length) {
|
||||
const pageSize = unref(getPaginationInfo)?.pageSize;
|
||||
const current = unref(getPaginationInfo)?.current;
|
||||
const total = unref(getPaginationInfo)?.total;
|
||||
const tableBody = tableEl.querySelector('.ant-table-body') as HTMLElement;
|
||||
const tr = tableEl.querySelector('.ant-table-tbody')?.children ?? [];
|
||||
const lastrEl = tr[tr.length - 1] as HTMLElement;
|
||||
const trHeight = lastrEl.offsetHeight;
|
||||
const dataHeight = trHeight * pageSize;
|
||||
if (tableBody && lastrEl) {
|
||||
if (current === 1 && pageSize > unref(getDataSourceRef).length && total <= pageSize) {
|
||||
tableBody.style.height = `${height}px`;
|
||||
} else {
|
||||
tableBody.style.height = `${dataHeight < height ? dataHeight : height}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240609---for【issues/8374】分页始终显示在底部
|
||||
}
|
||||
useWindowSizeFn(calcTableHeight, 280);
|
||||
onMountedOrActivated(() => {
|
||||
|
||||
@ -28,7 +28,9 @@ export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods:
|
||||
// update-begin--author:liaozhiyang---date:20250403---for:【issues/7812】linkageConfig改变了,vxetable没更新
|
||||
// linkageConfig变化时也需要执行
|
||||
const linkageConfig = toRaw(props.linkageConfig);
|
||||
console.log(linkageConfig);
|
||||
if (linkageConfig) {
|
||||
// console.log(linkageConfig);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20250403---for:【issues/7812】linkageConfig改变了,vxetable没更新
|
||||
let columns: JVxeColumn[] = [];
|
||||
if (isArray(props.columns)) {
|
||||
|
||||
@ -18,10 +18,10 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
|
||||
// rowId: props.rowKey,
|
||||
rowConfig: {
|
||||
keyField: props.rowKey,
|
||||
// 高亮hover的行
|
||||
isHover: true,
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-327】vxetable警告
|
||||
// 高亮hover的行
|
||||
highlightHoverRow: true,
|
||||
|
||||
// --- 【issues/209】自带的tooltip会错位,所以替换成原生的title ---
|
||||
// 溢出隐藏并显示tooltip
|
||||
@ -43,6 +43,7 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
|
||||
expandConfig: {
|
||||
iconClose: 'vxe-icon-arrow-right',
|
||||
iconOpen: 'vxe-icon-arrow-down',
|
||||
...props.expandConfig,
|
||||
},
|
||||
// 虚拟滚动配置,y轴大于xx条数据时启用虚拟滚动
|
||||
scrollY: {
|
||||
|
||||
@ -23,7 +23,12 @@ export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
|
||||
function createSortable() {
|
||||
let xTable = methods.getXTable();
|
||||
// let dom = xTable.$el.querySelector('.vxe-table--fixed-wrapper .vxe-table--body tbody')
|
||||
let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
|
||||
// let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
|
||||
let dom = xTable.$el.querySelector('.vxe-table--body-inner-wrapper > .vxe-table--body tbody');
|
||||
if (!dom) {
|
||||
console.warn('[JVxeTable] 拖拽排序初始化失败,可能是vxe-table升级导致的版本不兼容。');
|
||||
return;
|
||||
}
|
||||
let startChildren = [];
|
||||
sortable2 = Sortable.create(dom as HTMLElement, {
|
||||
handle: '.drag-btn',
|
||||
|
||||
@ -424,12 +424,13 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
|
||||
let xTable = getXTable();
|
||||
let { setActive, index } = options;
|
||||
index = index === -1 ? index : xTable.internalData.tableFullData[index];
|
||||
index = index == null ? -1 : index;
|
||||
// 插入行
|
||||
let result = await xTable.insertAt(rows, index);
|
||||
if (setActive) {
|
||||
// -update-begin--author:liaozhiyang---date:20240619---for:【TV360X-1404】vxetable警告
|
||||
// 激活最后一行的编辑模式
|
||||
xTable.setEditRow(result.rows[result.rows.length - 1]);
|
||||
xTable.setEditRow(result.rows[result.rows.length - 1], true);
|
||||
// -update-end--author:liaozhiyang---date:20240619---for:【TV360X-1404】vxetable警告
|
||||
}
|
||||
await recalcSortNumber();
|
||||
@ -763,7 +764,7 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
|
||||
// 4.1.0
|
||||
//await xTable.updateCache();
|
||||
// 4.1.1
|
||||
await xTable.cacheRowMap()
|
||||
await xTable.cacheRowMap(true)
|
||||
// update-end--author:liaozhiyang---date:20231011---for:【QQYUN-5133】JVxeTable 行编辑升级
|
||||
return await xTable.updateData();
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { App } from 'vue';
|
||||
// 引入 vxe-table
|
||||
import 'xe-utils';
|
||||
import VxeUIAll from 'vxe-pc-ui';
|
||||
import VXETable /*Grid*/ from 'vxe-table';
|
||||
import VXETablePluginAntd from 'vxe-table-plugin-antd';
|
||||
import 'vxe-pc-ui/lib/style.css';
|
||||
import 'vxe-table/lib/style.css';
|
||||
|
||||
import JVxeTable from './JVxeTable';
|
||||
@ -27,6 +29,7 @@ export function registerJVxeTable(app: App) {
|
||||
// 注册自定义组件
|
||||
registerAllComponent();
|
||||
// 执行注册方法
|
||||
app.use(VxeUIAll);
|
||||
app.use(VXETable, VXETableSettings);
|
||||
app.component('JVxeTable', JVxeTable);
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
const { createMessage } = useMessage();
|
||||
const checkedKeys = ref<Array<string | number>>([]);
|
||||
|
||||
const logColumns = ref<any>(columns);
|
||||
const logColumns = ref<any>(exceptionColumns);
|
||||
const searchSchema = ref<any>(searchFormSchema);
|
||||
const searchInfo = { logType: '4' };
|
||||
// 列表页面公共参数、方法
|
||||
|
||||
@ -26,11 +26,11 @@ export const columns: BasicColumn[] = [
|
||||
align:"center",
|
||||
dataIndex: 'blackList'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align:"center",
|
||||
dataIndex: 'status'
|
||||
},
|
||||
// {
|
||||
// title: '状态',
|
||||
// align:"center",
|
||||
// dataIndex: 'status'
|
||||
// },
|
||||
{
|
||||
title: '创建人',
|
||||
align:"center",
|
||||
@ -67,6 +67,11 @@ export const formSchema: FormSchema[] = [
|
||||
];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '原始地址',
|
||||
field: 'originUrl',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '请求方法',
|
||||
field: 'requestMethod',
|
||||
@ -127,11 +132,6 @@ export const formSchema: FormSchema[] = [
|
||||
component:"Input",
|
||||
field: 'body'
|
||||
},
|
||||
{
|
||||
label: '原始地址',
|
||||
field: 'originUrl',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '删除标识',
|
||||
field: 'delFlag',
|
||||
@ -252,7 +252,6 @@ export const openApiHeaderJVxeColumns: JVxeColumn[] = [
|
||||
title: '备注',
|
||||
key: 'note',
|
||||
type: JVxeTypes.input,
|
||||
width:"200px",
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue:'',
|
||||
},
|
||||
@ -297,7 +296,6 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
|
||||
title: '备注',
|
||||
key: 'note',
|
||||
type: JVxeTypes.input,
|
||||
width:"200px",
|
||||
placeholder: '请输入${title}',
|
||||
defaultValue:'',
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ enum Api {
|
||||
edit='/openapi/auth/edit',
|
||||
apiList= '/openapi/list',
|
||||
genAKSK = '/openapi/auth/genAKSK',
|
||||
permissionList='/openapi/permission/list',
|
||||
permissionList='/openapi/permission/getOpenApi',
|
||||
permissionAdd='/openapi/permission/add',
|
||||
deleteOne = '/openapi/auth/delete',
|
||||
deleteBatch = '/openapi/auth/deleteBatch',
|
||||
@ -87,15 +87,17 @@ export const batchDelete = (params, handleSuccess) => {
|
||||
* @param isUpdate
|
||||
*/
|
||||
export const saveOrUpdate = (params, isUpdate) => {
|
||||
let url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url: url, params }, { isTransformResponse: false });
|
||||
if (isUpdate) {
|
||||
return defHttp.put({ url: Api.edit, params }, { isTransformResponse: false });
|
||||
}
|
||||
return defHttp.post({ url: Api.save, params }, { isTransformResponse: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部权限列表接口
|
||||
* @param params
|
||||
*/
|
||||
export const getApiList = (params) => defHttp.get({ url: Api.apiList, params });
|
||||
export const getApiList = (params) => defHttp.get({ url: Api.apiList, params }, { isTransformResponse: false });
|
||||
|
||||
/**
|
||||
* 获取已授权项目的接口
|
||||
|
||||
@ -30,12 +30,11 @@ export const columns: BasicColumn[] = [
|
||||
align: "center",
|
||||
dataIndex: 'createTime'
|
||||
},
|
||||
{
|
||||
title: '关联系统用户名',
|
||||
align: "center",
|
||||
dataIndex: 'systemUserId_dictText',
|
||||
|
||||
},
|
||||
// {
|
||||
// title: '关联系统用户名',
|
||||
// align: "center",
|
||||
// dataIndex: 'createBy',
|
||||
// },
|
||||
];
|
||||
|
||||
// 高级查询数据
|
||||
@ -43,7 +42,7 @@ export const superQuerySchema = {
|
||||
name: {title: '授权名称',order: 0,view: 'text', type: 'string',},
|
||||
ak: {title: 'AK',order: 1,view: 'text', type: 'string',},
|
||||
sk: {title: 'SK',order: 2,view: 'text', type: 'string',},
|
||||
createBy: {title: '创建人',order: 3,view: 'text', type: 'string',},
|
||||
createBy: {title: '关联系统用户名',order: 3,view: 'text', type: 'string',},
|
||||
createTime: {title: '创建时间',order: 4,view: 'datetime', type: 'string',},
|
||||
systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
|
||||
// systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
|
||||
};
|
||||
|
||||
@ -11,9 +11,9 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="systemUserId">
|
||||
<a-form-item name="createBy">
|
||||
<template #label><span title="关联系统用户名">关联系统用户名</span></template>
|
||||
<JSearchSelect dict="sys_user,username,id" v-model:value="queryParam.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
|
||||
<JSearchSelect dict="sys_user,username,username" v-model:value="queryParam.createBy" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
|
||||
<!-- <a-input placeholder="请输入关联系统用户名" v-model:value="queryParam.systemUserId" allow-clear ></a-input>-->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@ -62,6 +62,7 @@
|
||||
<template v-slot:bodyCell="{ column, record, index, text }">
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<OpenApiAuthModal ref="registerModal" @success="handleSuccess"></OpenApiAuthModal>
|
||||
<AuthModal ref="authModal" @success="handleSuccess"></AuthModal>
|
||||
@ -73,7 +74,14 @@
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { columns, superQuerySchema } from './OpenApiAuth.data';
|
||||
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './OpenApiAuth.api';
|
||||
import {
|
||||
list,
|
||||
deleteOne,
|
||||
batchDelete,
|
||||
getImportUrl,
|
||||
getExportUrl,
|
||||
getGenAKSK, saveOrUpdate
|
||||
} from "./OpenApiAuth.api";
|
||||
import OpenApiAuthModal from './components/OpenApiAuthModal.vue'
|
||||
import AuthModal from './components/AuthModal.vue'
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
@ -157,9 +165,23 @@
|
||||
*/
|
||||
function handleEdit(record: Recordable) {
|
||||
registerModal.value.disableSubmit = false;
|
||||
registerModal.value.authDrawerOpen = true;
|
||||
registerModal.value.edit(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置事件
|
||||
* @param record
|
||||
*/
|
||||
async function handleReset(record: Recordable) {
|
||||
const AKSKObj = await getGenAKSK({});
|
||||
record.ak = AKSKObj[0];
|
||||
record.sk = AKSKObj[1];
|
||||
saveOrUpdate(record,true);
|
||||
// handleSuccess;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情
|
||||
*/
|
||||
@ -200,8 +222,12 @@
|
||||
auth: 'openapi:open_api_auth:edit'
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
label: '重置',
|
||||
popConfirm: {
|
||||
title: '是否重置AK,SK',
|
||||
confirm: handleReset.bind(null, record),
|
||||
placement: 'topLeft',
|
||||
},
|
||||
auth: 'openapi:open_api_auth:edit'
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,60 +1,66 @@
|
||||
<template>
|
||||
<a-spin :spinning="confirmLoading">
|
||||
<JFormContainer :disabled="disabled">
|
||||
<template #detail>
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection"> </BasicTable>
|
||||
</template>
|
||||
</JFormContainer>
|
||||
<a-row :span="24" style="margin-bottom: 10px">
|
||||
<a-col :span="12" v-for="item in apiList" @click="handleSelect(item)">
|
||||
<a-card :style="item.checked ? { border: '1px solid #3370ff' } : {}" hoverable class="checkbox-card" :body-style="{ width: '100%', padding: '10px' }">
|
||||
<div class="checkbox-name" style="display: flex; width: 100%; justify-content: space-between">
|
||||
<span>接口名称: {{ item.name }}</span>
|
||||
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker" @change="(e) => handleChange(e, item)"> </a-checkbox>
|
||||
</div>
|
||||
<div class="checkbox-name" style="margin-top: 4px">
|
||||
请求方式: <span>{{item.requestMethod}}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Pagination
|
||||
v-if="apiList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, defineExpose, nextTick, defineProps, computed } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { permissionAddFunction, getPermissionList, getApiList } from '../OpenApiAuth.api';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
|
||||
import { BasicTable } from '@/components/Table';
|
||||
import { useListPage } from '@/hooks/system/useListPage';
|
||||
import { columns } from '@/views/openapi/OpenApi.data';
|
||||
import { computed, defineExpose, defineProps, nextTick, reactive, ref } from "vue";
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApiList, getPermissionList, permissionAddFunction } from '../OpenApiAuth.api';
|
||||
import { Form, Pagination } from 'ant-design-vue';
|
||||
|
||||
const queryParam = reactive<any>({});
|
||||
//注册table数据
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '授权',
|
||||
api: getApiList,
|
||||
columns,
|
||||
canResize: false,
|
||||
useSearchForm: false,
|
||||
|
||||
beforeFetch: async (params) => {
|
||||
return Object.assign(params, queryParam);
|
||||
},
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
|
||||
tableContext;
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
formDisabled: { type: Boolean, default: false },
|
||||
formData: { type: Object, default: () => ({}) },
|
||||
formBpm: { type: Boolean, default: true },
|
||||
});
|
||||
const formRef = ref();
|
||||
const useForm = Form.useForm;
|
||||
const emit = defineEmits(['register', 'ok']);
|
||||
const formData = reactive<Record<string, any>>({
|
||||
apiAuthId: '',
|
||||
apiIdList: [],
|
||||
});
|
||||
const { createMessage } = useMessage();
|
||||
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
|
||||
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
//认证ID
|
||||
const apiAuthId = ref<string>('');
|
||||
//表单验证
|
||||
const validatorRules = reactive({});
|
||||
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
|
||||
//api列表
|
||||
const apiList = ref<any>([]);
|
||||
//选中的值
|
||||
const selectedRowKeys = ref<any>([]);
|
||||
//选中的数据
|
||||
const selectedRows = ref<any>([]);
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
|
||||
// 表单禁用
|
||||
const disabled = computed(() => {
|
||||
@ -68,6 +74,25 @@
|
||||
return props.formDisabled;
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
function reload() {
|
||||
getApiList({ pageNo: pageNo.value, pageSize: pageSize.value, column: 'createTime', order: 'desc'}).then((res)=>{
|
||||
if (res.success) {
|
||||
for (const item of res.result.records) {
|
||||
item.checked = false;
|
||||
}
|
||||
apiList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
setChecked();
|
||||
} else {
|
||||
apiList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
@ -78,23 +103,27 @@
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
function edit(record) {
|
||||
nextTick(() => {
|
||||
resetFields();
|
||||
//赋值
|
||||
formData.apiAuthId = record.id;
|
||||
async function edit(record) {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
pageNo.value = 1;
|
||||
pageSize.value = 10;
|
||||
apiAuthId.value = record.id;
|
||||
await nextTick(() => {
|
||||
// 获取当前已授权的项目
|
||||
getPermissionList({ apiAuthId: record.id }).then((res) => {
|
||||
if (res && res.length > 0) {
|
||||
let list = res.result.records || res.result;
|
||||
let ids = [];
|
||||
list.forEach((item) => {
|
||||
ids.push(item.apiId);
|
||||
if (res.length > 0) {
|
||||
res.forEach((item) => {
|
||||
if(item.ifCheckBox == "1"){
|
||||
selectedRowKeys.value.push(item.id);
|
||||
selectedRows.value.push(item);
|
||||
}
|
||||
});
|
||||
selectedRowKeys.value = ids;
|
||||
formData.apiIdList = ids;
|
||||
//设置选中
|
||||
setChecked();
|
||||
}
|
||||
});
|
||||
reload();
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,35 +131,21 @@
|
||||
* 提交数据
|
||||
*/
|
||||
async function submitForm() {
|
||||
if(selectedRowKeys.value.length === 0)
|
||||
return emit('ok');
|
||||
try {
|
||||
// 触发表单验证
|
||||
await validate();
|
||||
} catch ({ errorFields }) {
|
||||
if (errorFields) {
|
||||
const firstField = errorFields[0];
|
||||
if (firstField) {
|
||||
formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
return Promise.reject(errorFields);
|
||||
}
|
||||
confirmLoading.value = true;
|
||||
//时间格式化
|
||||
let model = formData;
|
||||
// model.apiIdList = selectedRowKeys.value;
|
||||
let model = {};
|
||||
let apiId = ""
|
||||
selectedRowKeys.value.forEach((item) => {
|
||||
apiId += item +",";
|
||||
})
|
||||
model.apiId = apiId;
|
||||
delete model.apiIdList
|
||||
model['apiId'] = apiId;
|
||||
model['apiAuthId'] = apiAuthId.value;
|
||||
await permissionAddFunction(model)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
createMessage.success(res.message);
|
||||
emit('ok');
|
||||
cleanData()
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
@ -139,11 +154,89 @@
|
||||
confirmLoading.value = false;
|
||||
});
|
||||
}
|
||||
const cleanData = () => {
|
||||
selectedRows.value = []
|
||||
selectedRowKeys.value = []
|
||||
};
|
||||
|
||||
/**
|
||||
* 复选框选中事件
|
||||
* @param item
|
||||
*/
|
||||
function handleSelect(item) {
|
||||
let id = item.id;
|
||||
const target = apiList.value.find((item) => item.id === id);
|
||||
if (target) {
|
||||
target.checked = !target.checked;
|
||||
}
|
||||
//存放选中的知识库的id
|
||||
if (!selectedRowKeys.value || selectedRowKeys.value.length == 0) {
|
||||
selectedRowKeys.value.push(id);
|
||||
selectedRows.value.push(item);
|
||||
return;
|
||||
}
|
||||
let findIndex = selectedRowKeys.value.findIndex((item) => item === id);
|
||||
if (findIndex === -1) {
|
||||
selectedRowKeys.value.push(id);
|
||||
selectedRows.value.push(item);
|
||||
} else {
|
||||
selectedRowKeys.value.splice(findIndex, 1);
|
||||
selectedRows.value.splice(findIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复选框选中事件
|
||||
*
|
||||
* @param e
|
||||
* @param item
|
||||
*/
|
||||
function handleChange(e, item: any) {
|
||||
if (e.target.checked) {
|
||||
selectedRowKeys.value.push(item.id);
|
||||
selectedRows.value.push(item);
|
||||
} else {
|
||||
let findIndex = selectedRowKeys.value.findIndex((val) => val === item.id);
|
||||
if (findIndex != -1) {
|
||||
selectedRowKeys.value.splice(findIndex, 1);
|
||||
selectedRows.value.splice(findIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选装状态
|
||||
*/
|
||||
function setChecked() {
|
||||
if (apiList.value && apiList.value.length > 0){
|
||||
let value = selectedRowKeys.value.join(',');
|
||||
apiList.value = apiList.value.map((item) => {
|
||||
if (value.indexOf(item.id) !== -1) {
|
||||
item.checked = true;
|
||||
} else {
|
||||
item.checked = false;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
add,
|
||||
edit,
|
||||
submitForm,
|
||||
cleanData
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -151,4 +244,28 @@
|
||||
.antd-modal-form {
|
||||
padding: 14px;
|
||||
}
|
||||
.list-footer {
|
||||
position: absolute;
|
||||
bottom: -22px;
|
||||
right: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.checkbox-card {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.checkbox-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.checkbox-name {
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.use-select {
|
||||
color: #646a73;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,12 +1,27 @@
|
||||
<template>
|
||||
<j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
|
||||
<AuthForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></AuthForm>
|
||||
</j-modal>
|
||||
<!-- <j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">-->
|
||||
<div style="position: relative;">
|
||||
<a-modal
|
||||
v-model:open="authDrawerOpen"
|
||||
class="custom-class"
|
||||
root-class-name="root-class-name"
|
||||
:root-style="{ color: 'blue' }"
|
||||
:body-style="{ padding: '20px' }"
|
||||
style="color: red"
|
||||
:title="title"
|
||||
:width="600"
|
||||
@after-open-change="authDrawerOpenChange"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<AuthForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></AuthForm>
|
||||
</a-modal>
|
||||
</div>
|
||||
<!-- </j-modal>-->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick, defineExpose } from 'vue';
|
||||
import AuthForm from './AuthForm.vue'
|
||||
import AuthForm from './AuthForm.vue';
|
||||
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
|
||||
|
||||
const title = ref<string>('');
|
||||
@ -16,6 +31,12 @@
|
||||
const registerForm = ref();
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const authDrawerOpen = ref(false);
|
||||
const authDrawerOpenChange = (val: any) => {
|
||||
if(!val)
|
||||
registerForm.value.cleanData()
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
@ -34,6 +55,7 @@
|
||||
function edit(record) {
|
||||
title.value = disableSubmit.value ? '详情' : '授权';
|
||||
visible.value = true;
|
||||
authDrawerOpen.value = true;
|
||||
nextTick(() => {
|
||||
registerForm.value.edit(record);
|
||||
});
|
||||
@ -59,6 +81,7 @@
|
||||
*/
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
authDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@ -19,11 +19,11 @@
|
||||
<a-input v-model:value="formData.sk" placeholder="请输入SK" disabled allow-clear ></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="关联系统用户名" v-bind="validateInfos.systemUserId" id="OpenApiAuthForm-systemUserId" name="systemUserId">
|
||||
<JSearchSelect dict="sys_user,username,id" v-model:value="formData.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<!-- <a-col :span="24">-->
|
||||
<!-- <a-form-item label="关联系统用户名" v-bind="validateInfos.systemUserId" id="OpenApiAuthForm-systemUserId" name="systemUserId">-->
|
||||
<!-- <JSearchSelect dict="sys_user,username,id" v-model:value="formData.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>-->
|
||||
<!-- </a-form-item>-->
|
||||
<!-- </a-col>-->
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
|
||||
<j-modal :title="title" :width="width" :maxHeight="200" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
|
||||
<OpenApiAuthForm ref="registerForm" @ok="submitCallback" :title="title" :formDisabled="disableSubmit" :formBpm="false"></OpenApiAuthForm>
|
||||
</j-modal>
|
||||
</template>
|
||||
|
||||
@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="'80%'" @ok="handleSubmit">
|
||||
<BasicModal :bodyStyle="{ padding: '20px' }" v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" width="80%" @ok="handleSubmit">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-col :span="10">
|
||||
<BasicForm @register="registerForm" ref="formRef" name="OpenApiForm" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-col :span="14">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-col :span="24" style="margin-top: -0.6em">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
resizable
|
||||
ref="openApiHeader"
|
||||
:loading="openApiHeaderTable.loading"
|
||||
:columns="openApiHeaderTable.columns"
|
||||
:dataSource="openApiHeaderTable.dataSource"
|
||||
:height="340"
|
||||
:height="240"
|
||||
:disabled="formDisabled"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:toolbar="true"
|
||||
size="mini"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<JVxeTable
|
||||
keep-source
|
||||
resizable
|
||||
ref="openApiParam"
|
||||
:loading="openApiParamTable.loading"
|
||||
:columns="openApiParamTable.columns"
|
||||
:dataSource="openApiParamTable.dataSource"
|
||||
:height="340"
|
||||
:height="240"
|
||||
:disabled="formDisabled"
|
||||
:rowNumber="true"
|
||||
:rowSelection="true"
|
||||
:toolbar="true"
|
||||
size="mini"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@ -75,10 +75,11 @@
|
||||
});
|
||||
//表单配置
|
||||
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
|
||||
labelWidth: 150,
|
||||
labelWidth: 100,
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
|
||||
@ -26,7 +26,7 @@ export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '手机',
|
||||
width: 150,
|
||||
dataIndex: 'telephone',
|
||||
dataIndex: 'phone',
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
|
||||
Reference in New Issue
Block a user