20260510215412

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

No files matched your search

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