ProjeQtOr 13.1.1 配置参数详解
逐项拆解 ProjeQtOr 13.1.1 的 parameters.php 与 parameter 表:数据库连接、目录、日志级别、邮件 SMTP、密码与认证、LDAP/SAML、计划财务、Cron 周期,含实测回显、生产模板与排障清单。
ProjeQtOr 13.1.1 配置参数详解
目标:把 ProjeQtOr 13.1.1 的配置拆成「
parameters.php引导层 +parameter数据表业务层」两套体系,逐项说明数据库连接、目录、日志、邮件、安全、计划财务、Cron 等参数的真实取值、修改方法与生效条件,并给出生产环境推荐模板与排障清单。版本查询日期:2026-10-28;实测日期:2026-10-28。
演示管理账号:
admin,密码:q1w2e3r4。安装步骤见站内文档 《ProjeQtOr 13.1.1 Ubuntu 22.04 安装指南》。
一、环境与前置条件
本章固定实测环境的版本、配置路径与权限,后续所有命令和回显都基于这套环境。读者应先在自己的服务器上执行同样的检查,确认版本一致后再改参数。
1.1 软件版本与运行状态
$ php -v | head -1
PHP 8.1.2-1ubuntu2.28 (cli) (built: Oct 19 2026 18:54:45) (NTS)
$ mariadb --version
mariadb Ver 15.1 Distrib 10.6.23-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
$ systemctl is-active nginx php8.1-fpm mariadb
active
active
active
| 组件 | 版本 | 与配置相关的职责 |
|---|---|---|
| Ubuntu | 22.04.5 LTS | 提供 systemd、apt 与目录权限模型 |
| Nginx | 1.18.0-6ubuntu14.22 | 读取 client_max_body_size,限制上传体积 |
| PHP-FPM | 8.1.2-1ubuntu2.28 | 读取 php.ini,执行 parameters.php |
| MariaDB | 10.6.23-0ubuntu0.22.04.1 | 存储 parameter 表与全部业务数据 |
| ProjeQtOr | 13.1.1 | 应用本身,配置读取逻辑在 model/Parameter.php |
1.2 配置文件的真实位置
$ ls -l /var/lib/projeqtor/config/
total 4
-rw-r--r-- 1 www-data www-data 564 Oct 27 17:29 parameters.php
$ cat /var/www/projeqtor/tool/parametersLocation.php
<?php
$parametersLocation = '/var/lib/projeqtor/config/parameters.php';
| 路径 | 作用 | 建议权限 |
|---|---|---|
/var/www/projeqtor | 程序代码,含 tool/parametersLocation.php 指针 | 755,属主 www-data |
/var/lib/projeqtor/config/parameters.php | 数据库连接、日志、UTF-8 开关 | 640,属主 www-data |
/var/lib/projeqtor/log | 运行日志,按天生成 | 755,属主 www-data |
/var/lib/projeqtor/files/attach | 附件 | 755,属主 www-data |
MariaDB projeqtor.parameter | 业务参数 | 仅数据库账号可读写 |
parameters.php 里含数据库明文密码,实测权限是 644,生产环境应改为 640,避免同机其他账号读取:
$ sudo chmod 640 /var/lib/projeqtor/config/parameters.php
$ ls -l /var/lib/projeqtor/config/parameters.php
-rw-r----- 1 www-data www-data 564 Oct 27 17:29 /var/lib/projeqtor/config/parameters.php
1.3 改参数前必须确认的三件事
- 改的是哪一层:数据库连接、日志路径、UTF-8 在
parameters.php;业务参数在parameter表。 - 谁来加载:PHP-FPM 在每次请求时重新执行
parameters.php,改完必须systemctl reload php8.1-fpm;数据库参数由应用在会话中缓存。 - 能否回滚:改前备份
parameters.php与parameter表,出问题能逐条还原。
二、配置体系总览
2.1 两层配置的分工
ProjeQtOr 把配置拆成两层,原因很直接:数据库连接信息不能存在数据库里,否则形成循环依赖。
| 层 | 载体 | 内容 | 修改方式 |
|---|---|---|---|
| 引导层 | parameters.php | 数据库类型/主机/端口/库名/账号/密码/前缀、SSL 证书路径、日志文件与级别、UTF-8 开关 | 编辑文件 + 重载 PHP-FPM |
| 业务层 | parameter 表 | 邮件、目录、密码策略、时区、Cron 周期、显示与财务等约 200 项 | Web「参数」界面,或 SQL |
tool/parametersLocation.php 只保存一个 $parametersLocation 变量,安装器把真正的配置文件放到 Web 根之外。升级覆盖代码目录时,这个指针文件必须保留,否则应用找不到配置。
2.2 读取顺序与缓存机制
model/Parameter.php 的 getGlobalParameter($code) 按以下顺序取值:
- 先看同名 PHP 全局变量(即
parameters.php里的$logFile、$logLevel、$enforceUTF8等)。存在就立即返回,不再查库。 - 对
paramDbType、paramDbHost、paramDbPort、paramDbPrefix、paramDbName、paramDbUser、paramDbPassword这七个键,未定义时直接返回空串,不查库。 - 其余键第一次访问时,一次性读取
parameter表中idUser is null and idProject is null的全部行,写入 session 的globalParametersArray,之后从 session 取。 - 某些键有代码级兜底,例如
paramReportTempDirectory未配置时返回../files/report/。
由此得到三条实践结论:
parameters.php的优先级最高,同名的数据库参数不会覆盖它。- 数据库参数缓存在 session 中,界面显示旧值时可退出重新登录强制重建缓存。
- 直接 SQL 改库后不要让应用长期持有旧会话,管理员改参应走「参数」界面,改完刷新页面确认。
2.3 用户级参数与全局参数
parameter 表用 idUser、idProject 两列区分作用域:
| 作用域 | 条件 | 实测行数 |
|---|---|---|
| 全局 | idUser IS NULL AND idProject IS NULL | 200 |
| 用户级 | idUser = 1(admin) | 12 |
| 用户级 | idUser = 2(guest) | 8 |
| 合计 | — | 220 |
用户级参数只覆盖个人偏好(主题、开始页、计划表隐藏列等),不会影响其他用户;getUserParameter() 先查用户行,查不到再回落全局。
三、parameters.php 详解
3.1 文件全文
$ sudo cat /var/lib/projeqtor/config/parameters.php
<?php
// =======================================================================================
// Automatically generated parameter file
// on 2026-10-28 01:29:43
// =======================================================================================
$paramDbType='mysql';
$paramDbHost='127.0.0.1';
$paramDbPort='3306';
$paramDbUser='projeqtor';
$paramDbPassword='q1w2e3r4';
$paramDbName='projeqtor';
$paramDbPrefix='';
$SslKey='';
$SslCert='';
$SslCa='';
$logFile='/var/lib/projeqtor/log/projeqtor_${date}.log';
$logLevel='2';
$enforceUTF8='1';
//======= END===
文件由 Web 安装器生成,顶部注释记录了生成时间。不要手工删除注释块,后续升级排查版本问题时,这个时间点能快速确认配置是哪次安装写入的。
3.2 字段逐项说明
| 变量 | 实测值 | 含义 | 修改注意 |
|---|---|---|---|
$paramDbType | mysql | 数据库驱动,MariaDB 也走 mysql | 可选 mysql、pgsql,装完不要改 |
$paramDbHost | 127.0.0.1 | 数据库地址 | 远程库填内网 IP,并放行 3306 |
$paramDbPort | 3306 | 数据库端口 | 与实例实际端口一致 |
$paramDbUser | projeqtor | 应用账号 | 只授予本库权限 |
$paramDbPassword | q1w2e3r4 | 明文密码 | 文件权限应 640 |
$paramDbName | projeqtor | 库名 | 改名需同步改授权 |
$paramDbPrefix | 空 | 表前缀 | 与库内实际表名一致,为空表示无前缀 |
$SslKey / $SslCert / $SslCa | 空 | 数据库 TLS 证书 | 启用加密时填绝对路径 |
$logFile | .../projeqtor_${date}.log | 日志模板 | ${date} 由程序替换为 Ymd,不要写死日期 |
$logLevel | 2 | 日志级别 | 2=TRACE,3=DEBUG,详见第六章 |
$enforceUTF8 | 1 | 强制 UTF-8 连接 | 中文环境保持 1 |
$paramDbPrefix 为空时,SQL 直接操作 parameter、resource 这类裸表名;如果安装时设置了前缀(如 pq_),改库前必须按前缀写表名,否则会报表不存在。
3.3 修改与生效验证
以把日志级别临时调到 3 为例:
$ sudo sed -i "s/^\$logLevel='2';/\$logLevel='3';/" /var/lib/projeqtor/config/parameters.php
$ grep logLevel /var/lib/projeqtor/config/parameters.php
$logLevel='3';
$ sudo systemctl reload php8.1-fpm
$ systemctl is-active php8.1-fpm
active
重载不重启,正在处理的请求不受影响。验证方式不是看进程,而是触发一次带调试输出的请求,再查日志里是否出现 DEBUG 行,详见 6.3。
3.4 改错后的真实报错
把 $paramDbPassword 改成错误值时,页面不会白屏,而是返回 200 状态码并渲染错误信息:
$ curl -s -o /tmp/dberr.html -w "%{http_code}\n" http://127.0.0.1/view/main.php
200
$ grep -o "SQLSTATE[^<]*" /tmp/dberr.html | head -3
SQLSTATE[HY000] [1045] Access denied for user 'projeqtor'@'localhost' (using password: YES)
同一时刻日志记录更完整的连接上下文:
2026-10-28 02:02:47.337 ***** ERROR ***** [] [1] SQL ERROR : Connection to Database failed
2026-10-28 02:02:47.337 ***** ERROR ***** [] [1] dns='mysql:host=127.0.0.1;port=3306;dbname=projeqtor'
2026-10-28 02:02:47.337 ***** ERROR ***** [] [1] user='projeqtor'
2026-10-28 02:02:47.337 ***** ERROR ***** [] [1] error=SQLSTATE[HY000] [1045] Access denied for user 'projeqtor'@'localhost' (using password: YES)
恢复正确密码后重载,页面立即回到 200 正常状态。改数据库密码时务必同步改三处:数据库账号、parameters.php、以及监控/备份脚本里的连接串。
四、parameter 表结构与读写
4.1 表结构
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "DESCRIBE parameter;"
Field Type Null Key Default Extra
id int(12) unsigned NO PRI NULL auto_increment
idUser int(12) unsigned YES MUL NULL
idProject int(12) unsigned YES NULL
parameterCode varchar(100) YES NULL
parameterValue varchar(4000) YES NULL
五个字段就能表达三种作用域:两列都为空是全局,idUser 有值是用户级,idProject 有值是项目级。parameterValue 是字符串,布尔值用 YES/NO,数字也按字符串存储。
4.2 行数统计
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT COUNT(*) AS total, SUM(idUser IS NULL AND idProject IS NULL) AS global_cnt, SUM(idUser=1) AS u1, SUM(idUser=2) AS u2 FROM parameter;"
total global_cnt u1 u2
220 200 12 8
新建库由安装后的首次登录自动写入默认参数,管理员使用过「参数」界面后行数会增加(用户级偏好被持久化)。
4.3 查询方法
按参数名精确查:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode='paramMailSmtpPort' AND idUser IS NULL;"
parameterCode parameterValue
paramMailSmtpPort 25
按前缀模糊查一类参数,例如所有邮件相关参数:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode LIKE 'paramMail%' AND idUser IS NULL;"
查用户级覆盖:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT idUser,parameterCode,parameterValue FROM parameter WHERE idUser IS NOT NULL ORDER BY idUser,parameterCode LIMIT 6;"
idUser parameterCode parameterValue
1 defaultMenu menuBarCustom
1 idFavoriteRow 1
1 newGui 1
1 planningHideColumnAssignedCost 1
1 planningHideColumnLeftCost 1
1 startPage startGuide.php
4.4 修改与回滚
直接改库适合批量调整或脚本化部署,改前先备份:
$ mysqldump -u projeqtor -pq1w2e3r4 projeqtor parameter > /var/backups/parameter-$(date +%F).sql
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "UPDATE parameter SET parameterValue='120' WHERE parameterCode='maxItemsInTodayLists' AND idUser IS NULL;"
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode='maxItemsInTodayLists' AND idUser IS NULL;"
parameterCode parameterValue
maxItemsInTodayLists 120
回滚就是把值改回去:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "UPDATE parameter SET parameterValue='100' WHERE parameterCode='maxItemsInTodayLists' AND idUser IS NULL;"
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode='maxItemsInTodayLists' AND idUser IS NULL;"
parameterCode parameterValue
maxItemsInTodayLists 100
⚠️ 直接 SQL 改参数不会经过应用的校验与缓存清理。改完应让所有在线用户重新登录,或在「参数」界面再保存一次同值,确保 session 中的 globalParametersArray 被重建。批量变更前建议先停 Cron,避免调度任务读到半套配置。
4.5 参数表的恢复
误删行时,用安装后立即做的备份整表恢复:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor < /var/backups/parameter-2026-10-28.sql
如果备份里表名带前缀,恢复脚本里的表名也要带前缀;恢复后同样需要重新登录。
五、目录参数
5.1 四个目录参数
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('paramAttachmentDirectory','documentRoot','paramReportTempDirectory','cronDirectory') ORDER BY parameterCode;"
parameterCode parameterValue
cronDirectory ../files/cron
documentRoot /var/lib/projeqtor/files/documents/
paramAttachmentDirectory /var/lib/projeqtor/files/attach/
paramReportTempDirectory /var/lib/projeqtor/files/report/
| 参数 | 用途 | 实测配置 |
|---|---|---|
paramAttachmentDirectory | 工单、任务等对象的附件 | 绝对路径,位于 Web 根之外 |
documentRoot | 文档管理模块的文件目录 | 绝对路径 |
paramReportTempDirectory | PDF/报表生成的临时文件 | 绝对路径,未配置时代码返回 ../files/report/ |
cronDirectory | Cron 的 RUNNING/STOP/HISTORY 等状态文件 | 相对路径,实测解析到 /var/www/projeqtor/files/cron |
5.2 相对路径的解析规则
cronDirectory 的值是 ../files/cron,应用按当前工作目录解析。Web 请求的当前目录是 /var/www/projeqtor,因此实际目录是 /var/www/projeqtor/files/cron:
$ find /var/www /var/lib -maxdepth 4 -name cron -type d
/var/www/projeqtor/files/cron
$ ls -l /var/www/projeqtor/files/cron/
total 12
-rw-r--r-- 1 www-data www-data 56 Oct 27 17:37 DELAYS
-rw-r--r-- 1 www-data www-data 124 Oct 27 17:37 HISTORY
-rw-r--r-- 1 www-data www-data 1256 Sep 24 08:58 index.php
生产环境建议把 cronDirectory 改成绝对路径(如 /var/www/projeqtor/files/cron),这样无论从哪个目录触发 Cron,状态文件位置都不会漂移。
5.3 目录缺失的真实排障
安装器只自动创建附件目录,文档目录与报表目录不会预建。把文档目录移走后,以 www-data 身份写入立即失败:
$ mv /var/lib/projeqtor/files/documents /var/lib/projeqtor/files/documents.bak
$ sudo -u www-data touch /var/lib/projeqtor/files/documents/probe.txt
touch: cannot touch '/var/lib/projeqtor/files/documents/probe.txt': No such file or directory
按参数值重建目录、修正属主后即可写入:
$ sudo mkdir -p /var/lib/projeqtor/files/documents
$ sudo chown www-data:www-data /var/lib/projeqtor/files/documents
$ sudo chmod 755 /var/lib/projeqtor/files/documents
$ sudo -u www-data touch /var/lib/projeqtor/files/documents/probe.txt && echo "write OK"
write OK
$ sudo rm -f /var/lib/projeqtor/files/documents/probe.txt
同样的方法适用于 paramReportTempDirectory。三件事必须同时满足:目录存在、属主是 PHP-FPM 运行用户、路径与参数值完全一致(末尾斜杠不影响,但拼写必须一致)。
六、日志参数
6.1 logFile 的日期占位符
$logFile='/var/lib/projeqtor/log/projeqtor_${date}.log'。程序在 logTracing() 里执行 pq_str_replace('${date}', date('Ymd'), $logFile),因此每天生成一个新文件:
$ ls -l /var/lib/projeqtor/log/
total 544
-rw-r--r-- 1 www-data www-data 348 Oct 27 17:36 projeqtor_20261027.log
-rw-r--r-- 1 www-data www-data 551827 Oct 27 17:46 projeqtor_20261028.log
日志目录必须是已存在的可写目录,否则程序会在页面上提示 invalidLogDir 或 lockedLogDir,而不是静默丢弃日志。生产环境建议把日志放到 /var/log/projeqtor/ 并配置 logrotate。
6.2 logLevel 的级别含义
logTracing($message, $level) 的级别定义:
| 级别 | 函数 | 日志标记 | 典型用途 |
|---|---|---|---|
| 1 | errorLog() | ***** ERROR ***** | 数据库连接失败、邮件发送失败 |
| 2 | traceLog() | ===== TRACE ===== | 登录、Cron 启停等关键流程 |
| 3 | debugLog() / debugTraceLog() | ----- DEBUG ----- | 登录校验细节、SQL 上下文 |
| 4 | scriptLog() | ..... SCRIPT .... | 每个脚本入口与客户端 IP |
只有 level <= logLevel 的消息才会写入文件。另有特殊值:logLevel=5 时只把 1–3 级消息直接输出到页面、不写文件;logLevel=9 直接 exit(),属于调试开关,生产环境禁止使用。
6.3 实测对比:级别 2 与级别 3
logLevel=2 时,一次成功登录只留下 TRACE 行:
2026-10-28 02:01:08.316 ===== TRACE ===== [1] NEW CONNECTED USER 'admin'
把 $logLevel 改为 3 并重载 PHP-FPM 后,同一操作会多出校验细节:
2026-10-28 02:00:56.629 ----- DEBUG ----- loginCheck : current db version = 'V13.1.1'
2026-10-28 02:00:56.629 ----- DEBUG ----- User->authenticate('admin', 'dd863cb4362a1dc5bee10414919e1f25e4363d154bc2aa0fa62728d405a5dd3b')
2026-10-28 02:00:56.630 ----- DEBUG ----- User->authenticate : sha256 encryption
2026-10-28 02:00:56.630 ----- DEBUG ----- User->authenticate : Successfull login
2026-10-28 02:00:56.639 ===== TRACE ===== [1] NEW CONNECTED USER 'admin'
排障时临时调到 3,问题定位后立刻改回 2。级别 3 会把密码校验令牌写进日志,长期开启既增大日志体积,也扩大了敏感信息暴露面。
6.4 日志时间与系统时间不一致
实测服务器的系统时区是 UTC,而 PHP 配置的 date.timezone 是 Asia/Shanghai:
$ date "+%Y-%m-%d %H:%M:%S %Z"
2026-10-27 18:01:19 UTC
$ php -r "echo date('Y-m-d H:i:s'),' ',date_default_timezone_get(),PHP_EOL;"
2026-10-28 02:01:19 Asia/Shanghai
日志文件名与行首时间都来自 PHP 的 date(),所以显示为 20261028 与 02:xx。排查时不要拿 date 的 UTC 时间去比对日志,先统一时区:
$ sudo timedatectl set-timezone Asia/Shanghai
$ sudo systemctl restart php8.1-fpm mariadb
6.5 日志体积控制
实测一天正常使用产生约 540 KB 日志,级别 3 会显著放大。用 logrotate 按天切割并保留 14 天:
$ sudo tee /etc/logrotate.d/projeqtor > /dev/null <<'EOF'
/var/log/projeqtor/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
su www-data www-data
}
EOF
copytruncate 适合程序自己持有文件句柄的场景,避免切割后进程继续写旧 inode。
七、邮件参数
7.1 邮件参数清单
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('paramMailSender','paramMailSmtpServer','paramMailSmtpPort','paramMailSmtpUsername','paramMailSmtpPassword','paramMailEol','paramAdminMail','mailerTestDest','mailerTestSender') ORDER BY parameterCode;"
parameterCode parameterValue
mailerTestDest admin@example.com
mailerTestSender sender
paramAdminMail admin@example.com
paramMailEol CRLF
paramMailSender projeqtor@example.com
paramMailSmtpPassword NULL
paramMailSmtpPort 25
paramMailSmtpServer localhost
paramMailSmtpUsername NULL
| 参数 | 作用 | 注意 |
|---|---|---|
paramMailSender | 发件人地址 | 必须与 SMTP 服务允许的域一致,否则被拒 |
paramMailSmtpServer | SMTP 主机 | 为空时测试页直接报必填错误 |
paramMailSmtpPort | SMTP 端口 | 25/465/587 按服务商要求 |
paramMailSmtpUsername / paramMailSmtpPassword | 认证账号 | 密码经 encryptPwd() 加密存储,查库看到的是密文 |
paramMailEol | 行结束符 | 默认 CRLF,部分国内 SMTP 需要 LF |
paramAdminMail | 管理员邮箱 | 用户找回密码、系统告警的接收地址 |
mailerTestDest / mailerTestSender | 测试收件人 / 发件人来源 | sender 表示用 paramMailSender,否则用当前登录用户邮箱 |
发送方式由代码决定:paramMailerType 未设置或为 phpmailer 时走 PHPMailer;为 mail 时调用 PHP mail();为 socket 且配置了认证账号时走内置 socket 实现。实测环境未设置该键,因此走 PHPMailer。
7.2 测试入口与操作
管理员登录后访问「参数」页的邮件测试按钮,等价于请求 tool/sendMailTest.php。测试使用 mailerTestTitle、mailerTestMessage、mailerTestDest 三个参数,其中 ${dbName} 与 ${date} 会被替换。
实测在 127.0.0.1:25 启动 Python 调试 SMTP 服务,先确认端口在监听:
$ ss -ltn | grep ':25 '
LISTEN 0 5 127.0.0.1:25 0.0.0.0:* users:(("python3",pid=36970,fd=3))
7.3 成功时的回显
sender of email : projeqtor@example.com
Email sent to 'admin@example.com'
页面实际 HTML 为:
sender of email : projeqtor@example.com<br/><span style='color:green; font-weight:bold'>Email sent to 'admin@example.com'</span>
这条成功回显只说明 SMTP 会话完成,不代表收件方一定收到。垃圾邮件过滤、发件域 SPF/DKIM 需要另外验证。
7.4 失败时的回显与日志
把 paramMailSmtpPort 改为 2525(该端口无服务)后,同一测试页返回红色错误:
sender of email : projeqtor@example.com<br/><div style='color:red; font-weight:bold;'>SMTP connect() failed. https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting</div>
日志同时记录目标地址、端口、入库编号与底层错误:
2026-10-28 02:00:21.748 ***** ERROR ***** [V13.1.1] [1] SMTP Server : localhost
2026-10-28 02:00:21.748 ***** ERROR ***** [V13.1.1] [1] SMTP Port : 2525
2026-10-28 02:00:21.748 ***** ERROR ***** [V13.1.1] [1] Mail stored in Database : #6
2026-10-28 02:00:21.748 ***** ERROR ***** [V13.1.1] [1] PHPMail error : SMTP connect() failed. https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting
2026-10-27 18:00:21 Connection: opening to localhost:2525, timeout=300, options=array ( 'ssl' => array ( 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true, ),)<br>
2026-10-27 18:00:21 Connection failed. Error #2: stream_socket_client(): Unable to connect to localhost:2525 (Connection refused) [/var/www/projeqtor/external/PHPMailer/src/SMTP.php line 388]<br>
2026-10-27 18:00:21 SMTP ERROR: Failed to connect to server: Connection refused (111)<br>
三个关键信息:
Mail stored in Database : #6说明邮件并未丢弃,而是写入了mailtosend表,恢复 SMTP 后可由 Cron 重发。Connection refused (111)是最常见的失败原因:主机可达但端口没有服务,先查端口再查防火墙。- PHPMailer 的调试行时间戳是 UTC(
18:00:21),与上面应用日志的02:00:21相差 8 小时,不是时钟错乱。
7.5 失败邮件的补发
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT id,mailTo,mailTitle,idStatus FROM mailtosend ORDER BY id DESC LIMIT 5;"
Cron 运行时会处理 mailtosend 队列。修复 SMTP 参数后,启动一次 Cron 即可补发;若队列里积压大量历史邮件,先备份再清理,避免一次性触发大量外发。
7.6 邮件排障顺序
- 参数层:服务器、端口、账号、密码、发件人是否齐全且正确。
- 网络层:
ss -ltn看本机端口,nc -zv <smtp主机> <端口>看远端连通。 - 认证层:用户名密码错误通常表现为
SMTP Error: Could not authenticate。 - 内容层:收件人地址非法时 PHPMailer 报
Invalid address。 - 队列层:确认
mailtosend是否有积压,避免修复后被历史邮件淹没。
八、安全与认证参数
8.1 密码策略参数
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('paramPasswordMinLength','paramPasswordStrength','paramDefaultPassword','requiredOldPassword','oldPasswordDifferentNew','lockPassword','passwordResetEnabled','otpForAdminUsers','otpForNewUsers','otpForDirectConnectionsWithSso') ORDER BY parameterCode;"
parameterCode parameterValue
lockPassword NULL
oldPasswordDifferentNew YES
otpForAdminUsers NO
otpForDirectConnectionsWithSso NO
otpForNewUsers NO
paramDefaultPassword q1w2e3r4
paramPasswordMinLength 5
paramPasswordStrength 1
passwordResetEnabled NO
requiredOldPassword YES
| 参数 | 实测 | 含义 |
|---|---|---|
paramPasswordMinLength | 5 | 最小长度,生产建议 8 以上 |
paramPasswordStrength | 1 | 复杂度:1 仅长度,2 加大写与小写,3 再加数字,4 再加特殊字符 |
paramDefaultPassword | q1w2e3r4 | 新建用户的初始密码,首次登录强制修改 |
requiredOldPassword | YES | 改密时必须输入旧密码 |
oldPasswordDifferentNew | YES | 新密码不能与旧密码相同 |
lockPassword | NULL | 锁定密码策略,非空时禁止改密 |
passwordResetEnabled | NO | 关闭「忘记密码」自助重置 |
otpForAdminUsers / otpForNewUsers | NO | 管理员/新用户强制双因素 |
otpForDirectConnectionsWithSso | NO | SSO 直连时是否要求 OTP |
启用双因素前必须先配置邮件或 TOTP 通道,否则用户会被锁在登录页。改密码策略后,已存在的弱密码不会自动失效,需要配合「密码过期」策略或强制重置。
8.2 密码如何存储
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT id,name,password,salt,crypto FROM resource WHERE id=1;"
id name password salt crypto
1 admin 1df0832aeb3dba10a643beef24a2651ec64a227c9a2d0466228fb940abb1ff54 pj13lab sha256
V3.0 起用户表已改名为 resource,isUser=1 的行才是可登录用户。密码字段不是明文,而是 sha256(明文 + salt),salt 每个用户独立,crypto 表明哈希算法。
8.3 登录时的加密与会话
浏览器提交的密码不是原始密码,而是 sha256(存储的哈希 + sessionSalt)。登录处理 tool/loginCheck.php 用 AES-CTR 解密登录名,User::authenticate() 再比对令牌。实测日志可完整还原这一过程:
2026-10-28 02:00:56.629 ----- DEBUG ----- loginCheck : current db version = 'V13.1.1'
2026-10-28 02:00:56.629 ----- DEBUG ----- User->authenticate('admin', 'dd863cb4362a1dc5bee10414919e1f25e4363d154bc2aa0fa62728d405a5dd3b')
2026-10-28 02:00:56.630 ----- DEBUG ----- User->authenticate : sha256 encryption
2026-10-28 02:00:56.630 ----- DEBUG ----- User->authenticate : Successfull login
令牌与期望值不一致时会明确记录:
2026-10-28 01:58:54.423 ----- DEBUG ----- User->authenticate : wrong password a41af864b404a64b6fc2c877a6cc912a16a8cd3e000b87544263032e5723be12!=agFHzF7m4GqsVZrBna7vvQ== (exit)
会话数据存放在 ProjeQtOr_projeqtor_0 命名空间下,sessionSalt 是每次会话生成的 16 字符随机串,另外还缓存 globalParametersArray 与用户偏好。查看当前会话文件数量:
$ sudo ls /var/lib/php/sessions | wc -l
11
要让所有在线用户强制重新登录,删除会话文件即可;重启 PHP-FPM 只会断开当前连接,不会清除会话:
$ sudo rm -f /var/lib/php/sessions/sess_*
$ sudo systemctl reload php8.1-fpm
8.4 LDAP 参数
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode LIKE 'paramLdap%' ORDER BY parameterCode;"
parameterCode parameterValue
paramLdap_allow_login false
paramLdap_base_dn dc=mydomain,dc=com
paramLdap_host localhost
paramLdap_port 389
paramLdap_search_pass secret
paramLdap_search_user cn=Manager,dc=mydomain,dc=com
paramLdap_user_filter uid=%USERNAME%
paramLdap_version 3
启用顺序:先用 ldapsearch 在服务器上验证绑定与过滤条件,再改 paramLdap_allow_login=true,最后按需设置 ldapDefaultProfile 与 ldapMsgOnUserCreation。paramLdap_search_pass 在 V9.2.0 之后按密文处理,直接 SQL 写明文会导致绑定失败,必须在界面里重新录入。LDAP 用户名统一转小写存储,这一点与本地账号不同。
8.5 SAML 与 SSO
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode LIKE 'SAML_%' ORDER BY parameterCode;"
parameterCode parameterValue
SAML_attributeMail mail
SAML_attributeUid uid
SAML_defaultProfile 5
SAML_ssoCommonName SSO
SAML 还需要在界面补充 IdP 实体 ID、证书、单点登录与登出地址。上线前用管理员账号完整走一遍登录与登出,确认属性映射能拿到稳定的 uid。
8.6 安全基线
parameters.php权限 640,数据库账号只授权本库。- 生产环境关闭
passwordResetEnabled,除非邮件通道已验证可用。 - 管理员开
otpForAdminUsers=YES,并保留一个可用的恢复路径。 - 日志级别保持 2,禁止长期 3 以上。
- 附件、文档、报表目录全部位于 Web 根之外。
九、计划、财务与显示参数
9.1 时区、语言与主题
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('paramDefaultTimezone','paramDefaultLocale','defaultTheme','startPage','paramRowPerPage','paramIconSize','paramTopIconSize','limitDisplayPlanning','maxItemsInTodayLists','maxProjectsToDisplay') ORDER BY parameterCode;"
parameterCode parameterValue
defaultTheme ProjeQtOrFlatBlue
limitDisplayPlanning 2000
maxItemsInTodayLists 100
maxProjectsToDisplay 25
paramDefaultLocale en
paramDefaultTimezone Asia/Shanghai
paramIconSize 22
paramTopIconSize 16
paramRowPerPage NULL
startPage today.php
paramDefaultTimezone 决定页面显示的日期时间,与应用日志同源;paramDefaultLocale 控制新用户默认语言;paramRowPerPage 为空时使用列表默认分页。limitDisplayPlanning 限制计划视图一次加载的记录数,超过 2000 条会截断,大项目应下调而不是上调,否则浏览器渲染会明显变慢。
9.2 工作时间与工作日
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('dayTime','startAM','endAM','startPM','endPM','OpenDayMonday','OpenDaySunday') ORDER BY parameterCode;"
parameterCode parameterValue
OpenDayMonday openDays
OpenDaySunday offDays
dayTime 8.00
endAM 12:00
endPM 18:00
startAM 08:00
startPM 14:00
dayTime 是一天标准工时,用于成本与负荷换算;OpenDay* 七个键决定计划表里的可用工作日,取值 openDays / offDays。调整午休与工时后,历史任务的已填工时不会重算,只影响后续排期。
9.3 编号与参考格式
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('billPrefix','billSuffix','billNumSize','billNumStart','referenceFormatPrefix','documentReferenceFormat','versionReferenceSuffix','draftSeparator') ORDER BY parameterCode;"
parameterCode parameterValue
billPrefix BILL
billNumSize 5
billNumStart 10000
billSuffix _FR
documentReferenceFormat {PROJ}-{TYPE}-{NUM}-{NAME}
draftSeparator _draft
referenceFormatPrefix {PROJ}-{TYPE}-
versionReferenceSuffix -{VERS}
编号格式里的 {} 是占位符,由对象字段替换。修改 billNumStart 只影响新单据,不会重排历史单据。draftSeparator 用于标识草稿文档,改错会导致草稿与正式版本命名冲突。
9.4 财务参数
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('currency','currencyPosition','ImputOfAmountClient','ImputOfAmountProvider','ImputOfBillLineClient','ImputOfBillLineProvider','consolidateValidated','useOrganizationBudgetElement') ORDER BY parameterCode;"
parameterCode parameterValue
ImputOfAmountClient HT
ImputOfAmountProvider HT
ImputOfBillLineClient HT
ImputOfBillLineProvider HT
consolidateValidated IFSET
currency €
currencyPosition after
useOrganizationBudgetElement NO
HT(不含税)与 TTC(含税)决定金额录入口径,切换前务必与财务确认;consolidateValidated 控制已确认数据是否进入汇总,IFSET 表示按设置执行。币种符号只影响显示,不改数据库中的数值。
9.5 显示与交互
paramLayoutObjectDetail=tab 决定详情区用页签布局,menuLeftDisplayMode=ICONTXT 控制左侧菜单图标与文字,paramFadeLoadingMode=true 开启加载淡入。这些参数只影响体验,不影响数据。用户级偏好(planningHideColumn*、startPage)会覆盖全局值,排查“别人正常我看不到”的问题时先查 idUser 行。
十、Cron 与通知参数
10.1 调度参数
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('cronSleepTime','cronCheckDates','cronCheckImport','cronCheckEmails','cronCheckNotifications','cronDirectory','notificationSystemActiv','cronArchiveTime','cronArchiveCloseItems','cronArchivePlannedDate','mailGroupActive','mailGroupPeriod') ORDER BY parameterCode;"
parameterCode parameterValue
cronArchiveCloseItems YES
cronArchivePlannedDate 18:30
cronArchiveTime 365
cronCheckDates 30
cronCheckEmails -1
cronCheckImport 60
cronCheckNotifications 3600
cronDirectory ../files/cron
cronSleepTime 10
mailGroupActive NO
mailGroupPeriod 60
notificationSystemActiv NO
| 参数 | 实测 | 单位 | 作用 |
|---|---|---|---|
cronSleepTime | 10 | 秒 | 每次循环休眠,所有周期都以它为步长递减 |
cronCheckDates | 30 | 秒 | 日期告警、里程碑提醒的检查间隔 |
cronCheckImport | 60 | 秒 | 扫描导入目录的间隔 |
cronCheckEmails | -1 | 秒 | 收邮件转备注;负值表示关闭,未配置时代码默认 300 |
cronCheckNotifications | 3600 | 秒 | 通知系统检查间隔,仅 notificationSystemActiv=YES 时生效 |
cronArchiveTime | 365 | 天 | 已关闭事项归档的时间阈值 |
cronArchivePlannedDate | 18:30 | 时刻 | 每天执行归档的时间 |
mailGroupPeriod | 60 | 分钟 | 邮件合并发送窗口,mailGroupActive=NO 时不合并 |
这些值是“秒数倒计时”,不是 cron 表达式。循环每次减去 cronSleepTime,减到 0 才执行一次,再把计数重置为配置值。因此把 cronSleepTime 调大,会同时拉长所有周期的实际间隔。
10.2 实测状态文件
$ cat /var/www/projeqtor/files/cron/DELAYS
SleepTime=10|CheckDates=30|CheckImport=60|CheckEmails=-1
$ tail -4 /var/www/projeqtor/files/cron/HISTORY
2026-10-28 01:34:05 | 1 | run
2026-10-28 01:35:35 | | stop
2026-10-28 01:37:13 | 1 | run
2026-10-28 01:37:43 | | stop
DELAYS 是当前运行实例的周期快照,改完参数需要重启 Cron 才会重写。HISTORY 记录启动、停止与操作者 ID。RUNNING 存在表示有实例在跑,超过 30 分钟未更新会被判定为僵死并自动清理。AUTO 文件只在批量模式下生成,普通运行不会出现。
10.3 通知与归档
notificationSystemActiv=NO 时 cronCheckNotifications 不参与调度,即使设置成 3600 也不会执行。开启通知前先确认邮件通道可用,否则通知只会堆积在 mailtosend。归档参数影响的是历史数据可见性:cronArchiveCloseItems=YES 会把已关闭且超过 365 天的事项归档,归档后默认列表不再显示,但数据仍在库中,可通过筛选条件查看。
十一、生产环境推荐配置
11.1 推荐值
| 参数 | 建议值 | 理由 |
|---|---|---|
$logLevel | 2 | 保留 TRACE,避免密码令牌入日志 |
$logFile | /var/log/projeqtor/projeqtor_${date}.log | 与代码、数据分离,便于 logrotate |
$enforceUTF8 | 1 | 中文不乱码 |
paramDefaultTimezone | Asia/Shanghai | 与业务时区一致 |
paramPasswordMinLength | 8 | 满足常见合规要求 |
paramPasswordStrength | 3 | 长度 + 大小写 + 数字 |
passwordResetEnabled | NO | 邮件通道未验证前关闭 |
otpForAdminUsers | YES | 管理员强制双因素 |
paramAttachmentDirectory | /var/lib/projeqtor/files/attach/ | 绝对路径,Web 根之外 |
documentRoot | /var/lib/projeqtor/files/documents/ | 同上 |
paramReportTempDirectory | /var/lib/projeqtor/files/report/ | 定时清理临时文件 |
cronDirectory | /var/www/projeqtor/files/cron | 绝对路径,避免 CWD 漂移 |
cronSleepTime | 10 | 10 秒粒度足够 |
cronCheckEmails | 300 | 未接入 IMAP 时保持 -1 |
limitDisplayPlanning | 500 | 大项目降低渲染压力 |
paramMailSmtpPort | 587 | 多数企业邮箱要求提交端口 + TLS |
paramMailSmtpUsername | 实际账号 | 必须与 SMTP 认证一致 |
paramMailEol | CRLF | 与主流 SMTP 兼容 |
11.2 落地步骤
$ sudo cp -a /var/lib/projeqtor/config/parameters.php /var/backups/parameters.php.$(date +%F)
$ mysqldump -u projeqtor -pq1w2e3r4 projeqtor parameter > /var/backups/parameter.$(date +%F).sql
然后按 3.3 修改 parameters.php 并重载 PHP-FPM,按 4.4 逐条调整数据库参数,最后重启一次 Cron 让 DELAYS 重写。
11.3 变更纪律
- 一次只改一类参数,改完立即验证,避免多个变量同时变化导致无法定位。
- 改邮件、密码策略、LDAP 这类登录相关参数前,保留一个已知可用的管理员会话。
- 所有 SQL 变更走工单记录,附备份文件路径与回滚语句。
- 变更后检查
HISTORY、mailtosend与错误日志,确认没有新增异常。
十二、常见问题与排障
12.1 改了 parameters.php 不生效
原因是没有重载 PHP-FPM,或改的是另一份配置文件。确认指针文件指向真实配置:
$ cat /var/www/projeqtor/tool/parametersLocation.php
<?php
$parametersLocation = '/var/lib/projeqtor/config/parameters.php';
$ sudo systemctl reload php8.1-fpm
12.2 数据库连接失败
页面返回 200 但内容含 SQLSTATE,日志有完整上下文:
***** ERROR ***** [] [1] SQL ERROR : Connection to Database failed
***** ERROR ***** [] [1] dns='mysql:host=127.0.0.1;port=3306;dbname=projeqtor'
***** ERROR ***** [] [1] user='projeqtor'
***** ERROR ***** [] [1] error=SQLSTATE[HY000] [1045] Access denied for user 'projeqtor'@'localhost' (using password: YES)
依次核对:库是否在运行、账号密码是否正确、dbname 是否存在、账号是否授权到该库。改回正确密码后重载 PHP-FPM 即恢复。
12.3 附件或文档上传失败
先看目录是否存在、属主是否正确:
$ sudo -u www-data test -w /var/lib/projeqtor/files/attach && echo "attach writable"
attach writable
$ ls -ld /var/lib/projeqtor/files/attach /var/lib/projeqtor/files/documents /var/lib/projeqtor/files/report
drwxr-xr-x 2 www-data www-data 4096 Oct 27 17:40 /var/lib/projeqtor/files/attach
drwxr-xr-x 2 www-data www-data 4096 Oct 27 18:01 /var/lib/projeqtor/files/documents
drwxr-xr-x 2 www-data www-data 4096 Oct 27 17:40 /var/lib/projeqtor/files/report
目录缺失的报错是 No such file or directory,权限不足是 Permission denied。另外检查 PHP 的 upload_max_filesize、post_max_size 与 Nginx 的 client_max_body_size 三者是否都不小于业务允许的最大附件:
$ php -r "echo ini_get('upload_max_filesize'),' / ',ini_get('post_max_size'),PHP_EOL;"
100M / 100M
12.4 邮件发不出去
先看测试页回显,再看日志里的 SMTP Server、SMTP Port 与 PHPMail error。端口无服务时是 Connection refused (111);主机名解析失败是 php_network_getaddresses;认证失败是 Could not authenticate。修复后启动 Cron 补发 mailtosend 队列。
12.5 日志里没有 DEBUG
logLevel=2 时只写 ERROR 与 TRACE。需要细节时临时调到 3:
$ sudo sed -i "s/^\$logLevel='2';/\$logLevel='3';/" /var/lib/projeqtor/config/parameters.php
$ sudo systemctl reload php8.1-fpm
排障结束改回 2。若调到 3 仍无输出,检查日志目录是否存在且属主为 www-data。
12.6 页面时间与服务器时间差 8 小时
系统时区是 UTC,PHP 时区是 Asia/Shanghai 时会看到这种差异:
$ date "+%Y-%m-%d %H:%M:%S %Z"
2026-10-27 18:01:19 UTC
$ php -r "echo date('Y-m-d H:i:s'),' ',date_default_timezone_get(),PHP_EOL;"
2026-10-28 02:01:19 Asia/Shanghai
统一系统时区后重启 PHP-FPM 与 MariaDB;只改 paramDefaultTimezone 不够,因为日志时间取的是 PHP 的 date.timezone。
12.7 参数改了但页面仍是旧值
数据库参数缓存在 session 的 globalParametersArray 中。退出重新登录,或在「参数」界面重新保存一次,都会重建缓存。多人协作时通知相关人员刷新,不要反复改库。
12.8 参数表被误改
先用备份恢复整表,再逐条核对关键项:
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor < /var/backups/parameter.2026-10-28.sql
$ mariadb -u projeqtor -pq1w2e3r4 projeqtor -e "SELECT parameterCode,parameterValue FROM parameter WHERE parameterCode IN ('paramDbDisplayName','dbVersion','cronSleepTime');"
恢复后必须重新登录,并检查 Cron 的 DELAYS 是否与配置一致。
十三、参考链接
- ProjeQtOr 官方站点:https://www.projeqtor.org/en/
- ProjeQtOr 官方新闻(版本发布记录):https://www.projeqtor.com/en/news/
- ProjeQtOr SourceForge 文件页:https://sourceforge.net/projects/projectorria/files/
- PHP 8.1 配置手册:https://www.php.net/manual/zh/configuration.file.php
- MariaDB 10.6 系统变量:https://mariadb.com/kb/en/server-system-variables/
- Nginx
client_max_body_size:https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size - PHPMailer 排障说明:https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting
以上链接与版本信息查询日期均为 2026-10-28。