活动介绍

response.addHeader的value内容被自动拼接

时间: 2024-02-23 12:01:24 浏览: 249
是的,`response.addHeader()` 方法会自动将同名的响应头的值拼接起来,以逗号分隔。这个行为是 HTTP 规范中定义的。 例如,如果先调用了以下代码: ``` response.addHeader("Set-Cookie", "foo=bar"); ``` 然后再调用以下代码: ``` response.addHeader("Set-Cookie", "baz=qux"); ``` 则实际上设置的响应头内容为: ``` Set-Cookie: foo=bar, baz=qux ``` 如果需要分别设置多个值的响应头,则可以使用 `response.setHeader()` 方法来设置不同的响应头名称和值。
相关问题

历史记录 avater 若依集成EasyPOI 实现创建多个工作薄list集合对象下载导出详细代码 在同一个EXPOR 回答 在若依框架中集成 EasyPOI 实现导出多个 List 数据为多个工作簿(Sheet)并保存在同一 Excel 文件中,可以通过以下步骤实现。EasyPOI 提供了便捷的注解方式和 API 来处理 Excel 文件的导出操作,特别适合处理多个工作簿导出的需求。 实体类定义 首先,定义用于导出的数据模型类,例如 User 和 Product,并使用 EasyPOI 的注解来标记字段对应的 Excel 列。 Java import cn.afterturn.easypoi.excel.annotation.Excel; public class User { @Excel(name = "用户ID") private int id; @Excel(name = "用户名") private String name; @Excel(name = "年龄") private int age; // Getter 和 Setter } Java import cn.afterturn.easypoi.excel.annotation.Excel; public class Product { @Excel(name = "产品ID") private int productId; @Excel(name = "产品名称") private String productName; @Excel(name = "价格") private double price; // Getter 和 Setter } 控制器方法实现导出功能 在控制器中编写导出方法,使用 ExcelExportUtil 类来构建 Excel 文件,并将多个 List 数据导出为多个工作簿。 Java import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.TemplateExportParams; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; @Controller public class ExportController { @GetMapping("/export") public void exportData(HttpServletResponse response) throws IOException { // 构建用户数据 List<User> userList = new ArrayList<>(); userList.add(new User(1, "张三", 25)); userList.add(new User(2, "李四", 30)); // 构建产品数据 List<Product> productList = new ArrayList<>(); productList.add(new Product(101, "手机", 2999.99)); productList.add(new Product(102, "电脑", 8999.99)); // 构建参数 Map<String, Object> map = new HashMap<>(); map.put("title", "数据导出"); // 构建用户工作簿参数 ExportParams userExportParams = new ExportParams("用户信息", "用户"); map.put("userList", ExcelExportUtil.exportExcel(userExportParams, User.class, userList)); // 构建产品工作簿参数 ExportParams productExportParams = new ExportParams("产品信息", "产品"); map.put("productList", ExcelExportUtil.exportExcel(productExportParams, Product.class, productList)); // 导出 Excel 文件 Workbook workbook = ExcelExportUtil.exportExcel(map, "template.xlsx"); response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=data.xlsx"); workbook.write(response.getOutputStream()); } } 模板文件配置 如果需要使用模板文件,可以在项目资源目录中放置一个名为 template.xlsx 的文件,该文件可以包含一些固定的样式或格式,EasyPOI 会在此基础上填充数据。 Maven 依赖配置 确保在 pom.xml 文件中添加 EasyPOI 的依赖项: XML <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-web</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-annotation</artifactId> <version>4.4.0</version> </dependency> 注意事项 模板文件:如果不需要模板文件,可以直接使用 ExcelExportUtil.exportExcel 方法生成 Excel 文件。 响应处理:在控制器方法中,通过 HttpServletResponse 将生成的 Excel 文件写入响应流,以便浏览器下载。 异常处理:在实际应用中,建议添加异常处理逻辑,以确保在发生错误时能够正确返回错误信息。 通过上述步骤,可以在若依框架中集成 EasyPOI 实现导出多个 List 数据为多个工作簿,并保存在同一 Excel 文件中。这种方法不仅代码简洁,而且易于维护和扩展。 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 如何在若依框架中实现Excel文件的导入功能? 思维导图 参考来源 CSDN CSDN 若依集成EasyPOI 实现创建多个工作薄list集合对象下载导出详细代码 在同一个EXPOR 回答 3.1 实体类定义与注解配置 在实现导出多个工作簿(Sheet)之前,需要定义用于导出的数据模型类。EasyPOI 通过注解方式实现字段与 Excel 列的映射。 Java import cn.afterturn.easypoi.excel.annotation.Excel; public class User { @Excel(name = "用户ID") private int id; @Excel(name = "用户名") private String name; @Excel(name = "年龄") private int age; // Getter 和 Setter } Java import cn.afterturn.easypoi.excel.annotation.Excel; public class Product { @Excel(name = "产品ID") private int productId; @Excel(name = "产品名称") private String productName; @Excel(name = "价格") private double price; // Getter 和 Setter } 3.2 控制器方法实现导出多个工作簿 在控制器中编写导出方法,通过 ExcelExportUtil 构建 Excel 文件,并将多个 List 数据导出为多个工作簿。通过 ExportParams 设置工作簿名称和标题。 Java import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; @Controller public class ExportController { @GetMapping("/export") public void exportData(HttpServletResponse response) throws IOException { // 构建用户数据 List<User> userList = new ArrayList<>(); userList.add(new User(1, "张三", 25)); userList.add(new User(2, "李四", 30)); // 构建产品数据 List<Product> productList = new ArrayList<>(); productList.add(new Product(101, "手机", 2999.99)); productList.add(new Product(102, "电脑", 8999.99)); // 构建参数 Map<String, Object> map = new HashMap<>(); map.put("title", "数据导出"); // 构建用户工作簿参数 ExportParams userExportParams = new ExportParams("用户信息", "用户"); map.put("userList", ExcelExportUtil.exportExcel(userExportParams, User.class, userList)); // 构建产品工作簿参数 ExportParams productExportParams = new ExportParams("产品信息", "产品"); map.put("productList", ExcelExportUtil.exportExcel(productExportParams, Product.class, productList)); // 导出 Excel 文件 Workbook workbook = ExcelExportUtil.exportExcel(map, "template.xlsx"); response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=data.xlsx"); workbook.write(response.getOutputStream()); } } 3.3 Maven 依赖配置 确保在 pom.xml 文件中添加 EasyPOI 的依赖项,以便支持导出功能。 XML <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-web</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-annotation</artifactId> <version>4.4.0</version> </dependency> 3.4 模板文件配置(可选) 如果需要使用模板文件,可以在项目资源目录中放置一个名为 template.xlsx 的文件,该文件可以包含一些固定的样式或格式,EasyPOI 会在此基础上填充数据。若不需要模板,可直接使用 ExcelExportUtil.exportExcel 方法生成 Excel 文件。 3.5 响应处理与异常处理 在控制器方法中,通过 HttpServletResponse 将生成的 Excel 文件写入响应流,以便浏览器下载。在实际应用中,建议添加异常处理逻辑,以确保在发生错误时能够正确返回错误信息1。 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 如何在若依框架中实现Excel文件的导入功能? 思维导图 参考来源 CSDN CSDN CSDN CSDN 若依集成EasyPOI 实现创建多个工作薄list集合对象下载导出详细代码 在同一个EXPOR 回答 3.1 实体类定义与注解配置 在实现导出多个工作簿(Sheet)之前,需要定义用于导出的数据模型类。EasyPOI 通过注解方式实现字段与 Excel 列的映射。 Java import cn.afterturn.easypoi.excel.annotation.Excel; public class User { @Excel(name = "用户ID") private int id; @Excel(name = "用户名") private String name; @Excel(name = "年龄") private int age; // Getter 和 Setter } Java import cn.afterturn.easypoi.excel.annotation.Excel; public class Product { @Excel(name = "产品ID") private int productId; @Excel(name = "产品名称") private String productName; @Excel(name = "价格") private double price; // Getter 和 Setter } 3.2 控制器方法实现导出多个工作簿 在控制器中编写导出方法,通过 ExcelExportUtil 构建 Excel 文件,并将多个 List 数据导出为多个工作簿。通过 ExportParams 设置工作簿名称和标题。 Java import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; @Controller public class ExportController { @GetMapping("/export") public void exportData(HttpServletResponse response) throws IOException { // 构建用户数据 List<User> userList = new ArrayList<>(); userList.add(new User(1, "张三", 25)); userList.add(new User(2, "李四", 30)); // 构建产品数据 List<Product> productList = new ArrayList<>(); productList.add(new Product(101, "手机", 2999.99)); productList.add(new Product(102, "电脑", 8999.99)); // 构建参数 Map<String, Object> map = new HashMap<>(); map.put("title", "数据导出"); // 构建用户工作簿参数 ExportParams userExportParams = new ExportParams("用户信息", "用户"); map.put("userList", ExcelExportUtil.exportExcel(userExportParams, User.class, userList)); // 构建产品工作簿参数 ExportParams productExportParams = new ExportParams("产品信息", "产品"); map.put("productList", ExcelExportUtil.exportExcel(productExportParams, Product.class, productList)); // 导出 Excel 文件 Workbook workbook = ExcelExportUtil.exportExcel(map, "template.xlsx"); response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=data.xlsx"); workbook.write(response.getOutputStream()); } } 3.3 Maven 依赖配置 确保在 pom.xml 文件中添加 EasyPOI 的依赖项,以便支持导出功能。 XML <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-web</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-annotation</artifactId> <version>4.4.0</version> </dependency> 3.4 模板文件配置(可选) 如果需要使用模板文件,可以在项目资源目录中放置一个名为 template.xlsx 的文件,该文件可以包含一些固定的样式或格式,EasyPOI 会在此基础上填充数据。若不需要模板,可直接使用 ExcelExportUtil.exportExcel 方法生成 Excel 文件。 3.5 响应处理与异常处理 在控制器方法中,通过 HttpServletResponse 将生成的 Excel 文件写入响应流,以便浏览器下载。在实际应用中,建议添加异常处理逻辑,以确保在发生错误时能够正确返回错误信息1。 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 如何在若依框架中实现Excel文件的导入功能? 思维导图 参考来源 CSDN CSDN CSDN CSDN CSDN Workbook workbook = ExcelExportUtil.exportExcel(map, "template.xlsx"); 报错 回答 问题分析 在使用 EasyPOI 的 ExcelExportUtil.exportExcel(map, template.xlsx) 方法进行 Excel 导出时,可能会出现报错。这类问题通常与模板文件路径、数据结构匹配、EasyPOI 版本兼容性或模板格式有关。 1. 模板文件路径问题 若 template.xlsx 文件路径不正确,EasyPOI 将无法读取模板内容,导致导出失败。应确保模板文件存在于类路径下,例如 resources 目录,并通过 ClassPathResource 或相对路径加载。 Java String templatePath = "template.xlsx"; InputStream templateInputStream = new ClassPathResource(templatePath).getInputStream(); 在调用 ExcelExportUtil.exportExcel(map, templatePath) 时,若 templatePath 是相对路径,应确保程序运行时该路径可访问。若使用绝对路径,需确保路径字符串格式正确,避免因路径拼接错误导致文件读取失败1。 2. 数据结构与模板字段不匹配 EasyPOI 在导出时会将 map 中的键与模板中的字段进行匹配。若 map 中的键名与模板中定义的占位符不一致,或缺少必要的字段,会导致数据填充失败。 例如,若模板中包含 ${userList} 和 ${productList},则 map 中必须包含这两个键,并且对应的值应为 List 类型。 Java map.put("userList", userList); map.put("productList", productList); 此外,若使用 @Excel 注解定义字段映射,需确保注解中的 name 属性与模板中的列标题一致,否则数据无法正确导出2。 3. EasyPOI 版本兼容性问题 不同版本的 EasyPOI 对模板处理方式可能不同。例如,旧版本可能不支持某些模板语法或导出方式。建议使用稳定版本(如 4.4.0)以确保兼容性。 XML <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.4.0</version> </dependency> 若使用较新版本仍存在问题,可尝试回退至已验证可用的版本,或查阅官方文档确认模板使用方式是否发生变化。 4. 模板文件格式问题 EasyPOI 支持 .xls 和 .xlsx 两种格式的模板文件,但在使用时需注意: 若使用 .xls 格式,需确保模板未被损坏,且字段占位符格式正确(如 ${userList})。 若使用 .xlsx 格式,需注意其对样式和合并单元格的支持更好,但对模板结构要求更高。 若模板中包含复杂的样式、合并单元格或条件格式,可能导致 EasyPOI 解析失败。建议先使用简单模板进行测试,再逐步增加复杂度。 5. 异常日志分析 当调用 ExcelExportUtil.exportExcel(map, template.xlsx) 报错时,应优先查看控制台日志或日志文件,定位具体错误信息。常见异常包括: FileNotFoundException:表示模板文件未找到,需检查路径是否正确。 NullPointerException:表示 map 或模板对象为空,需检查数据是否正确填充。 ExcelDataConvertException:表示数据类型转换失败,需检查实体类字段类型与模板字段是否匹配。 通过日志可快速定位问题根源,避免盲目修改代码。 6. 完整示例代码 以下为一个完整的导出方法示例,包含模板路径加载、数据填充与响应输出: Java @GetMapping("/export") public void exportData(HttpServletResponse response) throws IOException { // 构建用户数据 List<User> userList = Arrays.asList( new User(1, "张三", 25), new User(2, "李四", 30) ); // 构建产品数据 List<Product> productList = Arrays.asList( new Product(101, "手机", 2999.99), new Product(102, "电脑", 8999.99) ); // 构建参数 Map<String, Object> map = new HashMap<>(); map.put("userList", userList); map.put("productList", productList); // 加载模板文件 String templatePath = "template.xlsx"; InputStream templateInputStream = new ClassPathResource(templatePath).getInputStream(); // 导出 Excel 文件 Workbook workbook = ExcelExportUtil.exportExcel(map, templateInputStream); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=data.xlsx"); workbook.write(response.getOutputStream()); } 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 如何在若依框架中实现Excel文件的导入功能? 思维导图 参考来源 CSDN CSDN Workbook workbook = ExcelExportUtil.exportExcel(map, "template.xlsx"); 无法传值 Map<String, Object> 回答 在使用 ExcelExportUtil.exportExcel(map, template.xlsx) 时,若出现 map 传值失败的问题,通常是因为数据结构与模板字段不匹配或导出方式未正确使用。为确保 Map<String, Object> 参数能够正确传递给模板文件,需遵循以下要点。 使用 Map 传递数据的正确方式 ExcelExportUtil.exportExcel 方法支持将 Map<String, Object> 数据结构传递给模板进行填充。该 map 中的键应与模板中的占位符名称一致,值则为相应的数据对象或集合。例如,若模板中包含 ${userList} 和 ${productList},则 map 中应包含这两个键,并且对应的值应为 List 类型。 Java Map<String, Object> map = new HashMap<>(); map.put("userList", userList); map.put("productList", productList); 通过这种方式,EasyPOI 能够正确识别并填充模板中的数据字段1。 创建多个 Sheet 的方式 若需要在导出时创建多个 Sheet,可使用 List<Map<String, Object>> 结构,每个 Map 表示一个 Sheet 的配置。每个配置应包含 sheetName、data 和 entity 等信息。例如: Java List<Map<String, Object>> sheetsList = new ArrayList<>(); public Map<String, Object> createSheet(String sheetName, List<ExportVo> dataList) { if (StringUtils.isBlank(sheetName) || CollectionUtils.isEmpty(dataList)) { return new HashMap<>(); } ExportParams exportParams = new ExportParams(); exportParams.setSheetName(sheetName); exportParams.setHeight((short) 8); exportParams.setStyle(ExcelExportStylerDefaultImpl.class); Map<String, Object> map = new HashMap<>(); map.put("data", dataList); map.put("entity", ExportVo.class); map.put("title", exportParams); return map; } 将多个 Sheet 的配置加入 sheetsList 后,可通过 ExcelExportUtil.exportExcel(sheetsList, templatePath) 进行导出。 处理主从表结构 在处理主从表结构时,可使用 List<Map<String, Object>> 来表示每一行数据,其中主表字段和从表字段均作为键值对存在。例如: Java List<Map<String, Object>> dataList = new ArrayList<>(); Map<String, Object> row = new HashMap<>(); row.put("mainField", "主表数据"); row.put("detailField1", "详情1"); row.put("detailField2", "详情2"); dataList.add(row); 通过这种方式,可实现动态导出主从表数据,且无需固定从表字段数量4。 示例代码 以下为一个完整的导出方法示例,包含模板路径加载、数据填充与响应输出: Java @GetMapping("/export") public void exportData(HttpServletResponse response) throws IOException { // 构建用户数据 List<User> userList = Arrays.asList( new User(1, "张三", 25), new User(2, "李四", 30) ); // 构建产品数据 List<Product> productList = Arrays.asList( new Product(101, "手机", 2999.99), new Product(102, "电脑", 8999.99) ); // 构建参数 Map<String, Object> map = new HashMap<>(); map.put("userList", userList); map.put("productList", productList); // 加载模板文件 String templatePath = "template.xlsx"; InputStream templateInputStream = new ClassPathResource(templatePath).getInputStream(); // 导出 Excel 文件 Workbook workbook = ExcelExportUtil.exportExcel(map, templateInputStream); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=data.xlsx"); workbook.write(response.getOutputStream()); } 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 如何在若依框架中实现Excel文件的导入功能? 思维导图 参考来源 CSDN CSDN CSDN CSDN CSDN Caused by: java.lang.NoClassDefFoundError: org/apache/poi/xssf/usermodel/XSSFCellStyle 怎样解决 回答 依赖缺失问题分析 java.lang.NoClassDefFoundError: org.apache.poi.xssf.usermodel.XSSFCellStyle 通常表明项目中缺少 Apache POI 的相关依赖。在使用 Apache POI 进行 Excel 导出时,必须确保所有必要的依赖库都已正确引入,尤其是 poi-ooxml 模块,该模块包含了 XSSFCellStyle 等用于处理 .xlsx 格式文件的核心类1。 依赖配置建议 在 Maven 项目中,需在 pom.xml 文件中添加如下依赖以确保 POI 的完整支持: XML <!-- Apache POI 核心依赖 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.3</version> </dependency> <!-- POI 对 OOXML 格式(xlsx)的支持 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> </dependency> <!-- 可选:处理 Excel 样式和复杂格式 --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml-schemas</artifactId> <version>5.2.3</version> </dependency> 上述依赖确保了 XSSFCellStyle、XSSFWorkbook、XSSFSheet 等类的可用性,是构建 Excel 导出功能的基础1。 依赖冲突排查 若项目中已引入 POI 依赖但仍出现 NoClassDefFoundError,则可能是依赖版本冲突或被其他依赖覆盖。建议使用以下命令查看当前依赖树: Bash mvn dependency:tree 通过分析输出结果,可识别出是否存在多个版本的 POI 被引入,进而通过排除冲突依赖或统一版本号来解决该问题。 打包构建问题处理 在使用 jar 包部署时,若依赖未被正确打包进最终的可执行文件中,也可能导致运行时报错。确保构建插件配置如下,以将所有依赖打包进 jar 文件: XML <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> 通过该方式,可确保所有 POI 依赖在构建时被正确包含进 jar 包中,避免因依赖缺失导致的运行时错误1。 示例代码 以下为使用 POI 创建 Excel 文件并设置单元格样式的简单示例: Java import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileOutputStream; import java.io.IOException; public class ExcelExportExample { public static void main(String[] args) throws IOException { Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("Sheet1"); // 创建单元格样式 CellStyle headerStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); headerStyle.setFont(font); // 创建表头 Row headerRow = sheet.createRow(0); Cell headerCell = headerRow.createCell(0); headerCell.setCellValue("姓名"); headerCell.setCellStyle(headerStyle); // 创建数据行 Row dataRow = sheet.createRow(1); Cell dataCell = dataRow.createCell(0); dataCell.setCellValue("张三"); // 写出到文件 try (FileOutputStream fos = new FileOutputStream("example.xlsx")) { workbook.write(fos); } workbook.close(); } } 相关提问 如何在Spring Boot中使用EasyPOI导出Excel文件? EasyPOI支持哪些Excel导出功能? 在若依框架中使用EasyPOI导出Excel时如何处理大数据量? 思维导图 参考来源 CSDN easypoi 异常 org.apache.poi.common.Duplicatable 怎样解决 回答 我们注意到用户提到了两个引用: 引用[1]提到了使用POI时遇到的POIXMLException,但用户当前的问题是关于org.apache.poi.common.Duplicatable异常。 引用[2]则详细描述了当创建过多单元格样式时,会超过Excel的限制(64000个样式),并提出了通过复制样式(使用Duplicatable接口的copy方法)来避免计数增加的解决方案。 用户当前的问题:EasyPOI 报错 org.apache.poi.common.Duplicatable 异常解决方法 分析:根据引用[2]的内容,这个异常通常是由于在Excel中创建了超过64000个样式导致的。因为每个样式都会占用一个样式ID,而Excel限制了最大样式数量。在POI中,每次调用workbook.createCellStyle()都会创建一个新的样式并增加计数器,当超过最大限制时就会抛出异常。 解决思路:避免创建过多的样式对象,而是复用已有的样式。或者,当需要微调样式时,不要创建全新的样式,而是复制一个已有的样式并做修改,这样复制的样式不会增加计数(因为使用了Duplicatable.copy()方法)。 具体步骤: 定位问题:检查代码中创建单元格样式的地方,特别是在循环中创建样式的情况。 修改样式创建方式:将创建样式移到循环外部,然后通过复制样式来创建新的样式,而不是每次都调用createCellStyle()。 示例代码(引用[2]中的解决方案): 原始代码可能如下(在循环内部创建样式): Java for (int i = 0; i < 100000; i++) { CellStyle style = workbook.createCellStyle(); // 每次循环都创建一个新样式,计数增加 // ... 设置样式属性 cell.setCellStyle(style); } 修改后: Java // 先创建一个基础样式 CellStyle baseStyle = workbook.createCellStyle(); // 设置基础样式的一些通用属性,比如边框、字体等 for (int i = 0; i < 100000; i++) { // 复制基础样式,而不是创建新的样式 CellStyle newStyle = (CellStyle) ((Duplicatable) baseStyle).copy(); // 如果需要,可以修改newStyle的某些属性(注意:不能修改基础样式,否则会影响所有复制的样式) // 例如:设置不同的背景颜色等 // newStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); cell.setCellStyle(newStyle); } 注意:copy()方法返回的是Object,需要强制转换为CellStyle,并且前提是baseStyle实现了Duplicatable接口(实际上POI内置的样式类都实现了这个接口)。 这样,虽然我们创建了多个样式对象,但是通过复制的方式不会增加工作簿中样式ID的计数,因此可以避免超过64000个样式的限制。 在EasyPOI中的处理: EasyPOI是对POI的封装,因此我们可能需要查看EasyPOI的导出代码,看是否有地方在循环中创建样式。如果使用的是EasyPOI的注解导出,通常不会直接操作样式,但如果是自定义样式导出(比如使用IExcelExportStyler),则需要注意。 如果是在自定义的IExcelExportStyler实现中,需要避免在getStyles方法中频繁创建样式。EasyPOI在导出时会调用getStyles方法获取样式,因此应该在这个方法内部缓存样式,避免每次调用都创建新的样式。 例如,在IExcelExportStyler的实现类中,可以这样写: Java public class MyExcelExportStyler extends AbstractExcelExportStyler implements IExcelExportStyler { private CellStyle headerStyle; private CellStyle titleStyle; // 其他样式 public MyExcelExportStyler(Workbook workbook) { super(workbook); // 初始化样式 headerStyle = createHeaderStyle(); titleStyle = createTitleStyle(); // ... } @Override public CellStyle getHeaderStyle(short color) { // 如果需要根据颜色变化,可以复制基础样式并修改颜色 CellStyle style = (CellStyle) ((Duplicatable) headerStyle).copy(); style.setFillForegroundColor(color); return style; } // 其他方法类似 } 这样,在初始化时创建一次基础样式,后续通过复制来创建新样式,避免计数增加。 总结解决方法: 检查代码中创建单元格样式的位置,避免在循环中直接调用workbook.createCellStyle()。 使用Duplicatable.copy()方法来复制样式,而不是创建新样式。 在EasyPOI中,如果自定义了IExcelExportStyler,确保在构造方法中初始化基础样式,并在需要时复制这些样式。 通过以上方法,可以解决org.apache.poi.common.Duplicatable相关的异常(实际上是样式数量超过限制的问题)。 相关问题 §§相关问题§§ 如何优化EasyPOI导出大量数据时的性能? 在EasyPOI中如何自定义复杂的Excel样式? EasyPOI导出Excel时如何避免内存溢出? 解决 EasyPOI 报错 org.apache.poi.common.Duplicatable 异常 该异常通常是由于 Excel 样式数量超过限制(最大 64,000 个样式)导致的,以下是完整解决方案: 问题原因分析 Excel 样式限制:每个 .xlsx 文件最多支持 64,000 种独立样式2 样式创建方式:每次调用 workbook.createCellStyle() 会生成新样式并计数 循环中创建样式:在数据遍历中重复创建样式会快速耗尽样式配额 Java // 错误示范:在循环中创建样式(耗尽样式配额) for (Row row : sheet) { CellStyle style = workbook.createCellStyle(); // 每次循环都创建新样式 style.setFillForegroundColor(IndexedColors.YELLOW.index); cell.setCellStyle(style); } 解决方案:使用样式复制技术 通过 Duplicatable.copy() 复制样式而非创建新样式,避免计数增加: 1. 基础样式池(推荐) Java // 创建基础样式池 private Map<String, CellStyle> styleCache = new HashMap<>(); private CellStyle getOrCreateStyle(Workbook workbook, String styleKey) { if (!styleCache.containsKey(styleKey)) { CellStyle baseStyle = workbook.createCellStyle(); // 配置基础样式... styleCache.put(styleKey, baseStyle); } // 复制样式(不增加计数) return (CellStyle) ((Duplicatable) styleCache.get(styleKey)).copy(); } // 使用示例 for (Row row : sheet) { CellStyle style = getOrCreateStyle(workbook, "YELLOW_BG"); cell.setCellStyle(style); // 复制样式而非创建 } 2. 动态样式复制 Java // 创建基础样式(仅1次) CellStyle baseStyle = workbook.createCellStyle(); baseStyle.setFillForegroundColor(IndexedColors.YELLOW.index); for (Row row : sheet) { // 复制样式(关键步骤) CellStyle newStyle = (CellStyle) ((Duplicatable) baseStyle).copy(); // 按需修改特定属性(可选) if (specialCase) { newStyle.setFillForegroundColor(IndexedColors.RED.index); } cell.setCellStyle(newStyle); // 应用复制样式 } 优化建议 样式复用:相同样式的单元格共享样式实例 按需创建:仅在样式差异处创建新样式 样式池管理:对常用样式进行缓存(如标题样式、数据样式等) 批量处理:大数据量导出时使用 SXSSFWorkbook(流式导出) Java // 启用流式导出(避免OOM) SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 保留100行在内存中 验证效果 通过以下代码检查样式计数: Java int styleCount = workbook.getNumCellStyles(); System.out.println("当前样式数量: " + styleCount); // 应保持在合理范围 关键点:使用 Duplicatable.copy() 复制样式不会增加 workbook 的样式计数器,从而避免触发 64,000 的样式上限2。 相关提问 EasyPOI 导出大数据量时如何避免内存溢出? 如何在 EasyPOI 中实现动态单元格样式? EasyPOI 与原生 POI 的性能对比如何? 思维导图 参考来源 CSDN CSDN 若依框架 怎样Excel导出创个多工作薄导出不同数据详细可用代码

<think>我们将在若依框架中集成EasyPOI来实现多Sheet导出功能。根据引用[2]和引用[3]的提示,EasyPOI可以处理复杂的Excel导出,包括多Sheet的情况。以下是具体步骤: ### 1. 添加EasyPOI依赖 在`pom.xml`中添加EasyPOI依赖: ```xml <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency> ``` ### 2. 创建实体类(使用EasyPOI注解) ```java // 用户实体 @Data public class User { @Excel(name = "用户ID", orderNum = "0") private Long userId; @Excel(name = "用户名", orderNum = "1") private String userName; } // 部门实体 @Data public class Dept { @Excel(name = "部门ID", orderNum = "0") private Long deptId; @Excel(name = "部门名称", orderNum = "1") private String deptName; } ``` ### 3. 编写多Sheet导出工具类 ```java import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import org.apache.poi.ss.usermodel.Workbook; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.util.List; import java.util.Map; public class EasyPoiMultiSheetUtil { /** * 多Sheet导出 * @param response 响应对象 * @param fileName 文件名 * @param sheetMap Sheet数据映射 * key: Sheet名称, value: Map<"title"->Sheet标题, "entityType"->实体类类型, "data"->数据列表> */ public static void exportMultiSheet(HttpServletResponse response, String fileName, Map<String, Map<String, Object>> sheetMap) throws IOException { // 创建工作簿对象 Workbook workbook = null; // 遍历每个Sheet for (Map.Entry<String, Map<String, Object>> entry : sheetMap.entrySet()) { String sheetName = entry.getKey(); Map<String, Object> sheetInfo = entry.getValue(); ExportParams exportParams = new ExportParams(); exportParams.setSheetName(sheetName); exportParams.setTitle((String) sheetInfo.get("title")); exportParams.setType(ExcelType.XSSF); // 使用XSSF格式(支持大数据量) // 如果是第一个Sheet,则创建Workbook,否则追加Sheet if (workbook == null) { workbook = ExcelExportUtil.exportExcel(exportParams, (Class<?>) sheetInfo.get("entityType"), (List<?>) sheetInfo.get("data")); } else { ExcelExportUtil.exportExcel(exportParams, (Class<?>) sheetInfo.get("entityType"), (List<?>) sheetInfo.get("data"), workbook); } } // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx"); // 输出到响应流 workbook.write(response.getOutputStream()); workbook.close(); } } ``` ### 4. 在Controller中使用 ```java import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.List; import java.util.Map; @GetMapping("/exportMultiSheet") public void exportMultiSheet(HttpServletResponse response) throws IOException { // 准备多Sheet数据 Map<String, Map<String, Object>> sheetMap = new HashMap<>(); // 用户数据Sheet Map<String, Object> userSheet = new HashMap<>(); userSheet.put("title", "用户信息表"); userSheet.put("entityType", User.class); userSheet.put("data", userService.selectUserList()); sheetMap.put("用户信息", userSheet); // 部门数据Sheet Map<String, Object> deptSheet = new HashMap<>(); deptSheet.put("title", "部门信息表"); deptSheet.put("entityType", Dept.class); deptSheet.put("data", deptService.selectDeptList()); sheetMap.put("部门信息", deptSheet); // 执行导出 EasyPoiMultiSheetUtil.exportMultiSheet(response, "多Sheet报表", sheetMap); } ``` ### 5. 处理大数据量(参考引用[2]) 若数据量超过10万行,建议使用分页查询并导出,避免内存溢出: ```java // 在工具类中增加分页导出方法 public static void exportLargeDataMultiSheet(HttpServletResponse response, String fileName, Map<String, Map<String, Object>> sheetMap, int pageSize) throws IOException { Workbook workbook = null; for (Map.Entry<String, Map<String, Object>> entry : sheetMap.entrySet()) { String sheetName = entry.getKey(); Map<String, Object> sheetInfo = entry.getValue(); Class<?> entityType = (Class<?>) sheetInfo.get("entityType"); List<?> fullData = (List<?>) sheetInfo.get("data"); // 分页处理 int total = fullData.size(); int pages = (total + pageSize - 1) / pageSize; for (int page = 1; page <= pages; page++) { int fromIndex = (page - 1) * pageSize; int toIndex = Math.min(page * pageSize, total); List<?> subList = fullData.subList(fromIndex, toIndex); ExportParams exportParams = new ExportParams(); exportParams.setSheetName(sheetName + (pages > 1 ? "_" + page : "")); exportParams.setTitle((String) sheetInfo.get("title")); exportParams.setType(ExcelType.XSSF); if (workbook == null) { workbook = ExcelExportUtil.exportExcel(exportParams, entityType, subList); } else { ExcelExportUtil.exportExcel(exportParams, entityType, subList, workbook); } } } // 响应设置(同上) // ... } ``` ### 关键说明 1. **注解配置**:使用`@Excel`注解配置表头 2. **动态Sheet**:通过循环添加多个Sheet 3. **性能优化**: - 使用`ExcelType.XSSF`支持大数据量 - 分页处理避免OOM 4. **样式保留**:EasyPOI会保留模板中的样式(若有模板) > **注意**:实际项目中,若数据量极大,建议使用`SXSSFWorkbook`进行流式导出(EasyPOI底层支持)[^2]。 ### 效果 导出的Excel文件将包含: - **用户信息** Sheet:用户数据 - **部门信息** Sheet:部门数据 - 每个Sheet都有独立的标题和表头 §§相关问题§§ 1. 如何在若依框架中使用EasyPOI导入多Sheet的Excel文件? 2. EasyPOI如何处理带有合并单元格的复杂Excel导出? 3. 若依框架中如何实现Excel模板导出(使用EasyPOI)? 4. EasyPOI导出时如何设置单元格样式? 5. 如何解决EasyPOI导出大数据量Excel时的内存溢出问题?</think># 若依框架集成EasyPOI实现多Sheet导出 在若依框架中集成EasyPOI实现多Sheet导出功能非常简便,下面我将详细介绍完整的实现步骤和代码示例。 ## 实现思路 1. 添加EasyPOI依赖 2. 创建不同数据类型的实体类 3. 编写多Sheet导出工具类 4. 在Controller中调用导出方法 ## 完整实现代码 ### 1. 添加EasyPOI依赖 在`pom.xml`中添加EasyPOI依赖: ```xml <dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency> ``` ### 2. 创建实体类(使用EasyPOI注解) ```java // 用户实体 @Data public class User { @Excel(name = "用户ID", orderNum = "0") private Long userId; @Excel(name = "用户名", orderNum = "1") private String userName; @Excel(name = "创建时间", format = "yyyy-MM-dd", orderNum = "2") private Date createTime; } // 部门实体 @Data public class Dept { @Excel(name = "部门ID", orderNum = "0") private Long deptId; @Excel(name = "部门名称", orderNum = "1") private String deptName; @Excel(name = "部门人数", orderNum = "2") private Integer userCount; } // 角色实体 @Data public class Role { @Excel(name = "角色ID", orderNum = "0") private Long roleId; @Excel(name = "角色名称", orderNum = "1") private String roleName; @Excel(name = "角色标识", orderNum = "2") private String roleKey; } ``` ### 3. 编写多Sheet导出工具类 ```java import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import org.apache.poi.ss.usermodel.Workbook; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.util.List; import java.util.Map; public class EasyPoiMultiSheetUtil { /** * 多Sheet导出 * @param response 响应对象 * @param fileName 文件名 * @param sheetMap Sheet数据映射 * key: Sheet名称, value: Map<"title"->Sheet标题, "entityType"->实体类类型, "data"->数据列表> */ public static void exportMultiSheet(HttpServletResponse response, String fileName, Map<String, Map<String, Object>> sheetMap) throws IOException { // 创建工作簿对象 Workbook workbook = null; // 遍历每个Sheet for (Map.Entry<String, Map<String, Object>> entry : sheetMap.entrySet()) { String sheetName = entry.getKey(); Map<String, Object> sheetInfo = entry.getValue(); ExportParams exportParams = new ExportParams(); exportParams.setSheetName(sheetName); exportParams.setTitle((String) sheetInfo.get("title")); exportParams.setType(ExcelType.XSSF); // 使用XSSF格式(支持大数据量) // 如果是第一个Sheet,则创建Workbook,否则追加Sheet if (workbook == null) { workbook = ExcelExportUtil.exportExcel(exportParams, (Class<?>) sheetInfo.get("entityType"), (List<?>) sheetInfo.get("data")); } else { ExcelExportUtil.exportExcel(exportParams, (Class<?>) sheetInfo.get("entityType"), (List<?>) sheetInfo.get("data"), workbook); } } // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx"); // 输出到响应流 workbook.write(response.getOutputStream()); workbook.close(); } } ``` ### 4. 在Controller中使用 ```java import org.springframework.web.bind.annotation.GetMapping; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.List; import java.util.Map; @GetMapping("/exportMultiSheet") public void exportMultiSheet(HttpServletResponse response) throws IOException { // 准备多Sheet数据 Map<String, Map<String, Object>> sheetMap = new HashMap<>(); // 用户数据Sheet Map<String, Object> userSheet = new HashMap<>(); userSheet.put("title", "用户信息表"); userSheet.put("entityType", User.class); userSheet.put("data", userService.selectUserList()); sheetMap.put("用户信息", userSheet); // 部门数据Sheet Map<String, Object> deptSheet = new HashMap<>(); deptSheet.put("title", "部门信息表"); deptSheet.put("entityType", Dept.class); deptSheet.put("data", deptService.selectDeptList()); sheetMap.put("部门信息", deptSheet); // 角色数据Sheet Map<String, Object> roleSheet = new HashMap<>(); roleSheet.put("title", "角色信息表"); roleSheet.put("entityType", Role.class); roleSheet.put("data", roleService.selectRoleList()); sheetMap.put("角色信息", roleSheet); // 执行导出 EasyPoiMultiSheetUtil.exportMultiSheet(response, "系统数据报表", sheetMap); } ``` ### 5. 处理大数据量(可选) 对于大数据量导出,可以使用分页处理避免内存溢出: ```java // 在工具类中增加分页导出方法 public static void exportLargeDataMultiSheet(HttpServletResponse response, String fileName, Map<String, Map<String, Object>> sheetMap, int pageSize) throws IOException { Workbook workbook = null; for (Map.Entry<String, Map<String, Object>> entry : sheetMap.entrySet()) { String sheetName = entry.getKey(); Map<String, Object> sheetInfo = entry.getValue(); Class<?> entityType = (Class<?>) sheetInfo.get("entityType"); List<?> fullData = (List<?>) sheetInfo.get("data"); // 分页处理 int total = fullData.size(); int pages = (total + pageSize - 1) / pageSize; for (int page = 1; page <= pages; page++) { int fromIndex = (page - 1) * pageSize; int toIndex = Math.min(page * pageSize, total); List<?> subList = fullData.subList(fromIndex, toIndex); ExportParams exportParams = new ExportParams(); exportParams.setSheetName(sheetName + (pages > 1 ? "_" + page : "")); exportParams.setTitle((String) sheetInfo.get("title")); exportParams.setType(ExcelType.XSSF); if (workbook == null) { workbook = ExcelExportUtil.exportExcel(exportParams, entityType, subList); } else { ExcelExportUtil.exportExcel(exportParams, entityType, subList, workbook); } } } // 响应设置(同上) // ... } ``` ## 关键点说明 1. **EasyPOI注解使用**: - `@Excel`注解用于定义Excel列名和顺序 - `format`属性可以格式化日期等特殊类型 - `orderNum`控制列的顺序 2. **多Sheet实现原理**: - 使用`ExcelExportUtil.exportExcel()`创建第一个Sheet - 后续使用`ExcelExportUtil.exportExcel()`追加Sheet - 所有Sheet共享同一个Workbook对象 3. **性能优化**: - 使用`ExcelType.XSSF`支持大数据量导出 - 对于超过10万行的数据,建议使用分页处理 - 避免在循环中频繁创建Workbook对象 4. **响应头设置**: - 正确设置Content-Type为Excel格式 - 使用URLEncoder处理文件名,解决中文乱码问题 - 设置`Content-disposition`为attachment实现文件下载 ## 导出效果 导出的Excel文件将包含多个Sheet: 1. **用户信息** Sheet:用户数据列表 2. **部门信息** Sheet:部门数据列表 3. **角色信息** Sheet:角色数据列表 每个Sheet都有独立的表头、标题和数据格式,且所有Sheet都在同一个Excel文件中。 > **提示**:对于更复杂的导出需求(如合并单元格、自定义样式等),EasyPOI支持模板导出功能,可以创建Excel模板文件并填充数据[^2]。

# https://search.dangdang.com/?key=%B1%E0%B3%CC&act=input&page_index=1 # 导入requests import requests import time from bs4 import BeautifulSoup import mysql.connector import csv # 定义容器 用来存储所有数据 allContainer = [] for i in range(1, 36): # 判断当前是否为第一次循环 if i == 1: url = "https://search.dangdang.com/?key=%B1%E0%B3%CC&act=input&page_index=1" else: url = f"https://search.dangdang.com/?key=%B1%E0%B3%CC&act=input&page_index={i}" print(f"当前已完成第{i}次") # 循环休眠 防止检测 time.sleep(1) # 发起请求 # 请求头 header = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", "Cache-Control": "max-age=0", "Connection": "keep-alive", "Cookie": "ddscreen=2; __permanent_id=20250609184530979156760224679480468; __visit_id=20250609184530993130404124438448889; __out_refer=1749465931%7C!%7Cwww.baidu.com%7C!%7C; dest_area=country_id%3D9000%26province_id%3D111%26city_id%3D0%26district_id%3D0%26town_id%3D0; __rpm=s_112100.155956512835%2C155956512836..1749466159510%7Cs_112100.155956512835%2C155956512836..1749466166450; search_passback=1e0bf85a587c99ab37bc4668fc0100003945670025bc4668; __trace_id=20250609184927332100480187110221325", "Host": "search.dangdang.com", "Referer": "https://search.dangdang.com/?key=%B1%E0%B3%CC&act=input&page_index=2", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0", } response = requests.get(url, headers=header) # 设置响应的编码格式 # response.encoding = 'utf-8' # 自动识别编码方式(关键!) response.encoding = response.apparent_encoding # 将响应先保存至本地,然后先测试对本地页面进行数据解析 然后再进行多次爬取 # with open('../data/当当网.html', 'w', encoding='utf-8') as f: # f.write(response.text) htmlTree = BeautifulSoup(response.text, 'html.parser') allulEle = htmlTree.find_all('ul', class_="bigimg") for ul in allulEle: # 根据每一个ul标签中的li 进行指定数据的获取 allw1 = ul.find_all('li', recursive=False) # 获取w1下的p标签 for li_tag in allw1: rowContainer = [] # 提取书名 title_tag = li_tag.find_all('p', class_='name') if title_tag: a_tag = li_tag.find_all('a') if a_tag: title = a_tag[0].get('title') href = a_tag[0].get('href') link = f"https:{href}" rowContainer.append(title) rowContainer.append(link) else: title = "" href = "" else: title = "" href = "" pre_price = li_tag.find_all('span', class_='search_pre_price') for p in pre_price: PrePrice = p.get_text(strip=True) rowContainer.append(PrePrice) # 提取评论数 comment_count = li_tag.find('a', class_='search_comment_num') if comment_count: CommentCount = comment_count.get_text(strip=True) else: CommentCount = '0条评论' rowContainer.append(CommentCount) # 提取作者、出版时间、出版社 author_info = li_tag.find('p', class_='search_book_author') for p in author_info: AuthorInfo = p.get_text(strip=True).replace('\\\\', '').replace('/', '') if not AuthorInfo: AuthorInfo = '' rowContainer.append(AuthorInfo) allContainer.append(rowContainer) for i in allContainer: print(i) # 导入数据库模块 import mysql.connector # 使用当前库中的内置对象来创建数据库连接 mydb = mysql.connector.connect( host='localhost', # 当前mysql运行服务的地址 port=3306, # mysql服务的端口号 user='root', # mysql用户名 password='root', # 密码 database='dangdang' ) # 创建游标对象 mycursor = mydb.cursor() # discount VARCHAR ( 20 ), -- 折扣 # 创建图书信息表 create_table_sql = """ CREATE TABLE IF NOT EXISTS books ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR ( 255 ) NOT NULL, -- 书名 link VARCHAR ( 512 ), -- 链接 now_price VARCHAR ( 20 ), -- 现价 comment_count VARCHAR ( 50 ), -- 评论数 author VARCHAR ( 100 ), -- 作者 publish_date VARCHAR ( 20 ), -- 出版时间 publisher VARCHAR ( 100 ), -- 出版社 action VARCHAR ( 100 ), unidentified VARCHAR ( 20 ) ) """ # 执行建表语句 mycursor.execute(create_table_sql) # 插入语句 insert_sql = """ INSERT INTO books (title, link, now_price, comment_count, author, publish_date, publisher, action, unidentified) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """ for book in allContainer: if len(book) == 8: book.insert(4, '') mycursor.execute(insert_sql, list(book)) # 提交事务 mydb.commit() print("✅ 数据插入完成,共插入", len(allContainer), "条记录") # 关闭连接 mycursor.close() mydb.close() import pandas as pd # 转成 DataFrame df = pd.DataFrame(allContainer, columns=["书名", "链接", "现价", "评论数", "作者", "出版时间", "出版社", "可选状态", "未知"]) # 插入序号列(从 1 开始) df.insert(0, '序号', range(1, len(df) + 1)) # 保存为 Excel 文件 df.to_excel("../data/当当网.xlsx", index=False) print("✅ 数据已成功保存为 Excel 文件!") import jieba import wordcloud # 读取excel 文件到当前代码中 import openpyxl from wordcloud import WordCloud # 获取当前excel 表格对象 wb = openpyxl.load_workbook('../data/当当网.xlsx') # 获取当前表格中的sheet sheet = wb.worksheets[0] # 遍历当前的execl 对象 # min row = 2 代表的是从当前表格中的第二行开始获取 # min col = 3 代表获取第三列 # max col = 3 最大的列是三,之确保我们获取当前第三列 # 定义一个列表用来存储当前获取的所有的数据 data = [] for row in sheet.iter_rows(min_row=2, min_col=2, max_col=5): data.append(row[0].value) # 获取每个单元格中的value值 # print(data) # 对当前的数组中的元素进行分割词组 seg_list = jieba.cut(''.join(data), cut_all=False) # print(type(seg_list)) # # print('/'.join(seg_list)) # 引入当前的字体 作为词云图的渲染字体 fonts = '../data/AlibabaPuHuiTi-2-65-Medium.ttf' wc = WordCloud( # 通过属性对当前的词云图进行赋值 width=1200, # 宽600px height=600, background_color='white', max_font_size=50, min_font_size=10, font_path=fonts ) # 将分隔完成的数据 加载到当前的wc对象中 wc.generate_from_text(''.join(seg_list)) # 保存当前的结果到指定文件夹中 wc.to_file("../data/词云图.png") import numpy as np import pandas as pd # 这些设置有助于调试时查看完整的 DataFrame 数据,适合开发阶段使用 pd.set_option('display.max_columns', None) pd.set_option('display.max_rows', None) pd.set_option('display.width', None) pd.set_option('display.max_colwidth', None) newXml = pd.read_excel("../data/当当网.xlsx") print(newXml.shape) # 查看行数列数 print(newXml.info()) # 查看各列的数据类型及缺失值 # # 检查重复行 duplicates = newXml.duplicated(keep='first') print("重复行数量:", duplicates.sum()) # # 删除重复行 cleaned_data = newXml.drop_duplicates() print("删除重复后数据形状:", cleaned_data.shape) # # 删除含有空值的行 dropna_data = newXml.dropna() print("删除空值后数据形状:", dropna_data.shape) # 或者填充空值 # filled_data = newXml.fillna({"未知": "默认值"}) df = newXml.drop(columns=['未知']) print(df) # filled_data = newXml.fillna({"CPU信息": "未知", "等级": 0}) df.to_excel("../data/new当当.xlsx", index=False) import pandas as pd import numpy as np import pandas as pd from pyecharts import options as opts from pyecharts.charts import Bar, Line, Scatter, Pie, Radar from pyecharts.globals import ThemeType # 读取文件 excel_file = pd.ExcelFile('../data/new当当.xlsx') # 获取指定工作表中的数据 df = excel_file.parse('Sheet1') # 将出版社列转换为字符串类型 df['出版社'] = df['出版社'].astype(str) # 获取出版社和书名列的数据 publishers = df['出版社'].to_numpy() book_names = df['书名'].to_numpy() # 获取唯一的出版社 unique_publishers = np.unique(publishers) # 统计每个出版社的书籍数量 book_counts = np.array([np.sum(publishers == publisher) for publisher in unique_publishers]) # 构建结果 DataFrame result_df = pd.DataFrame({ '出版社': unique_publishers, '书籍数量': book_counts }) print(result_df) # 读取数据 df = pd.read_excel('../data/new当当.xlsx') # 数据预处理 # 转换现价列,提取数字 df['现价'] = df['现价'].str.extract('(\d+\.?\d*)').astype(float) # 转换评论数列,提取数字 df['评论数'] = df['评论数'].str.extract('(\d+)').astype(int) # 转换出版时间列,提取年份 df['出版年份'] = pd.to_datetime(df['出版时间']).dt.year # 图表1:价格分布直方图 hist, bins = pd.cut(df['现价'], bins=20, retbins=True) hist_value = hist.value_counts().sort_index() # 使用 Bar 来模拟直方图 histogram = ( Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="800px", height="400px")) .add_xaxis([f"{bins[i]:.2f}-{bins[i + 1]:.2f}" for i in range(len(bins) - 1)]) .add_yaxis("书籍数量", hist_value.tolist(), category_gap=0) .set_global_opts( title_opts=opts.TitleOpts(title="价格分布柱状图"), xaxis_opts=opts.AxisOpts(name="价格区间"), yaxis_opts=opts.AxisOpts(name="数量"), ) ) # 图表2:不同出版社出版书籍数量柱状图 publisher_counts = df['出版社'].value_counts() bar_publisher = ( Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="800px", height="400px")) .add_xaxis(publisher_counts.index.tolist()) .add_yaxis("出版书籍数量", publisher_counts.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="不同出版社出版书籍数量柱状图"), xaxis_opts=opts.AxisOpts(name="出版社", axislabel_opts={"rotate": 90}), yaxis_opts=opts.AxisOpts(name="出版书籍数量"), ) ) # 图表3:每年出版书籍数量折线图 yearly_counts = df['出版年份'].value_counts().sort_index() line_yearly = ( Line(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="800px", height="400px")) .add_xaxis(yearly_counts.index.astype(str).tolist()) .add_yaxis("出版书籍数量", yearly_counts.tolist(), is_smooth=True, symbol="circle") .set_global_opts( title_opts=opts.TitleOpts(title="每年出版书籍数量折线图"), xaxis_opts=opts.AxisOpts(name="出版年份"), yaxis_opts=opts.AxisOpts(name="出版书籍数量"), ) ) # 图表4:评论数前五书籍的书名与评论数柱状图 top_5_commented = df.nlargest(5, '评论数') bar_comment = ( Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT, width="800px", height="400px")) .add_xaxis(top_5_commented['书名'].tolist()) .add_yaxis("评论数", top_5_commented['评论数'].tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="评论数前五书籍的书名与评论数柱状图"), xaxis_opts=opts.AxisOpts(name="书名", axislabel_opts={"rotate": 90}), yaxis_opts=opts.AxisOpts(name="评论数"), ) ) # 图表5:价格与评论数的散点图 # 将现价列转换为字符串类型 df['现价'] = df['现价'].astype(str) # 提取价格数值 df['价格'] = df['现价'].str.extract(r'(\d+\.?\d*)').astype(float) # 检查价格列是否存在缺失值 print(f"价格列缺失值数量: {df['价格'].isna().sum()}") # 删除价格列为缺失值的行 df = df.dropna(subset=['价格']) # 定义价格区间 bins = [0, 50, 100, 150, 200, float('inf')] labels = ['0 - 50', '51 - 100', '101 - 150', '151 - 200', '200以上'] # 划分价格区间并统计数量 df['价格区间'] = pd.cut(df['价格'], bins=bins, labels=labels) price_range_counts = df['价格区间'].value_counts().reset_index(name='数量') # 使用 pyecharts 绘制饼状图 pie = ( Pie() .add( series_name="数量", data_pair=[list(z) for z in zip(price_range_counts['价格区间'], price_range_counts['数量'])], radius=["40%", "75%"], ) .set_global_opts( title_opts=opts.TitleOpts(title="价格区间与数量的饼状图"), legend_opts=opts.LegendOpts(orient="vertical", pos_top="15%", pos_left="2%"), ) .set_series_opts( label_opts=opts.LabelOpts(formatter="{b}: {d}%") ) ) # 将评论数列转换为字符串类型 df['评论数'] = df['评论数'].astype(str) # 提取评论数数值 df['评论数数值'] = df['评论数'].str.extract(r'(\d+\.?\d*)').astype(float) # 找出评论数前五的书籍 top_5_books = df.nlargest(5, '评论数数值', keep='all')[['书名', '评论数数值']] # 定义雷达图的指标 c_schema = [{"name": book_name, "max": top_5_books['评论数数值'].max()} for book_name in top_5_books['书名']] # 准备雷达图的数据 data = [[count for count in top_5_books['评论数数值'].values]] # 创建雷达图对象 ( Radar() .add_schema(schema=c_schema) .add( series_name="评论数", data=data, areastyle_opts=opts.AreaStyleOpts(opacity=0.2) ) .set_global_opts( title_opts=opts.TitleOpts(title="评论数前五的书籍的书名与评论数雷达图"), ) .render("../data/radar_chart_top5_books.html") ) # 统计不同出版社的书籍数量 publisher_book_count = df['出版社'].value_counts().reset_index() publisher_book_count.columns = ['出版社', '书籍数量'] # 选取书籍数量前 10 的出版社 top_10_publisher = publisher_book_count.nlargest(10, '书籍数量') # 创建散点图对象 scatter = ( Scatter() .add_xaxis(top_10_publisher['出版社'].tolist()) .add_yaxis( series_name="书籍数量", y_axis=top_10_publisher['书籍数量'].tolist(), symbol_size=10, label_opts=opts.LabelOpts(is_show=False) ) .set_global_opts( title_opts=opts.TitleOpts(title="不同出版社书籍数量前10的散点图"), xaxis_opts=opts.AxisOpts( name="出版社", type_="category", axislabel_opts=opts.LabelOpts(rotate=45, interval="auto") ), yaxis_opts=opts.AxisOpts(name="书籍数量"), ) ) # 保存图表 histogram.render("../data/price_distribution_histogram.html") bar_publisher.render("../data/publisher_book_count_bar.html") line_yearly.render("../data/yearly_book_count_line.html") bar_comment.render("../data/top_commented_books_bar.html") pie.render("../data/price_range_pie_chart.html") scatter.render("../data/scatter_top10_publisher_book_count.html") from flask import Flask, request, render_template_string, jsonify import requests # import requests # # # 定义一个message的变量,作为会话的容器 # messages = [{"role":"system","content":""}] # # # API KEY # API_KEY = "sk-ec2e933afb424766ba6bce9765960a3a" # # 设置请求头 # header = { # "Content-Type": "application/json", # 告知服务器我们传递的内容的数据类型 # "Authorization": f"Bearer {API_KEY}" # api_key # } # # # 请求地址 # url = "https://api.deepseek.com/chat/completions" # # # 因为要完成多轮对话 所以要有循环 # # 同时因为要完成用户的多次对话请求 # # def DeepSeekChat(userMessage): # # 1. 将用户输入的信息与角色进行拼接 从而变成一个完成的对话 # messages.append( {"role": "user", "content": userMessage}) # # # 2. 请求deepseek 请求过程中将我们携带的多个参数进行传递 # data = { # "model":"deepseek-chat", # "messages":messages, # "stream":False # } # # # 3. 发起请求 # response = requests.post(url, json=data, headers=header) # # # 4. 对response进行处理 # if response.status_code == 200: # # 获取响应内容 # result_json = response.json() # # 处理当前json中的内容 # airesult = result_json['choices'][0]['message']['content'] # # AI返回结果 # print(f"图书商城AI客服:{airesult}") # # 如何实现多轮对话 # # 将回复的内容继续追加到列表中,形成会话的闭合,结合上下文内容 # messages.append({"role": "assistant", "content": airesult}) # else: # print(response.text) # print("请求失败") # # print("图书商城欢迎您") # print("使用exit退出程序") # # while True: # userinput = input("你:") # if userinput == "exit": # print("退出当前程序") # break # else: # # 调用函数完成多次请求的发送 # # 将用户输入的内容作为参数调用函数 完成API的调用 # DeepSeekChat(userinput) 这是我写的代码,需要把爬虫得到的数据与deepseek相结合,使得可以在一个新页面上根据数据与ai对话,请你进行修改

<think>我们正在将爬虫数据与DeepSeekAPI集成到Flask应用中,实现基于数据的AI对话功能。根据引用[2]和引用[3],我们可以采用RAG(检索增强生成)技术。以下是具体步骤:###1.**数据准备**-爬取的数据需要清洗并存储为结构化的形式(如JSON或存入数据库)。-使用向量数据库(如FAISS、Chroma)存储文本的嵌入向量,以便快速检索。###2.**搭建Flask后端**-创建Flask应用,设置路由来处理用户请求。-设计两个主要端点:-一个用于接收用户问题,触发检索和生成回答。-一个用于展示对话历史或前端界面。###3.**RAG流程实现**-**检索**:当用户提问时,将问题转换为向量,从向量数据库中检索最相关的数据片段(如top_k=3)。-**生成**:将检索到的数据片段与用户问题一起构造成Prompt,发送给DeepSeekAPI生成回答。###4.**集成DeepSeekAPI**-在Flask中使用`requests`库调用DeepSeekAPI。-注意设置API密钥(从环境变量中获取)和请求头。###5.**前端展示**-使用HTML/JavaScript创建一个简单的前端页面,通过AJAX与后端交互,实现实时对话。###代码结构示例####文件结构```app/├──main.py#Flask主程序├──config.py#配置文件(API密钥等)├──rag_utils.py#RAG工具函数(检索、生成等)├──templates/│└──index.html#前端页面├──data/#存储爬虫数据└──vector_db/#向量数据库```####main.py示例```pythonfromflaskimportFlask,request,jsonify,render_templatefromrag_utilsimportget_retrieval_augmented_responseimportosapp=Flask(__name__)#配置DeepSeekAPI密钥(建议使用环境变量)DEEPSEEK_API_KEY=os.getenv('DEEPSEEK_API_KEY')ifnotDEEPSEEK_API_KEY:raiseValueError("DeepSeekAPIkeynotfoundinenvironmentvariables.")@app.route('/')defindex():returnrender_template('index.html')@app.route('/ask',methods=['POST'])defask():user_input=request.json.get('question')ifnotuser_input:returnjsonify({"error":"Noquestionprovided"}),400#使用RAG技术获取回答answer=get_retrieval_augmented_response(user_input)returnjsonify({"answer":answer})if__name__=='__main__':app.run(debug=True,port=7000)```####rag_utils.py关键函数```pythonimportrequestsfromyour_vector_db_moduleimportretrieve_top_k#假设你已经实现了向量检索defconstruct_prompt(query:str,context_items:list[str])->str:"""构造RAG提示"""context="\n".join([f"[参考数据{i+1}]:{item}"fori,iteminenumerate(context_items)])prompt=f"""根据以下参考信息回答问题:{context}问题:{query}"""returnpromptdefget_retrieval_augmented_response(query:str,top_k:int=3)->str:#1.检索相关上下文context_items=retrieve_top_k(query,top_k=top_k)#从向量数据库检索#2.构造Promptprompt=construct_prompt(query,context_items)#3.调用DeepSeekAPIresponse=requests.post("https://api.deepseek.com/chat",json={"messages":[{"role":"user","content":prompt}],"model":"deepseek-chat",},headers={"Authorization":f"Bearer{DEEPSEEK_API_KEY}"})ifresponse.status_code!=200:return"抱歉,我暂时无法回答这个问题。"data=response.json()returndata['choices'][0]['message']['content']```####向量数据库检索示例(使用sentence-transformers和FAISS)```python#向量数据库建立(单独脚本)fromsentence_transformersimportSentenceTransformerimportpandasaspdimportfaissimportnumpyasnpmodel=SentenceTransformer('BAAI/bge-base-zh-v1.5')#假设已加载爬虫数据到DataFramedf(columns:'id','content')df=pd.read_json('data/crawled_data.json')texts=df['content'].tolist()#生成向量embeddings=model.encode(texts,normalize_embeddings=True)dim=embeddings.shape[1]#创建FAISS索引index=faiss.IndexFlatIP(dim)index.add(embeddings.astype(np.float32))#保存索引faiss.write_index(index,'vector_db/crawled_data.index')``````python#在rag_utils.py中实现检索importfaissimportnumpyasnpfromsentence_transformersimportSentenceTransformermodel=SentenceTransformer('BAAI/bge-base-zh-v1.5')#加载索引index=faiss.read_index('vector_db/crawled_data.index')defretrieve_top_k(query:str,top_k=3):query_embedding=model.encode([query],normalize_embeddings=True).astype(np.float32)D,I=index.search(query_embedding,top_k)#I是索引列表,从df中获取对应文本#假设我们有全局变量df(实际应用中需优化为数据库或缓存)return[df.iloc[i]['content']foriinI[0]ifi!=-1]```###前端HTML示例(templates/index.html)```html<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>DeepSeek数据助手</title></head><body><divid="chat"><divid="history"></div><inputtype="text"id="question"placeholder="输入关于数据的问题..."><buttononclick="sendQuestion()">发送</button></div><script>functionsendQuestion(){constquestion=document.getElementById('question').value;if(!question)return;fetch('/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question})}).then(resp=>resp.json()).then(data=>{consthistoryDiv=document.getElementById('history');historyDiv.innerHTML+=`<p><b>你:</b>${question}</p>`;historyDiv.innerHTML+=`<p><b>助手:</b>${data.answer}</p>`;}).catch(console.error);}</script></body></html>```###环境变量与配置创建`.env`文件(注意不要提交到版本控制):```DEEPSEEK_API_KEY=your_api_key_here```###部署运行1.安装依赖:```bashpipinstallflaskrequestssentence-transformersfaiss-cpupandas```2.运行:```bashpythonmain.py```访问:http://localhost:7000###注意事项-此实现假设爬虫数据已经存储在本地文件并构建了向量索引。实际应用中,可能需要定期更新索引。-检索时可以根据相关性分数(距离)过滤低质量结果(引用[3]中设置`MIN_RELEVANCE_SCORE=0.3`)。-大量数据时考虑使用数据库(如SQLite)存储原始文本,向量索引存储嵌入向量和ID映射。通过上述步骤,你可以在Flask应用中构建一个基于爬虫数据和DeepSeek的智能对话系统。该方法利用了检索增强技术,使模型能够根据你的特定数据回答问题,提高了准确性和相关性。</think> 基于您的需求,我将分步解释如何将爬虫数据与DeepSeek API集成,并在Flask应用中实现基于数据的AI对话功能。解决方案主要参考引用[2]和[3]中的RAG技术实现。 ### 技术方案概览 整个系统使用**Flask后端 + DeepSeek API + RAG架构**实现以下流程: ``` 用户问题 → Flask接收 → 向量数据库检索相关数据 → 构建DeepSeek API请求 → 返回AI回答 ``` --- ### 具体实现步骤 #### 1. 环境配置与依赖安装 ```bash # 安装所需Python包 pip install flask requests sentence-transformers python-dotenv ``` #### 2. 项目结构 ``` your_project/ ├── app.py # Flask主应用 ├── .env # 环境变量(API密钥等) ├── vector_db.py # 向量数据库管理 ├── crawler_data/ # 存储爬取的数据 │ └── data.json # 清洗后的结构化数据 └── templates/ └── chat.html # 聊天界面 ``` #### 3. 实现核心功能 **a. 环境变量配置 (.env)** ```ini # .env文件 DEEPSEEK_API_KEY="your_api_key_here" DEEPSEEK_MODEL_NAME="deepseek-chat" MIN_RELEVANCE_SCORE=0.3 # 相关性阈值(引用[3]) ``` **b. 向量数据库管理 (vector_db.py)** ```python import json import numpy as np from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity class VectorDB: def __init__(self): self.model = SentenceTransformer('BAAI/bge-base-zh-v1.5') # 中文优化的嵌入模型 self.data = [] self.embeddings = [] def load_data(self, file_path): """加载爬虫数据并生成嵌入向量""" with open(file_path, 'r') as f: self.data = json.load(f) texts = [item['content'] for item in self.data] self.embeddings = self.model.encode(texts) def search(self, query, top_k=3): """检索最相关的数据片段""" query_embedding = self.model.encode([query]) similarities = cosine_similarity(query_embedding, self.embeddings)[0] sorted_indices = np.argsort(similarities)[::-1] # 按相似度降序排序 results = [] for idx in sorted_indices[:top_k]: if similarities[idx] > MIN_RELEVANCE_SCORE: # 引用[3]中的相关性过滤 results.append(self.data[idx]) return results ``` **c. Flask应用集成 (app.py)** ```python from flask import Flask, request, render_template, jsonify from vector_db import VectorDB from dotenv import load_dotenv import requests import os # 加载环境变量 load_dotenv() app = Flask(__name__) # 初始化向量数据库 vector_db = VectorDB() vector_db.load_data('crawler_data/data.json') @app.route('/') def chat_page(): """渲染聊天界面""" return render_template('chat.html') @app.route('/ask', methods=['POST']) def ask_question(): """处理用户问题""" question = request.json.get('question') if not question: return jsonify({"error": "No question provided"}), 400 # RAG检索相关数据 context_items = vector_db.search(question, top_k=3) # 构建DeepSeek API请求 messages = [ {"role": "system", "content": "你是一个专业的数据助手,基于以下信息回答问题"}, {"role": "user", "content": build_prompt(question, context_items)} ] # 调用DeepSeek API response = requests.post( "https://api.deepseek.com/chat", json={ "messages": messages, "model": os.getenv("DEEPSEEK_MODEL_NAME") }, headers={"Authorization": f"Bearer {os.getenv('DEEPSEEK_API_KEY')}"} ) if response.status_code != 200: return jsonify({"error": "API request failed"}), 500 return jsonify(response.json()['choices'][0]['message']) def build_prompt(question, context_items): """构建RAG提示""" context_str = "\n".join( f"[相关数据 {i+1}]: {item['content']}\n来源: {item['source']}" for i, item in enumerate(context_items) ) return f""" 用户问题: {question} 参考信息: {context_str} 请基于以上信息回答问题,如果信息不足请说明 """ if __name__ == '__main__': app.run(port=7000, debug=True) ``` **d. 聊天界面模板 (templates/chat.html)** ```html <!DOCTYPE html> <html> <head> <title>基于数据的DeepSeek对话</title> <style> #chat-container { max-width: 800px; margin: 0 auto; } #chat-history { height: 400px; overflow-y: scroll; border: 1px solid #ddd; padding: 10px; } .user-message { text-align: right; color: blue; } .ai-message { text-align: left; color: green; } </style> </head> <body> <div id="chat-container"> <h1>基于爬虫数据的AI问答系统</h1> <div id="chat-history"></div> <div> <input type="text" id="user-input" placeholder="输入你的问题..."> <button onclick="sendQuestion()">发送</button> </div> </div> <script> function sendQuestion() { const input = document.getElementById('user-input'); const question = input.value.trim(); if (!question) return; // 显示用户消息 appendMessage('user-message', question); input.value = ''; // 发送请求到后端 fetch('/ask', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({question}) }) .then(response => response.json()) .then(data => { if (data.error) { appendMessage('ai-message', '错误: ' + data.error); } else { appendMessage('ai-message', data.content); } }); } function appendMessage(className, content) { const history = document.getElementById('chat-history'); const messageDiv = document.createElement('div'); messageDiv.className = className; messageDiv.innerHTML = content; history.appendChild(messageDiv); history.scrollTop = history.scrollHeight; } </script> </body> </html> ``` #### 4. 数据格式要求 (crawler_data/data.json) 爬取的数据应结构化为: ```json [ { "source": "https://example.com/article1", "content": "这是第一条爬取的内容摘要..." }, { "source": "https://example.com/article2", "content": "这是第二条爬取的内容摘要..." } ] ``` --- ### 系统工作流程 1. 用户在前端输入问题 2. Flask后端接收问题: - 使用向量数据库检索最相关的数据片段 - 将问题+相关数据构建为RAG提示 - 调用DeepSeek API获取回答 3. 返回并显示AI生成的答案 此方案的核心优势: 1. 通过RAG技术结合爬虫数据与DeepSeek模型(引用[2]) 2. 使用相关度过滤提升答案质量(引用[3]) 3. 完整的Web应用方案,可直接部署 --- ### 注意事项 1. **API成本管理**: DeepSeek API按token计费,注意控制输入长度 2. **数据更新**: 定期重新构建向量数据库以包含新爬取数据 3. **安全性**: 在正式部署时: - 将`.env`加入`.gitignore` - 启用Flask CSRF保护 - 对用户输入做适当的清理过滤 4. **性能优化**: - 对于大型数据集,使用FAISS/Pinecone等专用向量数据库 - 实施请求缓存减少API调用 > 此架构在引用[2]和[3]基础上实现了完整的工作流,可直接用于基于爬虫数据的智能对话场景。 ---
阅读全文

相关推荐

package org.example.bim_design_service.controller; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.example.bim_design_service.dto.*; import org.example.bim_design_service.service.VersionControlService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; // VersionController.java @RestController @RequestMapping("/api/version") @RequiredArgsConstructor public class VersionController { private final VersionControlService versionControlService; /** * 检查版本更新 */ @PostMapping("/check") public ResponseEntity<VersionCheckResponse> checkVersion( @Valid @RequestBody VersionCheckRequest request) { System.out.println(); VersionCheckResponse response = versionControlService.checkVersion(request); return ResponseEntity.ok(response); } /** * 检查模块更新 */ @PostMapping("/check-modules") public ResponseEntity<ModuleUpdateListResponse> checkModuleUpdates( @Valid @RequestBody ModuleUpdateCheckRequest request) { ModuleUpdateListResponse response = versionControlService.checkModuleUpdates(request); return ResponseEntity.ok(response); } /** * 下载模块更新包 */ @PostMapping("/download") public ResponseEntity<byte[]> downloadUpdates( @RequestParam String versionCode, @RequestBody List<String> moduleNames) throws IOException { byte[] zipBytes = versionControlService.packageModulesForDownload(moduleNames, versionCode); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"update-" + versionCode + ".zip\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(zipBytes); } }package org.example.bim_design_service.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.time.LocalDateTime; // ClientModuleInfo.java @Data public class ClientModuleInfo { @NotBlank(message = "模块名称不能为空") private String moduleName; @NotNull(message = "创建时间不能为空") private LocalDateTime createdAt; public ClientModuleInfo() {} } package org.example.bim_design_service.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import java.util.List; // ModuleUpdateCheckRequest.java @Data public class ModuleUpdateCheckRequest { @NotBlank(message = "版本号不能为空") private String versionCode; @NotEmpty(message = "模块列表不能为空") private List<ClientModuleInfo> modules; public ModuleUpdateCheckRequest() {} }package org.example.bim_design_service.dto; import lombok.Data; import java.util.List; // ModuleUpdateListResponse.java @Data public class ModuleUpdateListResponse { private String versionCode; private List<ModuleUpdateResponse> updates; } package org.example.bim_design_service.dto; import lombok.Data; import java.time.LocalDateTime; // ModuleUpdateResponse.java @Data public class ModuleUpdateResponse { private String moduleName; private String fileUrl; private String fileHash; private Long fileSize; private LocalDateTime updatedAt; }package org.example.bim_design_service.dto; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; import lombok.Data; // VersionCheckRequest.java @Data public class VersionCheckRequest { @JsonProperty("version_code") @NotBlank(message = "版本号不能为空") private String versionCode; public VersionCheckRequest() {} } package org.example.bim_design_service.dto; import lombok.Data; // VersionCheckResponse.java @Data public class VersionCheckResponse { private String versionCode; private boolean needUpdate; } package org.example.bim_design_service.entity; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime; @Entity @Table(name = "module") @Data @Getter @Setter public class FileModule { @Id private String id; @Column(name = "module_name", nullable = false, unique = true) private String moduleName; private String description; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; } package org.example.bim_design_service.entity; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime; @Entity @Table(name = "version") @Data @Getter @Setter public class Version { @Id private String id; @Column(name = "version_code", nullable = false, unique = true) private String versionCode; @Column(name = "release_time", nullable = false) private LocalDateTime releaseTime; private String description; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; public Version(){ } private Version(String id, String versionCode, LocalDateTime releaseTime, String description, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.versionCode = versionCode; } } package org.example.bim_design_service.entity; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime; @Entity @Table(name = "version_module") @Data @Getter @Setter public class VersionModule { @Id private String id; @ManyToOne @JoinColumn(name = "version_id", nullable = false) private Version version; @ManyToOne @JoinColumn(name = "module_id", nullable = false) private FileModule module; @Column(name = "file_path", nullable = false) private String filePath; @Column(name = "file_hash", nullable = false) private String fileHash; @Column(name = "file_size", nullable = false) private Long fileSize; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; }package org.example.bim_design_service.repository; import org.example.bim_design_service.entity.FileModule; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; // ModuleRepository.java public interface ModuleRepository extends JpaRepository<FileModule, String> { Optional<FileModule> findByModuleName(String moduleName); }package org.example.bim_design_service.repository; import org.example.bim_design_service.entity.VersionModule; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; // VersionModuleRepository.java public interface VersionModuleRepository extends JpaRepository<VersionModule, String> { List<VersionModule> findByVersionId(String versionId); Optional<VersionModule> findByVersionIdAndModuleId(String versionId, String moduleId); }package org.example.bim_design_service.repository; import org.example.bim_design_service.entity.Version; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; // VersionRepository.java public interface VersionRepository extends JpaRepository<Version, String> { Optional<Version> findTopByOrderByReleaseTimeDesc(); Optional<Version> findByVersionCode(String versionCode); }package org.example.bim_design_service.service; import lombok.RequiredArgsConstructor; import org.example.bim_design_service.dto.*; import org.example.bim_design_service.entity.FileModule; import org.example.bim_design_service.entity.Version; import org.example.bim_design_service.entity.VersionModule; import org.example.bim_design_service.repository.ModuleRepository; import org.example.bim_design_service.repository.VersionModuleRepository; import org.example.bim_design_service.repository.VersionRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; // VersionControlService.java @Service @RequiredArgsConstructor public class VersionControlService { private final VersionRepository versionRepository; private final ModuleRepository moduleRepository; private final VersionModuleRepository versionModuleRepository; @Value("${file.storage.base-url}") private String fileBaseUrl; /** * 检查版本是否需要更新 */ public VersionCheckResponse checkVersion(VersionCheckRequest request) { System.out.println(request.getVersionCode()); // 获取最新版本 Optional<Version> latestVersionOpt = versionRepository.findTopByOrderByReleaseTimeDesc(); if (latestVersionOpt.isEmpty()) { System.out.println(("No version found")); throw new RuntimeException("服务端没有可用的版本"); } Version latestVersion = latestVersionOpt.get(); VersionCheckResponse response = new VersionCheckResponse(); // 比较版本 if (latestVersion.getVersionCode().equals(request.getVersionCode())) { response.setVersionCode(request.getVersionCode()); response.setNeedUpdate(false); } else { response.setVersionCode(latestVersion.getVersionCode()); response.setNeedUpdate(true); } return response; } /** * 检查模块更新 */ public ModuleUpdateListResponse checkModuleUpdates(ModuleUpdateCheckRequest request) { // 验证请求版本是否存在 Version version = versionRepository.findByVersionCode(request.getVersionCode()) .orElseThrow(() -> new RuntimeException("版本不存在")); // 获取该版本的所有模块 List<VersionModule> versionModules = versionModuleRepository.findByVersionId(version.getId()); List<ModuleUpdateResponse> updates = new ArrayList<>(); for (ClientModuleInfo clientModule : request.getModules()) { // 查找模块 Optional<FileModule> moduleOpt = moduleRepository.findByModuleName(clientModule.getModuleName()); if (moduleOpt.isEmpty()) { continue; // 跳过不存在的模块 } FileModule module = moduleOpt.get(); // 查找版本模块关联 Optional<VersionModule> versionModuleOpt = versionModuleRepository .findByVersionIdAndModuleId(version.getId(), module.getId()); if (versionModuleOpt.isPresent()) { VersionModule versionModule = versionModuleOpt.get(); // 检查是否需要更新(客户端模块时间早于服务端) if (clientModule.getCreatedAt().isBefore(versionModule.getCreatedAt())) { ModuleUpdateResponse update = new ModuleUpdateResponse(); update.setModuleName(module.getModuleName()); update.setFileUrl(fileBaseUrl + versionModule.getFilePath()); update.setFileHash(versionModule.getFileHash()); update.setFileSize(versionModule.getFileSize()); update.setUpdatedAt(versionModule.getUpdatedAt()); updates.add(update); } } } ModuleUpdateListResponse response = new ModuleUpdateListResponse(); response.setVersionCode(version.getVersionCode()); response.setUpdates(updates); return response; } /** * 打包模块更新文件为ZIP */ public byte[] packageModulesForDownload(List<String> moduleNames, String versionCode) throws IOException { // 验证版本 Version version = versionRepository.findByVersionCode(versionCode) .orElseThrow(() -> new RuntimeException("版本不存在")); // 创建临时ZIP文件 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { for (String moduleName : moduleNames) { // 查找模块 FileModule module = moduleRepository.findByModuleName(moduleName) .orElseThrow(() -> new RuntimeException("模块不存在: " + moduleName)); // 查找版本模块关联 VersionModule versionModule = versionModuleRepository .findByVersionIdAndModuleId(version.getId(), module.getId()) .orElseThrow(() -> new RuntimeException("模块版本关联不存在")); // 添加文件到ZIP Path filePath = Paths.get(versionModule.getFilePath()); if (Files.exists(filePath)) { ZipEntry entry = new ZipEntry(filePath.getFileName().toString()); zos.putNextEntry(entry); Files.copy(filePath, zos); zos.closeEntry(); } else { throw new RuntimeException("文件不存在: " + filePath); } } } return baos.toByteArray(); } }

<template> <el-row :gutter="20"> <el-col :span="6" :xs="24"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" @input="filterProjectTree" /> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >{{ isExpandAll ? '折叠' : '展开' }}</el-button> </el-col> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-folder-add" size="mini" @click="handleAddProject" v-hasPermi="['cms:project:add']" >新增项目</el-button> </el-col> </el-row> <el-tree v-if="refreshTable" style="margin-top: 0.8rem;" :data="filteredProjectTreeData" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" :default-expand-all="isExpandAll" ref="projectTree" empty-text="加载中,请稍候" node-key="id" @node-click="handleProjectClick" > <el-icon class="tree-icon" :size="16"> <svg-icon v-if="node.expanded" icon-class="folder-open" /> <svg-icon v-else icon-class="folder" /> </el-icon> 25" class="node-label"> <el-tooltip :show-after="300" :content="node.label" placement="top-start"> {{ ellipsis(node.label, 25) }} </el-tooltip> {{ node.label }} <el-button type="text" size="mini" icon="el-icon-edit" v-hasPermi="['cms:project:update']" @click.stop="() => handleEditProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-plus" v-hasPermi="['cms:project:add']" @click.stop="() => handleAddSubProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-delete" v-hasPermi="['cms:project:delete']" @click.stop="() => handleDeleteProject(data)" ></el-button> </el-tree> </el-col> <el-col :span="18" :xs="24"> <el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px"> <el-form-item label="文档标题" prop="title"> <el-input v-model="docQueryParams.title" placeholder="请输入文档标题" clearable size="mini" @keyup.enter.native="getDocumentList" prefix-icon="el-icon-search" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable> <el-option label="编制中✍" value="0" /> <el-option label="待评审✊" value="1" /> <el-option label="已评审👍" value="2" /> <el-option label="修改中🔧" value="3" /> <el-option label="开发中💪" value="4" /> <el-option label="已完成开发🆗" value="5" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button> </el-form-item> </el-form> <el-button type="success" icon="el-icon-download" size="mini" :disabled="selectedDocs.length === 0" @click="handleBatchExport" > 批量导出 ({{ selectedDocs.length }}) </el-button> <el-table v-loading="docLoading" :data="documentList" highlight-current-row @row-click="handleRowClick" @selection-change="handleSelectionChange" ref="docTable" > <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="50" align="center" /> <el-table-column label="编号" align="center" prop="serialNum" /> <el-table-column label="标题" align="center" prop="title" class-name="small-padding fixed-width" width="200" :show-overflow-tooltip="true" sortable > <template slot-scope="scope"> {{scope.row.title}}(v{{scope.row.articleVersion}}版本) <el-button v-show="!scope.row.title.startsWith('[诊断项]')" size="normal" type="text" icon="el-icon-tickets" @click="getArticleInfo(scope.row.id)">{{scope.row.title}}(v{{scope.row.articleVersion}}版本)</el-button> </template> </el-table-column> <el-table-column label="进度" prop="status" width="100"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === '0'" type="info" size="small">编制中✍</el-tag> <el-tag v-if="scope.row.status === '1'" type="warning" size="small">待评审✊</el-tag> <el-tag v-if="scope.row.status === '2'" type="success" size="small">已评审👍</el-tag> <el-tag v-if="scope.row.status === '3'" type="success" size="small">修改中🔧</el-tag> <el-tag v-if="scope.row.status === '4'" type="success" size="small">开发中💪</el-tag> <el-tag v-if="scope.row.status === '5'" type="success" size="small">已完成开发🆗</el-tag> </template> </el-table-column> <el-table-column label="创建人" prop="createBy" width="100" /> <el-table-column label="负责人" prop="director" width="100"/> <el-table-column label="创建时间" prop="createTime" width="140" /> <el-table-column label="操作" width="180" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button> </template> </el-table-column> </el-table> 0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> <el-icon class="title-icon"><svg-icon icon-class="document" /></el-icon> {{ currentDocument.title }} <el-form :model="currentDocument" ref="docForm" label-width="80px"> <el-form-item label="文档内容"> <Tinymce :height='600' v-model='currentDocument.content'></Tinymce> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-check" @click="saveDocument">保存</el-button> <el-button icon="el-icon-close" @click="backToList">取消</el-button> </el-form-item> </el-form> </el-col> </el-row> <el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%"> <el-form :model="projectForm" ref="projectForm" label-width="100px"> <el-form-item label="项目名称" prop="name" required> <el-input v-model="projectForm.name" placeholder="请输入项目名称" prefix-icon="el-icon-folder" /> </el-form-item> <el-form-item label="上级项目" prop="parentId"> <treeselect v-model="projectForm.parentId" :options="projectTreeData" :normalizer="normalizer" placeholder="选择上级项目" /> </el-form-item> <el-form-item label="项目描述" prop="description"> <el-input type="textarea" v-model="projectForm.description" :rows="3" /> </el-form-item> </el-form> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" icon="el-icon-check" @click="saveProject">保存</el-button> </el-dialog> </template> <script> import { getData, updateData } from "@/api/cms/data"; import { getProjectTree, getDocuments, saveProject, updateProject, deleteProject, removeMenusFromProject } from "@/api/cms/articleProject"; import Tinymce from '@/components/Tinymce'; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; import axios from 'axios'; export default { name: "ProjectManagement", components: { Treeselect, Tinymce }, data() { return { // 项目树相关数据 projectName: '', projectTreeData: [], filteredProjectTreeData: [], refreshTable: true, isExpandAll: true, defaultProps: { children: "children", label: "label" }, // 项目对话框相关 projectDialogVisible: false, projectDialogTitle: '', projectForm: { id: null, name: '', parentId: null, description: '' }, // 文档列表相关 activeView: 'list', // 'list' 或 'detail' docQueryParams: { serialNums: null, title: '', status: '', pageNum: 1, pageSize: 10 }, documentList: [], docTotal: 0, docLoading: false, selectedDocs: [], // 选中的文档 ids: [], // 选中文档ID集合 // 文档详情相关 currentDocument: { id: null, menuId: null, title: '', content: '', status: '0' }, // 当前选中的项目ID(用于删除操作) currentProjectId: null, // 导出相关数据 form: { ids: [], // 导出的文档ID集合 notesExportFlag: 1 // 默认导出详细 } }; }, created() { this.getProjectTree(); }, methods: { // ================= 项目树方法 ================= ellipsis(text, maxLength) { return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; }, toggleExpandAll() { this.refreshTable = false; this.isExpandAll = !this.isExpandAll; this.$nextTick(() => { this.refreshTable = true; }); }, filterNode(value, data) { if (!value) return true; return data.label.toLowerCase().includes(value.toLowerCase()); }, filterProjectTree() { this.$refs.projectTree.filter(this.projectName); }, handleProjectClick(data) { // 保存当前选中的项目ID this.currentProjectId = data.id; // 将serialNums字符串转换为数组 const serialNumsArray = data.serialNums ? data.serialNums.split(',').map(id => id.trim()) : []; // 将数组转换回逗号分隔的字符串用于查询 this.docQueryParams.serialNums = serialNumsArray.join(','); this.getDocumentList(); }, // ================= 项目管理方法 ================= handleAddProject() { this.projectForm = { id: null, name: '', parentId: null, description: '' }; this.projectDialogTitle = '新增项目'; this.projectDialogVisible = true; }, handleAddSubProject(data) { this.projectForm = { id: null, name: '', parentId: data.id, description: '' }; this.projectDialogTitle = '新增子项目'; this.projectDialogVisible = true; }, handleEditProject(data) { this.projectForm = { id: data.id, name: data.name, parentId: data.parentId, description: data.description || '' }; this.projectDialogTitle = '编辑项目'; this.projectDialogVisible = true; }, // 递归收集项目ID collectProjectIds(node) { let ids = [node.id]; if (node.children && node.children.length > 0) { node.children.forEach(child => { ids = ids.concat(this.collectProjectIds(child)); }); } return ids; }, // 修改后的删除项目方法 handleDeleteProject(data) { this.$confirm(确定删除项目 "${data.name}" 及其所有子项目吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 递归收集所有节点ID const ids = this.collectProjectIds(data); // 调用批量删除API deleteProject(ids).then(response => { if (response.code === 200) { this.$message.success('删除成功'); this.getProjectTree(); } else { this.$message.error(response.msg || '删除失败'); } }).catch(error => { this.$message.error('删除失败: ' + error.message); }); }); }, saveProject() { this.$refs.projectForm.validate(valid => { if (valid) { const saveMethod = this.projectForm.id ? updateProject : saveProject; saveMethod(this.projectForm).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.projectDialogVisible = false; this.getProjectTree(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); } }); }, normalizer(node) { return { id: node.id, label: node.name, children: node.children && node.children.length > 0 ? node.children : undefined }; }, // 获取项目树数据 getProjectTree() { getProjectTree().then(response => { if (response.code === 200 && response.data) { // 处理根节点 const rootNode = response.data; // 转换数据结构 this.projectTreeData = this.transformTreeData([rootNode]); this.filteredProjectTreeData = [...this.projectTreeData]; // 默认展开根节点 this.$nextTick(() => { if (this.projectTreeData.length > 0) { this.$refs.projectTree.setCurrentKey(rootNode.id); this.handleProjectClick(rootNode); } }); } else { this.$message.error('获取项目树失败: ' + (response.msg || '未知错误')); } }).catch(error => { console.error("获取项目树失败:", error); this.$message.error('获取项目树失败: ' + error.message); }); }, // 转换数据结构为el-tree需要的格式 transformTreeData(nodes) { if (!nodes || !Array.isArray(nodes)) return []; return nodes.map(node => ({ id: node.id, label: node.name, name: node.name, parentId: node.parentId, serialNums: node.serialNums, description: node.description, createBy: node.createBy, createTime: node.createTime, updateBy: node.updateBy, updateTime: node.updateTime, children: this.transformTreeData(node.children || []), rawData: node })); }, // ================= 文档管理方法 ================= // 在 methods 中添加递归收集 serialNum 的方法 collectAllSerialNums(node) { let serialNums = []; // 添加当前节点的 serialNum if (node.serialNums) { const ids = node.serialNums.split(',').map(id => id.trim()); serialNums = [...serialNums, ...ids]; } // 递归处理子节点 if (node.children && node.children.length > 0) { node.children.forEach(child => { serialNums = [...serialNums, ...this.collectAllSerialNums(child)]; }); } return serialNums; }, // 修改后的 getDocumentList 方法 getDocumentList() { // 查找当前选中的项目节点 const currentNode = this.$refs.projectTree.getNode(this.currentProjectId); if (!currentNode || !currentNode.data) { this.$message.error('未找到选中的项目'); return; } // 收集当前节点及其所有子节点的 serialNum const allSerialNums = this.collectAllSerialNums(currentNode.data); // 去重并转换为逗号分隔的字符串 const uniqueSerialNums = [...new Set(allSerialNums)].join(','); // 如果没有找到任何 serialNum,显示提示信息 if (!uniqueSerialNums) { this.documentList = []; this.docTotal = 0; this.$message.info('当前项目及其子项目没有关联任何文档'); return; } // 更新查询参数 this.docQueryParams.serialNums = uniqueSerialNums; this.docLoading = true; getDocuments(this.docQueryParams).then(response => { if (response.code === 200) { this.documentList = response.rows; this.docTotal = response.total; this.selectedDocs = []; // 清空选择 this.ids = []; // 清空ID集合 } else { this.$message.error(response.msg || '获取文档列表失败'); } this.docLoading = false; }).catch(error => { this.$message.error('获取文档列表失败: ' + error.message); this.docLoading = false; }); }, resetDocQuery() { this.docQueryParams.title = ''; this.docQueryParams.status = ''; this.getDocumentList(); }, handleEditDocument(row) { getData(row.id).then(response => { if (response.code === 200) { this.currentDocument = { id: response.data.id, menuId: response.data.menuId, title: response.data.title, content: response.data.content, status: response.data.status }; this.activeView = 'detail'; } else { this.$message.error(response.msg || '获取文档详情失败'); } }).catch(error => { this.$message.error('获取文档详情失败: ' + error.message); }); }, handleDeleteDocument(row) { this.$confirm(确定从项目中移除文档 "${row.title}" 吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用removeMenusFromProject API从项目中移除文档 // 参数: { projectId: 当前项目ID, menusIds: [文档ID] } const params = { projectId: this.currentProjectId, menusIds: [row.id] // 使用文档ID数组 }; removeMenusFromProject(params).then(response => { if (response.code === 200) { this.$message.success('文档已从项目中移除'); this.getDocumentList(); } else { this.$message.error(response.msg || '移除文档失败'); } }).catch(error => { this.$message.error('移除文档失败: ' + error.message); }); }); }, handleRowClick(row) { this.handleEditDocument(row); }, backToList() { this.activeView = 'list'; }, saveDocument() { const saveMethod = this.currentDocument.id ? updateData : addData; saveMethod(this.currentDocument).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.getDocumentList(); this.backToList(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); }, // ================= 导出功能方法 ================= // 多选处理 handleSelectionChange(selection) { this.selectedDocs = selection; this.ids = selection.map(item => item.id); }, // 导出文档(单篇) handleExportHttp(row, isDetail) { const params = { ids: row.id, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, word文档_${row.title}_${new Date().getTime()}.docx, { timeout: 60000 } ); }, // 批量导出文档 handleBatchExport() { if (this.selectedDocs.length === 0) { this.$message.warning('请选择要导出的文档'); return; } // 弹出选择导出类型的对话框 this.$confirm('请选择导出方式', '提示', { distinguishCancelAndClose: true, confirmButtonText: '导出详细', cancelButtonText: '导出简版', type: 'info' }).then(() => { // 导出详细 this.batchExportHttp(true); }).catch(action => { if (action === 'cancel') { // 导出简版 this.batchExportHttp(false); } }); }, // 批量导出文档实现 batchExportHttp(isDetail) { const params = { ids: this.ids, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, 批量文档_${new Date().getTime()}.zip, { timeout: 120000 } // 批量导出可能需要更长时间 ); }, // 下载方法实现 download(url, params, fileName, config = {}) { // 显示加载提示 const loading = this.$loading({ lock: true, text: '正在生成文档,请稍候...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); // 发送请求 axios.get(url, { params: params, responseType: 'blob', timeout: config.timeout || 30000, headers: { 'Authorization': Bearer ${this.$store.getters.token} } }) .then(response => { // 创建下载链接 const blob = new Blob([response.data], { type: response.headers['content-type'] }); const downloadUrl = window.URL.createObjectURL(blob); // 创建下载链接 const link = document.createElement('a'); link.href = downloadUrl; link.download = fileName; document.body.appendChild(link); // 触发下载 link.click(); // 清理资源 window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(link); this.$message.success('文档导出成功'); }) .catch(error => { console.error('导出失败:', error); // 处理错误响应 if (error.response) { if (error.response.status === 500) { this.$message.error('导出失败:服务器内部错误'); } else if (error.response.status === 401) { this.$message.error('导出失败:未授权访问'); } else if (error.response.status === 404) { this.$message.error('导出失败:API接口不存在'); } else { this.$message.error(导出失败:服务器错误 (${error.response.status})); } } else if (error.message.includes('timeout')) { this.$message.error('导出超时,请稍后再试'); } else { this.$message.error('导出失败:' + (error.message || '未知错误')); } }) .finally(() => { loading.close(); }); }, // 排序格式化方法 sortableFormatter(row, column) { if (this.ids.includes(row.id)) { return 1; } else { return 2; } }, // 跳转到文章详情页 getArticleInfo(articleId) { // id加密 const articleIdStr = EncryptJs(articleId, "f1827100d08ff039", "ed363078893c0329"); console.log('阅读的文档跳转加密后的ID:'+articleIdStr) let routeUrl = this.$router.resolve({ path: '/cms/doucumentView', query: { id: articleIdStr } }); window.open(routeUrl.href, '_blank'); } } }; </script> <style scoped> .app-container { padding: 20px; background-color: #f5f7fa; } .head-container { padding: 10px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .tree-node-content { display: flex; align-items: center; } .tree-icon { margin-right: 8px; color: #409EFF; } .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .node-actions { display: flex; align-items: center; } .doc-list-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .batch-actions { margin-bottom: 15px; } .doc-detail-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .doc-header { display: flex; align-items: center; margin-bottom: 20px; } .doc-title { display: flex; align-items: center; margin-left: 15px; margin-bottom: 0; font-size: 18px; color: #303133; } .title-icon { margin-right: 10px; color: #409EFF; } .doc-title .doc-icon { margin-right: 8px; color: #909399; } .el-tree { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; max-height: 70vh; overflow-y: auto; background-color: #ffffff; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } .el-table { margin-top: 10px; border-radius: 4px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .el-form-item { margin-bottom: 18px; } .el-tag { margin: 2px; } /* 响应式调整 */ @media (max-width: 768px) { .el-col-xs-24 { width: 100%; margin-bottom: 20px; } .doc-list-container, .doc-detail-container { padding: 10px; } .doc-header h2 { font-size: 16px; } } </style> 加个版本号在项目树上显示

@Override public void createArticleOutputFromTemplateHttp(HttpServletRequest request, HttpServletResponse response, ArticleData articleData) { if (articleData==null){ log.warn("勾选模板内容生成word文档的参数为空,不作处理,返回。"); return; } // 获取勾选的内容 Long[] ids = articleData.getIds(); if (ids==null || ids.length<=0){ log.warn("没有选择需要生成word文档的数据,不作处理,返回。"); return; } // 是否导出备注详情 int notesExportFlag = articleData.getNotesExportFlag(); // 导出数据准备---------------------------------- List<ArticleData> articleDataList = articleDataMapper.selectArticleDataByIds(ids); if (articleDataList==null || articleDataList.size()<=0){ log.warn("获取的word文档数据为空,不作处理,返回。"); return; } // 批量获取模块数据 ,然后进行排序======================================= // Long[] menu_ids = new Long[articleDataList.size()]; // for(int i=0;i<articleDataList.size();i++){ // menu_ids[i] = articleDataList.get(i).getMenuId(); // } // List<ArticleMenu> menu_list = articleMenuMapper.selectArticleMenuByMenuIds(menu_ids); // // 排序 // Collections.sort(menu_list, new Comparator<ArticleMenu>() { // @Override // public int compare(ArticleMenu o1, ArticleMenu o2) { // // TODO Auto-generated method stub // int flag = (int) (o1.getParentId()-o2.getParentId()); // if(flag == 0){ // flag = (int) (o1.getOrderNum()-o2.getOrderNum()); // } // return flag; // } // }); // ================================================================== // 获取数据 有序数据 // articleData.setMenuIds(menu_ids); // articleDataList = articleDataMapper.selectArticleDataByMenuIdsOrder(articleData); //各个 Long 含义: 层级、menuId(原先为父类模块ID)、排序order_num(注?:确保排序唯一),最底层map:数据、父类ID、排序 Map<Integer,Map<Long, Map<Long, List<Map<String, Object>>>>> myDatasMap = new HashMap<Integer,Map<Long, Map<Long, List<Map<String, Object>>>>>(); //菜单最深层数 int maxLevel = 1; // 菜单纵信息 List<Long[]> list_coloum = new ArrayList<Long[]>(); // 菜单纵信息及后面按显示顺序排序 List<String[]> list_coloum_str = new ArrayList<String[]>(); // 获取模块目录信息 for(ArticleData param : articleDataList){ //这里导出的时候加上备注的内容 if(notesExportFlag>0 && StringUtils.isNotBlank(param.getNotes())){ param.setContent(param.getContent() +"
" +"***备注***:" +""+param.getNotes()+""); } ArticleMenu menu = articleMenuMapper.selectArticleMenuByMenuId(param.getMenuId()); String[] menuIdArr = menu.getAncestors().split(","); Long[] menuIdsArr = new Long[menuIdArr.length+1]; // 用于排序(menu_id 和 序号 综合排) String[] menuIdsSort = new String[menuIdArr.length+1]; if(menuIdArr!=null && menuIdArr.length>0){ for(int i=0;i<menuIdArr.length;i++){ menuIdsArr[i] = Long.parseLong(menuIdArr[i].trim()); if(i>0){ ArticleMenu menu_temp = articleMenuMapper.selectArticleMenuByMenuId(Long.parseLong(menuIdArr[i].trim())); menuIdsSort[i] = menuIdArr[i].trim()+"&&"+menu_temp.getOrderNum(); }else{ menuIdsSort[i] = menuIdArr[i].trim(); } } } menuIdsArr[menuIdArr.length] = menu.getMenuId(); list_coloum.add(menuIdsArr); menuIdsSort[menuIdArr.length] = menu.getMenuId()+"&&"+menu.getOrderNum(); list_coloum_str.add(menuIdsSort); if(StringUtils.isNotBlank(param.getArticleVersion())){ param.setTitle(param.getTitle()+"(v"+param.getArticleVersion()+"版本)"); } // 解析获取模块信息-横信息 getArticleMenus(myDatasMap,maxLevel,menuIdsArr,param); } // 数据处理 if(myDatasMap != null && myDatasMap.size()>0){ String resourcePath = System.getProperty("user.dir") + "/src/main/resources/"; log.info("项目resource路径 resourcePath:"+resourcePath); List<Map<String, Object>> dataList = new ArrayList<Map<String, Object>>(); StringBuffer titleSb = new StringBuffer(); // 存放已经遍历到并处理的模块ID List<Long> menus_handled = new ArrayList<Long>(); // 记录多于1个模块的最深层数 int max_level = 0; int max_level_content = 0; // 菜单序号 int menu_num = 0; int ch_menu_num = 0; int ch2_menu_num = 0; int ch3_menu_num = 0; //按menu_id升序排序,之前使用的--20241016 // Collections.sort(list_coloum, new Comparator<Long[]>() { // @Override // public int compare(Long[] o1, Long[] o2) { // // TODO Auto-generated method stub // int flag = 0; // int len = o1.length>o2.length?o2.length:o1.length; // for(int i=0;i<len;i++){ // int diff = (int) (o1[i]-o2[i]); // flag = flag+diff; // } // return flag; // } // }); //按显示顺序升序排序list_coloum_str Collections.sort(list_coloum_str, new Comparator<String[]>() { @Override public int compare(String[] o1, String[] o2) { // TODO Auto-generated method stub int flag = 0; int len = o1.length>o2.length?o2.length:o1.length; for(int i=1;i<len;i++){ int num1 = Integer.parseInt(o1[i].split("&&")[1].trim()); int num2 = Integer.parseInt(o2[i].split("&&")[1].trim()); int diff = num1 - num2; flag = flag+diff; if(flag!=0){ break; } } return flag; } }); // for(Long[] arr_coloum : list_coloum){ // mu:for(int i=0;i<arr_coloum.length;i++){ // // 当前menuID // Long current_menu_id = arr_coloum[i]; for(String[] arr_coloum : list_coloum_str){ mu:for(int i=0;i<arr_coloum.length;i++){ // 当前menuID Long current_menu_id = Long.parseLong(arr_coloum[i].split("&&")[0].trim()); if(current_menu_id==null || current_menu_id==0 || current_menu_id.equals(0)){ continue; } if(menus_handled!=null&&menus_handled.size()>0){ for(Long menuid_handled : menus_handled){ if(menuid_handled.equals(current_menu_id)){ // 已处理的,就跳过 continue mu; } } } //层级、menuId(现为父类模块ID)、排序order_num(注?:确保排序唯一) Map<Long, Map<Long, List<Map<String, Object>>>> current_menu = myDatasMap.get(i); if(current_menu!=null && current_menu.size()>0){ List<Long> contentMenuIdsExist = new ArrayList<Long>(); for(Long current_parent_id : current_menu.keySet()){ Map<Long, List<Map<String, Object>>> current_parent_map = current_menu.get(current_parent_id); if(current_parent_map!=null && current_parent_map.size()>0){ for(Long current_order_num : current_parent_map.keySet()){ List<Map<String, Object>> current_order_list = current_parent_map.get(current_order_num); if(current_order_list!=null && current_order_list.size()>0){ for(Map<String, Object> current_order_map : current_order_list){ Long menuid = (Long) current_order_map.get("menuId"); if(current_menu_id.equals(menuid)){ log.info("项目resource路径 resourcePath:"+current_menu_id); // 层级、menuId(现为父类模块ID)、排序order_num(注?:确保排序唯一) int current_level_order = (int) current_order_map.get("menuLevel"); if(current_menu.size()==1 && current_parent_map.size()==1 && current_order_list.size() == 1 && (max_level_content==0 || (max_level_content>0 &¤t_level_order<max_level_content ))){ //说明该模块只有一个,这里作为标题 if(max_level_content>0){ if(current_level_order<max_level_content){ titleSb.append(current_order_map.get("menuName")+"_"); max_level = current_level_order; } }else{ titleSb.append(current_order_map.get("menuName")+"_"); max_level = current_level_order; } }else{ boolean isExist = false; if(contentMenuIdsExist!=null&&contentMenuIdsExist.size()>0){ for(Long menuKey : contentMenuIdsExist){ if(menuKey.equals(menuid)){ isExist = true; break; } } } if(!isExist){ int current_level = (int) current_order_map.get("menuLevel"); int level = current_level-max_level<1?1:current_level-max_level; if(current_level>max_level_content){ max_level_content = current_level; } // 给目录加上序号 String title_num = ""; if(level==1){ menu_num++; ch_menu_num = 0; //层级有变化,低一级目录序号清零 ch2_menu_num=0; ch3_menu_num=0; if(menu_num==1){ title_num = "一."; }else if(menu_num==2){ title_num = "二."; }else if(menu_num==3){ title_num = "三."; }else if(menu_num==4){ title_num = "四."; }else if(menu_num==5){ title_num = "五."; }else if(menu_num==6){ title_num = "六."; }else if(menu_num==7){ title_num = "七."; }else if(menu_num==8){ title_num = "八."; }else if(menu_num==9){ title_num = "九."; }else if(menu_num==10){ title_num = "十."; }else if(menu_num==11){ title_num = "十一."; }else if(menu_num==12){ title_num = "十二."; }else if(menu_num==13){ title_num = "十三."; }else if(menu_num==14){ title_num = "十四."; }else if(menu_num==15){ title_num = "十五."; }else if(menu_num==16){ title_num = "十六."; }else if(menu_num==17){ title_num = "十七."; }else if(menu_num==18){ title_num = "十八."; }else if(menu_num==19){ title_num = "十九."; }else if(menu_num==20){ title_num = "二十."; }else if(menu_num==21){ title_num = "二十一."; }else if(menu_num==22){ title_num = "二十二."; }else if(menu_num==23){ title_num = "二十三."; }else if(menu_num==24){ title_num = "二十四."; }else if(menu_num==25){ title_num = "二十五."; }else if(menu_num==26){ title_num = "二十六."; }else if(menu_num==27){ title_num = "二十七."; }else if(menu_num==28){ title_num = "二十八."; }else if(menu_num==29){ title_num = "二十九."; }else if(menu_num==30){ title_num = "三十."; } } if(level==2){ ch_menu_num++; ch2_menu_num=0; ch3_menu_num=0; title_num = menu_num+"."+ch_menu_num; } if(level==3){ ch2_menu_num++; ch3_menu_num=0; title_num = menu_num+"."+ch_menu_num+"."+ch2_menu_num; } if(level==4){ ch3_menu_num++; title_num = menu_num+"."+ch_menu_num+"."+ch2_menu_num+"."+ch3_menu_num; } Map<String, Object> wordData = new HashMap<>(); wordData.put("directory", title_num+" "+current_order_map.get("menuName")); Map<String, Object> wordDataMap = new HashMap<>(); wordDataMap.put("templateParth", resourcePath+"templates/bms_menu"+level+".docx"); wordDataMap.put("wordData", wordData); dataList.add(wordDataMap); } } if(current_order_map.get("content")!=null){ //加载内容 ArticleData current_article_data = (ArticleData) current_order_map.get("content"); if(current_article_data!=null){ int current_level = (int) current_order_map.get("menuLevel"); int level = current_level-max_level<1?1:current_level-max_level+1; if(current_level>max_level_content){ max_level_content = current_level; } // 给目录加上序号 String title_num = ""; if(level==1){ menu_num++; ch_menu_num = 0; //层级有变化,低一级目录序号清零 ch2_menu_num=0; ch3_menu_num=0; if(menu_num==1){ title_num = "一."; }else if(menu_num==2){ title_num = "二."; }else if(menu_num==3){ title_num = "三."; }else if(menu_num==4){ title_num = "四."; }else if(menu_num==5){ title_num = "五."; }else if(menu_num==6){ title_num = "六."; }else if(menu_num==7){ title_num = "七."; }else if(menu_num==8){ title_num = "八."; }else if(menu_num==9){ title_num = "九."; }else if(menu_num==10){ title_num = "十."; }else if(menu_num==11){ title_num = "十一."; }else if(menu_num==12){ title_num = "十二."; }else if(menu_num==13){ title_num = "十三."; }else if(menu_num==14){ title_num = "十四."; }else if(menu_num==15){ title_num = "十五."; }else if(menu_num==16){ title_num = "十六."; }else if(menu_num==17){ title_num = "十七."; }else if(menu_num==18){ title_num = "十八."; }else if(menu_num==19){ title_num = "十九."; }else if(menu_num==20){ title_num = "二十."; }else if(menu_num==21){ title_num = "二十一."; }else if(menu_num==22){ title_num = "二十二."; }else if(menu_num==23){ title_num = "二十三."; }else if(menu_num==24){ title_num = "二十四."; }else if(menu_num==25){ title_num = "二十五."; }else if(menu_num==26){ title_num = "二十六."; }else if(menu_num==27){ title_num = "二十七."; }else if(menu_num==28){ title_num = "二十八."; }else if(menu_num==29){ title_num = "二十九."; }else if(menu_num==30){ title_num = "三十."; } } if(level==2){ ch_menu_num++; ch2_menu_num=0; ch3_menu_num=0; title_num = menu_num+"."+ch_menu_num; } if(level==3){ ch2_menu_num++; ch3_menu_num=0; title_num = menu_num+"."+ch_menu_num+"."+ch2_menu_num; } if(level==4){ ch3_menu_num++; title_num = menu_num+"."+ch_menu_num+"."+ch2_menu_num+"."+ch3_menu_num; } //目录 Map<String, Object> wordData = new HashMap<>(); wordData.put("directory", " "+title_num+" "+current_article_data.getTitle()); Map<String, Object> wordDataMap = new HashMap<>(); //int current_level = (int) current_order_map.get("menuLevel"); //int level = current_level-max_level<1?1:current_level-max_level; wordDataMap.put("templateParth", resourcePath+"templates/bms_menu"+level+".docx"); wordDataMap.put("wordData", wordData); dataList.add(wordDataMap); // 内容 Map<String, Object> theData = new HashMap<>(); //=================================================================== // 测试内容 // File inputFile = new File("D:/Users/2104020051/Desktop/一些文件/富文本导入导出/亿纬_EVE-ICR18650_26V _ 知识管理 - PingCode.png"); // String base64 = ImageConvertBase64.toBase64(inputFile, true); // log.info("base64="+base64.substring(0, 200)+"......"); // String temp_img = "\"\""; //==================================================================== ////theData.put("menu", current_article_data.getTitle()); //theData.put("content", current_article_data.getContent()!=null? // current_article_data.getContent().replaceAll("/dev-api/profile", RuoYiConfig.getProfile())+temp_img // :"~标题为:"+current_article_data.getTitle()+"(id="+current_article_data.getId()+")的内容为空"); //针对内容的处理,主要是导出的时候图片的问题 String content = ""; if(current_article_data.getContent()!=null){ //文本编辑器类型(1:Quill富文本编辑器 2:CherryMarkdown 3:Tinymce富文本编辑器) int content_type = Integer.parseInt(current_article_data.getContentType().trim()); if(content_type==1){ content = current_article_data.getContent(); }else if(content_type==2 || content_type==3){ //2:CherryMarkdown 3:Tinymce富文本编辑器 List<String> imgValues = PoiWordUtil.getTagAttributeValues(current_article_data.getContent(), "img", "src"); if(imgValues!=null && imgValues.size()>0){ // 有图片时 content = current_article_data.getContent(); for (String value : imgValues) { String[] image_url_arr = value.split(Constants.RESOURCE_PREFIX); if(image_url_arr!=null && image_url_arr.length>=2){ //String image_url_local = value.replaceAll("/dev-api/profile", RuoYiConfig.getProfile()); //图片全路径 String image_url_local = RuoYiConfig.getProfile()+image_url_arr[1]; //图片全路径 log.info("图片路径="+image_url_local); //image_url_local = "D:/Users/2104020051/Desktop/一些文件/富文本导入导出/亿纬_EVE-ICR18650_26V _ 知识管理 - PingCode.png"; //"E:/ruoyi/uploadPath/upload/2024/07/30/bef0dd24-730b-41e7-946d-b1f86b09149f-(12) 亿纬_EVE-ICR18650_26V _ 知识管理 - PingCode.png"; File inputFile = new File(image_url_local); String base64 = ImageConvertBase64.toBase64(inputFile, true); log.info("base64="+base64.substring(0, 200)+"......"); log.info("value="+value); content = content.replace(value, base64); } } }else{ content = current_article_data.getContent(); } } }else{ content = "~标题为:"+current_article_data.getTitle()+"(id="+current_article_data.getId()+")的内容为空"; } theData.put("content", content); Map<String, Object> wordDataMap2 = new HashMap<>(); wordDataMap2.put("templateParth", resourcePath+"templates/bms_content.docx"); wordDataMap2.put("wordData", theData); dataList.add(wordDataMap2); contentMenuIdsExist.add(menuid); } } menus_handled.add((Long) current_order_map.get("menuId")); // 加入已处理集合 } } } } } } } } } Map<String, Object> wordHead_test = new HashMap<>(); wordHead_test.put("title", titleSb.substring(0, titleSb.length()-1)); wordHead_test.put("currentTime", DateUtils2.getCurrentDateTime()); //首页数据 Map<String, Object> wordHeadMap_test = new HashMap<>(); wordHeadMap_test.put("templateParth", resourcePath+"templates/bms_title.docx"); wordHeadMap_test.put("wordData", wordHead_test); // 上传文件路径 //String filePath = RuoYiConfig.getPathWithTemplePack(); // 新文件名称 //String current = DateUtils2.format(new Date(), "yyyyMMddHHmmssSSS"); //IdUtils.fastUUID() //String fileName = DateUtils.datePath() + "/"+ "Demo报告-"+ current +".docx"; //String documentName = titleSb+ current +".docx"; //String fileName = DateUtils.datePath() + File.separator + documentName; try { //File desc = FileUploadUtils.getAbsoluteFile(filePath, fileName); //String descLocalUrl = desc.getAbsolutePath(); //log.info("导出路径descLocalUrl:"+descLocalUrl);; // 生成并导出word文档 //PoiWordUtil.createDocxManyForRTFHtml(wordHeadMap_test, dataList, descLocalUrl); PoiWordUtil.createDocxManyForRTFHtmlWithHttp(wordHeadMap_test,dataList,request, response); //WordUtil.createDocxMany(wordHeadMap_test, dataList, descLocalUrl); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 多XWPFTemplate合并成一个word导出,http导出 * @param headMap * @param dataList * @param destParth */ public static void createDocxManyForRTFHtmlWithHttp(Map<String, Object> headMap,List<Map<String, Object>> dataList,HttpServletRequest request, HttpServletResponse response) { LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); Configure config = Configure.builder().useSpringEL() .bind("list", policy).build(); // 首页标题 String templateParth_head = headMap.get("templateParth")!=null?String.valueOf(headMap.get("templateParth")).trim():""; Map<String, Object> wordData_head = headMap.get("wordData")!=null?(Map<String, Object>) headMap.get("wordData"):null; if(StringUtils.isBlank(templateParth_head) || wordData_head==null){ log.warn("首页标题参数异常,不作解析导出"); return; } XWPFTemplate template_head = XWPFTemplate.compile(templateParth_head,config).render(wordData_head);// 首页数据调用模板,填充数据 if(dataList==null || dataList.size()<=0){ log.warn("模块数据为空,不作解析导出"); return; } try { //多文档集合 List<NiceXWPFDocument> list = new ArrayList<NiceXWPFDocument>(); NiceXWPFDocument niceXWPFDocument = template_head.getXWPFDocument(); // 生成目录 -------------------------------------------------------------- // 创建标题 XWPFParagraph title = niceXWPFDocument.createParagraph(); XWPFRun titleRun = title.createRun(); //空一行 titleRun.setText(""); // 设置为空字符串来创建一个新的段落 titleRun.addCarriageReturn(); // 确保这个运行不会与前一个运行合并 //空一行 titleRun = title.createRun(); titleRun.setText(""); // 设置为空字符串来创建一个新的段落 titleRun.addCarriageReturn(); // 确保这个运行不会与前一个运行合并 titleRun = title.createRun(); titleRun.setText("目录"); titleRun.setFontSize(16); titleRun.setBold(true); //空一行 titleRun = title.createRun(); titleRun.setText(""); // 设置为空字符串来创建一个新的段落 titleRun.addCarriageReturn(); // 确保这个运行不会与前一个运行合并 niceXWPFDocument.enforceUpdateFields(); CTAbstractNum cTAbstractNum = CTAbstractNum.Factory.newInstance(); XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum); XWPFNumbering numbering = niceXWPFDocument.createNumbering(); BigInteger numID = numbering.addAbstractNum(abstractNum); BigInteger numberingID = numbering.addNum(numID); // 插入目录 XWPFParagraph toc = niceXWPFDocument.createParagraph(); toc.setPageBreak(true); toc.setWordWrap(true); toc.setAlignment(ParagraphAlignment.CENTER); CTP ctP = toc.getCTP(); CTSimpleField tocField = ctP.addNewFldSimple(); tocField.setInstr("TOC \\o \"1-3\" \\h \\z \\u"); toc.getCTP().addNewR().addNewFldChar().setFldCharType(STFldCharType.END); // 生成目录end------------------------------------------------------------- XWPFRun runs = niceXWPFDocument.createParagraph().createRun(); //int num = 0; for(Map<String, Object> dataMap : dataList){ String templateParth_data = dataMap.get("templateParth")!=null?String.valueOf(dataMap.get("templateParth")).trim():""; Map<String, Object> wordData_data = dataMap.get("wordData")!=null?(Map<String, Object>) dataMap.get("wordData"):null; if(StringUtils.isBlank(templateParth_data) || wordData_data==null){ log.warn("模版数据参数异常,该数据不作解析导出,下一条.."); continue; } XWPFTemplate template_data = null;//XWPFTemplate.compile(templateParth_data,config).render(wordData_data);// wordData数据调用模板,填充数据 // 富文本的数据 if(wordData_data.get("content")!=null){ String content = (String) wordData_data.get("content"); String tmpContent = content.replace("<", "<").replace(">", ">").replace(""", "\"").replace("&", "&"); // 拼接html格式内容 StringBuffer sbf = new StringBuffer(); // 这里拼接一下html标签,便于word文档能够识别 sbf.append("<html " + "xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" xmlns:m=\"http://schemas.microsoft.com/office/2004/12/omml\" xmlns=\"http://www.w3.org/TR/REC-html40\"" + //将版式从web版式改成页面试图 ">"); sbf.append("<head>" + "" + "</head>"); sbf.append("<body>"); // 富文本内容 sbf.append(tmpContent); sbf.append("</body></html>"); HtmlRenderPolicy htmlRenderPolicy1 = new HtmlRenderPolicy(); Configure configure1 = Configure.builder() .bind("content", htmlRenderPolicy1) .build(); Map<String, Object> data1 = new HashMap<>(); data1.put("content", sbf.toString()); template_data = XWPFTemplate.compile(templateParth_data, configure1).render(data1); }else{ template_data = XWPFTemplate.compile(templateParth_data,config).render(wordData_data); } list.add(template_data.getXWPFDocument()); template_data.close(); } NiceXWPFDocument newDoc = niceXWPFDocument.merge(list,runs); //合并多个 //FileOutputStream out = new FileOutputStream(destParth);//要导出的文件名 //输出文件 request.setCharacterEncoding("utf-8"); //request.setAttribute(paramString, paramObject);.setAsyncTimeout(10000); //导出word格式"application/msword" -doc "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -docx response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); //response.setHeader("Access-Control-Expose-Headers","Content-disposition"); response.addHeader("Content-Disposition", "attachment;filename=" + wordData_head.get("title") + ".docx"); //过期时间设置 //response.setDateHeader("expires",System.currentTimeMillis() + 60 * 1000); ServletOutputStream out = response.getOutputStream(); newDoc.write(out); out.flush(); out.close(); newDoc.close(); niceXWPFDocument.close(); template_head.close(); log.info("导出生成完成..."); } catch (Exception e) { e.printStackTrace(); } } 这是导出文档的两个方法,现在文档内容的字体比标题的还要大,如何规范调整好文档正文的字体大小和格式

我前段时间写了一个python程序,用于抓取自己闲鱼页面的商品信息,但是为了避免违规,我想重新编写一个程序,用于在阿奇索后台(开发指南:接入指南 1、接入流程 【开发者操作】登录后台申请AppIds。 入口:https://open.agiso.com/#/my/application/app-list 【开发者操作】申请到AppId后,开发者可以登录后台管理AppId,这里可以查看和更换AppSecret、更改推送url、更改授权回调url等。 入口:https://open.agiso.com/#/my/application/app-list 【商家操作】授权,方法有二: 1、输入开发者提供的AppId(相当于告诉Agiso,允许这个AppId通过Agiso开放平台获取或操作商家自己的订单数据),勾选相应要授权的权限。授权后会显示一个Token,将Token复制给开发者。 入口:https://aldsIdle.agiso.com/#/open/authorize 2、开发者如果有开发自动授权,则商家可以通过访问以下页面进行授权: 入口:https://aldsIdle.agiso.com/#/authorize?appId={$请替换为要授权的开发者的appId}&state=2233 【开发者操作】开发者得到各个商家授权给的Token,并使用Token调用接口。调用接口时,需要使用AppSecret进行签名,具体签名方法参见下文。 注意:开发者与商家,也可以是同一个人。 2、获取AccessToken详解 手动模式自动模式 将你的AppId告诉您的用户,用户通过在授权页面(https://aldsIdle.agiso.com/#/open/authorize) 进行授权。用户授权完成后,会获得一个AccessToken,让您的用户把该AccessToken发给你。 AccessToken的有效期和您的用户购买Agiso软件的使用时间一致。如果您的用户续费,那么AccessToken的有效期也会延长。 3、调用接口详解 调用任何一个API都必须把AccessToken 和 ApiVersion 添加到Header ,格式为"Authorization: Bearer access_token",其中Bearer后面有一个空格。同时还需传入以下公共参数: timestamp 是 Date 时间戳,例如:1468476350。API服务端允许客户端请求最大时间误差为10分钟。 sign 是 string API输入参数签名结果,签名算法参照下面的介绍。 注意:接口调用配额,20次/秒。 4、签名算法 【对所有API请求参数(包括公共参数和业务参数,但除去sign参数和byte[]类型的参数),根据参数名称的ASCII码表的顺序排序。如:foo=1, bar=2, foo_bar=3, foobar=4排序后的顺序是bar=2, foo=1, foo_bar=3, foobar=4。 将排序好的参数名和参数值拼装在一起,根据上面的示例得到的结果为:bar2foo1foo_bar3foobar4。 把拼装好的字符串采用utf-8编码,在拼装的字符串前后加上app的secret后,使用MD5算法进行摘要,如:md5(secret+bar2foo1foo_bar3foobar4+secret); 5、Header设置示例代码 JavaC#PHP HttpPost httpPost = new org.apache.http.client.methods.HttpPost(url); httpPost.addHeader("Authorization","Bearer "+ accessToken); httpPost.addHeader("ApiVersion", "1"); 6、签名算法示例代码 JavaC#PHP Map<String, String> data = new HashMap<String, String>(); data.put("modifyTimeStart", "2016-07-13 10:44:30"); data.put("pageNo", "1"); data.put("pageSize", "20"); //timestamp 为调用Api的公共参数,详细说明参考接入指南 data.put("timestamp", '1468476350');//假设当前时间为2016/7/14 14:5:50 //对键排序 String[] keys = data.keySet().toArray(new String[0]); Arrays.sort(keys); StringBuilder query = new StringBuilder(); //头加入AppSecret ,假设AppSecret值为****************** query.append(this.getClientSecret()); for (String key : keys) { String value = data.get(key); query.append(key).append(value); } //到这query的值为******************modifyTimeStart2016-07-13 10:44:30pageNo1pageSize20timestamp1468476350 //尾加入AppSecret query.append(this.getClientSecret()); //query=******************modifyTimeStart2016-07-13 10:44:30pageNo1pageSize20timestamp1468476350****************** byte[] md5byte = encryptMD5(query.toString()); //sign 为调用Api的公共参数,详细说明参考接入指南 data.put("sign", byte2hex(md5byte)); //byte2hex(md5byte) = 935671331572EBF7F419EBB55EA28558 // Md5摘要 public byte[] encryptMD5(String data) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest md5 = MessageDigest.getInstance("MD5"); return md5.digest(data.getBytes("UTF-8")); } public String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex.toLowerCase()); } return sign.toString(); } 7、完整调用API示例代码 以下代码以调用LogisticsDummySend(更新发货状态)为例 JavaC#PHP public String LogisticsDummySend() { string accessToken = "*************"; string appSecret = "*************"; WebRequest apiRequest = WebRequest.Create("http://gw.api.agiso.com/aldsIdle/Trade/LogisticsDummySend"); apiRequest.Method = "POST"; apiRequest.ContentType = "application/x-www-form-urlencoded"; apiRequest.Headers.Add("Authorization", "Bearer " + accessToken); apiRequest.Headers.Add("ApiVersion", "1"); //业务参数 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); var args = new Dictionary<string,string>() { {"tids","123456789,987465123"}, 注意 tids是示例参数实际参数要以当前文档上的入参为准!!! {"timestamp",Convert.ToInt64(ts.TotalSeconds).ToString()} }; args.Add("sign", Sign(args, appSecret)); //拼装POST数据 string postData = ""; foreach (var p in args) { if (!String.IsNullOrEmpty(postData)) { postData += "&"; } string tmpStr = String.Format("{0}={1}", p.Key, HttpUtility.UrlEncode(p.Value)); postData += tmpStr; } using (var sw = new StreamWriter(apiRequest.GetRequestStream())) { sw.Write(postData); } WebResponse apiResponse = null; try { apiResponse = apiRequest.GetResponse(); } catch (WebException we) { if (we.Status == WebExceptionStatus.ProtocolError) { apiResponse = (we.Response as HttpWebResponse); } else{ //TODO:处理异常 return ""; } } using(Stream apiDataStream = apiResponse.GetResponseStream()){ using(StreamReader apiReader = new StreamReader(apiDataStream, Encoding.UTF8)){ string apiResult = apiReader.ReadToEnd(); apiReader.Close(); apiResponse.Close(); return apiResult; } } } //参数签名 public string Sign(IDictionary<string,string> args, string ClientSecret) { IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(args, StringComparer.Ordinal); string str = ""; foreach (var m in sortedParams) { str += (m.Key + m.Value); } //头尾加入AppSecret str = ClientSecret + str + ClientSecret; var encodeStr = MD5Encrypt(str); return encodeStr; } //Md5摘要 public static string MD5Encrypt(string text) { MD5 md5 = new MD5CryptoServiceProvider(); byte[] fromData = System.Text.Encoding.UTF8.GetBytes(text); byte[] targetData = md5.ComputeHash(fromData); string byte2String = null; for (int i = 0; i < targetData.Length; i++) { byte2String += targetData[i].ToString("x2"); } return byte2String; })作为API接入商家后台,我想要获取我自己的商品数据:(# helps.py(仅仅作为功能参考,可以根据实际编写需要改写) import os def display_help(): """显示帮助信息""" print(""" 商品信息抓取工具 v1.0 ========================== 使用方法: python main.py [选项] 选项: -u, --url URL 商品URL -o, --output PATH 输出文件路径(.docx) -f, --file FILE 批量任务文件路径 -t, --threads N 并发线程数(默认:2) -d, --debug 启用调试模式 -v, --verbose 显示详细输出 -h, --help 显示帮助信息 批量任务文件格式: URL | 输出文件名称 以#开头的行默认为注释,将不会进行任何的处理 示例: https://2.taobao.com/item?id=123 | item1.docx https://2.taobao.com/item?id=456 | item2.docx 输出文件: 1. Word文档(.docx) - 包含商品标题、描述和图片 2. Excel报告(.xlsx) - 包含word文档标题和元数据(价格,商品标题等) 日志文件: xianyu_scraper.log - 包含详细运行日志 依赖库: pip~=25.1.1 requests==2.32.4 beautifulsoup4==4.12.3 python-docx==1.1.0 pandas==2.3.1 openpyxl==3.1.5 fake-useragent==2.2.0 htmldocx==0.0.6 pillow==11.3.0 selenium==4.34.2 webdriver-manager==4.0.2 tqdm==4.67.1(进度条) """) def show_banner(): """显示程序横幅""" print(r""" 商品信息抓取工具 v1.0 ============================================================================== """),现在请为我编写程序,并且给我一个详细的接入阿奇索后台的步骤

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> <title>黑马点评</title> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .comment-input { @apply w-full min-h-[120px] border border-gray-300 text-lg leading-relaxed resize-none outline-none bg-white mb-3 px-4 py-3 rounded-lg transition-all duration-300; } .comment-input:focus { @apply border-primary ring-1 ring-primary/20; } .comment-input::placeholder { @apply text-gray-400; } .comment-box { @apply flex p-4 border-b border-gray-100; } .comment-icon { @apply w-10 h-10 rounded-full overflow-hidden mr-3 flex-shrink-0; } .comment-icon img { @apply w-full h-full object-cover; } .comment-user { @apply font-medium text-gray-800; } .comment-info { @apply flex-1; } .send-btn { @apply bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-full transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed; } } </style> <style> .header { position: relative; } .foot-view span { font-size: 12px; } .liked { color: #ff6633; } /* 评论输入框样式 */ .comment-container { position: fixed; bottom: 0; left: 0; right: 0; height: 0; background-color: white; z-index: 150; display: flex; flex-direction: column; box-shadow: -2px -2px 10px rgba(0,0,0,0.05); transition: height 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .comment-container.active { height: 60%; /* 增加评论容器高度 */ } .comment-header { height: 56px; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #E5E7EB; } .comment-header .cancel-btn { color: #6B7280; font-size: 16px; background: none; border: none; cursor: pointer; } .comment-header .send-btn { color: white; background-color: #FF7A00; border: 1px solid #FF7A00; /* 橙色边框 */ padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; transition: background-color 0.2s; } .comment-header .send-btn:hover { background-color: #FF6600; border-color: #FF6600; /* 悬停时边框颜色加深 */ } .comment-content { padding: 16px; display: flex; flex-direction: column; height: 100%; } /* 新增样式 */ .loading { display: flex; justify-content: center; align-items: center; height: 40px; color: #9CA3AF; } .empty-comment { text-align: center; padding: 20px 0; color: #9CA3AF; font-size: 14px; } .load-more { display: flex; justify-content: center; align-items: center; padding: 15px 0; color: #6B7280; font-size: 14px; cursor: pointer; } </style> </head> <body> ... 用户头像 {{blog.name}} {{formatTime(new Date(blog.createTime))}} {{followed ? '取消关注' : '关注'}} 店铺图片 {{shop.name}} <el-rate v-model="shop.score/10"> </el-rate> ¥{{shop.avgPrice}}/人 <svg t="1646634642977" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2187" width="20" height="20"> </svg> 点赞用户头像 {{blog.liked}}人点赞 网友评价 {{totalComments}}条 加载中... 没有更多评论了 加载评论中... 暂无评论,快来发表第一条评论吧 评论用户头像 {{comment.name}} Lv{{comment.level}} {{comment.content}} {{formatCommentTime(comment.createTime)}} 查看更多评论 <svg t="1646634642977" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2187" width="26" height="26"> </svg> {{blog.liked}} <button class="cancel-btn" @click="hideCommentBox">取消</button> 评论 <button class="send-btn" @click="sendComment" :disabled="!commentText.trim()">发送</button> <textarea class="comment-input" v-model="commentText" placeholder="输入评论内容..." ref="commentInput"></textarea> <script src="./js/vue.js"></script> <script src="./js/axios.min.js"></script> <script src="./js/element.js"></script> <script src="./js/common.js"></script> <script> let each = function (ary, callback) { for (let i = 0, l = ary.length; i < l; i++) { if (callback(ary[i], i) === false) break } } const app = new Vue({ el: "#app", data: { util, blog: {}, shop: {}, likes: [], user: {}, // 登录用户 followed: false, // 是否关注了 _width: 0, duration: 300, container: null, items: [], active: 0, start: { x: 0, y: 0 }, move: { x: 0, y: 0 }, sensitivity: 60, resistance: 0.3, commentText: '', // 评论内容 commentContainer: null, // 评论相关数据 comments: [], // 当前页评论列表 totalComments: 0, // 评论总数 currentPage: 1, // 当前页码 pageSize: 10, // 每页大小 hasMore: false, // 是否有更多数据 isLoading: false, // 是否正在加载 }, created() { let id = util.getUrlParam("id"); this.queryBlogById(id); this.fetchComments(id); }, mounted() { this.commentContainer = this.$refs.commentContainer; }, methods: { init() { // 获得父容器节点 this.container = this.$refs.swiper // 获得所有的子节点 this.items = this.container.querySelectorAll('.swiper-item') this.updateItemWidth() this.setTransform() this.setTransition('none') }, goBack() { history.back(); }, toOtherInfo(){ if(this.blog.userId === this.user.id){ location.href="/https/wenku.csdn.net/info.html" }else{ location.href="/https/wenku.csdn.net/other-info.html?id=" + this.blog.userId } }, queryBlogById(id) { axios.get("/blog/" + id) .then(({data}) => { data.images = data.images.split(",") this.blog = data; this.$nextTick(this.init); this.queryShopById(data.shopId) this.queryLikeList(id); this.queryLoginUser(); }) .catch(this.$message.error) }, queryShopById(shopId) { axios.get("/shop/" + shopId) .then(({data}) => { data.image = data.images.split(",")[0] this.shop = data }) .catch(this.$message.error) }, queryLikeList(id){ axios.get("/blog/likes/" + id) .then(({data}) => this.likes = data) .catch(this.$message.error) }, addLike(){ axios.put("/blog/like/" +this.blog.id) .then(({data}) => { axios.get("/blog/" + this.blog.id) .then(({data}) => { data.images = data.images.split(",") this.blog = data; this.queryLikeList(this.blog.id); }) .catch(this.$message.error) }) .catch(err => { this.$message.error(err) }) }, isFollowed(){ axios.get("/follow/or/not/" + this.blog.userId) .then(({data}) => this.followed = data) .catch(this.$message.error) }, follow(){ axios.put("/follow/" + this.blog.userId + "/" + !this.followed) .then(() => { this.$message.success(this.followed ? "已取消关注" : "已关注") this.followed = !this.followed }) .catch(this.$message.error) }, formatTime(b) { return b.getFullYear() + "年" + (b.getMonth() + 1) + "月" + b.getDate() + "日 "; }, formatMinutes(m) { if (m < 10) m = "0" + m return m; }, queryLoginUser(){ // 查询用户信息 axios.get("/user/me") .then(({ data }) => { // 保存用户 this.user = data; if(this.user.id !== this.blog.userId){ this.isFollowed(); } }) .catch(console.log) }, // 轮播图相关方法 updateItemWidth() { this._width = this.container.offsetWidth || document.documentElement.offsetWidth }, setTransform(offset) { offset = offset || 0 each(this.items, (item, i) => { let distance = (i - this.active) * this._width + offset let transform = translate3d(${distance}px, 0, 0) item.style.webkitTransform = transform item.style.transform = transform }) }, setTransition(duration) { duration = duration || this.duration duration = typeof duration === 'number' ? (duration + 'ms') : duration each(this.items, (item) => { item.style.webkitTransition = duration item.style.transition = duration }) }, moveStart(e) { this.start.x = e.changedTouches[0].pageX this.start.y = e.changedTouches[0].pageY this.setTransition('none') }, moving(e) { e.preventDefault() e.stopPropagation() let distanceX = e.changedTouches[0].pageX - this.start.x let distanceY = e.changedTouches[0].pageY - this.start.y if (Math.abs(distanceX) > Math.abs(distanceY)) { this.isMoving = true this.move.x = this.start.x + distanceX this.move.y = this.start.y + distanceY if ((this.active === 0 && distanceX > 0) || (this.active === (this.items.length - 1) && distanceX < 0)) { distanceX = distanceX * this.resistance } this.setTransform(distanceX) } }, moveEnd(e) { if (this.isMoving) { e.preventDefault() e.stopPropagation() let distance = this.move.x - this.start.x if (Math.abs(distance) > this.sensitivity) { if (distance < 0) { this.next() } else { this.prev() } } else { this.back() } this.reset() this.isMoving = false; } }, next() { let index = this.active + 1 this.go(index) }, prev() { let index = this.active - 1 this.go(index) }, reset() { this.start.x = 0 this.start.y = 0 this.move.x = 0 this.move.y = 0 }, back() { this.setTransition() this.setTransform() }, destroy() { this.setTransition('none') }, go(index) { this.active = index if (this.active < 0) { this.active = 0 } else if (this.active > this.items.length - 1) { this.active = this.items.length - 1 } this.$emit('change', this.active) this.setTransition() this.setTransform() }, // 评论相关方法 showCommentBox() { if (!this.user.id) { this.$message.warning("请先登录"); return; } this.commentContainer.classList.add('active'); setTimeout(() => { this.$refs.commentInput.focus(); }, 300); }, hideCommentBox() { this.commentContainer.classList.remove('active'); this.commentText = ''; }, sendComment() { const content = this.commentText.trim(); if (content) { // 使用FormData格式,与后端接口参数匹配 const formData = new FormData(); formData.append('comment', content); formData.append('blogId', this.blog.id); axios.post("/blog-comments", formData) .then(() => { this.$message.success("评论发送成功"); this.hideCommentBox(); this.commentText = ''; // 刷新第一页评论 this.fetchComments(this.blog.id, 1); }) .catch(err => { this.$message.error("评论发送失败: " + err.response?.data?.message || err.message); console.error(err); }); } else { this.$message.warning("请输入评论内容"); } }, // 获取评论列表 - 适配后端分页接口 fetchComments(blogId, page = 1) { // 如果是加载第一页,清空现有评论 if (page === 1) { this.comments = []; } this.isLoading = true; axios.get(/blog-comments/${blogId}, { params: { page: page, size: this.pageSize } }) .then((response) => { // 确保响应数据存在 if (!response.data) { this.$message.warning("评论数据为空"); this.hasMore = false; this.isLoading = false; return; } const pageResult = response.data; // 验证必要字段存在 if (pageResult.records === undefined || pageResult.totalRecords === undefined) { this.$message.warning("评论数据格式异常,缺少必要字段"); this.hasMore = false; this.isLoading = false; return; } // 更新评论列表 if (page === 1) { this.comments = pageResult.records || []; } else { this.comments = [...this.comments, ...(pageResult.records || [])]; } // 更新分页信息 this.totalComments = pageResult.totalRecords || 0; this.currentPage = pageResult.currentPage || page; this.pageSize = pageResult.pageSize || this.pageSize; // 判断是否还有更多数据 this.hasMore = this.comments.length < this.totalComments; }) .catch(err => { this.$message.error('评论加载失败: ' + err.response?.data?.message || '网络错误'); console.error(err); }) .finally(() => { this.isLoading = false; }); }, // 加载更多评论 loadMoreComments() { if (!this.isLoading && this.hasMore) { this.fetchComments(this.blog.id, this.currentPage + 1); } }, // 格式化评论时间(最终修复版) formatCommentTime(timestamp) { if (!timestamp) return ''; // 尝试将时间戳转换为数字 let timeValue; // 处理字符串类型的时间戳 if (typeof timestamp === 'string') { // 移除所有非数字字符 const cleanTimestamp = timestamp.replace(/[^\d]/g, ''); // 如果清理后为空字符串,尝试其他解析方法 if (!cleanTimestamp) { // 尝试直接解析原始字符串 try { timeValue = Number(timestamp); } catch (e) { console.error('Failed to parse timestamp:', timestamp); return '未知时间'; } } else { timeValue = Number(cleanTimestamp); } } else if (typeof timestamp === 'number') { timeValue = timestamp; } else { console.error('Unsupported timestamp type:', typeof timestamp); return '未知时间'; } // 验证转换后的时间戳是否为有效数字 if (isNaN(timeValue)) { console.error('Invalid timestamp value:', timestamp); return '未知时间'; } // 处理零值或负值时间戳 if (timeValue <= 0) { return '未知时间'; } // 根据时间戳范围智能判断(优化处理超长时间戳) let commentDate; // 特殊处理:如果时间戳大于当前时间的2倍,可能是前端时间戳生成问题 const currentTime = Date.now(); if (timeValue > currentTime * 2) { // 尝试将超长时间戳除以10的幂,直到合理范围 let adjustedTimestamp = timeValue; while (adjustedTimestamp > currentTime * 2 && adjustedTimestamp > 1e15) { adjustedTimestamp = Math.floor(adjustedTimestamp / 10); } // 使用调整后的时间戳 commentDate = new Date(adjustedTimestamp); } else { // 默认作为毫秒级时间戳处理 commentDate = new Date(timeValue); } // 验证日期有效性 if (isNaN(commentDate.getTime())) { console.error('Invalid date from timestamp:', timestamp); return '未知时间'; } // 计算与当前时间的差值(毫秒) const now = new Date(); const diffMs = now - commentDate; // 处理未来时间(diffMs为负数) if (diffMs < 0) { // 未来时间显示完整日期 return commentDate.toISOString().slice(0, 10); } // 计算差值(分钟、小时、天) const diffMinutes = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); // 相对时间显示 if (diffMinutes < 1) return '刚刚'; if (diffMinutes < 60) return ${diffMinutes}分钟前; if (diffHours < 24) return ${diffHours}小时前; if (diffDays < 7) return ${diffDays}天前; // 超过7天,显示完整日期 return commentDate.toISOString().slice(0, 10); } } }) </script> </body> </html>时间显示错了,传进来的"createTime": 1749989266662,后端System.currentTimeMillis()产生的,时间显示错了

------------------------ DataManager.js ------------------------ //{ 实体类的关联和属性列表 // “entities”: { // “Dingdan”: { // “properties”: { // “id”: “Integer”, // “number”: “String”, // “xiadan”: “Date”, // “jiaohuo”: “Date”, // “dingdan_chanpin”: “List<Dingdan_chanpin> (OneToMany)”, // “dingdan_chanpins_zujian”: “List<Dingdan_chanpin_zujian> (OneToMany)” // }, // “relations”: [ // “关联订单产品(Dingdan_chanpin)”, // “关联订单组件(Dingdan_chanpin_zujian)” // ] // }, // “Dingdan_chanpin”: { // “properties”: { // “id”: “Integer”, // “shuliang”: “Integer” // }, // “relations”: [ // “多对一关联订单(Dingdan)”, // “多对一关联产品(Chanpin)” // ] // }, // “Dingdan_chanpin_zujian”: { // “properties”: { // “id”: “Integer”, // “shuliang”: “Integer” // }, // “relations”: [ // “多对一关联订单(Dingdan)”, // “多对一关联组件(Chanpin_zujian)”, // “多对一关联板材(Bancai)” // ] // }, // “Jinhuo”: { // “properties”: { // “id”: “Integer”, // “shuliang”: “Integer”, // “date”: “Date” // }, // “relations”: [ // “多对一关联订单(Dingdan)”, // “多对一关联产品(Chanpin)”, // “多对一关联组件(Zujian)”, // “多对一关联板材(Bancai)”, // “多对一关联用户(User)” // ] // }, // “Kucun”: { // “properties”: { // “id”: “Integer”, // “shuliang”: “Long” // }, // “relations”: [ // “一对一关联板材(Bancai)” // ] // }, // “Mupi”: { // “properties”: { // “id”: “Integer”, // “name”: “String”, // “you”: “Boolean” // }, // “relations”: [ // “被板材关联(Bancai - mupi1/mupi2)” // ] // }, // “User”: { // “properties”: { // “id”: “Integer”, // “name”: “String”, // “andy”: “String”, // “pass”: “String”, // “role”: “int” // } // }, // “Zujian”: { // “properties”: { // “id”: “Integer”, // “name”: “String” // }, // “relations”: [ // “一对多关联产品组件(Chanpin_zujian)” // ] // }, // “Bancai”: { // “properties”: { // “id”: “Integer”, // “houdu”: “Double” // }, // “relations”: [ // “多对一关联材质(Caizhi)”, // “多对一关联木皮(Mupi - mupi1/mupi2)”, // “一对一关联库存(Kucun)” // ] // }, // “Caizhi”: { // “properties”: { // “id”: “Integer”, // “name”: “String” // }, // “relations”: [ // “一对多关联板材(Bancai)” // ] // }, // “Chanpin”: { // “properties”: { // “id”: “Integer”, // “bianhao”: “String” // }, // “relations”: [ // “一对多关联订单产品(Dingdan_chanpin)”, // “一对多关联产品组件(Chanpin_zujian)” // ] // }, // “Chanpin_zujian”: { // “properties”: { // “id”: “Integer”, // “one_howmany”: “Double” // }, // “relations”: [ // “多对一关联产品(Chanpin)”, // “多对一关联组件(Zujian)”, // “多对一关联板材(Bancai)” // ] // } // }, // “relationsSummary”: [ // “订单(Dingdan) 1:N 订单产品(Dingdan_chanpin)”, // “订单(Dingdan) 1:N 订单组件(Dingdan_chanpin_zujian)”, // “产品(Chanpin) 1:N 产品组件(Chanpin_zujian)”, // “组件(Zujian) 1:N 产品组件(Chanpin_zujian)”, // “板材(Bancai) 1:1 库存(Kucun)”, // “材质(Caizhi) 1:N 板材(Bancai)” // ] //} /** 优化后的关联解析函数(解决空值问题) @param {Object} data - 后端原始数据 @returns {Object} 处理后的完整关联数据 */ function __resolveDataReferences(data) { // 创建ID映射表(带空值保护) const idMaps = {}; Object.keys(data).forEach(key => { if (Array.isArray(data[key])) { idMaps[key] = new Map(); data[key].forEach(item => item.id && idMaps[key].set(item.id, item)); } }); // 通用关联解析方法(带安全检测) const resolveRef = (sourceArray, sourceKey, targetKey, refProperty) => { if (!Array.isArray(sourceArray)) return; sourceArray.forEach(item => { const refObj = item[refProperty]; if (refObj && refObj.id && idMaps[targetKey]) { const target = idMaps[targetKey].get(refObj.id); if (target) { // 建立正向引用 item[refProperty] = target; // 建立反向引用(自动创建关联数组) const reverseProp = sourceKey.endsWith('s') ? sourceKey.slice(0, -1) + '_list' : sourceKey + '_list'; if (!target[reverseProp]) target[reverseProp] = []; if (!target[reverseProp].includes(item)) { target[reverseProp].push(item); } } } }); }; // 处理特定关联(使用新安全方法) // 订单 ↔ 订单产品 if (data.dingdans && data.dingdan_chanpins) { resolveRef(data.dingdan_chanpins, ‘dingdans’, ‘dingdans’, ‘dingdan’); } // 订单 ↔ 订单组件 if (data.dingdans && data.dingdan_chanpin_zujians) { resolveRef(data.dingdan_chanpin_zujians, ‘dingdans’, ‘dingdans’, ‘dingdan’); } // 产品 ↔ 产品组件 if (data.chanpins && data.chanpin_zujians) { resolveRef(data.chanpin_zujians, ‘chanpins’, ‘chanpins’, ‘chanpin’); } // 组件 ↔ 产品组件 if (data.zujians && data.chanpin_zujians) { resolveRef(data.chanpin_zujians, ‘zujians’, ‘zujians’, ‘zujian’); } // 材质 ↔ 板材 if (data.caizhis && data.bancais) { resolveRef(data.bancais, ‘caizhis’, ‘caizhis’, ‘caizhi’); } // 板材 ↔ 库存(一对一) if (data.bancais && data.kucuns) { resolveRef(data.bancais, ‘kucuns’, ‘kucuns’, ‘kucun’); resolveRef(data.kucuns, ‘bancais’, ‘bancais’, ‘bancai’); // 反向引用 } // 板材 ↔ 木皮(mupi1/mupi2) if (data.bancais && data.mupis) { resolveRef(data.bancais, ‘mupis’, ‘mupis’, ‘mupi1’); resolveRef(data.bancais, ‘mupis’, ‘mupis’, ‘mupi2’); } // 订单产品 ↔ 产品 if (data.dingdan_chanpins && data.chanpins) { resolveRef(data.dingdan_chanpins, ‘chanpins’, ‘chanpins’, ‘chanpin’); } // 订单组件 ↔ 产品组件 if (data.dingdan_chanpin_zujians && data.chanpin_zujians) { resolveRef(data.dingdan_chanpin_zujians, ‘chanpin_zujians’, ‘chanpin_zujians’, ‘chanpin_zujian’); } // 订单组件 ↔ 板材 if (data.dingdan_chanpin_zujians && data.bancais) { resolveRef(data.dingdan_chanpin_zujians, ‘bancais’, ‘bancais’, ‘bancai’); } // 进货 ↔ 相关实体 if (data.jinhuos) { [‘dingdan’, ‘chanpin’, ‘zujian’, ‘bancai’, ‘user’].forEach(entity => { const plural = entity + ‘s’; if (data[plural]) { resolveRef(data.jinhuos, plural, plural, entity); } }); } return data; } function resolveDataReferences(data) { // 获取 data 对象的所有顶层键 const keys = Object.keys(data); // 遍历每个顶层键(如 users, posts 等) for (const key of keys) { const entities = data[key]; // 遍历该顶层键下的每个实体(如每个 user 或 post) for (const entity of entities) { // 遍历实体的每个属性 for (const attribute in entity) { if (entity.hasOwnProperty(attribute)) { var trpe=attribute.replace(/\d/g, ''); // 确保属性属于当前实体 if (Array.isArray(entity[attribute])) { if(data[trpe]==null){ trpe+="s" } // 如果属性是一个数组,则将数组中的每个 ID 替换为对应的实际对象 entity[attribute] = entity[attribute].map(item => data[trpe ]?.find(updateItem => updateItem.id === item.id) || item ); } else if (typeof entity[attribute] === "object" && entity[attribute] !== null) { // 如果属性是一个对象,则将其替换为对应的实际对象 entity[attribute] = data[trpe + "s"]?.find(updateItem => updateItem.id === entity[attribute].id); } } } } } console.log(data) return data; } /** 数据管理器类,负责与后端API通信并管理数据 */ class DataManager { constructor(baseUrl) { this.baseUrl = baseUrl; this.data = { bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], dingdan_chanpin_zujians: [], chanpin_zujians: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [] }; this.isSyncing = false; this.lastSync = null; // 回调注册表 this.callbacks = { // 全局回调 all: [], // 按实体类型分类的回调 bancais: [], dingdan: [], mupi: [], chanpin: [], kucun: [], dingdan_chanpin_zujian: [], chanpin_zujian: [], zujian: [], caizhi: [], dingdan_chanpin: [], user: [], // …其他实体 }; this.syncQueue = Promise.resolve(); } /** 获取所有数据 @returns {Promise} 是否成功 */ async fetchAll() { console.log(this) try { const response = await fetch(${this.baseUrl}/app/all); if (!response.ok) throw new Error(‘Network response was not ok’); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || ‘API error’); const resolvedData = resolveDataReferences(result.data); // 更新本地数据 Object.keys(this.data).forEach(key => { if (resolvedData[key]) { this.data[key] = resolvedData[key]; } }); this.lastSync = new Date(); // 关键改进:数据更新后触发刷新回调 this.triggerCallbacks(‘refresh’, ‘all’, this.data); return true; } catch (error) { console.error(‘Fetch error:’, error); // 触发错误回调 this.triggerCallbacks(‘fetch_error’, ‘all’, { error }); return false; } } /** 注册回调函数 @param {string} entity - 实体类型(如’bancai’)或’all’表示全局回调 @param {Function} callback - 回调函数,参数为(operation, data) */ registerCallback(entity, callback) { if (!this.callbacks[entity]) { this.callbacks[entity] = []; } this.callbacks[entity].push(callback); } /** 移除回调函数 @param {string} entity - 实体类型单数性质 @param {Function} callback - 要移除的回调函数 */ unregisterCallback(entity, callback) { if (!this.callbacks[entity]) return; const index = this.callbacks[entity].indexOf(callback); if (index !== -1) { this.callbacks[entity].splice(index, 1); } } /** 触发回调 @param {string} operation - 操作类型(‘add’, ‘update’, ‘delete’) @param {string} entity - 实体类型单数性质 @param {Object} data - 相关数据 */ triggerCallbacks(operation, entity, data) { // 触发全局回调 this.callbacks.all.forEach(cb => cb(operation, entity, data)); // 触发特定实体回调 if (this.callbacks[entity]) { this.callbacks[entity].forEach(cb => cb(operation, data)); } } /** 执行CRUD操作并触发回调 */ async crudOperation(operation, entity, data) { try { const response = await fetch(${this.baseUrl}/app/${operation}/${entity}, { method: ‘POST’, headers: {‘Content-Type’: ‘application/json’}, body: JSON.stringify(data) }); if (!response.ok) throw new Error(‘Network response was not ok’); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || ‘API error’); // 自动同步数据 this.syncData(); // 触发操作成功的回调 this.triggerCallbacks(operation, entity, data); return result; } catch (error) { console.error(‘CRUD error:’, error); // 触发操作失败的回调 this.triggerCallbacks(${operation}_error, entity, { data, error: error.message }); throw error; } } /** 执行CRUD操作 @param {string} operation - ‘add’, ‘delete’, ‘update’ @param {string} entity - 实体名称单数性质(小写) @param {Object} data - 要发送的数据 后端要求数据格式为{属性: “值”, 关联对象: {id:0}, 关联对象集: [{id:0}]} @returns {Promise} 响应结果 */ async crudOperation(operation, entity, data) { try { const response = await fetch(${this.baseUrl}/app/${operation}/${entity}, { method: ‘POST’, headers: {‘Content-Type’: ‘application/json’}, body: JSON.stringify(data) }); if (!response.ok) throw new Error('Network response was not ok'); const result = await response.json(); if (result.status !== 200) throw new Error(result.text || 'API error'); // 自动同步数据 this.syncQueue = this.syncQueue.then(async () => { await this.syncData(); // 同步完成后触发操作回调 this.triggerCallbacks(operation, entity, data); }); return result; } catch (error) { console.error(‘CRUD error:’, error); // 触发操作失败的回调 this.triggerCallbacks(${operation}_error, entity, { data, error: error.message }); throw error; } } /** 自动同步数据(防止频繁请求) */ async syncData() { if (this.isSyncing) { this.pendingSync = true; return; } this.isSyncing = true; try { await this.fetchAll(); } catch (error) { console.error(‘Sync failed:’, error); } finally { this.isSyncing = false; // 处理等待中的同步请求 if (this.pendingSync) { this.pendingSync = false; setTimeout(() => this.syncData(), 1000); } } } /** 添加实体 @param {string} entity - 实体名称单数性质 @param {Object} data - 实体数据 */ async addEntity(entity, data) { return this.crudOperation(‘add’, entity, data); } /** 更新实体 @param {string} entity - 实体名称单数性质 @param {Object} data - 实体数据(必须包含id) */ async updateEntity(entity, data) { return this.crudOperation(‘update’, entity, data); } /** 删除实体 @param {string} entity - 实体名称单数性质 @param {number} id - 实体ID */ async deleteEntity(entity, id) { return this.crudOperation(‘delete’, entity, {id}); } /** 新增方法:手动触发数据刷新 */ async refreshData() { return this.syncQueue = this.syncQueue.then(() => this.syncData()); } } export { DataManager }; // 创建单例实例 //const dataManager = new DataManager(‘http://127.0.0.1:8080/KuCun2’); //// 初始化时获取所有数据 //dataManager.fetchAll().then(() => { // console.log(‘Initial data loaded’); //}); // 导出数据对象,外部可以直接访问 data.bancais, data.dingdans 等 //export const data = dataManager.data; //// 导出操作方法 //export const addEntity = dataManager.addEntity.bind(dataManager); //export const updateEntity = dataManager.updateEntity.bind(dataManager); //export const deleteEntity = dataManager.deleteEntity.bind(dataManager); //export const fetchAll = dataManager.fetchAll.bind(dataManager); ------------------------ dingdan.html ------------------------ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>订单-产品-组件-板材查询系统</title> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <style> body { font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; } .query-section { background: #f8f9fa; border-radius: 6px; padding: 15px; margin-bottom: 25px; border-left: 4px solid #3498db; } .query-section h2 { color: #2c3e50; margin-top: 0; } label { display: block; margin-bottom: 8px; font-weight: bold; color: #34495e; } input, select, button { padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; margin-bottom: 15px; width: 100%; box-sizing: border-box; } button { background: #3498db; color: white; border: none; cursor: pointer; transition: background 0.3s; font-weight: bold; } button:hover { background: #2980b9; } .query-row { display: flex; gap: 15px; align-items: flex-end; } .query-row .form-group { flex: 1; } .results { background: white; border: 1px solid #eee; border-radius: 6px; padding: 15px; margin-top: 20px; max-height: 300px; overflow-y: auto; } .results h3 { margin-top: 0; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; } th { background: #f8f9fa; font-weight: bold; color: #2c3e50; } tr:hover td { background-color: #f1f8ff; } .loading { display: none; text-align: center; padding: 10px; color: #3498db; } .loading::after { content: "加载中..."; } </style> </head> <body> 订单-产品-组件-板材查询系统 查询条件 <label for="orderId">订单ID:</label> <input type="text" id="orderId" placeholder="输入订单ID"> <button id="searchButton">查询订单</button> 产品查询 <label for="productId">产品ID (从上方选择):</label> <select id="productId"></select> <button id="searchProductButton">查询产品</button> 组件查询 <label for="componentId">组件ID (从上方选择):</label> <select id="componentId"></select> <button id="searchComponentButton">查询组件</button> 板材查询 <script> // 全局数据存储 let globalData = { orders: [], products: [], components: [], boards: [] }; // 模拟后端数据存储 const mockData = { orders: [ {id: 1, number: "ORD-2023-001"}, {id: 2, number: "ORD-2023-002"} ], products: [ {id: 101, bianhao: "P-001", orderId: 1}, {id: 102, bianhao: "P-002", orderId: 1}, {id: 103, bianhao: "P-003", orderId: 2} ], components: [ {id: 201, name: "门板", productId: 101}, {id: 202, name: "抽屉面板", productId: 101}, {id: 203, name: "框架", productId: 102} ], boards: [ { id: 301, caizhi: {id: 401, name: "橡木"}, mupi1: {id: 501, name: "直纹橡木"}, houdu: 18.0, kucun: {id: 601, shuliang: 150} }, { id: 302, caizhi: {id: 402, name: "胡桃木"}, mupi1: {id: 502, name: "直纹胡桃木"}, houdu: 25.0, kucun: {id: 602, shuliang: 80} } ] }; // 通信函数实现 async function https(url, data, callback) { const defaultConfig = { contentType: 'application/json', dataType: 'json', timeout: 10000 }; // 显示对应区域的加载动画 const domain = url.split("/")[1]; const loadingElement = $("#" + domain + "Loading"); loadingElement.show(); try { // 在实际项目中是AJAX请求 // 这里用模拟数据代替 let response; // 模拟不同端点的响应 switch(url) { case "/order/search": // 模拟搜索延迟 await new Promise(resolve => setTimeout(resolve, 800)); response = new Information(200, "success", mockData.orders.filter( o => o.id == data.id )); break; case "/product/searchByOrder": await new Promise(resolve => setTimeout(resolve, 600)); response = new Information(200, "success", mockData.products.filter( p => p.orderId == data.orderId )); break; case "/component/searchByProduct": await new Promise(resolve => setTimeout(resolve, 500)); response = new Information(200, "success", mockData.components.filter( c => c.productId == data.productId )); break; case "/board/searchByComponent": await new Promise(resolve => setTimeout(resolve, 400)); // 在实际应用中,组件到板材可能有复杂的映射关系 response = new Information(200, "success", mockData.boards); break; default: response = new Information(404, "Endpoint not found", null); } console.log(response) console.log(mockData) // 在实际项目中: // const response = await $.ajax({ // ...defaultConfig, // url: BASE_URL + url, // method: 'POST', // data: JSON.stringify(data) // }); if (response.Status === 200) { callback?.(response.data); } else { handleBusinessError(response); } return response.data; } catch (error) { handleNetworkError(error, url); return null; } finally { loadingElement.hide(); } } function handleBusinessError(response) { console.error('业务错误:', response.text); alert(业务错误: ${response.text}); } function handleNetworkError(error, url) { console.error(网络请求错误: ${url}, error); alert(网络请求错误: ${url}, 请检查控制台); } // 通信信息类 class Information { constructor(status, text, data) { this.Status = status; this.text = text; this.data = data; } static NewSuccess(data) { return new Information(200, "success", data); } static Newfail(status, text, data) { return new Information(status, text, data); } } // 订单查询 $("#searchButton").click(() => { const orderId = $("#orderId").val(); if (!orderId) { alert("请输入订单ID"); return; } https("/order/search", { id: parseInt(orderId) }, function(orders) { globalData.orders = orders; renderOrderResults(); $("#productId").empty(); // 清除产品选择框 }); }); // 产品查询 $("#searchProductButton").click(() => { const productId = $("#productId").val(); if (!productId) { alert("请先查询订单并选择产品"); return; } // 查找当前订单ID const selectedOrderId = globalData.orders[0].id; https("/product/searchByOrder", { orderId: selectedOrderId }, function(products) { globalData.products = products; renderProductResults(); $("#componentId").empty(); // 清除组件选择框 }); }); // 组件查询 $("#searchComponentButton").click(() => { const componentId = $("#componentId").val(); if (!componentId) { alert("请先查询产品并选择组件"); return; } // 查找当前产品ID const selectedProductId = $("#productId").val(); https("/component/searchByProduct", { productId: parseInt(selectedProductId) }, function(components) { globalData.components = components; renderComponentResults(); // 自动发起板材查询 $("#boardLoading").show(); https("/board/searchByComponent", { componentId: parseInt(componentId) }, function(boards) { globalData.boards = boards; renderBoardResults(); }); }); }); // 渲染函数 function renderOrderResults() { const container = $("#orderResults"); container.empty(); if (!globalData.orders || globalData.orders.length === 0) { container.html("未找到相关订单"); return; } let html = 订单查询结果 (${globalData.orders.length}); html += ID 订单号 ; globalData.orders.forEach(order => { html += ${order.id} ${order.number} ; }); html += ; container.html(html); // 填充产品选择框 $("#productId").empty(); $("#productId").append('<option value="">-- 选择产品 --</option>'); globalData.products.forEach(product => { $("#productId").append(<option value="${product.id}">${product.bianhao}</option>); console.log(product) }); } function renderProductResults() { const container = $("#productResults"); container.empty(); if (!globalData.products || globalData.products.length === 0) { container.html("该订单下无产品"); return; } let html = 产品查询结果 (${globalData.products.length}); html += ID 产品编号 所属订单 ; globalData.products.forEach(product => { html += ${product.id} ${product.bianhao} ${product.orderId} ; }); html += ; container.html(html); // 填充组件选择框 $("#componentId").empty(); $("#componentId").append('<option value="">-- 选择组件 --</option>'); globalData.components.forEach(component => { $("#componentId").append(<option value="${component.id}">${component.name}</option>); }); } function renderComponentResults() { const container = $("#componentResults"); container.empty(); if (!globalData.components || globalData.components.length === 0) { container.html("该产品下无组件"); return; } let html = 组件查询结果 (${globalData.components.length}); html += ID 组件名称 所属产品 ; globalData.components.forEach(component => { html += ${component.id} ${component.name} ${component.productId} ; }); container.html(html); } function renderBoardResults() { const container = $("#boardResults"); container.empty(); if (!globalData.boards || globalData.boards.length === 0) { container.html("该组件无相关板材信息"); return; } let html = 板材查询结果 (${globalData.boards.length}); html += 板材ID 材质 木皮1 厚度(mm) 库存量 ; globalData.boards.forEach(board => { html += ${board.id} ${board.caizhi.name} ${board.mupi1.name} ${board.houdu} ${board.kucun ? board.kucun.shuliang : 0} ; }); container.html(html); } // 为选择框添加变更事件 $("#productId").change(function() { $("#searchProductButton").click(); }); $("#componentId").change(function() { $("#searchComponentButton").click(); }); </script> </body> </html> ------------------------ index.html ------------------------ <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>峤丞板材库存管理</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script> <script type="text/javascript" src="js/jsyilai.js"></script> <script type="module"> // 共享的DataManager类 import { DataManager } from './data/DataManager.js?'; document.addEventListener('DOMContentLoaded', async () => { try { // 创建实例并挂载到window window.dataManager = new DataManager('/KuCun2'); // 初始化数据 await window.dataManager.fetchAll(); console.log('Data Manager initialized successfully'); // 设置iframe通信 const iframe = document.getElementById('iframeid'); iframe.onload = () => { // 通知iframe数据已准备好 iframe.contentWindow.postMessage('DataManagerReady', '*'); }; // 如果iframe已经加载,立即发送消息 if (iframe.contentDocument.readyState === 'complete') { iframe.contentWindow.postMessage('DataManagerReady', '*'); } } catch (error) { console.error('Failed to initialize DataManager:', error); } }); </script> <style type="text/css"> *{ margin:0; padding:0; } .frame-header { height: 60px; background-color: #23262E; justify-content: space-between; } .frame-header-li{ font-family: Arial, Helvetica, sans-serif; font-size:40px; } .frame-ul{ } .frame-ul li{ border-style: solid; border-width:1px 0px 1px 0px; margin-top: 1px; height: 35px; text-align: center } #username{ position: absolute; right: 0; /* 靠右 */ } .frame-body { position: fixed; top: 60px; right: 0; bottom: 0; left: 0; display: flex; flex-direction: row; } .frame-side { scrollbar-width: none; /* firefox隐藏滚动条 */ -ms-overflow-style: none; /* IE 10+隐藏滚动条 */ overflow-x: hidden; overflow-y: auto; width: 200px; background-color:#9e5; } .frame-side::-webkit-scrollbar { display: none; /* Chrome Safari 隐藏滚动条*/ } .frame-main { flex-grow: 1; background-color:#fff; } .jiaoluo{ margin: auto; margin-right: 0px; } .menu { display: none; position: absolute; background-color: #f9f9f9; border: 1px solid #ccc; padding: 10px; list-style-type: none; margin: 0; z-index: 10; } </style> </head> <body> 峤丞木材仓库管理 峤丞木材仓库管理 个人资料 修改密码 注销 首页 板材查询 订单板材录入 人员管理 <iframe id="iframeid" name="main" src="main/bancai.html" width="100%" height="100%" frameborder="0"> </iframe> </body> </html> ------------------------ dingdan.js ------------------------ // 监听来自父窗口的消息 window.addEventListener('message', function(event) { // 确保消息来自父窗口且是 DataManagerReady 事件 if (event.source === window.parent && event.data === 'DataManagerReady') { initializeDataManager(); } }); // 初始化数据管理器 function initializeDataManager() { // 从父窗口获取 DataManager 实例 const dataManager = window.parent.dataManager; if (dataManager) { // 注册数据刷新回调 dataManager.registerCallback('all', function(operation, entity, data) { console.log("dhdhdh") // 当数据更新时刷新页面 updatePageData(dataManager); }); // 初始页面数据更新 updatePageData(dataManager); // 设置搜索功能 setupSearch(dataManager); } else { console.error('DataManager not available'); } } // 更新页面数据 function updatePageData(dataManager) { // 更新统计卡片 updateStats(dataManager); // 更新结果表格 updateTable(dataManager); // 更新最后更新时间 document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString(); } // 更新统计卡片数据 function updateStats(dataManager) { const data = dataManager.data; // 订单总数 document.getElementById('orderCount').textContent = data.dingdans?.length || 0; // 产品种类 document.getElementById('productCount').textContent = data.chanpins?.length || 0; // 板材库存种类 document.getElementById('materialCount').textContent = data.bancais?.length || 0; // 库存总量(所有库存数量之和) const totalStock = data.kucuns?.reduce((sum, kucun) => sum + kucun.shuliang, 0) || 0; document.getElementById('totalStock').textContent = totalStock; } // 更新结果表格 function updateTable(dataManager) { const tableBody = document.getElementById(‘resultBody’); tableBody.innerHTML = ‘’; // 清空现有内容 const data = dataManager.data; const results = []; let resultCount = 0; // 处理订单产品数据 if (data.dingdan_chanpins) { data.dingdan_chanpins.forEach(dc => { if (!dc.dingdan || !dc.chanpin) return; // 处理产品组件 if (dc.chanpin.chanpin_zujians) { dc.chanpin.chanpin_zujians.forEach(cz => { if (!cz.zujian || !cz.bancai) return; results.push({ orderNumber: dc.dingdan.number, productInfo: dc.chanpin.bianhao, productQuantity: dc.shuliang, component: cz.zujian.name, material: ${cz.bancai.id} (${cz.bancai.caizhi?.name || '未知材质'}), materialPerComponent: cz.one_howmany, materialOrderQuantity: dc.shuliang * cz.one_howmany, operation: '<button class="btn btn-sm btn-outline-primary">详情</button>' }); resultCount++; }); } }); } // 处理直接订单组件数据 if (data.dingdan_chanpin_zujians) { data.dingdan_chanpin_zujians.forEach(dcz => { if (!dcz.dingdan || !dcz.chanpin_zujian || !dcz.chanpin_zujian.zujian || !dcz.bancai) return; results.push({ orderNumber: dcz.dingdan.number, productInfo: dcz.chanpin_zujian.chanpin?.bianhao || '独立组件', productQuantity: dcz.shuliang, component: dcz.chanpin_zujian.zujian.name, material: ${dcz.bancai.id} (${dcz.bancai.caizhi?.name || '未知材质'}), materialPerComponent: dcz.chanpin_zujian.one_howmany, materialOrderQuantity: dcz.shuliang * dcz.chanpin_zujian.one_howmany, operation: '<button class="btn btn-sm btn-outline-primary">详情</button>' }); resultCount++; }); } // 填充表格 if (resultCount > 0) { document.getElementById('noResults').style.display = 'none'; results.forEach(row => { const tr = document.createElement('tr'); tr.innerHTML = ${row.orderNumber} ${row.productInfo} ${row.productQuantity} ${row.component} ${row.material} ${row.materialPerComponent} ${row.materialOrderQuantity} ${row.operation} ; tableBody.appendChild(tr); }); } else { document.getElementById('noResults').style.display = 'flex'; } // 更新结果计数 document.getElementById('resultCount').textContent = resultCount; } // 设置搜索功能 function setupSearch(dataManager) { // 订单搜索 ParseError: KaTeX parse error: Expected 'EOF', got '#' at position 3: ('#̲orderSearch').o…(this).val().toLowerCase(), 0); }); // 产品搜索 $('#productSearch').on('input', function() { filterTable($(this).val().toLowerCase(), 1); }); // 板材搜索 $('#materialSearch').on('input', function() { filterTable($(this).val().toLowerCase(), 4); }); // 木皮搜索 $('#woodSearch').on('input', function() { filterTable($(this).val().toLowerCase(), 4); // 假设材质信息在第4列 }); // 厚度搜索 $('#thicknessBtn').click(function() { const thickness = parseFloat($('#minThickness').val()); if (!isNaN(thickness)) { filterByThickness(thickness); } }); } // 表格过滤函数 function filterTable(searchTerm, columnIndex) { const rows = $(‘#resultBody tr’); let visibleCount = 0; rows.each(function() { const cellText = $(this).find(td:eq(${columnIndex})).text().toLowerCase(); const isMatch = cellText.includes(searchTerm); $(this).toggle(isMatch); // 高亮匹配文本 if (isMatch && searchTerm) { $(this).addClass('highlight'); } else { $(this).removeClass('highlight'); } if (isMatch) visibleCount++; }); // 更新结果计数 document.getElementById('resultCount').textContent = visibleCount; document.getElementById('noResults').style.display = visibleCount > 0 ? 'none' : 'flex'; } // 按厚度过滤 function filterByThickness(thickness) { const rows = $(‘#resultBody tr’); let visibleCount = 0; rows.each(function() { const materialText = $(this).find('td:eq(4)').text(); // 从文本中提取厚度值(假设格式为"ID (材质) 厚度mm") const match = materialText.match(/(\d+(\.\d+)?)\s*mm/); if (match) { const materialThickness = parseFloat(match[1]); const isMatch = !isNaN(materialThickness) && materialThickness >= thickness; $(this).toggle(isMatch); if (isMatch) visibleCount++; } else { $(this).hide(); } }); // 更新结果计数 document.getElementById('resultCount').textContent = visibleCount; document.getElementById('noResults').style.display = visibleCount > 0 ? 'none' : 'flex'; } // 如果直接加载test.html,尝试请求DataManager if (window.parent) { window.parent.postMessage(‘RequestDataManager’, ‘*’); } 修改dingdan.js,使之更契合数据原,板材显示要全面材质和木皮都要显示

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>万得数据库浏览器</title> <style> * { box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { margin: 0; padding: 20px; background-color: #f5f7fa; color: #333; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.1); padding: 25px; } h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; } .search-box { display: flex; margin-bottom: 20px; position: relative; } #search-input { flex: 1; padding: 12px 20px; font-size: 16px; border: 2px solid #3498db; border-radius: 50px; outline: none; transition: all 0.3s; } #search-input:focus { border-color: #2980b9; box-shadow: 0 0 10px rgba(52, 152, 219, 0.3); } #search-button { position: absolute; right: 5px; top: 5px; background: #3498db; color: white; border: none; padding: 8px 20px; border-radius: 50px; cursor: pointer; font-weight: bold; transition: background 0.3s; } #search-button:hover { background: #2980b9; } .results-container { display: none; margin-top: 30px; } .table-card { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 20px; overflow: hidden; border: 1px solid #e0e6ed; } .table-header { background: #3498db; color: white; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; } .table-name { font-weight: bold; font-size: 18px; } .export-btn { background: #27ae60; color: white; border: none; padding: 6px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; } .export-btn:hover { background: #219653; } .columns-table { width: 100%; border-collapse: collapse; } .columns-table th { background: #f8f9fa; text-align: left; padding: 12px 15px; border-bottom: 2px solid #e0e6ed; } .columns-table td { padding: 10px 15px; border-bottom: 1px solid #e0e6ed; } .columns-table tr:nth-child(even) { background-color: #f8f9fa; } .no-results { text-align: center; padding: 30px; color: #7f8c8d; font-style: italic; } .loading { display: none; text-align: center; padding: 20px; } .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error-message { color: #e74c3c; text-align: center; padding: 20px; display: none; } .export-dictionary-btn { background: linear-gradient(to right, #8e44ad, #9b59b6); color: white; border: none; padding: 12px 30px; border-radius: 50px; cursor: pointer; font-size: 16px; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.1); transition: all 0.3s ease; display: inline-block; margin: 20px auto; } .export-dictionary-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.2); background: linear-gradient(to right, #9b59b6, #8e44ad); } </style> </head> <body> 万得数据库浏览器 <input type="text" id="search-input" placeholder="输入表名、字段名或注释进行搜索..."> <button id="search-button">搜索</button> <button id="export-dictionary" class="export-dictionary-btn"> 导出数据库字典 </button> 正在搜索,请稍候... <script> // 在<script>标签内添加 document.getElementById('export-dictionary').addEventListener('click', function() { // 显示加载提示 const loadingElement = document.querySelector('.loading'); loadingElement.style.display = 'block'; loadingElement.querySelector('p').textContent = '正在生成数据库字典,请稍候...'; // 发起导出请求 window.location.href = '/qwind/export/dictionary'; // 2秒后隐藏加载提示(实际导出时间可能更长) setTimeout(() => { loadingElement.style.display = 'none'; }, 2000); }); document.addEventListener('DOMContentLoaded', function() { const searchInput = document.getElementById('search-input'); const searchButton = document.getElementById('search-button'); const resultsContainer = document.getElementById('results-container'); const loadingElement = document.querySelector('.loading'); const errorMessage = document.getElementById('error-message'); // 搜索按钮点击事件 searchButton.addEventListener('click', performSearch); // 回车键搜索 searchInput.addEventListener('keyup', function(event) { if (event.key === 'Enter') { performSearch(); } }); function performSearch() { const searchTerm = searchInput.value.trim(); if (!searchTerm) { resultsContainer.style.display = 'none'; errorMessage.style.display = 'block'; errorMessage.textContent = '请输入搜索内容'; return; } // 显示加载动画 loadingElement.style.display = 'block'; resultsContainer.style.display = 'none'; errorMessage.style.display = 'none'; // 发送AJAX请求 fetch(/qwind/search?q=${encodeURIComponent(searchTerm)}) .then(response => { if (!response.ok) { throw new Error('搜索失败'); } return response.json(); }) .then(data => { loadingElement.style.display = 'none'; if ('error' in data) { errorMessage.style.display = 'block'; errorMessage.textContent = 错误: ${data.error}; return; } // 处理搜索结果 displayResults(data); }) .catch(error => { loadingElement.style.display = 'none'; errorMessage.style.display = 'block'; errorMessage.textContent = 请求失败: ${error.message}; }); } function displayResults(data) { resultsContainer.innerHTML = ''; if (Object.keys(data).length === 0) { resultsContainer.innerHTML = 没有找到匹配的表或字段 ; resultsContainer.style.display = 'block'; return; } for (const [tableName, columns] of Object.entries(data)) { const tableCard = document.createElement('div'); tableCard.className = 'table-card'; tableCard.innerHTML = ${tableName} <button class="export-btn" data-table="${tableName}">导出数据</button> 字段名 注释/字段名 ${columns.map(col => ${col.name} ${col.comment} ).join('')} ; resultsContainer.appendChild(tableCard); } // 添加导出按钮事件 document.querySelectorAll('.export-btn').forEach(btn => { btn.addEventListener('click', function() { const tableName = this.getAttribute('data-table'); window.location.href = /qwind/export/${tableName}; }); }); resultsContainer.style.display = 'block'; } }); </script> </body> </html> from flask import Flask, render_template, request, send_file, jsonify import cx_Oracle import csv import io import os from flask import send_file from openpyxl import Workbook import io import pandas as pd app = Flask(__name__) # Oracle数据库配置 ORACLE_USER = os.getenv('ORACLE_USER', 'az_wasp') ORACLE_PASSWORD = os.getenv('ORACLE_PASSWORD', 'cmf+2011') ORACLE_DSN = os.getenv('ORACLE_DSN', '192.168.125.66/PDB_CMWADEV') def get_db_connection(): return cx_Oracle.connect(ORACLE_USER, ORACLE_PASSWORD, ORACLE_DSN) @app.route('/qwind') def index(): """显示搜索页面""" return render_template('index.html') @app.route('/qwind/search') def search_tables(): """根据查询条件搜索表和字段""" search_term = request.args.get('q', '').strip().upper() try: with get_db_connection() as conn: cursor = conn.cursor() # 查询匹配的表和字段 query = """ SELECT t.table_name, c.column_name, NVL(com.comments, c.column_name) AS column_comment FROM all_tables@wd_dblink_20230908 t JOIN all_tab_columns@wd_dblink_20230908 c ON t.table_name = c.table_name LEFT JOIN all_col_comments@wd_dblink_20230908 com ON c.table_name = com.table_name AND c.column_name = com.column_name WHERE (t.owner = 'WIND_FILESYNC' and c.owner = 'WIND_FILESYNC' and com.owner = 'WIND_FILESYNC') and t.table_name LIKE :1 OR c.column_name LIKE :1 OR com.comments LIKE :1 ORDER BY t.table_name, c.column_id """ # 添加通配符进行模糊匹配 search_pattern = f'%{search_term}%' cursor.execute(query, [search_pattern]) # 组织结果数据 results = {} for row in cursor: table_name, column_name, column_comment = row if table_name not in results: results[table_name] = [] results[table_name].append({ 'name': column_name, 'comment': column_comment }) return jsonify(results) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/qwind/export/') def export_table(table_name): """导出表数据为CSV""" try: with get_db_connection() as conn: cursor = conn.cursor() # 获取字段注释作为列名 col_query = """ SELECT col.column_name, NVL(com.comments, col.column_name) AS column_comment FROM all_tab_columns@wd_dblink_20230908 col LEFT JOIN all_col_comments@wd_dblink_20230908 com ON col.table_name = com.table_name AND col.column_name = com.column_name WHERE col.table_name = :1 and (col.owner = 'WIND_FILESYNC' and com.owner = 'WIND_FILESYNC') ORDER BY col.column_id """ cursor.execute(col_query, [table_name.upper()]) columns = cursor.fetchall() headers = [col[1] for col in columns] col_names = [col[0] for col in columns] # 构建数据查询(限制10万行) data_query = f""" SELECT {', '.join(col_names)} FROM wind_filesync.{table_name}@wd_dblink_20230908 WHERE ROWNUM <= 100000 """ print(data_query) cursor.execute(data_query) # 创建CSV内存文件 output = io.StringIO() writer = csv.writer(output) writer.writerow(headers) # 写入表头(字段注释) writer.writerows(cursor) # 写入数据 # 返回CSV文件 output.seek(0) return send_file( io.BytesIO(output.getvalue().encode()), mimetype='text/csv', as_attachment=True, attachment_filename=f'{table_name}_data.csv' ) except Exception as e: return f"Export failed: {str(e)}", 500 # 添加新路由 @app.route('/qwind/export/dictionary') def export_dictionary(): """导出数据库表和字段字典为Excel文件""" try: with get_db_connection() as conn: cursor = conn.cursor() # 执行提供的SQL查询 sql = """ SELECT t.table_name AS 表名, t2.comments AS 表中文名, t.column_name AS 字段名, t.comments AS 字段中文名, t4.data_type AS 字段类型, t4.data_length AS 字段长度, t3.num_rows AS "行数量(近似值)" FROM ALL_COL_COMMENTS@WD_DBLINK_20230908 t LEFT JOIN (SELECT * FROM ALL_TAB_COMMENTS@WD_DBLINK_20230908 WHERE owner = 'WIND_FILESYNC') t2 ON t.table_name = t2.table_name LEFT JOIN (SELECT * FROM ALL_TABLES@WD_DBLINK_20230908 t WHERE t.owner = 'WIND_FILESYNC') t3 ON t.table_name = t3.table_name LEFT JOIN (SELECT * FROM all_tab_columns@WD_DBLINK_20230908 WHERE owner = 'WIND_FILESYNC') t4 ON t.table_name = t4.table_name AND t.column_name = t4.column_name WHERE t.owner = 'WIND_FILESYNC' AND T.TABLE_NAME NOT LIKE '%TB_OBJECT%' AND T.TABLE_NAME NOT LIKE '%WINDHASH%' AND T.TABLE_NAME NOT LIKE '%TEST%' AND T.TABLE_NAME NOT LIKE '%==%' """ cursor.execute(sql) # 创建Excel工作簿 wb = Workbook() ws = wb.active ws.title = "数据库字典" # 添加表头 headers = [desc[0] for desc in cursor.description] for col_num, header in enumerate(headers, 1): ws.cell(row=1, column=col_num, value=header) # 添加数据 for row_num, row_data in enumerate(cursor, 2): for col_num, cell_value in enumerate(row_data, 1): ws.cell(row=row_num, column=col_num, value=cell_value) # 调整列宽 for col in ws.columns: max_length = 0 column = col[0].column_letter for cell in col: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) * 1.2 ws.column_dimensions[column].width = adjusted_width # 保存到内存 virtual_workbook = io.BytesIO() wb.save(virtual_workbook) virtual_workbook.seek(0) return send_file( virtual_workbook, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, attachment_filename='数据库字典.xlsx' ) except Exception as e: return f"导出字典失败: {str(e)}", 500 if __name__ == '__main__': #app.config['APPLICATION_ROOT'] = '/qwind' app.run(host='0.0.0.0', port=9093, debug=True) 我的前后端代码都放到了后端服务器,需要通过前端服务器的nginx进行访问,帮我配置nginx

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .layui-body { overflow-x: scroll; } </style> </head> <body> <form class="layui-form" action=""> <label class="layui-form-label">权限名</label> <input type="text" name="mod_name" required lay-verify="required" autocomplete="off" class="layui-input"> <label class="layui-form-label">用户组</label> <select name="user_group" lay-filter="required" id="user_group"> <option value=""></option> </select> <label class="layui-form-label">添加权限</label> <select name="add" lay-filter="required"> <option value=""></option> <option value="0">无</option> <option value="1">有</option> </select> <label class="layui-form-label">修改权限</label> <select name="set" lay-filter="required"> <option value=""></option> <option value="0">无</option> <option value="1">有</option> </select> <label class="layui-form-label">删除权限</label> <select name="del" lay-filter="required"> <option value=""></option> <option value="0">无</option> <option value="1">有</option> </select> <label class="layui-form-label">查询权限</label> <select name="get" lay-filter="required"> <option value=""></option> <option value="0">无</option> <option value="1">有</option> </select> </form> <button type="button" class="layui-btn layui-btn-normal" id="inquire">查询</button> <button type="button" class="layui-btn layui-btn-normal" id="reset">重置</button> 数据列表 <script type="text/html" id="toolbarDemo"> <button class="layui-btn layui-btn-sm" lay-event="add">修改</button> </script> <script src="../../layui/layui.js"></script> <script src="../../js/axios.min.js"></script> <script src="../../js/index.js"></script> <script src="../../js/base.js" charset="utf-8"></script> <script> var BaseUrl = baseUrl() layui.use(['element', 'layer', 'util'], function () { var element = layui.element , table = layui.table , layer = layui.layer , util = layui.util , $ = layui.$; let personInfo = JSON.parse(sessionStorage.personInfo) let user_group = personInfo.user_group let token = sessionStorage.token || null //头部事件 util.event('lay-header-event', { //左侧菜单事件 menuLeft: function (othis) { layer.msg('展开左侧菜单的操作', {icon: 0}); } , menuRight: function () { layer.open({ type: 1 , content: '处理右侧面板的操作' , area: ['260px', '100%'] , offset: 'rt' //右上角 , anim: 5 , shadeClose: true }); } }); // table 事件 table.render({ elem: '#newsClassification' , toolbar: true , url: BaseUrl + '/api/auth/get_list' , headers: { 'x-auth-token': token, 'Content-Type': 'application/json' } ,limits: [10] , page: { //支持传入 laypage 组件的所有参数(某些参数除外,如:jump/elem) - 详见文档 layout: ['limit', 'count', 'prev', 'page', 'next', 'skip'] //自定义分页布局 //,curr: 5 //设定初始在第 5 页 , groups: 1 //只显示 1 个连续页码 , first: false //不显示首页 , last: false //不显示尾页 } , cols: [[ {type: 'checkbox'} , {field: 'user_group', width: '10%', title: '用户组'} , {field: 'mod_name', width: '10%', title: '权限名称', templet: function (d) { if (d.path.replace('/'+d.table_name+'/','')=='table') { return ""+d.mod_name+"后台列表" } if (d.path.replace('/'+d.table_name+'/','')=='view') { return ""+d.mod_name+"后台详情" } if (d.path.replace('/'+d.table_name+'/','')=='list') { return ""+d.mod_name+"前台列表" } if (d.path.replace('/'+d.table_name+'/','')=='details') { return ""+d.mod_name+"前台详情" } if (d.path.replace('/'+d.table_name+'/','')=='edit') { return ""+d.mod_name+"前台编辑" } } } , { field: 'add', width: '10%', title: '添加权限', templet: function (d) { if (d.add == '0') { return "" } else { return "" } } } , { field: 'del', width: '10%', title: '删除权限', templet: function (d) { if (d.del == '0') { return "" } else { return "" } } } , { field: 'set', width: '10%', title: '修改权限', templet: function (d) { if (d.set == '0') { return "" } else { return "" } } } , { field: 'get', width: '10%', title: '查询权限', templet: function (d) { if (d.get == '0') { return "" } else { return "" } } } , { field: 'create_time', width: '15%', title: '新增时间', sort: true, templet: "{{layui.util.toDateString(d.create_time, 'yyyy-MM-dd HH:mm:ss')}}" } , { field: 'update_time', width: '15%', title: '更新时间', sort: true, templet: "{{layui.util.toDateString(d.update_time, 'yyyy-MM-dd HH:mm:ss')}}" } , {field: 'operate', title: '操作', toolbar: "#toolbarDemo"} ]] , page: true, request: { limitName: 'size' }, response: { statusName: 'code', //规定返回/Back的状态码字段为code statusCode: 200 //规定成功的状态码味200 }, parseData: function (res) { return { "code": 200, "msg": "", "count": res.result.count, "data": res.result.list } }, where: {like: 0, size: 10} }); layui.table.on('tool(newsClassification)', function (obj) { //注:tool是工具条事件名,test是table原始容器的属性 lay-filter="对应的值" var data = obj.data; //获得当前行数据 var layEvent = obj.event; //获得 lay-event 对应的值(也可以是表头的 event 参数对应的值) var tr = obj.tr; //获得当前行 tr 的DOM对象 if (layEvent == "add") { let auth_id = data.auth_id // window.location = ('./view.html?' + auth_id) layopen_dateil('./view.html?' + auth_id) } }); // 请求参数: let request = {like: 0, size: 10, page: 1, mod_name: '', user_group: '',add:'',set:'',del:'',get:'',orderby: 'create_time desc'} // 重置/Reset参数 let resetName = {like: 0, size: 10, page: 1, orderby: 'create_time desc'} fun('newsClassification', BaseUrl + '/api/auth/del', 'auth_id', request, resetName) async function get_list_user_group() { $.ajax({ url: BaseUrl + "/api/user_group/get_list", type: "get", async: false, success: function (data) { if (data && typeof data === 'string'){ data = JSON.parse(data); } if (data.result) { // 拿到单选框的父级节点 let select = document.querySelector("#user_group") let op1 = document.createElement('option') select.appendChild(op1) // 收集数据 长度 let count // 收集数据 数组 let arr = [] count = data.result.count arr = data.result.list for (let i = 0; i < arr.length; i++) { // 创建节点 let op = document.createElement('option') // 给节点赋值 op.innerHTML = arr[i].name op.value = arr[i].name // 新增/Add节点 select.appendChild(op) layui.form.render('select') } } } }); } get_list_user_group(); }); </script> </body> </html> 分析以上代码就行,各模块的作用,不要自己添加内容

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ nginx配置文件 user zceo; worker_processes auto; error_log /data/openresty/logs/error.log; pid /apps/openresty/run/nginx.pid; events { worker_connections 10240; } http { server_tokens off; include mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" | $http_accept | "$http_x_forwarded_for" | $host | ' '$request_time | $request_length | $upstream_addr | $upstream_status | $upstream_response_time | ' '$connection | $connection_requests '; access_log /data/openresty/logs/access.log main; sendfile on; tcp_nopush on; keepalive_timeout 65; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; #gzip_http_version 1.0; gzip_comp_level 3; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript; gzip_vary on; gzip_disable "MSIE [1-6]\."; gzip_proxied any; # 指定服务器名称hash表的框大小 server_names_hash_bucket_size 512; # header头缓冲区大小,cookie内容较大时可加大 client_header_buffer_size 3072k; large_client_header_buffers 8 3072k; #设置允许发布内容为8M client_max_body_size 500M; client_body_buffer_size 3072k; # websocket配置 map $http_upgrade $connection_upgrade { default upgrade; '' close; } #获取真实请求IP map $http_x_forwarded_for $clientRealIp { "" $remote_addr; ~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr; } map $http_user_agent $root_path { default /data/vue/bsp-workbenches-web; # 默认是 PC "~*Mobile" /data/vue/bsp-workbenches-h5; # 如果是移动设备,设置为 h5 路径 "~*Android" /data/vue/bsp-workbenches-h5; "~*iPhone" /data/vue/bsp-workbenches-h5; "~*iPad" /data/vue/bsp-workbenches-h5; "~*Windows Phone" /data/vue/bsp-workbenches-h5; } map $http_origin $cors_origin { default ""; ~^http?://(localhost|127\.0\.0\.1)(:\d+)?$ $http_origin; ~^https?://(.*\.)?gboss\.tech(?::\d+)?$ $http_origin; ~^https?://(.*\.)?ceboss\.cn(?::\d+)?$ $http_origin; ~^https?://(.*\.)?gmarketing\.cn(?::\d+)?$ $http_origin; } include vhosts/upstream.config; include vhosts/server.config; include vhosts/localhost.config; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ vhosts/upstream.config内容 upstream tsdasdaweb { server xxx.xxx.xxx.xxx:8080; keepalive 2000; } upstream csadasdweb { server xxx.xx.xxx.xxx:80; keepalive 2000; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ vhosts/server.config内容 多个下面类似配置 server { listen 80; server_name test-resource.gmarketing.tech; access_log /data/openresty/logs/access-test-resource.log main; error_log /data/openresty/logs/error-test-resource.log error; include /apps/openresty/nginx/conf/vhosts/test-resource-location.conf; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ vhosts/localhost.config内容: server { listen 80; server_name 127.0.0.1; location /ssadas { stub_status on; } location ^~ /csasdasdweb/ { proxy_pass http://asdasdadweb; include /apps/openresty/nginx/conf/vhosts/common-proxy.conf; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test-resource-location.conf内容 location ^~ /file-api/ { proxy_pass http://file-api/; include /apps/openresty/nginx/conf/vhosts/common-x-form.conf; include /apps/openresty/nginx/conf/vhosts/common-proxy.conf; } location /file { include /apps/openresty/nginx/conf/vhosts/file-from.conf; alias /data/share/files/; } location /robots.txt { alias /data/vue/robots/robots.txt; add_header Content-Type text/plain; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /apps/openresty/nginx/conf/vhosts/common-x-form.conf内容 add_header Content-Security-Policy "frame-ancestors https://*.ceboss.cn https://*.gmarketing.tech https://*.gboss.tech;"; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /apps/openresty/nginx/conf/vhosts/common-proxy.conf内容 proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 6000; proxy_read_timeout 6000; proxy_send_timeout 6000; proxy_buffer_size 3072k; proxy_buffers 16 3072k; proxy_busy_buffers_size 3072k; proxy_http_version 1.1; proxy_set_header Connection "Keep-Alive"; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /apps/openresty/nginx/conf/vhosts/file-from.conf内容 # CORS 配置 if ($http_origin ~* (https?://([^/]+\.)?(gmarketing\.tech|gboss\.tech|ceboss\.cn)(:\d+)?$)) { set $cors "true"; } if ($http_origin ~* (http?://([^/]+\.)?(gmarketing\.tech|gboss\.tech|ceboss\.cn|localhost:5173|localhost|localhost:5177)(:\d+)?$)) { set $cors "true"; } # 处理 .tiff 文件的 CORS 头 location ~* \.tiff$ { if ($cors = "true") { add_header 'Access-Control-Allow-Origin' "$http_origin" always; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; add_header 'Access-Control-Allow-Credentials' 'true' always; } try_files $uri =404; } # 核心 CORS 头 add_header 'Access-Control-Allow-Origin' "$http_origin" always; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; add_header 'Access-Control-Allow-Credentials' 'true' always; # 预检请求处理 if ($request_method = 'OPTIONS') { add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 后端页面文件代码: public class SecureUrlGenerator { /** * ??? Nginx secure_link ????? URL. * * @param uri ????????? (??: "/images/secret/document.jpg") * @param secretKey ? Nginx ?????????? * @param durationInSeconds ???????(?) * @return ?????? URL (????) */ public static String generate(String uri, String secretKey, int durationInSeconds) { try { // 1. ??????? (Unix apoch time in seconds) long expires = System.currentTimeMillis() / 1000 + durationInSeconds; // 2. ????????? // ????? Nginx ? secure_link_md5 ?????????? // ??: "$secure_link_expires$uri secret_key" String stringToSign = expires + uri + " " + secretKey; // 3. ?? MD5 ?? MessageDigest md = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md.digest(stringToSign.getBytes(StandardCharsets.UTF_8)); // 4. ? MD5 ???? URL ??? Base64 ?? // Nginx ? secure_link ????????: // - ?? '-' ?? '+' // - ?? '_' ?? '/' // - ????? padding '=' // Java 8 ? Base64.getUrlEncoder().withoutPadding() ?????????? String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(md5Bytes); // 5. ????? URL // ??: /path/to/resource?md5=<signature>&expires=<timestamp> return String.format("%s?sign=%s&t=%d", uri, signature, expires); } catch (NoSuchAlgorithmException e) { // ??????,?????????????????? e.printStackTrace(); throw new RuntimeException("MD5 apoch_time is not available.", e); } } /** * ???,??????? */ public static void main(String[] args) { // --- ???? --- https://pre-file.gmarketing.tech/file/sm9pS0sF6oK/20250611/2b2e90fca62e41b4a048b424dfb75234.jpg String resourceUri = "24Gy8U0OlRbQ/20250616/04fa62de6149b4d11e79fce3ac7638b4.jpg"; String mySecretKey = "q8xyMiuJJAomkrk"; // ????????,??Nginx???? int validDuration = 43200; // ?????12?? (43200?) // --- ???? URL --- String signedUrl = generate(resourceUri, mySecretKey, validDuration); // --- ???? --- System.out.println("?? URI: " + resourceUri); System.out.println("????? URL: " + signedUrl); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 请结合提供内容,修改nginx配置完成如下需求 测试环境Nginx 新的文件服务https://test-resource.gmarketing.tech 只允许通过含有码(sm9pS0sF6oK|24Gy8U0OlRbQ)的链接,并且需要对Referer进行验证,只允许通过含有以下Referer的链接访问【test-wb.gboss.tech test-console.gmarketing.tech test-sw.ceboss.cn】 测试环境Nginx 新的文件服务https://test-resource.gmarketing.tech 需要对sign和时间戳进行校验,加密Key是(q8xyMiuJJAomkrk 请勿泄露),加密方式见截图,加密后的链接示例(https://test-resource.gmarketing.tech/file/24Gy8U0OlRbQ/20250616/04fa62de6149b4d11e79fce3ac7638b4.jpg?sign=ei6aaK9I3H-9ph88m4mrpQ&t=1750103291)

大家在看

recommend-type

基于ADS的微带滤波器设计

微波滤波器是用来分离不同频率微波信号的一种器件。它的主要作用是抑制不需要的信号,使其不能通过滤波器,只让需要的信号通过。在微波电路系统中,滤波器的性能对电路的性能指标有很大的影响,因此如何设计出一个具有高性能的滤波器,对设计微波电路系统具有很重要的意义。
recommend-type

Pixhawk4飞控驱动.zip

已安装成功
recommend-type

ztecfg中兴配置加解密工具3.0版本.rar

中兴光猫配置文件加解密工具3.0 .\ztecfg.exe -d AESCBC -i .\(要解密的文件名)db_user_cfg.xml -o (解密后文件名)123.cfg
recommend-type

配置车辆-feedback systems_an introduction for scientists and engineers

5.2 道路场景 从界面右侧的道路场景列表中,双击载入所需的道路场景(如 Fld_FreeWay)。 PanoSim提供了 ADAS标准(ISO、Euro NCAP)典型场景库,如高速公路、乡村道路、 城镇、坡道、换道、停车场、高速出入口等。我们可根据用户需要定制丰富场景库。 PanoSim提供专门的道路场景设计工具,可通过常用工具栏\Tools\FieldBuilder 来创建自己的道路场景。 5.3 天气和光照 从右侧的实验环境列表栏中,通过双击载入所需的实验天气和光照。天气有多 云、雾天、雨天、雪天、晴天,光照有白天和夜晚,相关实验信息(如所选场景、天 气、车辆等),可在左侧实验信息栏中查看。 5.4 配置车辆 点击“Forward”,进入实验参数设置主界面(图 5-2)。
recommend-type

xilinx.com_user_IIC_AXI_1.0.zip

可以直接用在vivado 2017.4版本里。查看各个寄存器就知道用来干什么了,一号寄存器分频系数,二号的start、stop信号,三号寄存器8bit数据,四号寄存器只读,返回IIC状态和ACK信号,其中二号的一个bit可以用来不等待从机ACK,方便使用。

最新推荐

recommend-type

开发界面语义化:声控 + 画图协同生成代码.doc

开发界面语义化:声控 + 画图协同生成代码.doc
recommend-type

LABVIEW与三菱PLC通信:实现数据批量读写的高效库解决方案

如何通过LabVIEW与三菱PLC建立高效的通信桥梁,实现数据批量读写。首先概述了LabVIEW和三菱PLC的基本概念及其在工业自动化中的重要性。接着重点讲解了利用Modbus RTU协议构建通信连接的具体步骤和技术细节,包括初始化通信、发送读写请求、处理响应数据和关闭连接等功能。文中还提供了一个简化的代码示例,展示了如何在LabVIEW环境中实现这一过程。最后对这项技术进行了总结和展望,强调其在提高数据交互效率方面的潜力以及未来的广泛应用前景。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是那些熟悉LabVIEW或三菱PLC的人士。 使用场景及目标:适用于需要频繁进行数据交互的工业控制系统,如生产线监控、设备状态监测等场合。主要目的是提升数据传输的速度和可靠性,从而优化整个系统的运行效率。 阅读建议:读者可以通过本文深入了解LabVIEW与三菱PLC通信的实现方法,掌握批量数据读写库的设计思路,并将其应用于实际工程项目中。建议边阅读边尝试动手实践相关代码,以便更好地理解和吸收所学知识。
recommend-type

欧姆龙PLC NJ系列模切机程序:高级伺服运动与张力控制的应用实例

欧姆龙PLC NJ系列模切机项目的编程细节及其关键技术。主要内容涵盖12轴EtherCAT总线伺服运动控制,包括回零、点动、定位和速度控制;张力控制采用PID算法并进行收放卷径计算;隔膜自动纠偏控制利用模拟量数据平均化处理;同步运动控制实现凸轮表追剪和裁切;以及结构化编程和ST语言功能块的使用。项目结构规范,注释详尽,有助于理解和维护代码。通过本项目的学习,可以掌握PLC高端复杂应用的实际操作技能。 适合人群:从事工业自动化领域的工程师和技术人员,特别是对PLC编程和伺服运动控制有浓厚兴趣的人群。 使用场景及目标:适用于需要深入了解PLC编程技巧和自动化控制系统原理的技术人员。目标是提升编程能力和对复杂自动化系统的工作机制的理解。 其他说明:本文不仅提供具体的编程指导,还强调了项目管理和代码规范的重要性,为读者提供了全面的学习体验。
recommend-type

大班主题性区域活动计划表.doc

大班主题性区域活动计划表.doc
recommend-type

高校教研室工作计划.doc

高校教研室工作计划.doc
recommend-type

Python程序TXLWizard生成TXL文件及转换工具介绍

### 知识点详细说明: #### 1. 图形旋转与TXL向导 图形旋转是图形学领域的一个基本操作,用于改变图形的方向。在本上下文中,TXL向导(TXLWizard)是由Esteban Marin编写的Python程序,它实现了特定的图形旋转功能,主要用于电子束光刻掩模的生成。光刻掩模是半导体制造过程中非常关键的一个环节,它确定了在硅片上沉积材料的精确位置。TXL向导通过生成特定格式的TXL文件来辅助这一过程。 #### 2. TXL文件格式与用途 TXL文件格式是一种基于文本的文件格式,它设计得易于使用,并且可以通过各种脚本语言如Python和Matlab生成。这种格式通常用于电子束光刻中,因为它的文本形式使得它可以通过编程快速创建复杂的掩模设计。TXL文件格式支持引用对象和复制对象数组(如SREF和AREF),这些特性可以用于优化电子束光刻设备的性能。 #### 3. TXLWizard的特性与优势 - **结构化的Python脚本:** TXLWizard 使用结构良好的脚本来创建遮罩,这有助于开发者创建清晰、易于维护的代码。 - **灵活的Python脚本:** 作为Python程序,TXLWizard 可以利用Python语言的灵活性和强大的库集合来编写复杂的掩模生成逻辑。 - **可读性和可重用性:** 生成的掩码代码易于阅读,开发者可以轻松地重用和修改以适应不同的需求。 - **自动标签生成:** TXLWizard 还包括自动为图形对象生成标签的功能,这在管理复杂图形时非常有用。 #### 4. TXL转换器的功能 - **查看.TXL文件:** TXL转换器(TXLConverter)允许用户将TXL文件转换成HTML或SVG格式,这样用户就可以使用任何现代浏览器或矢量图形应用程序来查看文件。 - **缩放和平移:** 转换后的文件支持缩放和平移功能,这使得用户在图形界面中更容易查看细节和整体结构。 - **快速转换:** TXL转换器还提供快速的文件转换功能,以实现有效的蒙版开发工作流程。 #### 5. 应用场景与技术参考 TXLWizard的应用场景主要集中在电子束光刻技术中,特别是用于设计和制作半导体器件时所需的掩模。TXLWizard作为一个向导,不仅提供了生成TXL文件的基础框架,还提供了一种方式来优化掩模设计,提高光刻过程的效率和精度。对于需要进行光刻掩模设计的工程师和研究人员来说,TXLWizard提供了一种有效的方法来实现他们的设计目标。 #### 6. 系统开源特性 标签“系统开源”表明TXLWizard遵循开放源代码的原则,这意味着源代码对所有人开放,允许用户自由地查看、修改和分发软件。开源项目通常拥有活跃的社区,社区成员可以合作改进软件,添加新功能,或帮助解决遇到的问题。这种开放性促进了技术创新,并允许用户根据自己的需求定制软件。 #### 7. 压缩包子文件的文件名称列表 文件名称列表中的“txlwizard-master”可能指的是TXLWizard项目的主版本库或主分支。这个名称表明了这是项目源代码的中心点,其他开发者会从这个主分支拉取代码进行合作开发或部署。以“-master”结尾通常是版本控制系统中表示主要开发线路的常见约定,例如Git中的master(现在更常被称为main)分支。 通过这些知识点的详细解释,我们可以看到TXLWizard不仅是一个用于生成TXL文件的工具,它还整合了一系列的功能,使得电子束光刻掩模的设计工作更为高效和直观。同时,作为一个开源项目,它能够借助社区的力量不断进步,为用户带来更多的便利和创新。
recommend-type

【创新图生成:扣子平台的技术前沿与创新思维】:引领图像生成技术的新潮流

# 1. 扣子平台的图像生成技术概述 在当今数字化时代,图像生成技术正成为推动技术创新和社会变革的关键力量。扣子平台正是这一技术革新的代表。本章将对扣子平台的图像生成技术进行一个全面概述,探究它如何将理论技术转化为实际应用,并带来显著的社会影响。 扣子平台运用了先进的图像生成技术,将现实世界和数字空间无缝融合,打造了沉浸式的视觉体验。其技术的核心是通过高度复杂的算法模型,快速生成高质
recommend-type

海康威视机器视觉工程师考核

<think>好的,我现在需要帮助用户了解海康威视机器视觉工程师的考核内容和要求。根据用户提供的引用资料,特别是引用[1]和[2],里面提到了考核素材包分为初级和中级,涵盖理论、算法、应用案例等。首先,我要整理这些信息,确保结构清晰,符合用户要求的格式。 接下来,我需要确认素材包的具体内容,比如初级和中级的不同点。引用[2]提到初级包含基础理论、算法实现和实际案例,中级则增加复杂算法和项目分析。这部分需要分点说明,方便用户理解层次。 另外,用户可能想知道如何准备考核,比如下载素材、学习顺序、模拟考核等,引用[2]中有使用说明和注意事项,这部分也要涵盖进去。同时要注意提醒用户考核窗口已关闭,
recommend-type

Linux环境下Docker Hub公共容器映像检测工具集

在给出的知识点中,我们需要详细解释有关Docker Hub、公共容器映像、容器编排器以及如何与这些工具交互的详细信息。同时,我们会涵盖Linux系统下的相关操作和工具使用,以及如何在ECS和Kubernetes等容器编排工具中运用这些检测工具。 ### Docker Hub 和公共容器映像 Docker Hub是Docker公司提供的一项服务,它允许用户存储、管理以及分享Docker镜像。Docker镜像可以视为应用程序或服务的“快照”,包含了运行特定软件所需的所有必要文件和配置。公共容器映像指的是那些被标记为公开可见的Docker镜像,任何用户都可以拉取并使用这些镜像。 ### 静态和动态标识工具 静态和动态标识工具在Docker Hub上用于识别和分析公共容器映像。静态标识通常指的是在不运行镜像的情况下分析镜像的元数据和内容,例如检查Dockerfile中的指令、环境变量、端口映射等。动态标识则需要在容器运行时对容器的行为和性能进行监控和分析,如资源使用率、网络通信等。 ### 容器编排器与Docker映像 容器编排器是用于自动化容器部署、管理和扩展的工具。在Docker环境中,容器编排器能够自动化地启动、停止以及管理容器的生命周期。常见的容器编排器包括ECS和Kubernetes。 - **ECS (Elastic Container Service)**:是由亚马逊提供的容器编排服务,支持Docker容器,并提供了一种简单的方式来运行、停止以及管理容器化应用程序。 - **Kubernetes**:是一个开源平台,用于自动化容器化应用程序的部署、扩展和操作。它已经成为容器编排领域的事实标准。 ### 如何使用静态和动态标识工具 要使用这些静态和动态标识工具,首先需要获取并安装它们。从给定信息中了解到,可以通过克隆仓库或下载压缩包并解压到本地系统中。之后,根据需要针对不同的容器编排环境(如Dockerfile、ECS、Kubernetes)编写配置,以集成和使用这些检测工具。 ### Dockerfile中的工具使用 在Dockerfile中使用工具意味着将检测工具的指令嵌入到构建过程中。这可能包括安装检测工具的命令、运行容器扫描的步骤,以及将扫描结果集成到镜像构建流程中,确保只有通过安全和合规检查的容器镜像才能被构建和部署。 ### ECS与Kubernetes中的工具集成 在ECS或Kubernetes环境中,工具的集成可能涉及到创建特定的配置文件、定义服务和部署策略,以及编写脚本或控制器来自动执行检测任务。这样可以在容器编排的过程中实现实时监控,确保容器编排器只使用符合预期的、安全的容器镜像。 ### Linux系统下的操作 在Linux系统下操作这些工具,用户可能需要具备一定的系统管理和配置能力。这包括使用Linux命令行工具、管理文件系统权限、配置网络以及安装和配置软件包等。 ### 总结 综上所述,Docker Hub上的静态和动态标识工具提供了一种方法来检测和分析公共容器映像,确保这些镜像的安全性和可靠性。这些工具在Linux开发环境中尤为重要,因为它们帮助开发人员和运维人员确保他们的容器映像满足安全要求。通过在Dockerfile、ECS和Kubernetes中正确使用这些工具,可以提高应用程序的安全性,减少由于使用不安全的容器镜像带来的风险。此外,掌握Linux系统下的操作技能,可以更好地管理和维护这些工具,确保它们能够有效地发挥作用。
recommend-type

【扣子平台图像艺术探究:理论与实践的完美结合】:深入学习图像生成的艺术

# 1. 图像艺术的理论基础 艺术领域的每一个流派和技巧都有其理论基础。在图像艺术中,理论基础不仅是对艺术表现形式的认知,也是掌握艺术创作内在逻辑的关键。深入理解图像艺术的理论基础,能够帮助艺术家们在创作过程中更加明确地表达自己的艺术意图,以及更好地与观众沟通。 图像艺术的理论