安装 Node 版
Node 版作为常驻服务运行:一个进程、一个放在磁盘上的 SQLite 数据库,此外无需安装任何东西—— 没有外部数据库,没有缓存,没有队列。本教程从一个空目录一直讲到投入生产的实例,包括多数指南 略过的那些环节:系统服务、反向代理、TLS、备份与升级。
需要什么#
| 项目 | 版本 | 为什么 |
|---|---|---|
| Node.js | 22 或更新 | 产品依赖其内置测试运行器和较新的接口。 |
| 一个 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 端口启动,并把您引向安装向导。
| 变量 | 默认值 | 作用 |
|---|---|---|
PORT | 3000 | 监听端口。 |
NODE_ENV | — | 生产环境设为 production:严格 Cookie,不输出详细堆栈。 |
SESSION_SECRET | 自动生成 | 为会话加封。留空则在 data/session.key 中生成。 |
INSTALL_TOKEN | — | 安装向导在安装前索要。暴露在公网的服务器建议设置。 |
TRUST_PROXY | — | 位于受信代理之后时设为 1,且仅限于此。 |
DB_PATH | data/app.sqlite | 数据库。 |
UPLOAD_DIR、CV_DIR、VAULT_DIR、SIGN_DIR、DOCS_DIR、BACKUP_DIR | 位于 data/ 下 | 各文件目录:头像、简历、保险柜、签署夹、收到的单据、归档。 |
LOGIN_RATE_LIMIT | 10 | 每个地址每一刻钟的登录尝试次数。 |
GLOBAL_RATE_LIMIT | 300 | 每个地址每分钟的请求数。 |
API_RATE_LIMIT | — | 每个令牌每分钟的 API 调用数。 |
SESSION_IDLE_MINUTES | 60 | 超过这段闲置时间,会话即失效。 |
SESSION_MAX_HOURS | 12 | 绝对时长,任何操作都不会延长它。 |
ADMIN_EMAIL、ADMIN_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_EMAIL 与 ADMIN_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。共五步:
- 实例的语言,从 16 种中选择。
- 前置条件,逐项检查并显示。
- 公司:将出现在各处的名称。
- 每位新入职(非自由职业)员工获得的年假天数。
- 管理员账户:邮箱地址,以及至少十二位的密码。
一旦有了账户,/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。 独立站点,与软件本身分离。