T Toutadmin文档

安装 PHP 版

PHP 版原样放到虚拟主机上即可:没有 Composer,没有包管理器,也没有需要常驻的服务。一个目录、 一个配置文件、一个定时任务。本教程从空目录一直讲到投入生产的实例,包括多数指南略过的那些 环节:权限、TLS、定时任务、备份与升级。

需要什么#

项目版本为什么
PHP8.1 或更新联合类型、enumreadonlynever:代码用到了它们。
pdo_sqlite整个数据库。没有它什么也跑不起来。
mbstring十六种语言下字符串的正确处理。
openssl建议加密数据库中的机密(SMTP 密码、异地存放的令牌)。
curl建议发出 Webhook,以及把备份送到第三方。
intl建议日期写法与 Node 版一致。缺少它时,月份名称改由词典提供。

安装向导会逐项检查并在屏幕上显示,然后才让您继续:阻断性的问题会中止安装, 可选项则只提示、不阻断。

零依赖

没有任何东西需要用 composer 安装:IMAP 读取、tar.gz 归档、PDF、 CSV、二维码和 HTTP 客户端都写在产品内部。正因如此,它才能被放到一个您连命令行都没有的 主机上。

1. 上传文件#

取得 PHP 版的压缩包,然后上传。目录结构如下:

toutadmin/
├── app/            代码(Web 服务器绝不对外提供)
│   ├── Core/       内核:路由、数据库、会话、安全
│   ├── Modules/    业务:人力资源、财务、项目、质量……
│   ├── Controllers/
│   ├── views/      模板
│   ├── locales/    十六份词典
│   └── schema.sql  数据库结构,与 Node 版完全一致
├── public/         ← 网站根目录指向这里,别无他处
│   ├── index.php   前端控制器
│   ├── css/ js/
├── data/           数据库、保险柜、签署夹、收到的单据、备份
├── tools/          cron.php、check-keys.php
├── tests/
├── config.sample.php
└── VERSION
网站根目录指向 public/,而不是整个目录

这是本页最要紧的一点。如果服务器把整个目录都对外提供,data/app.sqlite 就变得人人可下载:整个数据库,包括密码摘要和保险柜。data/ 位于 public/ 之上,正是有意为之。

在虚拟主机上#

会遇到两种情况。如果您的服务商允许选择域名的根目录(cPanel、Plesk,以及多数支持“根目录” 模式的主机),请把整个目录放在 www/ 之外,并让域名指向 toutadmin/public

如果根目录是固定的——通常是 www/public_html/——请把 public/ 里的内容放进去,其余部分放在它的上一层,然后修改 public/index.php 开头的路径:

www/                  ← 固定的根目录
├── index.php
├── css/  js/
toutadmin/            ← 其余部分,Web 够不着的地方
├── app/
├── data/
└── config.php
// www/index.php —— 需要调整的那一行
require_once __DIR__ . '/../toutadmin/app/bootstrap.php';

万不得已,如果根目录之上什么也放不了,data/ 中随附的 .htaccess 已经会拒绝访问。那是一条安全带,不是解决方案:始终优先选择根目录之外的存放方式。

2. 写好配置#

cp config.sample.php config.php

然后打开 config.php

<?php
return [
    // SQLite 数据库与数据目录:位于网站根目录之外。
    'db_path'  => __DIR__ . '/data/app.sqlite',
    'data_dir' => __DIR__ . '/data',

    // 本实例专属的密钥。生成一次即可:
    //   php -r "echo bin2hex(random_bytes(32));"
    'session_secret' => '…',

    // 安装令牌:见下一步。
    'install_token' => '',

    // 对外地址,用于邮件中发出的链接。
    'base_url' => 'https://intranet.shili.com',

    // 只有位于受信代理之后才开启。
    'trust_proxy' => false,

    // 上限:每个地址每一刻钟的登录尝试次数,
    // 以及每个地址每分钟的请求数。
    'login_rate_limit'  => 10,
    'global_rate_limit' => 300,

    // 过期:闲置时长,以及任何操作都不会延长的绝对时长。
    'session_idle_minutes' => 60,
    'session_max_hours'    => 12,
];
实例密钥

它为签署夹中的签名加封,并加密数据库中的机密。丢失它会让既有签名失效。若您保留示例中的 值,产品会自行在 data/.instance-key 中生成一把(权限 0600)—— 那就别忘了把这个文件也一并纳入备份。

配置也可以放在别处:环境变量 TOUTADMIN_CONFIG 可指向另一个文件。

3. 设置安装令牌#

从上传文件到您进入安装向导之间,这个实例属于先发现它的人:谁先到,谁就 创建了管理员账户。这个窗口很短,但足够了。所以请在上线前先设一个随机值:

php -r "echo bin2hex(random_bytes(16));"
# 然后写入 config.php:
'install_token' => 'c3f1…',

向导会索要它,以恒定时间比对——普通比对会让令牌被逐字符猜出来——并把每次 拒绝都记入日志。没有令牌,什么也不会被创建。实例安装完成后,向导自行关闭:令牌可以留着, 它已不起作用。

4. 权限#

路径权限谁写入
data/0770Web 服务器,且仅限它
data/app.sqlite0660由安装过程创建
config.php0640无人——对服务器只读
app/public/0755无人——只读
# 以 PHP 以 www-data 身份运行的服务器为例
sudo chown -R nin:www-data /var/www/toutadmin
sudo find /var/www/toutadmin -type d -exec chmod 750 {} \;
sudo find /var/www/toutadmin -type f -exec chmod 640 {} \;
sudo chmod 770 /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/public

SQLite 还会在数据库旁边写入伴随文件(-wal-shm):需要可写的是 目录,而不只是那个数据库文件。

5. Web 服务器#

Nginx#

server {
    listen 443 ssl http2;
    server_name intranet.shili.com;

    root /var/www/toutadmin/public;   # 别无他处
    index index.php;

    ssl_certificate     /etc/letsencrypt/live/intranet.shili.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/intranet.shili.com/privkey.pem;

    client_max_body_size 20M;          # 保险柜上传、收到的单据

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # 上传的文件从不直接对外提供:它们要经过一条会校验
    # 会话和文件摘要的路由。
    location ~ ^/(data|app|tools|tests)/ { deny all; }
}

server {
    listen 80;
    server_name intranet.shili.com;
    return 301 https://$host$request_uri;
}

Apache#

<VirtualHost *:443>
    ServerName intranet.shili.com
    DocumentRoot /var/www/toutadmin/public

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/intranet.shili.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/intranet.shili.com/privkey.pem

    <Directory /var/www/toutadmin/public>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

随产品提供的 public/.htaccess 已经把所有地址都转向 index.php。 在 Apache 虚拟主机上,这就够了。

TLS#

只要连接是加密的,产品就会加上 Strict-Transport-Security 响应头,明文连接下 则绝不添加:从未加密页面上声明它,浏览器根本不会采纳,反倒会把一次本地试用锁在 https 里长达半年。在您自己维护的服务器上:

sudo certbot --nginx -d intranet.shili.com

若前面有代理或负载均衡器负责终结 TLS,请设 'trust_proxy' => true——也仅限 这种情况:否则任何人都能在请求头里随意声明 IP 地址和协议。

6. 定时任务#

PHP 站点只在请求发生时才运行。Node 版在其每小时巡检中所做的八项操作,这里改由主机的定时 任务完成:

* * * * * /usr/bin/php /var/www/toutadmin/tools/cron.php >> /var/log/toutadmin-cron.log 2>&1
任务做什么频率
关闭合同已到期的账户每次执行
为到期的订阅开具发票每次执行,且绝不重复开票
把到期事项转成通知每次执行,并去重
清理已读通知和审计日志按所设定的保留期限
收取财务邮箱(IMAP)若已配置收取
清空 Webhook 队列,含各次重试每次执行
生成自动备份并送往异地当设定的间隔已到

调用脚本的频率高于所设的间隔,并不会让备份更频繁:时候未到就什么也不会触发。若主机把定时 任务限制为每十五分钟一次,请把 * * * * * 改为 */15 * * * *—— Webhook 只是会带上这点延迟发出而已。

在没有定时任务的主机上

某些入门级主机并不提供。从第三方服务发起的 HTTP 调用(可用性监测之类)并不管用: cron.php 拒绝在命令行以外的地方执行,正是为了不让一个公开地址触发备份。 这种情况下,请在备份界面手动执行备份。

7. 安装向导#

打开您的域名。只要还没有任何账户,所有地址都会跳转到 /installation。 整个向导就在一页之内:

  1. 前置条件,逐项检查并显示——阻断性的会明确标出。
  2. 安装令牌,如果已设置的话。
  3. 公司:将出现在各处的名称,以及每位新入职(非自由职业)员工获得的年假天数。
  4. 实例的默认语言,从 16 种中选择。
  5. 管理员账户:邮箱地址,以及至少十二位的密码。

所有内容都在同一个事务中写入。一旦有了账户,/installation 就会转向登录页:向导已自行关闭,无需手动删除任何文件。

8. 检查一切正常#

# 测试,在本机运行(不会写入您的数据库)
php tests/run.php

# 翻译:没有任何界面用到词典中缺失的键
php tools/check-keys.php

然后在界面里:创建一名成员,用该账号登录,上传一个附件,手动执行一次备份,并在备份 界面校验它。这四个动作分别触及数据库、文件、权限和归档——也就是主机必须具备的全部能力。

9. 升级#

# 1. 先备份,永远如此
php tools/cron.php                     # 或使用“备份”界面

# 2. 替换代码,而不是数据
#    app/ public/ tools/ tests/ VERSION  ← 替换
#    data/ config.php                    ← 原样保留

# 3. 数据库会在首次请求时自行迁移

数据库结构通过幂等迁移演进:重复执行升级不会造成任何破坏。Node 版实例与 PHP 版实例共用 同一套结构——142 张表、1311 个字段——以及同样的 密码:数据库在两者之间迁移无需任何转换。

10. 真正做好备份#

tar.gz 归档包含数据库(通过 VACUUM INTO 复制,因此即便在写入 过程中也保持一致)以及五个文件目录:头像、收到的简历、保险柜、签署夹、收到的单据。归档中 每个文件都带有各自的 SHA-256 校验值,还原时会逐一核验。

留在它所保护的那台服务器上的归档,什么也保护不了:请在备份界面配置异地目标 (FTPS 或 Google Drive)。详见备份与还原页面。

先在本地试一试#

cp config.sample.php config.php
php -S localhost:8000 -t public public/index.php

PHP 自带的服务器足以让您了解这套产品。它不适合生产环境:单进程、无 TLS、无负载限制。

Toutadmin 文档 —— 构建于 2026-09-13。 独立站点,与软件本身分离。