
## 开发资料链接集合

[e9 开发资料链接](e9%20开发资料链接.md)

## 开发规范

[E9 开发规范](E9%20开发规范.md)
## 开发准则

一些建议的规范，根据本人多年开发踩过的坑总结的一些经验

### 异常处理

#### 避免将异常抛出到上层标准类

例如下面的代码，如果 execute() 方法内不手动捕获异常，如果异常抛出到上层的标准类，则异常日志将不会打印到集成日志，而是打印到 ecology 日志，打印到 ecology 日志有个弊端，就是文件很大，不利于查找日志，我建议将所有二开日志打印到集成日志，另外在上层类中不确定异常是如何处理，可能不会在日志中打印异常栈，导致错误难以查找。

❌ **没有捕获异常，异常将抛出到上层**

```java
public class InterfaceParamMappingDetailCmd extends AbstractCommand<InterfaceParamMappingDetail> {
    @Override
    public InterfaceParamMappingDetail execute(CommandContext commandContext) {
        if (id == null || id <= 0) {
            return null;
        }
        InterfaceParamMappingDetail detail = mappingDao.queryDetailMain(id);
        if (detail == null || detail.getId() == null) {
            return detail;
        }
        List<InterfaceParamMappingDetail.ParamMappingItem> mappings = buildMappings(detail);
        detail.setMappings(mappings);
        return detail;
    }
}
```

✅ **手动捕获异常并打印**

```java
public class InterfaceParamMappingDetailCmd extends AbstractCommand<InterfaceParamMappingDetail> {
    @Override
    public InterfaceParamMappingDetail execute(CommandContext commandContext) {
        try {
            if (id == null || id <= 0) {
                return null;
            }
            InterfaceParamMappingDetail detail = mappingDao.queryDetailMain(id);
            if (detail == null || detail.getId() == null) {
                return detail;
            }
            List<InterfaceParamMappingDetail.ParamMappingItem> mappings = buildMappings(detail);
            detail.setMappings(mappings);
            return detail;
        } catch (Exception e) {
            log.error("执行发生异常", e);
            throw  e;
        }
    }
}
```

对于其它地方也是如此，比如 WEB 接口类中的方法、流程 Action 类、计划任务等

WEB 接口类中的方法手动捕获异常：
（注：WebExceptionHandler.handle() 内已记录异常信息）

```java
@Path("handle")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response handle(@Context HttpServletRequest request,@Context HttpServletResponse response) {
    Map<String, Object> params = ParamUtil.requestJson2Map(request);
    try {
        Map<String, Object> result = getService().handle(params);
        return Response.ok(result).build();
    } catch (Exception e) {
        return WebExceptionHandler.handle(e);
    }
}
```

#### 不要直接打印异常

不要直接使用 `e.printStackTrace()` 打印异常，这样异常不会记录到集成日志，请将异常信息记录到日志

❌ **错误示例：**

```java
try{
    
}catch(Exception e){
    e.printStackTrace();
}
```

✅ **正确示例：**

```java
try{
    
}catch(Exception e){
    // 将异常打印到集成日志
    log.error("执行发生异常",e);
    // 对异常进行其它处理
}
```

### RecordSet 的使用

#### 不要忽略 execute() 方法的执行结果

以及类似的方法：`executeQuery()`、`executeUpdate()` 等。

这些方法的返回结果可以判断 sql 是否执行正确，如果忽略返回结果就不知道 sql 是否执行正确了，如果 sql 执行错误了但没有进行处理，继续执行后续的业务代码将会导致问题影响进一步扩大，难以排查原因。

❌ **不对返回结果进行判断，忽略结果**

```java
recordSet.executeQuery(sql, params.toArray())
```

✅ **判断返回结果，如果 SQL 执行失败则进行处理，例如抛出异常**

```java
if (!recordSet.executeQuery(sql, params.toArray())) {
    throw new SqlExecuteException("执行sql失败，sql: " + sql + "，params: ["
            + formId + ", " + fieldDbName + ", " + detailTableName + "]", sql);
}
```

#### 使用 getString()获取数值类型

`getInt()`、`getDouble()`、`getFloat()` 等方法会将字段值转换为指定的类型，因为返回的是基本类型，如果字段值为空将返回意想不到的值，例如 `getInt()` 将返回 -1，如果你没有对这样情况的返回结果进行判断，则错误的结果将会导致后面的业务出现错误，最好是先使用 `getString()` 获取字符串值，判断是否为空，然后再转换成指定类型。

可以使用 HuTool 中的 Convert 类转换成指定类型，如果转换失败它会返回 null，也就是字段值为空的时候返回 null，这样就知道这个字段是不是空的了

```java
Convert.toInt(recordSet.getString("parent_id"))
```

### 日志处理

1. 统一将二次开发日志输出到集成日志，不要将日志输出到 ecology 日志，ecology 日志很大日志不好找
2. 捕获异常时，将异常打印到日志，这样可以在日志中看到异常栈，不可以只打印异常信息，例如：

```java
try{
    
}catch(Exception e){
    // 将异常打印到集成日志
    log.error("执行发生异常",e);
    // 对异常进行其它处理
}
```

## 二次开发入门

### 开发环境搭建

[e9 后端开发环境搭建](e9%20后端开发环境搭建.md)

### 无侵入二次开发

[e9 后端无侵入二次开发](e9%20后端无侵入二次开发.md)

### 数据库操作

#### RecordSet 数据库操作类

代码中的数据库操作使用 RecordSet 类执行，使用时需要创建 RecordSet 对象

```java
RecordSet recordSet = new RecordSet();
```

进行查询操作使用 `executeQuery(sql,params)` 方法，该方法返回布尔值，如果执行不成功则返回 false，其中 params 参数是 sql 语句中的占位符(?)参数值，sql 执行时就会将参数的值替换到占位符中，为防止 sql 注入，如无特殊情况都使用占位符进行注入，不得使用字符串连接。

```java
boolean success = recordSet.executeQuery("SELECT ID FROM table WHERE name=? AND age=?",name,age);
if(!success){
    // sql 查询错误
    log.error("sql 查询错误");
}
// 查询操作一定要调用 next 方法，不然获取不到查询结果
recordSet.next();
recordSet.getInt("ID");
// 获取字段时可转换为其它类型
// recordSet.getString("ID");
```

如果需要查询多条数据，则使用 while 循环，`recordSet.next()` 方法可指定下一行查询数据，如果没有下一行则返回 false

```java
recordSet.executeQuery("SELECT ID FROM table WHERE name=? AND age=?",name,age);
while (recordSet.next()) {
    int id = recordSet.getInt("ID");
}
```

数据库修改操作使用 `executeUpdate(sql,params)` 方法执行，update, insert, delete 操作都可以使用此方法，如果执行失败则返回 false

```java
boolean success = recordSet.executeQuery("UPDATE name=?,age=? FROM table WHERE id=?",name,age,id);
if(!success){
    // 更新失败
    log.error("更新失败");
}
```

`execute(sql)` 也能执行 sql 查询和修改，但不建议使用，不能使用占位符，操作类型不明确，不能通过方法名确定是查询还是修改。

RecordSet 对数据库进行操作时不会在当前代码块抛出异常，如果 sql 执行不成功方法会返回 false。

##### 字段值获取方式

RecordSet 获取查询的字段值可以有以下方式

**通过字段名获取**，这种是最常用的

```java
recordSet.getString("name");
```

**通过列位置获取**，比如查询有两个字段，第一个字段的列位置就是1，第二个字段的列位置是2，列位置是从1开始的

```java
recordSet.getString(1)
```

##### 数字类型字段值获取问题

如果使用 `recordSet.getInt("field")` 或 `recordSet.getDouble("field")` 获取数字类型的字段值，如果字段值为空则获取到的值可能为 -1，在代码中需要注意判断，或者可以先使用 `recordSet.getString("field")` 获取到字符串值，判断是否为空，不为空才转为数字类型。

##### 批量操作

##### 相关问题

**RecordSet 批量执行报 java.util.ArrayList cannot be cast to java.lang.Stri：**

List中的参数个数与sql语句的占位符数量不一致导致

#### RecordSetDataSource 使用第三方数据源进行数据库操作

构造方法中传入第三方数据源标识，该标识可在系统后端页面集成中心中找到。

```java
private RecordSetDataSource rst = new RecordSetDataSource("E8OA");
```

#### RecordSetTrans 数据库操作类（事务）

使用该类可执行事务

**主要方法：**

- `setAutoCommit()` 是否自动提交事务，如要要事务回滚，需要设为false
- `commit()` 提交事务，在执行更新或插入操作后，可提交事务，使更改生效
- `rollback()` 回滚事务，在数据库执行失败（如返回 false 或抛出异常）可执行回滚操作，撤回之前进行的插入或更新。

##### ⚠️ 修改后缓存不刷新问题

使用 `RecordSetTrans` 要特别注意，不要使用 `executeSql()` 方法，使用此方法会导致**执行修改 sql 后缓存不会刷新**，请使用 `executeUpdate()` 替换

##### ⚠️ 事务未提交时不得用另外的 RecordSet 对象对修改的数据进行查询

例如使用 `RecordSetTrans` 对象插入了一条数据，未提交事务，此时用另外一个 `RecordSet` 对象查询插入的数据会导致查询不到。
未提交事务时如果要查询修改的数据，则可以使用执行数据修改的 `RecordSetTrans` 对象进行查询，或者提交事务后再进行查询。
#### RecordSetExecutionInterface 数据库操作接口

RecordSet 和 RecordSetTrans 这些数据库操作类都实现了 RecordSetExecutionInterface 接口，因此在方法中使用 RecordSetExecutionInterface 接口而不是具体的实现类对于事务执行很有帮助，如果不需要执行事务，则传入 RecordSet，如果需要执行事务则传入 RecordSetTrans。

```java
public static boolean insertWithRsInterface(String tableName, Map<String, Object> fieldData,
                                            RecordSetExecutionInterface recordSet) throws Exception {

}
```

**使用方法：**

`executeSql()` 方法可执行sql，可进行查询或修改（插入），可使用占位符注入参数，参数说明：

1. **sql**：执行sql
2. **isQuerySql**：是否为查询sql，如果是查询传入 true，如果是修改（插入）传入 false
3. **poolName**：连接池名称，用于连接外部数据源，如果是本地数据源传入空字符串
4. **hasNewConn**：是否新建连接，传入 true 或 false 都可以
5. **params**：sql 中的参数值，如果使用占位符注入参数（占位符:?）需要传入该参数

**sql 查询示例：**

```java
RecordSetExecutionInterface recordSet = new RecordSet();
if (!recordSet.executeSql(sql, true, "", false, uuid)) {
    logger.error("根据uuid查询数据id失败");
    continue;
}
if (!recordSet.next()) {
    logger.warn("无法查询到数据id，uuid:" + uuid);
    continue;
}
ids.add(recordSet.getInt("id"));
```

**sql 插入示例：**

```java
public static boolean inserByRsInterface(String tableName, Map<String, Object> fieldData,
                                            RecordSetExecutionInterface recordSet) throws Exception {
    String  sql = SqlUtil.buildInsertSql(tableName, new ArrayList<>(fieldData.keySet()));
    List<Object> insertData = new ArrayList<>(fieldData.values());
    logger.info("sql:" + sql);
    return recordSet.executeSql(sql,false,"",false ,insertData.toArray());
}
```

### java配置文件

#### 属性获取

##### 使用 BaseBean 获取（不推荐）

需要创建 BaseBean 对象，不推荐。

```java
BaseBean baseBean = new BaseBean();        
String propValue = baseBean.getPropValue("testProp", "sms");
```

##### 使用 Prop 类获取（推荐）

使用 `weaver.file.Prop` 获取配置文件属性，注意如果配置文件属性值有中文，获取属性值时会导致乱码，原因是没有使用 UTF-8 编码。

```java
// fileName：文件名，不需要带文件扩展名，propName：属性名
String value = Prop.getPropValue(fileName, propName);
```

属性值有中文时，只能将中文转为 unicode 放到配置文件中

转换地址：[在线 Unicode 编码转换 | 菜鸟工具](https://c.runoob.com/front-end/3602/)

##### 使用 Properties 类获取（支持中文）

使用 `java.util.Properties` 类获取配置属性可解决中文乱码问题，不过这种用法比较复杂，建议在有中文的情况下使用

首先需要获取 Properties对象

```java
/**
 * 获取配置文件，解决了配置文件中文乱码问题
 *
 * @param propFieldName 配置文件名称，不需要带扩展名后缀
 * @return Properties对象
 */
public static Properties fetchProperties(String propFieldName) {
    Properties prop = new Properties();
    String path = GCONST.getPropertyPath() + propFieldName + ".properties";
    try (InputStream inputStream = Files.newInputStream(Paths.get(path))) {
        prop.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    } catch (IOException e) {
        log.error("获取配置文件失败", e);
    }
    return prop;
}
```

然后就能获取属性值

```java
Properties properties = fetchProperties(fileName);
String value = properties.getProperty(propName);
```

#### 配置类

不建议将获取配置写在业务代码上，这样会导致获取配置太过分散，通常是建一个配置类获取配置，如：

```java
public class SuppApiConfiguration {
    private static final String FILE_NAME = "FW20241127-suppApi";
    private static final int TEN_MINUTE_EXPIRE = 60 * 10;
    private SuppApiConfiguration() {

    }

    public static String getAppId() {
        return PropertiesUtil.getPropValue(FILE_NAME, "appId", true, true, TEN_MINUTE_EXPIRE);
    }

    public static String getKey() {
        return PropertiesUtil.getPropValue(FILE_NAME, "key", true, true, TEN_MINUTE_EXPIRE);
    }

    public static String getServerUrl() {
        return PropertiesUtil.getPropValue(FILE_NAME, "serverUrl", true,
                true, TEN_MINUTE_EXPIRE);
    }

    public static String getBudgetChangeStatusApiUrl() {
        return PropertiesUtil.getPropValue(FILE_NAME, "budgetChangeStatusApiUrl", true,
                true, TEN_MINUTE_EXPIRE);
    }
}
```


### 打印日志

日志级别默认为 info，有关如何修改日志级别请查看本文档的运维与调试章节

二开日志一律打印到 Integration.log，不得打印到其它日志文件，如果打印到 ecology.log，由于该日志太过庞大会导致日志查找困难。

**创建日志对象**

```java
weaver.integration.logging.Logger log = weaver.integration.logging.LoggerFactory.getLogger(this.getClass());
```

 **打印日志**

```java
log.info("日志内容");
```

**可将异常打印到日志**

```java
try {
    Map<String, Object> result = getService().handle(params);
    return Response.ok(result).build();
} catch (Exception e) {
    log.error("错误", e);
    return WebExceptionHandler.handle(e);
}
```

### 缓存

参考：[泛微在线文档](https://e-cloudstore.com/doc.html?appId=da5134c3c57f4c6f87815cf1473f674f#4.%20%E7%BC%93%E5%AD%98%E7%9B%B8%E5%85%B3)

#### 判断是否使用 Redis

当系统集成 Redis 时，`Util_Redis.getInstance()` 不会为 null

```java
/**
 * 判断是否使用redis
 *
 * @return 是否使用redis
 */
public static boolean isRedis() {
    return Util_Redis.getInstance() != null;
}
```

### 常用数据库表

1. **workflow_requestbase**：流程请求表，记录每次发起流程的流程信息，里面可获取创建人、创建时间、当前节点、是否归档等信息
2. **workflow_base**：流程基本信息表，记录流程的基本信息和一些设置，包括流程名称、流程类型、表单id等
3. **workflow_currentoperator**：工作流请求节点操作人信息表，可获取节点操作人的操作信息，如操作类型、操作时间、是否超时等。
4. **workflow_requestlog**：工作流请求签字日志表，可获取流程流转的节点签字意见、操作时间、提交类型、下一个节点id等。
5. **docdetail**：知识模块文档信息表，可查看文档的标题、创建人、所属目录id等。
6. **hrmresource**：人力资源表，可查看组织架构中的人员信息。
7. **hrmdepartment**：人力资源部门表，可查看部门信息
8. **hrmsubcompany**：人力资源分部表，可查看分部信息

## WEB 接口开发

[e9 后端接口开发](e9%20后端接口开发.md)

## WeaverBoot 依赖注入

Weaverboot-E9 提供一套完整的 IOC + AOP 的解决方案。可以很快速的代理所有的接口。同时提供类似与spring 的依赖注入的写法。方便快速编码。

我不推荐使用 Weaverboot 进行依赖注入，可能会有在容器中找不到 bean 的情况，有一次我在实现类上加了@WeaIocService 注解，可却在日志中报错提示找不到接口的实现类。并且已经在官方找不到 Weaverboot 的文档。

参考：[泛微在线文档](https://e-cloudstore.com/doc.html?appId=da5134c3c57f4c6f87815cf1473f674f#3.5.1%20%E4%BD%BF%E7%94%A8%20Weaverboot-E9%EF%BC%88beta%EF%BC%89)

## 文档文件

### ImageFileManager 附件类

ImageFileManager 类可获取附件的相关信息

#### 获取附件信息

```java
ImageFileManager ifm = new ImageFileManager();
// 获取附件信息，imagefileid:附件id
ifm.getImageFileInfoById(Util.getIntValue(imagefileid));
String imageFileName = ifm.getImageFileName();
// 获取附件的输入流
is = ifm.getInputStream();
// 获取附件的存储路径，文件通常是zip，需要解压才可以获取到附件，不建议通过这种方式获取附件
String fileRealPath = ifm.getFileRealPath();
```

#### 获取附件文件

调用 `ImageFileManager.getInputStreamById(imagefileId)` 可以获取附件文件的输入流，然后通过输入流保存附件，例如

```java
try(InputStream inputStream = ImageFileManager.getInputStreamById(imagefileId)) {
    File file = new File(file_TO);
    // 保存附件到指定位置
    copyInputStreamToFile(inputStream, file);
} catch (IOException e) {
    e.printStackTrace();
}
```

获取的输入流并不是数据库中的压缩包文件，而是实际附件文件的输入流
![img_20260402152741.png](./files/img_20260402152741.png)

#### 保存附件并创建文档

```java
//保存附件
DocSaveService docSaveService = new DocSaveService();
ImageFileManager imageFileManager = new ImageFileManager();
imageFileManager.resetParameter();
imageFileManager.setImagFileName(imgFileName);
// data :附件的二进制数据
imageFileManager.setData(data);
// 保存附件
int imgId = imageFileManager.saveImageFile();
// 创建文档，doc_category：文档目录
int docId = docSaveService.accForDoc(Util.getIntValue(doc_category, -1), imgId, new User(Util.getIntValue(userId)));
```

### DocAccService类使用

#### 获取文档附件列表

```java
DocAccService docAccService = new DocAccService();
// 参数1:文档id，参数2:操作的用户，无论哪个用户都能取到，即便无权限，参数3:paramMap
Map<String, Object> result = docAccService.getDocAccList(238, new User(1), new HashMap<>());
System.out.println("结果："+result);
// 取到附件列表
List<Map<String, Object>> dataList = (List<Map<String, Object>>) result.get("dataList");
```

获取结果：

```json
{
  "dataList": [
    {
      "imagefileid": 9781,
      "hasVersion": 1,
      "canEdit": 0,
      "maxUploadSize": 100,
      "imagefilename": "test.docx",
      "filesize": "92KB",
      "limitType": "doc,docx",
      "fileVesion": 3,
      "isMainFile": 1,
      "caDownload": 0,
      "canReplace": 0,
      "mixUploadSize": 0,
      "versionId": 321,
      "docfiletype": "<i class='icon-document-word' style='font-size:24px'/>",
      "id": 238,
      "filesizenum": 93766
    }
  ],
  "userCandownload": 1,
  "status": 1
}
```

#### 替换附件

准备好一个已有的文件，替换文档中的附件

```java
// 将文件保存到服务器，生成一个附件
ImageFileManager imageFileManager = new ImageFileManager();
imageFileManager.resetParameter();
imageFileManager.setImagFileName("测试_qr.docx");
imageFileManager.setData(Files.readAllBytes(Paths.get(outDocPath)));
int imgId = imageFileManager.saveImageFile();
// 替换附件，imgId：新附件id，fileId：旧附件id，238：文档id，第四个参数：moduleParams，第五个参数：操作用户，将显示在文档中，显示谁替换了附件
docAccService.replaceAcc(imgId, Integer.parseInt(fileId), 238, "", new User(1));
```

replaceAc()第5个参数表示操作的用户，将显示在文档中

![img_20260402152905.png](./files/img_20260402152905.png)

#### 附件绑定到指定文档

例如流程里的附件，绑定到到另一个字段里的文档中的附件

```java
DocAccService docAccService = new DocAccService();
// fileId:附件id，docId:文档id
docAccService.buildRelForDoc(fileId, docId, true, -1);
```

#### 获取指定文档的所有附件id

aDocIds 为附件所在的文档id

```java
AttachUtil.convertDocsToImageID(aDocIds);
```

#### 获取文档下载链接的加密fileid

文档的下载链接为：`/weaver/weaver.file.FileDownload?fileid=附件id`

其中的fileid需要进行加密，可使用这个地址获取加密后的fileid

```
/docs/docs/takeCode.jsp?filecode=[密文]
```

例如：
```
localhost/docs/docs/takeCode.jsp?filecode=aa2c405dff482ee220b919a6f231398844439254a44b0006dfeaf186a13b05f7644c8b330224cc7bb70cb65cdd91491066e8662b6d7f37c27
```

后端可使用以下代码生成加密后的fileid

```java
String  fileid = DocDownloadCheckUtil.getDownloadfileidstr(filecode);
```

注意，fileid为附件id（iamgeid)而不是docid


## 流程

[流程模块开发](流程模块开发.md)

## 建模

### 模块 Action

**示例代码：**

```java
package weaver.formmode.customjavacode.modeexpand;

import java.util.HashMap;
import java.util.Map;

import weaver.formmode.customjavacode.AbstractModeExpandJavaCodeNew;
import weaver.general.Util;
import weaver.hrm.User;
import weaver.soa.workflow.request.RequestInfo;

/**
 * 说明
 * 修改时
 * 类名要与文件名保持一致
 * class文件存放位置与路径保持一致。
 * 请把编译后的class文件，放在对应的目录中才能生效
 * 注意 同一路径下java名不能相同。
 * @author Administrator
 *
 */
public class ModeExpandTemplate extends AbstractModeExpandJavaCodeNew {
   /**
    * 执行模块扩展动作
    * @param param
    *  param包含(但不限于)以下数据
    *  user 当前用户
    *  importtype 导入方式(仅在批量导入的接口动作会传输) 1 追加，2覆盖,3更新，获取方式(int)param.get("importtype")
    *  导入链接中拼接的特殊参数(仅在批量导入的接口动作会传输)，比如a=1，可通过param.get("a")获取参数值
    *  页面链接拼接的参数，比如b=2,可以通过param.get("b")来获取参数
    * @return 
    */
   public Map<String, String> doModeExpand(Map<String, Object> param) {
      Map<String, String> result = new HashMap<String, String>();
      try {
         User user = (User)param.get("user");
         int billid = -1;//数据id
         int modeid = -1;//模块id
         RequestInfo requestInfo = (RequestInfo)param.get("RequestInfo");
         if(requestInfo!=null){
            billid = Util.getIntValue(requestInfo.getRequestid());
            modeid = Util.getIntValue(requestInfo.getWorkflowid());
            if(billid>0&&modeid>0){
               //------请在下面编写业务逻辑代码------
            }
         }
      } catch (Exception e) {
         result.put("errmsg","自定义出错信息");
         result.put("flag", "false");
      }
      return result;
   }

}
```

**返回值:**

成功返回空map即可，如果失败可以返回出错信息

```java
result.put("errmsg","自定义出错信息");
result.put("flag", "false");
```

#### 获取表单字段数据

和流程action一样，建模action可以获取主表和明细表数据，在建模表单保存时执行接口动作也能获取到最新数据

```java
public class TestModeAction extends AbstractModeExpandJavaCodeNew {
    @Override
    public Map<String, String> doModeExpand(Map<String, Object> map) {
        RequestInfo requestInfo = (RequestInfo) map.get("RequestInfo");
        int billId = Util.getIntValue(requestInfo.getRequestid());
        RecordSet recordSet = new RecordSet();
        JSONObject params = new JSONObject();
        Property[] property = requestInfo.getMainTableInfo().getProperty();
        // 获取明细表数据
        Row[] rows = requestInfo.getDetailTableInfo().getDetailTable()[0].getRow();
        // 获取主表字段值
        String name = WorkflowUtil.getMainFieldValue(requestInfo, "mc");
        Map<String, String> result = new HashMap<>();
        User user = (User) map.get("user");
        String formId = (String) map.get("formId");
        result.put("userName",user.getLastname());
        result.put("formId",formId);
        return result;
    }
}
```

### 模块权限重构

如果使用代码在建模中插入数据，则需要执行权限重构，不然数据的创建者没有权限。权限重构可能会消耗些性能，但我觉得没什么影响。

```java
/**
 * 权限重构
 * @param creatorid  创建人
 * @param modeid  模块ID
 * @param dataid  数据id
 */
public static void  editModeDateShare(Integer creatorid,Integer modeid,Integer dataid){
   ModeRightInfo moderight=new ModeRightInfo();
   moderight.editModeDataShare(creatorid, modeid, dataid);
}
```

### 代码触发建模的提醒

在建模里可以在扩展里添加提醒，但如果我们不想在扩展里执行提醒而是在其它地方执行提醒怎么办呢？
![img_20260402153107.png](./files/img_20260402153107.png)

那我们可以在代码里面使用ModeDataInterface来执行提醒，拿到页面扩展id之后就可以执行提醒了

### 插入建模数据

插入建模表有几个必需插入的字段，如果插入时缺少这几个字段则会导致数据不显示在建模中。

- modedatacreatertype = 0
- modedatacreatedate 为今天日期
- modedatacreatetime 为现在的时间，格式为HH:mm:ss

**示例：**

```java
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
String dateStr = format.format(date);
format.applyPattern("HH:mm:ss");
String time = format.format(date);
String sql = "insert into uf_books (bookname,bookcounts,detail,gly,sfkjc,formmodeid,modedatacreater,modedatacreatertype,modedatacreatedate,modedatacreatetime)" +
        "values (?,?,?,?,?,?,1,0,?,?)";

recordSet.executeUpdate(sql,bookname,bookcounts,detail,gly,sfkjc,modeId,dateStr,time);
```

---

## 计划任务

计划任务可以周期性的执行，非常适合用在同步数据场景。

需要继承 BaseCronJob 类，并实现方法。

计划任务可传入配置参数，和流程 Action 一样，在全局中声明 String 类型成员即可，不能为 final

```java
public class TestJob extends BaseCronJob {
    /**
     * 计划任务参数，可从前端传入
     */
    private String param;
    
    @Override
    public void execute() {
        // 获取时间表达式
        String cronExpr = getCronExpr();
        getLogger().info("计划任务测试，cronExpr："+cronExpr);
    }
}
```

**计划任务配置：**

在后端集成中心中的计划任务，点击新建

![img_20260402153140.png](./files/img_20260402153140.png)

1. 计划任务标识不能和其它计划任务重复
2. 计划任务类填写类的全路径，例如：`com.customization.demo.TestJob`
3. 可配置计划任务参数，参数名与计划任务类中定义的变量名相同

![img_20260402153202.png](./files/img_20260402153202.png)

## 根据接口路径找到对应的类

开发人员如果要查看标准文件的代码就要根据接口的请求路径找到相应的代码文件，虽然可以根据接口路径按包名和文件名去查找，但有些麻烦而且有时候路径跟包名和文件名是对不上的，以下提供了便捷的方法可以迅速找到接口路径对应的文件

如果有源码则可以通过idea的自带工具找到接口路径对应的类（如下图），但如果接口只存放在jar包里或class文件里就不行了
![img_20260402153240.png](./files/img_20260402153240.png)

### 原理

根据类和方法上的注解，获取到指定的类，比如暴露接口的类和方法都有映射接口路径的@Path注解，这样就可以根据注解上的值来找到类了，对于其它映射接口路径注解也是一样的，比如@RequestMapping
![img_20260402153301.png](./files/img_20260402153301.png)

### 步骤

前提是要搭好开发环境

使用 reflections 框架根据注解查找类

1. **添加 reflections 框架依赖**

```xml
<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>
<dependency> 
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>21.0</version>
</dependency>
```

2. **编写查找代码**，可根据接口路径的上级路径和最终路径找到对应的类。

```java
public String getApiFilePath(String packagePrefix, String parentPath,
                             String endPath) throws Exception {
    // 可以在构造方法加上包路径，这样就会扫描指定包下的类，如 com.api ,如不指定包名则会扫描所有类，这样会比较慢
    Reflections f = new Reflections(packagePrefix);
    // 找到所有使用Path注解的类
    Set<Class<?>> set = f.getTypesAnnotatedWith(Path.class);
    for (Class<?> c : set) {
        Path path = c.getAnnotation(Path.class);
        // 该类的Path注解是否包含指定路径
        if (path.value() != null && path.value().contains(parentPath)) {
            System.out.println("找到类："+c.getName());
            // 获取该类的所有方法
            Method[] methods = c.getMethods();
            for (Method m : methods) {
                // 该方法是否使用Path注解，并且该注解是否包含指定接口路径
                if (m.isAnnotationPresent(Path.class)) {
                    Path methodPath = m.getAnnotation(Path.class);
                    if (methodPath.value().contains(endPath)) {
                        return c.getName();
                    }
                }
            }
        }
    }
    return "";
}
```

3. **示例：** 查找 `/api/workflow/reqform/detailData` 接口路径对应的类示例

类的Path注解一般是接口路径的前面几层，比如/workflow/reqform，接口上级路径可以按 reqform 查找，或者 workflow/reqform，

方法的Path注解就是接口路径的最后一层，按 detailData 进行查找

```java
@Test
public void findApiFile() throws Exception {
    String apiFilePath = getApiFilePath("com", "reqform", "detailData");
    if (StringUtils.isNotBlank(apiFilePath)) {
        System.out.println(apiFilePath);
    } else {
        System.out.println("未找到文件");
    }
}
```

输出结果：

```
找到类：com.api.workflow.web.RequestFormNewAction
com.api.workflow.web.RequestFormNewAction
```


## 认证

### OA 接口认证

如果没有配置接口白名单，需要获取 token 才能调用 OA 接口。

**认证步骤：**

1. **进行注册认证**
   - 对应接口：`/api/ec/dev/auth/regist`
   - 得到secret和spk

2. **获得token**
   - 对应接口：`/api/ec/dev/auth/applytoken`
   - 用上一步获得的secret和spk作为参数，获得token

```java
@Test
public void Test() throws Exception {
    // 进行注册认证，获取secret和spk，根据业务保存这两个数据
    JSONObject registerResult = JSONObject.parseObject(register());
    String secret = registerResult.getString("secret");
    String spk = registerResult.getString("spk");
    // 利用上一步获得的secret和spk，获取token
    String token = getToken(secret,spk);
    System.out.println("token:"+token);
}

public String getToken(String secret,String spk){
    RSA rsa = new RSA();
    String newSecret = rsa.encrypt(null,secret,null,"utf-8",spk,false);
    String result = HttpRequest.post("http://localhost/api/ec/dev/auth/applytoken")
            .header("appid","test")
            .header("secret",newSecret)
            .execute().body();
    JSONObject resultObj = JSONObject.parseObject(result);
    return resultObj.getString("token");
}

public String register(){
    return  HttpRequest.post("http://localhost/api/ec/dev/auth/regist")
            .header("appid","test")
            .execute().body();
}
```

参考：[Token认证使用步骤V3](note://WEBff762f10ead719d2ea6778555bbe38db)

### CAS 认证接入

标准功能支持 OA 接入 CAS 服务器，以下是 OA 作为 CAS 客户端，接入第三方CAS服务端进行统一认证示例

#### CAS服务端搭建

1. **下载CAS服务端项目**
   - 下载地址：<https://github.com/apereo/cas-overlay-template/tree/5.3>

2. 然后使用idea打开CAS项目，CAS项目需要使用JDK1.8版本，请注意项目的jdk也要选择1.8

3. 在idea中配置tomcat，并添加工件

![img_20260402153410.png](./files/img_20260402153410.png)

4. 修改application.properties文件，添加一下内容

```properties
cas.serviceRegistry.json.location=classpath:/services
cas.serviceRegistry.initFromJson=true
#忽略https安全协议，使用 HTTP 协议
cas.tgc.secure=false
```

并配置用户名和密码，比如这里的用户名是 sysadmin 密码是1
![img_20260402153447.png](./files/img_20260402153447.png)

![img_20260402153502.png](./files/img_20260402153502.png)
5. 修改service目录下的Apereo-10000002.json文件，修改serviceId 为 `^(http|https|imaps)://.*`
![img_20260402153527.png](./files/img_20260402153527.png)

6. 启动tomcat服务器，然后你应该看到CAS登录界面
![img_20260402153548.png](./files/img_20260402153548.png)

#### OA CAS配置

1. CAS Server地址就填写CAS项目启动后的地址，直接复制过去即可
![img_20260402153615.png](./files/img_20260402153615.png)

![img_20260402153639.png](./files/img_20260402153639.png)

2. 需要配置ecology地址
![img_20260402153708.png](./files/img_20260402153708.png)

3. 点击保存，然后点部署，服务重启之后进行验证
![img_20260402153945.png](./files/img_20260402153945.png)

#### 第三方系统接入OA CAS认证

**OA 配置**

1. OA开启CAS认证，OA作为认证服务端，勾选启用CAS集成
![img_20260402154001.png](./files/img_20260402154001.png)

2. 添加认证应用

将第三方添加到认证应用，这样第三方才可以使用OA的CAS认证
![img_20260402154026.png](./files/img_20260402154026.png)

填写应用的应用表示、业务地址、映射规则
![img_20260402154038.png](./files/img_20260402154038.png)

**第三方系统配置**

3. **在第三方系统中导入jar包**
![cas-client](./files/cas-client-core-3.2.1.jar)

4. **在第三方系统的web.xml中插入**

```xml
<!-- 统一退出的监听-->
<listener>
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

<filter>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
    <filter-name>CASFilter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
    <init-param>
        <!-- 设置统一认证平台登录地址-->
        <param-name>casServerLoginUrl</param-name>
        <param-value>http://127.0.0.1/sso/login?appid=test2</param-value>
    </init-param>
    <init-param>
        <!-- 设置业务系统地址-->
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080/</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CASFilter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>

<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <!-- 设置统一认证平台服务地址-->
        <param-name>casServerUrlPrefix</param-name>
        <param-value>http://127.0.0.1/sso</param-value>
    </init-param>
    <init-param>
        <!-- 设置业务系统地址-->
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080/</param-value>
    </init-param>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Validation Filter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>

<filter>
    <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
    <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>

<filter>
    <filter-name>CAS Assertion Thread Local Filter</filter-name>
    <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Assertion Thread Local Filter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>
```

需要更改统一认证地址和业务系统地址
![img_20260402154200.png](./files/img_20260402154200.png)

这里的appid为OA认证应用的标识
![img_20260402154211.png](./files/img_20260402154211.png)

![img_20260402154222.png](./files/img_20260402154222.png)

5. **在web.xml的最后添加自定义过滤器**

添加过滤器，获取通过统一认证登录后的用户名，构建自己的会话完成登录

```xml
<!--    自定义filter-->
    <filter>
        <filter-name>loginFilter</filter-name>
        <filter-class>com.yao.filter.LoginFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>loginFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
```

**过滤器代码**

```java
package com.yao.filter;

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author 姚礼林
 * @desc TODO
 * @date 2023/6/15
 */
public class LoginFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest)request;
        HttpServletResponse servletResponse = (HttpServletResponse) response;
        HttpSession session = servletRequest.getSession(false);
        Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
        AttributePrincipal principal = assertion.getPrincipal();
        // 获取到用户名，构造自己的会话完成登录
        String name = principal.getName();
        if (!"".equals(name)){
            session.setAttribute("userInfo",name);
            System.out.println("userName:"+name);
            chain.doFilter(request, response);
        }else {
            servletResponse.sendRedirect("toLogin");
        }
    }

    @Override
    public void destroy() {

    }
}
```

6. **退出登录**

退出时需要跳转到该地址：`认证地址/logout?service=业务系统地址`

**示例：**

清除自己的会话和统一认证的会话后跳到登出地址

在controller 中调用此地址登出

```java
@RequestMapping("/exit")
public String exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
    request.getSession().removeAttribute("userInfo");
    request.getSession().removeAttribute("_const_cas_assertion_");
    response.sendRedirect("http://127.0.0.1/sso/logout?service=http%3A%2F%2Flocalhost%3A8080%2Fspringmvc_interceptor_war_exploded");
    return "login";
}
```

#### 相关问题

**token 认证提示该账号存在异常,单点登录失败**

可能是由于licence过期导致，即使登录系统没有提示licence过期，认证时也会导致此错误，原因可能为licence今天到期，但是调用认证接口的服务器和oa服务器的时间不一致，导致判断为过期

**解决方法：**
上传新的licence

## 缓存

### redis

要在 redis 中获取 ecology 里的缓存，需要在 key 前面加上：`e9DataCache`，比如 `e9DataCache:test`

### 验证redis

打开页面：`/checkredis.jsp`
![img_20260402154340.png](./files/img_20260402154340.png)

## 组织同步

### 部门恢复删除

恢复以前删除的部门、分部

@action 参数需要为 delete，这样才会更新 canceled 字段，canceled 需要为 0

@action 为 delete 时只会处理canceled 字段

## 消息
![pptx](./files/短信、邮件、em7消息集成.pptx)

### 短信、邮件、em7消息集成

#### 发送邮件

需要先到OA设置邮件发送

![img_20260407084501.png](./files/img_20260407084501.png)

```java
//收件人，邮箱地址，多地址时用英文逗号分隔
String sendTo = "yaolilin@outlook.com";
//邮件标题
String emailTitle = "OA发送邮件测试";
//邮件内容
String emailInfo = builder.toString();
//邮件附件ids
String emailFileIds = "";
EmailWorkRunnable ewr = new EmailWorkRunnable(sendTo,emailTitle,emailInfo);
ewr.setImagefileids(emailFileIds.toString());
//result:true投递成功，false投递失败
boolean result = ewr.emailCommonRemind();
```

#### 发送短信

该功能需要确保短信服务正常

**使用示例**

```java
SMSSaveAndSend sms=new SMSSaveAndSend();
// 初始化类
sms.reset();
// 设置自定义手机号码
sms.setCustomernumber(recMobile); 
// 设置发送内容
sms.setMessage(message); 
// 设置发送人员id（sender=1的时候，为系统发送）
sms.setUserid(sender);
// 发送方式一：立即发送并实时返回是否发送成功
flag=sms.pageSend();
// 发送方式二：线程发送，返回是否可以发送，至于发送成功与否，需要在短信管理页面中进行查看
flag=sms.send();
```

### 消息中心发送

[消息推送](消息推送.md)

## 用户登录

### DoUserSessionCmd 创建用户会话

`com.api.common.cmd.login.DoUserSessionCmd` 类可以创建用户会话，让前端用户实现登录，比如用户访问了接口就能实现登录。

map 参数：
- `"userid" -> "1"`
- `"openType" -> "1"`

**示例：**

```java
Map<String, Object> params = new HashMap<String, Object>();
params.put("userid", id);
params.put("languid", 7);
// request 和 response 是接口传过来的
DoUserSessionCmd cmd = new DoUserSessionCmd(request, response, params);
cmd.execute(null);
```

### 向session和cookie存入用户信息实现登录

**应用场景:**

用jsp实现单点登录，拿到用户的信息（比如外键）后进行登录

**代码:**

```java
User user = new UserManager().getUserByUserIdAndLoginType(userid, "1");
if (user != null && !"".equals(user.getUID()) && !"".equals(user.getLoginid())) {
   request.getSession(true).setMaxInactiveInterval(1728000);
   request.getSession().setAttribute("weaver_user@bean", user);

   request.getSession(true).setAttribute("moniter", new OnLineMonitor("" + user.getUID(), user.getLoginip()));
   Util.setCookie(response, "loginfileweaver", "/main.jsp", 1728000);
   Util.setCookie(response, "loginidweaver", "" + user.getUID(), 1728000);
   Util.setCookie(response, "languageidweaver", "7", 1728000);

   Map logmessages = (Map) request.getSession().getServletContext().getAttribute("logmessages");
   if (logmessages == null) {
      logmessages = new WHashMap();
      logmessages.put("" + user.getUID(), "");
      request.getSession().getServletContext().setAttribute("logmessages", logmessages);
   }
   //登录日志
   weaver.systeminfo.SysMaintenanceLog log1 = new weaver.systeminfo.SysMaintenanceLog();
   log1.resetParameter();
   log1.setRelatedId(userid);
   log1.setRelatedName(user.getLastname());
   log1.setOperateType("6");
   log1.setOperateDesc("");
   log1.setOperateItem("60");
   log1.setOperateUserid(userid);
   log1.setClientAddress(request.getRemoteAddr());
   try {
      log1.setSysLogInfo();
   } catch (Exception e) {
   }
   return true;
} else {
   return false;
}
```

![jsp](./files/tooamb3.jsp)

## 标准工具

### HrmUserVarify 获取用户对象

可从 request 对象中获取到用户对象，常用于在后端接口中获取用户对象

```java
private User getUser(HttpServletRequest request, HttpServletResponse response) {
    HrmUserVarify.getUser(request, response);
    String var4 = request.getParameter("f_weaver_belongto_userid");
    String var5 = request.getParameter("f_weaver_belongto_usertype");
    return HrmUserVarify.getUser(request, response, var4, var5);
}
```

可用在Action类里获取用户

![img_20260402154646.png](./files/img_20260402154646.png)

另外也可以这样：

```java
User user =  (User)httpServletRequest.getSession(true).getAttribute("weaver_user@bean");
```

### GCONST 系统环境信息

GCONST 类可获取系统的一些环境信息，比如应用文件夹位置，日志文件夹位置等

**获取 ecology 文件夹在服务器中的绝对路径**

```java
GCONST.getRootPath()
```

**获取日志文件夹路径**

```java
GCONST.getLogPath()
```

**配置文件目录路径**

```java
GCONST.getPropertyPath()
```

### HttpManager HTTP 请求

HttpManager 类是一个用于请求 HTTP 的类，封装了 GET、POST等请求方法，比较方便

```java
HttpManager httpManager=new HttpManager();
// post 请求，载体为json
httpManager.postJsonData(Url,body.toJSONString(),headerMap)
```

### com.engine.workflow.util.CommonUtil

`delHTMLTag()` 方法可去除所有文本里的html标签

### com.engine.email.biz.Html2Text

注意：此类不是线程安全的，在多线程下可能会产生问题，不建议使用

`getContent()` 也能够去除html标签

例如获取审批意见，是会带上html标签的，可以使用 `Html2Text.getContent()` 去除标签

```java
// 1234
System.out.println(Html2Text.getContent("<div><p>1234</p></div>"));
```

### DefaultWeaTable 表格组件

很多前端的表格组件中的数据和样式并非在前端实现，而是在后端使用 AbstractWeaTable的实现类来生成表格数据和样式，比如列名、数据操作等。

DefaultWeaTable是 AbstractWeaTable的实现类，可以用此类来获取前端表格的数据、sql等信息。

关于表格组件SDK说明：[泛微在线文档](https://e-cloudstore.com/doc.html?appId=da5134c3c57f4c6f87815cf1473f674f)

#### 前端表格数据生成原理

在前端表格组件中会请求一个接口来获取表格的 sessionKey，是先在后端中构建表格组件类，也就是AbstractWeaTable的实现类，把数据sql、表格样式等信息写进去，并将此表格组件类与 sessionKey 绑定。

![img_20260402154738.png](./files/img_20260402154738.png)

获取 sessionKey 后，前端会调用一个通用接口：`/api/ec/dev/table/datas`，并携带此 sessionKey，来获取表格数据、列名、分页等信息，此接口就是用此 sessionKey 来获取之前的AbstractWeaTable实现类对象来获取表格信息，生成表格数据。

#### 修改表格 SQL

可以在后端做个接口拦截，拦截生成 sessionKey 的接口，获取表格对象，使用该表格对象修改表格的 SQL

![img_20260402154750.png](./files/img_20260402154750.png)

可以在前端接口请求时添加标记参数，来在后端判是否是前端指定位置请求的接口。

在后端接口拦截中通过接口返回数据获取到 sessionKey，然后获取表格对象

```java
String sessionKey = result.getString("sessionkey");
DefaultWeaTable table;
try {
    // 根据 sessionKey 获取表格对象
    table = WeaTableTools.checkTableStringConfig(sessionKey, DefaultWeaTable.class);
} catch (Exception e) {
    log.error("获取表格对象发生异常", e);
    return param.getData();
}
```

然后就能获取到表格数据的 sql 语句

```java
String sqlWhere = table.getSqlwhere();
String sqlForm = table.getSqlform();
String backFields = table.getBackfields();
```

表格组件中也提供了 sql setter 方法，可以将修改的 sql 传入 setter 方法中，例如修改 where 条件

```java
table.setSqlwhere(sqlWhere);
```

对表格对象进行修改后，需要调用 `WeaTableTools.setTableStringVal(sessionKey, WeaTableTools.toTableString(table))` 方法，这样才能让修改生效。

**案例：**

DefaultWeaTableColumn 类可用于修改标准列表的sql，对列表进行改造：

[后端-接口拦截 修改table查询sql 报表改造](后端-接口拦截%20修改table查询sql%20报表改造.md)

#### 添加列表字段

```java
DefaultWeaTable table = checkTableStringConfig(dataKey, DefaultWeaTable.class);
// 获取列表所有字段
List columns = table.getColumns();
DefaultWeaTableColumn column = new DefaultWeaTableColumn();
// 是否显示
column.setDisplay(WeaBooleanEnum.TRUE);
// 列名
column.setText("暂挂开始时间");
// 数据库字段名
column.setColumn("stop_start_time");
// 排序数据库字段名
column.setOrderkey("stop_start_time");
columns.add(column);
table.setColumns(columns);
```

可以参考标准文件中的代码：

col 里的属性就是 DefaultWeaTableColumn 类中的成员

```java
var35 = var35 + "<col width=\"10%\" display=\"false\"  text=\"\" column=\"createrid\" orderkey=\"t1.createrid\" transmethod=\"com.engine.odoc.util.OdocListShowNameUtil.getUserName\" />";
```

需要注意添加的字段在 DefaultWeaTable 的 Backfields 中必须要有

#### 示例-添加操作菜单

**效果：**

![img_20260402155103.png](./files/img_20260402155103.png)

目前还不知道如何设置按钮的点击动作，目前点击是没有反应的

需要调用 `setTransmethod()` 方法设置转换方法，改为自己的类方法路径，这样才能显示新添加的按钮，这个转换方法就是设置每个按钮是否显示，返回的是一个List列表，列表元素的索引就是按钮的索引，元素值为 true 表示显示按钮

![img_20260402155122.png](./files/img_20260402155122.png)

原来的转换方法只有到11的按钮索引，所以需要自己建立一个方法，添加新按钮的索引12

```java
public List<String> getOperations(String requestId, String params1, String params2) {
    OdocCustomListUtil util = new OdocCustomListUtil();
    List<String> operationShow = util.getWFSearchResultOperation(requestId, params1, params2);
    operationShow.add("true");
    return operationShow;
}
```

**添加操作菜单：**

```java
DefaultWeaTable table = checkTableStringConfig(dataKey, DefaultWeaTable.class);
table.getOperates().getPopedom()
        .setTransmethod("com.customization.yll.liuzhousteel.util.StopOdocListOperateUtil.getOperations");
List operates = table.getOperates().getOperate();
DefaultWeaTableOperate operate = new DefaultWeaTableOperate();
// 按钮id（索引），注意不能重复
operate.setIndex("12");
// 按钮名称
operate.setText("启用");
operate.setHref("javascript:window.openWorkflow()");
operate.setIsalwaysshow("true");
operates.add(operate);
table.getOperates().setOperate(operates);
```

参考：[泛微在线文档](https://e-cloudstore.com/doc.html?appId=da5134c3c57f4c6f87815cf1473f674f)

### ConstantsUtil 常量工具类

该工具类包含了多个系统页面访问地址常量

```java
public class ConstantsUtil {
    public static String INTEGRATION_OFS_RESTURL = "/integration/ofs/OfsRestServices.jsp";
    public static String INTEGRATION_OFS_EXAMPLE = GCONST.getContextPath() + "/integration/ofssend/OfsExample.jsp";
    public static String INTEGRATION_OFS_APPURL = GCONST.getContextPath() + "/mobile/plugin/1/view.jsp";
    public static String INTEGRATION_OFS_APPURLE9 = GCONST.getContextPath() + "/spa/workflow/static4mobileform/index.html";
    public static String INTEGRATION_OFS_SSOLOGIN = GCONST.getContextPath() + "/login/VerifySSoLogin.jsp";
_LOGIN = "/login/VerifyEimLogin.jsp?gopage=/wui/main.jsp&logintype=1&loginid=EIM_USERNAME&userpassword=EIM_PASSWORD";
    public static String INTEGRATION_RTX_LOGIN = "/login/RTXClientLoginOA.jsp?gopage=/wui/main.jsp";
    public static String INTEGRATION_RTX_LOGIN1 = "/login/VerifyRtxLogin.jsp";
    public static String INTEGRATION_SAP_LIB1 = GCONST.getContextPath() + "/integration/sapjar/sapjco-2-3/Linux amd64.zip";
    public static String INTEGRATION_SAP_LIB2 = GCONST.getContextPath() + "/integration/sapjar/sapjco-2-3/Linux ia64.zip";
    public static String INTEGRATION_SAP_LIB3 = GCONST.getContextPath() + "/integration/sapjar/sapjco21P_10-20007302.zip";
    public static String INTEGRATION_SAP_LIB4 = GCONST.getContextPath() + "/integration/sapjar/sapjco-2-3/Linux 32.zip";
    public static String INTEGRATION_SAP_LIB5 = GCONST.getContextPath() + "/integration/sapjar/compat-libstdc++-33-3.2.3-69.el6.x86_64.rpm";
    public static String INTEGRATION_SAP_LIB6 = GCONST.getContextPath() + "/integration/sapjar/sapjco-2-3/window amd64.zip";
    public static String INTEGRATION_SAP_LIB7 = GCONST.getContextPath() + "/integration/sapjar/sapjco21P_10-20007306.zip";
    public static String INTEGRATION_SAP_LIB8 = GCONST.getContextPath() + "/integration/sapjar/sapjco-2-3/windows 32bit.zip";
    public static String INTEGRATION_OUTTER_LOGIN = GCONST.getContextPath() + "/interface/Entrance.jsp";
    public static String INTEGRATION_OUTTER_LOGIN_GBK = GCONST.getContextPath() + "/interface/Entrance_gbk.jsp";
    public static String INTEGRATION_OUTTER_IMAGE = GCONST.getContextPath() + "/page/element/outterSys/resource/image/outterdefaultimag.png";
    public static String INTEGRATION_OUTTER_ACCOUNTSET = "/integration/accountSetting/";
    public static String INTEGRATION_OUTTER_ACCOUNTSET1 = GCONST.getContextPath() + "/spa/integration/static4engine/engine.html#/main/integration/accountSetting/";
    public static String INTEGRATION_DATASOURCE_UPLOADFILE = GCONST.getContextPath() + "/api/doc/upload/uploadFile";
    public static String INTEGRATION_WORKFLOW_URL = GCONST.getContextPath() + "/workflow/request/ViewRequest.jsp";
    public static String INTEGRATION_WORKFLOW_URL_NONE = "/workflow/request/ViewRequest.jsp";
    public static String INTEGRATION_WORKFLOW_PICTUREURL = "/workflow/request/WorkflowRequestPictureInner.jsp";
    public static String INTEGRATION_EXPDOC_EXPORT = GCONST.getContextPath() + "/workflow/request/ExportRequest.jsp";
    public static String INTEGRATION_COMMON_DOWNFILE = GCONST.getContextPath() + "/weaver/weaver.file.FileDownload";
    public static String INTEGRATION_COMMON_MAIN = GCONST.getContextPath() + "/wui/index.html#/main";
    public static String INTEGRATION_COMMON_LOGIN_OA = GCONST.getContextPath() + "/login/OALogin.jsp";
    public static String INTEGRATION_LDAP_DOWNLOAD = GCONST.getContextPath() + "/login/LdapAttachmentDownload.jsp";
    public static String INTEGRATION_SCHEDULE_RESTURL = GCONST.getContextPath() + "/schedule/restnew";

    public ConstantsUtil() {
    }
}
```

### weaver.general.Util 工具类

该工具包含了许多实用方法

#### 多语言解析 formatMultiLang()

例如解析多语言文本中的中文，中文标识为 7

```java
String str = "~`~`7 中华铅笔`~`8 `~`9 中華鉛筆`~`12 `~`15中華鉛筆`~`~";
System.out.println(Util.formatMultiLang(str,"7"));
```

### ConstantsMethodUtil

#### 获取系统访问地址 getOaAddress()

是获取系统设置中的访问地址，可能获取的不正确，因为如果设置中没有配外网地址，则获取的是内网地址，如果配置了 ngixn 代理，系统中没有配置 nginx 代理地址的话也只是获取本服务器的系统访问地址。

```java
ConstantsMethodUtil.getOaAddress();
```

### ExcelFile 表格文件

ExcelFile类可快速创建 excel 文件，可用于代码创建excel并导出，示例：

前端调用接口后，然后将浏览器地址切换到 `/weaver/weaver.file.ExcelOut` 就可以下载到文件

```java
@Path("/export")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String export(@Context HttpServletRequest request, @Context HttpServletResponse response){
    ExcelFile excelFile = new ExcelFile();
    excelFile.init();
    ExcelSheet sheet = new ExcelSheet();
    // 获取下一行
    ExcelRow row = sheet.newExcelRow();
    // 向该行的单元格写入信息
    row.addStringValue("序号");
    row.addStringValue("文件目录");
    row.addStringValue("文件类型");
    row.addStringValue("多维度标签");
    row.addStringValue("文件名称");
    row.addStringValue("文件编号");
    row.addStringValue("版本");
    row.addStringValue("发布日期");
    excelFile.setFilename("统计报表") ;
    excelFile.addSheet("统计报表", sheet) ;
    request.getSession(true).setAttribute("ExcelFile",excelFile);
    JSONObject result = new JSONObject();
    result.put("status", "S");
    return result.toJSONString();
}
```

### 获取html标签名称

**应用场景:**

在多语言下获取标签的名称，比如表单字段的名称，就可以根据字段的标签id获取到对应语言的标签名称。
例如下面的文档标签页名称，就是根据标签id获取到对应语言的名称  

![img_20260402155235.png](./files/img_20260402155235.png)

**使用**

param1: 标签id，在HtmlLabelInfo表里可查到 
param2: 用户的语言

```java
SystemEnv.getHtmlLabelName(18332, this.user.getLanguage())
```

## 数据库表

[e9 数据库表说明及常用sql](e9%20数据库表说明及常用sql.md)

## 契约锁

### 查询流程中的契约锁签署记录

```sql
select contractid from wf_qiyuesuosignerAddLog where  id = (select max(id) from wf_qiyuesuosignerAddLog where requestid=?)
```

**字段说明：**

| 字段名 (field)       | 值 (value)           |
| ----------------- | ------------------- |
| requestid         | 1315920             |
| nodeid            | 0                   |
| contractId        | 3390549900611064749 |
| setid             | NULL                |
| serverId          | NULL                |
| userid            | 25609               |
| usertype          | 0                   |
| ipaddress         | 10.8.54.141         |
| createDate        | 2025-08-06          |
| id                | 19                  |
| createTime        | 11:14:27            |
| requestJson       | NULL                |
| signUrl           | NULL                |
| signatoryName     | 柳州五菱新能源汽车有限公司       |
| signatoryContract | 15778286003         |
| signatoryType     | COMPANY             |
| actionType        | CORPORATE           |
| actionContract    | 15778286003         |
| actionUserids     | (空)                 |
| actionSealids     | (空)                 |
| nodeids           | 0                   |
### 查询流程中的签署文档

```sql
SELECT * FROM wf_qiyuesuoCreateDocLog WHERE requestid =1315920
```

**查询结果：**

| id | requestid | contractId | documentId | docid | imagefileid | userid | usertype | ipaddress | createDate | createTime | templateId | isViewImage |
|----|-----------|------------|------------|-------|-------------|--------|----------|-----------|------------|------------|------------|-------------|
| 19 | 1315920 | 3390549900611064749 | 3390549899516351400 | 1357042 | 4475248 | 25609 | 0 | 10.8.54.141 | 2025-08-06 | 11:14:26 | NULL | 0 |

**字段说明**

1. **documentId**：契约锁文档id
2. **imagefileid**：所属附件id，这个附件并不是签署文件，签署文件需要通过契约锁获取
3. **docid**：所属文档

### 获取契约锁签署文档

可根据查询流程中的签署文档，获取到文档id，然后调用契约锁接口 `/document/download` 下载文件

## JSP 页面

### ecology编译的jsp地址

```
ecology/WEB-INF/work/_jsp/
```

## 运维与调试

### 在线获取线程日志

线程日志可用于排查线程死锁、JVM内存溢出等问题

访问 `profiler/profiler.jsp`

### 日志说明

<style>
  .log-desc-table {
    width: 100%;
    table-layout: fixed;
  }

  .log-desc-table td:nth-child(2),
  .log-desc-table th:nth-child(2) {
    white-space: normal !important;
    word-break: break-all !important;
    overflow-wrap: anywhere !important;
    overflow: visible !important;
    text-overflow: clip !important;
  }
  </style>
<table class="log-desc-table">
  <colgroup>
    <col style="width: 22%;">
    <col style="width: 24%;">
    <col style="width: 32%;">
    <col style="width: 22%;">
  </colgroup>
  <tbody>
    <tr>
      <td>日志详解</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>1、ecology日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>memory_出问题日期.log</td><td>/ecology/log/mem/目录下</td><td>jvm内存日志（文件名所带日期，代表是日期当天的日志）</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>conn_出问题日期.log</td><td>/ecology/log/conn目录下</td><td>数据库连接泄漏日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>sqlcount_出问题日期.log</td><td>/ecology/log/sql/<br><br>目录下</td><td>查询结果超过1万条数据的sql记录日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>ecology</td><td>/ecology/log/目录下</td><td>ecology系统输出日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>sqltime_出问题日期.log</td><td>/ecology/log/sql/目录下</td><td>超过10秒响应的sql记录日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>warning_出问题日期.log</td><td>/ecology/log/warning/目录下</td><td>警告日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/ecology/log/thread/出问题日期/目录下</td><td>jvm线程日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>status_出问题日期.log</td><td>/ecology/log/status/目录下</td><td>系统启动重启日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr>
      <td>2、安全包日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>systemRunInfoyyyy-MM-dd.log</td><td>ecology/WEB-INF/securitylog/目录下</td><td>当发生安全问题和功能性问题的时候运行时错误日志</td><td>不自动清理且运维平台无相关清理选项</td></tr>
    <tr><td>systemSecurityyyyy-MM-dd.log</td><td>ecology/WEB-INF/securitylog/目录下</td><td>当发生安全问题和功能性问题的时候安全拦截日志</td><td>不自动清理且运维平台无相关清理选项</td></tr>
    <tr>
      <td>3、Resin日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>stdout-yyyyMMdd.log.gz</td><td>/weaver/Resin/log/目录下</td><td>记录应用输出日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>stderr-yyyyMMdd.log.gz</td><td>/weaver/Resin/log/目录下</td><td>记录应用错误日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr><td>jvm-default.log</td><td>/weaver/Resin/log/目录下</td><td>resin启动日志1</td><td>不自动清理且运维平台无相关清理选项</td></tr>
    <tr><td>watchdog-manager.log</td><td>/weaver/Resin/log/目录下</td><td>resin启动日志2</td><td>不自动清理且运维平台无相关清理选项</td></tr>
    <tr><td>access-日期.log</td><td>/weaver/Resin/logs/目录下</td><td>用户访问OA的记录</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr>
      <td>4、集成日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>integration.log_yyyymmdd.log</td><td>/ecology/log/integration/目录下</td><td>集成相关的功能问题日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr>
      <td>5、sqllog日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>yyyy-MM-dd hh-mm-ss.log</td><td>/ecology/WEB-INF/sqllog/目录下</td><td>当发生业务数据变更的时候记录日志</td><td>可通过运维平台-&gt;常用工具-&gt;日志文件管理设置</td></tr>
    <tr>
      <td>6、 运维平台3.0日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>6.1 运维平台3.0多出来的OA<br>监控日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>concurrent_出问题日期.log</td><td>/weaver/monitor3/app/data/concurrent目录下</td><td>线程并发日志（按照时间展示线程死锁状态等的数量信息）</td><td>十五天自动清理</td></tr>
    <tr><td>cpu_出问题日期.log</td><td>/weaver/monitor3/app/data/cpu目录下</td><td>按照时间点记录CPU的占用情况</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/monitor3/app/data/EbridgeMem目录下</td><td>云桥内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/monitor3/app/data/Ebridgethread/具体日期/目录下</td><td>云桥线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/monitor3/app/data/em7Mem目录下</td><td>em7内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/monitor3/app/data/EM7thread目录下</td><td>em7线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/monitor3/app/data/EmobileMem目录下</td><td>em6内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/monitor3/app/data/Emobilethread目录下</td><td>em6线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>memory出问题日期.log</td><td>/weaver/monitor3/app/data/ESearchmem目录下</td><td>微搜内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>pool_出问题日期_ecology.log</td><td>/weaver/monitor3/app/data/pool目录下</td><td>数据库连接池（数据库已用连接数，可用连接数，剩余连接数）</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/monitor3/app/data/thread目录下</td><td>40秒每次打印总线程</td><td>十五天自动清理</td></tr>
    <tr>
      <td>6.2、运维平台3.0自身日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>jvm-default.log</td><td>/weaver/monitor3/log目录下</td><td>运维平台启动日志1</td><td>七天自动清理</td></tr>
    <tr><td>stderr.log</td><td>/weaver/monitor3/log目录下</td><td>记录运维平台应用报错日志</td><td>七天自动清理</td></tr>
    <tr><td>stdout.log</td><td>/weaver/monitor3/log目录下</td><td>记录运维平台应用输出日志</td><td>七天自动清理</td></tr>
    <tr><td>watchdog-manager.log</td><td>/weaver/monitor3/log目录下</td><td>运维平台启动日志2</td><td>七天自动清理</td></tr>
    <tr><td>monitor.log</td><td>/weaver/monitor3/app/log/app目录下</td><td>运维平台系统输出日志</td><td>七天自动清理</td></tr>
    <tr><td>env.properties</td><td>monitor3/app/config/目录下</td><td>客户环境信息配置文件</td><td>不自动清理且运维平台无相关清理选项</td></tr>
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>7、运维平台2.0日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>7.1、运维平台2.0多出来的OA监控日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>concurrent_出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/concurrent目录下</td><td>线程并发日志（按照时间展示线程死锁状态等的数量信息）</td><td>十五天自动清理</td></tr>
    <tr><td>cpu_出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/cpu目录下</td><td>按照时间点记录CPU的占用情况</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/EbridgeMem目录下</td><td>云桥内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/Resin/monitor/resin/app/data/Ebridgethread/具体日期/目录下</td><td>云桥线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/em7Mem目录下</td><td>em7内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/Resin/monitor/resin/app/data/EM7thread目录下</td><td>em7线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>mem_出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/EmobileMem目录下</td><td>em6内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/Resin/monitor/resin/app/data/Emobilethread目录下</td><td>em6线程日志</td><td>十五天自动清理</td></tr>
    <tr><td>memory出问题日期.log</td><td>/weaver/Resin/monitor/resin/app/data/ESearchmem目录下</td><td>微搜内存日志</td><td>十五天自动清理</td></tr>
    <tr><td>pool_出问题日期_ecology.log</td><td>/weaver/Resin/monitor/resin/app/data/pool目录下</td><td>数据库连接池（数据库已用连接数，可用连接数，剩余连接数）</td><td>十五天自动清理</td></tr>
    <tr><td>thread_出问题具体时间.log</td><td>/weaver/Resin/monitor/resin/app/data/thread目录下</td><td>40秒每次打印总线程</td><td>十五天自动清理</td></tr>
    <tr>
      <td>7.2、运维平台2.0自身日志</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td>日志名称</td>
      <td>日志原始路径</td>
      <td>日志详细介绍</td>
      <td>日志清理间隔</td>
    </tr>
    <tr><td>jvm-default.log</td><td>/weaver/Resin/monitor/resin/log目录下</td><td>运维平台启动日志1</td><td>七天自动清理</td></tr>
    <tr><td>stderr.log</td><td>/weaver/Resin/monitor/resin/log目录下</td><td>记录运维平台应用报错日志</td><td>七天自动清理</td></tr>
    <tr><td>stdout.log</td><td>/weaver/Resin/monitor/resin/log目录下</td><td>记录运维平台应用输出日志</td><td>七天自动清理</td></tr>
    <tr><td>watchdog-manager.log</td><td>/weaver/Resin/monitor/resin/log目录下</td><td>运维平台启动日志2</td><td>七天自动清理</td></tr>
    <tr><td>monitor.log</td><td>/weaver/Resin/monitor/resin/app/log/app目录下</td><td>运维平台系统输出日志</td><td>七天自动清理</td></tr>
  </tbody>
</table>

### 修改集成日志级别

#### 通过配置文件修改

修改 `ecology/WEB-INF/log4jinit.properties` 文件，找到 `log4j.logger.integration` 属性，在这里可以修改日志级别，比如需要修改为 DEBUG 级别：

```properties
log4j.logger.integration=DEBUG,integration
```

修改之后需要重启服务

#### 通过代码动态修改

在代码中调用即可修改日志级别，不需要重启服务，但是只能在很短时间内生效，一旦日志对象重新创建（会重置日志级别）就会失效。

Logger 为 `org.apache.log4j.Logger` 包下的

```java
// 获取 Logger
Logger logger = Logger.getLogger("integration");

// 修改日志级别
logger.setLevel(Level.DEBUG);

// 设置根 Logger
Logger.getRootLogger().setLevel(Level.DEBUG);
```

### 查询接口访问日志

`Resin/logs/access.log` 可以查看接口的访问日志，如果想看是否调用了某个操作，可以到这个日志查看是否有这个操作的接口

### 应用调试与热部署

可以使用 arthas 对系统进行诊断，比如解决 jar 包冲突，查看类的信息等

地址：[arthas](https://arthas.aliyun.com/)

## 多语言处理

可以使用标准工具对多语言进行处理，详细可以看标准工具章节。另外也可以使用下面的代码对多语言进行处理。

传入中文名和英文名，转换成多语言格式

```java
public String getMultiLanguage(String cnName,String enName) {
    recordSet.executeQuery("SELECT id,language from syslanguage");
    int chineseId = 0;
    int englishId = 0;
    while (recordSet.next()) {
        if ("简体中文".equals(recordSet.getString("language"))) {
            chineseId = recordSet.getInt("id");
        }
        if ("English".equals(recordSet.getString("language"))) {
            englishId = recordSet.getInt("id");
        }
    }
    StringBuilder builder = new StringBuilder("~`");
    builder.append(String.format("~`%d %s",chineseId, cnName));
    if (!enName.isEmpty()) {
        builder.append(String.format("~`%d %s",englishId, enName));
    }
    builder.append("`~");
    return builder.toString();
}
```


## 开发问题

### 系统环境

#### 读取文件异常 java.nio.file.InvalidPathException: Malformed input or input contains unmappable characters

执行以下代码会出现该异常，这是由于Paths 类无法解析文件路径，当文件路径包含中文时就会出现异常，这可能和 jvm 的编码配置有关

```java
Files.exists(Paths.get(imageFilePath))
```

**解决方法：**

检查 jvm 编码配置，将以下 jsp 文件放入服务器中，放到 ecology/jsp/ 路径下，无需重启，访问 `[系统地址]/jsp/test1.jsp`

![jsp](./files/test1.jsp)

访问页面后如果输出的 `sun.jnu.encoding` 不是 UTF-8 或者 GBK，则表示 jvm 编码配置不正确

```
user.country=US; user.language=en; file.encoding=GBK; sun.jnu.encoding=ANSI_X3.4-1968;
```

需要修改 `/weaver/Resin/conf/resin.properties` 文件，在 `jvm_args` 属性最后面添加 `-Dsun.jnu.encoding=UTF-8` ，如果系统编码为 UTF-8，则为 `-Dsun.jnu.encoding=UTF-8` ，如果系统编码为 GBK，则为 `-Dsun.jnu.encoding=GBK` ，系统编码可在服务器上执行 locale 进行查看，输出的 LANG 就为系统编码，然后重启。

#### 提示出错，但是日志找不到报错

![img_20260402162837.png](./files/img_20260402162837.png)

一般对于 Error 级别的异常（比如 ClassNotFound、NoSuchMethod 异常）都可以在 ecology 日志和 Resin 日志中找到，对于其它运行时异常，你必须在代码中进行捕获，让它打印到集成日志中。

对于上图中的报错，如果在集成日志中找不到报错（如果你已经捕获了 Exception 异常），那可能就是因为打包时少了文件 ClassNotFound 或 NoSuchMethod 异常，这通常能在 Resin 错误日志 或 ecology 日志中找到报错信息，不过也有可能在所有日志中都找不到报错信息，你需要把包重新打上去，确保没有漏文件。
## 代码测试

### 在测试中使用真实环境
可能在测试中需要调用数据库，或者存入缓存，可以使用 `weaver.general.GCONST `来使用真实环境

```java
@Test
public void test() {
    // 必需设置 GCONST，设置 ecology 应用路径
    GCONST.setRootPath("/Users/yaolilin/weaver/ecology/");
    // 设置服务名称，固定为 ecology
    GCONST.setServerName("ecology");
    // 查询数据库
    RecordSet recordSet = new RecordSet();
    recordSet.executeQuery("select id from hrmresource where id=22");
    recordSet.next();
    int id = recordSet.getInt("id");
    Assert.assertEquals(22, id);
}
```

可以在单元测试的初始化方法中添加 GCONST ，这样在每个测试用例中都会生效。

```java
@Before
public void setUp() throws Exception {
    GCONST.setRootPath("/Users/yaolilin/weaver/ecology/");
    GCONST.setServerName("ecology");
}
```

### Cmd 代理类单元测试

测试 cmd 代理类，需要 mock execute 方法传入的 command 对象，让代理类能够正常执行 nextExecute() 方法，并模拟被代理类执行的返回结果。

cmd 代理类案例如下，这是一个代理会议数据获取的 cmd 代理类，在原 cmd 执行后过滤 cmd 执行结果中的会议数据。

```java
@CommandDynamicProxy(target = GetRoomReportDataCmd.class, desc = "会议室数据接口动态代理，过滤会议，只保留场馆类会议")
public class RoomReportDataCmdProxyWithMeetingFilter extends AbstractCommandProxy<Map<String, Object>> {
    private static final String ENABLE_FILTER_FLAG = "meetingFilterType";
    private final IntegrationLog log = new IntegrationLog(RoomReportDataCmdProxyWithMeetingFilter.class);

    private final RecordSet recordSet;
    private final MeetingFilterHelper meetingFilterHelper;

    public RoomReportDataCmdProxyWithMeetingFilter(RecordSet recordSet, MeetingFilterHelper meetingFilterHelper) {
        this.recordSet = recordSet;
        this.meetingFilterHelper = meetingFilterHelper;
    }

    public RoomReportDataCmdProxyWithMeetingFilter() {
        this.recordSet = new RecordSet();
        this.meetingFilterHelper = new MeetingFilterHelper(recordSet);
    }

    @Override
    public Map<String, Object> execute(Command<Map<String, Object>> command) {
        GetRoomReportDataCmd cmd = (GetRoomReportDataCmd) command;
        Map<String, Object> result = nextExecute(cmd);

        Map<String, Object> params = cmd.getParams();
        String meetingFilterType = Util.null2String(params.get(ENABLE_FILTER_FLAG));
        if (meetingFilterType.isEmpty()) {
            return result;
        }

        log.info("启用会议过滤");
        if (!result.containsKey("datas")) {
            return result;
        }

        filterMeeting(result, meetingFilterType);
        return result;
    }
}
```

单元测试类如下：
```java
class RoomReportDataCmdProxyWithMeetingFilterTest {
    @Mock
    private RecordSet recordSet;
    @Mock
    private MeetingFilterHelper meetingFilterHelper;
    private RoomReportDataCmdProxyWithMeetingFilter cmd;
    @Mock
    private GetRoomReportDataCmd getRoomReportDataCmd;
    
    // 原 cmd 的执行结果，这里数据过多不进行展示
    public static final String MOCK_JSON = "{}";

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);

        cmd = new RoomReportDataCmdProxyWithMeetingFilter(recordSet, meetingFilterHelper);
        when(meetingFilterHelper.isMeetingType(eq(7), anyInt())).thenReturn(true);
        JSONObject json = JSONObject.parseObject(MOCK_JSON);
        when(getRoomReportDataCmd.execute(any())).thenReturn(json);

        Map<String, Object> params = new HashMap<>(10);
        params.put("meetingFilterType", 1);
        when(getRoomReportDataCmd.getParams()).thenReturn(params);
    }

    @Test
    void execute() {
        Map<String, Object> result = cmd.execute(getRoomReportDataCmd);
        System.out.println("执行结果：" + JSON.toJSONString(result));
        Assertions.assertFalse(result.isEmpty());

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> rooms = (List<Map<String, Object>>) result.get("datas");
        // 遍历所有会议室
        for (Map<String, Object> room : rooms) {
            if (room.containsKey("info")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> days = (List<Map<String, Object>>) room.get("info");
                Map<String, Object> day = days.get(0);
                if (day.containsKey("meetings")) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> meetings = (List<Map<String, Object>>) day.get("meetings");
                    Assertions.assertEquals(1, meetings.size());

                    Map<String, Object> meeting = meetings.get(0);
                    String id = Util.null2String(meeting.get("id"));
                    Assertions.assertEquals("7", id);

                    if (day.containsKey("title")) {
                        @SuppressWarnings("unchecked")
                        List<List<String>> title = (List<List<String>>) day.get("title");
                        Assertions.assertEquals(1, title.size());

                        List<String> titleItem = title.get(0);
                        Assertions.assertTrue(titleItem.contains("会议名称:测试"));
                    }

                    Integer content = MapUtil.getInt(day, "content");
                    Assertions.assertEquals(1, content);
                }
            }
        }
    }
}
```

需要对测试类的依赖使用 Mockito 进行 Mock
```java
    @Mock
    private RecordSet recordSet;
    @Mock
    private MeetingFilterHelper meetingFilterHelper;
```
将 Mock 的依赖传入测试类中

```java
cmd = new RoomReportDataCmdProxyWithMeetingFilter(recordSet, meetingFilterHelper);
```

为了能够让 cmd 代理类能够正常运行，需要对 execute 方法传入的参数对象进行 Mock，并 Mock command 对象的执行结果，来模拟代理类运行过程中被代理类的执行。

```java
 // mock 被代理类 （command对象），将被传入 execute 方法中
 @Mock
 private GetRoomReportDataCmd getRoomReportDataCmd;
```

Mock command 对象的执行结果

```java
JSONObject json = JSONObject.parseObject(MOCK_JSON);
when(getRoomReportDataCmd.execute(any())).thenReturn(json);
```

测试时将 mock 的被代理类传入 execute 方法中

```java
Map<String, Object> result = cmd.execute(getRoomReportDataCmd);
```

之后就能对测试类的执行结果进行校验，验证是否过滤会议数据

## 依赖引入建议

### 构造方法传入依赖

对于非顶层类，为了能更好的进行单元测试，建议类中的外部依赖不要在类中 new 生成对象，而是通过构造函数传入，如：

```java
private final RecordSet recordSet;
private final MeetingFilterHelper meetingFilterHelper;

public RoomReportDataCmdProxyWithMeetingFilter(RecordSet recordSet, MeetingFilterHelper meetingFilterHelper) {
    this.recordSet = recordSet;
    this.meetingFilterHelper = meetingFilterHelper;
}
```

这样测试时能直接对依赖对象进行 Mock
### 无参构造和有参构造组合传入依赖

对于顶层类，也就是在项目中没有调用此类的，比如 Web 接口类、流程 Action 类和 Cmd 代理类，可以使用有参构造和无参构造的组合，如：

```java
private final RecordSet recordSet;
private final MeetingFilterHelper meetingFilterHelper;

public RoomReportDataCmdProxyWithMeetingFilter(RecordSet recordSet, MeetingFilterHelper meetingFilterHelper) {
    this.recordSet = recordSet;
    this.meetingFilterHelper = meetingFilterHelper;
}

public RoomReportDataCmdProxyWithMeetingFilter() {
    this.recordSet = new RecordSet();
    this.meetingFilterHelper = new MeetingFilterHelper(recordSet);
}
```

为什么使用此组合？因为有参构造在进行单元测试时可以传入 Mock 对象，而无参构造可以在系统运行时不影响对象生成，因为像流程 Action 类和 Cmd 代理类这些类都是要求有无参构造才能生成对象。

### 增加 setter 方法（非顶层类不推荐）

其次可以在成员声明中直接创建对象，并提供 setter 方法，这样不会有有参构造，代码更简洁，在单元测试时可以使用 setter 方法传入 Mock 对象。

对于非顶层类我不推荐这么做，这不是一个良好的规范，对于类中的外部依赖应该通过构造方法传入，而不是在类中创建。

注意：使用此方法要注意在测试时通过 new 直接创建对象会不会发生异常，如 `RecordSet` 类在单元测试时直接创建对象会发生异常。

```java
@Setter
public class RoomReportDataCmdProxyWithMeetingFilter extends AbstractCommandProxy<Map<String, Object>> {
    // 注意：单元测试时此对象不能创建，会发生异常，不要使用此方式创建对象，应该通过构造方法传入
    private final RecordSet recordSet = new RecordSet();
    // 对于此类在单元测试中不会发生异常，该类的构造方法不会抛出异常
    private final MeetingFilterHelper meetingFilterHelper= new MeetingFilterHelper(recordSet);
}
```

## 开发案例

### 流程

- [案例-公文流程实现撤回时收回意见](案例/案例-公文流程实现撤回时收回意见.md)
- [案例-流程根据对照流程自动流转](案例/案例-流程根据对照流程自动流转.md)
- [后端-开发案例 第三方同步流程数据到OA，点待办单点到第三方](案例/后端-开发案例%20第三方同步流程数据到OA，点待办单点到第三方.md)
- [在后端添加流程按钮](案例/在后端添加流程按钮.md)

### 集成

- [案例-柳州城市职业学院金智消息推送和流程待办推送案例分享](案例/案例-柳州城市职业学院金智消息推送和流程待办推送案例分享.md)
