在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脚本,它将用户名和当前日期写入数据库,然后返回新记录的索引:
<?php
function 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块中处理网络请求。