Framadate 1.1.19 排错 FAQ

Framadate 1.1.19 实测排错问答:composer framagit 超时与代理、use_smtp 硬编码致建票 500 与孤儿票、PHP 8.1 strftime CSV 500、ICS 参数 base64url 编码、投票 control 哈希与 choices[] 字段、save 分支语义、停库自动降级、迁移幂等等 13 个真实故障,每条附报错原文与修复回显。

FramadateFramadate自托管PHPMariaDB2026/11/01

Framadate 1.1.19 排错 FAQ

目标:把 Framadate 1.1.19(Ubuntu 22.04.5 + Apache 2.4.52 + PHP 8.1.2 + MariaDB 10.6.23,监听 8145)部署与试用中真实踩到的 13 个坑整理成「现象 → 定位 → 处置」问答,每条报错原文与修复回显均取自实测环境。部署步骤见 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》,日常运维见 《Framadate 1.1.19 运维与升级手册》。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS;Web:Apache 2.4.52(mod_php,Listen 8145)
  • PHP:8.1.2-1ubuntu2.28;数据库:MariaDB 10.6.23(库 framadate,账号 framadate,密码 q1w2e3r4)
  • 应用:Framadate 1.1.19,目录 /var/www/framadate,表前缀 fd_
  • 现场数据:日期投票 dvdEakcXnbWf7eXf(管理链接后缀 24 位 admin_id dvdEakcXnbWf7eXfgPd6E5R4),4 条投票记录(Alice/Bob/Charlie/Dave),control 哈希 d45ffae7e20458595a509d3781436602

二、排错工具箱

三条日志定位一切,先记住位置再查 FAQ:

ls -l /var/log/apache2/framadate_error.log /var/log/apache2/framadate_access.log /var/www/framadate/admin/stdout.log
-rw-r--r-- 1 root     root      8537 Nov  1 14:53 /var/log/apache2/framadate_access.log
-rw-r--r-- 1 root     root     70146 Nov  1 14:53 /var/log/apache2/framadate_error.log
-rw-r--r-- 1 www-data www-data   254 Nov  1 14:21 /var/www/framadate/admin/stdout.log

(access/error 随流量增长,stdout.log 为应用事件日志,实测全文仅两条 [CREATE_POLL] 行。)

  • HTTP 500 白屏:一律先 tail -f /var/log/apache2/framadate_error.log 复现一次,PHP Fatal 原文会带文件与行号;
  • 数据不对:直接查 fd_poll/fd_slot/fd_vote/fd_comment 四表 + fd_framadate_migration;
  • 建票留痕:admin/stdout.log 的 [CREATE_POLL] id:tXeTu4G6InJoAHGd 这类行可反查孤儿票。

三、症状速查表

症状根因跳转
composer install 卡在 git clone 超时framagit.org 直连不通4.1
建票「创建」按钮 500模板硬编码 use_smtp=true 且无 SMTP4.2
CSV 导出 500PHP 8.1 strftime 类型收紧4.3
ICS 导出 500/404参数须 base64url("d-m-Y|H:i")4.4
投票提示 Le nom n'est pas valide.control 哈希缺失/错误4.5
投票 POST 500 filterArray null缺 choices[] 数组字段4.6
投票变成「编辑旧票」save 字段语义误用4.7
全站 302 到 maintenance.phpMariaDB 停机(自动降级)4.8
install.php 总被弹回首页已配置,防重复安装4.9
migration 页全是 Passé迁移判重幂等,正常4.10
投票短链 404 查询串正常rewrite 三件套缺一4.11
500 且含 Permission denied tpl_cSmarty 缓存目录不可写4.12
GitHub API 查 releases 返回 404GitHub 同步副本 API 异常4.13

四、常见问题

4.1 composer install 卡在 git clone 超时

现象(实测原文):

Failed to execute git clone --mirror 'https://framagit.org/framasoft/framadate/o80-i18n'
... Failed to connect to framagit.org port 443 after 137538 ms

定位:14 个生产依赖里 13 个走 packagist(直连可用),唯独 o80/i18n 钉在 framagit.org 的 dev-develop 分支、必须现场 git clone,而该站直连实测超时。

处置:走代理重试(实测 192.168.1.72:7897 可用、192.168.1.62:7897 不可用):

export http_proxy=http://192.168.1.72:7897 https_proxy=http://192.168.1.72:7897
composer install --no-dev

成功回显(composer install --no-dev 实测尾 4 行;首次安装时为 14 包逐一 Downloading):

Package webmozart/path-util is abandoned, you should avoid using it. Use symfony/filesystem instead.
Generating autoload files
7 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

4.2 建票第二步点「创建」HTTP 500(SMTP connect() failed)

现象:三步向导最后提交直接 500,错误日志实测原文:

PHP Fatal error:  Uncaught PHPMailer\PHPMailer\Exception: SMTP connect() failed.
https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting
in /var/www/framadate/vendor/phpmailer/phpmailer/src/PHPMailer.php:1982
#2 /var/www/framadate/app/classes/Framadate/Services/MailService.php(67)
#3 /var/www/framadate/create_date_poll.php(213): Framadate\Services\MailService->send()

定位:安装向导生成的 config.php 把 'use_smtp' => true 硬编码写入(与 sample 的 false 不一致,属模板缺陷),create_date_poll.php 第 213 行建票后立即调 MailService->send(),本机无 SMTP 即抛异常。

处置(两步,缺一不可):

sed -i "s/'use_smtp' => true/'use_smtp' => false/" /var/www/framadate/app/inc/config.php

更隐蔽的坑:500 发生在投票已写库之后——票建好了但管理员拿不到 24 位管理链接,成为孤儿票。从 stdout.log 反查孤儿 ID 清理(实测该文件留下两条 CREATE_POLL,第一条即孤儿):

mysql -uframadate -pq1w2e3r4 framadate -e \
  "DELETE FROM fd_slot WHERE poll_id='tXeTu4G6InJoAHGd';
   DELETE FROM fd_poll WHERE id='tXeTu4G6InJoAHGd';"

重提交即 302 到管理链接页,实测成功票 dvdEakcXnbWf7eXf 入库。

4.3 CSV 导出 500:strftime TypeError(PHP 8.1 兼容)

现象(错误日志实测原文):

PHP Fatal error:  Uncaught TypeError: strftime(): Argument #2 ($timestamp) must be
of type ?int, string given in /var/www/framadate/exportcsv.php:80

定位:PHP 8.1 起 strftime() 第二参强制 ?int,而 fd_slot.title(时间戳以字符串存库,实测值 1794092400)取出即 string。项目 2021-12 停更,官方不会修,只能本地打补丁:

cp /var/www/framadate/exportcsv.php /var/www/framadate/exportcsv.php.bak
sed -i 's/strftime(\$format, \$slot->title)/strftime($format, (int) $slot->title)/' \
  /var/www/framadate/exportcsv.php
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" \
  "http://localhost:8145/exportcsv.php?poll=dvdEakcXnbWf7eXf"
200 text/csv; charset=utf-8

补丁后实测导出正文:"Alice","Oui","Non"、"Bob","Si nécessaire","Oui"。附带说明:表头出现若干 01/01/1970 列不是故障——建票日期网格勾选 1 天会生成 7 天槽位行,未选日子的 fd_slot.title 为空串,(int) 后成 0 即纪元零点;导出有效数据不受影响,介意的话删票重建时只勾选需要的日期。

4.4 ICS 导出 500/404:参数不是时间戳,是 base64url("d-m-Y|H:i")

现象:凭直觉把时间戳拼进 get_ical_file= 参数得到 500(页面回显 Internal error);或 URL 漏掉投票 ID 前缀、直接请求 /action/ics/参数 得到 404(实测 ics 404 text/html; charset=iso-8859-1)。

定位:studs.php 第 224~226 行实测源码——参数先 Utils::base64url_decode(),再按 | 拆成「日|时:分」两段:

if (isset($_GET['get_ical_file'])) {
    $dayAndTime = (string)filter_input(INPUT_GET, 'get_ical_file');
    $dayAndTime = Utils::base64url_decode($dayAndTime);
    $elements = explode("|", $dayAndTime);

且日期段必须匹配 /^\d{2}-\d{2}-\d{4}$/(d-m-Y 格式,日在前)。

处置:按格式造参数再请求(实测):

ICS=$(printf "08-11-2026|14:00" | base64 -w0 | tr "+/" "-_" | tr -d "=")
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" \
  "http://localhost:8145/dvdEakcXnbWf7eXf/action/get_ical_file/$ICS"
curl -s "http://localhost:8145/dvdEakcXnbWf7eXf/action/get_ical_file/$ICS" | head -6
200 text/calendar;charset=UTF-8
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Framasoft//Framadate//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTART;TZID=Europe/Paris:20261108T140000

注意 URL 里的动作名是 get_ical_file(.htaccess 的 ^([a-zA-Z0-9-]+)/action/([a-zA-Z_-]+)/(.+)$ 规则把它重写为 studs.php?poll=..&get_ical_file=..),查询串形态 studs.php?poll=dvdEakcXnbWf7eXf&get_ical_file=$ICS 同样可用。

4.5 投票提交提示 Le nom n'est pas valide.(control 哈希校验失败)

现象(实测复现:故意带错误 control POST):

curl -s -b jar "http://localhost:8145/studs.php?poll=dvdEakcXnbWf7eXf" \
  -d "poll=dvdEakcXnbWf7eXf&name=Charlie&control=WRONGHASH1234567890abcdef1234" \
  | grep -oE "alert-danger[^<]*<[^<]*"
alert-danger hidden-print" role="alert">Le nom n'est pas valide.<button type="button" class="close" data-dismiss="alert" aria-label="Fermer">

定位:报错文案指向「名字无效」,实际是 studs.php 第 162 行 filterMD5($_POST['control']) 校验失败后的统一提示——control 是服务端按票数据算出的 32 位 md5(本票实测 d45ffae7e20458595a509d3781436602),嵌在投票页隐藏域里防伪造。脚本化投票漏带或带错该哈希,就会被这句误导性文案挡回来。

处置:同一会话先 GET 投票页取隐藏域,再原样回传(实测表单提取命令):

curl -s -c jar "http://localhost:8145/studs.php?poll=dvdEakcXnbWf7eXf" -o v.html
CONTROL=$(grep -oE 'name="control" value="[a-f0-9]{32}"' v.html | grep -oE '[a-f0-9]{32}')

4.6 投票 POST 500:filterArray() Argument #1 must be of type array, null given

现象(错误日志实测原文):

PHP Fatal error:  Uncaught TypeError: Framadate\Services\InputService::filterArray():
Argument #1 ($arr) must be of type array, null given, called in
/var/www/framadate/studs.php on line 161

定位:studs.php 第 161 行直接消费 $_POST['choices'] 数组——每个时间槽一个 choices[] 元素,取值 0(拒绝)/1(待定)/2(同意)(CHOICE_REGEX 校验)。字段名拼错(如 check4=20)或整个不带,$_POST['choices'] 为 null 即 500。

处置(实测成功命令,Charlie、Dave 先后入库):

curl -s -b jar -o /dev/null -w "%{http_code}\n" \
  "http://localhost:8145/studs.php?poll=dvdEakcXnbWf7eXf" \
  -d "poll=dvdEakcXnbWf7eXf&name=Dave&save=&control=$CONTROL&choices[]=1&choices[]=2"
mysql -uframadate -pq1w2e3r4 framadate -e "SELECT name,choices FROM fd_vote;"
200
name      choices
Alice     20
Bob       12
Charlie   20
Dave      12

fd_vote.choices 是各位表态的拼接串:20 = 槽 1 同意 + 槽 2 拒绝,12 = 槽 1 待定 + 槽 2 同意。

4.7 投票提交后变成「编辑旧票」或毫无反应:save 字段语义

定位:studs.php 源码实测两个分支——第 123 行 if (!empty($_POST['save'])) 走「保存对旧票的编辑」(要求 save 非空且为旧票 uniqId 语境);第 159 行 elseif (isset($_POST['save'])) 才是「新增投票」,条件是 save 存在但为空值。

处置:脚本化新增投票必须显式带 save=(空值);完全省略该字段则两个分支都不命中,POST 看似 200 实际什么都没写库;给 save 填非空值则被当编辑旧票处理。4.6 节的成功命令即为标准形态。

4.8 全站突然 302 到 maintenance.php

现象(实测停库复现):

systemctl stop mariadb
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" http://localhost:8145/
curl -sL http://localhost:8145/ | grep -oE "SQLSTATE[^<]*" | head -1
302 -> http://localhost:8145/maintenance.php
SQLSTATE[HY000] [2002] No such file or directory

定位:这不是维护开关,是应用连不上数据库时的自动降级。[2002] No such file or directory 说明 PDO 走 socket 而 mariadb 服务没跑;若配置 host=localhost 想走 TCP,可改 DSN 为 127.0.0.1 排查。

处置:systemctl start mariadb 后实测首页恢复 200。主动维护(DDL、换存储)就用「停库即优雅降级」当维护模式用,见运维篇 6.4 节。

4.9 访问 install.php 总是被弹回首页

现象:curl -sI http://localhost:8145/admin/install.php 实测返回:

HTTP/1.1 302 Found
Location: http://localhost:8145/

定位:app/inc/config.php 已存在时安装器拒绝重入,属安全设计非故障。需要重配数据库直接改 config.php 文件即可,没有「重装」一说。

4.10 migration 页全是 Passé,是不是没执行

现象:升级/重跑 admin/migration.php 后页面 14 行全部显示 Passé。

定位:fd_framadate_migration 记录表判重(实测 13 条记录、migration重跑 200),Passé = 该迁移已在库里执行过、本次跳过——这正是幂等设计,反复重跑无副作用。只有库里缺的迁移才会真正执行并显示成功。

4.11 投票短链 404,查询串却正常

定位:URL_PROPRE、.htaccess、AllowOverride All、mod_rewrite 四者缺一,/dvdEakcXnbWf7eXf 就 404 而 studs.php?poll=dvdEakcXnbWf7eXf 正常。逐项核对:

ls -l /var/www/framadate/.htaccess
grep -n "AllowOverride" /etc/apache2/sites-available/framadate.conf
apache2ctl -M 2>/dev/null | grep rewrite
-rw-r--r-- 1 www-data www-data 702 Nov  1 14:18 /var/www/framadate/.htaccess
6:        AllowOverride All
 rewrite_module (shared)

改完 systemctl reload apache2,实测短链复归 200。

4.12 500 且日志含 Permission denied(tpl_c)

定位:Smarty 把模板编译到 tpl_c/,Web 身份(www-data)不可写即报错。实测健康权限为 www-data:www-data 755。

处置:

chown -R www-data:www-data /var/www/framadate/tpl_c

整目录属主建议一次性到位:chown -R www-data:www-data /var/www/framadate(安装篇 4.4 节同款命令)。

4.13 用 GitHub API 查 releases/tags/1.1.19 返回 404

现象:api.github.com/repos/framasoft/framadate/releases/tags/1.1.19 实测返回 404,误以为版本不存在。

定位:GitHub 上 framasoft/Framadate 只是同步副本(主战场在 framagit.org),其 Releases API 数据不全;但 tag 归档下载(archive/refs/tags/1.1.19.tar.gz)与 releases.atom 订阅源均正常。

处置:版本溯源改走 releases.atom(实测拿到 1.1.19=2021-12-23、1.1.18=2021-12-21、1.1.17=2021-10-18)与仓库页面 HTML;star/语言占比等指标用 shields.io 徽章接口取(实测 stars=103、PHP=45.8%)。

五、参考链接

Framadate 1.1.19 排错 FAQ - Quzuo