ProjeQtOr 13.1.1 配置参数详解

逐项拆解 ProjeQtOr 13.1.1 的 parameters.php 与 parameter 表:数据库连接、目录、日志级别、邮件 SMTP、密码与认证、LDAP/SAML、计划财务、Cron 周期,含实测回显、生产模板与排障清单。

ProjeQtOrProjeQtOrPHPMariaDBPHP 应用2026/10/28

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
组件版本与配置相关的职责
Ubuntu22.04.5 LTS提供 systemd、apt 与目录权限模型
Nginx1.18.0-6ubuntu14.22读取 client_max_body_size,限制上传体积
PHP-FPM8.1.2-1ubuntu2.28读取 php.ini,执行 parameters.php
MariaDB10.6.23-0ubuntu0.22.04.1存储 parameter 表与全部业务数据
ProjeQtOr13.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 改参数前必须确认的三件事

  1. 改的是哪一层:数据库连接、日志路径、UTF-8 在 parameters.php;业务参数在 parameter 表。
  2. 谁来加载:PHP-FPM 在每次请求时重新执行 parameters.php,改完必须 systemctl reload php8.1-fpm;数据库参数由应用在会话中缓存。
  3. 能否回滚:改前备份 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) 按以下顺序取值:

  1. 先看同名 PHP 全局变量(即 parameters.php 里的 $logFile、$logLevel、$enforceUTF8 等)。存在就立即返回,不再查库。
  2. 对 paramDbType、paramDbHost、paramDbPort、paramDbPrefix、paramDbName、paramDbUser、paramDbPassword 这七个键,未定义时直接返回空串,不查库。
  3. 其余键第一次访问时,一次性读取 parameter 表中 idUser is null and idProject is null 的全部行,写入 session 的 globalParametersArray,之后从 session 取。
  4. 某些键有代码级兜底,例如 paramReportTempDirectory 未配置时返回 ../files/report/。

由此得到三条实践结论:

  • parameters.php 的优先级最高,同名的数据库参数不会覆盖它。
  • 数据库参数缓存在 session 中,界面显示旧值时可退出重新登录强制重建缓存。
  • 直接 SQL 改库后不要让应用长期持有旧会话,管理员改参应走「参数」界面,改完刷新页面确认。

2.3 用户级参数与全局参数

parameter 表用 idUser、idProject 两列区分作用域:

作用域条件实测行数
全局idUser IS NULL AND idProject IS NULL200
用户级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 字段逐项说明

变量实测值含义修改注意
$paramDbTypemysql数据库驱动,MariaDB 也走 mysql可选 mysql、pgsql,装完不要改
$paramDbHost127.0.0.1数据库地址远程库填内网 IP,并放行 3306
$paramDbPort3306数据库端口与实例实际端口一致
$paramDbUserprojeqtor应用账号只授予本库权限
$paramDbPasswordq1w2e3r4明文密码文件权限应 640
$paramDbNameprojeqtor库名改名需同步改授权
$paramDbPrefix空表前缀与库内实际表名一致,为空表示无前缀
$SslKey / $SslCert / $SslCa空数据库 TLS 证书启用加密时填绝对路径
$logFile.../projeqtor_${date}.log日志模板${date} 由程序替换为 Ymd,不要写死日期
$logLevel2日志级别2=TRACE,3=DEBUG,详见第六章
$enforceUTF81强制 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文档管理模块的文件目录绝对路径
paramReportTempDirectoryPDF/报表生成的临时文件绝对路径,未配置时代码返回 ../files/report/
cronDirectoryCron 的 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) 的级别定义:

级别函数日志标记典型用途
1errorLog()***** ERROR *****数据库连接失败、邮件发送失败
2traceLog()===== TRACE =====登录、Cron 启停等关键流程
3debugLog() / debugTraceLog()----- DEBUG -----登录校验细节、SQL 上下文
4scriptLog()..... 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 服务允许的域一致,否则被拒
paramMailSmtpServerSMTP 主机为空时测试页直接报必填错误
paramMailSmtpPortSMTP 端口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&nbsp;:&nbsp;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&nbsp;:&nbsp;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>

三个关键信息:

  1. Mail stored in Database : #6 说明邮件并未丢弃,而是写入了 mailtosend 表,恢复 SMTP 后可由 Cron 重发。
  2. Connection refused (111) 是最常见的失败原因:主机可达但端口没有服务,先查端口再查防火墙。
  3. 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 邮件排障顺序

  1. 参数层:服务器、端口、账号、密码、发件人是否齐全且正确。
  2. 网络层:ss -ltn 看本机端口,nc -zv <smtp主机> <端口> 看远端连通。
  3. 认证层:用户名密码错误通常表现为 SMTP Error: Could not authenticate。
  4. 内容层:收件人地址非法时 PHPMailer 报 Invalid address。
  5. 队列层:确认 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
参数实测含义
paramPasswordMinLength5最小长度,生产建议 8 以上
paramPasswordStrength1复杂度:1 仅长度,2 加大写与小写,3 再加数字,4 再加特殊字符
paramDefaultPasswordq1w2e3r4新建用户的初始密码,首次登录强制修改
requiredOldPasswordYES改密时必须输入旧密码
oldPasswordDifferentNewYES新密码不能与旧密码相同
lockPasswordNULL锁定密码策略,非空时禁止改密
passwordResetEnabledNO关闭「忘记密码」自助重置
otpForAdminUsers / otpForNewUsersNO管理员/新用户强制双因素
otpForDirectConnectionsWithSsoNOSSO 直连时是否要求 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
参数实测单位作用
cronSleepTime10秒每次循环休眠,所有周期都以它为步长递减
cronCheckDates30秒日期告警、里程碑提醒的检查间隔
cronCheckImport60秒扫描导入目录的间隔
cronCheckEmails-1秒收邮件转备注;负值表示关闭,未配置时代码默认 300
cronCheckNotifications3600秒通知系统检查间隔,仅 notificationSystemActiv=YES 时生效
cronArchiveTime365天已关闭事项归档的时间阈值
cronArchivePlannedDate18:30时刻每天执行归档的时间
mailGroupPeriod60分钟邮件合并发送窗口,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 推荐值

参数建议值理由
$logLevel2保留 TRACE,避免密码令牌入日志
$logFile/var/log/projeqtor/projeqtor_${date}.log与代码、数据分离,便于 logrotate
$enforceUTF81中文不乱码
paramDefaultTimezoneAsia/Shanghai与业务时区一致
paramPasswordMinLength8满足常见合规要求
paramPasswordStrength3长度 + 大小写 + 数字
passwordResetEnabledNO邮件通道未验证前关闭
otpForAdminUsersYES管理员强制双因素
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 漂移
cronSleepTime1010 秒粒度足够
cronCheckEmails300未接入 IMAP 时保持 -1
limitDisplayPlanning500大项目降低渲染压力
paramMailSmtpPort587多数企业邮箱要求提交端口 + TLS
paramMailSmtpUsername实际账号必须与 SMTP 认证一致
paramMailEolCRLF与主流 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 是否与配置一致。

十三、参考链接

以上链接与版本信息查询日期均为 2026-10-28。