跨平台多用户环境下PDF表单“序列号生成的服务器端方案“
在PDF表单处理中,经常需要为每个表单生成唯一的序列号或表单编号。当所有表单都在同一台计算机上由同一用户处理时,可以通过JavaScript将编号存储在另一个表单或全局JavaScript数据中来实现。然而,当需要在多台计算机或多个用户环境中使用时,就需要更复杂的服务器端解决方案。
解决方案架构
要实现跨多环境的序列号生成功能,我们需要:
- 运行在表单中的JavaScript程序
- 运行在服务器上的实际Web服务
这里的"服务器"概念很广泛,可以是Windows Server、Mac OS Server、Linux等专业服务器系统,也可以是安装了服务器软件(如免费的XAMPP)的普通工作站。
技术限制
需要注意的是,Acrobat JavaScript实现的SOAP调用功能在免费Adobe Reader中仅在文档应用了"Forms"权限时才可用。要实现"Forms"权限,需要LiveCycle Reader Extensions软件。
表单设计
我们需要创建一个至少包含两个字段的表单:
- 一个字段用于存储表单编号/序列号
- 一个字段用于填写表单创建者或接收者的姓名
此外,表单还需要一个按钮来请求新的序列号。生成新序列号时,系统还应存储用户名和当前时间日期,以便后续追踪文档处理情况。
Web服务实现
为了简化PDF表单中的实现,我们首先创建提供唯一编号的Web服务。该服务应提供一个名为"getSerialNumber()"的函数,该函数接受一个参数(用户名)并返回包含新编号的字符串。
数据库设计
我们使用MySQL数据库来生成唯一且连续的编号。通过在MySQL数据库中定义一个作为主键的字段,每次插入新记录时,该索引会自动递增(从第一条记录的1开始)。
以下是创建所需数据库和表的MySQL命令:
CREATE DATABASE serialnumbers;
CREATE TABLE serialnumbers (idx INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30), date TIMESTAMP);
CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
GRANT INSERT,SELECT ON serialnumbers to 'theUser'@'localhost';
PHP实现
以下是实现该系统的PHP脚本,它将用户名和当前日期写入数据库,然后返回新记录的索引:
<?phpfunction getNextSerial($userName) {$mysqli = new mysqli("localhost", "theUser", "thePassword", "serialnumbers");if (mysqli_connect_errno()) {printf("Connect failed: %s\n", mysqli_connect_error());exit();}$query = "INSERT INTO serialnumbers (username, date) VALUES (\"" . $mysqli->real_escape_string($userName) . "\", NOW())";$mysqli->query($query);$idx = $mysqli->insert_id;$mysqli->close();return $idx;}ini_set("soap.wsdl_cache_enabled", "0");class getSerialResponse{public $return;}class getSerialClass{public function getSerialNumber($parameters){$response = new getSerialResponse();$response->return = getNextSerial($parameters->userName);return $response;}} $server = new SoapServer("GetSerialNumber.wsdl");$server->setClass("getSerialClass");$server->handle();
?>
此脚本假设在同一目录中存在名为"GetSerialNumber.wsdl"的WSDL文件,该文件又引用也需要位于同一目录中的模式文件。
PDF表单中的JavaScript实现
在示例文档中,我们使用以下JavaScript代码:
// 获取WSDL代理对象
var myProxy = Net.SOAP.connect("http://localhost/GetSerial/GetSerialNumber.wsdl?wsdl");// 从字段获取用户名
var userName = this.getField("UserName").value;
if (userName != "") {var result = myProxy.getSerialNumber(userName);this.getField("Result").value = util.printf("%04d", Number(result));// 隐藏按钮event.target.display = display.hidden;
}
else {app.alert("Please fill in a user name");
}
代码首先加载与PHP Web服务一起安装的WSDL文件,然后获取"UserName"字段的内容。如果该字段非空,则通过SOAP代理对象请求新的序列号,将结果格式化为带前导零的数字,并隐藏用于生成序列号的按钮。
部署注意事项
使用这些文件时,必须确保调整JavaScript和PHP代码中使用的所有URL,使其与您的安装匹配。示例中使用的是"http://localhost/GetSerial",您需要搜索该字符串并将其替换为安装目录的正确路径。
替代方案
如果SOAP实现过于复杂,可以考虑基于FDF的解决方案,它也能实现数据库与PDF表单之间的数据交互,而无需使用SOAP。
系统架构图
修正后的PHP Web服务代码
<?php
// 错误报告设置(开发环境)
error_reporting(E_ALL);
ini_set('display_errors', 1);/*** 获取下一个序列号* @param string $userName 用户名(自动过滤SQL注入)* @return int 序列号* @throws Exception 数据库错误时抛出*/
function getNextSerial($userName) {// 数据库配置(应存储在配置文件中)$config = ['host' => 'localhost','user' => 'serial_user','pass' => 'secure_password_123','db' => 'serial_numbers_db','port' => 3306];// 创建安全的数据库连接$mysqli = new mysqli($config['host'],$config['user'],$config['pass'],$config['db'],$config['port']);// 检查连接if ($mysqli->connect_errno) {throw new Exception("数据库连接失败: " . $mysqli->connect_error);}// 准备预处理语句防止SQL注入$stmt = $mysqli->prepare("INSERT INTO serial_numbers (username, create_date) VALUES (?, NOW())");if (!$stmt) {throw new Exception("预处理失败: " . $mysqli->error);}// 绑定参数并执行$stmt->bind_param("s", $userName);if (!$stmt->execute()) {throw new Exception("执行失败: " . $stmt->error);}$serial = $mysqli->insert_id;// 清理资源$stmt->close();$mysqli->close();return $serial;
}// 禁用WSDL缓存(开发环境)
ini_set("soap.wsdl_cache_enabled", "0");try {// 创建SOAP服务$server = new SoapServer("SerialService.wsdl");// 注册服务类$server->setClass("SerialService");// 处理请求$server->handle();
} catch (Exception $e) {// 记录错误日志file_put_contents('soap_error.log', date('[Y-m-d H:i:s] ') . $e->getMessage() . PHP_EOL, FILE_APPEND);// 返回SOAP错误header("HTTP/1.1 500 Internal Server Error");die($e->getMessage());
}/*** SOAP服务类*/
class SerialService {/*** 获取序列号* @param string $userName 用户名* @return string 格式化后的序列号(如"000123")*/public function getSerialNumber($userName) {// 验证输入if (empty($userName) {throw new SoapFault("Client", "用户名不能为空");}if (strlen($userName) > 30) {throw new SoapFault("Client", "用户名最长30个字符");}try {$serial = getNextSerial($userName);return str_pad($serial, 6, '0', STR_PAD_LEFT);} catch (Exception $e) {throw new SoapFault("Server", $e->getMessage());}}
}
?>
修正后的JavaScript代码
/*** 生成序列号按钮点击事件*/
function generateSerialNumber() {try {// 1. 获取用户名输入var userNameField = this.getField("UserName");var userName = userNameField.value.trim();// 2. 验证输入if (!userName) {app.alert({cTitle: "输入错误",cMsg: "请输入用户名后再生成序列号",nIcon: 2 // 警告图标});return;}// 3. 连接SOAP服务var wsdlUrl = "http://your-domain.com/SerialService.php?wsdl";var soapClient = Net.SOAP.connect(wsdlUrl);// 4. 调用服务var serialNumber = soapClient.getSerialNumber(userName);// 5. 显示结果this.getField("SerialNumber").value = serialNumber;this.getField("GenerateBtn").display = display.hidden;// 6. 记录生成时间this.getField("GenerationTime").value = util.printd("yyyy-mm-dd HH:MM:ss", new Date());} catch (e) {console.println("错误: " + e);app.alert({cTitle: "系统错误",cMsg: "生成序列号失败: " + e.message,nIcon: 3 // 错误图标});}
}// 初始化时检查是否已有序列号
if (this.getField("SerialNumber").value) {this.getField("GenerateBtn").display = display.hidden;
}
数据库优化方案
-- 创建专用数据库
CREATE DATABASE serial_numbers_db
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;-- 创建数据表(优化版)
CREATE TABLE serial_numbers (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,username VARCHAR(30) NOT NULL,create_date DATETIME NOT NULL,client_ip VARCHAR(45),user_agent VARCHAR(255),INDEX idx_username (username),INDEX idx_date (create_date)
) ENGINE=InnoDB;-- 创建专用用户(最小权限原则)
CREATE USER 'serial_user'@'%' IDENTIFIED BY 'secure_password_123';
GRANT INSERT, SELECT ON serial_numbers_db.* TO 'serial_user'@'%';
FLUSH PRIVILEGES;
常见错误解决方案
错误类型 | 可能原因 | 解决方案 |
---|---|---|
连接超时 | 服务器未启动/防火墙阻止 | 1. 检查PHP服务是否运行 2. 开放端口(通常80/443) |
数据库错误 | 凭证不正确/表不存在 | 1. 验证数据库配置 2. 执行提供的SQL创建表 |
SOAP解析失败 | WSDL文件错误 | 1. 确保WSDL可访问 2. 使用SoapUI测试服务 |
权限不足 | Adobe Reader限制 | 1. 使用Acrobat Pro 2. 申请Reader扩展权限 |
部署检查清单
-
服务器环境
- 安装PHP SOAP扩展 (
php-soap
) - 配置MySQL/MariaDB
- 设置正确的文件权限
- 安装PHP SOAP扩展 (
-
PDF表单配置
- 使用Acrobat Pro XI或更高版本
- 启用JavaScript特权
- 测试跨域访问
-
安全措施
- 替换示例数据库密码
- 配置HTTPS加密
- 实施IP访问限制
如需进一步测试,可以使用以下SOAP请求示例(通过Postman):
POST /SerialService.php HTTP/1.1
Host: your-domain.com
Content-Type: text/xml;charset=UTF-8
SOAPAction: "urn:SerialService#getSerialNumber"<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="urn:SerialService"><soapenv:Header/><soapenv:Body><ser:getSerialNumber><userName>测试用户</userName></ser:getSerialNumber></soapenv:Body>
</soapenv:Envelope>
应用场景示例:
以下代码实现了 PDF文档与远程SOAP服务交互 的功能,核心应用场景包括:
- 在PDF中动态获取网络时间(互联网授时)
- 通过SOAP协议调用远程Web服务
- 将服务返回结果嵌入PDF表单字段
- 实现动态文档内容更新(如序列号生成、时间戳)
适用于:电子合同签署时间认证、动态表单数据填充、许可证激活系统等场景。
// 获取网络时间服务
try {const cURL = "http://quan.suning.com/getSysTime.do";const oRequest = {"ns1:getSysTime": { xmlns: "http://quan.suning.com/",inputString: "sysTime1"}};const response = Net.SOAP.request({cURL: cURL,oRequest: oRequest,cAction: "http://quan.suning.com/getSysTime",cVersion: SOAPVersion.version_1_2,bEncoded: true // 启用XML编码});app.alert("当前网络时间:" + response.sysTime2);
} catch (e) {console.println("SOAP请求错误: " + e.message);app.alert("时间同步失败,请检查网络连接");
}
-
SOAP版本兼容性
// 强制使用1.2版本 cVersion: SOAPVersion.version_1_2
-
调试配置
SOAP.wireDump = "true"; // 显示原始通信数据
-
跨域安全配置
<!-- 服务器需配置CORS --> Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST
-
PDF权限清单
/* 需要启用的权限:- 网络访问- 表单提交- 动态内容执行- 外部对象访问 */
常见错误解决方案
错误类型 | 解决方案 |
---|---|
SOAPError: | 检查XML命名空间与服务器端匹配 |
MethodNotAllowed | 确保服务器允许POST方法 |
权限拒绝 | 启用Adobe Reader扩展权限 |
空响应 | 验证SOAP Action URI正确性 |
编码错误 | 设置bEncoded: true并检查XML结构 |
通过以上配置,可实现PDF文档与SOAP服务的稳定交互。建议使用Acrobat Pro XI以上版本进行调试,并始终在try-catch块中处理网络请求。