📋 文档信息项目说明文档名称泛微OA-金蝶API接口通用中间件说明文档版本v2.0创建日期2025年11月24日更新日期2025年12月20日作者徐东瑞当前功能API代理 + Token自动管理 + 前端框架 + 流程集成🎯 变更记录版本日期变更内容v1.02025-10-22初始版本，包含基础API代理功能v2.02025-11-24新增前端同步框架、流程集成、附件上传、权限控制等完整功能🎯 项目概述1.1 项目背景为解决泛微E9 OA系统与金蝶云星空旗舰版ERP系统之间的数据交互需求，开发一套完整的API代理中间件。该中间件不仅提供API代理功能，还包含前端同步框架、自动Token管理、附件上传、权限控制等完整解决方案，实现安全、稳定、高效的数据同步。1.2 已实现功能✅ **统一接入**：提供统一的API入口，简化前端调用✅ **Token管理**：自动管理金蝶API访问令牌，每小时自动刷新✅ **API代理**：完整的请求转发和响应处理机制✅ **附件上传**：支持文件流式上传到金蝶系统，避免内存溢出✅ **权限校验**：基于角色的细粒度访问控制✅ **前端框架**：可复用的JavaScript同步框架，支持步骤化执行，支持按条件执行步骤，支持延迟执行步骤✅ **流程集成**：与泛微E9流程表单无缝集成✅ **错误处理**：统一的错误解析和用户提示✅ **进度显示**：可视化同步进度提示🏗️ 系统架构2.1 整体架构图┌─────────────────────────────────────────────────────────────┐ │ 泛微E9 OA系统 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 流程表单 │ │ Ecode后台 │ │ 定时任务 │ │ │ │ (前端代码块) │ │ (框架代码) │ │ (Token管理) │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ │ │ │ ┌───────────▼───────────┐ │ │ │ API代理中间件 │ │ │ │ KingdeeProxyServlet │ │ │ └───────────┬───────────┘ │ └──────────────────────────┼─────────────────────────────────┘ │ ┌──────────────────────────▼─────────────────────────────────┐ │ 金蝶云星空ERP │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 业务API │ │ Token服务 │ │ 附件服务 │ │ │ │ /v2/... │ │ /oauth2/... │ │ /uploadFile │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘2.2 数据流向流程发起：用户在OA流程表单提交数据框架处理：前端KingdeeSync框架拦截提交事件步骤执行：按配置步骤依次调用金蝶API代理转发：中间件验证权限并转发请求金蝶处理：金蝶系统处理业务逻辑结果返回：响应逐层返回至前端界面进度提示：用户实时查看同步进度2.3 技术栈层级技术选型前端框架JavaScript ES5+、jQuery、Promise异步中间件Java Servlet、HttpURLConnection数据库SQL Server（泛微内置）JSON处理FastJSON、原生JSON文件处理流式传输、Zip解压、AES加密权限控制泛微角色体系、基于角色的访问控制📊 数据库设计3.1 数据表结构3.1.1 Token存储表 (uf\_kingdeeAPI)CREATE TABLE [uf_kingdeeAPI] ( [id] INT IDENTITY(1,1) PRIMARY KEY, [clientid] VARCHAR(50) NULL, -- 客户端ID（固定为'weaverE9'） [clientsecret] VARCHAR(100) NULL, -- 客户端密钥 [dlyhgh] VARCHAR(20) NULL, -- 代理用户工号 [accountid] VARCHAR(50) NULL, -- 账户ID [language] VARCHAR(10) DEFAULT 'zh_CN',-- 语言 [accesstoken] TEXT NULL, -- 访问令牌（Base64编码） [tokengxsj] DATETIME NULL, -- 令牌更新时间 -- 泛微建模引擎标准字段 [modedatamodifier] INT NULL, [modedatamodifydatetime] DATETIME NULL, [formmodeid] INT NULL, [modedatacreater] INT NULL, [modedatacreatertype] INT NULL, [modedatacreatedate] DATE NULL, [modedatacreatetime] TIME NULL, [form_biz_id] VARCHAR(50) NULL, [MODEUUID] VARCHAR(36) NULL ); -- 索引优化 CREATE INDEX idx_clientid ON uf_kingdeeAPI(clientid); CREATE INDEX idx_tokengxsj ON uf_kingdeeAPI(tokengxsj);3.2 数据示例INSERT INTO [uf_kingdeeAPI] ( [id], [clientid], [clientsecret], [dlyhgh], [accountid], [language], [accesstoken], [tokengxsj], [form_biz_id], [MODEUUID] ) VALUES ( 1, 'weaverE9', '2345678910JQKAwang.', '02250', '2077625015653633024', 'zh_CN', 'OPENAPIAUTH_MjA3NzYyNTAxNTY1MzYzMzAyNF9FSExTeVN0c0xPTnBIMEVpZDJIVnpRTHd0V0o4clFtbDFldFNLRkFubjRrb2tDODNWaWs3MzR6RlRhSGQyRmFublNCaDF2d2R3SFRKN0NWdjhjRnZlWThmVm92eGR5SnNVbUZUMDY=', '2025-11-24 14:19:33', '1000799', '97ffee80-c9f1-427d-9585-fe8d7e3abdc3' );🔧 核心功能模块4.1 Token自动管理模块4.1.1 Token获取定时任务配置项说明类名weaver.houdedev.cron.GetKingdeeTokenCmd执行频率0 0 0/1 * * ?（每小时执行一次）认证地址https://ierp.houde-food.com/ierp/kapi/oauth2/getToken请求参数client\_id、client\_secret、username、accountId、nonce、timestamp、language响应处理解析access\_token并更新到数据库4.1.2 Token存储管理自动获取：定时任务每小时自动刷新Token缓存机制：中间件缓存最新Token，减少数据库查询失效处理：Token过期时自动触发重新获取多应用支持：预留多金蝶应用配置扩展4.2 API代理服务模块4.2.1 核心Servlet属性值类名KingdeeProxyServlet包路径weaver.houdedev.cron访问路径/kingdee/proxy支持方法GET（健康检查）、POST（API代理）编码设置全面UTF-8支持，解决中文乱码问题4.2.2 请求处理流程4.3 前端同步框架模块4.3.1 KingdeeSync框架特性说明类结构面向对象设计，支持链式调用步骤管理支持顺序、条件、延迟步骤进度提示可视化弹窗显示执行进度错误处理自动解析金蝶错误信息响应处理统一的数据提取机制4.3.2 核心方法// 1. 创建同步实例 var sync = new KingdeeSync({ title: "同步进度", showProgress: true, timeout: 30000 }); // 2. 添加执行步骤 sync.addStep("步骤名称", requestBuilder, dataExtractor); // 3. 添加条件步骤 sync.addStepIf(condition, "步骤名称", requestBuilder); // 4. 添加延迟步骤 sync.addDelayStep("等待", 5000); // 5. 执行流程 sync.execute(callback);4.4 附件上传模块4.4.1 文件处理流程文件获取：根据OA附件ID从ImageFile表获取文件信息格式处理：支持ZIP压缩文件自动解压加密处理：支持AES加密文件解密流式上传：分块传输，避免大文件内存溢出关联绑定：将附件与金蝶单据ID关联4.4.2 关键技术多部分表单：使用multipart/form-data格式分块传输：setChunkedStreamingMode(8192)内存优化：8KB缓冲区流式读写超时设置：大文件上传120秒超时4.5 权限控制模块4.5.1 角色权限配置// 允许访问的角色ID列表 private static final String[] ALLOWED_ROLE_IDS = {"2", "26"}; // 允许访问的角色名称列表 private static final String[] ALLOWED_ROLE_NAMES = {"系统管理员", "ERP接口访问"};4.5.2 权限验证流程用户认证：通过 HrmUserVarify.getUser()获取当前用户角色查询：查询用户关联的所有角色权限检查：匹配角色ID或角色名称访问控制：无权限用户返回403错误🚀 接口详细设计5.1 API代理接口请求方式请求地址postOA地址/kingdee/proxy功能：代理调用任意金蝶API接口请求头：Content-Type: application/json;charset=UTF-8 Cookie: JSESSIONID=... (泛微会话Cookie)请求体：{ "apiUrl": "/v2/basedata/bd_supplier/addSup", "apiResultData": { "data": [ { "name": "供应商名称", "number": "GYS2025001", ... } ] }, "fileId": [123, 456], "attachmentUploadFileArgs": { "entityNumber": "bd_supplier", "controlKey": "d8b9_attachmentpanelap", "billPkId": "供应商单据ID" } }参数说明：参数名类型必填说明apiUrlString是金蝶API路径（不包含基础URL）apiResultDataObject是金蝶API要求的完整参数格式fileIdArray否OA附件文件ID列表attachmentUploadFileArgsObject否附件上传参数，包含单据关联信息响应：{ "status": true, "data": { "金蝶API返回的原始数据" }, "message": "请求成功", "attachmentResults": { "file_123": { "status": true, "message": "上传完成", "billId": "单据ID" } } }5.2 健康检查接口请求方式请求地址getOA地址/kingdee/proxy功能：服务健康状态检查响应：{ "status": true, "message": "服务正常，当前有 1 个有效token", "currentUser": "admin", "userRoles": "系统管理员(2), ERP接口访问(26)" }5.3 错误码定义错误码说明处理建议400请求参数错误检查apiUrl和apiResultData参数格式401用户未登录或Token无效重新登录或检查Token获取任务403权限不足为用户分配系统管理员或ERP接口访问角色404接口路径不存在检查金蝶API路径是否正确500服务器内部错误查看服务器日志，联系管理员502网络连接失败检查到金蝶系统的网络连通性504请求超时适当增加timeout参数值🔧 核心代码实现6.1 KingdeeProxyServlet 核心方法6.1.1 主请求处理protected void doPost(HttpServletRequest request, HttpServletResponse response) { // 1. 设置UTF-8编码（必须最先执行） request.setCharacterEncoding("UTF-8"); response.setContentEncoding("UTF-8"); // 2. 用户身份验证和权限校验 User user = HrmUserVarify.getUser(request, response); if (!checkUserRolePermission(user)) { returnError(response, 403, "权限不足"); return; } // 3. 读取并解析请求体 String requestBody = readRequestBody(request); JSONObject params = JSON.parseObject(requestBody); // 4. 获取最新Token String accessToken = getLatestTokenFromDB(); // 5. 调用金蝶API String apiResponse = sendPostRequest(fullUrl, jsonBody, accessToken); // 6. 处理附件上传（如果存在） if (hasAttachments(params) && isSuccess(apiResponse)) { uploadAttachments(fileIds, attachmentArgs, accessToken, billId); } // 7. 返回响应 returnSuccess(response, apiResponse); }6.1.2 流式附件上传private String sendStreamingMultipartPostRequest(String url, JSONObject args, InputStream fileStream, String fileName, String accessToken) { // 1. 设置分块传输模式 connection.setChunkedStreamingMode(8192); // 2. 构建多部分表单边界 String boundary = "----WebKitFormBoundary" + UUID.randomUUID(); // 3. 流式写入表单数据 try (OutputStream os = connection.getOutputStream()) { // 写入JSON参数部分 writeFormField(os, boundary, "attachmentUploadFileArgs", args.toJSONString(), "application/json"); // 写入文件部分 writeFileField(os, boundary, "file", fileName, fileStream); // 写入结束边界 writeBoundaryEnd(os, boundary); } // 4. 读取响应 return readResponse(connection); }6.2 前端同步框架核心（ecode）6.2.1 KingdeeSync类结构function KingdeeSync(options) { this.options = options || {}; this.steps = []; // 执行步骤数组 this.stepResults = []; // 步骤结果存储 this.currentStep = 0; // 当前步骤索引 this.progressModal = null; // 进度弹窗引用 } // 添加执行步骤 KingdeeSync.prototype.addStep = function(name, requestBuilder, dataExtractor) { this.steps.push({ name: name, requestBuilder: requestBuilder, dataExtractor: dataExtractor || function(response) { return { rawData: response.data }; } }); return this; // 支持链式调用 }; // 执行步骤 KingdeeSync.prototype.executeStep = function(stepIndex, callback) { var step = this.steps[stepIndex]; var self = this; // 更新进度显示 this.updateProgress(step.name); // 构建请求数据 var requestData = step.requestBuilder(this.stepResults); // 调用API this.callKingdeeAPI(requestData).then(function(response) { if (response.status && response.data.status) { // 成功：提取数据并继续下一步 var extractedData = step.dataExtractor(response, self.stepResults); self.stepResults.push(extractedData); self.currentStep++; self.executeStep(stepIndex + 1, callback); } else { // 失败：解析错误信息 var errorDetails = self.parseKingdeeError(response); self.handleStepError(step.name, new Error(errorDetails)); } }).catch(function(error) { self.handleStepError(step.name, error); }); };🛠️ 部署指南7.1 环境要求组件要求操作系统Windows Server 2012+ / Linux CentOS 7+Java环境JDK 1.8+应用服务器泛微E9自带Resin服务器数据库SQL Server 2012+ / Oracle 11g+ / MySQL 5.7+网络OA服务器可访问金蝶云星空API泛微版本E9 9.0及以上7.2 部署步骤7.2.1 数据库准备创建数据表：通过建模引擎创建uf\_kingdeeAPI表并添加字段初始化配置：插入金蝶API认证信息权限配置：确保OA数据库用户有读写权限7.2.2 文件部署部署class文件：/ecology/WEB-INF/classes/weaver/houdedev/cron/ ├── GetKingdeeTokenCmd.class ├── KingdeeProxyServlet.class └── ...配置web.xml：<servlet> <servlet-name>KingdeeProxyServlet</servlet-name> <servlet-class>weaver.houdedev.cron.KingdeeProxyServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>KingdeeProxyServlet</servlet-name> <url-pattern>/kingdee/proxy</url-pattern> </servlet-mapping>部署前端代码：将ecode代码上传到泛微ecode后台配置为全局可用的JavaScript模块7.2.3 定时任务配置登录泛微E9管理后台进入“集成中心” → “功能集成”→ “计划任务”点击“新建”按钮配置任务参数：参数项值任务名称金蝶Token自动获取类名weaver.houdedev.cron.GetKingdeeTokenCmd执行频率0 0 0/1 * * ?状态启用描述每小时获取金蝶API访问令牌7.3 权限配置角色分配：OA后台进入 组织权限中心 → 权限管理 → 角色设置为需要调用金蝶接口的用户分配“系统管理员”或“ERP接口访问”角色🔍 使用示例8.1 前端调用示例8.1.1 基本API调用// 供应商查询示例 jQuery.ajax({ url: '/kingdee/proxy', type: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ "apiUrl": "/v2/basedata/bd_supplier/query", "apiResultData": { "data": { "start_createtime": "2025-01-24 15:51:43", "end_createtime": "2025-03-25 15:55:45", "status": ["B", "C"] }, "pageSize": 10, "pageNo": 1 } }), success: function(response) { if (response.status) { console.log('查询成功:', response.data); } else { console.error('查询失败:', response.message); } } });8.1.2 流程集成示例// 流程表单前端代码块 jQuery(document).ready(function() { // 1. 注册当前流程 KingdeeSyncUtils.registerWorkflow(1595); // 2. 创建同步实例 var kingdeeSync = new KingdeeSync({ title: "供应商数据同步", showProgress: true, timeout: 30000 }); // 3. 定义执行步骤 kingdeeSync .addStep("新增供应商", buildSupplierCreateRequest, extractSupplierId) .addStep("提交审核", buildSubmitRequest) .addDelayStepIf(needAssign, "等待分配", 3000) .addStepIf(needAssign, "组织分配", buildAssignRequest); // 4. 注册提交事件 WfForm.registerCheckEvent(WfForm.OPER_SUBMIT, function(callback) { if (KingdeeSyncUtils.isEnabled()) { kingdeeSync.execute(callback); } else { callback(); } }); });8.2 健康检查示例// 定期检查服务状态 function checkServiceHealth() { fetch('/kingdee/proxy') .then(response => response.json()) .then(data => { if (data.status) { console.log('✅ 金蝶接口服务正常:', data.message); } else { console.error('❌ 金蝶接口服务异常:', data.message); } }) .catch(error => { console.error('❌ 服务连接失败:', error); }); } // 每5分钟检查一次 setInterval(checkServiceHealth, 5 * 60 * 1000);8.3 错误处理示例// 自定义错误处理 kingdeeSync.handleStepError = function(stepName, error) { // 记录错误日志 console.error('步骤"' + stepName + '"执行失败:', error); // 显示用户友好的错误信息 var userMessage = "同步失败，请联系管理员"; if (error.message.includes("权限不足")) { userMessage = "您没有操作权限，请联系系统管理员"; } else if (error.message.includes("网络连接")) { userMessage = "网络连接失败，请检查网络设置"; } else if (error.message.includes("超时")) { userMessage = "请求超时，请稍后重试"; } // 显示错误提示 WfForm.showMessage(userMessage, 2, 5); // 停止后续步骤 this.stopExecution(); };📈 监控与维护9.1 系统监控监控项监控方法告警阈值Token状态检查uf\_kingdeeAPI.tokengxsj更新时间超过55分钟服务可用性GET /kingdee/proxy 健康检查响应时间>3秒或状态非2009.2 故障排查常见问题及解决方案：问题现象可能原因解决方案Token无效错误1. 定时任务未执行2. 金蝶认证信息变更3. Token过期未刷新1. 检查计划任务状态2. 检查数据库缓存表配置3. 手动执行Token获取定时任务附件上传失败1. 文件路径不存在2. ZIP文件损坏3. 网络超时1. 检查ImageFile表记录2. 验证文件完整性3. 增加超时时间中文乱码1. 编码设置不一致2. 数据库编码问题3. 金蝶API编码限制1. 统一使用UTF-82. 检查数据库连接字符串3. 排查金蝶API配置权限拒绝1. 用户角色未配置2. 角色配置错误3. 会话过期1. 分配正确角色2. 检查角色ID/名称3. 重新登录OA系统性能缓慢1. 网络延迟2. 大文件处理3. 数据库查询慢1. 网络优化2. 分块传输大文件3. 添加数据库索引9.4 备份与恢复备份策略：配置文件备份：# 备份web.xml配置 cp /ecology/WEB-INF/web.xml /backup/web.xml.$(date +%Y%m%d) # 备份数据库配置 sqlcmd -S localhost -d ecology -Q "SELECT * FROM uf_kingdeeAPI" -o /backup/kingdee_config.sql代码备份：# 备份Java类文件 tar -czf /backup/kingdee_classes_$(date +%Y%m%d).tar.gz \ /ecology/WEB-INF/classes/weaver/houdedev/cron/恢复步骤：停止Resin服务恢复配置文件恢复class文件重启服务并验证📋 参考资料1. 金蝶API接口清单请登录金蝶旗舰版，进入菜单 开放服务云-OpenAPI-API开发，到API列表中查看地址及请求方式2. 泛微前端开发文档[button color="success" icon="" url="https://e-cloudstore.com/doc.html?appId=98cb7a20fae34aa3a7e3a3381dd8764e#E9%E6%B5%81%E7%A8%8B%E8%A1%A8%E5%8D%95%E5%89%8D%E7%AB%AF%E6%8E%A5%E5%8F%A3API" type="round"]点击前往[/button]3. 已应用流程流程名称实现功能HD-供应商新增流程1. 供应商新增1. 供应商自动分配至其他组织1. 流程附件同步至金蝶供应商附件中文档签署角色姓名签字日期编写人徐东瑞 2025-12-20审核人 批准人 文档状态：✅ 正式发布保密级别：内部机密分发范围：系统管理员、开发人员、实施顾问
Nginx判断UA标识，实现链接仅能在飞书内部打开，外部浏览器打开跳转至其他链接
server { server_name ai.jinyi-food.com; listen 80; # 飞书User-Agent检测和重定向逻辑 set $feishu_redirect 0; if ($http_user_agent ~* (Lark|Feishu)) { set $feishu_redirect 1; } # 如果不是飞书访问，则重定向到百度 if ($feishu_redirect = 0) { return 302 https://www.baidu.com; } location / { proxy_pass http://192.168.5.100; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto $scheme; } }
方案背景在OA审批流程配置中，针对常见的“申请人→部门经理→副总”审批场景，通常采用部门矩阵功能进行配置，这种方法能够满足大部分业务流程的审批需求。然而，在某些特殊情况下，会出现特定角色无需参与审批流程的现象。例如：当董事长代管研发部时，研发部的部门矩阵中“副总”审批岗位指向了董事长。但在用车申请等特定流程中，只需审批至部门经理即可，无需董事长审批。目前针对此类情况的解决方案是在流程路径中设置条件跳转，以跳过特定审批人。但这种做法存在明显弊端：由于组织架构中部门与岗位调整较为频繁，而流程中的路径条件无法批量检索与统一维护，导致审批路径未能及时随组织变动更新。其直接结果是用户频繁反馈不应由其审批的流程被错误地推送过来，影响了审批效率与用户体验。配置示例：解决方案思路通过OA流程配置操作者的外部接口功能，结合SQL语句，查询矩阵表中对应的审批人，如果需要跳过审批，则返回一个没有人员对应的ID（如0），此时流程节点会找不到审批人，再利用流程异常自动流转至下一节点功能跳过该节点。不需要跳过审批则正常返回矩阵对应审批人。SQL语句SELECT CASE WHEN fgld = 3 THEN 0 ELSE fgld END AS fgld_result FROM Matrixtable_1 WHERE bm = $sqbm$ --Matrixtable_1对应OA中部门矩阵表 --fgld对应分管领导，3代表杨总的ID，0没有人员对应 --$sqbm$对应流程表单中需要取值的部门字段配置方法配置流程节点操作者配置流程异常处理最终效果正常审批跳过审批优点通用性强，可用于所有流程可拓展性强，可通过变通满足其他类似要求，举一反三减少流程维护频次降低流程错误几率
记录一次利用DeepSeek生成泛微E9流程使用情况的展示页
前言泛微E9版本的后台对于流程使用分析功能上还是比较欠缺的，公司使用OA系统的时间也比较久了，有很多没有必要的流程还存在在系统重，如果单条筛选工作量巨大，之前就发现DS对泛微数据库的结构记录的比较准确，索性就让DS帮我完成这一工作，再进阶成网页形式展现出来，也方便日后再做流程统计的时候查看。效果图最终版SQL代码WITH CurrentFlows AS ( SELECT wb.id, wb.workflowname, wb.activeVersionID, wb.isvalid, -- 添加 isvalid 字段 wb.workflowtype, -- 添加 workflowtype 字段 MAX(wf.createdate) AS last_created FROM workflow_base wb JOIN workflow_requestbase wf ON wb.id = wf.workflowid WHERE wb.id = wb.activeVersionID OR wb.activeVersionID IS NULL GROUP BY wb.id, wb.workflowname, wb.activeVersionID, wb.isvalid, wb.workflowtype ), TotalCounts AS ( SELECT wb.workflowname, COUNT(*) AS total_submissions, SUM(CASE WHEN wf.createdate >= '2025-01-01' AND wf.createdate < '2026-01-01' THEN 1 ELSE 0 END) AS submissions_2025 FROM workflow_base wb JOIN workflow_requestbase wf ON wb.id = wf.workflowid GROUP BY wb.workflowname ) SELECT cf.id AS 流程类型ID, cf.workflowname AS 流程类型名称, cf.activeVersionID AS 活动版本ID, -- 映射 isvalid 字段 CASE cf.isvalid WHEN 0 THEN '无效' WHEN 1 THEN '有效' WHEN 2 THEN '测试' ELSE '未知状态' END AS 流程状态, -- 获取 workflowtype 的中文名称 wt.typename AS 路径类型, tc.total_submissions AS 历史总提交次数, tc.submissions_2025 AS [2025年提交次数], cf.last_created AS 最后创建时间 FROM CurrentFlows cf JOIN TotalCounts tc ON cf.workflowname = tc.workflowname -- 关联 workflow_type 表获取路径类型中文名 LEFT JOIN workflow_type wt ON cf.workflowtype = wt.id ORDER BY cf.last_created DESC;最终版PHP代码<?php // 数据库配置信息 $serverName = "数据IP,1433"; // 服务器地址（IP:端口） $databaseName = "数据表名"; // 数据库名称 $username = "数据库用户名"; // 登录用户名 $password = "数据库密码"; // 登录密码 // 连接选项 $connectionOptions = [ "Database" => $databaseName, // 目标数据库 "Uid" => $username, // 用户名 "PWD" => $password, // 密码 "CharacterSet" => "UTF-8" // 字符集 ]; // 尝试连接数据库 $conn = sqlsrv_connect($serverName, $connectionOptions); // 检查连接是否成功 if ($conn === false) { die(" ❌ 数据库连接失败！ " . print_r(sqlsrv_errors(SQLSRV_ERR_ALL), true) . " "); } // 定义用户提供的 SQL 查询 $query = " WITH CurrentFlows AS ( SELECT wb.id, wb.workflowname, wb.activeVersionID, wb.isvalid, wb.workflowtype, MAX(wf.createdate) AS last_created FROM workflow_base wb JOIN workflow_requestbase wf ON wb.id = wf.workflowid WHERE wb.id = wb.activeVersionID OR wb.activeVersionID IS NULL GROUP BY wb.id, wb.workflowname, wb.activeVersionID, wb.isvalid, wb.workflowtype ), TotalCounts AS ( SELECT wb.workflowname, COUNT(*) AS total_submissions, SUM(CASE WHEN wf.createdate >= '2025-01-01' AND wf.createdate < '2026-01-01' THEN 1 ELSE 0 END) AS submissions_2025 FROM workflow_base wb JOIN workflow_requestbase wf ON wb.id = wf.workflowid GROUP BY wb.workflowname ) SELECT cf.id AS 流程类型ID, cf.workflowname AS 流程类型名称, cf.activeVersionID AS 活动版本ID, CASE cf.isvalid WHEN 0 THEN '无效' WHEN 1 THEN '有效' WHEN 2 THEN '测试' ELSE '未知状态' END AS 流程状态, wt.typename AS 路径类型, tc.total_submissions AS 历史总提交次数, tc.submissions_2025 AS [2025年提交次数], cf.last_created AS 最后创建时间 FROM CurrentFlows cf JOIN TotalCounts tc ON cf.workflowname = tc.workflowname LEFT JOIN workflow_type wt ON cf.workflowtype = wt.id ORDER BY wt.typename ASC; "; // 执行查询 $result = sqlsrv_query($conn, $query); // 检查查询是否成功 if ($result === false) { die(" ❌ 查询执行失败！ " . print_r(sqlsrv_errors(SQLSRV_ERR_ALL), true) . " "); } // 获取结果集并统计行数 $rows = []; $rowCount = 0; while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { $rows[] = $row; $rowCount++; } // 释放资源并关闭连接 sqlsrv_free_stmt($result); sqlsrv_close($conn); // 处理路径类型字符串的函数 function cleanPathType($pathType) { // 如果包含特殊格式，提取中文名称 if (preg_match('/~`~`\d+\s+([\x{4e00}-\x{9fa5}]+)/u', $pathType, $matches)) { return $matches[1]; // 返回匹配到的中文部分 } // 如果包含"系统默认工作流"的变体，统一替换 if (strpos($pathType, '系统默认工作流') !== false || strpos($pathType, 'System default workflow') !== false || strpos($pathType, '系統默認工作流') !== false) { return '系统默认工作流'; } return $pathType; } ?> 流程数据查询结果 - 增强版 流程数据统计结果 最后更新时间: <?= date('Y-m-d H:i:s') ?> 数据筛选面板 流程名称 流程状态 所有状态 有效 无效 测试 路径类型 最小总提交数 最小2025年提交数 重置筛选 应用筛选 流程总数 <?= $rowCount ?> 有效流程 <?= count(array_filter($rows, function($row) { return $row['流程状态'] === '有效'; })) ?> 2025年提交总数 <?= array_sum(array_column($rows, '2025年提交次数')) ?> 序号 流程类型ID 流程类型名称 活动版本ID 流程状态 路径类型 历史总提交次数 2025年提交次数 最后创建时间 <?php if ($rowCount > 0): ?> <?php $counter = 1; ?> <?php foreach ($rows as $row): ?> <?= $counter++ ?> <?= htmlspecialchars($row['流程类型ID'] ?? '') ?> <?php // 处理路径类型字符串 $pathType = $row['流程类型名称'] ?? ''; $cleanedPathType = cleanPathType($pathType); ?> <?= htmlspecialchars($cleanedPathType) ?> <?= htmlspecialchars($row['活动版本ID'] ?? '') ?> <span class="status-badge <?= ($row['流程状态'] === '有效') ? 'valid' : (($row['流程状态'] === '无效') ? 'invalid' : (($row['流程状态'] === '测试') ? 'testing' : '')) ?>"> <?= htmlspecialchars($row['流程状态'] ?? '') ?> <?php // 处理路径类型字符串 $pathType = $row['路径类型'] ?? ''; $cleanedPathType = cleanPathType($pathType); ?> <?= htmlspecialchars($cleanedPathType) ?> <?= number_format($row['历史总提交次数'] ?? 0) ?> <?= number_format($row['2025年提交次数'] ?? 0) ?> <?= htmlspecialchars($row['最后创建时间'] ?? '') ?> <?php endforeach; ?> <?php else: ?> 未查询到符合条件的记录 <?php endif; ?> 显示 <?= $rowCount ?> 条记录中的 <?= $rowCount ?> 条 导出数据 刷新数据 数据源: ecology 数据库 | 服务器: 192.168.5.29 左右滑动查看完整表格
泛微E9查询某一时间段流程退回情况和退回意见
数据库代码：SELECT log.requestid AS 流程ID, log.workflowid AS 流程定义ID, workflow.workflowname AS 流程名称, log.nodeid AS 退回节点ID, from_node.nodename AS 退回节点名称, log.destnodeid AS 退回目标节点ID, to_node.nodename AS 目标节点名称, log.operator AS 操作人ID, res.lastname AS 操作人姓名, log.receivedPersons AS 接收人, log.operatedate AS 操作日期, log.operatetime AS 操作时间, REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( CAST(log.remark AS NVARCHAR(MAX)), ' ', ''), ' ', ''), ' ', ''), ' ', ''), ' ', ''), ' ', '') AS 退回意见 FROM workflow_requestlog log JOIN workflow_nodebase from_node ON log.nodeid = from_node.id JOIN workflow_nodebase to_node ON log.destnodeid = to_node.id JOIN HrmResource res ON log.operator = res.id JOIN workflow_base workflow ON log.workflowid = workflow.id WHERE logtype = '0' -- 3表示退回操作 AND log.operatedate like '%2025%'执行效果：数据库字段定义：在workflow_requestlog表的logtype字段中，0代表批准操作，2代表提交操作，3代表退回操作，7代表转发操作，9代表批注操作。
GitHub项目地址：飞书 To ChatGpt飞书 To 文心一言支持功能：## 👻 机器人功能 🗣 语音交流：私人直接与机器人畅所欲言「Whisper」 🕵️ 图片推理: 借助大模型互动式对话图片「GPT4V」 💬 多话题对话：支持私人和群聊多话题讨论，高效连贯 🖼 文本成图：支持文本成图和以图搜图 「DALLE-3」 🛖 场景预设：内置丰富场景列表，一键切换AI角色 🎭 角色扮演：支持场景模式，增添讨论乐趣和创意 🤖 AI模式：内置4种AI模式，感受AI的智慧与创意 🔄 上下文保留：回复对话框即可继续同一话题讨论 ⏰ 自动结束：超时自动结束对话，支持清除讨论历史 📝 富文本卡片：支持富文本卡片回复，信息更丰富多彩 👍 交互式反馈：即时获取机器人处理结果 🎰 余额查询：即时获取token消耗情况 🔙 历史回档：轻松回档历史对话，继续话题讨论 🚧 🔒 管理员模式：内置管理员模式，使用更安全可靠 🚧 🌐 多token负载均衡：优化生产级别的高频调用场景 ↩️ 支持反向代理：为不同地区的用户提供更快、更稳定的访问体验 📚 与飞书文档互动：成为企业员工的超级助手 🚧 🎥 话题内容秒转PPT：让你的汇报从此变得更加简单 🚧 📊 表格分析：轻松导入飞书表格，提升数据分析效率 🚧 🍊 私有数据训练：利用公司产品信息对GPT二次训练，更好地满足客户个性化需求 🚧操作关键步骤：1.部署好Go语言环境、目前生产环境使用版本为go version go version go1.22.0 linux/amd642.config.yaml修改配置信息3.执行后台运行命令，避免宝塔关闭命令提示符窗口后服务停止运行#执行服务 nohup go run main.go #停止服务 ps -ef | grep cpolar kill -9 PID效果展示：
代码如下：$().ready(function () { //WfForm.OPER_SAVE为保存操作，SUMBIT为提交操作，必须要有callback(); 否则将会中断操作 WfForm.registerCheckEvent(WfForm.OPER_SAVE, function (callback) { var url = "http://www.baidu.com"; //设置往form表单中添的元素，name=value的方式，设置一个json数组对象 var params = { "billid":WfForm.getFieldValue("field15631"), "sqr": $('div[data-fieldmark="field15632"] .ant-select-selection__choice__content a').text(), "fyftbm": $('div[data-fieldmark="field5994"] .ant-select-selection__choice__content a').text(), "cxsj":WfForm.getFieldValue("field15633"), "ccrs":WfForm.getFieldValue("field10980"), "ckxm":WfForm.getFieldValue("field10981"), "lxdh":WfForm.getFieldValue("field11378"), "yclx":$('div[data-fieldmark="field5997"] .ant-select-selection-selected-value').text(), "sfd":$('div[data-fieldmark="field11379"] .ant-select-selection-selected-value').text(), "mdd":WfForm.getFieldValue("field5996"), "yjycjssj":WfForm.getFieldValue("field15634"), "sjxm":$('div[data-fieldmark="field6004"] .ant-select-selection__choice__content a').text(), "sjdh":WfForm.getFieldValue("field10982"), "cph":WfForm.getFieldValue("field6003") } $.ajax({ url: url, // 目标资源 cache: false, //true 如果当前请求有缓存的话，直接使用缓存。如果该属性设置为 false，则每次都会向服务器请求 async: false, //默认是true，即为异步方式 data: params, dataType: "jsonp", // 服务器响应的数据类型 type: "POST", // 请求方式 success: function (data) { console.log(data) } }); }); callback(); });
金蝶单据撤销时，给相关人员发送飞书提醒功能实现
前言金蝶云星空标准产品自带的消息推送功能无法在单据撤销时发送消息通知，只能在保存等其他操作触发消息通知，并且触发成功后，消息只能发送至金蝶客户端和云之家消息平台内，无法发送至办公沟通软件，恰好飞书有多种接口支持通过webhook的形式将消息发送给相关人员，可以对功能进行实现。操作步骤1.在金蝶BOS协同开发平台，打开单据，操作列表内找到相关操作，在服务插件内注册一个新的python插件，经复制代码。示例代码：# -*- coding: utf-8 -*- import clr import sys #添加对cloud插件开发的常用组件的引用 clr.AddReference('System') clr.AddReference('System.Data') clr.AddReference('Kingdee.BOS') clr.AddReference('Kingdee.BOS.Core') clr.AddReference('Kingdee.BOS.App') clr.AddReference('Kingdee.BOS.ServiceHelper') clr.AddReference('Newtonsoft.Json') #导入cloud基础库中的常用实体对象（分命名空间导入，不会递归导入） from Kingdee.BOS import * from Kingdee.BOS.Core import * from Kingdee.BOS.Core.Bill import * from Kingdee.BOS.Core.DynamicForm.PlugIn import * from Kingdee.BOS.Core.DynamicForm.PlugIn.ControlModel import * from System import * from System.Data import * from System.Net import * from System.Text import * from System.IO import * #from System.String import * from Kingdee.BOS.App.Data import * from System.Collections.Generic import List from Kingdee.BOS.ServiceHelper import * from Newtonsoft.Json.Linq import * sys.setdefaultencoding('utf-8') def OnPreparePropertys(e): e.FieldKeys.Add("FBillTypeID"); # e.FieldKeys.Add("FProviderContactId");#这里使用的是字段标识，后面从数据包取值用的是绑定实体属性 e.FieldKeys.Add("F_RNCV_Wly");#采购员基础资料 # e.FieldKeys.Add("PurchaseOrgId")#采购组织基础资料 # e.FieldKeys.Add("SupplierId")#供应商基础资料 def EndOperationTransaction(e): #这里获取单据数据包方式和前面一样 #print(e.DataEntitys); for billObj in e.DataEntitys: billId=billObj["Id"];#单据ID billNo=billObj["F_RNCV_BillNo"];#单据编号 wlyObj = billObj["F_RNCV_Wly"];#物流员基础资料 wly_name = ("{0}").format(wlyObj["Name"]);#采购员姓名 msgdata = '{\"billno\":\"'+billNo+'\",\"wly\":\"'+wly_name+'\",\"time\":\"'+DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")+'\"}' webRequest=HttpPost(msgdata,'飞书机器人webhook地址') def ValidateResult(responseText): s=('{0}').format(responseText); # if (s.find("response_error:")>=0): # failText = s.TrimStart("response_error:".ToCharArray()) + "发送消息失败，请自行联系相关人员，避免发货出现问题！"; # return failText; return s; def HttpPost(Content, Url): httpRequest = HttpWebRequest.Create(Url); httpRequest.Method = "POST"; httpRequest.ContentType = "application/json"; httpRequest.Timeout = 1000 * 60 * 10; reqStream = httpRequest.GetRequestStream(); sContent = Content; bytes = Encoding.UTF8.GetBytes(sContent); reqStream.Write(bytes, 0, bytes.Length); reqStream.Flush(); repStream = httpRequest.GetResponse().GetResponseStream(); reader = StreamReader(repStream); return ValidateResult(reader.ReadToEnd());2.前往飞书机器人助手，新增机器人应用，配置好相关信息，新增消息推送流程，开始由webhook触发，添加传参，{"billno":"", "wly":"", "time":"" }，分别为单据编号、物流员姓名和操作时间，后面添加并行分支节点和发送消息节点，每个节点条件为物流员参数等于某个人时，即给该人员发送飞书消息提醒，相关图示如下：最终效果
金蝶多选基础资料列表无法显示的解决办法
【应用场景】将多选基础资料字段的名称合并到文本字段。标准产品不支持在列表上对多选基础资料进行过滤和显示，如果想对多选基础资料数据进行过滤和显示，目前只能将多选基础资料冗余一份文本数据，然后对该份文本数据进行过滤和显示。【案例演示】采购订单，将多选基础资料字段的名称合并到文本字段。【实现步骤】登录BOSIDE，扩展采购订单，新增多选基础资料字段和文本字段。在基本信息上新增实体服务规则，保存元数据，开发完毕。前置条件：F\_Jac\_MulBase <> null and len(F\_Jac\_MulBase)>0为真计算公式：F\_Jac\_Text = ','.join(map(lambda x:x.Name.ToString(),F\_Jac\_MulBase.TargetObjects))F\_Jac\_Text1 = ','.join(map(lambda x:x.Number,F\_Jac\_MulBase.TargetObjects))为假计算公式：F\_Jac\_Text = ''F\_Jac\_Text1 = ''【功能验证】登录业务站点，打开采购订单新增界面，录入或删除多选基础资料后，编码和文本字段会自动更新数据。【参考资料】获取多选基础资料的内码：','.join(map(lambda x:x.Id,F\_Jac\_MulBase.TargetObjects))获取多选基础资料的编码：','.join(map(lambda x:x.Number,F\_Jac\_MulBase.TargetObjects))获取多选基础资料的名称：','.join(map(lambda x:x.Name.ToString(),F\_Jac\_MulBase.TargetObjects))
Windows打开金蝶提示为了对电脑进行保护，已经阻止此应用
问题症状：解决办法：1.进入”控制面板“--”用户账户“--”用户账户“，选择”更改用户账户控制设置“，拉倒最后。2.按【win+R】快捷键打开运行，输入gpedit.msc，打开后依次进入”Windows设置“--”安全设置“--”本地策略“--”安全选项“，找到”用户账户控制：以管理员批准模式运行所有管理员“，双击将其设置为”已禁用“即可。如果家庭版提示找不到gpedit.msc没有策略组进行以下操作：a.桌面新建文本文档，将下面代码复制到文本文档中：@echo off pushd "%\~dp0" dir /b C:\\Windows\\servicing\\Packages\\Microsoft-Windows-GroupPolicy-ClientExtensions-Package\~3\*.mum >List.txt dir /b C:\\Windows\\servicing\\Packages\\Microsoft-Windows-GroupPolicy-ClientTools-Package\~3\*.mum >>List.txt for /f %%i in ('findstr /i . List.txt 2^>ul') do dism /online /norestart /add-package:"C:\\Windows\\servicing\\Packages\\%%i"pauseb.将文件后缀改为.batc.运行该批处理文件即可。如果运行时提示权限错误，需右键文件选择”以管理员身份运行“。完成后继续执行第二步操作。