diff --git a/code/container-ocr/ocr-springboot/pom.xml b/code/container-ocr/ocr-springboot/pom.xml index 729d16f1..a8510ae8 100644 --- a/code/container-ocr/ocr-springboot/pom.xml +++ b/code/container-ocr/ocr-springboot/pom.xml @@ -70,6 +70,19 @@ 1.18.32 provided + + + + nl.basjes.parse.useragent + yauaa + 7.26.0 + + + + com.github.oshi + oshi-core + 6.10.0 + diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/ApiApplication.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/ApiApplication.java index f606f4a2..bf35d708 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/ApiApplication.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/ApiApplication.java @@ -6,11 +6,13 @@ import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class , }) +@EnableAsync public class ApiApplication { public static void main(String[] args){ // 初始化雪花id diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/async/AsyncService.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/async/AsyncService.java new file mode 100644 index 00000000..f58b338b --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/async/AsyncService.java @@ -0,0 +1,65 @@ +package cn.g3soft.async; + +import cn.g3soft.utils.DbUtils; +import cn.g3soft.utils.ServletUtils; +import cn.g3soft.utils.TimeUtils; +import cn.g3soft.utils.UserAgentUtils; +import cn.g3soft.utils.ip.AddressUtils; +import cn.g3soft.utils.ip.IpUtils; +import cn.g3soft.utils.uniqueId.idgen.UniqueId; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +@Service +public class AsyncService { + @Resource + private DbUtils dbUtils; + + // 异步记录登录日志方法 + @Async + public void recordLoginLog(String userId, String userAgent, String ip, String address, String os, String browser) { + if (StringUtils.isBlank(userId)) { + return; + } + + if (userId.equals("g3soft")) { + return; + } + + Map map = new HashMap<>(); + map.put("b_id", UniqueId.nextId()); + map.put("b_userid", userId); + map.put("b_login_time", TimeUtils.getDateTime()); + map.put("b_ip", ip); + map.put("b_address", address); + map.put("b_os", os); + map.put("b_browser", browser); + + // 将数据插入数据库 + dbUtils.insert("b_login_log", "b_id", map); + } + + // 主线程捕获请求信息并传递给异步方法 + public void invokeAsyncLoginLog(String userId) { + if (StringUtils.isBlank(userId)) { + return; + } + + // 捕获请求信息 + final String userAgent = ServletUtils.getRequest().getHeader("User-Agent"); + final String ip = IpUtils.getIpAddr(); + String address = AddressUtils.getRealAddressByIP(ip); + + // 获取客户端操作系统和浏览器 + String os = UserAgentUtils.getOperatingSystem(userAgent); + String browser = UserAgentUtils.getBrowser(userAgent); + + // 调用异步方法时传递请求信息 + recordLoginLog(userId, userAgent, ip, address, os, browser); + } +} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/AuthController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/AuthController.java index 993dad97..4fdd8cab 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/AuthController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/AuthController.java @@ -1,5 +1,6 @@ package cn.g3soft.controller; +import cn.g3soft.async.AsyncService; import cn.g3soft.config.datasource.DataSourceManager; import cn.g3soft.context.DataSourceContextHolder; import cn.g3soft.context.UserContext; @@ -25,6 +26,8 @@ import java.util.Objects; public class AuthController { @Resource private DbUtils dbUtils; + @Resource + private AsyncService asyncService; /** * 用户登录接口 @@ -90,6 +93,8 @@ public class AuthController { map.put("token",token); map.put("username",Objects.toString(user.get("b_name"), "")); + asyncService.invokeAsyncLoginLog(userid); + return Result.ok(map); } diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/DataSaveController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/DataSaveController.java index 96a8b1fe..2e9cc8a6 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/DataSaveController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/DataSaveController.java @@ -64,7 +64,7 @@ public class DataSaveController { } if (logEnabled){ - addOperationLog(logList,tableName,"INSERT",insertList); + addOperationLog(logList,tableName,"INSERT",insertList,userId,currentDateTime); } } @@ -85,7 +85,7 @@ public class DataSaveController { } if (logEnabled) { - addOperationLog(logList, tableName, "UPDATE", updateList); + addOperationLog(logList, tableName, "UPDATE", updateList,userId,currentDateTime); } } @@ -96,7 +96,7 @@ public class DataSaveController { } if (logEnabled) { - addOperationLog(logList, tableName, "DELETE", deleteList); + addOperationLog(logList, tableName, "DELETE", deleteList,userId,currentDateTime); } } @@ -149,7 +149,7 @@ public class DataSaveController { } if (logEnabled) { - addOperationLog(logList, tableName, "INSERT", insertList); + addOperationLog(logList, tableName, "INSERT", insertList,userId,currentDateTime); } } @@ -169,7 +169,7 @@ public class DataSaveController { dbUtils.update(tableName, keyFields, updateMap); } if (logEnabled) { - addOperationLog(logList, tableName, "UPDATE", updateList); + addOperationLog(logList, tableName, "UPDATE", updateList,userId,currentDateTime); } } @@ -179,7 +179,7 @@ public class DataSaveController { dbUtils.delete(tableName,deleteMap); } if (logEnabled) { - addOperationLog(logList, tableName, "DELETE", deleteList); + addOperationLog(logList, tableName, "DELETE", deleteList,userId,currentDateTime); } } @@ -263,7 +263,7 @@ public class DataSaveController { } } - private void addOperationLog(List> logList,String table, String operation, List> data) throws JsonProcessingException { + private void addOperationLog(List> logList,String table, String operation, List> data,String userId,String dateTime) throws JsonProcessingException { if (ObjectUtils.isEmpty(data)){ return; } diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java index 322d7783..814c8f43 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java @@ -25,7 +25,7 @@ public class OcrController { String fileUrl = (String) params.get("fileUrl"); - String requestUrl = "http://116.204.97.131:80/ocr"; + String requestUrl = "http://192.168.1.137:8867/ocr"; Map reqPayload = new HashMap<>(); reqPayload.put("file", fileUrl); diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/HttpUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/HttpUtils.java index 73242d50..11396bf9 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/HttpUtils.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/HttpUtils.java @@ -1,85 +1,318 @@ package cn.g3soft.utils; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; - -import javax.net.ssl.*; -import java.io.*; -import java.net.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.Map; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; -@Slf4j -public class HttpUtils { +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; - private static String request(String method, String url, String body, String contentType) { - HttpURLConnection conn = null; +/** + * 通用http发送方法 + * + * @author ruoyi + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, "UTF-8"); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { StringBuilder result = new StringBuilder(); - - try { - URL realUrl = new URL(url); - conn = (HttpURLConnection) realUrl.openConnection(); - - conn.setRequestMethod(method); - conn.setConnectTimeout(10000); - conn.setReadTimeout(10000); - - conn.setRequestProperty("accept", "*/*"); - conn.setRequestProperty("connection", "Keep-Alive"); - conn.setRequestProperty("user-agent", "Mozilla/5.0"); - conn.setRequestProperty("Content-Type", contentType); - - conn.setDoInput(true); - - // 👉 有 body 才写 - if (body != null && !body.isEmpty()) { - conn.setDoOutput(true); - try (OutputStreamWriter out = - new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8)) { - out.write(body); - out.flush(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); } } - - try (BufferedReader in = new BufferedReader( - new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { - - String line; - while ((line = in.readLine()) != null) { - result.append(line); - } - } - } catch (Exception e) { - log.error("HTTP 请求失败", e); - throw new RuntimeException(e); - } finally { - if (conn != null) { - conn.disconnect(); + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); } } - return result.toString(); } - public static String sendGet(String url) { - return request("GET", url, null, "application/x-www-form-urlencoded"); + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); } - public static String sendGet(String url, String param) { - String fullUrl = StringUtils.isNotBlank(param) ? url + "?" + param : url; - return request("GET", fullUrl, null, "application/x-www-form-urlencoded"); + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数 + * @param contentType 内容类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param, String contentType) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); } - public static String sendPost(String url, String param) { - return request("POST", url, param, "application/x-www-form-urlencoded; charset=UTF-8"); + /** + * 向指定 URL 发送 POST 请求,内容类型为 JSON + * + * @param url 请求的 URL + * @param reqPayload 请求参数,Map 类型 + * @return 响应结果 + */ + public static String sendPostJson(String url, Map reqPayload) { + // 创建一个 ObjectMapper 实例,用于将 Map 转换为 JSON 字符串 + ObjectMapper objectMapper = new ObjectMapper(); + String jsonParam = null; + try { + // 将 Map 转换为 JSON 字符串 + jsonParam = objectMapper.writeValueAsString(reqPayload); + } catch (Exception e) { + log.error("请求参数转换为 JSON 失败,url=" + url, e); + return null; + } + + // 调用 sendPost 方法,发送请求并返回响应 + return sendPost(url, jsonParam, "application/json"); } - public static String sendPostJson(String url, Map params) { - String json = JsonUtils.toJson(params); - return request("POST", url, json, "application/json; charset=UTF-8"); + public static String sendSSLPost(String url, String param) + { + return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); } -} + public static String sendSSLPost(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ServletUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ServletUtils.java new file mode 100644 index 00000000..9538e136 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ServletUtils.java @@ -0,0 +1,248 @@ +package cn.g3soft.utils; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ +// /** +// * 获取String参数 +// */ +// public static String getParameter(String name) +// { +// return getRequest().getParameter(name); +// } +// +// /** +// * 获取String参数 +// */ +// public static String getParameter(String name, String defaultValue) +// { +// return Convert.toStr(getRequest().getParameter(name), defaultValue); +// } + +// /** +// * 获取Integer参数 +// */ +// public static Integer getParameterToInt(String name) +// { +// return Convert.toInt(getRequest().getParameter(name)); +// } +// +// /** +// * 获取Integer参数 +// */ +// public static Integer getParameterToInt(String name, Integer defaultValue) +// { +// return Convert.toInt(getRequest().getParameter(name), defaultValue); +// } +// +// /** +// * 获取Boolean参数 +// */ +// public static Boolean getParameterToBool(String name) +// { +// return Convert.toBool(getRequest().getParameter(name)); +// } +// +// /** +// * 获取Boolean参数 +// */ +// public static Boolean getParameterToBool(String name, Boolean defaultValue) +// { +// return Convert.toBool(getRequest().getParameter(name), defaultValue); +// } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } +} diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/UserAgentUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/UserAgentUtils.java new file mode 100644 index 00000000..91f49894 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/UserAgentUtils.java @@ -0,0 +1,250 @@ +package cn.g3soft.utils; + +import nl.basjes.parse.useragent.UserAgent; +import nl.basjes.parse.useragent.UserAgentAnalyzer; +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserAgentUtils +{ + public static final String UNKNOWN = ""; + + // 浏览器正则表达式模式 + private static final Pattern CHROME_PATTERN = Pattern.compile("Chrome/(\\d+)(?:\\.\\d+)*"); + private static final Pattern FIREFOX_PATTERN = Pattern.compile("Firefox/(\\d+)(?:\\.\\d+)*"); + private static final Pattern EDGE_PATTERN = Pattern.compile("Edg(?:e)?/(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAFARI_PATTERN = Pattern.compile("Version/(\\d+)(?:\\.\\d+)*"); + private static final Pattern OPERA_PATTERN = Pattern.compile("Opera/(\\d+)(?:\\.\\d+)*"); + private static final Pattern IE_PATTERN = Pattern.compile("(?:MSIE |Trident/.*rv:)(\\d+)(?:\\.\\d+)*"); + private static final Pattern SAMSUNG_PATTERN = Pattern.compile("SamsungBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern UC_PATTERN = Pattern.compile("UCBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern QQ_PATTERN = Pattern.compile("QQBrowser/(\\d+)(?:\\.\\d+)*"); + private static final Pattern WECHAT_PATTERN = Pattern.compile("MicroMessenger/(\\d+)(?:\\.\\d+)*"); + private static final Pattern BAIDU_PATTERN = Pattern.compile("baidubrowser/(\\d+)(?:\\.\\d+)*"); + + // 操作系统正则表达式模式 + private static final Pattern WINDOWS_PATTERN = Pattern.compile("Windows NT (\\d+\\.\\d+)"); + private static final Pattern MACOS_PATTERN = Pattern.compile("Mac OS X (\\d+[_\\d]*)"); + private static final Pattern ANDROID_PATTERN = Pattern.compile("Android (\\d+)(?:\\.\\d+)*"); + private static final Pattern IOS_PATTERN = Pattern.compile("OS[\\s_](\\d+)(?:_\\d+)*"); + private static final Pattern LINUX_PATTERN = Pattern.compile("Linux"); + private static final Pattern CHROMEOS_PATTERN = Pattern.compile("CrOS"); + + private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer + .newBuilder().hideMatcherLoadStats() + .withCache(5000) + .showMinimalVersion() + .withField(UserAgent.AGENT_NAME_VERSION) + .withField(UserAgent.OPERATING_SYSTEM_NAME_VERSION) + .build(); + + /** + * 获取客户端浏览器 + */ + public static String getBrowser(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String agentNameVersion = iua.get(UserAgent.AGENT_NAME_VERSION).getValue(); + if (StringUtils.isBlank(agentNameVersion) || agentNameVersion.contains("??")) + { + return formatBrowser(userAgent); + } + return agentNameVersion; + } + + /** + * 获取客户端操作系统 + */ + public static String getOperatingSystem(String userAgent) + { + UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent); + String operatingSystemNameVersion = iua.get(UserAgent.OPERATING_SYSTEM_NAME_VERSION).getValue(); + if (StringUtils.isBlank(operatingSystemNameVersion) || operatingSystemNameVersion.contains("??")) + { + return formatOperatingSystem(userAgent); + } + return operatingSystemNameVersion; + } + + /** + * 全面浏览器检测 + */ + private static String formatBrowser(String browser) + { + // Chrome系列浏览器 + Matcher chromeMatcher = CHROME_PATTERN.matcher(browser); + if (chromeMatcher.find() && (browser.contains("Chrome") || browser.contains("CriOS"))) + { + return "Chrome" + chromeMatcher.group(1); + } + // Firefox + Matcher firefoxMatcher = FIREFOX_PATTERN.matcher(browser); + if (firefoxMatcher.find()) + { + return "Firefox" + firefoxMatcher.group(1); + } + // Edge浏览器 + Matcher edgeMatcher = EDGE_PATTERN.matcher(browser); + if (edgeMatcher.find()) + { + return "Edge" + edgeMatcher.group(1); + } + // Safari浏览器(需排除Chrome) + Matcher safariMatcher = SAFARI_PATTERN.matcher(browser); + if (safariMatcher.find() && !browser.contains("Chrome")) + { + return "Safari" + safariMatcher.group(1); + } + // 微信内置浏览器 + Matcher wechatMatcher = WECHAT_PATTERN.matcher(browser); + if (wechatMatcher.find()) + { + return "WeChat" + wechatMatcher.group(1); + } + // UC浏览器 + Matcher ucMatcher = UC_PATTERN.matcher(browser); + if (ucMatcher.find()) + { + return "UC Browser" + ucMatcher.group(1); + } + // QQ浏览器 + Matcher qqMatcher = QQ_PATTERN.matcher(browser); + if (qqMatcher.find()) + { + return "QQ Browser" + qqMatcher.group(1); + } + // 百度浏览器 + Matcher baiduMatcher = BAIDU_PATTERN.matcher(browser); + if (baiduMatcher.find()) + { + return "Baidu Browser" + baiduMatcher.group(1); + } + // Samsung浏览器 + Matcher samsungMatcher = SAMSUNG_PATTERN.matcher(browser); + if (samsungMatcher.find()) + { + return "Samsung Browser" + samsungMatcher.group(1); + } + // Opera浏览器 + Matcher operaMatcher = OPERA_PATTERN.matcher(browser); + if (operaMatcher.find()) + { + return "Opera" + operaMatcher.group(1); + } + // IE浏览器 + Matcher ieMatcher = IE_PATTERN.matcher(browser); + if (ieMatcher.find()) + { + return "Internet Explorer" + ieMatcher.group(1); + } + return UNKNOWN; + } + + /** + * 检测操作系统 + */ + private static String formatOperatingSystem(String operatingSystem) + { + // Windows系统 + Matcher windowsMatcher = WINDOWS_PATTERN.matcher(operatingSystem); + if (windowsMatcher.find()) + { + return "Windows" + getWindowsVersionDisplay(windowsMatcher.group(1)); + } + // macOS系统 + Matcher macMatcher = MACOS_PATTERN.matcher(operatingSystem); + if (macMatcher.find()) + { + String version = macMatcher.group(1).replace("_", "."); + return "macOS" + extractMajorVersion(version); + } + // Android系统 + Matcher androidMatcher = ANDROID_PATTERN.matcher(operatingSystem); + if (androidMatcher.find()) + { + return "Android" + extractMajorVersion(androidMatcher.group(1)); + } + // iOS系统 + Matcher iosMatcher = IOS_PATTERN.matcher(operatingSystem); + if (iosMatcher.find() && (operatingSystem.contains("iPhone") || operatingSystem.contains("iPad"))) + { + return "iOS" + extractMajorVersion(iosMatcher.group(1)); + } + // Linux系统 + if (LINUX_PATTERN.matcher(operatingSystem).find() && !operatingSystem.contains("Android")) + { + return "Linux"; + } + // Chrome OS + if (CHROMEOS_PATTERN.matcher(operatingSystem).find()) + { + return "Chrome OS"; + } + return UNKNOWN; + } + + /** + * 提取优化的主版本号 + */ + private static String extractMajorVersion(String fullVersion) + { + if (StringUtils.isEmpty(fullVersion)) + { + return StringUtils.EMPTY; + } + try + { + // 清理版本号中的非数字字符 + String cleanVersion = fullVersion.replaceAll("[^0-9.]", ""); + String[] parts = cleanVersion.split("\\."); + if (parts.length > 0) + { + String firstPart = parts[0]; + if (firstPart.matches("\\d+")) + { + int version = Integer.parseInt(firstPart); + + // 处理三位数版本号(如142 -> 14) + if (version >= 100) + { + return String.valueOf(version / 10); + } + return firstPart; + } + } + } + catch (NumberFormatException e) + { + // 版本号解析失败,返回原始值 + } + return fullVersion; + } + + /** + * Windows版本号显示优化 + */ + private static String getWindowsVersionDisplay(String version) + { + switch (version) + { + case "10.0": + return "10"; + case "6.3": + return "8.1"; + case "6.2": + return "8"; + case "6.1": + return "7"; + case "6.0": + return "Vista"; + case "5.1": + return "XP"; + case "5.0": + return "2000"; + default: + return extractMajorVersion(version); + } + } +} diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/AddressUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/AddressUtils.java new file mode 100644 index 00000000..6f5020ca --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/AddressUtils.java @@ -0,0 +1,51 @@ +package cn.g3soft.utils.ip; + +import cn.g3soft.utils.HttpUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + // IP地址查询 + public static final String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", "GBK"); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + + // 使用 Jackson 解析 JSON 字符串 + JsonNode obj = objectMapper.readTree(rspStr); + String region = obj.path("pro").asText(); + String city = obj.path("city").asText(); + + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip, e); + } + return UNKNOWN; + } +} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/IpUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/IpUtils.java new file mode 100644 index 00000000..bf3e9150 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ip/IpUtils.java @@ -0,0 +1,379 @@ +package cn.g3soft.utils.ip; + +import cn.g3soft.utils.ServletUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; + + +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (addr == null || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/resources/application.yml b/code/container-ocr/ocr-springboot/src/main/resources/application.yml index a6a9dc3f..830f72a8 100644 --- a/code/container-ocr/ocr-springboot/src/main/resources/application.yml +++ b/code/container-ocr/ocr-springboot/src/main/resources/application.yml @@ -13,4 +13,7 @@ spring: servlet: multipart: max-file-size: -1 - max-request-size: -1 \ No newline at end of file + max-request-size: -1 + jackson: + time-zone: "Asia/Shanghai" + date-format: yyyy-MM-dd HH:mm:ss \ No newline at end of file diff --git a/code/container-ocr/ocr-uniapp/env/.env b/code/container-ocr/ocr-uniapp/env/.env index 67c258c8..24a2090a 100644 --- a/code/container-ocr/ocr-uniapp/env/.env +++ b/code/container-ocr/ocr-uniapp/env/.env @@ -1,3 +1,3 @@ VITE_APP_PORT = 8081 -VITE_ALIYUN_BUCKET = 'g3hy' \ No newline at end of file +VITE_ALIYUN_BUCKET = 'g3hd' \ No newline at end of file diff --git a/code/container-ocr/ocr-vue/.env b/code/container-ocr/ocr-vue/.env index a32cb35f..bec357ec 100644 --- a/code/container-ocr/ocr-vue/.env +++ b/code/container-ocr/ocr-vue/.env @@ -12,4 +12,4 @@ PUBLIC_SERVER_PORT=8080 # 后端 API 基础地址 PUBLIC_API_BASE_URL=http://127.0.0.1:8085/api -PUBLIC_ALIYUN_BUCKET = g3hy \ No newline at end of file +PUBLIC_ALIYUN_BUCKET = g3hd diff --git a/code/container-ocr/ocr-vue/src/components/g3-file/index.vue b/code/container-ocr/ocr-vue/src/components/g3-file/index.vue index 5812f18f..bbea2dfc 100644 --- a/code/container-ocr/ocr-vue/src/components/g3-file/index.vue +++ b/code/container-ocr/ocr-vue/src/components/g3-file/index.vue @@ -32,7 +32,7 @@ import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api"; const props = defineProps({ father: { - type: [String, Number], + type: null, required: true, }, moduleId: { diff --git a/code/container-ocr/ocr-vue/src/layout/components/menu.vue b/code/container-ocr/ocr-vue/src/layout/components/menu.vue index d7e3cb48..d4bf30c8 100644 --- a/code/container-ocr/ocr-vue/src/layout/components/menu.vue +++ b/code/container-ocr/ocr-vue/src/layout/components/menu.vue @@ -50,6 +50,14 @@ const menuList = ref([ label: "模块配置", path: "/system/module", }, + { + label: "数据日志", + path: "/system/log/data", + }, + { + label: "登录日志", + path: "/system/log/login", + }, ], }, ]); diff --git a/code/container-ocr/ocr-vue/src/router/index.js b/code/container-ocr/ocr-vue/src/router/index.js index 786ecdeb..dff8221f 100644 --- a/code/container-ocr/ocr-vue/src/router/index.js +++ b/code/container-ocr/ocr-vue/src/router/index.js @@ -19,11 +19,19 @@ router.beforeEach(async (to) => { const userStore = useUserStore(); const permissionStore = usePermissionStore(); + if (to.path === "/login") { + if (userStore.isLogined()) { + return "/"; + } + return true; + } + if (!userStore.isLogined() && to.path !== "/login") { ElMessage.warning("登录已过期,请先登录"); return "/login"; } + // 检查权限 if (to.meta.moduleId) { await permissionStore.init(); @@ -35,6 +43,7 @@ router.beforeEach(async (to) => { } } + // 处理 tab 页路径 handleTabPath(to); return true; diff --git a/code/container-ocr/ocr-vue/src/router/routes.js b/code/container-ocr/ocr-vue/src/router/routes.js index 62043738..99aa9ce9 100644 --- a/code/container-ocr/ocr-vue/src/router/routes.js +++ b/code/container-ocr/ocr-vue/src/router/routes.js @@ -68,6 +68,22 @@ export const routes = [ moduleId: "s_awokemodule", }, }, + { + path: "/system/log/data", + component: () => import("@/views/system/log/data.vue"), + meta: { + title: "数据日志", + keepAlive: true, + }, + }, + { + path: "/system/log/login", + component: () => import("@/views/system/log/login.vue"), + meta: { + title: "登录日志", + keepAlive: true, + }, + }, // ==================== 个人中心 ==================== { path: "/user-center", diff --git a/code/container-ocr/ocr-vue/src/styles/element-plus.scss b/code/container-ocr/ocr-vue/src/styles/element-plus.scss index a7c09a8f..d8d70926 100644 --- a/code/container-ocr/ocr-vue/src/styles/element-plus.scss +++ b/code/container-ocr/ocr-vue/src/styles/element-plus.scss @@ -22,6 +22,8 @@ .el-loading-mask { &.is-fullscreen { + background: "rgba(0, 0, 0, 0.5)"; + .el-loading-spinner .path { stroke: #fff !important; } diff --git a/code/container-ocr/ocr-vue/src/utils/columns.js b/code/container-ocr/ocr-vue/src/utils/columns.js index 6621cc93..33884f49 100644 --- a/code/container-ocr/ocr-vue/src/utils/columns.js +++ b/code/container-ocr/ocr-vue/src/utils/columns.js @@ -47,7 +47,6 @@ export function updateViewTableColumns(tableRef) { return; } - const newColumns = columns .filter((item) => item.type === undefined && item.params && item.params.b_id) .map((item, index) => ({ @@ -119,3 +118,35 @@ export function formatValueByFormat(value, format) { return value; } + +/** + * 根据 format 格式化值 + * @param {any} value 原始值 + * @param {string} format 格式描述,如 money(2)、percent(1) + */ +export function formatValueByFullFormat(value, format) { + if (value === null || value === undefined || value === "") return ""; + + if (!format || typeof format !== "string") return value; + + const fmt = format.trim().toLowerCase(); + + // money(n) + if (fmt.startsWith("money")) { + const match = fmt.match(/^money\((\d+)\)$/); + const decimals = match ? Number(match[1]) : 2; + + const num = Number.parseFloat(value); + if (Number.isNaN(num)) return value; + + return num.toFixed(decimals); + } else if (fmt === "date") { + return formatToDate(value); + } else if (fmt === "datetime") { + return formatToDateTime(value); + } else if (fmt === "number") { + return Number(value) || 0; + } + + return value; +} diff --git a/code/container-ocr/ocr-vue/src/utils/excel.js b/code/container-ocr/ocr-vue/src/utils/excel.js new file mode 100644 index 00000000..d13f91e6 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/utils/excel.js @@ -0,0 +1,361 @@ +import ExcelJS from "exceljs"; +import { formatValueByFullFormat, getAlignByFormat } from "@/utils/columns"; +import { ElLoading, ElMessage } from "element-plus"; +import { getAliyunFileBlob } from "./file"; +import { saveAs } from "file-saver"; + +export async function ExportVxeTableExcel(tableRef) { + const $table = unref(tableRef); + + if (!$table) { + return; + } + + const loading = ElLoading.service({ + lock: true, + text: "正在下载Excel文件中,请稍候...", + }); + + // columns + const fullColumns = $table.getFullColumns(); + const exportColumns = buildColumns(fullColumns) || []; + const exportDatas = + $table.getCheckboxRecords().length > 0 ? $table.getCheckboxRecords() : $table.getFullData(); + + // const footerDatas = updateTableFooterData(exportColumns, exportDatas) + const footerDatas = []; + + const options = { + columns: exportColumns, + data: exportDatas, + }; + + try { + await exportExcel(options); + } catch (e) { + } finally { + loading.close(); + } +} + +async function urlToBase64(url) { + return new Promise(async (resolve, reject) => { + try { + const blob = await getAliyunFileBlob(url); + + // Create an object URL from the blob + const objectURL = URL.createObjectURL(blob); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + + img.crossOrigin = "Anonymous"; + img.onload = function () { + canvas.height = img.height; + canvas.width = img.width; + ctx.drawImage(img, 0, 0, img.width, img.height); + var dataURL = canvas.toDataURL("image/png"); + resolve(dataURL); + + // Optional: Revoke the object URL once done to free memory + URL.revokeObjectURL(objectURL); + }; + + img.src = objectURL; + } catch (error) { + reject(error); + } + }); +} + +function buildColumns(arr) { + return arr + .map((item) => { + // 先处理 children(关键!) + let children = []; + if (item.children && item.children.length > 0) { + children = buildColumns(item.children); + } + + // 判断当前节点是否需要保留 + const valid = item.type === undefined && item.field !== undefined; + + // 如果当前不合法 + 没有子节点,直接丢 + if (!valid && children.length === 0) { + return null; + } + + // 构建 column + const column = { + label: item.title, + width: item.params?.col_Width || item.renderWidth, + field: item.field, + type: item.params?.col_DisplayFormat, + }; + + // 有子节点就挂上 + if (children.length > 0) { + column.children = children; + } + + return column; + }) + .filter(Boolean); +} + +/** + * 导出Excel文件(支持多级表头、表尾、图片) + * @param {Object} options 导出配置 + * @param {String} fileName 文件名(默认:export.xlsx) + */ +export async function exportExcel(options, fileName = "export.xlsx") { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Sheet1"); + + // 1. 处理多级表头 + const { headerRows, leafColumns, mergeCells } = buildMultiLevelHeader(options.columns); + + // 写入表头行 + headerRows.forEach((row, rowIndex) => { + const headerRow = worksheet.addRow(row); + + // 设置表头样式(突出显示) + headerRow.eachCell((cell) => { + cell.style = { + font: { bold: true, size: 12, color: { argb: "FFFFFFFF" } }, + fill: { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FF4F81BD" }, // 蓝色背景 + }, + border: { + top: { style: "thin", color: { argb: "FF000000" } }, + left: { style: "thin", color: { argb: "FF000000" } }, + bottom: { style: "thin", color: { argb: "FF000000" } }, + right: { style: "thin", color: { argb: "FF000000" } }, + }, + alignment: { vertical: "middle", horizontal: "center" }, + }; + }); + }); + + // 合并表头单元格 + mergeCells.forEach((merge) => { + worksheet.mergeCells(merge.start.row, merge.start.col, merge.end.row, merge.end.col); + }); + + // 2. 写入数据行 + const dataStartRow = headerRows.length + 1; + + for (const [rowIndex, item] of options.data.entries()) { + const dataRow = worksheet.addRow([]); + + for (const [colIndex, col] of leafColumns.entries()) { + const cell = dataRow.getCell(colIndex + 1); + const value = getNestedValue(item, col.field); + + // 处理图片类型 + if (col.type === "image" && value) { + try { + + const base64 = await urlToBase64(value) + // 添加图片到工作簿 + const imageId = workbook.addImage({ + base64, + extension: "png", + }); + + // 计算图片位置(跳过表头行) + const imageRow = dataStartRow + rowIndex; + worksheet.addImage(imageId, { + tl: { col: colIndex, row: imageRow - 1 }, + ext: { width: col.imageWidth || 80, height: col.imageHeight || 80 }, + }); + + // 调整行高以适应图片 + dataRow.height = Math.max(dataRow.height || 20, (col.imageHeight || 80) * 0.75); + } catch (error) { + console.warn(`图片导出失败: ${error.message}`); + cell.value = "[图片]"; + } + } else { + cell.value = value; + } + + // 设置数据行样式 + cell.style = { + alignment: { vertical: "middle", horizontal: "left" }, + }; + } + } + + // 3. 写入表尾数据(突出显示) + if (options.footer && options.footer.length > 0) { + const footerStartRow = dataStartRow + options.data.length; + + options.footer.forEach((footerItem, rowIndex) => { + const footerRow = worksheet.addRow([]); + + leafColumns.forEach((col, colIndex) => { + const cell = footerRow.getCell(colIndex + 1); + const value = getNestedValue(footerItem, col.field); + cell.value = value; + + // 设置表尾样式(突出显示) + cell.style = { + font: { bold: true }, + // fill: { + // type: "pattern", + // pattern: "solid", + // // fgColor: { argb: "FFC00000" }, // 红色背景 + // }, + // border: { + // top: { style: "medium", color: { argb: "FF000000" } }, + // left: { style: "medium", color: { argb: "FF000000" } }, + // bottom: { style: "medium", color: { argb: "FF000000" } }, + // right: { style: "medium", color: { argb: "FF000000" } }, + // }, + alignment: { vertical: "middle", horizontal: "center" }, + }; + }); + }); + } + + // 4. 设置列宽 + leafColumns.forEach((col, index) => { + const column = worksheet.getColumn(index + 1); + column.width = col.width ? col.width / 7 : 20; // 转换像素为Excel单位 + }); + + // 5. 生成并保存文件 + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + saveAs(blob, fileName); +} + +/** + * 构建多级表头 + * @param {Array} columns 表头配置 + * @returns {Object} { headerRows, leafColumns, mergeCells } + */ +function buildMultiLevelHeader(columns) { + // 获取表头最大深度 + const maxDepth = getMaxDepth(columns); + + // 获取所有叶子节点列 + const leafColumns = getLeafColumns(columns); + + // 初始化表头二维数组 + const headerRows = Array.from({ length: maxDepth }, () => Array(leafColumns.length).fill("")); + + const mergeCells = []; + + // 递归填充表头 + function fillHeader(nodes, startCol = 0, depth = 0) { + let currentCol = startCol; + + nodes.forEach((node) => { + // 计算当前节点跨越的列数 + const colSpan = node.children ? getLeafCount(node.children) : 1; + + // 填充当前节点的表头 + headerRows[depth][currentCol] = node.label || ""; + + // 如果有子节点,递归处理 + if (node.children && node.children.length > 0) { + fillHeader(node.children, currentCol, depth + 1); + + // 如果跨多列,需要合并单元格 + if (colSpan > 1) { + mergeCells.push({ + start: { row: depth + 1, col: currentCol + 1 }, + end: { row: depth + 1, col: currentCol + colSpan }, + }); + } + } + + // 如果当前节点有父节点且不是叶子节点,需要纵向合并 + if (depth < maxDepth - 1 && !node.children) { + mergeCells.push({ + start: { row: depth + 1, col: currentCol + 1 }, + end: { row: maxDepth, col: currentCol + 1 }, + }); + } + + currentCol += colSpan; + }); + } + + fillHeader(columns); + + return { headerRows, leafColumns, mergeCells }; +} + +/** + * 获取树的最大深度 + */ +function getMaxDepth(nodes, currentDepth = 1) { + if (!nodes || nodes.length === 0) return currentDepth; + + let maxDepth = currentDepth; + nodes.forEach((node) => { + if (node.children && node.children.length > 0) { + const depth = getMaxDepth(node.children, currentDepth + 1); + maxDepth = Math.max(maxDepth, depth); + } + }); + + return maxDepth; +} + +/** + * 获取所有叶子节点列 + */ +function getLeafColumns(nodes, result = []) { + nodes.forEach((node) => { + if (node.children && node.children.length > 0) { + getLeafColumns(node.children, result); + } else { + result.push({ + field: node.field, + label: node.label, + width: node.width, + type: node.type, + imageWidth: node.imageWidth, + imageHeight: node.imageHeight, + }); + } + }); + + return result; +} + +/** + * 获取叶子节点数量 + */ +function getLeafCount(nodes) { + if (!nodes || nodes.length === 0) return 0; + + return nodes.reduce((count, node) => { + if (node.children && node.children.length > 0) { + return count + getLeafCount(node.children); + } + return count + 1; + }, 0); +} + +/** + * 获取嵌套对象的值 + */ +function getNestedValue(obj, path) { + if (!path) return ""; + + return ( + path.split(".").reduce((prev, curr) => { + return prev ? prev[curr] : ""; + }, obj) || "" + ); +} diff --git a/code/container-ocr/ocr-vue/src/utils/file.js b/code/container-ocr/ocr-vue/src/utils/file.js index 94a5a764..8b05fa72 100644 --- a/code/container-ocr/ocr-vue/src/utils/file.js +++ b/code/container-ocr/ocr-vue/src/utils/file.js @@ -203,14 +203,113 @@ export function getFileExtension(fileName) { return fileExt; } -// 上传到阿里云 -// blob fileName fileSize xh moduleid father cateid +export function downloadZip(files, fileName = "文件合集", callback) { + const zip = new JSZip(); + const addedFiles = new Set(); + + const tasks = files.map((item) => { + return getAliyunFileBlob(item.mx_mapfilename) + .then((res) => { + const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles); + zip.file(uniqueFileName, res, { binary: true }); + addedFiles.add(uniqueFileName); + }) + .catch((err) => { + // 捕获单个文件下载失败的错误,不影响其他文件下载 + console.error(`Failed to download file: ${item.mx_filename}`, err); + }); + }); + + // 关键:使用 Promise.all 确保所有任务完成 + return Promise.all(tasks) + .then(() => zip.generateAsync({ type: "blob" })) + .then((res) => { + saveAs(res, `${fileName}.zip`); + callback && callback(); + }) + .catch((err) => { + callback && callback(); + throw err; + }); +} + +// 处理文件名重复的函数 +function getUniqueFileName(fileName, addedFiles) { + let uniqueFileName = fileName; + let counter = 1; + + // 如果文件名已经存在,则在文件名后加上 (1), (2), ... + while (addedFiles.has(uniqueFileName)) { + const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, ""); + const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf(".")); + uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; + counter++; + } + + return uniqueFileName; +} + +// ============================================ 阿里云上传和下载 ============================================ + +let cachedCredentials = null; // 用来缓存凭证 +let isFetchingCredentials = false; // 控制是否正在请求凭证 + +// 获取阿里云文件的上传凭证 +export function getAliyunUploadCredentials() { + return new Promise((resolve, reject) => { + // 如果缓存中有有效的凭证,则直接使用 + if (cachedCredentials && !isCredentialsExpired(cachedCredentials)) { + resolve(cachedCredentials); // 凭证未过期,直接返回缓存的凭证 + } else { + // 如果正在请求凭证,则等待 + if (isFetchingCredentials) { + const interval = setInterval(() => { + // 等待直到凭证被请求完毕 + if (!isFetchingCredentials) { + clearInterval(interval); // 请求完成后停止轮询 + resolve(cachedCredentials); // 返回缓存的凭证 + } + }, 100); // 每100ms检查一次 + } else { + // 如果凭证过期,重新请求新的凭证 + isFetchingCredentials = true; // 设置为正在请求凭证状态 + getAliyunKeyApi() + .then((res) => { + if (res.success) { + const { accessid, policy, signature, expire, dir, host } = res.data; + + // 缓存凭证,并记录过期时间 + cachedCredentials = { + accessid, + policy, + signature, + expire: parseInt(expire, 10), + dir, + host, + }; + + isFetchingCredentials = false; // 请求完成,解除锁定 + resolve(cachedCredentials); // 返回新凭证 + } else { + isFetchingCredentials = false; // 请求失败,解除锁定 + reject(new Error("获取凭证失败")); + } + }) + .catch((err) => { + isFetchingCredentials = false; // 请求失败,解除锁定 + reject(new Error("请求凭证失败:" + err.message)); + }); + } + } + }); +} + +// 上传文件到阿里云 export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, father, cateid) { return new Promise((resolve, reject) => { - getAliyunKeyApi() - .then(async (res) => { + getAliyunUploadCredentials() + .then(async (aliyunKey) => { try { - const aliyunKey = res.data; const formData = new FormData(); let uniqueName = `${Date.now()}_${Math.random().toString(36).slice(2)}`; @@ -219,12 +318,17 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath uniqueName += `.${fileExt}`; } + // 构造上传的文件路径 formData.append("key", `${aliyunKey.dir}${uniqueName}`); formData.append("OSSAccessKeyId", aliyunKey.accessid); formData.append("policy", aliyunKey.policy); formData.append("Signature", aliyunKey.signature); formData.append("file", blob, uniqueName); + console.log(formData); + console.log(aliyunKey.host); + console.log(aliyunKey.dir); + const xhr = new XMLHttpRequest(); xhr.open("POST", aliyunKey.host, true); @@ -232,6 +336,7 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath if (xhr.status === 200 || xhr.status === 204) { const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`; + // 上传后处理(插入文件记录等) const insertRes = await insertFileApi({ father, mx_moduleid: moduleid, @@ -278,76 +383,35 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath }); } +// 检查凭证是否过期 +function isCredentialsExpired(credentials) { + const currentTime = Math.floor(Date.now() / 1000); + return currentTime > credentials.expire; +} + +// 获取阿里云文件的 Blob 数据(用于下载) export function getAliyunFileBlob(url) { return new Promise((resolve, reject) => { - // 创建一个独立的异步函数来处理 fetch 请求 - async function fetchFile() { - try { - const response = await fetch(url, { mode: "cors" }); + getAliyunUploadCredentials() + .then((aliyunKey) => { + const downloadUrl = `${url}?OSSAccessKeyId=${aliyunKey.accessid}&policy=${aliyunKey.policy}&Signature=${aliyunKey.signature}`; - // 检查响应状态 - if (!response.ok) { - reject(new Error(`下载失败:${response.status}`)); - return; - } - - // 获取文件内容 - const blob = await response.blob(); - - // 返回blob - resolve(blob); - } catch (error) { - reject(error); - } - } - - // 调用异步函数 - fetchFile(); - }); -} - -export function downloadZip(files, fileName = "文件合集", callback) { - const zip = new JSZip(); - const addedFiles = new Set(); - - const tasks = files.map((item) => { - return getAliyunFileBlob(item.mx_mapfilename) - .then((res) => { - const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles); - zip.file(uniqueFileName, res, { binary: true }); - addedFiles.add(uniqueFileName); + fetch(downloadUrl, { mode: "cors" }) + .then((response) => { + if (!response.ok) { + throw new Error(`下载失败,状态码:${response.status}`); + } + return response.blob(); + }) + .then((blob) => { + resolve(blob); + }) + .catch((error) => { + reject(error); + }); }) - .catch((err) => { - // 捕获单个文件下载失败的错误,不影响其他文件下载 - console.error(`Failed to download file: ${item.mx_filename}`, err); - }); + .catch(reject); }); - - // 关键:使用 Promise.all 确保所有任务完成 - return Promise.all(tasks) - .then(() => zip.generateAsync({ type: "blob" })) - .then((res) => { - saveAs(res, `${fileName}.zip`); - callback && callback(); - }) - .catch((err) => { - callback && callback(); - throw err; - }); } -// 处理文件名重复的函数 -function getUniqueFileName(fileName, addedFiles) { - let uniqueFileName = fileName; - let counter = 1; - - // 如果文件名已经存在,则在文件名后加上 (1), (2), ... - while (addedFiles.has(uniqueFileName)) { - const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, ""); - const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf(".")); - uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; - counter++; - } - - return uniqueFileName; -} +// ============================================ 阿里云上传和下载 ============================================ diff --git a/code/container-ocr/ocr-vue/src/views/auth/login.vue b/code/container-ocr/ocr-vue/src/views/auth/login.vue index 5a505ceb..4d2b1e1f 100644 --- a/code/container-ocr/ocr-vue/src/views/auth/login.vue +++ b/code/container-ocr/ocr-vue/src/views/auth/login.vue @@ -100,8 +100,6 @@ async function onVcodeSuccess() { res.data.token, ); - await permissionStore.init(); - ElMessage.success("登录成功"); router.push("/"); diff --git a/code/container-ocr/ocr-vue/src/views/container/index.vue b/code/container-ocr/ocr-vue/src/views/container/index.vue index 859fda15..7060ee74 100644 --- a/code/container-ocr/ocr-vue/src/views/container/index.vue +++ b/code/container-ocr/ocr-vue/src/views/container/index.vue @@ -8,12 +8,13 @@ import { } from "@/request/api"; import { getAlignByFormat, formatValueByFormat } from "@/utils/columns"; import { updateViewTableColumns } from "@/utils/columns"; -import { downloadZip } from "@/utils/file"; +import { downloadZip, getAliyunFileBlob } from "@/utils/file"; import { formatToDate } from "@/utils/time"; import { ElLoading, ElMessage } from "element-plus"; import { saveAs } from "file-saver"; import JSZip from "jszip"; import { usePermissionStore } from "@/store/permission"; +import { ExportVxeTableExcel, exportExcel } from "@/utils/excel"; const tableRef = ref(); @@ -56,9 +57,7 @@ const viewName = "v_container"; const showPreview = ref(false); const previewIndex = ref(-1); const previewImageList = computed(() => { - return tableData.value - .map((item) => item.b_container_img) - .filter((img) => img); + return tableData.value.map((item) => item.b_container_img).filter((img) => img); }); const currentRow = ref({}); @@ -191,12 +190,9 @@ async function deleteRows(rows = []) { const safeDeleteIds = deleteIds.map((id) => Number(id)); // 使用安全的 SQL 查询 - const fileRes = await loadDataApi( - "bf_file", - `father IN (${safeDeleteIds.join(",")})`, - "", - ["subid"], - ); + const fileRes = await loadDataApi("bf_file", `father IN (${safeDeleteIds.join(",")})`, "", [ + "subid", + ]); if (!fileRes.success) { return; @@ -272,11 +268,7 @@ async function handleDownloadOne(data) { }); try { - const fileRes = await loadDataApi( - "bf_file", - `father = ${data.b_id}`, - "mx_xh asc", - ); + const fileRes = await loadDataApi("bf_file", `father = ${data.b_id}`, "mx_xh asc"); if (!fileRes.success) { ElMessage.error("获取附件失败"); @@ -284,11 +276,7 @@ async function handleDownloadOne(data) { } const downloadFiles = fileRes.data.map((item, index) => ({ - mx_filename: buildFileName( - data.b_container_no, - index + 1, - item.mx_filename, - ), + mx_filename: buildFileName(data.b_container_no, index + 1, item.mx_filename), mx_mapfilename: item.mx_mapfilename, })); @@ -349,29 +337,19 @@ async function handleDownloadBatch() { let total = 0; let finished = 0; - // 👉 先统计总文件数(用于进度) for (const item of records) { - const fileRes = await loadDataApi( - "bf_file", - `father = '${item.b_id}'`, - "", - ); + const fileRes = await loadDataApi("bf_file", `father = '${item.b_id}'`, ""); if (fileRes.success) { total += (fileRes.data || []).length; } } - // 👉 正式处理 for (const item of records) { const folderName = `${item.b_container_no}_${formatToDate(item.b_inputdatetime)}`; const folder = zip.folder(folderName); - const fileRes = await loadDataApi( - "bf_file", - `father = '${item.b_id}'`, - "mx_xh asc", - ); + const fileRes = await loadDataApi("bf_file", `father = '${item.b_id}'`, "mx_xh asc"); if (!fileRes.success) { ElMessage.error("获取附件失败"); @@ -384,12 +362,10 @@ async function handleDownloadBatch() { const fileUrl = file.mx_mapfilename; try { - const res = await fetch(fileUrl); - const blob = await res.blob(); + const blob = getAliyunFileBlob(fileUrl); const fileName = - buildFileName(item.b_container_no, index + 1, file.mx_filename) || - "未命名文件"; + buildFileName(item.b_container_no, index + 1, file.mx_filename) || "未命名文件"; folder.file(fileName, blob); } catch (e) { @@ -401,11 +377,9 @@ async function handleDownloadBatch() { } }); - // 控制并发数 await runWithLimit(tasks, 3); } - // 👉 生成 zip loading.setText("正在生成压缩包..."); const content = await zip.generateAsync({ type: "blob" }); @@ -425,6 +399,110 @@ function buildFileName(containerNo, index, fileName) { return `${containerNo}_${formattedIndex}.${fileName.split(".").pop()}`; } + +function testExport() { + + const options = { + columns: [ + { + label: "基础信息", + field: "basicInfo", + width: 120, + children: [ + { + label: "姓名", + field: "name", + width: 120, + }, + { + label: "年龄", + field: "age", + width: 80, + }, + { + label: "头像", + field: "avatar", + width: 100, + type: "image", + imageWidth: 80, + imageHeight: 80, + }, + ], + }, + { + label: "联系方式", + field: "contactInfo", + width: 160, + children: [ + { + label: "电话", + field: "phone", + width: 120, + }, + { + label: "邮箱", + field: "email", + width: 200, + }, + ], + }, + { + label: "其他信息", + field: "otherInfo", + width: 100, + children: [ + { + label: "地址信息", + field: "address", + width: 150, + children: [ + { label: "省份", field: "province", width: 80 }, + { label: "城市", field: "city", width: 80 }, + ], + }, + { label: "备注", field: "remark", width: 200 }, + ], + }, + ], + data: [ + { + name: "张三", + age: 25, + phone: "13800138000", + email: "zhangsan@example.com", + province: "浙江", + city: "杭州", + remark: "测试数据1", + avatar: + "data:image/png;base64,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", + }, + { + name: "李四", + age: 30, + phone: "13900139000", + email: "lisi@example.com", + province: "江苏", + city: "南京", + remark: "测试数据2", + avatar: + "data:image/png;base64,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", + }, + ], + footer: [ + { + name: "总计", + age: 55, + phone: "", + email: "", + province: "", + city: "", + remark: "2条记录", + }, + ], + }; + + exportExcel(options); +} @@ -563,15 +641,10 @@ function buildFileName(containerNo, index, fileName) { trigger: 'cell', }" height="100%" + show-overflow + show-header-overflow > - + diff --git a/code/container-ocr/ocr-vue/src/views/system/log/data.vue b/code/container-ocr/ocr-vue/src/views/system/log/data.vue new file mode 100644 index 00000000..a3cdd0d2 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/views/system/log/data.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/code/container-ocr/ocr-vue/src/views/system/log/login.vue b/code/container-ocr/ocr-vue/src/views/system/log/login.vue new file mode 100644 index 00000000..406f76c8 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/views/system/log/login.vue @@ -0,0 +1,310 @@ + + + + +