Commit 679602e3 by 杨阔

Initial commit

parents
File added
# Default ignored files
/shelf/
/workspace.xml
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="customer-visit" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="customer-visit" options="-parameters" />
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractClassShouldStartWithAbstractNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractMethodOrInterfaceMethodMustUseJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidApacheBeanUtilsCopy" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCallStaticSimpleDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCommentBehindStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidComplexCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidConcurrentCompetitionRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidDoubleOrFloatEqualCompare" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidManuallyCreateThread" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidMissUseOfMathRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNegationOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNewDateGetTime" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidPatternCompileInMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidReturnInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidStartWithDollarAndUnderLineNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidUseTimer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBigDecimalAvoidDoubleConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBooleanPropertyShouldNotStartWithIs" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithSubListToArrayList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithToArray" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassMustHaveAuthor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassNamingShouldBeCamel" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCollectionInitShouldAssignCapacity" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCommentsMustBeJavadocFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConcurrentExceptionWithModifyOriginSubList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConstantFieldShouldBeUpperCase" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCountDownShouldInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaDontModifyInForeachCircle" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaEnumConstantsMustHaveComment" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaExceptionClassShouldEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaIbatisMethodQueryForList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLockShouldWithTryFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLowerCamelCaseVariableNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodReturnWrapperType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodTooLong" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPackageNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustOverrideToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustUsePrimitiveField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoNoDefaultValue" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaRemoveCommentedCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaServiceOrDaoClassShouldEndWithImpl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSneakyThrowsWithoutExceptionType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaStringConcat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTestClassShouldEndWithTestNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadLocalShouldRemove" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadPoolCreation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadShouldSetName" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTransactionMustHaveRollback" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUndefineMagicConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
File added
[DEBUG] Shutting down 'noop' factory
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>customer-visit</artifactId>
<version>1.0.0</version>
<name>Customer Visit Simulator</name>
<description>Customer Visit Simulator Backend</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Apache PDFBox for PDF parsing -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.1</version>
</dependency>
<!-- Apache POI for Word and Excel parsing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Apache POI for old Word (.doc) parsing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Jsoup for HTML parsing -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<!-- OpenPDF for PDF generation (better Chinese support) -->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.35</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.example.customervisit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CustomerVisitApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerVisitApplication.class, args);
}
}
package com.example.customervisit.config;
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;
import java.util.Arrays;
import java.util.List;
/**
* 全局 CORS:支持跨域与 OPTIONS 预检(含自定义头 X-KT-Token、X-Username)。
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("X-Username", "Authorization"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
package com.example.customervisit.config;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.dto.KTAuthResponse;
import com.example.customervisit.service.KTAuthService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@RequiredArgsConstructor
@Slf4j
public class KTAuthInterceptor implements HandlerInterceptor {
private final KTAuthService ktAuthService;
private final ObjectMapper objectMapper;
private static final String TOKEN_HEADER = "X-KT-Token";
private static final String TOKEN_PARAM = "ktToken";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI();
String method = request.getMethod();
// 跳过 OPTIONS 预检请求
if ("OPTIONS".equalsIgnoreCase(method)) {
return true;
}
if (requestUri.contains("/healthcheck") || requestUri.contains("/api/user-permissions/kt-auth")) {
return true;
}
if (!requestUri.startsWith("/api/")) {
return true;
}
String token = request.getHeader(TOKEN_HEADER);
if (token == null || token.isEmpty()) {
token = request.getParameter(TOKEN_PARAM);
}
if (token == null || token.isEmpty()) {
sendErrorResponse(response, "请提供KT token (X-KT-Token header或ktToken参数)");
return false;
}
log.debug("正在验证KT token: {}", token.substring(0, Math.min(10, token.length())) + "...");
try {
KTAuthResponse authResponse = ktAuthService.authenticateWithKT(token);
if (authResponse.getCode() != null && authResponse.getCode() == 0 && authResponse.getData() != null) {
String username = authResponse.getData().getUsername();
request.setAttribute("X-Username", username);
log.debug("KT token验证成功, username: {}", username);
return true;
} else {
String errorMsg = authResponse.getMsg() != null ? authResponse.getMsg() : "KT token验证失败";
log.warn("KT token验证失败: {}", errorMsg);
sendErrorResponse(response, errorMsg);
return false;
}
} catch (Exception e) {
log.error("KT token验证异常: {}", e.getMessage(), e);
sendErrorResponse(response, "KT token验证异常: " + e.getMessage());
return false;
}
}
private void sendErrorResponse(HttpServletResponse response, String message) throws Exception {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
ApiResponse<Object> errorResponse = ApiResponse.error(message);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
package com.example.customervisit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() throws KeyManagementException, NoSuchAlgorithmException {
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
if (connection instanceof HttpsURLConnection) {
((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory());
((HttpsURLConnection) connection).setHostnameVerifier((hostname, session) -> true);
}
super.prepareConnection(connection, httpMethod);
}
};
factory.setConnectTimeout(300000);
factory.setReadTimeout(300000);
return new RestTemplate(factory);
}
}
package com.example.customervisit.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class UsernameInterceptor implements HandlerInterceptor {
private static final ThreadLocal<String> currentUsername = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// 从请求头获取username
String username = request.getHeader("X-Username");
// 如果请求头中没有,从请求参数获取
if (username == null || username.isEmpty()) {
username = request.getParameter("username");
}
// 存储到ThreadLocal
if (username != null && !username.isEmpty()) {
currentUsername.set(username);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理ThreadLocal
currentUsername.remove();
}
public static String getCurrentUsername() {
return currentUsername.get();
}
}
package com.example.customervisit.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final UsernameInterceptor usernameInterceptor;
private final KTAuthInterceptor ktAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ktAuthInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/user-permissions/kt-auth",
"/healthcheck"
);
registry.addInterceptor(usernameInterceptor)
.addPathPatterns("/api/**");
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiConfigRequest;
import com.example.customervisit.dto.DefaultProviderRequest;
import com.example.customervisit.service.ConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/config")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class ConfigController {
private final ConfigService configService;
@PostMapping("/api-config")
public Map<String, Object> saveApiConfig(@RequestBody ApiConfigRequest request) {
configService.saveApiConfig(request.getProvider(), request.getApiKey(), request.getEndpoint());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return result;
}
@GetMapping("/api-config/{provider}")
public Map<String, String> getApiConfig(@PathVariable String provider) {
return configService.getApiConfig(provider);
}
@PostMapping("/default-provider")
public Map<String, Object> saveDefaultProvider(@RequestBody DefaultProviderRequest request) {
configService.saveDefaultProvider(request.getProvider());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return result;
}
@GetMapping("/default-provider")
public Map<String, String> getDefaultProvider() {
Map<String, String> result = new HashMap<>();
result.put("provider", configService.getDefaultProvider());
return result;
}
// 向后兼容的API
@PostMapping("/api-key")
public Map<String, Object> saveApiKey(@RequestBody Map<String, String> request) {
String apiKey = request.get("apiKey");
configService.saveConfig("deepseek_api_key", apiKey);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return result;
}
@GetMapping("/api-key")
public Map<String, String> getApiKey() {
Map<String, String> result = new HashMap<>();
result.put("apiKey", configService.getConfig("deepseek_api_key"));
return result;
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.GeneratePlanRequest;
import com.example.customervisit.dto.ReviewRequest;
import com.example.customervisit.dto.SimulateRequest;
import com.example.customervisit.service.DeepSeekService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/deepseek")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class DeepSeekController {
private final DeepSeekService deepSeekService;
@PostMapping("/plan")
public Map<String, String> generatePlan(@RequestBody GeneratePlanRequest request) {
return deepSeekService.generatePlan(request);
}
@PostMapping("/simulate")
public Map<String, String> simulateConversation(@RequestBody SimulateRequest request) {
return deepSeekService.simulateConversation(request);
}
@PostMapping("/review")
public Map<String, String> analyzeReview(@RequestBody ReviewRequest request) {
return deepSeekService.analyzeReview(request);
}
}
\ No newline at end of file
package com.example.customervisit.controller;
import com.example.customervisit.dto.ExportPdfRequest;
import com.example.customervisit.service.PdfExportService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
@RestController
@RequestMapping("/api/export")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class ExportController {
private final PdfExportService pdfExportService;
@PostMapping(value = "/pdf", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> exportPdf(@Valid @RequestBody ExportPdfRequest request) {
String filename = request.getFilename();
if (filename == null || filename.isBlank()) {
filename = "visit-plan-" + LocalDate.now() + ".pdf";
}
if (!filename.toLowerCase().endsWith(".pdf")) {
filename = filename + ".pdf";
}
byte[] pdf = pdfExportService.exportMarkdownAsPdf(request.getTitle(), request.getContent());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDisposition(ContentDisposition.attachment()
.filename(filename, StandardCharsets.UTF_8)
.build());
headers.setContentLength(pdf.length);
return ResponseEntity.ok().headers(headers).body(pdf);
}
}
package com.example.customervisit.controller;
import com.example.customervisit.entity.Feedback;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.repository.FeedbackRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/feedbacks")
@CrossOrigin(origins = "*")
public class FeedbackController {
@Autowired
private FeedbackRepository feedbackRepository;
/**
* 获取拜访规划的所有反馈
*/
@GetMapping("/visit-plan/{visitPlanId}")
public ApiResponse<List<Feedback>> getFeedbacksByVisitPlanId(@PathVariable Long visitPlanId) {
List<Feedback> feedbacks = feedbackRepository.findByVisitPlanIdOrderByCreatedAtAsc(visitPlanId);
return ApiResponse.success(feedbacks);
}
/**
* 创建反馈
*/
@PostMapping
public ApiResponse<Feedback> createFeedback(@RequestBody Feedback feedback) {
Feedback savedFeedback = feedbackRepository.save(feedback);
return ApiResponse.success(savedFeedback);
}
/**
* 获取反馈数量
*/
@GetMapping("/visit-plan/{visitPlanId}/count")
public ApiResponse<Long> getFeedbackCount(@PathVariable Long visitPlanId) {
long count = feedbackRepository.countByVisitPlanId(visitPlanId);
return ApiResponse.success(count);
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/upload")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class FileUploadController {
@Value("${file.upload.path}")
private String uploadPath;
@PostMapping("/file")
public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> response = new HashMap<>();
if (file.isEmpty()) {
response.put("success", false);
response.put("message", "没有文件上传");
return response;
}
try {
// 创建上传目录
Path uploadDir = Paths.get(uploadPath);
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = originalFilename != null ?
originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
String filename = UUID.randomUUID().toString() + extension;
// 保存文件
Path filePath = uploadDir.resolve(filename);
Files.copy(file.getInputStream(), filePath);
// 提取文件内容
String content = extractFileContent(filePath.toFile(), file.getContentType());
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("originalname", originalFilename);
fileInfo.put("filename", filename);
fileInfo.put("path", filePath.toString());
fileInfo.put("mimetype", file.getContentType());
fileInfo.put("size", file.getSize());
fileInfo.put("content", content);
response.put("success", true);
response.put("file", fileInfo);
return response;
} catch (IOException e) {
response.put("success", false);
response.put("message", "文件上传失败: " + e.getMessage());
return response;
}
}
private String extractFileContent(File file, String contentType) {
try {
if (contentType != null && contentType.equals("application/pdf")) {
return extractPdfContent(file);
} else if (contentType != null && (contentType.equals("application/msword") ||
contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))) {
return extractWordContent(file);
} else if (contentType != null && (contentType.equals("application/vnd.ms-excel") ||
contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))) {
return extractExcelContent(file);
} else {
return "不支持的文件类型: " + contentType;
}
} catch (Exception e) {
return "文件内容提取失败: " + e.getMessage();
}
}
private String extractPdfContent(File file) throws IOException {
try (PDDocument document = Loader.loadPDF(file)) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document);
}
}
private String extractWordContent(File file) throws IOException {
try (FileInputStream fis = new FileInputStream(file);
XWPFDocument document = new XWPFDocument(fis)) {
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
return extractor.getText();
}
}
private String extractExcelContent(File file) throws IOException {
StringBuilder content = new StringBuilder();
try (FileInputStream fis = new FileInputStream(file);
Workbook workbook = file.getName().endsWith(".xlsx") ?
new XSSFWorkbook(fis) : new HSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
for (Cell cell : row) {
content.append(getCellValue(cell)).append("\t");
}
content.append("\n");
}
}
return content.toString();
}
private String getCellValue(Cell cell) {
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
return String.valueOf(cell.getNumericCellValue());
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
default:
return "";
}
}
}
package com.example.customervisit.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HealthCheck {
@GetMapping("/healthcheck")
public ResponseEntity refreshRoute() {
return ResponseEntity.ok("UP");
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.entity.DialogContext;
import com.example.customervisit.service.DialogService;
import com.example.customervisit.service.FileParserService;
import com.example.customervisit.service.IntentAnalyzeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class IntentAnalyzeController {
private final IntentAnalyzeService intentAnalyzeService;
private final DialogService dialogService;
private final FileParserService fileParserService;
@PostMapping(value = "/intent-analyze", consumes = "multipart/form-data")
public ResponseEntity<ApiResponse<Map<String, Object>>> analyzeIntent(
@RequestParam("content") String content,
@RequestParam("visitPlanId") Long visitPlanId,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "intent", required = false) String intent,
@RequestParam(value = "files", required = false) List<MultipartFile> files,
@RequestParam(value = "customerInfo", required = false) String customerInfoJson) {
if (content == null || content.trim().isEmpty()) {
return ResponseEntity.ok(ApiResponse.success(Map.of("intent", "UNKNOWN", "response", "请输入内容")));
}
// 解析多个文件内容 - 使用专门的文件解析服务
StringBuilder combinedFileContent = new StringBuilder();
StringBuilder fileNames = new StringBuilder();
if (files != null && !files.isEmpty()) {
try {
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file != null && !file.isEmpty()) {
String fileContent = fileParserService.parseFile(file);
String fileName = file.getOriginalFilename();
if (fileContent != null && !fileContent.isEmpty()) {
if (combinedFileContent.length() > 0) {
combinedFileContent.append("\n\n--- 文件分割线 ---\n\n");
}
combinedFileContent.append("【文件 ").append(i + 1).append(": ").append(fileName).append("】\n");
combinedFileContent.append(fileContent);
}
if (fileNames.length() > 0) {
fileNames.append(", ");
}
fileNames.append(fileName);
}
}
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.ok(ApiResponse.success(Map.of("intent", "ERROR", "response", "文件解析失败:" + e.getMessage())));
}
}
String fileContent = combinedFileContent.length() > 0 ? combinedFileContent.toString() : null;
String fileName = fileNames.length() > 0 ? fileNames.toString() : null;
// 解析客户信息
Map<String, Object> customerInfo = null;
if (customerInfoJson != null && !customerInfoJson.trim().isEmpty()) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
customerInfo = mapper.readValue(customerInfoJson, Map.class);
} catch (Exception e) {
e.printStackTrace();
// 忽略客户信息解析错误,不影响主流程
}
}
try {
// 如果前端直接传递了 intent 参数,直接使用它,跳过意图分析
String finalIntent = intent;
if (intent == null || intent.trim().isEmpty()) {
// 意图识别结果
finalIntent = intentAnalyzeService.analyzeIntent(content.trim(), visitPlanId);
}
// 根据结果调用大模型,传递文件内容、客户信息和时间戳
Map<String, Object> result = intentAnalyzeService.executeAction(visitPlanId, finalIntent, content.trim(), fileName, fileContent, customerInfo, timestamp);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.ok(ApiResponse.success(Map.of("intent", "ERROR", "response", "系统异常:" + e.getMessage())));
}
}
@GetMapping("/dialog-context/{visitPlanId}")
public ResponseEntity<ApiResponse<List<DialogContext>>> getDialogContext(@PathVariable Long visitPlanId) {
List<DialogContext> contexts = dialogService.getContextByVisitPlanId(visitPlanId);
return ResponseEntity.ok(ApiResponse.success(contexts));
}
}
\ No newline at end of file
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.dto.MeetingNoteRequest;
import com.example.customervisit.entity.MeetingNote;
import com.example.customervisit.service.MeetingNoteService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/meeting-notes")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class MeetingNoteController {
private final MeetingNoteService meetingNoteService;
@PostMapping
public ApiResponse<MeetingNote> createMeetingNote(@Valid @RequestBody MeetingNoteRequest request) {
MeetingNote meetingNote = meetingNoteService.createMeetingNote(request);
return ApiResponse.success("会议纪要创建成功", meetingNote);
}
@GetMapping
public ApiResponse<List<MeetingNote>> getAllMeetingNotes() {
List<MeetingNote> meetingNotes = meetingNoteService.getAllMeetingNotes();
return ApiResponse.success(meetingNotes);
}
@GetMapping("/{id}")
public ApiResponse<MeetingNote> getMeetingNoteById(@PathVariable Long id) {
MeetingNote meetingNote = meetingNoteService.getMeetingNoteById(id);
return ApiResponse.success(meetingNote);
}
@PutMapping("/{id}")
public ApiResponse<MeetingNote> updateMeetingNote(@PathVariable Long id, @Valid @RequestBody MeetingNoteRequest request) {
MeetingNote meetingNote = meetingNoteService.updateMeetingNote(id, request);
return ApiResponse.success("会议纪要更新成功", meetingNote);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteMeetingNote(@PathVariable Long id) {
meetingNoteService.deleteMeetingNote(id);
return ApiResponse.success("会议纪要删除成功", null);
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* 用户信息接口控制器
* <p>
* 用于转发请求到外部用户信息服务
*/
@RestController
@RequestMapping("/api")
@Slf4j
@CrossOrigin(origins = "*")
public class UserInfoController {
@Value("${external-api.kt-userinfo-url}")
private String ktUserinfoUrl;
/**
* 转发到外部用户信息接口
* <p>
* 根据itCode获取用户维度信息(DC02/DC03权限数据)
*
* @param itCode 用户itCode
* @return 外部接口返回的用户信息数据
*/
@GetMapping("/userinfo/query/oneDimensionByItCode")
public ApiResponse<Map<String, Object>> queryOneDimensionByItCode(@RequestParam String itCode) {
log.info("调用用户信息接口, itCode: {}", itCode);
if (itCode == null || itCode.isEmpty()) {
return ApiResponse.error("itCode参数不能为空");
}
try {
RestTemplate restTemplate = new RestTemplate();
// 构建URL参数
String urlWithParams = ktUserinfoUrl + "?itCode=" + itCode;
Map<String, Object> response = restTemplate.getForObject(urlWithParams, Map.class);
log.info("外部接口返回数据: {}", response);
return ApiResponse.success(response);
} catch (Exception e) {
log.error("调用外部用户信息接口失败", e);
return ApiResponse.error("调用外部接口失败: " + e.getMessage());
}
}
}
package com.example.customervisit.controller;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.dto.KTAuthResponse;
import com.example.customervisit.entity.UserPermission;
import com.example.customervisit.service.KTAuthService;
import com.example.customervisit.service.UserPermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/user-permissions")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class UserPermissionController {
private final UserPermissionService userPermissionService;
private final KTAuthService ktAuthService;
/**
* 获取所有用户权限配置
*/
@GetMapping
public ApiResponse<List<UserPermission>> getAllPermissions() {
List<UserPermission> permissions = userPermissionService.getAllPermissions();
return ApiResponse.success(permissions);
}
/**
* 根据用户名获取权限配置
*/
@GetMapping("/user/{username}")
public ApiResponse<UserPermission> getPermissionByUsername(@PathVariable String username) {
UserPermission permission = userPermissionService.getPermissionByUsername(username);
if (permission == null) {
return ApiResponse.error("用户权限配置不存在");
}
return ApiResponse.success(permission);
}
/**
* 检查用户是否可以查看所有数据
*/
@GetMapping("/check")
public ApiResponse<Boolean> checkCanViewAll(@RequestParam String username) {
boolean canViewAll = userPermissionService.canViewAll(username);
return ApiResponse.success(canViewAll);
}
/**
* 创建或更新用户权限
*/
@PostMapping
public ApiResponse<UserPermission> savePermission(@RequestBody UserPermission permission) {
UserPermission saved = userPermissionService.savePermission(permission);
return ApiResponse.success("权限配置保存成功", saved);
}
/**
* 删除用户权限
*/
@DeleteMapping("/{id}")
public ApiResponse<Void> deletePermission(@PathVariable Long id) {
userPermissionService.deletePermission(id);
return ApiResponse.success("权限配置删除成功", null);
}
/**
* 根据用户名删除权限
*/
@DeleteMapping("/user/{username}")
public ApiResponse<Void> deletePermissionByUsername(@PathVariable String username) {
userPermissionService.deletePermissionByUsername(username);
return ApiResponse.success("权限配置删除成功", null);
}
/**
* KT系统集成接口 - 校验权限并获取username
* <p>
* 前端需要传入KT系统的token,后端会调用KT系统接口校验权限
* 成功后返回username,前端需要在后续请求中通过请求头X-Username或参数username传递
*
* @param request 包含KT token的请求体
* @return 包含username的响应数据
*/
@PostMapping("/kt-auth")
public ApiResponse<Map<String, Object>> authenticateWithKT(@RequestBody Map<String, String> request) {
String token = request.get("token");
if (token == null || token.isEmpty()) {
return ApiResponse.error("token参数不能为空");
}
// 调用KT系统认证服务
KTAuthResponse ktResponse = ktAuthService.authenticateWithKT(token);
// KT系统返回code=0表示成功
if (ktResponse.getCode() != null && ktResponse.getCode() == 0 && ktResponse.getData() != null) {
String username = ktResponse.getData().getUsername();
// 构建返回数据
Map<String, Object> result = new HashMap<>();
result.put("username", username);
result.put("userId", ktResponse.getData().getUserId());
result.put("realname", ktResponse.getData().getRealname());
result.put("email", ktResponse.getData().getEmail());
result.put("mobile", ktResponse.getData().getMobile());
result.put("deptId", ktResponse.getData().getDeptId());
result.put("tenantId", ktResponse.getData().getTenantId());
result.put("avatar", ktResponse.getData().getAvatar());
// 检查用户权限配置
UserPermission permission = userPermissionService.getPermissionByUsername(username);
result.put("canViewAll", permission != null && permission.getCanViewAll());
return ApiResponse.success("KT系统认证成功", result);
} else {
String errorMsg = ktResponse.getMsg() != null ? ktResponse.getMsg() : "KT系统认证失败";
return ApiResponse.error(errorMsg);
}
}
}
package com.example.customervisit.dto;
import lombok.Data;
@Data
public class ApiConfigRequest {
private String provider;
private String apiKey;
private String endpoint;
}
package com.example.customervisit.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "操作成功", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
}
package com.example.customervisit.dto;
import lombok.Data;
@Data
public class DefaultProviderRequest {
private String provider;
}
package com.example.customervisit.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ExportPdfRequest {
@NotBlank
private String title;
@NotBlank
private String content;
private String filename;
}
package com.example.customervisit.dto;
import lombok.Data;
import java.util.List;
@Data
public class GeneratePlanRequest {
private String customerName;
private String visitPurpose;
private String visitDate;
private String visitLocation;
private String customerParticipants;
private String ourParticipants;
private List<UploadedFile> files;
@Data
public static class UploadedFile {
private String originalname;
private String filename;
private String path;
private String mimetype;
private Long size;
private String content;
}
}
package com.example.customervisit.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* KT系统认证响应DTO
*/
@Data
public class KTAuthResponse {
/**
* 响应码,0表示成功
*/
private Integer code;
/**
* 响应消息
*/
private String msg;
/**
* 响应数据
*/
private KTAuthData data;
@Data
public static class KTAuthData {
/**
* 用户ID
*/
private Integer userId;
/**
* 父ID
*/
private Integer parentId;
/**
* 用户类型
*/
private String userType;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realname;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String mobile;
/**
* 状态
*/
private Integer status;
/**
* 部门ID
*/
private Integer deptId;
/**
* 租户ID
*/
private Integer tenantId;
/**
* 头像
*/
private String avatar;
/**
* 系统key
*/
private String sysKey;
/**
* logo
*/
private String logo;
/**
* 系统名称
*/
private String sysName;
/**
* 系统首页
*/
private String sysHome;
/**
* 域名
*/
private String domain;
/**
* 是否系统管理员
*/
private Boolean sysAdmin;
/**
* 是否租户管理员
*/
private Boolean sysTenantAdmin;
}
}
package com.example.customervisit.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class MeetingNoteRequest {
private Long planId;
@NotBlank(message = "会议纪要内容不能为空")
private String notes;
private String reviewAnalysis;
private String actionItems;
}
package com.example.customervisit.dto;
import lombok.Data;
@Data
public class ReviewRequest {
private Long planId;
private String meetingNotes;
}
package com.example.customervisit.dto;
import lombok.Data;
import java.util.List;
@Data
public class SimulateRequest {
private String message;
private List<ContextMessage> context;
@Data
public static class ContextMessage {
private String role;
private String content;
}
}
package com.example.customervisit.dto;
import lombok.Data;
import java.util.List;
@Data
public class UserPermissionRequest {
private int page = 0;
private int size = 10;
private String cusId;
private String cusName;
private String username;
private List<PermissionItem> permissions;
@Data
public static class PermissionItem {
private String userCode;
private String type;
private String code;
private String name;
}
}
\ No newline at end of file
package com.example.customervisit.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.Map;
@Data
public class VisitPlanRequest {
@NotBlank(message = "客户名称不能为空")
private String customerName;
private String customerIndustry;
@NotBlank(message = "拜访目的不能为空")
private String visitPurpose;
private String visitDate;
private String visitLocation;
private String customerParticipants;
private String ourParticipants;
private String createdBy;
private String plan;
private Map<String, Object> customerInfo;
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "config")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Config {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "config_key", unique = true, nullable = false)
private String key;
@Column(name = "config_value", nullable = false, columnDefinition = "TEXT")
private String value;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "customers")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
@Id
@Column(name = "id", length = 50)
private String id;
@Column(name = "cus_id")
private Long cusId;
@Column(name = "cus_name", length = 200)
private String cusName;
@Column(name = "cus_attribute")
private Integer cusAttribute;
@Column(name = "cus_attribute_name", length = 100)
private String cusAttributeName;
@Column(name = "cus_industry")
private Integer cusIndustry;
@Column(name = "cus_industry_name", length = 100)
private String cusIndustryName;
@Column(name = "first_trade_id")
private Integer firstTradeId;
@Column(name = "second_trade_id")
private Integer secondTradeId;
@Column(name = "first_trade_name", length = 100)
private String firstTradeName;
@Column(name = "second_trade_name", length = 100)
private String secondTradeName;
@Column(name = "province_name", length = 100)
private String provinceName;
@Column(name = "province_code", length = 20)
private String provinceCode;
@Column(name = "cus_region")
private Integer cusRegion;
@Column(name = "cus_state")
private Integer cusState;
@Column(name = "manage_region_id")
private Integer manageRegionId;
@Column(name = "manage_region_code", length = 20)
private String manageRegionCode;
@Column(name = "region_name", length = 100)
private String regionName;
@Column(name = "rel_manage_region_name", length = 100)
private String relManageRegionName;
@Column(name = "industry_id")
private Integer industryId;
@Column(name = "industry_name", length = 100)
private String industryName;
@Column(name = "is_id", length = 50)
private String isId;
@Column(name = "is_name", length = 100)
private String isName;
@Column(name = "os_id", length = 50)
private String osId;
@Column(name = "os_name", length = 100)
private String osName;
@Column(name = "bs_id", length = 50)
private String bsId;
@Column(name = "bs_name", length = 100)
private String bsName;
@Column(name = "trade_manager_id", length = 50)
private String tradeManagerId;
@Column(name = "trade_manager_name", length = 100)
private String tradeManagerName;
@Column(name = "c05", length = 200)
private String c05;
@Column(name = "c06", length = 200)
private String c06;
@Column(name = "c07", length = 200)
private String c07;
@Column(name = "c08", length = 200)
private String c08;
@Column(name = "c09", length = 200)
private String c09;
@Column(name = "city", length = 100)
private String city;
@Column(name = "county", length = 100)
private String county;
@Column(name = "grade_id")
private Integer gradeId;
@Column(name = "grade", length = 50)
private String grade;
@Column(name = "group_code", length = 50)
private String groupCode;
@Column(name = "group_desc", length = 100)
private String groupDesc;
@Column(name = "es_contact", length = 200)
private String esContact;
@Column(name = "buyer_contact_name", length = 100)
private String buyerContactName;
@Column(name = "ss_code", length = 50)
private String ssCode;
@Column(name = "ss_name", length = 100)
private String ssName;
@Column(name = "kt_os_code", length = 50)
private String ktOsCode;
@Column(name = "kt_os_name", length = 100)
private String ktOsName;
@Column(name = "kt_is_code", length = 50)
private String ktIsCode;
@Column(name = "kt_is_name", length = 100)
private String ktIsName;
@Column(name = "kt_bs_name", length = 100)
private String ktBsName;
@Column(name = "kt_bs_code", length = 50)
private String ktBsCode;
@Column(name = "kt_ss_code", length = 50)
private String ktSsCode;
@Column(name = "kt_ss_name", length = 100)
private String ktSsName;
@Column(name = "s01_code", length = 50)
private String s01Code;
@Column(name = "s01_name", length = 100)
private String s01Name;
@Column(name = "s02_code", length = 50)
private String s02Code;
@Column(name = "s02_name", length = 100)
private String s02Name;
@Column(name = "s03_code", length = 50)
private String s03Code;
@Column(name = "s03_name", length = 100)
private String s03Name;
@Column(name = "s04_code", length = 50)
private String s04Code;
@Column(name = "s04_name", length = 100)
private String s04Name;
@Column(name = "s05_code", length = 50)
private String s05Code;
@Column(name = "s05_name", length = 100)
private String s05Name;
@Column(name = "s06_code", length = 50)
private String s06Code;
@Column(name = "s06_name", length = 100)
private String s06Name;
@Column(name = "s07_code", length = 50)
private String s07Code;
@Column(name = "s07_name", length = 100)
private String s07Name;
@Column(name = "s08_code", length = 50)
private String s08Code;
@Column(name = "s08_name", length = 100)
private String s08Name;
@Column(name = "s09_code", length = 50)
private String s09Code;
@Column(name = "s09_name", length = 100)
private String s09Name;
@Column(name = "s10_code", length = 50)
private String s10Code;
@Column(name = "s10_name", length = 100)
private String s10Name;
@Column(name = "s11_code", length = 50)
private String s11Code;
@Column(name = "s11_name", length = 100)
private String s11Name;
@Column(name = "s12_code", length = 50)
private String s12Code;
@Column(name = "s12_name", length = 100)
private String s12Name;
@Column(name = "s13_code", length = 50)
private String s13Code;
@Column(name = "s13_name", length = 100)
private String s13Name;
@Column(name = "is_general_manager_code", length = 50)
private String isGeneralManagerCode;
@Column(name = "is_front_manager_code", length = 50)
private String isFrontManagerCode;
@Column(name = "is_second_manager_code", length = 50)
private String isSecondManagerCode;
@Column(name = "is_leader_code", length = 50)
private String isLeaderCode;
@Column(name = "manager_it_code", length = 50)
private String managerItCode;
@Column(name = "industry_higher_director_code", length = 50)
private String industryHigherDirectorCode;
@Column(name = "industry_director_code", length = 50)
private String industryDirectorCode;
@Column(name = "region_leader_code", length = 50)
private String regionLeaderCode;
@Column(name = "region_director_code", length = 50)
private String regionDirectorCode;
@Column(name = "region_front_manager_code", length = 50)
private String regionFrontManagerCode;
@Column(name = "region_second_manager_code", length = 50)
private String regionSecondManagerCode;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (id == null || id.isEmpty()) {
id = String.valueOf(System.currentTimeMillis());
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "dialog_contexts")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DialogContext {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "visit_plan_id")
private Long visitPlanId;
@Column(name = "user_message", columnDefinition = "TEXT")
private String userMessage;
@Column(name = "intent")
private String intent;
@Column(name = "response", columnDefinition = "TEXT")
private String response;
@Column(name = "context_data", columnDefinition = "TEXT")
private String contextData;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "feedbacks")
public class Feedback {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "visit_plan_id", nullable = false)
private Long visitPlanId;
@Column(name = "content", nullable = false, length = 2000)
private String content;
@Column(name = "created_by")
private String createdBy;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVisitPlanId() {
return visitPlanId;
}
public void setVisitPlanId(Long visitPlanId) {
this.visitPlanId = visitPlanId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "meeting_notes")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MeetingNote {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "plan_id")
private Long planId;
@Column(name = "notes", nullable = false, columnDefinition = "TEXT")
private String notes;
@Column(name = "review_analysis", columnDefinition = "TEXT")
private String reviewAnalysis;
@Column(name = "action_items", columnDefinition = "TEXT")
private String actionItems;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_logs")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "action", nullable = false)
private String action;
@Column(name = "details", columnDefinition = "TEXT")
private String details;
@Column(name = "ip_address")
private String ipAddress;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
public UserLog(String username, String action, String details, String ipAddress) {
this.username = username;
this.action = action;
this.details = details;
this.ipAddress = ipAddress;
}
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_messages")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "visit_plan_id")
private Long visitPlanId;
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
@Column(name = "file_name")
private String fileName;
@Column(name = "file_content", columnDefinition = "TEXT")
private String fileContent;
@Column(name = "intent")
private String intent;
@Column(name = "response", columnDefinition = "TEXT")
private String response;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "user_permissions")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserPermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@Column(name = "can_view_all")
private Boolean canViewAll = false;
@Column(name = "description")
private String description;
@Column(name = "created_at")
private java.time.LocalDateTime createdAt;
@Column(name = "updated_at")
private java.time.LocalDateTime updatedAt;
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "visit_plans")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitPlan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "cus_id")
private Long cusId;
@Column(name = "customer_name", nullable = false)
private String customerName;
@Column(name = "customer_industry")
private String customerIndustry;
@Column(name = "visit_purpose", nullable = false, columnDefinition = "TEXT")
private String visitPurpose;
@Column(name = "visit_date")
private String visitDate;
@Column(name = "visit_location")
private String visitLocation;
@Column(name = "customer_participants")
private String customerParticipants;
@Column(name = "our_participants")
private String ourParticipants;
@Column(name = "created_by")
private String createdBy;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "visit_plan_details")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VisitPlanDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "plan_id", nullable = false)
private Long planId;
@Column(name = "content", columnDefinition = "TEXT")
private String content;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "created_by")
private String createdBy;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "visit_reviews")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitReview {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "plan_id", nullable = false)
private Long planId;
@Column(name = "visit_plan_id")
private Long visitPlanId;
@Column(name = "review_result", columnDefinition = "TEXT")
private String reviewResult;
@Column(name = "created_by")
private String createdBy;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
\ No newline at end of file
package com.example.customervisit.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "visit_simulations")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitSimulation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "plan_id", nullable = false)
private Long planId;
@Column(name = "visit_plan_id", nullable = false)
private Long visitPlanId;
@Column(name = "simulation_result", columnDefinition = "LONGTEXT")
private String simulationResult;
@Column(name = "created_by")
private String createdBy;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.Config;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ConfigRepository extends JpaRepository<Config, Long> {
Optional<Config> findByKey(String key);
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.Customer;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, String> {
Optional<Customer> findByCusId(Long cusId);
Optional<Customer> findByCusName(String cusName);
List<Customer> findByProvinceName(String provinceName);
List<Customer> findByCity(String city);
Page<Customer> findByCusId(Long cusId, Pageable pageable);
Page<Customer> findByCusNameContaining(String cusName, Pageable pageable);
Page<Customer> findByCusIdAndCusNameContaining(Long cusId, String cusName, Pageable pageable);
@Query("SELECT c FROM Customer c WHERE " +
"(c.industryId IN :industryIds) OR " +
"(c.provinceCode IN :provinceCodes)")
Page<Customer> findByIndustryIdInOrProvinceCodeIn(
@Param("industryIds") List<Integer> industryIds,
@Param("provinceCodes") List<String> provinceCodes,
Pageable pageable);
@Query("SELECT c FROM Customer c WHERE " +
"(c.industryId IN :industryIds) OR " +
"(c.provinceCode IN :provinceCodes) " +
"AND (c.cusId = :cusId) " +
"AND (c.cusName LIKE %:cusName%)")
Page<Customer> findByIndustryIdInOrProvinceCodeInWithFilters(
@Param("industryIds") List<Integer> industryIds,
@Param("provinceCodes") List<String> provinceCodes,
@Param("cusId") Long cusId,
@Param("cusName") String cusName,
Pageable pageable);
@Query("SELECT c FROM Customer c WHERE " +
"c.isId = :username OR c.osId = :username OR c.ssCode = :username OR c.bsId = :username")
Page<Customer> findByUserFields(@Param("username") String username, Pageable pageable);
@Query("SELECT c FROM Customer c WHERE " +
"(c.isId = :username OR c.osId = :username OR c.ssCode = :username OR c.bsId = :username) " +
"AND (:cusId IS NULL OR c.cusId = :cusId) " +
"AND (:cusName IS NULL OR c.cusName LIKE %:cusName%)")
Page<Customer> findByUserFieldsWithFilters(
@Param("username") String username,
@Param("cusId") Long cusId,
@Param("cusName") String cusName,
Pageable pageable);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.DialogContext;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DialogContextRepository extends JpaRepository<DialogContext, Long> {
List<DialogContext> findByVisitPlanIdOrderByCreatedAtAsc(Long visitPlanId);
void deleteByVisitPlanId(Long visitPlanId);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.Feedback;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface FeedbackRepository extends JpaRepository<Feedback, Long> {
/**
* 根据拜访规划ID查询所有反馈,按创建时间正序(最早的在前,最新的在后)
*/
List<Feedback> findByVisitPlanIdOrderByCreatedAtAsc(Long visitPlanId);
/**
* 根据拜访规划ID查询反馈数量
*/
long countByVisitPlanId(Long visitPlanId);
/**
* 根据拜访规划ID删除所有反馈
*/
void deleteByVisitPlanId(Long visitPlanId);
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.MeetingNote;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MeetingNoteRepository extends JpaRepository<MeetingNote, Long> {
List<MeetingNote> findAllByOrderByCreatedAtDesc();
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.UserLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserLogRepository extends JpaRepository<UserLog, Long> {
List<UserLog> findByUsernameOrderByCreatedAtDesc(String username);
List<UserLog> findTop100ByOrderByCreatedAtDesc();
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.UserMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserMessageRepository extends JpaRepository<UserMessage, Long> {
List<UserMessage> findByVisitPlanIdOrderByCreatedAtDesc(Long visitPlanId);
List<UserMessage> findByIntentOrderByCreatedAtDesc(String intent);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.UserPermission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserPermissionRepository extends JpaRepository<UserPermission, Long> {
Optional<UserPermission> findByUsername(String username);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.VisitPlanDetail;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface VisitPlanDetailRepository extends JpaRepository<VisitPlanDetail, Long> {
/**
* 根据拜访规划ID查询所有详情
*/
List<VisitPlanDetail> findByPlanIdOrderByCreatedAtDesc(Long planId);
/**
* 根据拜访规划ID删除所有详情
*/
void deleteByPlanId(Long planId);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.VisitPlan;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface VisitPlanRepository extends JpaRepository<VisitPlan, Long> {
List<VisitPlan> findAllByOrderByCreatedAtDesc();
/**
* 根据创建人查询拜访规划列表
*/
List<VisitPlan> findByCreatedByOrderByCreatedAtDesc(String createdBy);
/**
* 分页查询所有拜访规划
*/
Page<VisitPlan> findAllByOrderByCreatedAtDesc(Pageable pageable);
/**
* 根据创建人分页查询拜访规划
*/
Page<VisitPlan> findByCreatedByOrderByCreatedAtDesc(String createdBy, Pageable pageable);
/**
* 根据客户cusId查询拜访规划列表
*/
List<VisitPlan> findByCusIdOrderByCreatedAtDesc(Long cusId);
/**
* 根据客户cusId分页查询拜访规划
*/
Page<VisitPlan> findByCusIdOrderByCreatedAtDesc(Long cusId, Pageable pageable);
/**
* 根据客户cusId查询最新的拜访规划
*/
VisitPlan findFirstByCusIdOrderByCreatedAtDesc(Long cusId);
}
package com.example.customervisit.repository;
import com.example.customervisit.entity.VisitReview;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface VisitReviewRepository extends JpaRepository<VisitReview, Long> {
Optional<VisitReview> findLatestByPlanIdOrderByCreatedAtDesc(Long planId);
List<VisitReview> findByPlanIdOrderByCreatedAtDesc(Long planId);
void deleteByPlanId(Long planId);
}
\ No newline at end of file
package com.example.customervisit.repository;
import com.example.customervisit.entity.VisitSimulation;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface VisitSimulationRepository extends JpaRepository<VisitSimulation, Long> {
List<VisitSimulation> findByPlanId(Long planId);
VisitSimulation findFirstByPlanIdOrderByCreatedAtDesc(Long planId);
void deleteByPlanId(Long planId);
}
package com.example.customervisit.service;
import com.example.customervisit.entity.Config;
import com.example.customervisit.repository.ConfigRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ConfigService {
private final ConfigRepository configRepository;
@Transactional
public void saveConfig(String key, String value) {
Optional<Config> existingConfig = configRepository.findByKey(key);
if (existingConfig.isPresent()) {
Config config = existingConfig.get();
config.setValue(value);
configRepository.save(config);
} else {
Config config = new Config();
config.setKey(key);
config.setValue(value);
configRepository.save(config);
}
}
public String getConfig(String key) {
return configRepository.findByKey(key)
.map(Config::getValue)
.orElse("");
}
public String getConfig(String key, String defaultValue) {
String value = getConfig(key);
return value.isEmpty() ? defaultValue : value;
}
@Transactional
public void saveApiConfig(String provider, String apiKey, String endpoint) {
saveConfig(provider + "_api_key", apiKey);
if (endpoint != null && !endpoint.isEmpty()) {
saveConfig(provider + "_api_endpoint", endpoint);
}
}
public Map<String, String> getApiConfig(String provider) {
Map<String, String> config = new HashMap<>();
config.put("key", getConfig(provider + "_api_key"));
config.put("endpoint", getConfig(provider + "_api_endpoint"));
return config;
}
@Transactional
public void saveDefaultProvider(String provider) {
saveConfig("default_api_provider", provider);
}
public String getDefaultProvider() {
return getConfig("default_api_provider", "deepseek");
}
}
package com.example.customervisit.service;
import com.example.customervisit.dto.UserPermissionRequest;
import com.example.customervisit.entity.Customer;
import org.springframework.data.domain.Page;
import java.util.List;
public interface CustomerService {
Customer saveCustomer(Customer customer);
Customer findByCusId(Long cusId);
Customer findByCusName(String cusName);
List<Customer> findAll();
void deleteById(String id);
Page<Customer> findByPage(int page, int size, String cusId, String cusName);
Page<Customer> findByPermissions(UserPermissionRequest request);
}
\ No newline at end of file
package com.example.customervisit.service;
import com.example.customervisit.dto.GeneratePlanRequest;
import com.example.customervisit.dto.ReviewRequest;
import com.example.customervisit.dto.SimulateRequest;
import java.util.Map;
public interface DeepSeekService {
Map<String, String> generatePlan(GeneratePlanRequest request);
Map<String, String> simulateConversation(SimulateRequest request);
Map<String, String> analyzeReview(ReviewRequest request);
}
\ No newline at end of file
package com.example.customervisit.service;
import com.example.customervisit.dto.ApiResponse;
import com.example.customervisit.entity.DialogContext;
import com.example.customervisit.entity.VisitPlan;
import com.example.customervisit.repository.DialogContextRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class DialogService {
private final DialogContextRepository dialogContextRepository;
private final VisitPlanService visitPlanService;
public List<DialogContext> getContextByVisitPlanId(Long visitPlanId) {
return dialogContextRepository.findByVisitPlanIdOrderByCreatedAtAsc(visitPlanId);
}
@Transactional
public DialogContext saveContext(Long visitPlanId, String userMessage, String intent, String response, String contextData) {
DialogContext context = DialogContext.builder()
.visitPlanId(visitPlanId)
.userMessage(userMessage)
.intent(intent)
.response(response)
.contextData(contextData)
.build();
return dialogContextRepository.save(context);
}
@Transactional
public void deleteContextByVisitPlanId(Long visitPlanId) {
dialogContextRepository.deleteByVisitPlanId(visitPlanId);
}
public String buildContextPrompt(Long visitPlanId) {
List<DialogContext> contexts = getContextByVisitPlanId(visitPlanId);
StringBuilder prompt = new StringBuilder();
prompt.append("历史对话记录(用于理解上下文):\n");
prompt.append("================================\n");
for (DialogContext context : contexts) {
prompt.append("用户: ").append(context.getUserMessage()).append("\n");
prompt.append("意图: ").append(context.getIntent()).append("\n");
prompt.append("系统回复: ").append(context.getResponse()).append("\n");
prompt.append("--------------------------------\n");
}
return prompt.toString();
}
}
\ No newline at end of file
package com.example.customervisit.service;
import org.springframework.web.multipart.MultipartFile;
public interface FileParserService {
String parseFile(MultipartFile file) throws Exception;
}
\ No newline at end of file
package com.example.customervisit.service;
import com.example.customervisit.entity.VisitPlan;
import java.util.Map;
public interface IntentAnalyzeService {
String analyzeIntent(String content, Long visitPlanId);
Map<String, Object> executeAction(Long visitPlanId, String intent, String userMessage, String fileName, String fileContent, Map<String, Object> customerInfo, String timestamp);
String generatePlan(VisitPlan plan, String fileContent);
String startSimulation(VisitPlan plan, String fileContent);
String performPostMortem(VisitPlan plan, String fileContent);
void saveCustomerInfo(Map<String, Object> customerInfo);
}
\ No newline at end of file
package com.example.customervisit.service;
import com.example.customervisit.dto.KTAuthResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
@RequiredArgsConstructor
@Slf4j
public class KTAuthService {
@Value("${kt.auth.url}")
private String ktAuthUrl;
private final RestTemplate restTemplate;
/**
* 调用KT系统校验权限并获取username
*
* @param token KT系统的token
* @return KTAuthResponse 包含username等信息的响应对象
*/
public KTAuthResponse authenticateWithKT(String token) {
// 开发环境:如果token为dev-token,直接返回成功响应
if ("dev-token".equals(token)) {
log.info("开发环境:使用dev-token跳过KT系统认证");
KTAuthResponse response = new KTAuthResponse();
response.setCode(0);
response.setMsg("success");
KTAuthResponse.KTAuthData data = new KTAuthResponse.KTAuthData();
data.setUsername("admin");
data.setRealname("管理员");
response.setData(data);
return response;
}
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("token", token);
// 创建请求实体
HttpEntity<String> entity = new HttpEntity<>(headers);
// 调用KT系统接口
log.info("正在调用KT系统认证接口: {}", ktAuthUrl);
ResponseEntity<KTAuthResponse> response = restTemplate.exchange(
ktAuthUrl,
HttpMethod.GET,
entity,
KTAuthResponse.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
KTAuthResponse body = response.getBody();
log.info("KT系统响应: code={}, msg={}, username={}",
body.getCode(),
body.getMsg(),
body.getData() != null ? body.getData().getUsername() : "null");
return body;
} else {
log.error("KT系统认证失败, 状态码: {}", response.getStatusCode());
KTAuthResponse errorResponse = new KTAuthResponse();
errorResponse.setCode(500);
errorResponse.setMsg("KT系统认证失败");
return errorResponse;
}
} catch (Exception e) {
log.error("KT系统认证异常: {}", e.getMessage(), e);
KTAuthResponse errorResponse = new KTAuthResponse();
errorResponse.setCode(500);
errorResponse.setMsg("KT系统认证异常: " + e.getMessage());
return errorResponse;
}
}
}
package com.example.customervisit.service;
import com.example.customervisit.dto.MeetingNoteRequest;
import com.example.customervisit.entity.MeetingNote;
import com.example.customervisit.repository.MeetingNoteRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MeetingNoteService {
private final MeetingNoteRepository meetingNoteRepository;
@Transactional
public MeetingNote createMeetingNote(MeetingNoteRequest request) {
MeetingNote meetingNote = new MeetingNote();
meetingNote.setPlanId(request.getPlanId());
meetingNote.setNotes(request.getNotes());
meetingNote.setReviewAnalysis(request.getReviewAnalysis());
meetingNote.setActionItems(request.getActionItems());
return meetingNoteRepository.save(meetingNote);
}
public List<MeetingNote> getAllMeetingNotes() {
return meetingNoteRepository.findAllByOrderByCreatedAtDesc();
}
public MeetingNote getMeetingNoteById(Long id) {
return meetingNoteRepository.findById(id)
.orElseThrow(() -> new RuntimeException("会议纪要不存在"));
}
@Transactional
public MeetingNote updateMeetingNote(Long id, MeetingNoteRequest request) {
MeetingNote meetingNote = getMeetingNoteById(id);
meetingNote.setPlanId(request.getPlanId());
meetingNote.setNotes(request.getNotes());
meetingNote.setReviewAnalysis(request.getReviewAnalysis());
meetingNote.setActionItems(request.getActionItems());
return meetingNoteRepository.save(meetingNote);
}
@Transactional
public void deleteMeetingNote(Long id) {
MeetingNote meetingNote = getMeetingNoteById(id);
meetingNoteRepository.delete(meetingNote);
}
}
package com.example.customervisit.service;
import com.example.customervisit.config.UsernameInterceptor;
import com.example.customervisit.entity.UserLog;
import com.example.customervisit.repository.UserLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class UserLogService {
private final UserLogRepository userLogRepository;
@Transactional
public void logAction(String action, String details) {
String username = UsernameInterceptor.getCurrentUsername();
if (username == null || username.isEmpty()) {
username = "anonymous";
}
String ipAddress = getClientIpAddress();
UserLog log = new UserLog(username, action, details, ipAddress);
userLogRepository.save(log);
}
@Transactional
public void logAction(String username, String action, String details) {
String ipAddress = getClientIpAddress();
UserLog log = new UserLog(username, action, details, ipAddress);
userLogRepository.save(log);
}
public List<UserLog> getUserLogs(String username) {
return userLogRepository.findByUsernameOrderByCreatedAtDesc(username);
}
public List<UserLog> getRecentLogs() {
return userLogRepository.findTop100ByOrderByCreatedAtDesc();
}
private String getClientIpAddress() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
} catch (Exception e) {
// 忽略异常
}
return "unknown";
}
}
package com.example.customervisit.service;
import com.example.customervisit.entity.UserPermission;
import com.example.customervisit.repository.UserPermissionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserPermissionService {
private final UserPermissionRepository userPermissionRepository;
/**
* 检查用户是否可以查看所有数据
* 如果用户在权限表中且canViewAll为true,则可以查看所有数据
* 如果用户不在权限表中或canViewAll为false,则只能查看自己的数据
*/
public boolean canViewAll(String username) {
if (username == null || username.isEmpty()) {
return false;
}
return userPermissionRepository.findByUsername(username)
.map(UserPermission::getCanViewAll)
.orElse(false);
}
/**
* 获取所有权限配置
*/
public List<UserPermission> getAllPermissions() {
return userPermissionRepository.findAll();
}
/**
* 根据用户名获取权限配置
*/
public UserPermission getPermissionByUsername(String username) {
return userPermissionRepository.findByUsername(username)
.orElse(null);
}
/**
* 创建或更新用户权限
*/
@Transactional
public UserPermission savePermission(UserPermission permission) {
// 如果已存在则更新
UserPermission existing = userPermissionRepository.findByUsername(permission.getUsername())
.orElse(null);
if (existing != null) {
existing.setCanViewAll(permission.getCanViewAll());
existing.setDescription(permission.getDescription());
return userPermissionRepository.save(existing);
}
return userPermissionRepository.save(permission);
}
/**
* 删除用户权限
*/
@Transactional
public void deletePermission(Long id) {
userPermissionRepository.deleteById(id);
}
/**
* 根据用户名删除权限
*/
@Transactional
public void deletePermissionByUsername(String username) {
userPermissionRepository.findByUsername(username)
.ifPresent(userPermissionRepository::delete);
}
}
package com.example.customervisit.service;
import com.example.customervisit.entity.VisitPlan;
import com.example.customervisit.entity.VisitSimulation;
import java.util.List;
public interface VisitSimulationService {
VisitSimulation createVisitSimulation(Long planId, Long visitPlanId, String simulationResult, String createdBy);
VisitSimulation getLatestSimulationByPlanId(Long planId);
List<VisitSimulation> getSimulationsByPlanId(Long planId);
void deleteSimulation(Long id);
void deleteSimulationByPlanId(Long planId);
String generatePlan(VisitPlan plan);
String startSimulation(VisitPlan plan);
String performPostMortem(VisitPlan plan);
}
package com.example.customervisit.service.impl;
import com.example.customervisit.dto.UserPermissionRequest;
import com.example.customervisit.entity.Customer;
import com.example.customervisit.entity.UserPermission;
import com.example.customervisit.repository.CustomerRepository;
import com.example.customervisit.repository.UserPermissionRepository;
import com.example.customervisit.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(rollbackFor = Exception.class)
public class CustomerServiceImpl implements CustomerService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private UserPermissionRepository userPermissionRepository;
@Override
public Customer saveCustomer(Customer customer) {
return customerRepository.save(customer);
}
@Override
public Customer findByCusId(Long cusId) {
Optional<Customer> customer = customerRepository.findByCusId(cusId);
return customer.orElse(null);
}
@Override
public Customer findByCusName(String cusName) {
Optional<Customer> customer = customerRepository.findByCusName(cusName);
return customer.orElse(null);
}
@Override
public List<Customer> findAll() {
return customerRepository.findAll();
}
@Override
public void deleteById(String id) {
customerRepository.deleteById(id);
}
@Override
public Page<Customer> findByPage(int page, int size, String cusId, String cusName) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
if ((cusId != null && !cusId.isEmpty()) && (cusName != null && !cusName.isEmpty())) {
return customerRepository.findByCusIdAndCusNameContaining(Long.parseLong(cusId), cusName, pageable);
} else if (cusId != null && !cusId.isEmpty()) {
return customerRepository.findByCusId(Long.parseLong(cusId), pageable);
} else if (cusName != null && !cusName.isEmpty()) {
return customerRepository.findByCusNameContaining(cusName, pageable);
} else {
return customerRepository.findAll(pageable);
}
}
@Override
public Page<Customer> findByPermissions(UserPermissionRequest request) {
Pageable pageable = PageRequest.of(request.getPage(), request.getSize(), Sort.by(Sort.Direction.DESC, "createdAt"));
String username = request.getUsername();
// 处理过滤条件
Long cusId = null;
if (request.getCusId() != null && !request.getCusId().isEmpty()) {
try {
cusId = Long.parseLong(request.getCusId());
} catch (NumberFormatException e) {
// ignore invalid cusId
}
}
String cusName = request.getCusName();
// 1. 根据username查询user_permissions表
Optional<UserPermission> userPermissionOpt = userPermissionRepository.findByUsername(username);
if (userPermissionOpt.isPresent()) {
UserPermission userPermission = userPermissionOpt.get();
// 2. 如果用户在表中且can_view_all=true,则查询所有客户列表
if (Boolean.TRUE.equals(userPermission.getCanViewAll())) {
if ((cusId != null) || (cusName != null && !cusName.isEmpty())) {
// 有过滤条件
if (cusId != null && cusName != null && !cusName.isEmpty()) {
return customerRepository.findByCusIdAndCusNameContaining(cusId, cusName, pageable);
} else if (cusId != null) {
return customerRepository.findByCusId(cusId, pageable);
} else {
return customerRepository.findByCusNameContaining(cusName, pageable);
}
} else {
// 没有过滤条件,查询所有
return customerRepository.findAll(pageable);
}
} else {
// 3. 如果用户在表中且can_view_all=false,则按照DC02和DC03权限查询
return findByDCPermissions(request, cusId, cusName, pageable);
}
} else {
// 4. 如果用户不在表中,则查询客户表中is_id、os_id、ss_code、bs_id这几个字段是否有一个等于username
if ((cusId != null) || (cusName != null && !cusName.isEmpty())) {
return customerRepository.findByUserFieldsWithFilters(username, cusId, cusName, pageable);
} else {
return customerRepository.findByUserFields(username, pageable);
}
}
}
private Page<Customer> findByDCPermissions(UserPermissionRequest request, Long cusId, String cusName, Pageable pageable) {
// 从权限数据中提取DC02和DC03的code
List<Integer> industryIds = new ArrayList<>(); // DC02 -> industry_id
List<String> provinceCodes = new ArrayList<>(); // DC03 -> province_code
if (request.getPermissions() != null) {
for (UserPermissionRequest.PermissionItem item : request.getPermissions()) {
if ("DC02".equals(item.getType()) && item.getCode() != null) {
try {
provinceCodes.add(item.getCode());
} catch (NumberFormatException e) {
// ignore invalid code
}
} else if ("DC03".equals(item.getType()) && item.getCode() != null) {
industryIds.add(Integer.parseInt(item.getCode()));
}
}
}
// 如果没有权限数据,返回空结果
if (industryIds.isEmpty() && provinceCodes.isEmpty()) {
return Page.empty(pageable);
}
// 如果有过滤条件,使用带过滤的查询
if ((cusId != null) || (cusName != null && !cusName.isEmpty())) {
return customerRepository.findByIndustryIdInOrProvinceCodeInWithFilters(
industryIds.isEmpty() ? null : industryIds,
provinceCodes.isEmpty() ? null : provinceCodes,
cusId,
cusName,
pageable);
} else {
// 没有过滤条件,使用简单查询
return customerRepository.findByIndustryIdInOrProvinceCodeIn(
industryIds.isEmpty() ? null : industryIds,
provinceCodes.isEmpty() ? null : provinceCodes,
pageable);
}
}
}
\ No newline at end of file
package com.example.customervisit.service.impl;
import com.example.customervisit.service.FileParserService;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class FileParserServiceImpl implements FileParserService {
@Override
public String parseFile(MultipartFile file) throws Exception {
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("文件名不能为空");
}
String extension = getFileExtension(filename).toLowerCase();
return switch (extension) {
case "txt" -> parseTxtFile(file);
case "docx" -> parseDocxFile(file);
case "doc" -> parseDocFile(file);
case "pdf" -> parsePdfFile(file);
default -> throw new IllegalArgumentException("不支持的文件类型: " + extension);
};
}
private String getFileExtension(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex == -1) {
return "";
}
return filename.substring(lastDotIndex + 1);
}
private String parseTxtFile(MultipartFile file) throws IOException {
try (InputStream is = file.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is)) {
Charset charset = detectCharset(bis);
bis.mark(0);
bis.reset();
return new String(file.getBytes(), charset);
}
}
private Charset detectCharset(BufferedInputStream bis) throws IOException {
bis.mark(1024);
byte[] bom = new byte[4];
int read = bis.read(bom);
bis.reset();
if (read >= 2 && bom[0] == (byte) 0xFF && bom[1] == (byte) 0xFE) {
return StandardCharsets.UTF_16LE;
}
if (read >= 2 && bom[0] == (byte) 0xFE && bom[1] == (byte) 0xFF) {
return StandardCharsets.UTF_16BE;
}
if (read >= 3 && bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF) {
return StandardCharsets.UTF_8;
}
return StandardCharsets.UTF_8;
}
private String parseDocxFile(MultipartFile file) throws IOException {
try (InputStream is = file.getInputStream();
XWPFDocument document = new XWPFDocument(is)) {
StringBuilder content = new StringBuilder();
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
content.append(paragraph.getText()).append("\n");
}
return content.toString().trim();
}
}
private String parseDocFile(MultipartFile file) throws IOException {
try (InputStream is = file.getInputStream();
HWPFDocument document = new HWPFDocument(is)) {
WordExtractor extractor = new WordExtractor(document);
return extractor.getText().trim();
}
}
private String parsePdfFile(MultipartFile file) throws IOException {
try (PDDocument document = Loader.loadPDF(file.getBytes())) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document).trim();
}
}
}
\ No newline at end of file
package com.example.customervisit.service.impl;
import com.example.customervisit.entity.VisitPlan;
import com.example.customervisit.entity.VisitPlanDetail;
import com.example.customervisit.entity.VisitSimulation;
import com.example.customervisit.repository.VisitPlanDetailRepository;
import com.example.customervisit.repository.VisitSimulationRepository;
import com.example.customervisit.service.ConfigService;
import com.example.customervisit.service.VisitSimulationService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Service
@RequiredArgsConstructor
public class VisitSimulationServiceImpl implements VisitSimulationService {
private final VisitSimulationRepository visitSimulationRepository;
private final VisitPlanDetailRepository visitPlanDetailRepository;
private final ConfigService configService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String KT_API_URL = "https://aiapi.legendkaitian.com/v1/chat/completions";
@Override
public VisitSimulation createVisitSimulation(Long planId, Long visitPlanId, String simulationResult, String createdBy) {
VisitSimulation simulation = new VisitSimulation();
simulation.setPlanId(planId);
simulation.setVisitPlanId(visitPlanId);
simulation.setSimulationResult(simulationResult);
simulation.setCreatedBy(createdBy);
return visitSimulationRepository.save(simulation);
}
@Override
public VisitSimulation getLatestSimulationByPlanId(Long planId) {
return visitSimulationRepository.findFirstByPlanIdOrderByCreatedAtDesc(planId);
}
@Override
public List<VisitSimulation> getSimulationsByPlanId(Long planId) {
return visitSimulationRepository.findByPlanId(planId);
}
@Override
public void deleteSimulation(Long id) {
visitSimulationRepository.deleteById(id);
}
@Override
public void deleteSimulationByPlanId(Long planId) {
visitSimulationRepository.deleteByPlanId(planId);
}
@Override
public String generatePlan(VisitPlan plan) {
String prompt = """
请根据以下客户信息,生成一份详细的拜访规划:
客户名称:%s
客户行业:%s
拜访目的:%s
拜访日期:%s
拜访地点:%s
客户参与人员:%s
我方参与人员:%s
请生成包含以下内容的拜访规划:
1. 拜访目标
2. 开场话术
3. 核心议题
4. 预期成果
5. 应对策略
""".formatted(
plan.getCustomerName(),
plan.getCustomerIndustry(),
plan.getVisitPurpose(),
plan.getVisitDate(),
plan.getVisitLocation(),
plan.getCustomerParticipants(),
plan.getOurParticipants()
);
return callLLM(prompt);
}
@Override
public String startSimulation(VisitPlan plan) {
// 从VisitPlanDetail表获取最新的拜访规划内容
VisitPlanDetail latestDetail = visitPlanDetailRepository.findByPlanIdOrderByCreatedAtDesc(plan.getId()).stream()
.findFirst()
.orElse(null);
String planContent = latestDetail != null && latestDetail.getContent() != null ? latestDetail.getContent() : "暂无";
String prompt = """
请模拟客户视角的对话场景。
客户信息:
客户名称:%s
客户行业:%s
拜访目的:%s
拜访地点:%s
当前拜访规划:
%s
请模拟客户可能的回应和提问,以对话形式呈现,帮助我方人员进行拜访演练。
""".formatted(
plan.getCustomerName(),
plan.getCustomerIndustry(),
plan.getVisitPurpose(),
plan.getVisitLocation(),
planContent
);
return callLLM(prompt);
}
@Override
public String performPostMortem(VisitPlan plan) {
VisitSimulation latestSimulation = getLatestSimulationByPlanId(plan.getId());
String simulationResult = latestSimulation != null ? latestSimulation.getSimulationResult() : "暂无";
// 从VisitPlanDetail表获取最新的拜访规划内容
VisitPlanDetail latestDetail = visitPlanDetailRepository.findByPlanIdOrderByCreatedAtDesc(plan.getId()).stream()
.findFirst()
.orElse(null);
String planContent = latestDetail != null && latestDetail.getContent() != null ? latestDetail.getContent() : "暂无";
String prompt = """
请对以下拜访进行复盘分析:
客户名称:%s
客户行业:%s
拜访目的:%s
拜访日期:%s
拜访规划:
%s
模拟结果:
%s
请从以下方面进行复盘分析:
1. 目标达成情况
2. 沟通亮点
3. 改进建议
4. 后续跟进计划
""".formatted(
plan.getCustomerName(),
plan.getCustomerIndustry(),
plan.getVisitPurpose(),
plan.getVisitDate(),
planContent,
simulationResult
);
return callLLM(prompt);
}
private String callLLM(String prompt) {
try {
String apiKey = configService.getConfig("api_key");
if (apiKey.isEmpty()) {
throw new Exception("API key未设置");
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiKey);
ObjectNode requestBody = objectMapper.createObjectNode();
requestBody.put("model", "qwen3");
requestBody.put("temperature", 0.7);
requestBody.put("max_tokens", 2000);
ArrayNode messages = requestBody.putArray("messages");
ObjectNode userMessage = messages.addObject();
userMessage.put("role", "user");
userMessage.put("content", prompt);
HttpEntity<String> entity = new HttpEntity<>(requestBody.toString(), headers);
ResponseEntity<JsonNode> response = restTemplate.exchange(
KT_API_URL,
HttpMethod.POST,
entity,
JsonNode.class,
180000
);
return response.getBody()
.path("choices").get(0)
.path("message").path("content").asText();
} catch (Exception e) {
e.printStackTrace();
return "调用大模型失败:" + e.getMessage();
}
}
}
\ No newline at end of file
package com.example.customervisit.util;
import java.util.regex.Pattern;
/**
* 去除模型输出的「思考」片段,避免写入拜访规划正文。
*/
public final class PlanOutputSanitizer {
private static final Pattern XML_THINK = Pattern.compile(
"(?is)" + Pattern.quote("\u003cthink\u003e") + "\\s*.*?" + Pattern.quote("\u003c/think\u003e") + "\\s*");
private static final Pattern XML_REDACTED_REASONING = Pattern.compile(
"(?is)" + Pattern.quote("\u003credacted_reasoning\u003e") + "\\s*.*?"
+ Pattern.quote("\u003c/redacted_reasoning\u003e") + "\\s*");
private static final Pattern FENCE_THINKING =
Pattern.compile("(?i)```\\s*thinking\\s*\\R[\\s\\S]*?```\\s*");
private static final Pattern FENCE_REASONING =
Pattern.compile("(?i)```\\s*reasoning\\s*\\R[\\s\\S]*?```\\s*");
/** 部分模型使用完整单词 thinking */
private static final Pattern XML_THINKING = Pattern.compile(
"(?is)" + Pattern.quote("\u003cthinking\u003e") + "\\s*.*?"
+ Pattern.quote("\u003c/thinking\u003e") + "\\s*");
private PlanOutputSanitizer() {
}
public static String sanitize(String text) {
if (text == null || text.isEmpty()) {
return text;
}
String s = text;
s = XML_THINK.matcher(s).replaceAll("\n");
s = XML_REDACTED_REASONING.matcher(s).replaceAll("\n");
s = FENCE_THINKING.matcher(s).replaceAll("\n");
s = FENCE_REASONING.matcher(s).replaceAll("\n");
s = XML_THINKING.matcher(s).replaceAll("\n");
s = s.replaceAll("(?m)\n{3,}", "\n\n");
return s.trim();
}
}
spring:
application:
name: customer-visit
datasource:
url: jdbc:mysql://localhost:3306/customer_visit?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
hbm2ddl:
foreign_keys: false
jackson:
time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
# File upload path
file:
upload:
path: ${user.dir}/uploads
# Logging
logging:
level:
com.example.customervisit: DEBUG
org.springframework.web: DEBUG
# KT Auth
kt:
auth:
url: https://usf-uat-admin.tst.cp.xcloud.legendkaitian.com/sys/user/token
# 外部接口配置
external-api:
# kt-userinfo-url: http://kt-uat-crm-common.tst.cp.xcloud.legendkaitian.com/userinfo/query/oneDimensionByItCode
# kt-customer-url: http://kt-uat-crm-common.tst.cp.xcloud.legendkaitian.com/common/esdataimprot/query/customerByItCode
kt-userinfo-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/userinfo/query/oneDimensionByItCode
kt-customer-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/common/esdataimprot/query/customerByItCode
spring:
application:
name: customer-visit
datasource:
url: jdbc:mysql://tidb.legendkaitian.com:4000/aisandbox?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: aisandbox
password: yUQ1C48VG1zl6WYi
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
hbm2ddl:
foreign_keys: false
jackson:
time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
# File upload path
file:
upload:
path: ${user.dir}/uploads
# Logging
logging:
level:
com.example.customervisit: DEBUG
org.springframework.web: DEBUG
# KT Auth
kt:
auth:
url: https://api-jx-usf.prod.cp.xcloud.legendkaitian.com/sys/user/token
# 外部接口配置
external-api:
kt-userinfo-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/userinfo/query/oneDimensionByItCode
kt-customer-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/common/esdataimprot/query/customerByItCode
spring:
application:
name: customer-visit
datasource:
url: jdbc:mysql://etl-tst.kt.lenovo.com:4000/aisandbox?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: aisandbox
password: Unzt9npj3I1lOWh4
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
hbm2ddl:
foreign_keys: false
jackson:
time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
# File upload path
file:
upload:
path: ${user.dir}/uploads
# Logging
logging:
level:
com.example.customervisit: DEBUG
org.springframework.web: DEBUG
# KT Auth
kt:
auth:
url: https://api-xc-usf.tst.cp.xcloud.legendkaitian.com/sys/user/token
# 外部接口配置
external-api:
# kt-userinfo-url: http://kt-uat-crm-common.tst.cp.xcloud.legendkaitian.com/userinfo/query/oneDimensionByItCode
# kt-customer-url: http://kt-uat-crm-common.tst.cp.xcloud.legendkaitian.com/common/esdataimprot/query/customerByItCode
kt-userinfo-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/userinfo/query/oneDimensionByItCode
kt-customer-url: https://jx-crm-common.prod.cp.xcloud.legendkaitian.com/common/esdataimprot/query/customerByItCode
\ No newline at end of file
server:
port: 8080
spring:
application:
name: customer-visit
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
你是一位专业的销售顾问,擅长为联想销售团队制定客户拜访规划。请根据提供的客户信息和背景文件内容,生成详细、结构化的拜访规划,充分利用背景文件中的信息来提高规划的准确性和针对性。请自动判断客户行业,并在规划中添加"客户背景分析"部分,包括客户公司及参会客户个人信息。
特别注意:
- 拜访时间非常重要,请确保规划的时效性,根据拜访日期合理安排准备工作和后续跟进计划
- 必要情况下,要考虑启动网络实时搜索功能补充最新信息,确保准备工作充分
- 拜访地点信息也很重要,请根据地点特性调整拜访策略和准备工作
## 一、重要要求:
1. 必须包含"客户背景分析"部分,且分析必须详细深入
2. 必须充分利用上传的文件内容进行分析,提取关键信息
3. 客户行业必须自动判断并填写,确保准确性
4. 所有部分都必须详细填写,不要留空
5. 必须包含本方参与人员信息
6. 客户背景分析部分必须基于事实信息,避免空泛描述
7. 要结合背景文件中的具体内容进行分析,体现针对性
8. 必须考虑拜访时间的时效性,确保规划的及时性和相关性
9. 必须根据拜访地点调整策略和准备工作
## 二、输出格式要求
```markdown
# 联想销售团队拜访规划
## 1. 拜访基本信息
- 客户名称:[客户名称]
- 客户行业:[自动判断的客户行业]
- 拜访目的及背景:[拜访目的及背景]
- 拜访日期:[拜访日期]
- 拜访地点:[拜访地点]
- 客户参与人员:[客户参与人员]
- 本方参与人员:[本方参与人员]
## 2. 客户背景分析
- 客户公司分析:[基于背景信息和文件内容,详细分析客户公司的业务范围、规模、市场地位、行业竞争力、近期发展动态、主要产品或服务等]
- 参会人员分析:[基于背景信息和文件内容,详细分析参会客户个人的职位、职责、专业背景、可能的关注点、决策权限等]
- 本方人员分析:[分析本方参与人员的优势、专业背景、角色分工和与客户的关系]
- 客户需求分析:[基于背景信息和文件内容,分析客户的具体需求、痛点和期望]
## 3. 拜访目标
- 主要目标:[详细描述主要目标]
- 次要目标:[详细描述次要目标]
- 预期成果:[详细描述预期成果]
## 4. 准备工作
- 资料准备:[列出需要准备的资料]
- 产品知识:[需要了解的产品知识]
- 竞争对手分析:[竞争对手情况分析]
- 客户背景调查:[客户背景信息,结合背景文件内容]
- 时效性准备:[基于拜访日期的时间敏感性准备工作]
- 地点相关准备:[基于拜访地点的特殊准备工作]
## 5. 拜访流程
1. [时间点] - [活动内容]
2. [时间点] - [活动内容]
3. [时间点] - [活动内容]
## 6. 话题建议
- 话题1:[话题内容及展开方向,结合背景文件内容]
- 话题2:[话题内容及展开方向,结合背景文件内容]
- 话题3:[话题内容及展开方向,结合背景文件内容]
## 7. 话术推荐
- 开场白:[具体话术]
- 产品介绍:[具体话术,结合背景文件内容]
- 异议处理:[具体话术]
- 结束话术:[具体话术]
## 8. 可能的问题及应对策略
- 问题1:[具体问题]
应对策略:[详细应对方案]
- 问题2:[具体问题]
应对策略:[详细应对方案]
- 问题3:[具体问题]
应对策略:[详细应对方案]
## 9. 后续跟进计划
- 短期跟进:[1-3天内的跟进计划]
- 中期跟进:[1-2周内的跟进计划]
- 长期跟进:[1个月以上的跟进计划]
- 时效性跟进:[基于拜访日期的时间敏感性跟进计划]
```
## 三、【最终输出强制要求(必严格执行,违反则输出无效)】
1. 正文须从「# 联想销售团队拜访规划」这一级标题开始。
2. 禁止输出任何前置话术(包括但不限于“好的”“用户让我”“首先”“接下来”等所有过渡、回应类文字);
3. 禁止输出任何思考过程、分析逻辑、假设说明(包括“由于没有背景文件”“我需要推断”等类似表述);
4. 禁止添加任何额外补充、提醒(包括“可进一步优化”“需补充背景文件”等文字);
5. 仅输出「固定模板格式」的拜访规划正文,严格沿用模板的标题、层级、列表样式,只替换[ ]占位符,不新增、不删减任何模块;
6. 正文必须从「# 联想销售团队拜访规划」开始,前面无任何多余段落、标签、解释。
7. 若未提供背景文件/客户信息,仅针对[ ]占位符,补充合理且贴合“电气设备行业”(北京中辰普安电气科技有限公司)的常规内容,不额外说明“假设”“推测”等前提。
8. 禁止使用、<thinking><redacted_reasoning>、以及 Markdown 的 ```thinking 代码块等包裹思考内容;
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment