T Toutadmin文档

安装 Node 版

Node 版作为常驻服务运行:一个进程、一个放在磁盘上的 SQLite 数据库,此外无需安装任何东西—— 没有外部数据库,没有缓存,没有队列。本教程从一个空目录一直讲到投入生产的实例,包括多数指南 略过的那些环节:系统服务、反向代理、TLS、备份与升级。

需要什么#

项目版本为什么
Node.js22 或更新产品依赖其内置测试运行器和较新的接口。
一个 C 编译器build-essential除非您的平台已有预编译二进制包,否则 better-sqlite3 会在安装时现场编译。
本地磁盘存放 SQLite 与上传的文件。切勿使用网络共享:见下文。

单核、512 MB 内存的机器足以支撑几十人。要紧的不是算力而是磁盘:它必须是本地的,并且有备份。

1. 获取并安装#

git clone <您的代码库> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev

npm ci 而不是 npm install:它严格按锁定文件安装,且从不改写它。 在服务器上,一个悄悄漂移的版本就是一次谁也说不清的故障。

如果编译失败

better-sqlite3 是唯一需要编译 C 代码的依赖。 sudo apt install -y build-essential python3 能解决绝大多数情况。

2. 配置#

cp .env.example .env

一切都通过环境变量设置——从 .env 读取,或由您的服务管理器注入。没有一项是必填的: 没有 .env 时,实例在 3000 端口启动,并把您引向安装向导。

变量默认值作用
PORT3000监听端口。
NODE_ENV生产环境设为 production:严格 Cookie,不输出详细堆栈。
SESSION_SECRET自动生成为会话加封。留空则在 data/session.key 中生成。
INSTALL_TOKEN安装向导在安装前索要。暴露在公网的服务器建议设置。
TRUST_PROXY位于受信代理之后时设为 1,且仅限于此。
DB_PATHdata/app.sqlite数据库。
UPLOAD_DIRCV_DIRVAULT_DIRSIGN_DIRDOCS_DIRBACKUP_DIR位于 data/各文件目录:头像、简历、保险柜、签署夹、收到的单据、归档。
LOGIN_RATE_LIMIT10每个地址每一刻钟的登录尝试次数。
GLOBAL_RATE_LIMIT300每个地址每分钟的请求数。
API_RATE_LIMIT每个令牌每分钟的 API 调用数。
SESSION_IDLE_MINUTES60超过这段闲置时间,会话即失效。
SESSION_MAX_HOURS12绝对时长,任何操作都不会延长它。
ADMIN_EMAILADMIN_PASSWORD无界面安装:启动时直接创建管理员。
会话密钥

改动 SESSION_SECRET 会把所有人一次性踢下线。请只设置一次,随机生成 (node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"), 并与其他资料一并备份。切勿把它留在 git 代码库里。

设置安装令牌#

从首次启动到您进入安装向导之间,这个实例属于先发现它的人:谁先到,谁就创建了 管理员账户。因此请在开放端口之前先设好 INSTALL_TOKEN

node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# 然后写入 .env:
INSTALL_TOKEN=c3f1…

向导以恒定时间比对它——普通比对会让令牌被逐字符猜出来——并把每一次拒绝都记入 日志。实例安装完成后,向导会自行关闭。

无界面安装#

用于自动化部署时,ADMIN_EMAILADMIN_PASSWORD 会在启动时直接创建 管理员,跳过向导。之后请把它们移除:留在服务环境里的密码,凡能读到该服务的人都能读到。

3. 权限与数据存放位置#

sudo useradd --system --home /var/www/toutadmin --shell /usr/sbin/nologin toutadmin
sudo chown -R toutadmin:toutadmin /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/data
sudo chmod 640 /var/www/toutadmin/.env

代码可以保持只读;唯一需要可写的是 data/。SQLite 会在数据库旁边写入伴随文件 (-wal-shm):要紧的是目录,而不只是那个数据库文件。

切勿放在网络共享上

NFS 与 SMB 在文件锁上会撒谎。SQLite 正是靠这个锁来阻止两处同时写入:放在共享盘上,数据库 终将无声无息地损坏。永远用本地磁盘——该送到别处去的是备份,不是数据库本身。

4. 服务#

手工启动的话,一关终端产品就停了。把它交给 systemd:

# /etc/systemd/system/toutadmin.service
[Unit]
Description=Toutadmin
After=network.target

[Service]
Type=simple
User=toutadmin
WorkingDirectory=/var/www/toutadmin
EnvironmentFile=/var/www/toutadmin/.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=5

# 该服务只需要写入 data/。
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/toutadmin/data

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toutadmin
sudo systemctl status toutadmin
journalctl -u toutadmin -f

那五行加固配置不是装饰:ProtectSystem=strict 让整个文件系统不可写, ReadWritePaths 再单独放开唯一必须可写的目录。这样一来,任意写入漏洞也只能触及 data/

5. 反向代理#

切勿把 3000 端口直接暴露在公网:它不做 TLS,也没有理由去学。

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

    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 / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

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

同时让服务只监听回环地址,这样 3000 端口就只有代理够得着。

TLS#

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

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

TRUST_PROXY 与代理配套,且仅限如此

这个变量让服务器采信响应头中声明的来源地址。在 nginx 之后,这正是所需的——否则所有请求都 像是来自 127.0.0.1,按地址计的限流也就形同虚设。而前面没有代理时情况恰好相反: 任何人都能随意声明地址,绕开限流。

6. 安装向导#

打开您的域名。只要还没有任何账户,所有地址都会跳转到 /installation。共五步:

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

一旦有了账户,/installation 就会转向登录页:向导已自行关闭,无需手动删除任何文件。

7. 定期巡检#

与 PHP 版不同,这里无需安排任何定时任务:服务自带调度器,每小时醒来一次,做八件事。

巡检做什么保证
关闭合同已到期的账户登录时和打开仪表板时也会校验
为到期的订阅开具发票唯一索引确保不会重复开票
把到期事项转成通知去重键:同一到期事项只提醒一次
清理已读通知和审计日志按所设定的保留期限
收取财务邮箱(IMAP)若已配置收取
清空 Webhook 队列,含各次重试五次尝试,之后放弃
生成自动备份并送往异地当设定的间隔已到

每一项操作都是幂等的:重复执行的巡检不会重复开票,也不会重复通知。正因如此, 您可以随时重启服务而无需多想。

8. 检查一切正常#

# 测试(不会写入您的数据库)
npm test

# 一个演示实例,可以进去走走
node scripts/seed-demo.js

然后在界面里:创建一名成员,用该账号登录,上传一个附件,手动执行一次备份,并在备份 界面校验它。这四个动作分别触及数据库、文件、权限和归档。

9. 升级#

# 1. 先备份,永远如此——从“备份”界面

# 2. 更新代码
cd /var/www/toutadmin
git pull
npm ci --omit=dev

# 3. 重启;数据库会在启动时自行迁移
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager

数据库结构通过幂等迁移演进:重复执行升级不会造成任何破坏。data/.env 从不被触碰。

回退#

把代码退回上一版本并重启即可。迁移从不删除字段:除非 更新日志另有明确说明,迁移后的数据库仍可被上一版本读取。若有疑虑, 就还原第 1 步所做的那份归档。

10. 真正做好备份#

tar.gz 归档中包含数据库和五个文件目录。数据库通过 SQLite 的在线备份复制, 即便在写入过程中也能得到一致的副本——PHP 版通过 VACUUM INTO 达到同样效果。 每个文件都带有各自的 SHA-256 校验值,还原时会逐一核验。

留在它所保护的那台服务器上的归档,什么也保护不了:请在备份界面配置异地目标 (FTPS 或 Google Drive),并订阅 sauvegarde.echec 的 Webhook,以便在异地投送失败 时收到通知。详见备份与还原页面。

先在本地试一试#

npm install
cp .env.example .env
npm run dev        # 每次改动后自动重启

打开 http://localhost:3000。本地请把 NODE_ENV 留空:在 production 下,Cookie 被标记为“安全”,在明文连接上不会被保留——您会在登录页上 绕圈子。

转到 PHP 版,或从 PHP 版迁来#

两个版本共用同一套数据库结构——142 张表、1311 个字段——以及同一种 密码格式。停掉一边,复制 app.sqlite 和各文件目录,启动另一边:无需任何转换。 参见两个版本

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