20260510215412

This commit is contained in:
oneao committed 2026-05-10 21:54:13 +08:00
1 parent 85bb38e99b
commit 56f5ba4e6b
12 files changed
+167 -108

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+2 -1
View File
@@ -38,10 +38,11 @@
<version>1.2.23</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/com.microsoft.sqlserver/mssql-jdbc -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>6.2.2.jre8</version>
<version>13.4.0.jre8</version>
</dependency>
<dependency>
@@ -1,24 +1,33 @@
package cn.g3soft.config.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import cn.g3soft.context.DataSourceContextHolder;
import cn.g3soft.utils.SpringUtils;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
@Slf4j
public class DataSourceManager extends AbstractRoutingDataSource {
private static final String CONFIG_PATH = "dbconfigs/";
private static final Pattern ORG_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9_-]+$");
private static final String VALIDATION_QUERY = "SELECT 1";
private static volatile DataSourceManager routingDataSource;
private static final Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
public DataSourceManager() {
setTargetDataSources(new ConcurrentHashMap<>(dataSources));
routingDataSource = this;
setTargetDataSources(toTargetDataSources());
afterPropertiesSet();
}
@@ -27,22 +36,13 @@ public class DataSourceManager extends AbstractRoutingDataSource {
return DataSourceContextHolder.getDataSourceKey();
}
/**
* 获取连接(带异常保护)
*/
public static Connection getConnection(String orgCode) throws Exception {
try {
DataSource dataSource = getOrCreateDataSource(orgCode);
return dataSource.getConnection();
} catch (Exception e) {
throw new RuntimeException("获取数据库连接失败: " + orgCode, e);
}
DataSource ds = getOrCreateDataSource(orgCode);
return ds.getConnection();
}
/**
* 获取或创建数据源
*/
public static DataSource getOrCreateDataSource(String orgCode) throws Exception {
validateOrgCode(orgCode);
DataSource ds = dataSources.get(orgCode);
if (ds != null) {
@@ -50,47 +50,37 @@ public class DataSourceManager extends AbstractRoutingDataSource {
}
synchronized (DataSourceManager.class) {
ds = dataSources.get(orgCode);
if (ds != null) {
return ds;
}
// 创建数据源
ds = createDataSource(orgCode);
// ===== 关键:先验证连接,防止缓存坏数据源 =====
try (Connection conn = ds.getConnection()) {
// 成功才缓存
dataSources.put(orgCode, ds);
} catch (Exception e) {
if (ds instanceof DruidDataSource) {
((DruidDataSource) ds).close();
}
closeQuietly(ds);
throw new RuntimeException("数据库连接失败: " + orgCode, e);
}
// 刷新 routing datasource
refresh();
log.info("Loaded datasource for orgCode: {}", orgCode);
return ds;
}
}
/**
* 刷新 routing datasource
*/
private static void refresh() {
DataSourceManager routing = SpringUtils.getBean(DataSourceManager.class);
routing.setTargetDataSources(new ConcurrentHashMap<>(dataSources));
DataSourceManager routing = routingDataSource;
if (routing == null) {
return;
}
routing.setTargetDataSources(toTargetDataSources());
routing.afterPropertiesSet();
}
/**
* 创建数据源(已优化:防止无限重连/卡死)
*/
private static DataSource createDataSource(String orgCode) throws Exception {
String file = CONFIG_PATH + orgCode + ".properties";
try (InputStream in = DataSourceManager.class
@@ -98,39 +88,87 @@ public class DataSourceManager extends AbstractRoutingDataSource {
.getResourceAsStream(file)) {
if (in == null) {
throw new RuntimeException("未找到配置文件: " + file);
throw new RuntimeException("未找到数据源配置文件: " + file);
}
Properties p = new Properties();
p.load(in);
requireProperty(p, "url", file);
requireProperty(p, "username", file);
requireProperty(p, "password", file);
requireProperty(p, "driver", file);
DruidDataSource ds = new DruidDataSource();
ds.setName(orgCode);
ds.setUrl(p.getProperty("url"));
ds.setUsername(p.getProperty("username"));
ds.setPassword(p.getProperty("password"));
ds.setDriverClassName(p.getProperty("driver"));
// ===== 基础连接池配置 =====
ds.setInitialSize(1);
ds.setMaxActive(10);
ds.setMinIdle(1);
ds.setMaxActive(10);
ds.setMaxWait(3000);
// ===== 核心:防止卡死 / 无限重试 =====
ds.setMaxWait(3000); // 获取连接最大等待3秒
ds.setBreakAfterAcquireFailure(true); // 获取失败直接抛异常
ds.setConnectionErrorRetryAttempts(0); // 不重试
ds.setFailFast(true); // 初始化时检测连接
ds.setBreakAfterAcquireFailure(true);
ds.setConnectionErrorRetryAttempts(0);
ds.setFailFast(true);
// ===== 连接校验 =====
ds.setValidationQuery("SELECT 1");
ds.setTestOnBorrow(true);
ds.setTestWhileIdle(false);
ds.setValidationQuery(VALIDATION_QUERY);
ds.setTestOnBorrow(false);
ds.setTestOnReturn(false);
ds.setTestWhileIdle(true);
ds.setTimeBetweenEvictionRunsMillis(60000);
ds.setMinEvictableIdleTimeMillis(300000);
ds.setKeepAlive(false);
ds.setPoolPreparedStatements(false);
ds.setFilters("stat,slf4j");
ds.setUseGlobalDataSourceStat(true);
ds.setConnectionProperties(
"druid.stat.mergeSql=true;" +
"druid.stat.slowSqlMillis=3000;" +
"druid.stat.logSlowSql=true"
);
ds.setRemoveAbandoned(false);
// 初始化(此时若连接失败会快速抛异常)
ds.init();
return ds;
}
}
}
private static Map<Object, Object> toTargetDataSources() {
return new HashMap<>(dataSources);
}
private static void validateOrgCode(String orgCode) {
if (StringUtils.isBlank(orgCode)) {
throw new IllegalArgumentException("机构码不能为空");
}
if (!ORG_CODE_PATTERN.matcher(orgCode).matches()) {
throw new IllegalArgumentException("机构码只能包含字母、数字、下划线和中划线: " + orgCode);
}
}
private static void requireProperty(Properties properties, String key, String file) {
if (StringUtils.isBlank(properties.getProperty(key))) {
throw new IllegalArgumentException("数据源配置缺少必要项 " + key + ": " + file);
}
}
private static void closeQuietly(DataSource dataSource) {
if (dataSource instanceof DruidDataSource) {
try {
((DruidDataSource) dataSource).close();
} catch (Exception e) {
log.warn("Close datasource failed", e);
}
}
}
}
@@ -7,64 +7,77 @@ import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DruidConfig {
// ===================== 固定配置 =====================
private static final boolean ENABLED = true;
private static final String URL_PATTERN = "/druid/*";
private static final String LOGIN_USERNAME = "admin";
private static final String LOGIN_PASSWORD = "admin";
// 只允许本机访问(生产环境建议改内网IP)
private static final String ALLOW = "127.0.0.1";
private static final String DENY = "";
private static final boolean RESET_ENABLE = false;
private static final String EXCLUSIONS =
"*.js,*.gif,*.jpg,*.jpeg,*.png,*.css,*.ico,*.map,/druid/*";
// ===================================================
@Bean
public ServletRegistrationBean<StatViewServlet> statViewServlet() {
// 监控信息显示页面
StatViewServlet statViewServlet = new StatViewServlet();
// 访问监控信息显示页面的 url 路径(建议值为 /druid/* )
String urlPattern = "/druid/*";
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(statViewServlet, urlPattern);
StatViewServlet servlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> bean =
new ServletRegistrationBean<>(servlet, URL_PATTERN);
Map<String, String> initParams = new HashMap<>();
// 是否允许清空统计数据
initParams.put("resetEnable", "false");
// 登录监控信息显示页面的用户名
initParams.put("loginUsername", "admin");
// 登录监控信息显示页面的密码
initParams.put("loginPassword", "admin");
// 允许访问控制(格式:ip地址、ip地址/子网掩码位数)逗号分隔多个地址
initParams.put("allow", "127.0.0.1");
// 拒绝访问控制(格式:ip地址、ip地址/子网掩码位数)逗号分隔多个地址
// initParams.put("deny", "");
initParams.put("resetEnable", String.valueOf(RESET_ENABLE));
initParams.put("loginUsername", LOGIN_USERNAME);
initParams.put("loginPassword", LOGIN_PASSWORD);
initParams.put("allow", ALLOW);
if (DENY != null && !DENY.isEmpty()) {
initParams.put("deny", DENY);
}
bean.setInitParameters(initParams);
bean.setEnabled(ENABLED);
return bean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> webStatFilter() {
FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>();
// 网络监控过滤器(用于采集 web-jdbc 关联监控的数据)
FilterRegistrationBean<WebStatFilter> bean =
new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
// 过滤所有的 url 路径
Collection<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
bean.setUrlPatterns(urlPatterns);
bean.setUrlPatterns(Collections.singletonList("/*"));
bean.setOrder(2);
Map<String, String> initParams = new HashMap<>();
// 排除不必要采集的 url 路径,以逗号“,”分割
initParams.put("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,*.map,/druid/*");
// 是否使用 session 监控功能
initParams.put("exclusions", EXCLUSIONS);
initParams.put("sessionStatEnable", "true");
// 是否使用 session 监控最大数量(默认是1000)
initParams.put("sessionStatMaxCount", "1000");
// 使得 druid 能够知道当前的 session 的用户是谁,根据需要,把改值修改为你 user 信息保存在 session 中的 sessionName
initParams.put("principalSessionName", "session_user_key");
// 如果你的 user 信息保存在 cookie 中,你可以配置 principalCookieName,使得 druid 知道当前的 user 是谁,根据需要,把该值修改为你 user 信息保存在 cookie 中的 cookieName
initParams.put("principalCookieName", "cookie_user_key");
// 是否监控单个 url 调用的 sql 列表
initParams.put("profileEnable", "true");
bean.setInitParameters(initParams);
bean.setEnabled(ENABLED);
return bean;
}
}
}
@@ -0,0 +1,7 @@
server:
servlet:
context-path: / # 应用的访问路径
port: 8080
file:
server-url: http://ser369.g3soft.cn/api
@@ -0,0 +1,7 @@
server:
servlet:
context-path: /api/ # 应用的访问路径
port: 80
file:
server-url: http://192.168.5.6:80/api
@@ -1,13 +1,14 @@
server:
servlet:
context-path: / # 应用的访问路径
# context-path: / # 应用的访问路径
# context-path: /api/ # 应用的访问路径
tomcat:
uri-encoding: UTF-8 # URI编码
accept-count: 1000 # 连接数满后的排队数
threads:
max: 800 # 最大线程数
min-spare: 100 # 初始化的线程数
port: 8080
# port: 80
spring:
servlet:
@@ -17,13 +18,15 @@ spring:
jackson:
time-zone: "Asia/Shanghai"
date-format: yyyy-MM-dd HH:mm:ss
profiles:
active: dev
file:
base-dir: D:/data/file
static-path: /static/file
# server-url: http://192.168.1.137:80/api
server-url: http://ser369.g3soft.cn/api
# server-url: http://192.168.5.6:80/api
# server-url: http://ser369.g3soft.cn/api
common-dir: common
container-dir: container
temp-dir: temp
@@ -1,10 +1,10 @@
#url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR
#username=sa
#password=zhang520..
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR;encrypt=false
username=sa
password=123456@aaa
password=zhang520..
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
#url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
#username=sa
#password=123456@aaa
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
+2 -2
View File
@@ -10,7 +10,7 @@ PUBLIC_SERVER_PORT=8080
# API
# 后端 API 基础地址
PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
# PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
PUBLIC_API_BASE_URL=http://192.168.5.6:80/api
PUBLIC_ALIYUN_BUCKET = g3hd
-8
View File
@@ -18,7 +18,6 @@ declare module 'vue' {
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -28,23 +27,16 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSplitter: typeof import('element-plus/es')['ElSplitter']
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
@@ -6,11 +6,8 @@ import { useRouter } from "vue-router";
import Vcode from "vue3-puzzle-vcode";
import { reset401Flag } from "@/request";
import { usePermissionStore } from "@/store/permission";
const router = useRouter();
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const form = ref({
orgid: 'g3hd',
@@ -614,6 +614,7 @@ function buildFileName(containerNo, index, fileName) {
<el-image
style="width: 100px; height: 100px; cursor: pointer"
:src="row.b_container_img"
:lazy="true"
@click="handlePreviewShow(row.b_container_img)"
/>
</template>