diff --git a/.gitignore b/.gitignore
index bb2b050..0f4c0f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,144 +22,20 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
-
-# ---> Python
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
+.vscode
+log
+# ---> Maven
target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+.idea/
+.DS_Store
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f1d8fac
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,199 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+ com.jiagutech
+ siliqua_recognition
+ 0.0.1-SNAPSHOT
+ siliqua_recognition
+ 角果识别
+
+
+
+ 8
+ 8
+ 8
+ UTF-8
+ 1.6.1
+ 1.18.30
+ 5.8.22
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 2.3.2
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.5.0
+
+
+ org.mybatis
+ mybatis
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ 8.2.0
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ 2.0.43
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+
+ com.alibaba
+ easyexcel
+ 4.0.2
+
+
+ commons-io
+ commons-io
+ 2.16.1
+
+
+
+ cn.dev33
+ sa-token-spring-boot-starter
+ 1.37.0
+
+
+ cn.dev33
+ sa-token-jwt
+ 1.37.0
+
+
+ cn.dev33
+ sa-token-redis-jackson
+ 1.37.0
+
+
+
+
+
+ cn.hutool
+ hutool-bom
+ ${hutool.version}
+ pom
+ import
+
+
+ com.huaweicloud
+ esdk-obs-java
+ 3.20.6.1
+ compile
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.23.1
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.23.1
+
+
+ io.springfox
+ springfox-swagger2
+ 2.7.0
+
+
+
+ io.springfox
+ springfox-swagger-ui
+ 2.7.0
+
+
+ com.github.xiaoymin
+ knife4j-spring-boot-starter
+ 2.0.8
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jiagutech/RecognitionRun.java b/src/main/java/com/jiagutech/RecognitionRun.java
new file mode 100644
index 0000000..3e8d5b1
--- /dev/null
+++ b/src/main/java/com/jiagutech/RecognitionRun.java
@@ -0,0 +1,15 @@
+package com.jiagutech;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@MapperScan("com.jiagutech.mapper")
+@EnableAsync
+@SpringBootApplication
+public class RecognitionRun {
+ public static void main(String[] args) {
+ SpringApplication.run(RecognitionRun.class, args);
+ }
+}
diff --git a/src/main/java/com/jiagutech/client/RecognitionModelClient.java b/src/main/java/com/jiagutech/client/RecognitionModelClient.java
new file mode 100644
index 0000000..0be6b68
--- /dev/null
+++ b/src/main/java/com/jiagutech/client/RecognitionModelClient.java
@@ -0,0 +1,61 @@
+package com.jiagutech.client;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.jiagutech.dto.ModelPredictResponse;
+import com.jiagutech.dto.RecordModel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RecognitionModelClient {
+
+ @Value("${recognition.model.url}")
+ private String modelUrl;
+
+ private final RestTemplate restTemplate;
+
+
+ public List predict(File file, String modeName, float lou, float conf) {
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ FileSystemResource fileResource = new FileSystemResource(file);
+ if (!fileResource.exists()) {
+ throw new IllegalArgumentException("文件不存在: " + file.getAbsolutePath());
+ }
+ body.add("image", fileResource);
+ body.add("model_name", modeName);
+ body.add("lou", lou);
+ body.add("conf", conf);
+
+ // 设置请求头
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType("multipart/form-data; charset=UTF-8"));
+ HttpEntity> requestEntity = new HttpEntity<>(body, headers);
+ ResponseEntity result = restTemplate.exchange(modelUrl + "/api/predict", HttpMethod.POST, requestEntity, String.class);
+ if (result.getStatusCode() == HttpStatus.OK) {
+ return JSON.parseArray(result.getBody(), ModelPredictResponse.class);
+ }
+ return Collections.emptyList();
+ }
+
+ public RecordModel models() {
+ return restTemplate.getForObject(modelUrl + "/api/models", RecordModel.class);
+ }
+
+
+}
diff --git a/src/main/java/com/jiagutech/common/HttpStatus.java b/src/main/java/com/jiagutech/common/HttpStatus.java
new file mode 100644
index 0000000..1fa257e
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/HttpStatus.java
@@ -0,0 +1,93 @@
+package com.jiagutech.common;
+
+/**
+ * 返回状态码
+ *
+ * @author Lion Li
+ */
+public interface HttpStatus {
+ /**
+ * 操作成功
+ */
+ int SUCCESS = 200;
+
+ /**
+ * 对象创建成功
+ */
+ int CREATED = 201;
+
+ /**
+ * 请求已经被接受
+ */
+ int ACCEPTED = 202;
+
+ /**
+ * 操作已经执行成功,但是没有返回数据
+ */
+ int NO_CONTENT = 204;
+
+ /**
+ * 资源已被移除
+ */
+ int MOVED_PERM = 301;
+
+ /**
+ * 重定向
+ */
+ int SEE_OTHER = 303;
+
+ /**
+ * 资源没有被修改
+ */
+ int NOT_MODIFIED = 304;
+
+ /**
+ * 参数列表错误(缺少,格式不匹配)
+ */
+ int BAD_REQUEST = 400;
+
+ /**
+ * 未授权
+ */
+ int UNAUTHORIZED = 401;
+
+ /**
+ * 访问受限,授权过期
+ */
+ int FORBIDDEN = 403;
+
+ /**
+ * 资源,服务未找到
+ */
+ int NOT_FOUND = 404;
+
+ /**
+ * 不允许的http方法
+ */
+ int BAD_METHOD = 405;
+
+ /**
+ * 资源冲突,或者资源被锁
+ */
+ int CONFLICT = 409;
+
+ /**
+ * 不支持的数据,媒体类型
+ */
+ int UNSUPPORTED_TYPE = 415;
+
+ /**
+ * 系统内部错误
+ */
+ int ERROR = 500;
+
+ /**
+ * 接口未实现
+ */
+ int NOT_IMPLEMENTED = 501;
+
+ /**
+ * 系统警告消息
+ */
+ int WARN = 601;
+}
diff --git a/src/main/java/com/jiagutech/common/Knife4jConfig.java b/src/main/java/com/jiagutech/common/Knife4jConfig.java
new file mode 100644
index 0000000..f0f6341
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/Knife4jConfig.java
@@ -0,0 +1,50 @@
+package com.jiagutech.common;
+
+import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * 自定义 Knife4j 接口文档的配置
+ */
+@Configuration
+@EnableSwagger2
+@EnableKnife4j
+public class Knife4jConfig {
+
+ @Bean(value = "defaultApi2")
+ public Docket defaultApi2() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ .apiInfo(apiInfo())
+ .select()
+ // 项目controller的位置
+ .apis(RequestHandlerSelectors.basePackage("com.jiagutech.controller"))
+ .paths(PathSelectors.any())
+ .build();
+ }
+
+ /**
+ * api 信息
+ *
+ * @return
+ */
+ private ApiInfo apiInfo() {
+ return new ApiInfoBuilder()
+ .title("角果识别")
+ .description("角果识别接口文档")
+ .termsOfServiceUrl("http://recognition.jiagutech.com")
+ .contact(new Contact("yeguangzhang", "http://recognition.jiagutech.com", "yeguangzhang@126.com"))
+ .version("1.0.0")
+ .build();
+ }
+}
+
diff --git a/src/main/java/com/jiagutech/common/MybatisPlusConfig.java b/src/main/java/com/jiagutech/common/MybatisPlusConfig.java
new file mode 100644
index 0000000..3a78489
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/MybatisPlusConfig.java
@@ -0,0 +1,23 @@
+package com.jiagutech.common;
+
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MybatisPlusConfig {
+
+ /**
+ * 添加分页插件
+ */
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
+ return interceptor;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/ObsConfig.java b/src/main/java/com/jiagutech/common/ObsConfig.java
new file mode 100644
index 0000000..b566b6b
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/ObsConfig.java
@@ -0,0 +1,39 @@
+package com.jiagutech.common;
+
+
+import com.obs.services.ObsClient;
+import com.obs.services.ObsConfiguration;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+@Data
+@Configuration
+@NoArgsConstructor
+@ConfigurationProperties(prefix = "huawei.obs")
+public class ObsConfig {
+
+
+ private String endPoint;
+ private String ak;
+ private String sk;
+
+ private String bucketName;
+ private String pathPrefix;
+
+ @Bean
+ @Scope("singleton")
+ public ObsClient obsClient() {
+// ObsConfiguration config = new ObsConfiguration();
+// config.setConnectionTimeout(3000);
+// config.setSocketTimeout(5000);
+// config.setMaxErrorRetry(2);
+// config.setEndPoint(endPoint);
+
+ ObsClient obsClient = new ObsClient(ak, sk, endPoint);
+ return obsClient;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/RestTemplateConfig.java b/src/main/java/com/jiagutech/common/RestTemplateConfig.java
new file mode 100644
index 0000000..8774730
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/RestTemplateConfig.java
@@ -0,0 +1,41 @@
+package com.jiagutech.common;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+
+
+/**
+ * RestTemplate工具类,主要用来提供RestTemplate对象
+ */
+@Configuration
+public class RestTemplateConfig {
+
+
+
+ @Value("${http.template.connect-timeout}")
+ private int connectionTimeout;
+ @Value("${http.template.read-timeout}")
+ private int readTimeout;
+
+ /**
+ * 创建RestTemplate对象
+ */
+ @Bean
+ public RestTemplate restTemplate() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(connectionTimeout); // 设置连接超时时间(毫秒)
+ factory.setReadTimeout(readTimeout); // 设置读取超时时间(毫秒)
+ RestTemplate restTemplate = new RestTemplate(factory);
+ restTemplate.setInterceptors(Collections.singletonList(new RestTemplateInterceptor()));
+ restTemplate.getMessageConverters().set(1,new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文编码
+ return restTemplate;
+ }
+}
+
diff --git a/src/main/java/com/jiagutech/common/RestTemplateInterceptor.java b/src/main/java/com/jiagutech/common/RestTemplateInterceptor.java
new file mode 100644
index 0000000..4922d98
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/RestTemplateInterceptor.java
@@ -0,0 +1,33 @@
+package com.jiagutech.common;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@Slf4j
+public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
+
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
+
+ // 打印出请求的详细信息
+ log.info("URI: {}", request.getURI());
+
+ // 执行请求并获取响应
+ ClientHttpResponse response = execution.execute(request, body);
+
+ // 打印出响应的详细信息
+ log.info("Response Status: {}", response.getStatusCode());
+ log.info("Response Headers: {}", response.getHeaders());
+ response.getBody(); // 读取响应体,确保内容被完全读取,这样连接才能被关闭
+
+ return response;
+
+ }
+}
diff --git a/src/main/java/com/jiagutech/common/UserConstants.java b/src/main/java/com/jiagutech/common/UserConstants.java
new file mode 100644
index 0000000..bcb634d
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/UserConstants.java
@@ -0,0 +1,143 @@
+package com.jiagutech.common;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public interface UserConstants {
+
+ /**
+ * 平台内系统用户的唯一标志
+ */
+ String SYS_SESSION = "CURRENT_USER";
+
+
+ /**
+ * 正常状态
+ */
+ String NORMAL = "0";
+
+ /**
+ * 异常状态
+ */
+ String EXCEPTION = "1";
+
+ /**
+ * 用户正常状态
+ */
+ String USER_NORMAL = "0";
+
+ /**
+ * 用户封禁状态
+ */
+ String USER_DISABLE = "1";
+
+ /**
+ * 角色正常状态
+ */
+ String ROLE_NORMAL = "0";
+
+ /**
+ * 角色封禁状态
+ */
+ String ROLE_DISABLE = "1";
+
+ /**
+ * 部门正常状态
+ */
+ String DEPT_NORMAL = "0";
+
+ /**
+ * 部门停用状态
+ */
+ String DEPT_DISABLE = "1";
+
+ /**
+ * 岗位正常状态
+ */
+ String POST_NORMAL = "0";
+
+ /**
+ * 岗位停用状态
+ */
+ String POST_DISABLE = "1";
+
+ /**
+ * 字典正常状态
+ */
+ String DICT_NORMAL = "0";
+
+ /**
+ * 是否为系统默认(是)
+ */
+ String YES = "Y";
+
+ /**
+ * 是否菜单外链(是)
+ */
+ String YES_FRAME = "0";
+
+ /**
+ * 是否菜单外链(否)
+ */
+ String NO_FRAME = "1";
+
+ /**
+ * 菜单正常状态
+ */
+ String MENU_NORMAL = "0";
+
+ /**
+ * 菜单停用状态
+ */
+ String MENU_DISABLE = "1";
+
+ /**
+ * 菜单类型(目录)
+ */
+ String TYPE_DIR = "M";
+
+ /**
+ * 菜单类型(菜单)
+ */
+ String TYPE_MENU = "C";
+
+ /**
+ * 菜单类型(按钮)
+ */
+ String TYPE_BUTTON = "F";
+
+ /**
+ * Layout组件标识
+ */
+ String LAYOUT = "Layout";
+
+ /**
+ * ParentView组件标识
+ */
+ String PARENT_VIEW = "ParentView";
+
+ /**
+ * InnerLink组件标识
+ */
+ String INNER_LINK = "InnerLink";
+
+ /**
+ * 用户名长度限制
+ */
+ int USERNAME_MIN_LENGTH = 2;
+ int USERNAME_MAX_LENGTH = 20;
+
+ /**
+ * 密码长度限制
+ */
+ int PASSWORD_MIN_LENGTH = 5;
+ int PASSWORD_MAX_LENGTH = 20;
+
+ /**
+ * 超级管理员ID
+ */
+ Long SUPER_ADMIN_ID = 1L;
+
+}
diff --git a/src/main/java/com/jiagutech/common/WebCrosConfig.java b/src/main/java/com/jiagutech/common/WebCrosConfig.java
new file mode 100644
index 0000000..392aee8
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/WebCrosConfig.java
@@ -0,0 +1,32 @@
+package com.jiagutech.common;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+@Configuration
+public class WebCrosConfig {
+
+
+ @Bean
+ public CorsFilter corsFilter() {
+ CorsConfiguration config = new CorsConfiguration();
+ config.addAllowedOrigin("*");
+ config.addAllowedMethod("GET");
+ config.addAllowedMethod("POST");
+ config.addAllowedMethod("PUT");
+ config.addAllowedMethod("DELETE");
+ config.addAllowedMethod("OPTIONS");
+ config.addAllowedHeader("*");
+ config.setAllowCredentials(false);
+ config.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+
+ return new CorsFilter(source);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/exception/BizCode.java b/src/main/java/com/jiagutech/common/exception/BizCode.java
new file mode 100644
index 0000000..1ebe4a3
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/exception/BizCode.java
@@ -0,0 +1,63 @@
+package com.jiagutech.common.exception;
+
+import lombok.Getter;
+
+@Getter
+public enum BizCode implements CodeInterface {
+ General_Success(200, "接口调用成功"),
+ ServerError(10001, "服务器异常"),
+ General_Failure(10004, "接口调用失败"),
+ General_DBError(10005, "DB错误"),
+ General_ParameterInvalid(12001, "参数校验失败"),
+ /**
+ * 通用类
+ */
+ USER_UNREGISTERED(12001, "用户未注册"),
+ NOT_FOUND(12002, "数据不存在"),
+ LOGIN_TYPE_ERROR(12005, "登录类型错误"),
+
+
+ /**
+ * 文件服务
+ */
+ FILE_DOWNLOAD_ERROR(90001, "文件下载错误"),
+
+ FILE_UPLOAD_ERROR(90002, "文件上传失败"),
+
+
+
+ USER_NOT_FOUND(40004, "用户不存在"),
+
+ CAPTCHA_ERROR(40003, "验证码错误"),
+
+ PASSWORD_ERROR(40002, "密码错误"),
+
+ TOKEN_TIMEOUT_ERROR(40006, "token已过期"),
+
+ USER_STOP_FOUND(40008, "用户被禁用"),
+ USER_STATUS_ERROR(40009,"用户未审核通过"),
+
+ USER_NOT_LOGIN(40011, "用户未登录,请登录后再操作"),
+
+ ACCESS_TOKEN_INVALID(40040, "用户token无效,请重新登录"),
+
+ ACCESS_NOT_ALLOWABLE(50001, "服务不允许直接访问"),
+
+ PERMISSION_NOT_FOUND(40001,"该用户无操作权限"),
+
+ USER_PHONE_EXIST(40012, "用户手机号已存在"),
+
+
+ ;
+
+
+ private final Integer code;
+
+ private final String msg;
+
+ BizCode(Integer code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+}
diff --git a/src/main/java/com/jiagutech/common/exception/BusinessException.java b/src/main/java/com/jiagutech/common/exception/BusinessException.java
new file mode 100644
index 0000000..ac9e0a7
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/exception/BusinessException.java
@@ -0,0 +1,53 @@
+package com.jiagutech.common.exception;
+
+import lombok.Data;
+
+@Data
+public class BusinessException extends RuntimeException {
+
+ private int code;
+
+ private String message;
+
+ public BusinessException( int code,String message) {
+ super(message);
+ this.code = code;
+ this.message = message;
+ }
+
+ public BusinessException(String message, Throwable cause, int code, String message1) {
+ super(message, cause);
+ this.code = code;
+ this.message = message1;
+ }
+
+ public BusinessException(Throwable cause, int code, String message) {
+ super(cause);
+ this.code = code;
+ this.message = message;
+ }
+
+ public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, int code, String message1) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ this.code = code;
+ this.message = message1;
+ }
+
+ public BusinessException(CodeInterface biz) {
+ super();
+ this.code = biz.getCode();
+ this.message = biz.getMsg();
+ }
+
+ public BusinessException(String message) {
+ super(message);
+ this.code= BizCode.General_ParameterInvalid.getCode();
+ this.message =message;
+ }
+
+ public BusinessException(BizCode biz, String msg) {
+ super();
+ this.code = biz.getCode();
+ this.message = msg;
+ }
+}
diff --git a/src/main/java/com/jiagutech/common/exception/CodeInterface.java b/src/main/java/com/jiagutech/common/exception/CodeInterface.java
new file mode 100644
index 0000000..5f8e091
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/exception/CodeInterface.java
@@ -0,0 +1,22 @@
+package com.jiagutech.common.exception;
+
+/**
+ * 业务异常接口
+ */
+public interface CodeInterface {
+
+ /**
+ * 消息
+ *
+ * @return String
+ */
+ String getMsg();
+
+ /**
+ * 业务代码
+ *
+ * @return int
+ */
+ Integer getCode();
+
+}
diff --git a/src/main/java/com/jiagutech/common/exception/GlobalExceptionHandler.java b/src/main/java/com/jiagutech/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..043a399
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,80 @@
+package com.jiagutech.common.exception;
+
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotRoleException;
+import com.alibaba.fastjson2.JSON;
+import com.jiagutech.dto.common.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(BindException.class)
+ public ResponseEntity handleBindExceptions(BindException ex) {
+ log.error("参数绑定失败", ex);
+ Map errorMap = new HashMap<>();
+ for (FieldError fieldError : ex.getFieldErrors()) {
+ errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
+ }
+ return new ResponseEntity<>(R.fail(BizCode.General_ParameterInvalid.getCode(), JSON.toJSONString(errorMap)), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) {
+ log.error("参数校验失败", ex);
+ Map errorMap = new HashMap<>();
+ for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
+ errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
+ }
+ return new ResponseEntity<>(R.fail(BizCode.General_ParameterInvalid.getCode(), JSON.toJSONString(errorMap)), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(BusinessException.class)
+ public ResponseEntity handleBusinessException(BusinessException ex) {
+ return new ResponseEntity<>(R.fail(ex.getCode(), ex.getMessage()), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
+ log.error("参数类型错误", ex);
+ return new ResponseEntity<>(R.fail(new BusinessException("参数类型错误")), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(NotLoginException.class)
+ public ResponseEntity handleLoginException(Exception ex) {
+ log.error("系统异常", ex);
+ return new ResponseEntity<>(R.fail(BizCode.USER_NOT_LOGIN.getCode(), BizCode.USER_NOT_LOGIN.getMsg()), HttpStatus.UNAUTHORIZED);
+ }
+
+ @ExceptionHandler(NotRoleException.class)
+ public ResponseEntity handlePermissionException(Exception ex) {
+ log.error("鉴权失败", ex);
+ return new ResponseEntity<>(R.fail(BizCode.PERMISSION_NOT_FOUND.getCode(), BizCode.PERMISSION_NOT_FOUND.getMsg()), HttpStatus.FORBIDDEN);
+ }
+
+ @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
+ public ResponseEntity handleDuplicateException(Exception ex) {
+ log.error("数据重复", ex);
+ return new ResponseEntity<>(R.fail(BizCode.USER_PHONE_EXIST.getCode(), BizCode.USER_PHONE_EXIST.getMsg()), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleCustomException(Exception ex) {
+ log.error("系统异常", ex);
+ return new ResponseEntity<>(R.fail(BizCode.General_Failure), HttpStatus.BAD_REQUEST);
+ }
+}
diff --git a/src/main/java/com/jiagutech/common/sa/SaPermissionImpl.java b/src/main/java/com/jiagutech/common/sa/SaPermissionImpl.java
new file mode 100644
index 0000000..b2014a2
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/sa/SaPermissionImpl.java
@@ -0,0 +1,38 @@
+package com.jiagutech.common.sa;
+
+import cn.dev33.satoken.stp.StpInterface;
+import com.jiagutech.dto.LoginUser;
+import com.jiagutech.utils.LoginUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * sa-token 权限管理实现类
+ *
+ * @author Lion Li
+ */
+public class SaPermissionImpl implements StpInterface {
+
+ /**
+ * 获取菜单权限列表
+ */
+ @Override
+ public List getPermissionList(Object loginId, String loginType) {
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * 获取角色权限列表
+ */
+ @Override
+ public List getRoleList(Object loginId, String loginType) {
+ LoginUser loginUser = LoginUtil.getLoginUser();
+ if (loginUser == null) {
+ return new ArrayList<>();
+ }
+ return Collections.emptyList();
+ }
+}
diff --git a/src/main/java/com/jiagutech/common/sa/SaTokenConfig.java b/src/main/java/com/jiagutech/common/sa/SaTokenConfig.java
new file mode 100644
index 0000000..05048dd
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/sa/SaTokenConfig.java
@@ -0,0 +1,30 @@
+package com.jiagutech.common.sa;
+
+import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
+import cn.dev33.satoken.stp.StpInterface;
+import cn.dev33.satoken.stp.StpLogic;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * sa-token 配置
+ */
+@Configuration
+public class SaTokenConfig {
+
+ @Bean
+ public StpLogic getStpLogicJwt() {
+ // Sa-Token 整合 jwt (简单模式)
+ return new StpLogicJwtForSimple();
+ }
+
+ /**
+ * 权限接口实现(使用bean注入方便用户替换)
+ */
+ @Bean
+ public StpInterface stpInterface() {
+ return new SaPermissionImpl();
+ }
+
+
+}
diff --git a/src/main/java/com/jiagutech/common/sa/SaTokenConfigure.java b/src/main/java/com/jiagutech/common/sa/SaTokenConfigure.java
new file mode 100644
index 0000000..3436490
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/sa/SaTokenConfigure.java
@@ -0,0 +1,34 @@
+package com.jiagutech.common.sa;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Slf4j
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+ // 注册 Sa-Token 拦截器,打开注解式鉴权功能
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ // 注册 Sa-Token 拦截器,打开注解式鉴权功能
+ registry.addInterceptor(new SaInterceptor(handle -> {
+ try {
+ log.info("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath());
+ StpUtil.checkLogin();
+ log.info("-------- 此 path 校验成功:" + SaHolder.getRequest().getRequestPath());
+ } catch (Exception e) {
+ log.info("-------- 此 path 校验失败:" + SaHolder.getRequest().getRequestPath());
+ throw e;
+ }
+ }))
+ .addPathPatterns("/**")
+ .excludePathPatterns("/user/login", "user/logout",
+ "/user/register", "/record/export/**", "/favicon.ico","/error",
+ "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs/**", "/doc.html/**", "/webjars/**"
+ );
+ }
+}
diff --git a/src/main/java/com/jiagutech/common/thread/MonitorThreadPoolExecutor.java b/src/main/java/com/jiagutech/common/thread/MonitorThreadPoolExecutor.java
new file mode 100644
index 0000000..8f857d5
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/MonitorThreadPoolExecutor.java
@@ -0,0 +1,105 @@
+package com.jiagutech.common.thread;
+
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.*;
+
+@Slf4j
+public class MonitorThreadPoolExecutor extends ThreadPoolExecutor {
+
+ private final ThreadLocal executeStartTime;
+ protected String poolName;
+ private final int slowTaskThreshold;
+ private final int queueTimeThreshold;
+
+ private final int queueSizeThreshold;
+
+ private static final int DEFAULT_SLOW_TASK_TIME = 1000;
+
+ private static final int DEFAULT_QUEUE_TIME = 100;
+
+ private static final int DEFAULT_QUEUE_SIZE = 450;
+
+
+ public MonitorThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, String poolName) {
+ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new NameThreadFactory(poolName), new AbortPolicy(), poolName);
+ }
+
+ public MonitorThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, String poolName) {
+ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new AbortPolicy(), poolName);
+ }
+
+ public MonitorThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler, String poolName) {
+ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new NameThreadFactory(poolName), handler, poolName);
+ }
+
+ public MonitorThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler, String poolName) {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
+ this.poolName = poolName;
+ executeStartTime = new ThreadLocal<>();
+ slowTaskThreshold = 0;
+ queueTimeThreshold = 0;
+ queueSizeThreshold = 0;
+ }
+
+ public MonitorThreadPoolExecutor(ThreadPoolConfig poolConfig) {
+ super(poolConfig.getCorePoolSize(),
+ poolConfig.getMaxPoolSize(),
+ poolConfig.getKeepAliveSeconds(),
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<>(poolConfig.getQueueCapacity()), new NameThreadFactory(poolConfig.getThreadName()),
+ (r, executor1) -> log.info("任务被中断"));
+ this.poolName = poolConfig.getThreadName();
+ executeStartTime = new ThreadLocal<>();
+ this.slowTaskThreshold = poolConfig.getTaskThreshold() > 0 ? poolConfig.getTaskThreshold() : DEFAULT_SLOW_TASK_TIME;
+ this.queueTimeThreshold = poolConfig.getQueueTimeThreshold() > 0 ? poolConfig.getQueueTimeThreshold() : DEFAULT_QUEUE_TIME;
+ this.queueSizeThreshold = poolConfig.getQueueSizeThreshold() > 0 ? poolConfig.getQueueSizeThreshold() : DEFAULT_QUEUE_SIZE;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ super.execute(command);
+ }
+
+ @Override
+ protected void beforeExecute(Thread t, Runnable r) {
+ super.beforeExecute(t, r);
+ executeStartTime.set(System.nanoTime());
+ }
+
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ MonitoredRunnable monitored = null;
+ try {
+ long executeEndNano = System.nanoTime();
+ Long executeStartTime = this.executeStartTime.get();
+ monitored = (MonitoredRunnable) r;
+ long queueNanoTime = monitored.getInQueueNanoTime();
+ int queueTime = (int) ((executeStartTime - queueNanoTime) / 1000000L);
+ int executeTime = (int) ((executeEndNano - executeStartTime) / 1000000L);
+ ThreadStatistics packStatistics = null;
+ if (monitored.getThreadLocal() != null) {
+ packStatistics = monitored.getThreadLocal().get();
+ }
+ if (executeTime > this.slowTaskThreshold || queueTime > this.queueTimeThreshold || getQueue().size() >= queueSizeThreshold) {
+ log.warn("线程池({}),触发告警,当前任务执行时间={},当前任务排队时间={},队列线程总数={} ", Thread.currentThread().getName(), executeTime, queueTime, getQueue().size());
+ if (packStatistics != null) {
+ log.warn("当前任务各阶段耗时统计:{}", packStatistics);
+ }
+ }
+
+ if (t != null) {
+ log.error("线程池名称 = {}, 执行异常的任务数+1", poolName);
+ }
+ } catch (Exception ignore) {
+ } finally {
+ executeStartTime.remove();
+ if (monitored != null && monitored.getThreadLocal() != null) {
+ monitored.getThreadLocal().remove();
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/java/com/jiagutech/common/thread/MonitoredRunnable.java b/src/main/java/com/jiagutech/common/thread/MonitoredRunnable.java
new file mode 100644
index 0000000..3e039a8
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/MonitoredRunnable.java
@@ -0,0 +1,40 @@
+package com.jiagutech.common.thread;
+
+
+
+public class MonitoredRunnable implements Runnable {
+
+ private final Runnable runnable;
+ private ThreadLocal threadLocal;
+ private final long inQueueNanoTime;
+
+ public MonitoredRunnable(Runnable runnable, ThreadLocal threadLocal) {
+ this.runnable = runnable;
+ this.threadLocal = threadLocal;
+ this.inQueueNanoTime = System.nanoTime();
+ }
+
+
+ public ThreadLocal getThreadLocal() {
+ return threadLocal;
+ }
+
+ public void setThreadLocal(ThreadLocal threadLocal) {
+ this.threadLocal = threadLocal;
+ }
+
+ public long getInQueueNanoTime() {
+ return inQueueNanoTime;
+ }
+
+ public MonitoredRunnable(Runnable runnable) {
+ this.runnable = runnable;
+ this.inQueueNanoTime = System.nanoTime();
+ }
+
+
+ @Override
+ public void run() {
+ this.runnable.run();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/thread/NameThreadFactory.java b/src/main/java/com/jiagutech/common/thread/NameThreadFactory.java
new file mode 100644
index 0000000..dd183e4
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/NameThreadFactory.java
@@ -0,0 +1,21 @@
+package com.jiagutech.common.thread;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class NameThreadFactory implements ThreadFactory {
+
+ private String namePrefix;
+
+ private AtomicInteger nextId = new AtomicInteger(1);
+
+ public NameThreadFactory(String namePrefix) {
+ this.namePrefix = namePrefix;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, namePrefix + nextId.getAndIncrement());
+ }
+
+ }
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/thread/ThreadExecutorConfig.java b/src/main/java/com/jiagutech/common/thread/ThreadExecutorConfig.java
new file mode 100644
index 0000000..c98d965
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/ThreadExecutorConfig.java
@@ -0,0 +1,31 @@
+package com.jiagutech.common.thread;
+
+import io.netty.util.concurrent.ThreadProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.Resource;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Slf4j
+@Configuration
+@ConditionalOnProperty(prefix = "thread", name = "enabled", havingValue = "true")
+public class ThreadExecutorConfig {
+ @Resource
+ private ThreadPoolConfig threadPoolConfig;
+
+
+ // @Primary
+ @Bean(name = "customThreadPool")
+ public ThreadPoolExecutor getcustomExecutor() {
+ return generateExecutor(threadPoolConfig);
+ }
+
+ private ThreadPoolExecutor generateExecutor(ThreadPoolConfig threadPoolConfig) {
+ return new MonitorThreadPoolExecutor(threadPoolConfig);
+ }
+
+
+}
diff --git a/src/main/java/com/jiagutech/common/thread/ThreadPoolConfig.java b/src/main/java/com/jiagutech/common/thread/ThreadPoolConfig.java
new file mode 100644
index 0000000..85ae211
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/ThreadPoolConfig.java
@@ -0,0 +1,29 @@
+package com.jiagutech.common.thread;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "thread.pool")
+public class ThreadPoolConfig {
+
+ private int corePoolSize;
+
+ private int maxPoolSize;
+
+ private int keepAliveSeconds;
+
+ private int queueCapacity;
+
+ private String threadName;
+
+ private int taskThreshold;
+
+ private int queueTimeThreshold;
+
+ private int queueSizeThreshold;
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/common/thread/ThreadStatistics.java b/src/main/java/com/jiagutech/common/thread/ThreadStatistics.java
new file mode 100644
index 0000000..bd7d1ad
--- /dev/null
+++ b/src/main/java/com/jiagutech/common/thread/ThreadStatistics.java
@@ -0,0 +1,118 @@
+package com.jiagutech.common.thread;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Data
+@Accessors(chain = true)
+public class ThreadStatistics {
+
+ private String imei;
+
+ private long totalCost;
+
+ private long start;
+
+ private long finish;
+
+ private Map phases = new ConcurrentHashMap<>();
+
+ /**
+ * 指定阶段处理统计
+ *
+ * @param statisticsEnum
+ */
+ public void startPhase(StatisticsEnum statisticsEnum) {
+ phases.computeIfAbsent(statisticsEnum, key -> new Item());
+ }
+
+ /**
+ * 完成指定部分的处理统计
+ *
+ * @param statisticsEnum 指定部分
+ */
+ public void finishPhase(StatisticsEnum statisticsEnum) {
+ Item item = phases.getOrDefault(statisticsEnum, null);
+ if (item != null) {
+ item.finish();
+ }
+ }
+
+ public void finishStatistics() {
+ this.finish = System.currentTimeMillis();
+ this.totalCost = this.finish - this.start;
+ }
+
+
+ public ThreadStatistics(long start) {
+ this.start = start;
+ }
+
+
+ @Data
+ public static class Item {
+ public Item() {
+ start = System.currentTimeMillis();
+ }
+
+ long start;
+
+ volatile long finish;
+
+ volatile long cost;
+
+ /**
+ * 完成处理统计
+ */
+ public void finish() {
+ if (finish != 0) {
+ return;
+ }
+ this.finish = System.currentTimeMillis();
+ this.cost = this.finish - start;
+ }
+
+ @Override
+ public String toString() {
+ return "Item{" +
+ "cost=" + cost +
+ '}';
+ }
+ }
+
+
+ public enum StatisticsEnum {
+ /**
+ * 基础信息解析
+ */
+ base_info,
+ /**
+ * 解密
+ */
+ decrypt,
+ /**
+ * 数据包解析
+ */
+ deserialization,
+
+ /**
+ * 数据保存到ck
+ */
+ save;
+
+ }
+
+ @Override
+ public String toString() {
+ return "Statistics{" +
+ "imei='" + imei + '\'' +
+ ", totalCost=" + totalCost +
+ ", start=" + start +
+ ", finish=" + finish +
+ ", phases=" + phases +
+ '}';
+ }
+}
diff --git a/src/main/java/com/jiagutech/controller/RecordController.java b/src/main/java/com/jiagutech/controller/RecordController.java
new file mode 100644
index 0000000..a7ef1f2
--- /dev/null
+++ b/src/main/java/com/jiagutech/controller/RecordController.java
@@ -0,0 +1,76 @@
+package com.jiagutech.controller;
+
+import cn.hutool.core.map.MapUtil;
+import com.jiagutech.dto.CreateRecordRequest;
+import com.jiagutech.dto.RecordItem;
+import com.jiagutech.dto.RecordModel;
+import com.jiagutech.dto.common.PageRequest;
+import com.jiagutech.dto.common.PageResult;
+import com.jiagutech.dto.common.R;
+import com.jiagutech.dto.RecordDetail;
+import com.jiagutech.entity.RecordContentEntity;
+import com.jiagutech.service.RecordService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.util.List;
+
+@Api(tags = "识别记录")
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/record")
+public class RecordController {
+ private final RecordService recordService;
+
+
+ @ApiOperation(value = "创建识别记录")
+ @PostMapping(value = "/createRecord", consumes = "multipart/form-data", produces = "application/json")
+ public R createRecord(@Valid @ModelAttribute CreateRecordRequest request) {
+ String taskId = recordService.createRecordAsync(request);
+ return R.ok(MapUtil.of("taskId", taskId));
+ }
+
+ @ApiOperation(value = "识别记录处理进度")
+ @GetMapping("/taskProgress/{taskId}")
+ public Float getTaskProgress(@PathVariable String taskId) {
+ return recordService.getTaskProgress(taskId);
+ }
+
+ @ApiOperation(value = "获取识别记录详情")
+ @GetMapping("/getRecordDetail/{recordId}")
+ public R getRecordDetail(@PathVariable Long recordId) {
+ return R.ok(recordService.getRecordDetail(recordId));
+ }
+
+ @ApiOperation(value = "导出识别记录",notes = "下载文件(响应类型为文件流)")
+ @GetMapping(value = "/export/{recordId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public void exportRecord(@PathVariable Long recordId, HttpServletResponse response) {
+ recordService.exportRecord(recordId, response);
+ }
+
+ @ApiOperation(value = "分页获取识别记录")
+ @PostMapping("/page")
+ public R> getPages(@RequestBody PageRequest pageRequest) {
+ return R.ok(recordService.getPages(pageRequest));
+ }
+
+ @ApiOperation(value = "更新识别记录内容")
+ @PutMapping("/updateRecordContent")
+ public void updateRecord(@RequestBody RecordContentEntity recordContentEntity) {
+ recordService.updateRecordContent(recordContentEntity);
+ }
+
+
+ @ApiOperation(value = "获取模型列表")
+ @GetMapping("/models")
+ public R getModels() {
+ return R.ok(recordService.getModels());
+ }
+}
diff --git a/src/main/java/com/jiagutech/controller/UserController.java b/src/main/java/com/jiagutech/controller/UserController.java
new file mode 100644
index 0000000..5deb9c0
--- /dev/null
+++ b/src/main/java/com/jiagutech/controller/UserController.java
@@ -0,0 +1,61 @@
+package com.jiagutech.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.jiagutech.dto.LoginRequest;
+import com.jiagutech.dto.LoginResponse;
+import com.jiagutech.dto.common.R;
+import com.jiagutech.dto.UserRequest;
+import com.jiagutech.service.UserService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@Api(tags = "用户")
+@Slf4j
+@RestController
+@RequestMapping("/user")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/info")
+ public String info() {
+ log.info("info");
+ return "info";
+ }
+
+ @ApiOperation(value = "登录")
+ @PostMapping("/login")
+ public R login(@Validated @RequestBody LoginRequest loginBody) {
+ LoginResponse loginVo = userService.login(loginBody);
+ return R.ok(loginVo);
+ }
+
+ @ApiOperation(value = "登出")
+ @GetMapping("/logout")
+ @PostMapping("/logout")
+ public R logout() {
+ StpUtil.logout();
+ return R.ok("logout");
+ }
+
+ @ApiOperation(value = "注册")
+ @PostMapping(value = "/register", consumes = "application/json")
+ public R register(@Validated @RequestBody UserRequest user) {
+ userService.register(user);
+ return R.ok();
+ }
+
+
+ @ApiOperation(value = "审批")
+ @PutMapping(value = "/approval/{userId}")
+ public R approval(@PathVariable Long userId) {
+ userService.approval(userId);
+ return R.ok();
+ }
+
+}
diff --git a/src/main/java/com/jiagutech/dto/CreateRecordRequest.java b/src/main/java/com/jiagutech/dto/CreateRecordRequest.java
new file mode 100644
index 0000000..812083e
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/CreateRecordRequest.java
@@ -0,0 +1,38 @@
+package com.jiagutech.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.*;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@ApiModel("创建记录请求")
+public class CreateRecordRequest {
+ @ApiModelProperty(value = "图片列表", required = true)
+ @NotEmpty(message = "文件不能为空")
+ private MultipartFile[] files;
+ @ApiModelProperty(value = "模型名称", required = true)
+ @NotBlank(message = "模型名称不能为空")
+ private String modelName;
+ @ApiModelProperty(value = "重叠度", required = true)
+ @NotNull(message = "IOU 不能为空")
+ @Min(value = 0, message = "IOU 不能小于 0")
+ @Max(value = 1, message = "IOU 不能大于 1")
+ private Float iou;
+ @ApiModelProperty(value = "置信度", required = true)
+ @NotNull(message = "Conf 不能为空")
+ @Min(value = 0, message = "Conf 不能小于 0")
+ @Max(value = 1, message = "Conf 不能大于 1")
+ private Float conf;
+ @ApiModelProperty(value = "任务名称", required = true)
+ @NotBlank(message = "任务名称不能为空")
+ private String taskName;
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/dto/LoginRequest.java b/src/main/java/com/jiagutech/dto/LoginRequest.java
new file mode 100644
index 0000000..3210b3b
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/LoginRequest.java
@@ -0,0 +1,21 @@
+package com.jiagutech.dto;
+
+
+import com.jiagutech.common.UserConstants;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+@Data
+public class LoginRequest implements Serializable {
+
+ @NotBlank(message = "{user.phone.not.blank}")
+ private String phone;
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+ private String password;
+
+ private String grantType = "password";
+}
diff --git a/src/main/java/com/jiagutech/dto/LoginResponse.java b/src/main/java/com/jiagutech/dto/LoginResponse.java
new file mode 100644
index 0000000..536dd54
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/LoginResponse.java
@@ -0,0 +1,37 @@
+package com.jiagutech.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 登录验证信息
+ */
+@Data
+public class LoginResponse {
+
+ /**
+ * 授权令牌
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+
+ /**
+ * 刷新令牌
+ */
+ @JsonProperty("refresh_token")
+ private String refreshToken;
+
+ /**
+ * 授权令牌 access_token 的有效期
+ */
+ @JsonProperty("expire_in")
+ private Long expireIn;
+
+ /**
+ * 刷新令牌 refresh_token 的有效期
+ */
+ @JsonProperty("refresh_expire_in")
+ private Long refreshExpireIn;
+
+
+}
diff --git a/src/main/java/com/jiagutech/dto/LoginUser.java b/src/main/java/com/jiagutech/dto/LoginUser.java
new file mode 100644
index 0000000..e841a5a
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/LoginUser.java
@@ -0,0 +1,86 @@
+package com.jiagutech.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @ClassName LoginUser
+ * @author: zhangyeguang
+ * @create: 2024-09-01 09:28
+ * @Version 1.0
+ * @description:
+ **/
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+
+ /**
+ * 用户ID
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long userId;
+
+ /**
+ * 部门
+ */
+ private String unitName;
+
+ /**
+ * 用户唯一标识
+ */
+ private String token;
+
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * 用户昵称
+ */
+ private String realName;
+ /**
+ * 手机号
+ */
+ private String phone;
+ @JsonIgnore
+ private String password;
+
+ private String email;
+ /**
+ * 头像
+ */
+ private String avatar;
+
+ private LocalDateTime createTime;
+
+ private Integer status;
+
+}
diff --git a/src/main/java/com/jiagutech/dto/ModelPredictData.java b/src/main/java/com/jiagutech/dto/ModelPredictData.java
new file mode 100644
index 0000000..bf05656
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/ModelPredictData.java
@@ -0,0 +1,10 @@
+package com.jiagutech.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ModelPredictData{
+ private List recognitionData;
+}
diff --git a/src/main/java/com/jiagutech/dto/ModelPredictResponse.java b/src/main/java/com/jiagutech/dto/ModelPredictResponse.java
new file mode 100644
index 0000000..514387c
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/ModelPredictResponse.java
@@ -0,0 +1,14 @@
+package com.jiagutech.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ModelPredictResponse {
+ private Integer classify;
+ private String name;
+ private int[][] points;
+ private Float length;
+}
+
diff --git a/src/main/java/com/jiagutech/dto/RecordContentExcel.java b/src/main/java/com/jiagutech/dto/RecordContentExcel.java
new file mode 100644
index 0000000..15d26fa
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/RecordContentExcel.java
@@ -0,0 +1,31 @@
+package com.jiagutech.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+@Data
+public class RecordContentExcel {
+
+ @ExcelProperty("文件名")
+ private String name;
+ @ExcelProperty("总喙数")
+ private int beakNum;
+ @ExcelProperty("总柄数")
+ private int handleNum;
+ @ExcelProperty("总角果数")
+ private int rapeNum;
+ @ExcelProperty("置信度")
+ private float conf;
+ @ExcelProperty("重叠度")
+ private float iou;
+ @ExcelProperty("比例系数")
+ private int scale;
+
+ @ExcelProperty("角果长")
+ private float rapeLength;
+
+ @ExcelProperty("喙长")
+ private float beakLength;
+ @ExcelProperty("柄长")
+ private float handleLength;
+}
diff --git a/src/main/java/com/jiagutech/dto/RecordDetail.java b/src/main/java/com/jiagutech/dto/RecordDetail.java
new file mode 100644
index 0000000..a3988cd
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/RecordDetail.java
@@ -0,0 +1,27 @@
+package com.jiagutech.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jiagutech.entity.RecordContentEntity;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+public class RecordDetail {
+ private Long id;
+
+ private String name;
+
+ private int imageNum;
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+ ;
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime updateTime;
+
+ private List content;
+}
diff --git a/src/main/java/com/jiagutech/dto/RecordExcel.java b/src/main/java/com/jiagutech/dto/RecordExcel.java
new file mode 100644
index 0000000..269bdb5
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/RecordExcel.java
@@ -0,0 +1,31 @@
+package com.jiagutech.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+@Data
+public class RecordExcel {
+
+ @ExcelProperty("总图片数")
+ private int imageNum;
+ @ExcelProperty("总喙数")
+ private int beakNum;
+ @ExcelProperty("总柄数")
+ private int handleNum;
+ @ExcelProperty("总角果数")
+ private int rapeNum;
+ @ExcelProperty("平均角果数")
+ private float avgRapeNum;
+ @ExcelProperty("最大角果数")
+ private int maxRapeNum;
+ @ExcelProperty("最少角果数")
+ private int minRapeNum;
+
+ @ExcelProperty("平均角果长度")
+ private float avgRapeLength;
+
+ @ExcelProperty("平均喙长度")
+ private float avgBeakLength;
+ @ExcelProperty("平均柄长度")
+ private float avgHandleLength;
+}
diff --git a/src/main/java/com/jiagutech/dto/RecordItem.java b/src/main/java/com/jiagutech/dto/RecordItem.java
new file mode 100644
index 0000000..4162674
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/RecordItem.java
@@ -0,0 +1,29 @@
+package com.jiagutech.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+public class RecordItem {
+
+ private String recordName;
+
+ private Long recordId;
+
+ private int imageNum;
+
+ private String userName;
+
+ private Long userId;
+
+ private String unitName;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+
+ private String phone;
+}
diff --git a/src/main/java/com/jiagutech/dto/RecordModel.java b/src/main/java/com/jiagutech/dto/RecordModel.java
new file mode 100644
index 0000000..d381ed6
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/RecordModel.java
@@ -0,0 +1,26 @@
+package com.jiagutech.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class RecordModel {
+ private List loaded_models;
+
+ private List models;
+
+ @Data
+ public static class ModelDetail {
+
+ private float conf;
+
+ private float iou;
+
+ private String name;
+
+ private String desc;
+
+ private String file;
+ }
+}
diff --git a/src/main/java/com/jiagutech/dto/UserRequest.java b/src/main/java/com/jiagutech/dto/UserRequest.java
new file mode 100644
index 0000000..6cb2188
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/UserRequest.java
@@ -0,0 +1,43 @@
+package com.jiagutech.dto;
+
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+/**
+ * @ClassName UserRequest
+ * @author: zhangyeguang
+ * @create: 2024-08-31 10:00
+ * @Version 1.0
+ * @description:
+ **/
+@Data
+@NoArgsConstructor
+public class UserRequest {
+
+ /**
+ * 用户昵称
+ */
+ @Size(min = 2, max = 30, message = "用户姓名长度不能超过{max}个字符")
+ private String realName;
+
+ /**
+ * 手机号码
+ */
+ @Pattern(regexp = "^1[34578]\\d{9}$", message = "手机号码格式不正确")
+ private String phone;
+
+
+ /**
+ * 密码
+ */
+ private String password;
+
+
+ private String unitName;
+
+
+}
diff --git a/src/main/java/com/jiagutech/dto/common/PageRequest.java b/src/main/java/com/jiagutech/dto/common/PageRequest.java
new file mode 100644
index 0000000..e285dcf
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/common/PageRequest.java
@@ -0,0 +1,23 @@
+package com.jiagutech.dto.common;
+
+import lombok.Data;
+
+/**
+ * @ClassName PageInfo
+ * @author: zhangyeguang
+ * @create: 2024-09-02 15:21
+ * @Version 1.0
+ * @description:
+ **/
+@Data
+public class PageRequest implements java.io.Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private int pageNum;
+ private int pageSize;
+ private String oderByColumn;
+
+ private T request;
+
+
+}
diff --git a/src/main/java/com/jiagutech/dto/common/PageResult.java b/src/main/java/com/jiagutech/dto/common/PageResult.java
new file mode 100644
index 0000000..53f8bd2
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/common/PageResult.java
@@ -0,0 +1,33 @@
+package com.jiagutech.dto.common;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+/**
+ * @ClassName PageResult
+ * @author: zhangyeguang
+ * @create: 2024-09-02 15:22
+ * @Version 1.0
+ * @description:
+ **/
+@Data
+@Accessors(chain = true)
+public class PageResult {
+ private int total;
+ private int pageSize;
+ private int pageNum;
+ private int pages;
+ private List records;
+
+ public static PageResult of(int total, int pageSize, int pageNum, List records) {
+ PageResult pageResult = new PageResult<>();
+ pageResult.setTotal(total);
+ pageResult.setPageSize(pageSize);
+ pageResult.setPageNum(pageNum);
+ pageResult.setPages(total==0||pageSize==0?0:total % pageSize == 0 ? total / pageSize : total / pageSize + 1);
+ pageResult.setRecords(records);
+ return pageResult;
+ }
+}
diff --git a/src/main/java/com/jiagutech/dto/common/R.java b/src/main/java/com/jiagutech/dto/common/R.java
new file mode 100644
index 0000000..d7a0f34
--- /dev/null
+++ b/src/main/java/com/jiagutech/dto/common/R.java
@@ -0,0 +1,110 @@
+package com.jiagutech.dto.common;
+
+
+
+import com.jiagutech.common.HttpStatus;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.io.Serializable;
+
+/**
+ * 响应信息主体
+ *
+ * @author Lion Li
+ */
+@Data
+@NoArgsConstructor
+public class R implements Serializable {
+
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 成功
+ */
+ public static final int SUCCESS = 200;
+
+ /**
+ * 失败
+ */
+ public static final int FAIL = 500;
+
+ private int code;
+
+ private String msg;
+
+ private T data;
+
+ public static R ok() {
+ return restResult(null, SUCCESS, "操作成功");
+ }
+
+ public static R ok(T data) {
+ return restResult(data, SUCCESS, "操作成功");
+ }
+
+ public static R ok(String msg) {
+ return restResult(null, SUCCESS, msg);
+ }
+
+ public static R ok(String msg, T data) {
+ return restResult(data, SUCCESS, msg);
+ }
+
+ public static R fail() {
+ return restResult(null, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg) {
+ return restResult(null, FAIL, msg);
+ }
+
+ public static R fail(T data) {
+ return restResult(data, FAIL, "操作失败");
+ }
+
+ public static R fail(String msg, T data) {
+ return restResult(data, FAIL, msg);
+ }
+
+ public static R fail(int code, String msg) {
+ return restResult(null, code, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @return 警告消息
+ */
+ public static R warn(String msg) {
+ return restResult(null, HttpStatus.WARN, msg);
+ }
+
+ /**
+ * 返回警告消息
+ *
+ * @param msg 返回内容
+ * @param data 数据对象
+ * @return 警告消息
+ */
+ public static R warn(String msg, T data) {
+ return restResult(data, HttpStatus.WARN, msg);
+ }
+
+ private static R restResult(T data, int code, String msg) {
+ R r = new R<>();
+ r.setCode(code);
+ r.setData(data);
+ r.setMsg(msg);
+ return r;
+ }
+
+ public static Boolean isError(R ret) {
+ return !isSuccess(ret);
+ }
+
+ public static Boolean isSuccess(R ret) {
+ return R.SUCCESS == ret.getCode();
+ }
+}
diff --git a/src/main/java/com/jiagutech/entity/RecordContentEntity.java b/src/main/java/com/jiagutech/entity/RecordContentEntity.java
new file mode 100644
index 0000000..c958476
--- /dev/null
+++ b/src/main/java/com/jiagutech/entity/RecordContentEntity.java
@@ -0,0 +1,42 @@
+package com.jiagutech.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.jiagutech.dto.ModelPredictResponse;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+@TableName(value = "record_content", autoResultMap = true)
+public class RecordContentEntity {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private Long recordId;
+
+ private String imageName;
+
+ private String imageUrl;
+ @TableField(typeHandler = JacksonTypeHandler.class)
+ private List recognitionData;
+
+ private int siliquaNum;
+
+ private int handleNum;
+
+ private int beakNum;
+
+ private int size;
+ //比例
+ private int scale;
+
+ private float conf;
+
+ private float iou;
+}
diff --git a/src/main/java/com/jiagutech/entity/RecordEntity.java b/src/main/java/com/jiagutech/entity/RecordEntity.java
new file mode 100644
index 0000000..0d5aefd
--- /dev/null
+++ b/src/main/java/com/jiagutech/entity/RecordEntity.java
@@ -0,0 +1,29 @@
+package com.jiagutech.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.springframework.data.annotation.Id;
+
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+@TableName("record_info")
+public class RecordEntity {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String name;
+
+ private Long userId;
+
+ private int imageNum;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+
+}
diff --git a/src/main/java/com/jiagutech/entity/UserEntity.java b/src/main/java/com/jiagutech/entity/UserEntity.java
new file mode 100644
index 0000000..4063f7c
--- /dev/null
+++ b/src/main/java/com/jiagutech/entity/UserEntity.java
@@ -0,0 +1,28 @@
+package com.jiagutech.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("siliqua_user")
+@Accessors(chain = true)
+public class UserEntity {
+ @TableId
+ private long id;
+
+ private String phone;
+ private String password;
+ private String realName;
+
+ private String unitName;
+
+ private LocalDateTime createTime;
+
+ private int status;
+
+
+}
diff --git a/src/main/java/com/jiagutech/handler/AddCellRangeWriteHandler.java b/src/main/java/com/jiagutech/handler/AddCellRangeWriteHandler.java
new file mode 100644
index 0000000..416c290
--- /dev/null
+++ b/src/main/java/com/jiagutech/handler/AddCellRangeWriteHandler.java
@@ -0,0 +1,26 @@
+package com.jiagutech.handler;
+
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AddCellRangeWriteHandler implements SheetWriteHandler {
+
+ private final List rangeCellList;
+
+ public AddCellRangeWriteHandler(List rangeCellList) {
+ this.rangeCellList = (rangeCellList == null) ? Collections.emptyList() : rangeCellList;
+ }
+
+ public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+ Sheet sheet = writeSheetHolder.getSheet();
+ for (CellRangeAddress cellRangeAddress : this.rangeCellList) {
+ sheet.addMergedRegionUnsafe(cellRangeAddress);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/handler/ExcelFillCellMergeStrategy.java b/src/main/java/com/jiagutech/handler/ExcelFillCellMergeStrategy.java
new file mode 100644
index 0000000..0cb83bf
--- /dev/null
+++ b/src/main/java/com/jiagutech/handler/ExcelFillCellMergeStrategy.java
@@ -0,0 +1,103 @@
+package com.jiagutech.handler;
+
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+@Slf4j
+public class ExcelFillCellMergeStrategy implements CellWriteHandler {
+
+ // 需要创建合并区的列
+ private int[] mergeColumnIndex;
+ // 从第几行后开始合并,取列头行
+ private int mergeRowIndex;
+
+ public ExcelFillCellMergeStrategy() {
+ }
+
+ public ExcelFillCellMergeStrategy(int mergeRowIndex, int[] mergeColumnIndex) {
+ this.mergeRowIndex = mergeRowIndex;
+ this.mergeColumnIndex = mergeColumnIndex;
+ }
+
+ @Override
+ public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex,
+ Integer relativeRowIndex, Boolean isHead) {
+
+ }
+
+ @Override
+ public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex,
+ Boolean isHead) {
+
+ }
+
+ @Override
+ public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
+ List> list, Cell cell, Head head,
+ Integer integer, Boolean aBoolean) {
+ if (aBoolean){
+ log.info("表头,不处理");
+ return;
+ }
+ int curRowIndex = cell.getRowIndex();
+ int curColIndex = cell.getColumnIndex();
+ if (curRowIndex > mergeRowIndex) {
+ for (int i = 0; i < mergeColumnIndex.length; i++) {
+ // 需合并的列
+ if (curColIndex == mergeColumnIndex[i]) {
+ mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * 当前单元格向上合并
+ *
+ * @param writeSheetHolder
+ * @param cell 当前单元格
+ * @param curRowIndex 当前行
+ * @param curColIndex 当前列
+ */
+ private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
+ Object curData = cell.getCellType() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
+ Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
+ Object preData = preCell.getCellType() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
+ // 将当前单元格数据与上一个单元格数据比较
+ Boolean dataBool = preData.equals(curData);
+ //此处需要注意:因为我是按照序号确定是否需要合并的,所以获取每一行第一列数据和上一行第一列数据进行比较,如果相等合并
+ Boolean bool = cell.getRow().getCell(0).getNumericCellValue() == cell.getSheet().getRow(curRowIndex - 1).getCell(0).getNumericCellValue();
+ if (dataBool && bool) {
+ Sheet sheet = writeSheetHolder.getSheet();
+ List mergeRegions = sheet.getMergedRegions();
+ boolean isMerged = false;
+ for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
+ CellRangeAddress cellRangeAddr = mergeRegions.get(i);
+ // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
+ if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
+ sheet.removeMergedRegion(i);
+ cellRangeAddr.setLastRow(curRowIndex);
+ sheet.addMergedRegion(cellRangeAddr);
+ isMerged = true;
+ }
+ }
+ // 若上一个单元格未被合并,则新增合并单元
+ if (!isMerged) {
+ CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
+ sheet.addMergedRegion(cellRangeAddress);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/handler/ModelPredictResponseListTypeHandler.java b/src/main/java/com/jiagutech/handler/ModelPredictResponseListTypeHandler.java
new file mode 100644
index 0000000..d77d2a8
--- /dev/null
+++ b/src/main/java/com/jiagutech/handler/ModelPredictResponseListTypeHandler.java
@@ -0,0 +1,27 @@
+package com.jiagutech.handler;
+
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.jiagutech.dto.ModelPredictResponse;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+public class ModelPredictResponseListTypeHandler extends JacksonTypeHandler {
+ public ModelPredictResponseListTypeHandler(Class> type) {
+ super(type);
+ }
+
+ @Override
+ public Object getResult(ResultSet rs, String columnName) throws SQLException {
+ String json = rs.getString(columnName);
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ return objectMapper.readValue(json, new TypeReference>(){});
+ } catch (Exception e) {
+ throw new SQLException("Error deserializing recognition_data", e);
+ }
+ }
+}
diff --git a/src/main/java/com/jiagutech/mapper/RecordContentMapper.java b/src/main/java/com/jiagutech/mapper/RecordContentMapper.java
new file mode 100644
index 0000000..70b63f8
--- /dev/null
+++ b/src/main/java/com/jiagutech/mapper/RecordContentMapper.java
@@ -0,0 +1,13 @@
+package com.jiagutech.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jiagutech.entity.RecordContentEntity;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+
+public interface RecordContentMapper extends BaseMapper {
+
+ List selectListByRecordId(@Param("recordId") Long recordId);
+}
diff --git a/src/main/java/com/jiagutech/mapper/RecordMapper.java b/src/main/java/com/jiagutech/mapper/RecordMapper.java
new file mode 100644
index 0000000..4fbbde0
--- /dev/null
+++ b/src/main/java/com/jiagutech/mapper/RecordMapper.java
@@ -0,0 +1,14 @@
+package com.jiagutech.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jiagutech.dto.RecordDetail;
+import com.jiagutech.dto.RecordItem;
+import com.jiagutech.entity.RecordEntity;
+
+public interface RecordMapper extends BaseMapper {
+
+ RecordDetail getRecordDetail(long id);
+
+ Page pageList(Page page, Object o);
+}
diff --git a/src/main/java/com/jiagutech/mapper/UserMapper.java b/src/main/java/com/jiagutech/mapper/UserMapper.java
new file mode 100644
index 0000000..7259442
--- /dev/null
+++ b/src/main/java/com/jiagutech/mapper/UserMapper.java
@@ -0,0 +1,10 @@
+package com.jiagutech.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jiagutech.dto.LoginUser;
+import com.jiagutech.entity.RecordContentEntity;
+import com.jiagutech.entity.UserEntity;
+
+public interface UserMapper extends BaseMapper {
+ LoginUser selectLoginUserByPhone(String phone);
+}
diff --git a/src/main/java/com/jiagutech/record/RecordTaskHandler.java b/src/main/java/com/jiagutech/record/RecordTaskHandler.java
new file mode 100644
index 0000000..9dae20d
--- /dev/null
+++ b/src/main/java/com/jiagutech/record/RecordTaskHandler.java
@@ -0,0 +1,93 @@
+package com.jiagutech.record;
+
+import com.jiagutech.common.thread.MonitoredRunnable;
+import com.jiagutech.dto.CreateRecordRequest;
+import com.jiagutech.dto.LoginUser;
+import com.jiagutech.utils.LoginUtil;
+import lombok.Data;
+import lombok.SneakyThrows;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.net.URL;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Data
+@Accessors(chain = true)
+@Slf4j
+public class RecordTaskHandler {
+
+ private RecordTask recordTask;
+ private ThreadPoolExecutor theadPoolExecutor;
+ private RecordTaskProcessor processor;
+
+
+
+ public RecordTaskHandler(ThreadPoolExecutor threadPoolExecutor, RecordTaskProcessor processor) {
+ this.theadPoolExecutor = threadPoolExecutor;
+ this.processor = processor;
+ }
+
+ protected void processTask() {
+ theadPoolExecutor.execute(new MonitoredRunnable(() -> {
+ try {
+ processor.handle(recordTask);
+ log.info("任务完成,任务ID:{}", recordTask.getTaskId());
+ } finally {
+ for (File file : recordTask.getFiles()) {
+ log.info("删除临时文件:{}", file.getAbsolutePath());
+ if (file.exists()) {
+ if (!file.delete())
+ log.error("删除临时文件失败:{}", file.getAbsolutePath());
+ }
+ }
+ RecordTaskManager.removeRecordTask(recordTask.getTaskId());
+ recordTask = null;
+
+ }
+ }));
+ }
+
+ @SneakyThrows
+ public void createTask(CreateRecordRequest request, String taskId) {
+ URL resource = RecordTaskHandler.class.getClassLoader().getResource("");
+ assert resource != null;
+ String path = resource.getPath();
+ File[] files = new File[request.getFiles().length];
+ for (int i = 0; i < request.getFiles().length; i++) {
+ MultipartFile file = request.getFiles()[i];
+ File file1 = new File(path + file.getOriginalFilename());
+ file.transferTo(file1);
+ files[i] = file1;
+ }
+
+ this.recordTask = new RecordTask()
+ .setFiles(files)
+ .setModelName(request.getModelName())
+ .setIou(request.getIou())
+ .setConf(request.getConf())
+ .setProgress(5f)
+ .setTaskName(request.getTaskName())
+ .setTaskId(taskId)
+ .setLoginUser(LoginUtil.getLoginUser());
+ processTask();
+ }
+
+ @Data
+ @Accessors(chain = true)
+ public static class RecordTask {
+ private String taskId;
+ private File[] files;
+ private String modelName;
+ private Float iou;
+ private Float conf;
+ private Float progress;
+ private String taskName;
+ private LoginUser loginUser;
+
+ }
+
+
+}
diff --git a/src/main/java/com/jiagutech/record/RecordTaskManager.java b/src/main/java/com/jiagutech/record/RecordTaskManager.java
new file mode 100644
index 0000000..4352fdb
--- /dev/null
+++ b/src/main/java/com/jiagutech/record/RecordTaskManager.java
@@ -0,0 +1,21 @@
+package com.jiagutech.record;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+public class RecordTaskManager {
+
+ private static final ConcurrentHashMap recordTasks = new ConcurrentHashMap<>();
+
+ public static void addRecordTask(String taskId, RecordTaskHandler handler) {
+ recordTasks.put(taskId, handler);
+ }
+
+ public static RecordTaskHandler getRecordTask(String taskId) {
+ return recordTasks.get(taskId);
+ }
+
+ public static void removeRecordTask(String taskId) {
+ recordTasks.remove(taskId);
+ }
+
+}
diff --git a/src/main/java/com/jiagutech/record/RecordTaskProcessor.java b/src/main/java/com/jiagutech/record/RecordTaskProcessor.java
new file mode 100644
index 0000000..234325f
--- /dev/null
+++ b/src/main/java/com/jiagutech/record/RecordTaskProcessor.java
@@ -0,0 +1,112 @@
+package com.jiagutech.record;
+
+import com.jiagutech.client.RecognitionModelClient;
+import com.jiagutech.dto.ModelPredictResponse;
+import com.jiagutech.entity.RecordContentEntity;
+import com.jiagutech.entity.RecordEntity;
+import com.jiagutech.mapper.RecordContentMapper;
+import com.jiagutech.mapper.RecordMapper;
+import com.jiagutech.utils.HuaweiObs;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RecordTaskProcessor {
+ private final RecordMapper recordMapper;
+
+ private final RecordContentMapper recordContentMapper;
+
+ private final RecognitionModelClient modelClient;
+
+ private final HuaweiObs huaweiObs;
+
+ @Transactional
+ public void handle(@NonNull RecordTaskHandler.RecordTask recordTask) {
+ log.info("开始处理任务:{}", recordTask.getTaskId());
+ float progress = 5f;
+ RecordEntity recordEntity = new RecordEntity().setName(recordTask.getTaskName())
+ .setImageNum(recordTask.getFiles().length)
+ .setUserId(recordTask.getLoginUser().getUserId())
+ .setCreateTime(LocalDateTime.now())
+ .setUpdateTime(LocalDateTime.now());
+ recordMapper.insert(recordEntity);
+ recordTask.setProgress(progress);
+ for (File file : recordTask.getFiles()) {
+ List predict = modelClient.predict(file, recordTask.getModelName(), recordTask.getIou(), recordTask.getConf());
+ int handleNum = 0;
+ int rapeNum = 0;
+ int beakNum = 0;
+ for (ModelPredictResponse item : predict) {
+ item.setLength(getMaxLength(item.getPoints()));
+ switch (item.getClassify()) {
+ case 0:
+ rapeNum++;
+ break;
+ case 1:
+ beakNum++;
+ break;
+ default:
+ handleNum++;
+ }
+ }
+ progress += (float) (100 / recordTask.getFiles().length - 10);
+ recordTask.setProgress(progress);
+ if (!CollectionUtils.isEmpty(predict)) {
+ String imageUrl;
+ try {
+ imageUrl = huaweiObs.uploadFile(file);
+ progress += 2f;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ file.deleteOnExit();
+ }
+ RecordContentEntity recordContentEntity = new RecordContentEntity()
+ .setRecordId(recordEntity.getId()).setImageUrl(imageUrl)
+ .setRecognitionData(predict)
+ .setImageName(file.getName())
+ .setConf(recordTask.getConf())
+ .setIou(recordTask.getIou())
+ .setHandleNum(handleNum)
+ .setBeakNum(beakNum)
+ .setSiliquaNum(rapeNum);
+ recordContentMapper.insert(recordContentEntity);
+ progress += 3f;
+ }
+
+ }
+
+ }
+
+
+ private static double calculateDistance(int x1, int y1, int x2, int y2) {
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+ }
+
+ private float getMaxLength(int[][] points) {
+ // 计算四条边的长度
+ double ab = calculateDistance(points[0][0], points[0][1], points[1][0], points[1][1]); // AB
+ double bc = calculateDistance(points[1][0], points[1][1], points[2][0], points[2][1]); // BC
+ double cd = calculateDistance(points[2][0], points[2][1], points[3][0], points[3][1]); // CD
+ double da = calculateDistance(points[3][0], points[3][1], points[0][0], points[0][1]); // DA
+ double max = Math.max(Math.max(ab, bc), Math.max(cd, da));
+ BigDecimal b = new BigDecimal(max);
+ // 找出最大边长
+ return b.setScale(2, RoundingMode.HALF_UP).floatValue();
+
+
+ }
+
+}
diff --git a/src/main/java/com/jiagutech/service/RecordService.java b/src/main/java/com/jiagutech/service/RecordService.java
new file mode 100644
index 0000000..bb8c631
--- /dev/null
+++ b/src/main/java/com/jiagutech/service/RecordService.java
@@ -0,0 +1,33 @@
+package com.jiagutech.service;
+
+import com.jiagutech.dto.CreateRecordRequest;
+import com.jiagutech.dto.RecordDetail;
+import com.jiagutech.dto.RecordItem;
+import com.jiagutech.dto.RecordModel;
+import com.jiagutech.dto.common.PageRequest;
+import com.jiagutech.dto.common.PageResult;
+import com.jiagutech.entity.RecordContentEntity;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.servlet.http.HttpServletResponse;
+
+public interface RecordService {
+
+ SseEmitter createRecord(MultipartFile[] files, String modelName, Float lou, Float conf);
+
+ String createRecordAsync(CreateRecordRequest request);
+
+ Float getTaskProgress(String taskId);
+
+ RecordDetail getRecordDetail(Long recordId);
+
+ void exportRecord(Long recordId, HttpServletResponse response);
+
+ PageResult getPages(PageRequest pageRequest);
+
+ void updateRecordContent(RecordContentEntity recordContentEntity);
+
+ RecordModel getModels();
+
+}
diff --git a/src/main/java/com/jiagutech/service/RecordServiceImpl.java b/src/main/java/com/jiagutech/service/RecordServiceImpl.java
new file mode 100644
index 0000000..1dc0c4a
--- /dev/null
+++ b/src/main/java/com/jiagutech/service/RecordServiceImpl.java
@@ -0,0 +1,286 @@
+package com.jiagutech.service;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.util.MapUtils;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.alibaba.excel.write.merge.OnceAbsoluteMergeStrategy;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.alibaba.excel.write.metadata.style.WriteCellStyle;
+import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
+import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.jiagutech.client.RecognitionModelClient;
+import com.jiagutech.dto.*;
+import com.jiagutech.dto.common.PageRequest;
+import com.jiagutech.dto.common.PageResult;
+import com.jiagutech.entity.RecordContentEntity;
+import com.jiagutech.handler.ExcelFillCellMergeStrategy;
+import com.jiagutech.mapper.RecordContentMapper;
+import com.jiagutech.mapper.RecordMapper;
+import com.jiagutech.record.RecordTaskHandler;
+import com.jiagutech.record.RecordTaskManager;
+import com.jiagutech.record.RecordTaskProcessor;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.math3.analysis.function.Max;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URLEncoder;
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RecordServiceImpl implements RecordService {
+
+ private final ThreadPoolExecutor customThreadPool;
+
+ private final RecordTaskProcessor taskProcessor;
+ private final RecordMapper recordMapper;
+ private final RecordContentMapper recordContentMapper;
+ private final RecognitionModelClient recognitionModelClient;
+
+ @Override
+ public SseEmitter createRecord(MultipartFile[] files, String modelName, Float lou, Float conf) {
+ return null;
+ }
+
+ @Override
+ public String createRecordAsync(CreateRecordRequest request) {
+ String taskId = UUID.randomUUID().toString();
+ RecordTaskHandler recordTaskHandler = new RecordTaskHandler(customThreadPool, taskProcessor);
+ recordTaskHandler.createTask(request, taskId);
+ RecordTaskManager.addRecordTask(taskId, recordTaskHandler);
+ return taskId;
+ }
+
+
+ @Override
+ public Float getTaskProgress(String taskId) {
+ RecordTaskHandler recordTask = RecordTaskManager.getRecordTask(taskId);
+ if (recordTask == null) {
+ return 100f;
+ }
+ if (recordTask.getRecordTask() == null) {
+ return 100f;
+ }
+
+ return recordTask.getRecordTask().getProgress();
+ }
+
+ @Override
+ public RecordDetail getRecordDetail(Long recordId) {
+ return recordMapper.getRecordDetail(recordId);
+ }
+
+ @Override
+ public void exportRecord(Long recordId, HttpServletResponse response) {
+ RecordDetail recordDetail = this.getRecordDetail(recordId);
+ List content = recordDetail.getContent();
+ if (CollectionUtils.isNotEmpty(content)) {
+ RecordExcel recordExcel = new RecordExcel();
+ recordExcel.setImageNum(recordDetail.getImageNum());
+ int beakNum = 0;
+ int rapeNum = 0;
+ int handleNum = 0;
+ int rapeMax = 0;
+ int rapeMin = 10000;
+ List avgBeakLen = new ArrayList<>();
+ List avgRapeLen = new ArrayList<>();
+ List avgHandleLen = new ArrayList<>();
+ List recordContentExcelList = new ArrayList<>();
+ List mergeStrategies = new ArrayList<>();
+ int startRowIndex = 1;
+ int endRowIndex = 0;
+ //处理记录详情数据
+ for (RecordContentEntity record : content) {
+ //统计各分类总数
+ beakNum += record.getBeakNum();
+ rapeNum += record.getSiliquaNum();
+ handleNum += record.getHandleNum();
+ if (record.getSiliquaNum() > rapeMax) {
+ rapeMax = record.getSiliquaNum();
+ }
+ if (record.getSiliquaNum() < rapeMin) {
+ rapeMin = record.getSiliquaNum();
+ }
+ List rapeLenList = new ArrayList<>();
+ List beakLenList = new ArrayList<>();
+ List handleLenList = new ArrayList<>();
+
+ List recognitionData = record.getRecognitionData();
+ int size = record.getSize();
+ int scale = record.getScale();
+ //把识别数据进行分类
+ for (ModelPredictResponse item : recognitionData) {
+ float len;
+ if (size > 0 && scale > 0) {
+ BigDecimal b = new BigDecimal(item.getLength() * size / scale);
+ len = b.setScale(2, RoundingMode.HALF_UP).floatValue();
+ } else {
+ len = item.getLength();
+ }
+
+ switch (item.getClassify()) {
+ case 0:
+ rapeLenList.add(len);
+ break;
+ case 1:
+ beakLenList.add(len);
+ break;
+ default:
+ handleLenList.add(len);
+ }
+ }
+ int len = Math.max(rapeLenList.size(), Math.max(handleLenList.size(), beakLenList.size()));
+ //创建表格行记录
+ for (int i = 0; i < len; i++) {
+ endRowIndex++;
+ RecordContentExcel contentExcel = new RecordContentExcel();
+ //需要合并的列开始
+ contentExcel.setName(record.getImageName());
+ contentExcel.setBeakNum(record.getBeakNum());
+ contentExcel.setRapeNum(record.getSiliquaNum());
+ contentExcel.setHandleNum(record.getHandleNum());
+ contentExcel.setScale(record.getScale());
+ contentExcel.setConf(record.getConf());
+ contentExcel.setIou(record.getIou());
+ //需要合并的列结束
+ recordContentExcelList.add(contentExcel);
+ if (i <= rapeLenList.size() - 1) {
+ contentExcel.setRapeLength(rapeLenList.get(i));
+ }
+ if (i <= beakLenList.size() - 1) {
+ contentExcel.setBeakLength(beakLenList.get(i));
+ }
+ if (i <= handleLenList.size() - 1) {
+ contentExcel.setHandleLength(handleLenList.get(i));
+ }
+ }
+ //创建单元格的合并策略, 前7列数据需要合并
+ for (int i = 0; i <= 6; i++) {
+ OnceAbsoluteMergeStrategy mergeStrategy = new OnceAbsoluteMergeStrategy(
+ startRowIndex,
+ endRowIndex,
+ i, i);
+ mergeStrategies.add(mergeStrategy);
+ }
+
+ startRowIndex += len;
+ avgHandleLen.add(handleLenList.stream().mapToDouble(Float::doubleValue).average().orElse(0.0));
+ avgRapeLen.add(rapeLenList.stream().mapToDouble(Float::doubleValue).average().orElse(0.0));
+ avgBeakLen.add(beakLenList.stream().mapToDouble(Float::doubleValue).average().orElse(0.0));
+ }
+ recordExcel.setBeakNum(beakNum);
+ recordExcel.setRapeNum(rapeNum);
+ recordExcel.setHandleNum(handleNum);
+ recordExcel.setMaxRapeNum(rapeMax);
+ recordExcel.setMinRapeNum(rapeMin);
+ DecimalFormat dfnum = new DecimalFormat("##0.00");
+ float avgRapeNum = Float.parseFloat(dfnum.format((float) rapeNum / content.size()));
+ recordExcel.setAvgRapeNum(avgRapeNum);
+ double v = avgBeakLen.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+ BigDecimal b = new BigDecimal(v);
+ recordExcel.setAvgBeakLength(b.setScale(2, RoundingMode.HALF_UP).floatValue());
+ double r = avgRapeLen.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+ BigDecimal b1 = new BigDecimal(r);
+ recordExcel.setAvgRapeLength(b1.setScale(2, RoundingMode.HALF_UP).floatValue());
+ double h = avgHandleLen.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
+ BigDecimal b2 = new BigDecimal(h);
+ recordExcel.setAvgHandleLength(b2.setScale(2, RoundingMode.HALF_UP).floatValue());
+ ServletOutputStream outputStream = null;
+ try {
+ outputStream = response.getOutputStream();
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding("utf-8");
+ // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
+ String fileName = URLEncoder.encode(recordDetail.getName(), "UTF-8").replaceAll("\\+", "%20");
+ response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+ //创建 Cell 样式
+ WriteCellStyle writeCellStyle = new WriteCellStyle();
+ //设置垂直对齐方式
+ writeCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+ //设置水平对齐方式
+ writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
+ ExcelWriter writer = EasyExcel.write(outputStream).autoCloseStream(false).build();
+ //总览表格的sheet
+ WriteSheet recordSheet = EasyExcel.writerSheet("总览").head(RecordExcel.class).build();
+ //详情表格的sheet
+ ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet("详情")
+ .registerWriteHandler(new HorizontalCellStyleStrategy(null, writeCellStyle))
+ .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
+ .head(RecordContentExcel.class);
+ //添加单元格合并策略
+ for (OnceAbsoluteMergeStrategy mergeStrategy : mergeStrategies) {
+ sheetBuilder.registerWriteHandler(mergeStrategy);
+ }
+ WriteSheet contentSheet = sheetBuilder.build();
+ writer.write(Collections.singletonList(recordExcel), recordSheet);
+ writer.write(recordContentExcelList, contentSheet);
+ writer.finish();
+ } catch (Exception e) {
+ log.error("下载文件失败", e);
+ response.reset();
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+ Map map = MapUtils.newHashMap();
+ map.put("status", "failure");
+ map.put("message", "下载文件失败" + e.getMessage());
+ try {
+ response.getWriter().println(JSON.toJSONString(map));
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ } finally {
+ try {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+
+ }
+
+
+ @Override
+ public PageResult getPages(PageRequest pageRequest) {
+ Page page = new Page(pageRequest.getPageNum(), pageRequest.getPageSize());
+ Page result = recordMapper.pageList(page, null);
+ return PageResult.of((int) result.getTotal(), (int) result.getSize(), (int) result.getCurrent(), result.getRecords());
+
+ }
+
+
+ @Override
+ public void updateRecordContent(RecordContentEntity recordContentEntity) {
+ recordContentMapper.updateById(recordContentEntity);
+ }
+
+ @Override
+ public RecordModel getModels() {
+ return recognitionModelClient.models();
+ }
+}
diff --git a/src/main/java/com/jiagutech/service/TaskProgressSSEService.java b/src/main/java/com/jiagutech/service/TaskProgressSSEService.java
new file mode 100644
index 0000000..9064543
--- /dev/null
+++ b/src/main/java/com/jiagutech/service/TaskProgressSSEService.java
@@ -0,0 +1,36 @@
+package com.jiagutech.service;
+
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class TaskProgressSSEService {
+
+ private final ConcurrentHashMap sseEmitters = new ConcurrentHashMap<>();
+ public SseEmitter createProgressEmitter(String taskId) {
+ SseEmitter emitter = new SseEmitter();
+ sseEmitters.put(taskId, emitter);
+ return emitter;
+ }
+
+ public void sendProgress(String taskId, int progress) throws Exception {
+ SseEmitter emitter = sseEmitters.get(taskId);
+ if (emitter != null) {
+ emitter.send(progress);
+ if (progress >= 100) {
+ emitter.complete();
+ sseEmitters.remove(taskId);
+ }
+ }
+ }
+
+ public void completeEmitter(String taskId) {
+ SseEmitter emitter = sseEmitters.get(taskId);
+ if (emitter != null) {
+ emitter.complete();
+ sseEmitters.remove(taskId);
+ }
+ }
+}
diff --git a/src/main/java/com/jiagutech/service/UserService.java b/src/main/java/com/jiagutech/service/UserService.java
new file mode 100644
index 0000000..2292e55
--- /dev/null
+++ b/src/main/java/com/jiagutech/service/UserService.java
@@ -0,0 +1,13 @@
+package com.jiagutech.service;
+
+import com.jiagutech.dto.LoginRequest;
+import com.jiagutech.dto.LoginResponse;
+import com.jiagutech.dto.UserRequest;
+
+public interface UserService {
+ LoginResponse login(LoginRequest loginBody);
+
+ void register(UserRequest user);
+
+ void approval(Long userId);
+}
diff --git a/src/main/java/com/jiagutech/service/UserServiceImpl.java b/src/main/java/com/jiagutech/service/UserServiceImpl.java
new file mode 100644
index 0000000..2543568
--- /dev/null
+++ b/src/main/java/com/jiagutech/service/UserServiceImpl.java
@@ -0,0 +1,71 @@
+package com.jiagutech.service;
+
+import cn.dev33.satoken.secure.BCrypt;
+import cn.dev33.satoken.stp.StpUtil;
+import com.jiagutech.common.UserConstants;
+import com.jiagutech.common.exception.BizCode;
+import com.jiagutech.common.exception.BusinessException;
+import com.jiagutech.dto.LoginRequest;
+import com.jiagutech.dto.LoginResponse;
+import com.jiagutech.dto.LoginUser;
+import com.jiagutech.dto.UserRequest;
+import com.jiagutech.entity.UserEntity;
+import com.jiagutech.mapper.UserMapper;
+import com.jiagutech.utils.EmailUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+ private final UserMapper userMapper;
+ private final EmailUtil emailUtil;
+
+
+ @Override
+ public LoginResponse login(LoginRequest loginBody) {
+ String phone = loginBody.getPhone();
+ String password = loginBody.getPassword();
+ LoginUser user = userMapper.selectLoginUserByPhone(phone);
+ if (user == null) {
+ throw new BusinessException(BizCode.USER_NOT_FOUND);
+ }
+ if (user.getStatus() != 1) {
+ throw new BusinessException(BizCode.USER_STATUS_ERROR);
+ }
+ boolean checkPw = BCrypt.checkpw(password, user.getPassword());
+ if (!checkPw) {
+ throw new BusinessException(BizCode.PASSWORD_ERROR);
+ }
+ StpUtil.login(user.getUserId());
+ user.setLoginTime(System.currentTimeMillis());
+ StpUtil.getSession().set(UserConstants.SYS_SESSION, user);
+ LoginResponse loginVO = new LoginResponse();
+ loginVO.setAccessToken(StpUtil.getTokenValue());
+ loginVO.setExpireIn(StpUtil.getTokenTimeout());
+ return loginVO;
+ }
+
+ @Override
+ public void register(UserRequest user) {
+ UserEntity userEntity = new UserEntity()
+ .setCreateTime(LocalDateTime.now())
+ .setStatus(2)
+ .setRealName(user.getRealName())
+ .setUnitName(user.getUnitName())
+ .setPhone(user.getPhone())
+ .setPassword(BCrypt.hashpw(user.getPassword()));
+ userMapper.insert(userEntity);
+ emailUtil.sendEmail(null, user.getPhone(), user.getUnitName(), user.getRealName());
+
+ }
+
+ @Override
+ public void approval(Long userId) {
+ UserEntity userEntity = userMapper.selectById(userId);
+ userEntity.setStatus(1);
+ userMapper.updateById(userEntity);
+ }
+}
diff --git a/src/main/java/com/jiagutech/utils/EmailUtil.java b/src/main/java/com/jiagutech/utils/EmailUtil.java
new file mode 100644
index 0000000..952eb29
--- /dev/null
+++ b/src/main/java/com/jiagutech/utils/EmailUtil.java
@@ -0,0 +1,45 @@
+package com.jiagutech.utils;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Component;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class EmailUtil {
+
+ private final JavaMailSender mailSender;
+ @Value("${email.to}")
+ private String MAILTO;
+
+ public void sendEmail(String to, String phone, String unitName, String realName) {
+ try {
+ // 构建邮件
+ MimeMessage message = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
+
+ helper.setFrom("noreply@pushmail.jiagutech.com");
+ String receiver = to == null ? MAILTO : to;
+ helper.setTo(receiver);
+ helper.setSubject("新注册用户待审核");
+
+ String text = String.format("【用户待审核】现有%s用户%s(%s)
已注册成为角果识别系统的新用户" +
+ ",请您尽快审核!
", unitName, realName, phone);
+ helper.setText(text, true); // true 表示 HTML 格式
+
+ // 发送邮件
+ mailSender.send(message);
+ log.info("邮件发送成功: {}", receiver);
+
+ } catch (MessagingException e) {
+ log.error("邮件发送失败: ", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/jiagutech/utils/ExcelBigNumberConvert.java b/src/main/java/com/jiagutech/utils/ExcelBigNumberConvert.java
new file mode 100644
index 0000000..b2f5504
--- /dev/null
+++ b/src/main/java/com/jiagutech/utils/ExcelBigNumberConvert.java
@@ -0,0 +1,52 @@
+package com.jiagutech.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+
+/**
+ * 大数值转换
+ * Excel 数值长度位15位 大于15位的数值转换位字符串
+ *
+ * @author Lion Li
+ */
+@Slf4j
+public class ExcelBigNumberConvert implements Converter {
+
+ @Override
+ public Class supportJavaTypeKey() {
+ return Long.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ return CellDataTypeEnum.STRING;
+ }
+
+ @Override
+ public Long convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
+ return Convert.toLong(cellData.getData());
+ }
+
+ @Override
+ public WriteCellData