package org.jeecg.modules.demo.copywriting.controller; 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.demo.copywritingScheme.entity.CopywritingScheme; import org.jeecg.modules.demo.copywritingScheme.service.ICopywritingSchemeService; 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; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryRuleEnum; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.demo.contract.entity.SemanticWord; import org.jeecg.modules.demo.contract.service.ISemanticWordService; import org.jeecg.modules.demo.copywriting.entity.Copywriting; import org.jeecg.modules.demo.copywriting.service.ICopywritingService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import org.jeecgframework.poi.excel.ExcelImportUtil; import org.jeecgframework.poi.excel.def.NormalExcelConstants; import org.jeecgframework.poi.excel.entity.ExportParams; import org.jeecgframework.poi.excel.entity.ImportParams; import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; import org.jeecg.common.system.base.controller.JeecgController; 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; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSON; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import org.jeecg.common.aspect.annotation.AutoLog; import org.apache.shiro.authz.annotation.RequiresPermissions; /** * @Description: 文案 * @Author: jeecg-boot * @Date: 2025-10-13 * @Version: V1.0 */ @Tag(name="文案") @RestController @RequestMapping("/copywriting/copywriting") @Slf4j @EnableAsync public class CopywritingController extends JeecgController { @Autowired public ICopywritingService copywritingService; @Autowired private ISemanticWordService semanticWordService;; @Autowired private ISysUserService sysUserService; @Autowired private ICopywritingSchemeService copywritingSchemeService; /** * 分页列表查询 * * @param copywriting * @param pageNo * @param pageSize * @param req * @return */ //@AutoLog(value = "文案-分页列表查询") @Operation(summary="文案-分页列表查询") @GetMapping(value = "/list") public Result> queryPageList(Copywriting copywriting, @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req) { QueryWrapper 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 (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 page = new Page(pageNo, pageSize); IPage pageList = copywritingService.page(page, queryWrapper); for (Copywriting item : pageList.getRecords()) { item.setSemanticWord(semanticWordService.getById(item.getWordId())); } return Result.OK(pageList); } @Operation(summary="文案-查询发送门户文章总量") @GetMapping(value = "/count") public Result> count(Copywriting copywriting, @RequestParam(name="role", defaultValue="无") String role, @RequestParam(name="user", defaultValue="无") String user, HttpServletRequest req) { QueryWrapper 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(); 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> upAvgTime(Copywriting copywriting, HttpServletRequest req) { return Result.OK("5"); } /** * 添加 * * @param copywriting * @return */ @AutoLog(value = "文案-添加") @Operation(summary="文案-添加") @RequiresPermissions("copywriting:copywriting:add") @PostMapping(value = "/add") public Result add(@RequestBody Copywriting copywriting) { if (copywriting.getOutStatus() == null) { copywriting.setOutStatus("1"); } copywritingService.save(copywriting); return Result.OK("添加成功!"); } /** * 编辑 * * @param copywriting * @return */ @AutoLog(value = "文案-编辑") @Operation(summary="文案-编辑") @RequiresPermissions("copywriting:copywriting:edit") @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) public Result edit(@RequestBody Copywriting copywriting) { copywritingService.updateById(copywriting); return Result.OK("编辑成功!"); } /** * 通过id删除 * * @param id * @return */ @AutoLog(value = "文案-通过id删除") @Operation(summary="文案-通过id删除") @RequiresPermissions("copywriting:copywriting:delete") @DeleteMapping(value = "/delete") public Result delete(@RequestParam(name="id",required=true) String id) { copywritingService.removeById(id); return Result.OK("删除成功!"); } /** * 批量删除 * * @param ids * @return */ @AutoLog(value = "文案-批量删除") @Operation(summary="文案-批量删除") @RequiresPermissions("copywriting:copywriting:deleteBatch") @DeleteMapping(value = "/deleteBatch") public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { this.copywritingService.removeByIds(Arrays.asList(ids.split(","))); return Result.OK("批量删除成功!"); } /** * 通过id查询 * * @param id * @return */ //@AutoLog(value = "文案-通过id查询") @Operation(summary="文案-通过id查询") @GetMapping(value = "/queryById") public Result queryById(@RequestParam(name="id",required=true) String id) { Copywriting copywriting = copywritingService.getById(id); if(copywriting==null) { return Result.error("未找到对应数据"); } return Result.OK(copywriting); } /** * 导出excel * * @param request * @param copywriting */ @RequiresPermissions("copywriting:copywriting:exportXls") @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, Copywriting copywriting) { return super.exportXls(request, copywriting, Copywriting.class, "文案"); } @Value("${jeecg.path.upload}") private String uploadPath; /** * 通过excel导入数据 * * @param request * @param response * @return */ @RequiresPermissions("copywriting:copywriting:importExcel") @RequestMapping(value = "/importExcel", method = RequestMethod.POST) public Result importExcel(HttpServletRequest request, HttpServletResponse response) { return super.importExcel(request, response, Copywriting.class); } @RequiresPermissions("copywriting:copywriting:aiCreateCopyWriting") @RequestMapping(value = "/aiCreateCopyWriting", method = RequestMethod.POST) public Result aiCreateCopyWriting( @RequestParam(name = "file", required = false) String jianli, @RequestParam(name = "url", required = false) String jianli2, @RequestParam String wenanyaoqiu, @RequestParam String louchu, @RequestParam String youshang, @RequestParam String wenti, @RequestParam String user, @RequestParam String csId) { if (csId == null){ csId = "2020795745607319553"; } CopywritingScheme copywritingScheme = new CopywritingScheme(); copywritingScheme.setId(csId); return getResult(jianli, jianli2, wenanyaoqiu, louchu, youshang, wenti, user, copywritingScheme); } public Result getResult(String jianli, String jianli2, String wenanyaoqiu, String louchu, String youshang, String wenti, String user, CopywritingScheme cs) { QueryWrapper copywritingSchemeQueryWrapper = new QueryWrapper<>(); if (cs.getId() == null){ cs.setId("2020795745607319553"); } copywritingSchemeQueryWrapper.eq("id", cs.getId()); List list = copywritingSchemeService.list(copywritingSchemeQueryWrapper); if(list.size()==0) { return Result.error("方案不存在"); } else { cs = list.get(0); } // 配置信息 String serverFileRoot = uploadPath; String workflowUrl = cs.getWorkflowUrl(); String fileUploadUrl = cs.getFileUploadUrl(); // 文件上传接口(假设) String authToken = cs.getAuthToken(); String userId = user; String appId = cs.getAppId(); try { Map inputs = new HashMap<>(); if (jianli != null) { // 步骤2:构建 inputs 参数(按接口要求格式) List jianliFileList = Arrays.asList(jianli.split(",")); if (jianliFileList != null && !jianliFileList.isEmpty()) { // 步骤1:上传简历文件,获取 upload_file_id 列表 List jianliFileIds = new ArrayList<>(); for (String fileName : jianliFileList) { // 修正:只过滤路径遍历字符,保留合法的 / String safeFileName = File.separator + fileName.trim() // 过滤 ../ 和 ./ 序列(防止访问上级目录) .replaceAll("\\.\\./", "") .replaceAll("\\./", ""); // 进一步安全校验:确保拼接后的文件路径在服务器根目录下(核心安全措施) File file = new File(serverFileRoot + safeFileName); String canonicalPath = file.getCanonicalPath(); // 获取标准化路径(自动解析 ../ 等) String canonicalRootPath = new File(serverFileRoot).getCanonicalPath(); // 校验:如果文件路径不在服务器根目录下,视为非法请求 if (!canonicalPath.startsWith(canonicalRootPath)) { throw new RuntimeException("非法文件访问:" + fileName); } // 再判断文件是否存在 if (!file.exists() || !file.isFile()) { return Result.error("服务器不存在文件:" + safeFileName + ",路径:" + file.getAbsolutePath()); } // 调用文件上传接口,获取 upload_file_id String fileId = uploadFileToServer(file, fileUploadUrl, authToken); jianliFileIds.add(fileId); } // 处理简历文件(jianli 是 variable_name,对应接口的 {variable_name}) if (!jianliFileIds.isEmpty()) { // 若支持多个文件,可能需要数组形式;单个文件则直接放对象 List> jianliFiles = new ArrayList<>(); for (String fileId : jianliFileIds) { Map 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 { inputs.put("jianli", new ArrayList<>()); // 空文件列表 } } } if (jianli2 != null) { inputs.put("jianli2", jianli2); } // 添加其他文本参数 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 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(200000); // 步骤4:调用工作流接口 RestTemplate restTemplate = new RestTemplate(factory); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + authToken); HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity 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("

$1

"); // 一级标题→

test = h2Pattern.matcher(test).replaceAll("

$1

"); // 二级标题→

test = h3Pattern.matcher(test).replaceAll("

$1

"); // 三级标题→

test = h4Pattern.matcher(test).replaceAll("

$1

"); // 新增:四级标题→

test = boldPattern.matcher(test).replaceAll("$1"); // 加粗→ test = hrPattern.matcher(test).replaceAll("
"); // 分隔线→
// 处理无序列表(先标记列表项,再包裹
    ) test = ulStartPattern.matcher(test).replaceAll("
  • $1
  • "); test = test.replaceAll("(
  • .*?
  • \\r?\\n?)+", "
      $0
    "); // 保留原有回车(将\n转为HTML换行
    ,同时保留原始换行符) test = test.replaceAll("\\r?\\n", "
    \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()); } } /** * 新增的生成标题接口方法 */ @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 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 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> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity 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)) { fis.read(fileBytes); } // 构建多部分请求体 MultiValueMap parts = new LinkedMultiValueMap<>(); parts.add("file", new ByteArrayResource(fileBytes) { @Override public String getFilename() { return file.getName(); // 必须设置文件名 } }); HttpEntity> requestEntity = new HttpEntity<>(parts, headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity 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()); } } }