ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Domino BS开发Excel导入导出:LotusScript与Java代理双轨方案

Domino BS开发Excel导入导出:LotusScript与Java代理双轨方案 简介面向Lotus Domino B/S应用开发者的Excel数据交换技术说明文档聚焦于Agent中通过LotusScript代理和Java代理完成Excel导入导出的核心实现覆盖数据分析、报表生成、数据迁移等典型场景适合初中级后端开发者直接参照。资源包仅含1个doc文档压缩后大小35KB内容精炼代码示例与实现思路集中在同一份文档内便于快速定位。目前已有140人学习浏览属于小而实用的Domino开发素材。文档从Java代理导出入手细致讲解设置application/vnd.ms-excel输出类型、利用PrintWriter与TAB分隔生成浏览器可识别的Excel内容、遍历视图并读取文档域值、处理多值字段等关键环节同时补充了Apache POI解析Excel并创建或更新NotesDocument的导入思路以及LotusScript中基于NotesDocument和NotesItem的近似做法。整体覆盖导入与导出两端能帮助开发者避开MIME类型、多值域处理等常见问题快速在Domino B/S应用中实现Excel数据交换能力。1. 在Domino BS应用里导入导出Excel一张Excel表逼出的LotusScript和Java代理双轨方案接手一个Domino BS项目最容易被客户用一张Excel表问住“把系统里的数据导成Excel再把Excel导回来行不行” 平时做C/S开发随手用LotusScript调Excel COM就能搞定可到了BS模式代理跑在Domino服务器上服务器多半不装Office浏览器端又不能直接碰文件。于是lotus Domino BS开发里的Excel导入导出就成了一门专门的技术活。常见做法是两条腿走路LotusScript代理负责轻量、兼容老格式的导入导出Java代理用Apache POI处理真正的.xlsx。这篇文章把两种代理从选型、代码到踩坑完整讲一遍适合正在做Domino Web应用、被数据交换卡住的开发同事。2. 先定技术路线再写代理LotusScript和Java代理在Excel导入导出上的分工与选型拿到需求别急着写代码。先确认三件事文件是xls、xlsx还是csv服务器上有没有Office数据量有多大这三个答案基本决定了你选哪条路。2.1 为什么BS模式下的Excel读写不能依赖服务器上的Excel COM在C/S客户端程序里LotusScript调用Excel COM是常规操作CreateObject(Excel.Application)然后打开工作簿、读写单元格。但业务从C/S迁到B/S之后代理是在Domino HTTP任务里跑的这有两个改变。第一服务器通常不装Office。为了省授权和资源生产环境几乎不会给Domino服务器安装Excel。你写的COM代码在开发机跑得好好的一上服务器就是“自动化错误”或“服务器没有权限创建对象”。第二就算装了OfficeExcel COM是给交互式桌面设计的。多个用户通过BS并发提交数据时每个请求都去启动一个Excel进程很快服务器上就会堆满EXCEL.EXE最后权限冲突、模态对话框弹不出来、进程死锁。我在一个项目里见过服务器因为Excel进程残留导致HTTP假死输完tell http restart才恢复。所以服务器端操作Excel第一条铁律就是不要碰COM。那么可选路径有哪些对Domino代理来说现实的路有四条利用ODBC把Excel当数据源、让用户先转成CSV再解析、利用Java代理里的POI库直接解析、以及让数据库另存为OpenDocument格式再处理。下面一节把它们的适用边界分开说。2.2 LotusScript代理和Java代理的能力边界一个只能摸边一个能玩二进制LotusScript代理对Excel的处理能力本质上是“文件系统 ODBC 字符串处理”的组合用NotesODBCConnection连接Excel的DSN。可行但需要每台服务器配置ODBC数据源而且要求Excel文件有固定的列结构运行不稳定。用自由文件I/O读取CSV。这是最实用的LotusScript方案Excel直接另存为CSV服务器只要读文本。生成CSV或HTML表格让Excel打开。这是导出功能最常见的妥协方案。Java代理的能力完全不同。Domino的Java代理跑在JVM里可以引用第三方库所以能用Apache POI直接读写二进制.xls和.xlsx。POI可以处理单元格类型、日期、公式、多个Sheet、格式样式甚至能用SXSSF处理十万行以上的大文件。代价是需要管理jar依赖和JVM版本稍微重一点。选型建议如果需求是“把Excel里的两列数据导进Notes表单”或者“把视图列表导成表格给客户看”用LotusScript CSV/HTML足够如果需求带“必须用.xlsx、保留格式、多Sheet、千行以上、有日期运算”直接上Java代理不要在LotusScript里硬写。2.3 一张选型对照表xls/csv/xlsx分别该用哪种代理我这里放一张实际项目里用的决策表按下述规则选输入/输出场景推荐代理原因客户提供CSV数据量几千行以内LotusScript代理读文本简单部署零依赖客户提供老式.xls纯数据导入Java代理 POI(HSSF).xls是二进制OLE格式LS没有可靠解析办法客户提供.xlsx含日期/数字格式Java代理 POI(XSSF)xlsx是zip包内的XMLPOI能正确处理类型导出报表接受.csv格式LotusScript代理生成CSV字符串最直接导出报表必须.xlsx且要样式Java代理 POI(XSSF)LS生成不了真正的xlsx二进制服务器上完全没有额外jar权限LotusScript代理 CSV有些涉密环境不允许往JVM放第三方库注意表格里的规则不是死的。实际中我还会看一个因素文件后缀是.xlsx但打开一看是CSV用Excel另存出来的假Excel这类文件用LotusScript也能处理。所以选型的第一动作是判断真实格式而不是只看后缀。判断真实格式可以按下面三步走。第一步用二进制查看工具读取文件前4个字节。第二步看到50 4B 03 04即ASCII字符PK说明是xlsx看到D0 CF 11 E0说明是xlsOLE看到逗号和字段名说明是纯文本CSV。第三步如果文件是CSV但后缀被改成.xlsx读取前几行就应该能识别这类文件用LotusScript直接读文本反而更快。3. 用LotusScript代理做导入导出CSV和HTML表格是最不折腾的落地路如果你已经决定用LotusScript下面这套做法是我在多个老项目里验证过的。它不花哨但每一步都是能跑的。3.1 LotusScript的三种Excel路线OLE、ODBC和文件解析为什么最后选文件解析LotusScript实现Excel交换常见说法有三种逐一说明发生了什么。OLE/COM路线用CreateObject(Excel.Application)。这条路线在C/S下很好用BS下不建议理由上一章已经说了。如果需要保留单元格格式、公式且业务量很小并且服务器正好装了Office可以试但并发一高就是坑。ODBC路线把Excel文件配置成系统DSN使用LotusScript的NotesODBCConnection查询表数据。这个方案不需要Office稳定度比COM高但配置DSN需要服务器管理员权限而且Excel文件路径、Sheet名写死在DSN里文件一换就得重配。在BS开发里用户上传的Excel文件名字和列顺序每天都在变ODBC这条路不适合做自助导入。文件解析路线把Excel另存为CSV或HTML表格然后LotusScript按文本处理。这是最实用的一条路。优点是代码只有文件读写和字符串分割不依赖服务器额外环境缺点是用户要容忍“先另存为CSV”这个步骤。如果业务方连这个妥协都不接受就直接跳到Java代理。因此LotusScript代理的定位就是处理CSV或Excel导出的兼容文本格式。下面两个小节给出导入和导出各一段能直接改的代码。3.2 LotusScript代理接收上传的CSV并导入Notes文档完整代码与参数说明这里假设CSV文件已经通过某种方式放到服务器本地路径比如XPages上传控件写入共享目录或是管理员定期拷贝到C:\data\。代理通过URL参数传入文件路径。下面的LotusScript代理读取一个两列CSV字段分别是姓名、部门导入到User表单Sub Initialize Dim session As New NotesSession Dim db As NotesDatabase Dim doc As NotesDocument Dim ctx As NotesAgentContext Dim params As NotesDocument Dim filePath As String Dim fileNum As Integer Dim line As String Dim sepPos As Integer Set db session.CurrentDatabase Set ctx session.AgentContext 在BS代理里URL参数通过DocumentContext传入 Set params ctx.DocumentContext If Not params.HasItem(csvPath) Then Print 请提供 csvPath 参数例如?OpenAgentcsvPathC:\data\user.csv Exit Sub End If filePath params.GetItemValueString(csvPath) fileNum Freefile() On Error GoTo handler Open filePath For Input As #fileNum 逐行读取CSV第一行通常是表头这里直接跳过 Dim firstLine As Boolean firstLine True While Not EOF(fileNum) Line Input #fileNum, line If firstLine Then firstLine False GoTo continueLoop End If If line Then GoTo continueLoop sepPos InStr(line, ,) If sepPos 0 Then Set doc db.CreateDocument() doc.Form User doc.Name Left(line, sepPos - 1) doc.Department Mid(line, sepPos 1) doc.Save True, False, False End If continueLoop: Wend Close #fileNum Print 导入完成请到视图中检查 Exit Sub handler: Close #fileNum Print 导入失败 Error 行号 Erl End Sub逻辑说明代理先从DocumentContext取csvPath参数这是Domino Web代理最基本的数据传入方式很多从C/S转过来的同事会在这里卡住。然后用自由文件I/O逐行读取按逗号分割。doc.Save True, False, False第一个参数是“创建唯一键冲突时报错”这里不重要。最后出错时用Error和Erl输出错误信息方便排错。参数说明分隔符默认是逗号如果Excel另存的是“CSV UTF-8逗号分隔”就是逗号如果是中文版Excel另存的“CSV逗号分隔”通常是ANSI编码LotusScript按当前区域默认字符集读取一般没问题。如果字段本身包含逗号上面的切分就会出错这时候需要更完整的CSV解析器或者要求用户在Excel里先做“数据 - 分列”预处理。实际项目里因为字段带逗号而翻车的比例不低我一般会在导入前先把源文件做一次列数校验。3.3 LotusScript代理把视图数据导出为CSV并生成下载附件LotusScript没法直接设置HTTP响应头输出二进制所以我的做法是先在服务器上生成CSV文件然后作为附件挂到临时文档再输出一个下载链接。这段代码导出UserView视图的三列数据Sub Initialize Dim session As New NotesSession Dim db As NotesDatabase Dim view As NotesView Dim col As NotesViewEntryCollection Dim entry As NotesViewEntry Dim doc As NotesDocument Dim rt As NotesRichTextItem Dim fileNum As Integer Dim csvPath As String Dim fileName As String Set db session.CurrentDatabase Set view db.GetView(UserView) Set col view.AllEntries fileName export_ Format(Now, YYYYMMDDHHMMSS) .csv csvPath C:\temp\ fileName fileNum Freefile() Open csvPath For Output As #fileNum Print #fileNum, 姓名,部门,入职日期 Set entry col.GetFirstEntry() While Not entry Is Nothing If entry.IsDocument Then Print #fileNum, entry.ColumnValues(0) , entry.ColumnValues(1) , entry.ColumnValues(2) End If Set entry col.GetNextEntry(entry) Wend Close #fileNum 把CSV挂到临时文档 Set doc db.CreateDocument() doc.Form TempExport Set rt doc.CreateRichTextItem(Body) Call rt.EmbedObject(EMBED_ATTACHMENT, , csvPath) Call doc.Save(True, False, False) 输出下载链接/0/是数据库默认视图的文档展开方式 Print a href db.URL /0/ doc.UniversalID /$FILE/ fileName 下载CSV/a End Sub逻辑说明用Print #fileNum, ...写文件注意和前面导入的Line Input对应。entry.IsDocument能过滤掉视图中的分类行避免把合计行导出。附件挂到临时文档后Domino的附件下载机制会自动把文件放到/$FILE/路径下用户点击链接即可下载。参数说明视图列数要和ColumnValues下标匹配。如果视图列的值有换行符或逗号导出到CSV会错位简单处理是把ColumnValues里的内容用StrReplace去掉换行符再用双引号包裹字段。实际中我会先做数据清洗再写文件。3.4 LotusScript方案的两个边界条件什么时候必须交给Java代理LotusScript方案不能解决的问题通常在需求变更时暴露出来。第一客户要求“我要原来的Excel带格式带多个Sheet”。CSV和纯文本输出做不到。第二数据量超过一万行服务器的Line Input逐行处理会变慢而且Print到CSV也占内存。第三客户的Excel文件里有真正的日期单元格、批注、合并单元格这些在CSV转换中全丢了。一旦出现这三类信号别在LotusScript里硬扛直接进入下一章的Java代理方案。这不是说LotusScript不行而是领域不同LotusScript再强也只是文本级工具本质上不是为解析二进制文档设计的。4. 用Java代理实现真正的.xlsx导入导出POI集成和可抄作业的代理代码Java代理是Domino BS开发里处理Excel的正道。下面从环境配置到代码一一说明。4.1 把Apache POI装进Domino Java代理环境依赖与版本匹配很多同事第一次听到“Java代理”会误以为和Java动态代理有关系。其实Domino的Java代理就是一个在服务器上运行的Java类入口是AgentBase和JDK动态代理完全是两码事。Domino的Java代理不读应用的WEB-INF/lib除非你同时用XPages后台Bean它跑在Domino自带的JVM里。要让代理认POI需要把jar放进Domino安装目录的jvm\lib\ext。常见路径是C:\Program Files\HCL\Domino\jvm\lib\extIBM和HCL版本略有不同。放好后在Domino控制台执行tell http restart重启HTTP任务后新类才会被加载。依赖不是只要一个poi.jar。处理xlsx需要POI的多个模块。我在生产环境用的最小依赖是这样一组jar以POI 4.1.2为例jar作用poi-4.1.2.jar处理xls老格式poi-ooxml-4.1.2.jar处理xlsx格式poi-ooxml-schemas-4.1.2.jarxlsx的XML Schema定义xmlbeans-3.1.0.jarXSSF依赖commons-collections4-4.4.jar集合工具commons-compress-1.21.jarZIP处理commons-io-2.11.0.jar流工具版本匹配是关键Domino 9.0.x自带的JVM是JDK 6/7POI 4.x要求JDK 8如果服务器没升级就退到POI 3.17。Domino 10/11/12内置的JVM支持JDK 8可以用POI 4.x或5.x。版本不匹配时最常见的报错是UnsupportedClassVersionError。装好之后先用命令行在服务器上执行java -cp poi-4.1.2.jar org.apache.poi.ss.usermodel.WorkbookFactory验证类能加载再跑代理。4.2 Java代理读取Excel文件并写入Notes数据库导入代理核心代码这里的文件路径同样来自URL参数。代码用WorkbookFactory.create自动识别xls和xlsx示例读取第一张Sheet跳过表头创建User文档import lotus.domino.*; import org.apache.poi.ss.usermodel.*; import java.io.*; import java.util.Date; public class ExcelImportAgent extends AgentBase { public void NotesMain() { try { Session session getSession(); AgentContext ctx session.getAgentContext(); Database db ctx.getCurrentDatabase(); // 通过URL参数获取服务器上的Excel文件路径 Document docCtx ctx.getDocumentContext(); String filePath docCtx.getItemValueString(filePath); if (filePath.length() 0) { throw new RuntimeException(缺少filePath参数例如?OpenAgentfilePathC:/temp/user.xlsx); } FileInputStream fis new FileInputStream(filePath); Workbook workbook WorkbookFactory.create(fis); Sheet sheet workbook.getSheetAt(0); // 从第2行开始第0行当表头 for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) continue; Document doc db.createDocument(); doc.replaceItemValue(Form, User); doc.replaceItemValue(Name, cellToString(row.getCell(0))); doc.replaceItemValue(Department, cellToString(row.getCell(1))); // 日期字段单独处理避免POI把日期转成序列数 Date joinDate cellToDate(row.getCell(2)); if (joinDate ! null) { doc.replaceItemValue(JoinDate, joinDate); } doc.save(true, true, false); } workbook.close(); fis.close(); System.out.println(导入完成共处理 (sheet.getLastRowNum()) 行数据); } catch (Exception e) { e.printStackTrace(); } } private String cellToString(Cell cell) { if (cell null) return ; // POI 4.x 使用 CellType 枚举POI 3.x 使用 Cell.CELL_TYPE_STRING cell.setCellType(CellType.STRING); return cell.getStringCellValue().trim(); } private Date cellToDate(Cell cell) { if (cell null) return null; if (cell.getCellType() CellType.NUMERIC DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } return null; } }逻辑说明ctx.getDocumentContext()只在Web代理调用时才有内容如果代理被定时任务调用需要改成从其他配置来源读取路径。cellToString把单元格强制转成字符串这样数字、文本统一处理但如果单元格是公式setCellType(CellType.STRING)取的是公式缓存的字符串不一定是最新值更稳妥是调getCellType()判断公式后走getCachedFormulaResultType()。示例没有处理公式生产环境建议单独处理。参数说明save(true, true, false)的第二个参数传true会生成$UpdatedBy第三个传false表示不覆盖同名文档具体按业务调整。sheet.getLastRowNum()返回的是最后一行的索引实际行数是getLastRowNum()1循环里从1开始是为了跳过表头逻辑上没问题。如果Excel里有空白行getRow(i)返回null代码里的continue会兜住。4.3 Java代理导出视图数据为xlsx生成附件供下载导出示例使用XSSFWorkbook生成真正的.xlsx然后挂到临时文档import lotus.domino.*; import org.apache.poi.xssf.usermodel.*; import java.io.*; public class ExcelExportAgent extends AgentBase { public void NotesMain() { try { Session session getSession(); AgentContext ctx session.getAgentContext(); Database db ctx.getCurrentDatabase(); // 要导出的视图通过参数传入 Document docCtx ctx.getDocumentContext(); String viewName docCtx.getItemValueString(viewName); if (viewName.length() 0) viewName UserView; View view db.getView(viewName); ViewEntryCollection vec view.getAllEntries(); XSSFWorkbook wb new XSSFWorkbook(); XSSFSheet sheet wb.createSheet(导出数据); XSSFRow header sheet.createRow(0); header.createCell(0).setCellValue(姓名); header.createCell(1).setCellValue(部门); header.createCell(2).setCellValue(入职日期); ViewEntry entry vec.getFirstEntry(); int rowIndex 1; while (entry ! null) { if (entry.isDocument()) { XSSFRow row sheet.createRow(rowIndex); row.createCell(0).setCellValue(entry.getColumnValueString(0)); row.createCell(1).setCellValue(entry.getColumnValueString(1)); row.createCell(2).setCellValue(entry.getColumnValueString(2)); } ViewEntry next vec.getNextEntry(entry); entry.recycle(); // 关键释放Domino对象否则大视图会内存泄漏 entry next; } // 生成服务器临时文件 String tmpDir C:/temp/; String fileName export_ System.currentTimeMillis() .xlsx; FileOutputStream fos new FileOutputStream(tmpDir fileName); wb.write(fos); fos.close(); wb.close(); // 把xlsx作为附件挂到临时文档 Document fileDoc db.createDocument(); fileDoc.replaceItemValue(Form, TempExport); RichTextItem rt fileDoc.createRichTextItem(Body); rt.embedObject(EmbeddedObject.EMBED_ATTACHMENT, , tmpDir fileName); fileDoc.save(true, true, false); // 返回下载链接 String link db.getURL() /0/ fileDoc.getUniversalID() /$FILE/ fileName; System.out.println(a href link 下载Excel/a); } catch (Exception e) { e.printStackTrace(); } } }逻辑说明entry.recycle()在Java代理里特别重要。Domino的Java API与C API之间有对象引用计数不回收的话大视图跑一次就可能把服务器内存吃光。使用ViewEntryCollection遍历时每次取下一个entry之前必须recycle掉当前entry这就是老开发常说的“Domino对象生命周期管理”。参数说明getColumnValueString取的是视图列的显示值日期列会按视图格式转成字符串所以导出的日期可能不是标准日期格式。如果需要原始日期值可以改为entry.getColumnValue(2)返回DateTime再格式化。db.getURL()返回数据库的URL形式如果代理被放在某个子目录部署建议拼成http://host/db.nsf避免反斜杠问题。4.4 参数传递和返回结果约定让代理可以被XPages、按钮和URL复用同一个代理可以按action参数做方法分发避免建一堆代理。我在实际项目里是这样约定的参数名类型必填说明actionString是import / exportfilePathStringimport时填Excel文件在服务器上的绝对路径viewNameStringexport时填要导出的视图名称formNameString否导入时创建的Notes表单名默认UsertargetFolderString否导出时所在临时目录默认C:/temp示例URLhttp://host/db.nsf/excelAgent?OpenAgentactionimportfilePathC:/temp/user.xlsxformNameUser http://host/db.nsf/excelAgent?OpenAgentactionexportviewNameUserView返回结果我用统一的一段HTML成功时输出“处理完成”和下载链接失败时输出异常信息。在XPages里可以用facesContext.getExternalContext().redirect()把请求重定向到代理URL再把返回值当作普通HTML处理也可以在按钮事件中用Command([ToolsRunMacro])跑服务器代理但BS开发中更推荐直接调用OpenAgent。5. Excel代理开发避坑指南5个真实翻车现场和对应解决方案下面这几条不是网上抄的是我自己在项目里遇到过的每一条都花过不止一个晚上。5.1 现象ClassNotFoundException: org.apache.poi.xssf.usermodel.XSSFWorkbook导入代理一跑控制台立即报错XSSFWorkbook类找不到。原因POI的jar没有放对地方。Domino的Java代理默认从jvm\lib\ext加载第三方类而不是从应用的WEB-INF\lib加载。很多同事把poi的jar丢进app.war/WEB-INF/lib但那只对XPages的Java类有效普通Java代理读不到。解决把POI相关jar复制到Domino程序目录下的jvm\lib\ext然后在Domino控制台执行tell http restart。如果还报错用控制台命令show java versions确认JVM版本再检查POI版本与JDK是否匹配。实在不想动服务器也可以把代理改成XPages后台Bean在应用中直接依赖jar但这就绕出“Java代理”了。5.2 现象导入的日期全部变成数字或1911年Excel里的日期单元格导入后Notes显示为42920这样的数字或者变成1911-01-01。原因POI读取日期时如果对每个单元格统一调用了getStringCellValue()日期会被当作普通数字串成42920。因为Excel的日期本质是序列数。DateUtil.isCellDateFormatted(cell)能识别日期格式但如果之前已经调用了setCellType(CellType.STRING)日期格式的标志可能被清除。解决在代码里区分日期处理。先判断cell.getCellType()如果是NUMERIC再调DateUtil.isCellDateFormatted(cell)是日期就用getDateCellValue()不是日期才走字符串转换。同时写入Notes文档时用java.util.Date让POI到Notes的日期时间类型自动转换。不要先转字符串再塞进Notes的DateTime字段。5.3 现象导出的xlsx下载后Office提示“文件格式与扩展名不匹配”用户下载后双击打开Excel弹警告说文件已损坏或扩展名不匹配。原因最常见的是把CSV或HTML内容直接命名成.xlsx扩展名。另一种是POI的workbook对象没有close()输出流没有flush()导致文件内容不完整。还有一种代理里用System.out打印了调试信息Domino把这些输出混进了HTTP响应如果代理同时输出了二进制数据文件头会变成HTML或带有空行整个文件就废了。解决首先确认导出代码用的是FileOutputStream workbook.write(fos) fos.close() wb.close()而不是Print输出。其次确认下载链接指向的是/$FILE/附件路径而不是代理本身。最后可以在服务器上用文本编辑器打开生成的xlsx前几个字符应该是PK如果是那说明输出被HTML污染了。我自己的习惯是导出后先在本机用7-Zip验证一遍确认能打开再放给用户。5.4 现象代理读取上传的Excel文件时报“FileNotFoundException”XPages里明明上传了文件Java代理却找不到文件。原因Domino HTTP上传的文件在请求处理完以后临时目录会被清理。上传和代理是两个独立的HTTP请求上传请求结束后临时文件可能已经不在原路径。另外Domino的临时目录路径默认在数据目录的IBM_TECHNICAL_SUPPORT或者系统临时目录下不同版本路径不一样很难硬编码。解决不要依赖上传控件的临时文件路径。正确的做法是在同一个请求里处理上传如果用的是XPages在文件上传的托管Bean里直接把InputStream交给POI解析或者先把附件转存到Notes文档的RichText字段代理再从这个文档的EmbeddedObject里取字节流。示例EmbeddedObject obj doc.getAttachment(Body); obj.extractFile(C:/temp/target.xlsx);这样就能拿到稳定路径。5.5 现象LotusScript读CSV中文乱码Java代理读又一切正常用LotusScript的Line Input读CSV中文全变成“锟斤拷”但同一个文件在Java里用InputStreamReader读正常。原因LotusScript的Line Input按系统默认区域代码读取文件。中文Windows服务器默认ANSIGBK时能读GBK文件如果CSV是UTF-8编码LotusScript就会按ANSI解码得到乱码。而Java代理可以显式指定InputStreamReader的字符集所以正常。解决如果文件是UTF-8优先用Java代理读如果一定要用LotusScript让用户把Excel另存为“CSV逗号分隔”而不是“CSV UTF-8逗号分隔”前者是ANSILotusScript在中文环境能读对。实在不行在LotusScript里先以二进制方式读取前3字节判断BOM再决定用哪种方式解码但LotusScript处理字节流很繁琐不如直接用Java。6. 把导入导出做成一套通用BS接口参数表、验证方法和我的落地习惯6.1 设计统一入口代理action、fileName、formName三个参数就够了把所有导入导出代码合并进一个代理类用action分发。我在上一个项目里就是这样做的代码量没有增加维护成本大大降低。参数约定见第4.4节的表这里补一条不要用真实的Windows路径作为唯一标识应该加一个fileMark参数用随机数或时间戳标识一次上传避免并发时互相覆盖。关键代码片段省略异常处理和具体读写public void NotesMain() { Session session getSession(); AgentContext ctx session.getAgentContext(); Document dc ctx.getDocumentContext(); String action dc.getItemValueString(action); if (import.equals(action)) { doImport(session, ctx, dc); } else if (export.equals(action)) { doExport(session, ctx, dc); } }逻辑说明分发逻辑非常直接每个分支单独写方法比写多个代理更容易维护。参数校验在分支方法里做不要让主方法包揽一切。6.2 线上验证清单用这三种Excel文件把代理打回原形每次改完导入导出代码我至少会用三种文件验证。第一种一个真正的.xlsx包含日期、数字、文本、空行、带逗号的字段用Excel本机保存。第二种一个.xls老格式同样内容验证HSSF路径。第三种一个纯CSV文件但后缀改成.xlsx用来验证“假Excel”场景。如果三种都能正确处理基本可以上线。日期字段我再提一个习惯导入后立即在Notes里打开刚创建的文档看日期的时区和时间部分是否准确。如果日期比Excel里多了一天或少了八小时通常是服务器时区与POI的DateUtil处理冲突这时要明确在代码里指定TimeZone而不是靠服务器默认时区。最后一个教训写在结尾我最早做这个功能时为图省事让LotusScript直接用Line Input读一个所谓的大Excel实际是个CSV结果客户反馈数据错位。后来养成一个习惯——无论需求多简单我都会先确认文件真实格式再决定用哪种代理。很多Excel导入导出的坑其实不是代码不行而是在选型那一刻就错了。希望帮到你。本文还有配套的精品资源点击获取
返回列表