|
|
@@ -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¶m1=1";
|
|
|
+ String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0¶m1=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;
|
|
|
+```
|