chengf
2026-02-07 e310dfdc93c20ac0c3b5fcd1a95de298cfce2ae9
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/demo/copywriting/controller/CopywritingController.java
@@ -3,13 +3,19 @@
import java.io.File;
import java.io.FileInputStream;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import com.alibaba.fastjson2.JSONObject;
import opennlp.tools.dictionary.serializer.Entry;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.service.ISysUserService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -38,6 +44,10 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
@@ -58,11 +68,14 @@
@RestController
@RequestMapping("/copywriting/copywriting")
@Slf4j
@EnableAsync
public class CopywritingController extends JeecgController<Copywriting, ICopywritingService> {
   @Autowired
   private ICopywritingService copywritingService;
    @Autowired
    public ICopywritingService copywritingService;
     @Autowired
     private ISemanticWordService semanticWordService;
     private ISemanticWordService semanticWordService;;
     @Autowired
     private ISysUserService sysUserService;
   
   /**
    * 分页列表查询
@@ -91,8 +104,19 @@
        }
        if (StringUtils.isNotBlank(copywriting.getWordLike())){
            queryWrapper.exists("SELECT 1 FROM semantic_word WHERE semantic_word.id = copywriting.word_id " +
                            "AND semantic_word.word LIKE '%" + copywriting.getWordLike() + "%'");
                    "AND semantic_word.word LIKE '%" + copywriting.getWordLike() + "%'");
        }
        if (StringUtils.isNotBlank(copywriting.getContractId())) {
            // 1. 拼接关联 contact 表的 EXISTS 子查询,使用 contact.id 作为外键关联
            // 2. 使用 MyBatis-Plus 的参数占位符避免 SQL 注入,而不是直接字符串拼接
            String existsSql = "SELECT 1 FROM semantic_word " +
                    "LEFT JOIN contract ON semantic_word.contract_id = contract.id " + // 关联 contact 表(外键关联)
                    "WHERE semantic_word.id = copywriting.word_id " +
                    "AND contract.id = " + copywriting.getContractId(); // 使用 contact 表的 id 作为条件
            // 给 QueryWrapper 设置参数,避免 SQL 注入
            queryWrapper.exists(existsSql);
        }
        Page<Copywriting> page = new Page<Copywriting>(pageNo, pageSize);
      IPage<Copywriting> pageList = copywritingService.page(page, queryWrapper);
@@ -101,6 +125,47 @@
        }
      return Result.OK(pageList);
   }
     @Operation(summary="文案-查询发送门户文章总量")
     @GetMapping(value = "/count")
     public Result<IPage<Copywriting>> count(Copywriting copywriting,
                                             @RequestParam(name="role", defaultValue="无") String role,
                                             @RequestParam(name="user", defaultValue="无") String user,
                                                     HttpServletRequest req) {
         QueryWrapper<Copywriting> queryWrapper = QueryGenerator.initQueryWrapper(copywriting, req.getParameterMap());
         if (StringUtils.isNotBlank(copywriting.getTitleLike())) {
             queryWrapper.like("title", copywriting.getTitleLike());
         }
         if (StringUtils.isNotBlank(copywriting.getTitleLike()) && StringUtils.isNotBlank(copywriting.getWordLike())) {
             queryWrapper.or();
         }
         if (StringUtils.isNotBlank(copywriting.getWordLike())){
             queryWrapper.exists("SELECT 1 FROM semantic_word WHERE semantic_word.id = copywriting.word_id " +
                     "AND semantic_word.word LIKE '%" + copywriting.getWordLike() + "%'");
         }
         if (!user.equals("无")){
             QueryWrapper qw = new QueryWrapper<SysUser>();
             qw.eq("id", user);
             String userName = ((SysUser)((Page) sysUserService.queryPageList(req, qw, 1, 1).getResult()).getRecords().get(0)).getUsername();
             queryWrapper.eq("create_by", userName);
         }
         long count = copywritingService.count(queryWrapper);
         return Result.OK(count+"");
     }
     @Operation(summary="文案-查询发送门户文章总量")
     @GetMapping(value = "/upAvgTime")
     public Result<IPage<Copywriting>> upAvgTime(Copywriting copywriting,
                                             HttpServletRequest req) {
         return Result.OK("5");
     }
   /**
    *   添加
@@ -113,6 +178,9 @@
   @RequiresPermissions("copywriting:copywriting:add")
   @PostMapping(value = "/add")
   public Result<String> add(@RequestBody Copywriting copywriting) {
        if (copywriting.getOutStatus() == null) {
            copywriting.setOutStatus("1");
        }
      copywritingService.save(copywriting);
      return Result.OK("添加成功!");
@@ -192,6 +260,9 @@
        return super.exportXls(request, copywriting, Copywriting.class, "文案");
    }
    @Value("${jeecg.path.upload}")
    private String uploadPath;
    /**
      * 通过excel导入数据
    *
@@ -208,86 +279,279 @@
     @RequiresPermissions("copywriting:copywriting:aiCreateCopyWriting")
     @RequestMapping(value = "/aiCreateCopyWriting", method = RequestMethod.POST)
     private Result<?> aiCreateCopyWriting(
                                           @RequestParam String[] jianli,
                                           @RequestParam String wenanyaoqiu,
                                           @RequestParam String louchu,
                                           @RequestParam String youshang,
                                           @RequestParam String wenti,
                                           @RequestParam String user) {
         // 工作流接口配置(请替换为实际值)
         String path = "D:\\opt\\upFiles\\";
         String workflowUrl = "http://14.103.174.44/v1/workflows/run"; // 目标工作流接口地址
         String authToken = "app-J1Tqytg0ZetcrVTF2fVHHY8B"; // 接口认证Token(解决401问题)
         String userId = user; // 调用者标识(解决user参数缺失问题)
     public Result<?> aiCreateCopyWriting(
             @RequestParam("jianli") String jianli,
             @RequestParam String wenanyaoqiu,
             @RequestParam String louchu,
             @RequestParam String youshang,
             @RequestParam String wenti,
             @RequestParam String user) {
         return getResult(jianli, wenanyaoqiu, louchu, youshang, wenti, user);
     }
     public Result<?> getResult(String jianli, String wenanyaoqiu, String louchu, String youshang, String wenti, String user) {
         if (jianli == null || jianli.equals("")) {
             return Result.error("请选择文件");
         }
         // 配置信息
         String serverFileRoot = uploadPath;
         String workflowUrl = "http://14.103.174.44/v1/workflows/run";
         String fileUploadUrl = "http://14.103.174.44/v1/files/upload"; // 文件上传接口(假设)
         String authToken = "app-J1Tqytg0ZetcrVTF2fVHHY8B";
         String userId = user;
         String appId = "cf85fe4d-b76b-4c4c-801a-1336c880d473";
         try {
             // 1. 构建请求头(包含认证信息)
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
             headers.set("Authorization", "Bearer " + authToken); // 添加Token认证
             // 步骤1:上传简历文件,获取 upload_file_id 列表
             List<String> jianliFileList = Arrays.asList(jianli.split(","));
             List<String> jianliFileIds = new ArrayList<>();
             for (String fileName : jianliFileList) {
                 // 修正:只过滤路径遍历字符,保留合法的 /
                 String safeFileName = File.separator + fileName.trim()
                         // 过滤 ../ 和 ./ 序列(防止访问上级目录)
                         .replaceAll("\\.\\./", "")
                         .replaceAll("\\./", "");
             // 2. 构建业务参数容器(inputs是接口必填字段)
             Map<String, Object> inputs = new HashMap<>();
             // 添加文本参数
             // 处理简历文件(转为可序列化的List<Map>,避免使用流对象)
             List<Map<String, String>> jianliFileList = new ArrayList<>();
             for (String fileName : jianli) {
                 // 拼接服务器上的完整文件路径
                 File file = new File(path + fileName.trim());
                 if (!file.exists() || !file.isFile()) {
                     return Result.error("服务器上不存在文件:" + fileName);
// 进一步安全校验:确保拼接后的文件路径在服务器根目录下(核心安全措施)
                 File file = new File(serverFileRoot + safeFileName);
                 String canonicalPath = file.getCanonicalPath(); // 获取标准化路径(自动解析 ../ 等)
                 String canonicalRootPath = new File(serverFileRoot).getCanonicalPath();
// 校验:如果文件路径不在服务器根目录下,视为非法请求
                 if (!canonicalPath.startsWith(canonicalRootPath)) {
                     throw new RuntimeException("非法文件访问:" + fileName);
                 }
                 // 读取文件内容并转为Base64
                 byte[] fileBytes = readFileToBytes(file);
                 String base64Content = Base64.getEncoder().encodeToString(fileBytes);
                 // 封装文件信息
                 Map<String, String> fileInfo = new HashMap<>();
                 fileInfo.put("name", fileName);
                 fileInfo.put("content", base64Content);
                 jianliFileList.add(fileInfo);
// 再判断文件是否存在
                 if (!file.exists() || !file.isFile()) {
                     return Result.error("服务器不存在文件:" + safeFileName + ",路径:" + file.getAbsolutePath());
                 }
                 // 调用文件上传接口,获取 upload_file_id
                 String fileId = uploadFileToServer(file, fileUploadUrl, authToken);
                 jianliFileIds.add(fileId);
             }
             inputs.put("jianli", jianliFileList); // 简历文件列表(符合接口要求)
             inputs.put("wenanyaoqiu", wenanyaoqiu);// 文案要求
             inputs.put("louchu", louchu);          // 露出
             inputs.put("youshang", youshang);      // 优势(修正笔误)
             inputs.put("wenti", wenti);            // 问题
             // 4. 构建完整请求体(包含所有必填顶层参数)
             Map<String, Object> requestBody = new HashMap<>();
             requestBody.put("inputs", inputs);    // 核心业务参数容器(解决inputs缺失问题)
             requestBody.put("user", userId);      // 用户标识(解决user缺失问题)
             // 如需指定工作流ID,添加以下参数(根据接口文档确认)
             // requestBody.put("workflow_id", "your_workflow_id");
             // 步骤2:构建 inputs 参数(按接口要求格式)
             Map<String, Object> inputs = new HashMap<>();
             // 5. 发送请求到工作流接口
             RestTemplate restTemplate = new RestTemplate();
             HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
             ResponseEntity<String> response = restTemplate.postForEntity(
                     workflowUrl,
                     requestEntity,
                     String.class
             );
             // 6. 处理响应结果
             if (response.getStatusCode() == HttpStatus.OK) {
                 return Result.OK("文案生成成功", JSONObject.parseObject(response.getBody()).getJSONObject("data").getJSONObject("outputs").getString("http"));
             // 处理简历文件(jianli 是 variable_name,对应接口的 {variable_name})
             if (!jianliFileIds.isEmpty()) {
                 // 若支持多个文件,可能需要数组形式;单个文件则直接放对象
                 List<Map<String, String>> jianliFiles = new ArrayList<>();
                 for (String fileId : jianliFileIds) {
                     Map<String, String> fileInfo = new HashMap<>();
                     fileInfo.put("transfer_method", "local_file"); // 固定值,接口要求
                     fileInfo.put("upload_file_id", fileId); // 上传文件返回的ID
                     fileInfo.put("type", "document"); // 文件类型,如 document/image 等(按接口要求)
                     jianliFiles.add(fileInfo);
                 }
                 inputs.put("jianli", jianliFiles); // jianli 对应 {variable_name}
             } else {
                 return Result.error("工作流接口返回异常,状态码:" + response.getStatusCodeValue());
                 inputs.put("jianli", new ArrayList<>()); // 空文件列表
             }
             // 添加其他文本参数
             inputs.put("benchmarkUrl", wenanyaoqiu);
             inputs.put("louchu", louchu);
             inputs.put("youshang", youshang);
             inputs.put("wenti", wenti);
             // 添加系统参数
             inputs.put("sys.user_id", userId);
             inputs.put("sys.app_id", appId);
             // 步骤3:构建完整请求体
             Map<String, Object> requestBody = new HashMap<>();
             requestBody.put("inputs", inputs); // 顶层 inputs 容器
             requestBody.put("user", user); // 顶层 inputs 容器
             SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// 连接超时:建立TCP连接的超时时间,单位毫秒(建议设5秒)
             factory.setConnectTimeout(5000);
// 读取超时:等待服务端响应数据的超时时间,单位毫秒(根据接口耗时调整,这里设30秒)
             factory.setReadTimeout(100000);
             // 步骤4:调用工作流接口
             RestTemplate restTemplate = new RestTemplate(factory);
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
             headers.set("Authorization", "Bearer " + authToken);
             HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
             ResponseEntity<String> response = restTemplate.postForEntity(workflowUrl, requestEntity, String.class);
             if (response.getStatusCode() == HttpStatus.OK) {
                 if (JSONObject.parseObject(response.getBody()).getJSONObject("data").get("outputs") == null) {
                     return Result.error(JSONObject.parseObject(response.getBody()).getJSONObject("data").getString("error"));
                 }
                 else{
                     String test = JSONObject.parseObject(response.getBody()).getJSONObject("data").getJSONObject("outputs").getString("http");
// ========== 中间操作:MD转HTML格式文本(保留所有回车,新增####四级标题处理) ==========
// 1. 定义MD语法正则(补充####四级标题,其他保留)
                     Pattern h1Pattern = Pattern.compile("^# (.*)", Pattern.MULTILINE);
                     Pattern h2Pattern = Pattern.compile("^## (.*)", Pattern.MULTILINE);
                     Pattern h3Pattern = Pattern.compile("^### (.*)", Pattern.MULTILINE);
                     Pattern h4Pattern = Pattern.compile("^#### (.*)", Pattern.MULTILINE); // 新增:四级标题正则
                     Pattern ulStartPattern = Pattern.compile("^- (.*)", Pattern.MULTILINE);
                     Pattern boldPattern = Pattern.compile("\\*\\*(.*?)\\*\\*");
                     Pattern hrPattern = Pattern.compile("^---+$", Pattern.MULTILINE);
// 2. 逐步替换MD语法为HTML标签(保留所有换行,新增四级标题转换)
                     test = h1Pattern.matcher(test).replaceAll("<h1>$1</h1>"); // 一级标题→<h1>
                     test = h2Pattern.matcher(test).replaceAll("<h2>$1</h2>"); // 二级标题→<h2>
                     test = h3Pattern.matcher(test).replaceAll("<h3>$1</h3>"); // 三级标题→<h3>
                     test = h4Pattern.matcher(test).replaceAll("<h4>$1</h4>"); // 新增:四级标题→<h4>
                     test = boldPattern.matcher(test).replaceAll("<strong>$1</strong>"); // 加粗→<strong>
                     test = hrPattern.matcher(test).replaceAll("<hr/>"); // 分隔线→<hr/>
// 处理无序列表(先标记列表项,再包裹<ul>)
                     test = ulStartPattern.matcher(test).replaceAll("<li>$1</li>");
                     test = test.replaceAll("(<li>.*?</li>\\r?\\n?)+", "<ul>$0</ul>");
// 保留原有回车(将\n转为HTML换行<br>,同时保留原始换行符)
                     test = test.replaceAll("\\r?\\n", "<br/>\n");
// ========== 转换结束,test为HTML格式且处理了####四级标题 ==========
                     return Result.OK("文案生成成功", test);
                 }
             } else {
                 return Result.error("工作流接口异常,状态码:" + response.getStatusCodeValue());
             }
         } catch (NullPointerException e) {
             e.printStackTrace();
             return Result.error("不支持的文件格式:" + jianli.split("\\.")[jianli.split("\\.").length - 1]);
         } catch (Exception e) {
             e.printStackTrace();
             return Result.error("生成文案异常:" + e.getMessage());
         }
     }
     private byte[] readFileToBytes(File file) throws Exception {
     /**
      * 新增的生成标题接口方法
      */
     @RequiresPermissions("copywriting:copywriting:aiCreateTitle")
     @RequestMapping(value = "/aiCreateTitle", method = RequestMethod.POST)
     public Result<?> aiCreateTitle(
             @RequestParam String louchu,
             @RequestParam String yuyici,
             @RequestParam String startTime,
             @RequestParam String endTime,
             @RequestParam String user) { // 保留user参数,用于接口鉴权/归属
         return getResult(louchu, yuyici, startTime, endTime, user);
     }
     public static Result<?> getResult(String louchu, String yuyici, String startTime, String endTime, String user) {
         // 2. 配置固定参数(和原有方法保持一致,可根据实际情况调整)
         String workflowUrl = "http://14.103.174.44/v1/workflows/run"; // 标题生成的工作流地址,若和文案不同需修改
         String authToken = "app-F09iyl3p5448JoKufR2CRpWG";
         String appId = "cf85fe4d-b76b-4c4c-801a-1336c880d473";
         try {
             // 3. 构建inputs参数(适配标题生成接口的参数格式)
             Map<String, Object> inputs = new HashMap<>();
             // 添加业务参数
             inputs.put("louchu", louchu);        // 露出
             inputs.put("yuyici", yuyici);        // 语气词
             inputs.put("startTime", startTime);  // 开始时间
             inputs.put("endTime", endTime);      // 结束时间
             // 添加系统参数(和原有方法一致)
             inputs.put("sys.user_id", user);
             inputs.put("sys.app_id", appId);
             // 4. 构建完整请求体
             Map<String, Object> requestBody = new HashMap<>();
             requestBody.put("inputs", inputs);   // 顶层inputs容器
             requestBody.put("user", user);       // 顶层user参数(保持和原有接口一致)
             // 5. 调用工作流接口
             RestTemplate restTemplate = new RestTemplate();
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.APPLICATION_JSON);
             headers.set("Authorization", "Bearer " + authToken);
             HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
             ResponseEntity<String> response = restTemplate.postForEntity(workflowUrl, requestEntity, String.class);
             // 6. 处理接口返回结果
             if (response.getStatusCode() == HttpStatus.OK) {
                 JSONObject responseJson = JSONObject.parseObject(response.getBody());
                 JSONObject dataJson = responseJson.getJSONObject("data");
                 // 校验返回结果结构
                 if (dataJson == null) {
                     return Result.error("接口返回数据格式异常:无data字段");
                 }
                 if (dataJson.get("outputs") == null) {
                     String errorMsg = dataJson.getString("error") != null ? dataJson.getString("error") : "标题生成失败,无具体错误信息";
                     return Result.error(errorMsg);
                 }
                 // 提取标题结果(假设outputs里的key是"title",需根据实际接口返回调整)
                 String title = dataJson.getJSONObject("outputs").getString("title");
                 if (title != null) {
                     return Result.OK("标题生成成功", title);
                 } else {
                     try {
                         return Result.OK(JSONObject.parseObject(dataJson.get("outputs").toString()).get("llm"));
                     } catch (Exception e ){
                     }
                 }
             } else {
                 return Result.error("工作流接口调用异常,状态码:" + response.getStatusCodeValue());
             }
         } catch (Exception e) {
             e.printStackTrace();
             return Result.error("生成标题异常:" + e.getMessage());
         }
         return Result.error("");
     }
     /**
      * 调用文件上传接口,获取 upload_file_id(修正版)
      */
     private String uploadFileToServer(File file, String uploadUrl, String authToken) throws Exception {
         // 构建文件上传请求(multipart/form-data 格式)
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.MULTIPART_FORM_DATA);
         headers.set("Authorization", "Bearer " + authToken);
         // 读取文件内容
         byte[] fileBytes = new byte[(int) file.length()];
         try (FileInputStream fis = new FileInputStream(file)) {
             byte[] bytes = new byte[(int) file.length()];
             fis.read(bytes);
             return bytes;
             fis.read(fileBytes);
         }
         // 构建多部分请求体
         MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
         parts.add("file", new ByteArrayResource(fileBytes) {
             @Override
             public String getFilename() {
                 return file.getName(); // 必须设置文件名
             }
         });
         HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(parts, headers);
         RestTemplate restTemplate = new RestTemplate();
         ResponseEntity<Map> response = restTemplate.postForEntity(uploadUrl, requestEntity, Map.class);
         // 修正:只要状态码是200 OK,且包含id字段,就是上传成功
         if ((response.getStatusCode() == HttpStatus.OK || response.getStatusCode() == HttpStatus.CREATED) && response.getBody() != null) {
             Object fileId = response.getBody().get("id");
             if (fileId != null) {
                 return fileId.toString(); // 成功返回upload_file_id
             } else {
                 throw new RuntimeException("文件上传成功,但未返回id字段,响应:" + response.getBody());
             }
         } else {
             throw new RuntimeException("文件上传失败,状态码:" + response.getStatusCode() + ",响应:" + response.getBody());
         }
     }
}