瀏覽代碼

优化JNPF快速开发平台源码配置及启动顺序

zhaojinyu 9 小時之前
父節點
當前提交
b967814be8

+ 1132 - 0
技术分享/JNPF快速开发平台源码配置及启动顺序.md

@@ -0,0 +1,1132 @@
+# JNPF 快速开发平台 — 源码配置顺序及启动指南
+
+> **环境要求**:Java 21、Maven 3.9.8
+
+---
+
+## 1. Nexus 私有 Maven 仓库配置
+
+使用文档包提供的 `nexus-3.20.1-01-win64` 搭建本地 Maven 私有库。
+
+> **参考文档**:[Nexus 安装配置教程](https://blog.csdn.net/z793397795/article/details/121771214)
+
+| 配置项 | 值 |
+|---|---|
+| 访问地址 | `http://127.0.0.1:9999` |
+| 用户名 | `admin` |
+| 密码 | `admin123` |
+
+**Nexus 管理命令(管理员终端执行)**:
+
+```powershell
+# 安装
+nexus.exe /install
+
+# 启动
+nexus.exe /start
+
+# 停止
+nexus.exe /stop
+
+# 卸载
+nexus.exe /uninstall
+```
+
+> **注意**:首次启动需要 3-5 分钟。
+
+---
+
+## 2. Maven 配置
+
+1. 本地安装 Maven 3.9.8
+2. 将文档包中提供的 `settings.xml` 替换至本地 Maven 对应目录
+3. 若下载依赖报错,可修改获取依赖文件的 URL
+
+---
+
+## 3. 数据库创建
+
+> 字符集:`utf8mb4`,排序规则:`utf8mb4_general_ci`
+
+### 3.1 平台数据库 `jnpf_init`
+
+导入 `jnpf-database/MySQL/jnpf_db_init.sql`。
+
+若需纯净数据库(不含 Demo 示例),以新建查询方式导入 `jnpf_dbnull_init.sql`。
+
+> 如有更新脚本(Update 目录下),请按日期顺序依次执行。
+
+### 3.2 系统调度数据库 `jnpf_xxjob`
+
+导入 `jnpf-database/MySQL/jnpf_xxjob_init.sql`。
+
+### 3.3 流程数据库 `jnpf_flow`
+
+导入 `jnpf-database/MySQL/jnpf_flow_init.sql`。
+
+### 3.4 租户数据库 `jnpf_tenant_init`(单体版无需)
+
+导入 `jnpf-database/MySQL/jnpf_tenant_init.sql`。
+
+---
+
+## 4. 源码配置及打包
+
+> **重要**:以下模块需严格按顺序构建,注意依赖关系。
+
+### 4.1 构建 `jnpf-common` 公共模块
+
+#### 4.1.1 安装 `jnpf-common-core`
+
+在 IDEA 中,双击右侧 **Maven** > `jnpf-common` > `jnpf-boot-common` > `jnpf-common-core` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 4.1.2 安装 `jnpf-dependencies`
+
+在 IDEA 中,双击右侧 **Maven** > `jnpf-common` > `jnpf-dependencies` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 4.1.3 安装 `file-core-starter`
+
+IDEA 打开 `jnpf-file-core-starter` 项目,双击右侧 **Maven** > `jnpf-file-core-starter` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 4.1.4 安装 `scheduletask`
+
+IDEA 打开 `jnpf-scheduletask` 项目,双击右侧 **Maven** > `jnpf-scheduletask` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 4.1.5 安装 `jnpf-common` 总包
+
+在 IDEA 中,双击右侧 **Maven** > `jnpf-common` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### Redis 配置修复
+
+配置文件中 Redis 部分若报错,请调整为以下格式:
+
+```yaml
+# redis 单机模式
+data:
+  redis:
+    database: 1
+    host: usky-cloud-redis
+    port: 6379
+    password: 123456
+    timeout: 3000
+    lettuce:            # Lettuce 为 Redis 的 Java 驱动包
+      pool:
+        max-active: 8   # 连接池最大连接数
+        max-wait: -1ms  # 连接池最大阻塞等待时间(-1 表示无限制)
+        min-idle: 0     # 连接池最小空闲连接
+        max-idle: 8     # 连接池最大空闲连接
+```
+
+---
+
+### 4.2 `jnpf-datareport` 报表后端
+
+#### 环境配置
+
+1. 打开 `ureport2-console/src/main/resources/application.yml`
+2. 修改以下配置:
+   - 端口配置
+   - 数据库配置和 Redis 配置
+   - 是否开启多租户
+
+#### 数据库配置示例
+
+```yaml
+# MySQL 数据库
+datasource:
+  db-type: MySQL
+  host: 127.0.0.1
+  port: 3306
+  db-name: jnpf_init
+  username: dbuser
+  password: dbpasswd
+  db-schema:
+  prepare-url:
+```
+
+#### 启动
+
+运行 `ureport2-console/src/main/java/com.bstek.ureport.console/DataReportApplication`。
+
+打包方式:直接 Maven 打包。
+
+---
+
+### 4.3 `jnpf-java-datareport-univer` Java Univer 报表后端
+
+#### 4.3.1 数据库
+
+使用数据库 `jnpf_init`。
+
+#### 4.3.2 导入依赖
+
+详见 `jnpf-java-datareport-univer-core` 项目中的 `README.md`。
+
+#### 4.3.3 IDEA 打开 `jnpf-java-datareport-univer-core` 项目
+
+**选择是否加密**(加密与否影响 `jnpf-java-datareport-univer` 项目的启动方式):
+
+| 场景 | 操作 |
+|---|---|
+| **不使用加密** | IDEA 右侧 **Maven** > **Profiles** 去除勾选 `encrypted`,刷新 Maven |
+| **使用加密** | IDEA 右侧 **Maven** > **Profiles** 勾选 `encrypted`,刷新 Maven |
+
+**安装加密插件**:双击右侧 **Maven** > `jnpf-datareport-univer-core` > `clean`,将自动安装加密打包插件。
+
+**本地安装**:双击右侧 **Maven** > `jnpf-datareport-univer-core` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 4.3.4 修改配置并打包
+
+**场景一:`jnpf-java-datareport-univer-core` 项目未使用加密**
+
+1. 在 IDEA 中,右侧 **Maven** > **Profiles** 去除勾选 `encrypted`,刷新 Maven
+2. 找到 `jnpf-datareport-univer-admin/src/main/java/jnpf/ReportUniverApplication.java`,右键运行
+
+**场景二:`jnpf-java-datareport-univer-core` 项目使用加密**
+
+1. 在 IDEA 中,右侧 **Maven** > **Profiles** 勾选 `encrypted`,刷新 Maven
+2. 双击右侧 **Maven** > `jnpf-datareport-univer` > `clean`,将自动安装加密打包插件,并创建 `jnpf-datareport-univer-entity/target/copylib` 复制依赖包用于下一步运行
+
+---
+
+### 4.4 `jnpf-workflow` 工作流引擎后端
+
+#### 4.4.1 数据库
+
+> 字符集:`utf8mb4`,排序规则:`utf8mb4_general_ci`
+
+创建 `jnpf_flow` 数据库,导入 `jnpf-database/MySQL/jnpf_flow_init.sql`。
+
+#### 4.4.2 导入依赖
+
+详见 `jnpf-workflow-core` 项目中的 `README.md`。
+
+#### 4.4.3 打开 `jnpf-workflow-core` 项目
+
+**选择是否加密**(加密与否影响 `jnpf-workflow` 项目的启动方式):
+
+| 场景 | 操作 |
+|---|---|
+| **不使用加密** | IDEA 右侧 **Maven** > **Profiles** 去除勾选 `encrypted`,刷新 Maven |
+| **使用加密** | IDEA 右侧 **Maven** > **Profiles** 勾选 `encrypted`,刷新 Maven |
+
+**安装加密插件**:双击右侧 **Maven** > `jnpf-workflow-core` > `clean`,将自动安装加密打包插件。
+
+**本地安装**:双击右侧 **Maven** > `jnpf-workflow-core` > **Lifecycle** > `install`,安装至本地仓库。
+
+#### 项目配置
+
+编辑 `jnpf-workflow-admin/src/main/resources/application.yml`。
+
+**场景一:`jnpf-workflow-core` 项目未使用加密**
+
+1. IDEA 右侧 **Maven** > **Profiles** 去除勾选 `encrypted`,刷新 Maven
+2. 找到 `jnpf-workflow-admin/src/main/java/jnpf/JnpfFlowableApplication.java`,右键运行
+
+**场景二:`jnpf-workflow-core` 项目使用加密**
+
+1. IDEA 右侧 **Maven** > **Profiles** 勾选 `encrypted`,刷新 Maven
+2. 双击右侧 **Maven** > `jnpf-workflow` > `clean`,将自动安装加密打包插件,并创建 `jnpf-workflow-admin/target/copylib` 复制依赖包用于下一步运行
+
+---
+
+### 4.5 `jnpf-scheduletask` 调度服务端
+
+> **前置条件**:`jnpf-common` 公共模块已构建成功。
+
+1. 在 IDEA 左侧 **Project** 中,右键 `jnpf-scheduletask` > `xxl-job-admin` > `pom.xml`,选择 **Add as Maven Project**,将 `xxl-job-admin` 转为 Maven 项目
+2. 双击右侧 **Maven** > `xxl-job-admin` > **Lifecycle** > `package`
+3. 将 `/xxl-job-admin/target/xxl-job-admin-5.0.0-RELEASE.jar` 上传至服务器部署
+
+---
+
+### 4.6 `jnpf-java-boot` 项目主程序
+
+1. 编辑 `jnpf-admin/src/main/resources/application.yml`
+
+```yaml
+# application.yml 第 6 行,可选值:dev(开发环境-默认)/ test(测试环境)/ preview(预生产)/ prod(生产环境)
+active: dev
+```
+
+2. 修改 `application-dev.yml` 中的配置信息(数据库、Redis 等)
+
+---
+
+### 4.7 `jnpf-file-preview` 文件在线预览模块(kkFileView)
+
+#### 4.7.1 开发环境
+
+1. **IDEA 导入项目**
+2. **调整配置**:打开 `server/src/main/config/application.properties`
+   - 运行端口:`30090`(默认),位于配置第 1 行
+   - Redis 配置:位于配置第 45-49 行
+   - 预览服务地址:本地开发环境修改为 `http://localhost:30090`
+3. **启动项目**:运行 `server/src/main/java/cn/keking/ServerMain`
+4. **验证**:访问 `http://localhost:30090` 测试页面
+
+#### 4.7.2 生产环境
+
+1. **打包**:`/server/target` 目录下主要文件:
+   - `kkFileView-xxx.jar`(一般用于更新)
+   - `kkFileView-xxx.zip`(Windows 环境首次部署)
+   - `kkFileView-xxx.tar.gz`(Linux 环境首次部署)
+2. **上传至服务器**:
+   - 解压上传的文件
+   - 调整配置:打开 `config/application.properties`,参考开发环境配置
+   - 进入 `bin` 目录,运行 `startup` 脚本(首次部署后,更新及维护均在 `bin` 目录下操作)
+
+#### 4.7.3 启动报错修复
+
+##### WebUtilsTests 类修改
+
+若启动报错,需修改 `cn.keking.utils.WebUtilsTests`,将原注释掉的测试类替换为:
+
+```java
+package cn.keking.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class WebUtilsTests {
+
+    @Test
+    void encodeUrlFileNameTest() {
+        // 测试对 URL 中的文件名部分进行 UTF-8 编码
+        String in = "https://file.keking.cn/demo/hello#0.txt";
+        String out = "https://file.keking.cn/demo/hello%230.txt";
+        assertEquals(out, WebUtils.encodeUrlFileName(in));
+    }
+
+    @Test
+    void encodeUrlFileNameTestWithParams() {
+        // 测试对 URL 中的文件名部分进行 UTF-8 编码
+        // URL 带参数
+        // 文件名 "#hello&world" 中的 "&" 应该被编码成为 "%26",而 ? 后的参数列表中的 "&" 不会被编码
+        String in = "https://file.keking.cn/demo/#hello&world.txt?param0=0&param1=1";
+        String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0&param1=1";
+        assertEquals(out, WebUtils.encodeUrlFileName(in));
+    }
+
+    @Test
+    void encodeUrlFullFileNameTestWithParams() {
+        // 测试对 URL 中使用 fullfilename 参数的文件名部分进行 UTF-8 编码
+        String in = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello#0.txt";
+        String out = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello%230.txt";
+        assertEquals(out, WebUtils.encodeUrlFileName(in));
+    }
+}
+```
+
+##### WebUtils 类修改
+
+同时需修改使用上述测试类的 `WebUtils` 类文件:
+
+```java
+public class WebUtils {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
+    private static final String BASE64_MSG = "base64";
+
+    /**
+     * 获取标准的 URL
+     *
+     * @param urlStr url
+     * @return 标准的 URL
+     */
+    public static URL normalizedURL(String urlStr) throws GalimatiasParseException, MalformedURLException {
+        return io.mola.galimatias.URL.parse(urlStr).toJavaURL();
+    }
+
+    /**
+     * 对文件名进行编码
+     */
+    public static String encodeFileName(String name) {
+        try {
+            name = URLEncoder.encode(name, "UTF-8").replaceAll("\\+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+        return name;
+    }
+
+    /**
+     * 去除 fullfilename 参数
+     *
+     * @param urlStr url
+     * @return 处理后的 url
+     */
+    public static String clearFullfilenameParam(String urlStr) {
+        // 去除特定参数字段
+        Pattern pattern = Pattern.compile("(&fullfilename=[^&]*)");
+        Matcher matcher = pattern.matcher(urlStr);
+        return matcher.replaceAll("");
+    }
+
+    /**
+     * 对 URL 进行编码
+     */
+    public static String urlEncoderencode(String urlStr) {
+        String fullFileName = getUrlParameterReg(urlStr, "fullfilename");
+        if (org.springframework.util.StringUtils.hasText(fullFileName)) {
+            urlStr = clearFullfilenameParam(urlStr);
+        } else {
+            fullFileName = getFileNameFromURL(urlStr);
+        }
+        if (KkFileUtils.isIllegalFileName(fullFileName)) {
+            return null;
+        }
+        if (!UrlEncoderUtils.hasUrlEncoded(fullFileName)) {
+            try {
+                urlStr = URLEncoder.encode(urlStr, "UTF-8")
+                    .replaceAll("\\+", "%20")
+                    .replaceAll("%3A", ":")
+                    .replaceAll("%2F", "/")
+                    .replaceAll("%3F", "?")
+                    .replaceAll("%26", "&")
+                    .replaceAll("%3D", "=");
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+            }
+        }
+        return urlStr;
+    }
+
+    /**
+     * 获取 url 中的参数
+     *
+     * @param url  url
+     * @param name 参数名
+     * @return 参数值
+     */
+    public static String getUrlParameterReg(String url, String name) {
+        Map<String, String> mapRequest = new HashMap<>();
+        String strUrlParam = truncateUrlPage(url);
+        if (strUrlParam == null) {
+            return "";
+        }
+        String[] arrSplit = strUrlParam.split("[&]");
+        for (String strSplit : arrSplit) {
+            String[] arrSplitEqual = strSplit.split("[=]");
+            if (arrSplitEqual.length > 1) {
+                mapRequest.put(arrSplitEqual[0], arrSplitEqual[1]);
+            } else if (!arrSplitEqual[0].equals("")) {
+                mapRequest.put(arrSplitEqual[0], "");
+            }
+        }
+        return mapRequest.get(name);
+    }
+
+    /**
+     * 去掉 url 中的路径,留下请求参数部分
+     *
+     * @param strURL url 地址
+     * @return url 请求参数部分
+     */
+    private static String truncateUrlPage(String strURL) {
+        String strAllParam = null;
+        strURL = strURL.trim();
+        String[] arrSplit = strURL.split("[?]");
+        if (strURL.length() > 1) {
+            if (arrSplit.length > 1) {
+                if (arrSplit[1] != null) {
+                    strAllParam = arrSplit[1];
+                }
+            }
+        }
+        return strAllParam;
+    }
+
+    /**
+     * 从 url 中剥离出文件名
+     *
+     * @param url 格式如:http://www.com.cn/20171113164107_月度绩效表模板(新).xls?UCloudPublicKey=...
+     * @return 文件名
+     */
+    public static String getFileNameFromURL(String url) {
+        if (url.toLowerCase().startsWith("file:")) {
+            try {
+                URL urlObj = new URL(url);
+                url = urlObj.getPath().substring(1);
+            } catch (MalformedURLException e) {
+                e.printStackTrace();
+            }
+        }
+        String noQueryUrl = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
+        return noQueryUrl.substring(noQueryUrl.lastIndexOf("/") + 1);
+    }
+
+    /**
+     * 从 multipart file 中剥离出文件名
+     *
+     * @param file 文件
+     * @return 文件名
+     */
+    public static String getFileNameFromMultipartFile(MultipartFile file) {
+        String fileName = file.getOriginalFilename();
+        assert fileName != null;
+        fileName = HtmlUtils.htmlEscape(fileName, KkFileUtils.DEFAULT_FILE_ENCODING);
+
+        int unixSep = fileName.lastIndexOf('/');
+        int winSep = fileName.lastIndexOf('\\');
+        int pos = (Math.max(winSep, unixSep));
+        if (pos != -1) {
+            fileName = fileName.substring(pos + 1);
+        }
+        return fileName;
+    }
+
+    /**
+     * 从 url 中获取文件后缀
+     *
+     * @param url url
+     * @return 文件后缀
+     */
+    public static String suffixFromUrl(String url) {
+        String nonPramStr = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
+        String fileName = nonPramStr.substring(nonPramStr.lastIndexOf("/") + 1);
+        return KkFileUtils.suffixFromFileName(fileName);
+    }
+
+    /**
+     * 对 url 中的文件名进行 UTF-8 编码
+     *
+     * @param url url
+     * @return 文件名编码后的 url
+     */
+    public static String encodeUrlFileName(String url) {
+        String fullFileNameParam = getUrlParameterReg(url, "fullfilename");
+        if (StringUtils.isNotBlank(fullFileNameParam)) {
+            try {
+                fullFileNameParam = URLEncoder.encode(fullFileNameParam, "UTF-8")
+                    .replaceAll("\\+", "%20");
+            } catch (UnsupportedEncodingException e) {
+                return null;
+            }
+            url = url.replace(
+                "fullfilename=" + StringUtils.trimToEmpty(getUrlParameterReg(url, "fullfilename")),
+                "fullfilename=" + fullFileNameParam
+            );
+        } else {
+            String noQueryUrl = url.substring(0, url.contains("?") ? url.indexOf("?") : url.length());
+            int fileNameStartIndex = noQueryUrl.lastIndexOf('/') + 1;
+            int fileNameEndIndex = noQueryUrl.lastIndexOf('.');
+            if (fileNameEndIndex < fileNameStartIndex) {
+                return url;
+            }
+            try {
+                String encodedFileName = URLEncoder.encode(
+                    noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), "UTF-8"
+                ).replaceAll("\\+", "%20");
+                url = url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex);
+            } catch (UnsupportedEncodingException e) {
+                return null;
+            }
+        }
+        return url;
+    }
+
+    /**
+     * 从 ServletRequest 获取预览的源 url,已 base64 解码
+     *
+     * @param request 请求 request
+     * @return url
+     */
+    public static String getSourceUrl(ServletRequest request) {
+        String url = request.getParameter("url");
+        String urls = request.getParameter("urls");
+        String currentUrl = request.getParameter("currentUrl");
+        String urlPath = request.getParameter("urlPath");
+        if (StringUtils.isNotBlank(url)) {
+            return decodeUrl(url);
+        }
+        if (StringUtils.isNotBlank(currentUrl)) {
+            return decodeUrl(currentUrl);
+        }
+        if (StringUtils.isNotBlank(urlPath)) {
+            return decodeUrl(urlPath);
+        }
+        if (StringUtils.isNotBlank(urls)) {
+            urls = decodeUrl(urls);
+            String[] images = urls.split("\\|");
+            return images[0];
+        }
+        return null;
+    }
+
+    /**
+     * 判断地址是否正确
+     */
+    public static boolean isValidUrl(String url) {
+        String regStr = "^((https|http|ftp|rtsp|mms|file)://)";
+        Pattern pattern = Pattern.compile(regStr);
+        Matcher matcher = pattern.matcher(url);
+        return matcher.find();
+    }
+
+    /**
+     * 将 Base64 字符串解码,再解码 URL 参数,默认使用 UTF-8
+     *
+     * @param source 原始 Base64 字符串
+     * @return decoded string
+     *
+     * 示例:aHR0cHM6Ly9maWxlLmtla2luZy5jbi9kZW1vL+S4reaWhy5wcHR4
+     *   → https://file.keking.cn/demo/%E4%B8%AD%E6%96%87.pptx
+     *   → https://file.keking.cn/demo/中文.pptx
+     */
+    public static String decodeUrl(String source) {
+        String url = decodeBase64String(source, StandardCharsets.UTF_8);
+        if (!StringUtils.isNotBlank(url)) {
+            return null;
+        }
+        return url;
+    }
+
+    /**
+     * 将 Base64 字符串使用指定字符集解码
+     *
+     * @param source  原始 Base64 字符串
+     * @param charsets 字符集
+     * @return decoded string
+     */
+    public static String decodeBase64String(String source, Charset charsets) {
+        try {
+            return new String(
+                Base64.getDecoder().decode(source.replaceAll(" ", "+").replaceAll("\n", "")),
+                charsets
+            );
+        } catch (Exception e) {
+            if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
+                LOGGER.error("url 解码异常,接入方法错误未使用 BASE64");
+            } else {
+                LOGGER.error("url 解码异常,其他错误", e);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * 获取 url 的 host
+     *
+     * @param urlStr url
+     * @return host
+     */
+    public static String getHost(String urlStr) {
+        try {
+            URL url = new URL(urlStr);
+            return url.getHost().toLowerCase();
+        } catch (MalformedURLException ignored) {
+        }
+        return null;
+    }
+
+    /**
+     * 获取 session 中的 String 属性
+     */
+    public static String getSessionAttr(HttpServletRequest request, String key) {
+        HttpSession session = request.getSession();
+        if (session == null) {
+            return null;
+        }
+        Object value = session.getAttribute(key);
+        if (value == null) {
+            return null;
+        }
+        return value.toString();
+    }
+
+    /**
+     * 获取 session 中的 long 属性
+     */
+    public static long getLongSessionAttr(HttpServletRequest request, String key) {
+        String value = getSessionAttr(request, key);
+        if (value == null) {
+            return 0;
+        }
+        return Long.parseLong(value);
+    }
+
+    /**
+     * session 中设置属性
+     */
+    public static void setSessionAttr(HttpServletRequest request, String key, Object value) {
+        HttpSession session = request.getSession();
+        if (session == null) {
+            return;
+        }
+        session.setAttribute(key, value);
+    }
+
+    /**
+     * 移除 session 中的属性
+     */
+    public static void removeSessionAttr(HttpServletRequest request, String key) {
+        HttpSession session = request.getSession();
+        if (session == null) {
+            return;
+        }
+        session.removeAttribute(key);
+    }
+}
+```
+
+##### Office 组件报错
+
+> 如果启动报错找不到 Office 组件,请确认 `office.home` 配置是否有误。
+
+**根本原因**:`office.home` 指向的路径下找不到 LibreOffice / OpenOffice 可执行文件,导致 `OfficePluginManager` 初始化抛出 `RuntimeException`。
+
+**解决步骤**:
+
+1. **确认本机是否已安装 LibreOffice**(kkFileView 4.4 仅支持 LibreOffice 7.x/6.x,不再支持 OpenOffice)
+
+CentOS / Rocky:
+```bash
+yum list installed | grep libreoffice
+```
+
+Ubuntu / Debian:
+```bash
+dpkg -l | grep libreoffice
+```
+
+2. **如未安装,按系统执行对应安装命令**
+
+CentOS(7.x 系列):
+```bash
+sudo yum install -y libreoffice libreoffice-headless libreoffice-writer libreoffice-calc libreoffice-impress
+```
+
+Ubuntu:
+```bash
+sudo apt update
+sudo apt install -y libreoffice libreoffice-java-common
+```
+
+3. **如果 yum 仓库中没有 LibreOffice 7.6,请手动下载安装**
+
+```bash
+# 安装依赖
+sudo yum install -y cairo cups-libs libSM libXrender libXext fontconfig
+
+# 下载 LibreOffice 7.6.7
+cd /opt
+sudo wget https://downloadarchive.documentfoundation.org/libreoffice/old/7.6.7.2/rpm/x86_64/LibreOffice_7.6.7.2_Linux_x86-64_rpm.tar.gz
+
+# 解压并安装
+sudo tar -xf LibreOffice_7.6.7.2_Linux_x86-64_rpm.tar.gz
+cd LibreOffice_7.6.7.2_Linux_x86-64_rpm/RPMS
+sudo yum localinstall -y *.rpm
+
+# 验证安装
+/opt/libreoffice7.6/program/soffice --version
+```
+
+---
+
+## 5. 服务器部署文件清单
+
+需上传至服务器的 JAR 包列表:
+
+| 文件名 | 大小 |
+|---|---|
+| `jnpf-admin-5.2.0-RELEASE.jar` | ~208 MB |
+| `jnpf-datareport-5.2.0-RELEASE.jar` | ~181 MB |
+| `jnpf-datareport-univer-admin-5.2.0-RELEASE.jar` | ~151 MB |
+| `jnpf-workflow-admin-1.0.0-RELEASE.jar` | ~65 MB |
+| `xxl-job-admin-5.2.0-RELEASE.jar` | ~105 MB |
+| `kkFileView-4.4.0-RELEASE`(目录) | — |
+
+---
+
+## 6. Nginx 配置
+
+```nginx
+server {
+    # 站点端口
+    listen 80;
+    # 站点访问地址(IP 或域名)
+    server_name jnpf.jnpfsoft.com;
+    index index.html index.htm;
+    # 站点路径
+    root /home/jnpf/nginx/html/jnpf;
+
+    # JNPF-Start
+
+    client_max_body_size 100m;
+    proxy_set_header Cookie $http_cookie;
+    proxy_set_header X-Forwarded-Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+    # 开启 gzip
+    gzip on;
+    gzip_static on;
+    gzip_disable "msie6";
+    gzip_min_length 100;
+    gzip_buffers 4 16K;
+    gzip_comp_level 3;
+    gzip_types text/plain application/javascript application/x-javascript text/css
+               application/xml text/javascript application/x-httpd-php
+               image/jpeg image/gif image/png application/json;
+    gzip_vary off;
+
+    # 前端主项目伪静态
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # 大屏伪静态
+    location /DataV {
+        try_files $uri $uri/ /DataV/index.html;
+    }
+
+    # Ureport 报表伪静态
+    location /Report/icons/ {
+        try_files $uri $uri/ /Report/icons/;
+    }
+
+    # 主项目后端接口
+    location /api/ {
+        proxy_pass http://192.168.10.196:30000;
+    }
+
+    # WebSocket
+    location /websocket {
+        proxy_pass http://192.168.10.196:30000/api/message/websocket;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_read_timeout 600s;
+    }
+
+    # Ureport 报表后端接口
+    location /ReportServer/ {
+        proxy_pass http://192.168.10.196:30007/;
+    }
+
+    # Flowable 后端接口
+    location /api/Flow {
+        proxy_pass http://192.168.10.196:31000;
+    }
+
+    # Univer 数据报表接口
+    location /api/Report {
+        proxy_pass http://192.168.10.196:32000;
+    }
+
+    # 文件预览后端接口
+    location /FileServer {
+        proxy_pass http://192.168.10.196:30090;
+    }
+
+    location ~ /FileServer/*.*\.(js|css)?$ {
+        proxy_pass http://192.168.100.196:30090;
+    }
+
+    # JNPF-End
+}
+```
+
+> 需将项目 `jnpf-resources` 中静态文件上传至服务器 Nginx 配置的静态文件访问路径。
+
+### 静态资源目录结构
+
+```bash
+├── BiVisualPath          # 大屏设计
+├── DocumentFile          # 文档
+├── DocumentPreview       # 文档预览
+├── EmailFile             # 邮件附件
+├── IMContentFile         # IM 聊天附件
+├── Language              # 语言包
+├── ReportFile            # 报表相关资源
+├── SystemFile            # 系统附件
+├── TemplateCodeVue3      # 代码生成器模板
+│    ├── helper           # 前端调用 API 接口
+│    ├── java             # 后端模板(各表单类型通用)
+│    ├── macro            # 流程表单模板
+│    ├── PublicMacro      # 公用宏(通用方法和变量)
+│    ├── TemplateCode1    # 发起表单(流程表单)
+│    ├── TemplateCode2    # 功能表单(表单列表)
+│    ├── TemplateCode3    # 功能流程(表单列表流程)
+│    ├── TemplateCode4    # 纯表单
+│    ├── TemplateCode5    # 纯表单 + 流程
+│    └── TemplateCode6    # 视图代码生成
+├── TemplateFile          # 其他模板文档
+├── TemporaryFile         # 临时存放目录
+├── UserAvatar            # 用户头像
+└── WebAnnexFile          # 其他
+```
+
+---
+
+## 7. JNPF v6x 前端配置
+
+> **环境要求**:Node.js v20.15.1
+
+### 7.1 安装 pnpm
+
+```bash
+npm install -g pnpm
+```
+
+### 7.2 VSCode 打开项目
+
+打开 `jnpf-web-monorepo-v6x`。
+
+### 7.3 复制插件
+
+将解压包中的 `jnpf-bpmn-v6x` 与 `jnpf-univer-v6x` 复制到:
+
+```
+jnpf-web-monorepo-v6x\packages\jnpf\plugins\
+```
+
+并按规范修改名称。
+
+### 7.4 复制 git.ts
+
+将下载文件中 `git.ts` 文件复制到:
+
+```
+D:\jnpf\jnpf-web-monorepo-v6x\internal\node-utils\src
+```
+
+### 7.5 安装依赖
+
+```bash
+pnpm install
+```
+
+### 7.6 后端接口配置
+
+修改项目根目录 `.env.development` 中的后端接口地址和 WebSocket 地址:
+
+- Java 项目本地开发默认接口地址:`http://localhost:30000`
+- .NET 项目本地开发默认接口地址:`http://localhost:5000`
+
+```bash
+# 后端接口
+VITE_PROXY = [["/dev","http://localhost:30000"]]
+
+# WebSocket 地址(将后端接口协议改为 ws)
+VITE_GLOB_WEBSOCKET_URL='ws://localhost:30000'
+```
+
+### 7.7 关联项目配置
+
+打开 `apps\web\.env` 配置文件:
+
+```bash
+# 密钥
+VITE_CIPHER_KEY=自行修改
+
+# 高德地图相关 key
+VITE_A_MAP_JS_KEY=自行修改
+VITE_A_MAP_WEB_KEY=自行修改
+VITE_A_MAP_SECURITY_JS_CODE=自行修改
+```
+
+### 7.8 本地运行报错处理
+
+本地运行 `pnpm dev` 可能会报 `jiti` 相关错误。
+
+#### 解决方案
+
+1. **确认正确的 jiti 目录位置**:
+
+```
+D:\jnpf\jnpf-web-monorepo-v6x\node_modules\.pnpm\jiti@2.5.1\node_modules\jiti
+```
+
+2. **重新创建符号链接**(修正路径层级):
+
+```powershell
+# 确保当前目录是:
+# D:\jnpf\jnpf-web-monorepo-v6x\node_modules\.pnpm\jiti@2.5.1\node_modules\.pnpm\jiti@2.5.1\node_modules
+
+# 创建指向正确 jiti 目录的符号链接(相对路径)
+New-Item -ItemType SymbolicLink -Path "jiti" -Target "..\..\..\jiti"
+
+# 或使用绝对路径(最稳妥)
+New-Item -ItemType SymbolicLink -Path "jiti" -Target "D:\jnpf\jnpf-web-monorepo-v6x\node_modules\.pnpm\jiti@2.5.1\node_modules\jiti"
+```
+
+3. **重新启动项目**:
+
+```powershell
+cd D:\jnpf\jnpf-web-monorepo-v6x
+pnpm dev
+```
+
+### 7.9 打包
+
+本地运行无误后执行打包:
+
+```bash
+pnpm build
+```
+
+打包产物路径:
+
+```
+D:\jnpf\jnpf-web-monorepo-v6x\apps\web\dist
+```
+
+将 `/dist/` 下所有文件上传至服务器。
+
+---
+
+## 8. 服务器中服务启动命令
+
+按顺序逐个模块启动:
+
+```bash
+cd /home/uskycloud_c01/software/jnpfv6/
+#运行脚本
+./start
+
+下面为脚本中内容:
+
+sh servicectl start jnpf-admin-6.0.0-RELEASE.jar
+sleep 5
+
+sh servicectl start xxl-job-admin-6.0.0-RELEASE.jar
+sleep 5
+
+sh servicectl start jnpf-datareport-6.0.0-RELEASE.jar
+sleep 5
+
+sh servicectl start jnpf-datareport-univer-admin-6.0.0-RELEASE.jar
+sleep 5
+
+sh servicectl start jnpf-workflow-admin-1.0.0-RELEASE.jar
+sleep 5
+
+cd /home/uskycloud_c01/software/jnpfv6/kkFileView-4.4.0-RELEASE/bin/
+sh shutdown.sh
+sleep 5
+sh startup.sh
+```
+
+---
+
+## 附录 A:JNPF 售后工单复派次数触发器
+
+使用 MySQL 5.7 `BEFORE INSERT` 触发器实现:插入前按 `project_name` 分组计数,自动生成「第 N 次」写入 `f_batch` 字段。
+
+### 触发器 SQL
+
+```sql
+DELIMITER //
+
+-- 先删旧触发器(避免冲突)
+DROP TRIGGER IF EXISTS shgd_f_batch;
+
+CREATE TRIGGER shgd_f_batch
+BEFORE INSERT ON mt755699032755536005
+FOR EACH ROW
+BEGIN
+    DECLARE cnt INT DEFAULT 0;
+    DECLARE num_chars VARCHAR(20);  -- 存储中文数字
+
+    -- 同项目名称、同租户、未删除的记录数
+    SELECT COUNT(*)
+    INTO cnt
+    FROM mt755699032755536005
+    WHERE project_name = NEW.project_name
+      AND f_tenant_id = NEW.f_tenant_id
+      AND (f_delete_mark IS NULL OR f_delete_mark <> 1);
+
+    -- 数字转中文
+    SET cnt = cnt + 1;
+    SET num_chars = CASE cnt
+        WHEN 1 THEN '一'
+        WHEN 2 THEN '二'
+        WHEN 3 THEN '三'
+        WHEN 4 THEN '四'
+        WHEN 5 THEN '五'
+        WHEN 6 THEN '六'
+        WHEN 7 THEN '七'
+        WHEN 8 THEN '八'
+        WHEN 9 THEN '九'
+        WHEN 10 THEN '十'
+        ELSE '十'  -- 超过 10 次默认显示"第十几次"
+    END;
+
+    -- 生成"第N次"(中文)
+    SET NEW.f_batch = CONCAT('第', num_chars, '次');
+
+END //
+
+DELIMITER ;
+```
+
+### 关键点说明
+
+| 设计点 | 说明 |
+|---|---|
+| **BEFORE INSERT** | 插入前即可算好并写入 `f_batch`,避免 AFTER INSERT 的"同表更新"风险 |
+| **租户隔离** | 加上 `f_tenant_id = NEW.f_tenant_id`,多租户环境不会串数 |
+| **软删除过滤** | 排除 `f_delete_mark = 1` 的已删除数据,计数更准确 |
+| **并发安全** | InnoDB 行级锁保证并发插入时计数不重复、不跳号 |
+
+---
+
+## 附录 B:合并版触发器(f_batch + f_project_name 自动赋值)
+
+```sql
+-- 1. 删除原有触发器(如果存在)
+DROP TRIGGER IF EXISTS `shgd_f_batch`;
+
+-- 2. 新建合并后的触发器:既生成 f_batch,又自动赋值 f_project_name
+DELIMITER //
+
+CREATE DEFINER=`root`@`%` TRIGGER `shgd_f_batch`
+BEFORE INSERT ON `mt755699032755536005`
+FOR EACH ROW
+BEGIN
+    DECLARE cnt INT DEFAULT 0;
+    DECLARE num_chars VARCHAR(20);
+
+    -- 入库时自动把 project_name 赋值给 f_project_name
+    SET NEW.f_project_name = NEW.project_name;
+
+    -- 同项目名称、同租户、未删除的记录数
+    SELECT COUNT(*)
+    INTO cnt
+    FROM mt755699032755536005
+    WHERE project_name = NEW.project_name
+      AND f_tenant_id = NEW.f_tenant_id
+      AND (f_delete_mark IS NULL OR f_delete_mark <> 1);
+
+    -- 数字转中文
+    SET cnt = cnt + 1;
+    SET num_chars = CASE cnt
+        WHEN 1 THEN '一'
+        WHEN 2 THEN '二'
+        WHEN 3 THEN '三'
+        WHEN 4 THEN '四'
+        WHEN 5 THEN '五'
+        WHEN 6 THEN '六'
+        WHEN 7 THEN '七'
+        WHEN 8 THEN '八'
+        WHEN 9 THEN '九'
+        WHEN 10 THEN '十'
+        ELSE '十'
+    END;
+
+    -- 生成"第N次"(中文)
+    SET NEW.f_batch = CONCAT('第', num_chars, '次');
+
+END //
+
+DELIMITER ;
+
+-- 3. 更新触发器:更新时同步 f_project_name
+CREATE DEFINER=`root`@`%` TRIGGER `shgd_f_project_name_update`
+BEFORE UPDATE ON `mt755699032755536005`
+FOR EACH ROW
+SET NEW.f_project_name = NEW.project_name;
+```

文件差異過大導致無法顯示
+ 0 - 1632
技术分享/JNPF快速开发平台源码配置顺序及启动.md


部分文件因文件數量過多而無法顯示