This commit is contained in:
oneao committed 2026-03-28 17:22:49 +08:00
1 parent ce466130c0
commit 9c026f00ad
36 files changed
+2145 -951

No files matched your search

+11
View File
@@ -83,6 +83,17 @@
<artifactId>oshi-core</artifactId>
<version>6.10.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -7,12 +7,14 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DruidDataSourceAutoConfigure.class ,
})
@EnableAsync
@EnableAsync // 异步
@EnableScheduling // 定时任务
public class ApiApplication {
public static void main(String[] args){
// 初始化雪花id
@@ -0,0 +1,46 @@
package cn.g3soft.config;
import cn.g3soft.utils.PathUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "file")
public class FileConfig {
private String baseDir;
private String tempDir;
private String containerDir;
private String staticPath;
private String serverUrl;
public String getTempPath() {
return PathUtils.joinFilePath(baseDir, tempDir);
}
public String getContainerPath(){
return PathUtils.joinFilePath(baseDir,containerDir);
}
public String buildTempFilePath(String fileName) {
return PathUtils.joinFilePath(baseDir, tempDir, fileName);
}
public String getStaticUrlPrefix() {
return PathUtils.joinUrl(serverUrl, staticPath);
}
public String buildTempUrl(String fileName) {
return PathUtils.joinUrl(serverUrl, staticPath, tempDir, fileName);
}
public String buildFileUrl(String relativePath) {
return PathUtils.joinUrl(serverUrl, staticPath, relativePath);
}
}
@@ -3,28 +3,38 @@ package cn.g3soft.config;
import cn.g3soft.interceptor.AuthInterceptor;
import cn.g3soft.interceptor.DataSourceInterceptor;
import cn.g3soft.interceptor.RequestWrapperFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private FileConfig fileConfig;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/login",
"/error"
"/error",
"/static/**"
);
registry.addInterceptor(new DataSourceInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/login",
"/error"
"/error",
"/static/**"
);
}
@@ -33,7 +43,13 @@ public class WebMvcConfig implements WebMvcConfigurer {
FilterRegistrationBean<RequestWrapperFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestWrapperFilter());
registration.addUrlPatterns("/*");
registration.setOrder(1); // 一定要靠前
registration.setOrder(1);
return registration;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(fileConfig.getStaticPath() + "/**")
.addResourceLocations("file:" + fileConfig.getUploadDir() + "/");
}
}
@@ -7,8 +7,10 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -18,123 +20,26 @@ import java.util.Map;
public class FileController {
@Resource
private DbUtils dbUtils;
@Resource
private FileUploadUtils fileUploadUtils;
@PostMapping("/getAliyunKey")
public Result<Map<String,Object>> getAliyunKey(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "bucketName");
String bucketName = (String) params.get("bucketName");
String orgId = UserContext.get().getOrgId();
String requestUrl = "http://g3oss.g3soft.cn:8082/upload?orgid=" + orgId + "&bucket=" + bucketName;
String reqResult = HttpUtils.sendGet(requestUrl);
Map<String, Object> objectMap = JsonUtils.toMap(reqResult);
return Result.ok(objectMap);
}
@PostMapping("/insert")
@Transactional(rollbackFor = Exception.class)
public Result<Long> insertFile(@RequestBody Map<String,Object> params) {
ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid");
String userId = UserContext.get().getUserId();
long nextId = UniqueId.nextId();
params.put("subid", nextId);
params.put("mx_inputuser_id",userId);
params.put("mx_inputdatetime", TimeUtils.getDateTime());
dbUtils.insert("bf_file","subid",params);
return Result.ok(nextId);
}
@PostMapping("/delete")
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
String tableName = params.get("tableName").toString();
String bucketName = params.get("bucketName").toString();
String id = params.get("subid").toString();
// 查询数据
List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
// 如果没有数据,直接返回删除成功
if (ObjectUtils.isEmpty(maps)) {
return Result.ok("删除成功");
}
String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
String orgId = UserContext.get().getOrgId();
int index = mxMapfilename.indexOf(orgId + "/");
// 如果没找到,就转换orgId为大写
if (index < 0) {
orgId = orgId.toUpperCase();
index = mxMapfilename.indexOf(orgId + "/");
}
String objectName = mxMapfilename.substring(index);
// 删除记录
Map<String, Object> deleteParams = new HashMap<>();
deleteParams.put("subid", id);
int deleteCount = dbUtils.delete(tableName, deleteParams);
// 如果删除成功,发送删除请求
if (deleteCount > 0) {
if(StringUtils.isBlank(objectName)){
return Result.fail("删除失败,文件格式有误");
}
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
/**
* 上传临时文件接口
*/
@PostMapping("/uploadTemp")
public Result<Map<String, String>> uploadTemp(@RequestParam("file") MultipartFile file,
@RequestParam(value = "fileName") String fileName) {
Map<String, String> response = new HashMap<>();
try {
HttpUtils.sendGet(requestUrl);
} catch (Exception e) {
throw new RuntimeException("删除文件失败",e);
// 调用工具类上传临时文件,返回临时 URL
String tempUrl = fileUploadUtils.uploadTemp(file,fileName);
// 返回临时文件 URL
response.put("tempUrl", tempUrl);
return Result.ok(response);
} catch (IOException e) {
return Result.fail("文件上传失败:" + e.getMessage());
}
}
return Result.ok();
}
@PostMapping("/deleteAliyunFile")
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteAliyunFile(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "bucketName", "mx_mapfilename");
String bucketName = params.get("bucketName").toString();
String mxMapfilename = params.get("mx_mapfilename").toString();
String orgId = UserContext.get().getOrgId();
int index = mxMapfilename.indexOf(orgId + "/");
// 如果没找到,就转换orgId为大写
if (index < 0) {
orgId = orgId.toUpperCase();
index = mxMapfilename.indexOf(orgId + "/");
}
String objectName = mxMapfilename.substring(index);
if(StringUtils.isBlank(objectName)){
return Result.fail("删除失败,文件格式有误");
}
// 发送删除请求
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
System.out.println(requestUrl);
try {
HttpUtils.sendGet(requestUrl);
} catch (Exception e) {
throw new RuntimeException("删除文件失败",e);
}
return Result.ok();
}
}
@@ -33,7 +33,7 @@ public class OcrController {
reqPayload.put("visualize", false);
try {
String response = HttpUtils.sendPostJson(requestUrl, reqPayload);
String response = HttpUtils.postJson(requestUrl, reqPayload);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response);
@@ -0,0 +1,140 @@
package cn.g3soft.controller;
import cn.g3soft.context.UserContext;
import cn.g3soft.utils.*;
import cn.g3soft.utils.uniqueId.idgen.UniqueId;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/file")
public class OssController {
@Resource
private DbUtils dbUtils;
@PostMapping("/getAliyunKey")
public Result<Map<String,Object>> getAliyunKey(@RequestBody Map<String, Object> params) throws Exception {
ValidationUtils.checkRequiredFields(params, "bucketName");
String bucketName = (String) params.get("bucketName");
String orgId = UserContext.get().getOrgId();
String requestUrl = "http://g3oss.g3soft.cn:8082/upload?orgid=" + orgId + "&bucket=" + bucketName;
String reqResult = HttpUtils.get(requestUrl);
Map<String, Object> objectMap = JsonUtils.toMap(reqResult);
return Result.ok(objectMap);
}
@PostMapping("/insert")
@Transactional(rollbackFor = Exception.class)
public Result<Long> insertFile(@RequestBody Map<String,Object> params) {
ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid");
String userId = UserContext.get().getUserId();
long nextId = UniqueId.nextId();
params.put("subid", nextId);
params.put("mx_inputuser_id",userId);
params.put("mx_inputdatetime", TimeUtils.getDateTime());
dbUtils.insert("bf_file","subid",params);
return Result.ok(nextId);
}
@PostMapping("/delete")
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
String tableName = params.get("tableName").toString();
String bucketName = params.get("bucketName").toString();
String id = params.get("subid").toString();
// 查询数据
List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
// 如果没有数据,直接返回删除成功
if (ObjectUtils.isEmpty(maps)) {
return Result.ok("删除成功");
}
String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
String orgId = UserContext.get().getOrgId();
int index = mxMapfilename.indexOf(orgId + "/");
// 如果没找到,就转换orgId为大写
if (index < 0) {
orgId = orgId.toUpperCase();
index = mxMapfilename.indexOf(orgId + "/");
}
String objectName = mxMapfilename.substring(index);
// 删除记录
Map<String, Object> deleteParams = new HashMap<>();
deleteParams.put("subid", id);
int deleteCount = dbUtils.delete(tableName, deleteParams);
// 如果删除成功,发送删除请求
if (deleteCount > 0) {
if(StringUtils.isBlank(objectName)){
return Result.fail("删除失败,文件格式有误");
}
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
try {
HttpUtils.get(requestUrl);
} catch (Exception e) {
throw new RuntimeException("删除文件失败",e);
}
}
return Result.ok();
}
@PostMapping("/deleteAliyunFile")
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteAliyunFile(@RequestBody Map<String, Object> params) {
ValidationUtils.checkRequiredFields(params, "bucketName", "mx_mapfilename");
String bucketName = params.get("bucketName").toString();
String mxMapfilename = params.get("mx_mapfilename").toString();
String orgId = UserContext.get().getOrgId();
int index = mxMapfilename.indexOf(orgId + "/");
// 如果没找到,就转换orgId为大写
if (index < 0) {
orgId = orgId.toUpperCase();
index = mxMapfilename.indexOf(orgId + "/");
}
String objectName = mxMapfilename.substring(index);
if(StringUtils.isBlank(objectName)){
return Result.fail("删除失败,文件格式有误");
}
// 发送删除请求
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
System.out.println(requestUrl);
try {
HttpUtils.get(requestUrl);
} catch (Exception e) {
throw new RuntimeException("删除文件失败",e);
}
return Result.ok();
}
}
@@ -0,0 +1,44 @@
package cn.g3soft.schedule;
import cn.g3soft.config.FileConfig;
import cn.g3soft.utils.PathUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
@Component
public class TempFIleSchedule {
@Resource
private FileConfig fileConfig;
/**
* 定时清理临时文件(临时文件超过1天后清理)
*/
@Scheduled(cron = "0 0 3 * * ?") // 每天3点清理
public void cleanTempFiles() {
File tempDir = new File(PathUtils.joinFilePath(fileConfig.getTempPath()));
if (tempDir.exists()) {
File[] files = tempDir.listFiles();
if (files != null) {
for (File file : files) {
if (isFileExpired(file)) {
file.delete();
}
}
}
}
}
/**
* 判断文件是否过期(假设超过1天的文件删除)
* @param file 文件
* @return 是否过期
*/
private boolean isFileExpired(File file) {
long lastModified = file.lastModified();
long now = System.currentTimeMillis();
return now - lastModified > 86400000L; // 1天的毫秒数
}
}
@@ -0,0 +1,99 @@
package cn.g3soft.utils;
import cn.g3soft.config.FileConfig;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* 通用文件上传工具类
* 规则:
* - 文件名保持原始或指定
* - 同一天同箱号重复 -> 自动生成 _1/_2/... 文件夹
* - 使用 FileUtils 拼接路径
*/
@Component
public class FileContainerUtils {
@Resource
private FileConfig fileConfig;
/**
* 单文件上传
*/
public String upload(MultipartFile file, String containerNo, String fileName) throws IOException {
if (file == null || file.isEmpty()) {
throw new RuntimeException("文件不能为空");
}
// 1. 日期路径 yyyy/MM/dd
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
// 2. 基础目录:上传目录 + 日期
String basePath = PathUtils.joinFilePath(fileConfig.getContainerPath(), datePath);
// 3. 计算最终目录(箱号 + 重复批次)
File finalDir = resolveContainerDir(basePath, containerNo);
if (!finalDir.exists()) {
finalDir.mkdirs();
}
// 4. 最终文件
File dest = new File(finalDir, fileName);
file.transferTo(dest);
// 5. 相对路径,用于生成URL
String relativePath = PathUtils.joinFilePath(
datePath,
finalDir.getName(),
fileName
);
return fileConfig.buildFileUrl(relativePath);
}
/**
* 多文件上传
*/
public List<String> uploadBatch(MultipartFile[] files, String containerNo, String[] fileNames) throws IOException {
if (files == null || files.length == 0) {
return new ArrayList<>();
}
List<String> urls = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
String name = (fileNames != null && i < fileNames.length) ? fileNames[i] : files[i].getOriginalFilename();
urls.add(upload(files[i], containerNo, name));
}
return urls;
}
/**
* 解析容器目录
* - 如果同一天已存在该箱号目录 -> 自动加 _1/_2/... 避免覆盖
*/
private File resolveContainerDir(String basePath, String containerNo) {
File containerDir = new File(PathUtils.joinFilePath(basePath, containerNo));
if (!containerDir.exists()) {
return containerDir;
}
// 已存在 -> 找下一个可用序号
int i = 1;
while (true) {
File dir = new File(PathUtils.joinFilePath(basePath, containerNo + "_" + i));
if (!dir.exists()) {
return dir;
}
i++;
}
}
}
@@ -0,0 +1,87 @@
package cn.g3soft.utils;
import cn.g3soft.config.FileConfig;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
public class FileUploadUtils {
@Resource
private FileConfig fileConfig;
/**
* 上传文件到临时路径
* @param file 上传的文件
* @param fileName 如果前端传递了文件名,使用传递的文件名
* @return 临时文件的URL
*/
public String uploadTemp(MultipartFile file, String fileName) throws IOException {
// 1. 生成最终文件名
String finalFileName = generateTempFileName(file, fileName);
// 2. 临时目录路径
File tempDir = new File(fileConfig.getTempPath());
// 如果临时目录不存在,创建目录
if (!tempDir.exists()) {
tempDir.mkdirs();
}
// 3. 保存文件到临时目录
File tempFile = new File(tempDir, finalFileName);
file.transferTo(tempFile);
// 4. 返回临时文件的 URL(供 OCR 使用)
return fileConfig.buildTempUrl(finalFileName);
}
/**
* 根据文件名生成临时文件名(带时间戳 + UUID + 文件后缀)
* @param file 上传的文件
* @param fileName 传递的文件名(可选)
* @return 生成的最终文件名
*/
private String generateTempFileName(MultipartFile file, String fileName) {
// 优先使用前端传递的 fileName
String originalFilename = (fileName != null && !fileName.isEmpty())
? fileName
: file.getOriginalFilename();
// 如果没有获取到文件名,设置一个默认文件名
if (originalFilename == null) {
originalFilename = "file";
}
// 获取文件后缀
String extension = "";
if (originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
// 生成带时间戳和 UUID 的文件名
return LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "_"
+ UUID.randomUUID().toString().replace("-", "") + extension;
}
/**
* 删除临时文件(成功识别后删除)
* @param tempFileName 临时文件的文件名
*/
public void deleteTempFile(String tempFileName) {
String filePath = PathUtils.joinFilePath(fileConfig.getBaseDir(), fileConfig.getTempDir(), tempFileName);
File tempFile = new File(tempFileName);
if (tempFile.exists()) {
tempFile.delete();
}
}
}
@@ -0,0 +1,117 @@
package cn.g3soft.utils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
public class FileUtils {
/**
* =========================
* 最简调用:自动生成文件名 + 不覆盖
* =========================
*/
public static String upload(MultipartFile file, String uploadPath) throws IOException {
return upload(file, uploadPath, null, false);
}
/**
* =========================
* 指定文件名 + 不覆盖
* =========================
*/
public static String upload(MultipartFile file,
String uploadPath,
String fileName) throws IOException {
return upload(file, uploadPath, fileName, false);
}
/**
* =========================
* 控制是否覆盖 + 自动生成文件名
* =========================
*/
public static String upload(MultipartFile file,
String uploadPath,
boolean overwrite) throws IOException {
return upload(file, uploadPath, null, overwrite);
}
/**
* =========================
* 核心方法(唯一执行逻辑)
* =========================
*/
public static String upload(MultipartFile file,
String uploadPath,
String fileName,
boolean overwrite) throws IOException {
// 1. 校验文件
if (file == null || file.isEmpty()) {
throw new RuntimeException("上传文件不能为空");
}
// 2. 创建目录
File dir = new File(uploadPath);
if (!dir.exists()) {
boolean created = dir.mkdirs();
if (!created) {
throw new RuntimeException("目录创建失败: " + uploadPath);
}
}
// 3. 获取原始文件名
String originalName = file.getOriginalFilename();
// 4. 处理文件名
if (fileName == null || fileName.trim().isEmpty()) {
fileName = generateFileName(originalName);
} else {
// 如果用户传了文件名,但没有后缀,则补上
if (!fileName.contains(".") && originalName != null && originalName.contains(".")) {
fileName = fileName + getSuffix(originalName);
}
}
// 5. 目标文件
File dest = new File(dir, fileName);
// 6. 处理覆盖逻辑
if (dest.exists() && !overwrite) {
String newName = generateFileName(fileName);
dest = new File(dir, newName);
fileName = newName;
}
// 7. 保存文件
file.transferTo(dest);
// 8. 返回文件路径(你也可以改成返回 URL)
return dest.getAbsolutePath();
}
/**
* =========================
* 生成唯一文件名
* =========================
*/
private static String generateFileName(String originalName) {
return UUID.randomUUID().toString().replace("-", "")
+ getSuffix(originalName);
}
/**
* =========================
* 获取文件后缀
* =========================
*/
private static String getSuffix(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf("."));
}
}
@@ -1,29 +1,12 @@
package cn.g3soft.utils;
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.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;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
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;
import okhttp3.*;
/**
* 通用http发送方法
@@ -32,287 +15,92 @@ import org.springframework.http.MediaType;
*/
public class HttpUtils
{
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url)
{
return sendGet(url, StringUtils.EMPTY);
}
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
.build();
/**
* 向指定 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");
}
private static final MediaType JSON = MediaType.get("application/json; charset=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();
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();
}
}
catch (Exception ex)
{
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
// ==================== GET ====================
public static String get(String url) throws Exception {
Request request = new Request.Builder()
.url(url)
.get()
.build();
/**
* 向指定 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);
}
/**
* 向指定 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();
}
/**
* 向指定 URL 发送 POST 请求,内容类型为 JSON
*
* @param url 请求的 URL
* @param reqPayload 请求参数,Map 类型
* @return 响应结果
*/
public static String sendPostJson(String url, Map<String, Object> 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 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[] {};
try (Response response = client.newCall(request).execute()) {
return response.body() != null ? response.body().string() : null;
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier
{
@Override
public boolean verify(String hostname, SSLSession session)
{
return true;
public static String get(String url, Map<String, Object> params, String contentType) throws Exception {
HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder();
if (params != null) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() != null) {
urlBuilder.addQueryParameter(
entry.getKey(),
String.valueOf(entry.getValue())
);
}
}
}
Request.Builder builder = new Request.Builder()
.url(urlBuilder.build())
.get();
if (contentType != null && !contentType.isEmpty()) {
builder.addHeader("Content-Type", contentType);
}
try (Response response = client.newCall(builder.build()).execute()) {
return response.body() != null ? response.body().string() : null;
}
}
// ==================== POST FORM ====================
public static String postForm(String url, Map<String, String> params) throws Exception {
FormBody.Builder builder = new FormBody.Builder();
for (Map.Entry<String, String> entry : params.entrySet()) {
builder.add(entry.getKey(), entry.getValue());
}
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
try (Response response = client.newCall(request).execute()) {
return response.body() != null ? response.body().string() : null;
}
}
// ==================== POST JSON ====================
// ==================== POST JSON(Map版本)====================
public static String postJson(String url, Map<String, Object> params) throws Exception {
// 1. Map 转 JSON 字符串
String json = MAPPER.writeValueAsString(params);
// 2. 构建请求体
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
// 3. 执行请求
try (Response response = client.newCall(request).execute()) {
return response.body() != null ? response.body().string() : null;
}
}
}
@@ -0,0 +1,42 @@
package cn.g3soft.utils;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathUtils {
public static String joinUrl(String... parts) {
if (parts == null || parts.length == 0) return "";
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (part == null || part.trim().isEmpty()) continue;
part = part.trim();
if (sb.length() == 0) {
// 第一段:保留协议(http://)
sb.append(part.replaceAll("/+$", ""));
} else {
sb.append("/");
sb.append(part.replaceAll("^/+", "").replaceAll("/+$", ""));
}
}
return sb.toString();
}
public static String joinFilePath(String... paths) {
if (paths == null || paths.length == 0) return "";
Path result = null;
for (String p : paths) {
if (p == null || p.trim().isEmpty()) continue;
if (result == null) {
result = Paths.get(p.trim());
} else {
result = result.resolve(p.trim());
}
}
return (result == null) ? "" : result.normalize().toString();
}
}
@@ -7,6 +7,9 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class AddressUtils
{
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
@@ -28,7 +31,12 @@ public class AddressUtils
try
{
String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", "GBK");
Map<String,Object> params = new HashMap<>();
params.put("ip",ip);
params.put("json",true);
String rspStr = HttpUtils.get(IP_URL, params, "GBK");
if (StringUtils.isEmpty(rspStr))
{
log.error("获取地理位置异常 {}", ip);
@@ -17,3 +17,11 @@ spring:
jackson:
time-zone: "Asia/Shanghai"
date-format: yyyy-MM-dd HH:mm:ss
file:
static-path: /static/file
server-url: http://192.168.1.137:80/api
base-dir: D:/data
container-dir: container
temp-dir: temp
@@ -3,8 +3,8 @@ username=sa
password=zhang520..
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
# url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3BoxOCR
# username=sa
# password=123456@aaa
# driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
#url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3BoxOCR
#username=sa
#password=123456@aaa
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
@@ -0,0 +1,84 @@
package cn.g3soft;
import cn.g3soft.utils.HttpUtils;
import cn.g3soft.utils.Result;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
public class DEMO {
@Test
public void testHello() throws Exception {
long startTime = System.nanoTime(); // 开始时间
String filePath = "D:\\100全关门-3\\20241005082220A664.jpg"; // 你的图片路径
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
// String fileUrl = Base64.getEncoder().encodeToString(fileBytes);
String fileUrl = "http://g3hd.oss-cn-hangzhou.aliyuncs.com/g3hd/2026/03/28/1774665878210_960.jpg";
String requestUrl = "http://127.0.0.1:8867/ocr";
Map<String, Object> reqPayload = new HashMap<>();
reqPayload.put("file", fileUrl);
reqPayload.put("fileType", 1);
reqPayload.put("visualize", false);
try {
String response = HttpUtils.postJson(requestUrl, reqPayload);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response);
JsonNode ocrResults = root.path("result").path("ocrResults");
if (!ocrResults.isArray() || ocrResults.isEmpty()) {
System.out.println("空");
}
String number = "";
double maxScore = -1.0;
for (JsonNode item : ocrResults) {
JsonNode pruned = item.path("prunedResult");
JsonNode recTexts = pruned.path("rec_texts");
JsonNode recScores = pruned.path("rec_scores");
for (int i = 0; i < recTexts.size(); i++) {
double score = recScores.get(i).asDouble();
if (score > maxScore) {
maxScore = score;
number = recTexts.get(i).asText();
}
}
}
Map<String, Object> result = new HashMap<>();
result.put("boxNumber", number);
result.put("score", maxScore);
} catch (Exception e) {
e.printStackTrace();
} finally {
long endTime = System.nanoTime(); // 结束时间
long durationMs = (endTime - startTime) / 1_000_000;
System.out.println("耗时: " + durationMs + " ms");
}
}
}
-1
View File
@@ -1,5 +1,4 @@
{
"editor.cursorSmoothCaretAnimation": "on",
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
+1 -1
View File
@@ -1,3 +1,3 @@
VITE_APP_PORT = 8081
VITE_APP_PORT = 8080
VITE_ALIYUN_BUCKET = 'g3hd'
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { useUserStore } from './store'
import PLATFORM from './utils/platform'
const userStore = useUserStore()
@@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage, uploadFileToAliyun } from '@/utils/file'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadTempFileApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
import { uploadToOss } from '@/utils/oss'
const boxImage = ref(null)
@@ -24,11 +25,17 @@ const subFileList = ref([])
const tipContainerNo = ref('')
onLoad(async () => {
toast.loading('正在初始化中...', {
overlay: true,
})
await getUniqueIdApi().then((res) => {
if (res.success) {
form.value.b_id = res.data
}
})
toast.hide()
})
function handleChooseBox() {
@@ -46,14 +53,11 @@ function handleChooseBox() {
})
imageCompress(tempFile.path).then((compressRes) => {
uploadFileToAliyun(compressRes.path, tempFile.name, 'image').then((res) => {
if (res.url) {
form.value.containerImageUrl = res.url
ocrFinish.value = false
console.log(compressRes)
uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
if (res.success) {
recognizeOcrBoxApi({
fileUrl: form.value.containerImageUrl,
fileUrl: res.data.tempUrl,
}).then((ocrRes) => {
if (ocrRes.success) {
toast.success('识别成功,请检查箱号是否正确')
@@ -132,31 +136,6 @@ function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
})
}
async function afterRead(fileItem: UploadFileItem) {
// 设置上传中状态
fileItem.status = 'uploading'
fileItem.message = '正在上传'
fileItem.isUpload = false
// 上传到阿里云
const res = await uploadFileToAliyun(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 上传完成
fileItem.status = 'done'
fileItem.message = '上传成功'
fileItem.url = res.url
subFileList.value = [...subFileList.value]
}
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
console.log(fileItem)
@@ -172,15 +151,13 @@ function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promis
}
function onRemove(index: number, item: UploadFileItem) {
if (!item.isUpload) {
return
}
if (item.status === 'done') {
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
})
}
}
async function handleConfirm() {
@@ -204,74 +181,76 @@ async function handleConfirm() {
transparent: false,
})
// 图片已经上传成功,保存主表即可
const mainTable = {
b_id: form.value.b_id,
b_container_no: form.value.containerNo,
b_container_img: form.value.containerImageUrl,
b_ocr_no: form.value.ocrNo,
b_ocr_score: form.value.ocrScore || 0.00,
}
//
const fileInserts = []
// // 图片已经上传成功,保存主表即可
// const mainTable = {
// b_id: form.value.b_id,
// b_container_no: form.value.containerNo,
// b_container_img: form.value.containerImageUrl,
// b_ocr_no: form.value.ocrNo,
// b_ocr_score: form.value.ocrScore || 0.00,
// }
const containerNo = form.value.containerNo
// const fileInserts = []
subFileList.value.forEach(async (fileItem, index) => {
const uniqueRes = await getUniqueIdApi()
const indexNumber = index + 2
// const containerNo = form.value.containerNo
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: indexNumber,
mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: fileItem.url,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
})
// subFileList.value.forEach(async (fileItem, index) => {
// const uniqueRes = await getUniqueIdApi()
// const indexNumber = index + 2
const uniqueRes = await getUniqueIdApi()
// fileInserts.push({
// subid: uniqueRes.data,
// father: form.value.b_id,
// mx_xh: indexNumber,
// mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
// mx_filesize: fileItem.file.size,
// mx_fileext: getFileExtension(fileItem.file.name),
// mx_mapfilename: fileItem.url,
// mx_moduleid: 'b_container',
// mx_cate_id: '1',
// })
// })
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: 0,
mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
mx_filesize: boxImage.value.size,
mx_fileext: getFileExtension(boxImage.value.name),
mx_mapfilename: form.value.containerImageUrl,
mx_moduleid: 'b_container',
mx_cate_id: '0',
})
// const uniqueRes = await getUniqueIdApi()
const reqData = [
{
tableName: 'b_container',
keyField: 'b_id',
inserts: [mainTable],
updates: [],
deletes: [],
},
{
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
deletes: [],
},
]
// fileInserts.push({
// subid: uniqueRes.data,
// father: form.value.b_id,
// mx_xh: 0,
// mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
// mx_filesize: boxImage.value.size,
// mx_fileext: getFileExtension(boxImage.value.name),
// mx_mapfilename: form.value.containerImageUrl,
// mx_moduleid: 'b_container',
// mx_cate_id: '0',
// })
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
tipContainerNo.value = form.value.containerNo
showResult.value = true
}
})
// const reqData = [
// {
// tableName: 'b_container',
// keyField: 'b_id',
// inserts: [mainTable],
// updates: [],
// deletes: [],
// },
// {
// tableName: 'bf_file',
// keyField: 'subid',
// inserts: fileInserts,
// updates: [],
// deletes: [],
// },
// ]
// saveObjectApi(reqData).then((res) => {
// if (res.success) {
// toast.success('保存成功')
// tipContainerNo.value = form.value.containerNo
// showResult.value = true
// }
// })
}
function buildFileName(containerNo, index, fileName) {
@@ -317,13 +296,15 @@ async function continueAdd() {
<template>
<view class="g3-container">
<template v-if="!showResult">
<view class="g3-content">
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
<template #title>
<sar-icon name="close" size="32rpx" style="margin-right: 16rpx;"/>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
集装箱号(OCR识别)
</text>
</view>
</template>
<view class="step-content box">
<view v-if="ocrFinish" class="header">
@@ -393,12 +374,19 @@ async function continueAdd() {
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" title="其他图片" border style="margin-top: 32rpx;">
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@@ -406,114 +394,14 @@ async function continueAdd() {
/>
</view>
</wd-cell-group>
<!-- <view class="g3-content">
<view class="step">
<view class="step-header">
<sar-tag theme="primary" size="large">
1
</sar-tag>
<view>集装箱号识别(OCR自动识别)</view>
</view>
<view class="step-content box">
<view v-if="ocrFinish" class="header ">
<sar-alert
v-if="!isError"
type="success"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别成功,请检查箱号是否正确!
<view>
准确率:{{ computedOcrScore }}
</view>
</sar-alert>
<sar-alert
v-else
type="danger"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别失败,请检查手动填写集装箱号
</sar-alert>
</view>
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="image" size="56rpx" color="#999" />
<text class="placeholder-text">
上传箱号图片,自动识别箱号
</text>
</view>
<view v-else class="box-image">
<wd-img
:src="boxImage.path"
width="100%"
height="100%"
:enable-preview="true"
/>
<view class="delete-btn" @click="handleDeleteBoxImage()">
<sar-icon name="close" size="20rpx" color="#fff" />
</view>
</view>
</view>
<view class="box-input">
<view class="header">
<view class="label-container">
<view>集装箱号</view>
<view class="required-asterisk">
*
</view>
</view>
<view class="format">
格式: 4字母 + 7数字
</view>
</view>
<sar-input
v-model="form.containerNo"
placeholder="请输入箱号"
/>
</view>
</view>
</view>
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
2、其它照片录入
</sar-tag>
</view>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</view>
</view>
<view class="g3-footer">
<view class="footer-main">
<view class="submit-btn" @click="handleConfirm">
确认上传
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
上传
</sar-button>
</view>
</view>
</view> -->
</template>
<view v-else class="g3-result">
@@ -526,7 +414,7 @@ async function continueAdd() {
</text>
</view>
</template>
<sar-button long style="width: 360rpx;font-size: 38rpx;" @click="continueAdd">
<sar-button long style="font-size: 38rpx;" @click="continueAdd">
继续录入
</sar-button>
</sar-result>
@@ -0,0 +1,627 @@
<script lang="ts" setup>
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadTempFileApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
import { uploadToOss } from '@/utils/oss'
const boxImage = ref(null)
const ocrFinish = ref(false)
const isError = ref(false)
const showResult = ref(false)
const form = ref({
b_id: 0,
containerNo: '',
containerImageUrl: '',
ocrScore: 0.00,
ocrNo: '',
})
const subFileList = ref([])
const tipContainerNo = ref('')
onLoad(async () => {
toast.loading('正在初始化中...', {
overlay: true,
})
await getUniqueIdApi().then((res) => {
if (res.success) {
form.value.b_id = res.data
}
})
toast.hide()
})
function handleChooseBox() {
uni.chooseImage({
count: 1,
success: (res) => {
if (Array.isArray(res.tempFiles) && res.tempFiles.length > 0) {
const tempFile: any = res.tempFiles[0]
boxImage.value = tempFile
toast.loading('OCR识别中,请稍等 . . .', {
overlay: true,
transparent: false,
})
imageCompress(tempFile.path).then((compressRes) => {
console.log(compressRes)
// uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
// console.log(res)
// })
uploadToOss(compressRes.path, tempFile.name, 'image').then((res) => {
if (res.url) {
form.value.containerImageUrl = res.url
ocrFinish.value = false
recognizeOcrBoxApi({
fileUrl: form.value.containerImageUrl,
}).then((ocrRes) => {
if (ocrRes.success) {
toast.success('识别成功,请检查箱号是否正确')
const score = Number(ocrRes?.data?.score ?? 0)
form.value.ocrScore = Math.floor(score * 100) / 100
form.value.containerNo = ocrRes.data.boxNumber
form.value.ocrNo = ocrRes.data.boxNumber
isError.value = false
}
}).catch(() => {
isError.value = true
}).finally(() => {
ocrFinish.value = true
})
}
})
})
}
},
})
}
const computedOcrScore = computed(() => {
return `${(Math.floor(form.value.ocrScore * 100)).toFixed(0)}%`
})
function resetForm() {
boxImage.value = null
form.value.containerImageUrl = ''
form.value.containerNo = ''
form.value.ocrNo = ''
form.value.ocrScore = 0.00
}
function handleDeleteBoxImage() {
if (!form.value.containerImageUrl) {
resetForm()
return
}
toast.loading('正在删除中 . . .', {
overlay: true,
transparent: false,
})
deleteAliyunFileApi(form.value.containerImageUrl).then((res) => {
if (res.success) {
resetForm()
toast.success('删除成功')
}
})
}
function onItemClick(fileItem: UploadFileItem) {
console.log(fileItem)
if (!isImageUrl(fileItem.mx_mapfilename) && !isVideoUrl(fileItem.mx_mapfilename)) {
downloadFile(fileItem.mx_mapfilename, fileItem.name)
}
}
function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
// 非图片直接允许上传
if (!isImage(file.name))
return true
// 图片异步压缩,返回修改后的 UploadFile 对象
return imageCompress(file.path).then((res) => {
file.path = res.path
file.size = res.size
return file
}).catch((err) => {
console.error('图片压缩失败:', err)
return file
})
}
async function afterRead(fileItem: UploadFileItem) {
// 设置上传中状态
fileItem.status = 'uploading'
fileItem.message = '正在上传'
fileItem.isUpload = false
try {
// 上传到阿里云
const res = await uploadToOss(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 上传完成
fileItem.status = 'done'
fileItem.message = '上传成功'
fileItem.url = res.url
subFileList.value = [...subFileList.value]
}
catch (e) {
// 上传完成
fileItem.status = 'failed'
fileItem.message = '上传失败'
subFileList.value = [...subFileList.value]
}
}
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
console.log(fileItem)
return new Promise((resolve, reject) => {
dialog.confirm({
title: '提示',
message: `确定删除该张图片吗?`,
onConfirm: () => resolve(),
onCancel: () => reject(),
onClose: () => reject(),
})
})
}
function onRemove(index: number, item: UploadFileItem) {
if (item.status === 'done') {
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
})
}
}
async function handleConfirm() {
if (form.value.containerImageUrl === '') {
toast.fail('集装箱号图片不能为空')
return
}
if (form.value.containerNo === '') {
toast.fail('集装箱号不能为空')
return
}
if (!validateContainerNo(form.value.containerNo)) {
toast.fail('集装箱号不符合规则,请检查')
return
}
toast.loading('正在保存中 . . .', {
overlay: true,
transparent: false,
})
// 图片已经上传成功,保存主表即可
const mainTable = {
b_id: form.value.b_id,
b_container_no: form.value.containerNo,
b_container_img: form.value.containerImageUrl,
b_ocr_no: form.value.ocrNo,
b_ocr_score: form.value.ocrScore || 0.00,
}
const fileInserts = []
const containerNo = form.value.containerNo
subFileList.value.forEach(async (fileItem, index) => {
const uniqueRes = await getUniqueIdApi()
const indexNumber = index + 2
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: indexNumber,
mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: fileItem.url,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
})
const uniqueRes = await getUniqueIdApi()
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: 0,
mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
mx_filesize: boxImage.value.size,
mx_fileext: getFileExtension(boxImage.value.name),
mx_mapfilename: form.value.containerImageUrl,
mx_moduleid: 'b_container',
mx_cate_id: '0',
})
const reqData = [
{
tableName: 'b_container',
keyField: 'b_id',
inserts: [mainTable],
updates: [],
deletes: [],
},
{
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
deletes: [],
},
]
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
tipContainerNo.value = form.value.containerNo
showResult.value = true
}
})
}
function buildFileName(containerNo, index, fileName) {
const formattedIndex = String(index).padStart(2, '0')
return `${containerNo}_${formattedIndex}.${fileName.split('.').pop()}`
}
// 集装箱号校验
function validateContainerNo(containerNo) {
// 正则表达式: 4个字母 + 7个数字
const pattern = /^[A-Z]{4}\d{7}$/i
// 使用正则进行匹配
if (pattern.test(containerNo)) {
return true
}
else {
return false
}
}
async function continueAdd() {
const idRes = await getUniqueIdApi()
if (idRes.success) {
boxImage.value = null
ocrFinish.value = false
isError.value = false
resetForm()
form.value.b_id = idRes.data
subFileList.value = []
showResult.value = false
tipContainerNo.value = ''
}
}
</script>
<template>
<view class="g3-container">
<template v-if="!showResult">
<view class="g3-content">
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
集装箱号(OCR识别)
</text>
</view>
</template>
<view class="step-content box">
<view v-if="ocrFinish" class="header">
<sar-alert
v-if="!isError"
type="success"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别成功,请检查箱号是否正确!
<view>
准确率:{{ computedOcrScore }}
</view>
</sar-alert>
<sar-alert
v-else
type="danger"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别失败,请检查手动填写集装箱号
</sar-alert>
</view>
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="plus" size="156rpx" color="#999" />
</view>
<view v-else class="box-image">
<wd-img
:src="boxImage.path"
width="100%"
height="100%"
:enable-preview="true"
/>
<view class="delete-btn" @click="handleDeleteBoxImage()">
<sar-icon name="close" size="20rpx" color="#fff" />
</view>
</view>
</view>
<view class="box-input">
<view class="header">
<view class="label-container">
<view>集装箱号</view>
<view class="required-asterisk">
*
</view>
</view>
<view class="format">
格式: 4字母 + 7数字
</view>
</view>
<sar-input
v-model="form.containerNo"
placeholder="请输入箱号"
/>
</view>
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</wd-cell-group>
</view>
<view class="g3-footer">
<view class="footer-main">
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
上传
</sar-button>
</view>
</view>
</template>
<view v-else class="g3-result">
<sar-result status="success" title="操作成功" description="已成功录入箱号 为 xxx 的记录">
<template #description>
<view class="result-tip">
已成功录入箱号
<text class="container-no">
"{{ tipContainerNo }}"
</text>
</view>
</template>
<sar-button long style="font-size: 38rpx;" @click="continueAdd">
继续录入
</sar-button>
</sar-result>
</view>
</view>
</template>
<style lang="scss" scoped>
page {
background-color: #f8f9fa;
}
.g3-result {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.result-tip {
font-size: 36rpx;
}
.container-no {
font-weight: 600;
color: #4caf50;
}
}
.g3-container {
display: flex;
flex-direction: column;
height: 100%;
.g3-content {
flex: 1;
overflow-y: auto;
padding: 16rpx 0rpx;
}
.g3-footer {
width: 100%;
.footer-main {
padding: 16rpx;
}
}
}
.step {
margin-bottom: 32rpx;
.step-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
}
.step-content {
padding: 32rpx;
}
.box {
display: flex;
flex-direction: column;
.box-main {
height: 326rpx;
}
.box-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: var(--sar-upload-select-bg);
border-radius: var(--sar-upload-select-border-radius);
.placeholder-text {
color: #999;
font-size: 28rpx;
margin-top: 8rpx;
}
}
.box-image {
width: 100%;
height: 100%;
position: relative;
.delete-btn {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 28rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
}
.box-input {
margin-top: 16rpx;
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
margin-bottom: 12rpx;
width: 100%;
.label-container {
position: relative;
}
.required-asterisk {
position: absolute;
top: -5rpx;
right: -12rpx;
color: red;
font-size: 24rpx;
}
.format {
color: #999;
font-size: 24rpx;
}
.tip {
color: #999;
}
}
}
.submit-btn {
width: 100%;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #3a6ee8, #5c8df6);
box-shadow: 0 6rpx 12rpx rgba(58, 110, 232, 0.18);
transition: all 0.15s;
}
.submit-btn:active {
transform: scale(0.98);
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
}
</style>
+1 -1
View File
@@ -60,7 +60,7 @@
"layout": "default",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "default"
"navigationStyle": "custom"
}
}
],
@@ -1,152 +1,269 @@
<script setup lang="ts">
import { deleteFileApi } from '@/request/api'
import { toast } from 'sard-uniapp'
import { ref } from 'vue'
import { loadPageDataApi } from '@/request/api'
import { useUserStore } from '@/store'
import { deepEqual, groupFieldsBySpan } from '@/utils/common'
import { getCurrentDate } from '@/utils/time'
const userStore = useUserStore()
onShow(() => {
userStore.requireLogin()
const pagingRef = ref<any>(null)
const dataList = ref<any[]>([])
const itemFields = [
{ label: '录入时间', value: item => item.b_inputdatetime, span: 12 },
{ label: '文件数量', value: item => item.v_file_count, span: 12 },
]
const groupedFields = groupFieldsBySpan(itemFields)
// 搜索关键字
const searchValue = ref('')
// onShow(() => {
// pagingRef.value?.reload()
// })
onMounted(() => {
pagingRef.value?.reload()
})
const swiperCurrent = ref(0)
const swiperList = [
{},
{},
{},
]
function handleToLogin() {
uni.reLaunch({
url: '/pages-sub/login/index',
})
// 搜索
function handleSearch() {
pagingRef.value.reload()
}
function toLinkUrl(url) {
if (!url) {
return
// 模拟分页请求
function queryList(pageNo: number, pageSize: number) {
toast.loading('正在加载中')
const userid = userStore.userInfo.userid
let searchCondition = `2 > 1 AND b_inputuser_id = '${userid}'`
if (searchValue.value) {
searchCondition += `b_container_no LIKE '%${searchValue.value}%' `
}
uni.navigateTo({
url,
loadPageDataApi({
viewName: 'v_container',
searchCondition,
page: pageNo,
size: pageSize,
orderField: 'b_inputdatetime desc',
})
.then((res) => {
const rows = res.data.rows || []
console.log(rows)
if (rows) {
pagingRef.value.complete(rows)
}
})
.finally(() => {
toast.hide()
})
}
function deleteAliyunFiles() {
const subids = [
787154320330821,
787154320388165,
]
subids.forEach((subid) => {
deleteFileApi(subid)
function handleToFile(itemData) {
const searchids = [itemData.b_id, itemData.father]
.filter(Boolean)
.join(',')
uni.navigateTo({
url: `/pages-sub/file/index?moduleid=xc_conyard_in&father=${itemData.father}&searchids=${encodeURIComponent(searchids)}&cateid=016&edit=1`,
})
}
</script>
<template>
<view>
<view class="swiper-container">
<swiper
:current="swiperCurrent"
autoplay
:interval="3000"
class="swiper"
@change="swiperCurrent = $event.detail.current"
<z-paging
ref="pagingRef" v-model="dataList"
auto-show-back-to-top
@query="queryList"
>
<swiper-item>
<image
src="@/static/images/bg.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
<swiper-item>
<image
src="@/static/images/bg1.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
<swiper-item>
<image
src="@/static/images/bg2.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
</swiper>
<sar-swiper-dot
:current="swiperCurrent"
:list="swiperList"
<template #top>
<view style="border-bottom: 1px solid #ddd;">
<sar-search
v-model="searchValue"
placeholder="请输入箱号搜索"
@search="handleSearch"
/>
</view>
<view v-if="userStore.isLogined" class="grid-container">
<sar-grid :columns="1" gap="24rpx">
<sar-grid-item
@click="toLinkUrl('/pages-sub/upload/index')"
</template>
<!-- 列表内容 -->
<view
v-for="(item, index) in dataList"
:key="index"
class="list-item"
>
<view class="grid-item">
<image src="/static/images/home/waybill.png" />
<text>集装箱装货采集</text>
<view class="list-item__header">
<view class="list-item__main">
<sar-image
:src="item.b_container_img"
width="160rpx"
height="160rpx"
/>
</view>
</sar-grid-item>
</sar-grid>
</view>
<sar-result v-else status="warning" title="尚未登陆" style="margin-top: 32rpx;">
<sar-button
@click="handleToLogin"
>
前去登录
</sar-button>
</sar-result>
<view class="list-item__body">
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
集装箱号:
</view>
<view class="list-item__value" style="font-weight: bold;color:var(--sar-primary)">
{{ item.b_container_no }}
</view>
</view>
</sar-col>
</sar-row>
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
录入时间:
</view>
<view class="list-item__value">
{{ item.b_inputdatetime }}
</view>
</view>
</sar-col>
</sar-row>
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
照片数量:
</view>
<view class="list-item__value">
{{ item.v_file_count }}
</view>
</view>
</sar-col>
</sar-row>
</view>
<view class="list-item__footer">
<!-- <sar-button inline size="small" square @click="handleToFile(item)">
拍照
</sar-button> -->
</view>
<view class="list-item__line" />
</view>
</z-paging>
</view>
</template>
<style lang="scss" scoped>
<style scoped lang="scss">
@import '@/style/mixins.scss';
page {
background-color: #f1f5f9;
}
.dialog-loading {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
}
.swiper-container {
position: relative;
.swiper {
height: 400rpx;
width: 100%;
/* tab 栏 */
.tabs {
display: flex;
border-bottom: 2rpx solid #eee;
background-color: #fff;
.tabs__item {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 30rpx;
color: #888;
}
.tabs__item--active {
color: var(--sar-primary);
font-weight: bold;
border-bottom: 4rpx solid var(--sar-primary);
}
}
.grid-container {
padding-top: 32rpx;
:deep(.sar-grid__content) {
/* 列表项 */
.list-item {
position: relative;
padding: 16rpx;
background-color: #fff;
}
:deep(.sar-grid) {
row-gap: 0 !important;
}
:deep(.sar-grid__item) {
margin-bottom: 24rpx;
&.list-item--desktop {
.list-item__header {
max-width: 1000rpx;
margin: 0 auto;
}
:deep(.sar-grid__wrapper) {
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
.list-item__body {
max-width: 1000rpx;
margin: 0 auto;
}
:deep(.sar-badge) {
width: 42rpx;
height: 42rpx;
.list-item__footer {
max-width: 1000rpx;
margin: 0 auto;
}
}
.grid-item {
.list-item__header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 36rpx;
image {
width: 128rpx;
height: 128rpx;
}
text {
margin-top: 16rpx;
font-weight: 600;
color: #000;
.list-item__body {
padding: 24rpx 0rpx;
display: flex;
flex-direction: column;
@include flexGap(16rpx, 'column');
.list-item__info {
display: flex;
align-items: start;
width: 100%;
font-size: 32rpx;
line-height: 1;
.list-item__label {
display: block;
min-width: 192rpx;
color: #4b5563;
text-align: right;
white-space: nowrap;
}
.list-item__value {
flex: 1;
color: #111827;
word-break: break-all;
}
}
}
.list-item__footer {
display: flex;
justify-content: flex-end;
@include flexGap(36rpx, '');
:deep(.sar-button) {
padding: 0rpx 48rpx;
font-size: 32rpx;
}
}
.list-item__line {
position: absolute;
bottom: 0rpx;
left: 0rpx;
height: 1px;
width: 100%;
background-color: #dcdcdc;
}
}
</style>
@@ -1,4 +1,4 @@
import { httpPost } from './index'
import { httpPost, httpUpload } from './index'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
@@ -16,7 +16,7 @@ interface GetaliyunkeyParams {
bucketName: string
}
export function getaliyunkeyApi(params: GetaliyunkeyParams) {
export function getAliyunkeyApi(params: GetaliyunkeyParams) {
return httpPost('/file/getAliyunKey', params)
}
@@ -92,3 +92,23 @@ interface UpdatePasswordParams {
export function updatePasswordApi(params: UpdatePasswordParams) {
return httpPost(`/auth/updatePassword`, params)
}
interface PageParams {
page: number
size: number
viewName: string
searchCondition?: string
orderField?: string
searchColumns?: string[]
}
export function loadPageDataApi(params: PageParams) {
return httpPost(
'/data/page',
params,
)
}
export function uploadTempFileApi(filePath: string, fileName: string) {
return httpUpload('/file/uploadTemp', filePath, fileName)
}
+186 -139
View File
@@ -5,99 +5,109 @@ import { useUserStore } from '@/store'
import { joinUrl } from '@/utils/common'
import { isH5 } from '@/utils/platform'
function request<T = any>(option: UniApp.RequestOptions): Promise<IResData<T>> & { abort: () => void } {
let task: UniApp.RequestTask | undefined
let aborted = false
const promise = new Promise<IResData<T>>((resolve, reject) => {
// 判断是否是登录请求
const isLogin = option.url?.includes('login')
const readDbJson = new Promise<Record<string, string>>((res, rej) => {
/**
* ======================
* 读取机构配置
* ======================
*/
function readDbJson(): Promise<Record<string, string>> {
return new Promise((resolve, reject) => {
if (isH5) {
// H5:用 request 请求,带时间戳保证最新
const timestamp = Date.now()
uni.request({
url: `/g3ocr-app/static/db.json?t=${timestamp}`,
method: 'GET',
success: r => res(r.data as Record<string, string>),
fail: err => rej(err),
success: r => resolve(r.data as Record<string, string>),
fail: err => reject(err),
})
}
else {
// 非 H5 直接返回本地导入的 JSON
res(dbJson)
resolve(dbJson as Record<string, string>)
}
})
}
readDbJson
.then((dbRes) => {
if (aborted)
return reject(new Error('请求已取消'))
/**
* ======================
* 获取 baseUrl + headers
* ======================
*/
async function getBaseConfig(option: UniApp.RequestOptions, isLogin: boolean) {
const dbRes = await readDbJson()
// 组织请求 URL
let reqUrl = ''
const userStore = useUserStore()
const { token, userid, orgid } = userStore.userInfo || {}
if (!dbRes || Object.keys(dbRes).length === 0) {
toast.fail('机构配置获取失败,请联系管理人员!')
return reject(new Error('机构配置获取失败,请联系管理人员!'))
throw new Error('机构配置获取失败')
}
let reqUrl = ''
if (isLogin) {
const dataObj: any = typeof option.data === 'string'
? parseFormUrlencoded(option.data)
: option.data || {}
const orgidFromData = dataObj.orgid || ''
reqUrl = dbRes[orgidFromData]
reqUrl = dbRes[dataObj.orgid || '']
}
else {
reqUrl = dbRes[orgid || '']
}
if (!reqUrl) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.showToast({ title: '当前登录已过期,请重新登录', icon: 'error' })
uni.reLaunch({
url: '/pages-sub/login/index',
})
}
else {
uni.showToast({ title: '机构码错误', icon: 'error' })
handleInvalidOrg()
throw new Error('机构码错误')
}
return reject(new Error('机构码错误'))
}
// 设置 headers
const headers: Record<string, any> = { ...(option.header || {}) }
if (!isLogin) {
headers.Authorization = `Bearer ${token}`
headers.userid = encodeURIComponent(userid)
headers.orgid = orgid
}
// 拼接完整 URL
option.url = joinUrl(reqUrl, option.url || '')
option.header = headers
return {
baseUrl: reqUrl,
headers,
}
}
// 发请求
task = uni.request({
...option,
url: option.url,
success: (res) => {
/**
* ======================
* 统一错误处理
* ======================
*/
function handleInvalidOrg() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.showToast({ title: '当前登录已过期,请重新登录', icon: 'none' })
uni.reLaunch({ url: '/pages-sub/login/index' })
}
else {
uni.showToast({ title: '机构码错误', icon: 'none' })
}
}
/**
* ======================
* 统一响应处理
* ======================
*/
function handleResponse<T>(
res: any,
resolve: (data: IResData<T>) => void,
reject: (err: any) => void,
aborted: boolean,
) {
if (aborted)
return reject(new Error('请求已取消'))
if (res.statusCode !== 200) {
console.error('HTTP错误', res.statusCode, res.data)
toast.fail(`请求失败:${res.statusCode}`)
return reject(new Error(`HTTP错误: ${res.statusCode}`))
}
@@ -105,74 +115,150 @@ function request<T = any>(option: UniApp.RequestOptions): Promise<IResData<T>> &
let data: IResData<T>
try {
// 解析最外层响应
data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
data = typeof res.data === 'string'
? JSON.parse(res.data)
: res.data
// 如果 data.data 是字符串,则解析成对象
if (data.data && typeof data.data === 'string') {
try {
data.data = JSON.parse(data.data)
}
catch {}
}
}
catch {
// 如果解析失败,保持原始字符串即可
// console.warn('data.data 不是合法 JSON 字符串', data.data)
toast.fail('响应解析失败')
return reject(new Error('响应解析失败'))
}
}
}
catch (err) {
console.log(`解析响应JSON失败:${err}`)
toast.fail('响应数据解析失败')
return reject(new Error('响应数据解析失败'))
// 401 优先处理(避免被后面拦截)
if (data.code === 401) {
uni.showToast({ title: '请重新登录', icon: 'none' })
handleInvalidOrg()
return reject(new Error('未授权'))
}
if (data.code === 200) {
data.success = true
resolve(data)
}
else {
toast.fail(data.message || '请求错误')
reject(new Error(data.message || '请求错误'))
toast.fail(data.message || '请求失败')
reject(new Error(data.message || '请求失败'))
}
}
// 登录失效处理
if (data.code === 401) {
uni.showToast({ title: '请重新登录', icon: 'none' })
/**
* ======================
* 主请求方法
* ======================
*/
function request<T = any>(
option: UniApp.RequestOptions,
): Promise<IResData<T>> & { abort: () => void } {
let task: UniApp.RequestTask | undefined
let aborted = false
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.reLaunch({
url: '/pages-sub/login/index',
})
}
}
},
const promise = (async () => {
const isLogin = option.url?.includes('login')
const { baseUrl, headers } = await getBaseConfig(option, isLogin)
option.url = joinUrl(baseUrl, option.url || '')
option.header = headers
return new Promise<IResData<T>>((resolve, reject) => {
task = uni.request({
...option,
success: res =>
handleResponse<T>(res, resolve, reject, aborted),
fail: (err) => {
if (aborted)
return reject(new Error('请求已取消'))
toast.fail('网络请求失败,请联系管理人员')
reject(new Error(err.errMsg || '网络请求失败'))
toast.fail('网络错误')
reject(err)
},
})
})
.catch((err) => {
toast.fail('机构配置获取失败,请联系管理人员!')
console.error(err)
reject(new Error('机构配置获取失败,请联系管理人员!'))
})
}) as Promise<IResData<T>> & { abort: () => void }
})()
promise.abort = () => {
const finalPromise = promise as Promise<IResData<T>> & { abort: () => void }
finalPromise.abort = () => {
aborted = true
task?.abort()
}
return promise
return finalPromise
}
/**
* ======================
* 上传文件
* ======================
*/
export function httpUpload<T = any>(
url: string,
filePath: string,
fileName: string,
formData?: Record<string, any>,
option?: UniApp.UploadFileOption,
) {
let task: UniApp.UploadTask | undefined
let aborted = false
const promise = (async () => {
const isLogin = url.includes('login')
const { baseUrl, headers } = await getBaseConfig(
{ url, data: formData } as any,
isLogin,
)
const fullUrl = joinUrl(baseUrl, url)
const mergedFormData = {
...formData,
fileName,
}
return new Promise<IResData<T>>((resolve, reject) => {
task = uni.uploadFile({
url: fullUrl,
filePath,
name: 'file',
formData: mergedFormData,
header: {
...headers,
...(option?.header || {}),
},
success: res =>
handleResponse<T>(res, resolve, reject, aborted),
fail: (err) => {
if (aborted)
return reject(new Error('请求已取消'))
toast.fail('网络错误')
reject(err)
},
...option,
})
})
})()
const finalPromise = promise as Promise<IResData<T>> & { abort: () => void }
finalPromise.abort = () => {
aborted = true
task?.abort()
}
return finalPromise
}
/**
* ======================
* 工具函数
* ======================
*/
function parseFormUrlencoded(str: string) {
return str.split('&').reduce((acc, pair) => {
const [key, value] = pair.split('=')
@@ -181,39 +267,16 @@ function parseFormUrlencoded(str: string) {
}, {} as Record<string, string>)
}
// GET
request.get = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'GET',
url,
data,
...option,
})
}
/**
* ======================
* 方法封装
* ======================
*/
request.get = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) =>
request<T>({ method: 'GET', url, data, ...option })
// PUT
request.put = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'PUT',
url,
data,
...option,
})
}
// DELETE
request.delete = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'DELETE',
url,
data,
...option,
})
}
// POST JSON
request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) =>
request<T>({
method: 'POST',
url,
data,
@@ -223,17 +286,10 @@ request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOpt
},
...option,
})
}
// POST 表单
request.postForm = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
let payload: string
if (data) {
// 使用 qs.stringify 或 URLSearchParams
payload = qs.stringify(data, { arrayFormat: 'repeat' })
// 生成 tables=val1&tables=val2 格式
}
const payload = data ? qs.stringify(data, { arrayFormat: 'repeat' }) : undefined
return request<T>({
method: 'POST',
url,
@@ -247,16 +303,7 @@ request.postForm = <T = any>(url: string, data?: any, option?: UniApp.RequestOpt
}
export function httpPost<T = any>(url: string, data?: any, option?: UniApp.RequestOptions) {
return request<T>({
method: 'POST',
url,
data,
header: {
'Content-Type': 'application/json',
...(option?.header || {}),
},
...option,
})
return request.postJson<T>(url, data, option)
}
export default request
@@ -1,3 +1,3 @@
{
"g3hd": "http://192.168.5.6:80/api"
"g3hd": "http://192.168.1.137:80/api"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -1,9 +1,9 @@
@font-face {
font-family: 'iconfont'; /* Project id 5142758 */
src:
url('data:application/x-font-woff2;charset=utf-8;base64,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')
url('data:application/x-font-woff2;charset=utf-8;base64,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')
format('woff2'),
url('//at.alicdn.com/t/c/font_5142758_pgr9uja0qt.ttf?t=1773887288142') format('truetype');
url('//at.alicdn.com/t/c/font_5142758_32yu5am87gx.ttf?t=1774661340978') format('truetype');
}
.iconfont {
@@ -14,18 +14,18 @@
-moz-osx-font-smoothing: grayscale;
}
.iconfont-tupian:before {
content: '\e865';
}
.iconfont-iconyuanbanben_OCR:before {
content: '\e6b6';
}
.iconfont-fasongtijiao:before {
content: '\e693';
}
.iconfont-tupian:before {
content: '\e8ba';
}
.iconfont-OCRshibie:before {
content: '\e658';
}
.iconfont-save:before {
content: '\e8c7';
}
@@ -5,7 +5,7 @@ page,
.sar-portal {
--sar-input-border-color: #ddd;
// --sar-primary: #34495e;
--sar-primary: #4d80f0;
--sar-primary: #0052d9;
--sar-rounded: 8rpx;
--sar-dialog-title-font-size: 42rpx;
--sar-dialog-message-font-size: 36rpx;
@@ -1,5 +1,5 @@
import { toast } from 'sard-uniapp'
import { getaliyunkeyApi } from '@/request/api'
import { getOssKey } from './oss'
import { isApp, isH5, isMpWeixin } from './platform'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
@@ -115,70 +115,12 @@ export function formatFileSize(size: number): string {
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
}
export interface AliyunUploadResult {
url: string // 上传后的完整 URL
}
/**
* 上传文件到阿里云 OSS
* @param filePath 本地文件路径
* @param fileName 文件名
* @param fileType 文件类型,可选:image/video/file
* @returns Promise<AliyunUploadResult>
* 上传文件到阿里云 OSS(优化版)
*/
export async function uploadFileToAliyun(
filePath: string,
fileName: string,
fileType: any,
): Promise<AliyunUploadResult> {
// 获取 OSS 临时上传凭证
const keyRes = await getaliyunkeyApi({
bucketName: aliyunBucket,
})
const { accessid, callback, dir, host, policy, signature } = keyRes.data
// 构建文件在 OSS 的存储路径
const fileExtension = fileName.split('.').pop()?.toLowerCase() || 'tmp'
const timestamp = Date.now()
const random = Math.floor(Math.random() * 1000)
const key = `${dir}${timestamp + random}.${fileExtension}`
const formData: Record<string, any> = {
key,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
name: fileName,
}
if (callback)
formData.callback = callback
// 使用 Promise 包裹 uni.uploadFile
return new Promise<AliyunUploadResult>((resolve, reject) => {
uni.uploadFile({
url: host,
filePath,
name: 'file',
fileName,
fileType,
formData,
success: (res) => {
if (res.statusCode === 200) {
resolve({
url: `${host}/${key}`,
})
}
else {
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
}
},
fail: (err) => {
reject(err)
},
})
})
}
/** =================== 工具函数 =================== */
/** 获取文件名:从 URL 提取最后一段 */
@@ -0,0 +1,116 @@
// utils/ossManager.ts
import { getAliyunkeyApi } from '@/request/api'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
interface OssKey {
accessid: string
callback?: string
dir: string
host: string
policy: string
signature: string
expire: number
}
interface OssUpdateResult {
url: string // 上传后的完整 URL
}
// ✅ 模块级缓存(天然单例)
let ossKeyCache: OssKey | null = null
let ossKeyPromise: Promise<OssKey> | null = null
/**
* 获取 OSS Key(缓存 + 并发锁)
*/
export async function getOssKey(): Promise<OssKey> {
const now = Math.floor(Date.now() / 1000)
const SAFE_GAP = 10 // 提前10秒过期
// ✅ 命中缓存
if (ossKeyCache && ossKeyCache.expire - SAFE_GAP > now) {
console.log('===命中缓存===')
return ossKeyCache
}
// ✅ 并发锁
if (ossKeyPromise) {
return ossKeyPromise
}
// ✅ 请求
ossKeyPromise = getAliyunkeyApi({
bucketName: aliyunBucket,
})
.then((res) => {
console.log('===重新请求===')
const data = res.data
ossKeyCache = {
accessid: data.accessid,
callback: data.callback,
dir: data.dir,
host: data.host,
policy: data.policy,
signature: data.signature,
expire: Number(data.expire),
}
return ossKeyCache
})
.finally(() => {
ossKeyPromise = null
})
return ossKeyPromise
}
export async function uploadToOss(
filePath: string,
fileName: string,
fileType?: 'image' | 'video' | 'file',
): Promise<OssUpdateResult> {
// ✅ 使用缓存后的 key
const { accessid, callback, dir, host, policy, signature } = await getOssKey()
// 生成文件路径
const ext = fileName.split('.').pop()?.toLowerCase() || 'tmp'
const key = `${dir}${Date.now()}_${Math.floor(Math.random() * 1000)}.${ext}`
const formData: Record<string, any> = {
key,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
name: fileName,
}
if (callback) {
formData.callback = callback
}
return new Promise<OssUpdateResult>((resolve, reject) => {
uni.uploadFile({
url: host,
filePath,
name: 'file',
fileName,
fileType: 'image',
formData,
success: (res) => {
if (res.statusCode === 200) {
resolve({
url: `${host}/${key}`,
})
}
else {
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
}
},
fail: reject,
})
})
}
@@ -1,9 +1,3 @@
/*
* @Author: 菲鸽
* @Date: 2024-03-28 19:13:55
* @Last Modified by: 菲鸽
* @Last Modified time: 2024-03-28 19:24:55
*/
export const platform = __UNI_PLATFORM__
export const isH5 = __UNI_PLATFORM__ === 'h5'
export const isApp = __UNI_PLATFORM__ === 'app'
@@ -13,6 +7,11 @@ export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
export const isHarmony = __UNI_PLATFORM__.startsWith('app-harmony')
function isInWeChat() {
const ua = navigator.userAgent.toLowerCase()
return ua.includes('micromessenger')
}
const PLATFORM = {
platform,
isH5,
@@ -22,5 +21,6 @@ const PLATFORM = {
isMpAplipay,
isMpToutiao,
isHarmony,
isInWeChat
}
export default PLATFORM
@@ -1,4 +1,6 @@
import { httpPost } from ".";
import { httpPost, httpUpload } from ".";
// 登录
export function loginApi(orgid, userid, password) {
@@ -112,6 +112,41 @@ export function httpPost(url, data, config) {
.then((res) => res.data);
}
/**
* 通用文件上传
* @param {string} url - 上传接口地址
* @param {File | File[]} file - 单个或多个文件
* @param {Object} extraData - 额外参数(可选)
* @param {Object} config - axios 配置(可选)
* @returns {Promise<any>}
*/
export function httpUpload(url, file, extraData = {}, config = {}) {
const formData = new FormData();
// 兼容单文件 & 多文件
if (Array.isArray(file)) {
file.forEach((f) => {
formData.append("file", f);
});
} else {
formData.append("file", file);
}
// 追加额外参数
Object.keys(extraData).forEach((key) => {
formData.append(key, extraData[key]);
});
return axiosInstance
.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
...config,
})
.then((res) => res.data);
}
export function reset401Flag() {
hasShown401 = false;
}
@@ -94,6 +94,9 @@ async function loadTableColumns() {
}
}
/**
* 上传到临时文件夹
*/
async function loadTableDatas() {
loading.table = true;