Next.js 全栈管理系统,包含 采购报销 与 进度管理 两大模块,共用飞书 OAuth、角色体系与通知基础设施。
- 技术文档:
docs/TECH.md - 消息发送矩阵:
docs/NOTIFICATIONS.md - 测试手册:
docs/TESTING.md - 工作规范:
docs/AGENTS.md
cp .env.example .env # 填写飞书凭证、AUTH_SECRET、POSTGRES_PASSWORD
npm install
# 本地开发只启动 PostgreSQL,应用在宿主机运行
docker compose up -d postgres
npm run db:deploy
npm run db:seed-acceptance-checklists
npm run dev本轮起不迁移旧 SQLite 数据;首次部署从空 PostgreSQL 库开始,通过 seed 初始化角色和规则。
访问 http://localhost:3000 ,使用飞书登录。
日志默认输出到 stdout/stderr,生产和测试为 JSON line,开发可设置 LOG_FORMAT=pretty。常用级别为 LOG_LEVEL=debug|info|warn|error|silent;敏感字段会自动脱敏,详细规范见 docs/TECH.md。
| 服务 | 运行位置 | 端口 |
|---|---|---|
| PostgreSQL | Docker postgres |
5432 |
| Next.js | Docker app 或宿主机 npm run dev |
3000 |
| cron | Docker cron 或宿主机 npm run cron |
无 HTTP 端口 |
宿主机本地开发也使用同一个 PostgreSQL:
docker compose up -d postgres
npm run db:deploy # schema 有更新时
npm run dev若本机 5432 已被占用,在 .env 设置 POSTGRES_PORT=5433,并同步修改 DATABASE_URL 中的端口。
适合内网服务器一键拉起 Web + 定时任务,数据与附件通过 Docker Volume 持久化。
cp .env.example .env编辑 .env,至少填写:
AUTH_SECRETFEISHU_APP_ID/FEISHU_APP_SECRETNEXT_PUBLIC_APP_URL— 后台任务默认生成的系统地址(如https://pnx.demonmaster.cn)APP_ALLOWED_ORIGINS— 允许访问和登录跳转的完整 origin 列表
双入口访问时不要设置 AUTH_URL / NEXTAUTH_URL。飞书后台「重定向 URL」需要同时添加域名和内网 IP 对应的回调:
https://pnx.demonmaster.cn/api/auth/callback/feishu
http://10.4.150.222:3000/api/auth/callback/feishu
DATABASE_URL 无需修改,docker-compose.yml 会为容器内 app/cron 自动设置 PostgreSQL 连接串。
docker compose up -d --build如果当前用户没有 Docker socket 权限,可在 .env 或当前 shell 设置 SUDO_PASSWORD,然后使用仓库提供的辅助脚本:
./scripts/docker-compose-sudo.sh up -d --buildSUDO_PASSWORD 只用于宿主机 sudo docker compose ...,不会传入 app/cron 容器。
- app:Next.js 应用,默认映射端口
3000(可通过.env设置APP_PORT=8080改宿主机端口) - postgres:PostgreSQL 16 数据库
- cron:定时任务(采购日报、进度提醒、周报),与 app 共用 PostgreSQL
首次启动会自动执行 npm run db:deploy 应用 PostgreSQL migration。
在 prisma/seed.ts 填入你的飞书 openId 后,临时开启 seed:
# docker-compose.yml → app → environment 追加一行(仅首次)
RUN_DB_SEED: "true"然后:
docker compose up -d appseed 成功后删除 RUN_DB_SEED 行并再次 docker compose up -d,避免重复写入。
也可先飞书登录一次,再在容器内手动 seed:
docker compose exec app npx tsx prisma/seed.tsdocker compose logs -f app # 查看应用日志
docker compose logs -f cron # 查看定时任务日志
docker compose down # 停止
docker compose up -d --build # 更新代码后重新构建| 内容 | Docker Volume |
|---|---|
| PostgreSQL 数据 | postgres-data → 容器内 /var/lib/postgresql/data |
| 上传附件 | app-uploads → 容器内 /app/storage/uploads/ |
# 备份数据库到当前目录(会提示输入 POSTGRES_PASSWORD)
docker compose exec postgres pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-management_system}" > backup-$(date +%F).sql
# 恢复到空库
docker compose exec -T postgres psql -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-management_system}" < backup.sql更完整的 Docker 说明见 docs/TECH.md。
| 变量 | 说明 |
|---|---|
DATABASE_URL |
PostgreSQL 连接串,如 postgresql://postgres:<密码>@localhost:5432/management_system |
SHADOW_DATABASE_URL |
Prisma migration diff 使用的 shadow 库,建议库名以 _shadow 结尾 |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB / POSTGRES_PORT |
Docker PostgreSQL 用,见 .env.example |
AUTH_SECRET |
Auth.js 密钥,可用 openssl rand -hex 32 生成 |
FEISHU_APP_ID |
飞书 OAuth / 通讯录主应用 App ID,也是消息机器人的兼容默认值 |
FEISHU_APP_SECRET |
飞书 OAuth / 通讯录主应用 App Secret |
FEISHU_NOTIFICATION_APP_ID |
可选,通知机器人 App ID;普通私信通知、状态结果、提醒、反馈使用它发送 |
FEISHU_NOTIFICATION_APP_SECRET |
可选,通知机器人 App Secret;未配置时回退 FEISHU_APP_ID / FEISHU_APP_SECRET |
FEISHU_APPROVAL_APP_ID |
可选,审批机器人 App ID;只发送审批、验收、确认等待处理消息 |
FEISHU_APPROVAL_APP_SECRET |
可选,审批机器人 App Secret;未配置时回退通知机器人 |
FEISHU_DIRECT_MESSAGE_ALLOWED_NAMES / FEISHU_DIRECT_MESSAGE_ALLOWED_OPEN_IDS / FEISHU_DIRECT_MESSAGE_ALLOWED_UNION_IDS |
可选,飞书私信收件人临时 allowlist;用于测试或演练防误发,未配置时不限制;同时配置多个身份维度时必须全部匹配。Playwright 启动的应用服务默认只允许 李棋轩 |
FEISHU_WEBHOOK_URL |
采购通知群 Webhook(与 FEISHU_PROCUREMENT_WEBHOOK_URL 二选一,后者优先) |
FEISHU_PROCUREMENT_WEBHOOK_URL |
采购专用群 Webhook |
FEISHU_WEBHOOK_SECRET |
可选,Webhook 签名校验密钥 |
FEISHU_PROCUREMENT_WEBHOOK_SECRET |
可选,采购 Webhook 签名(未配置时回退 FEISHU_WEBHOOK_SECRET) |
FEISHU_EVENT_ENCRYPT_KEY |
可选,事件订阅加密密钥(飞书后台「事件与回调」) |
FEISHU_VERIFICATION_TOKEN |
可选,事件订阅校验 Token |
FEISHU_WS_BOT_KIND |
可选,长连接使用的机器人,notification 或 approval,默认 notification |
ENABLE_FEISHU_WS |
可选,是否安装通知机器人长连接,默认 false |
ENABLE_FEISHU_APPROVAL_WS |
可选,是否安装审批机器人长连接,默认 true |
PROGRESS_DAILY_SUMMARY_CHECK_CRON |
可选,每日进度摘要 DB 设置检查频率,默认每 5 分钟;每天 1–8 个实际发送时间在管理员面板 /admin/reminders 的“每日卡片”中配置,相邻时间至少间隔 5 分钟 |
PROGRESS_DAILY_SUMMARY_CRON |
兼容旧配置,仅在数据库中尚无每日卡片设置时用简单每日 cron 推导首个发送时间;已有设置以后以管理员面板为准 |
NEXT_PUBLIC_APP_URL |
后台任务默认系统地址(cron 飞书卡片按钮跳转用) |
APP_ALLOWED_ORIGINS |
允许登录跳转和飞书按钮生成的完整 origin 列表 |
LAN_HOST |
dev server 局域网访问 IP |
ALLOWED_DEV_ORIGINS |
Next dev 允许访问资源的额外 host 列表 |
-
在飞书开放平台创建企业自建应用
-
开启能力:网页应用(OAuth)+ 机器人(群消息)
-
安全设置 → 重定向 URL 添加(须与下方完全一致,多一个斜杠也会 20029):
https://pnx.demonmaster.cn/api/auth/callback/feishu http://10.4.150.222:3000/api/auth/callback/feishu http://localhost:3000/api/auth/callback/feishu也可在登录页
/login底部查看当前系统使用的地址。 -
权限管理:开通
contact:user.base:readonly(获取用户基本信息,用于登录) -
配置两个消息机器人:
- 通知机器人:发送普通私信通知、状态结果、提醒、反馈等。
- 审批机器人:只发送需要处理的审批、验收、确认待办;未单独配置时自动回退通知机器人。
- 若审批机器人是独立飞书应用,系统会用
union_id发送审批私信;请确保用户登录过系统或执行过通讯录同步,否则审批私信会失败并等待 outbox 重试,不会降级到通知机器人。
-
将采购群 Webhook 对应的机器人拉入采购通知群;群 Webhook 是独立的群通知入口,不参与
notification/approval私信分流 -
获取采购机器人的 Webhook,填入
FEISHU_WEBHOOK_URL或FEISHU_PROCUREMENT_WEBHOOK_URL -
OAuth 登录和通讯录同步继续使用
FEISHU_APP_ID;消息发送优先使用FEISHU_NOTIFICATION_*,审批待办优先使用FEISHU_APPROVAL_*
若飞书后台「事件与回调」要求配置 Request URL,不要填网站首页;本项目使用官方 SDK 长连接接收事件,无需公网回调地址。
- 确保
.env已配置FEISHU_APP_ID、FEISHU_APP_SECRET,或已配置当前长连接机器人对应的消息应用凭证 - 启动长连接进程(开发:
npm run feishu:ws;审批机器人生产环境由./service/install.sh默认安装pnx-management-feishu-approval-ws.service并自动启动)。通知机器人长连接需设置ENABLE_FEISHU_WS=true后再安装;手动运行审批长连接可用FEISHU_WS_BOT_KIND=approval npm run feishu:ws。 - 日志出现「长连接已建立」后,在飞书开放平台 事件与回调 → 选择 使用长连接接收事件/回调
- 订阅事件(如
im.message.receive_v1);在 回调配置 启用card.action.trigger(采购审批按钮依赖此回调) - 若后台启用了加密策略,将
Encrypt Key/Verification Token填入FEISHU_EVENT_ENCRYPT_KEY、FEISHU_VERIFICATION_TOKEN
在飞书开放平台为应用开通 cardkit:card:write(卡片写权限),否则私信里的审批回调按钮无法发送。
待确认卡片嵌入报销截图还需审批应用开通 im:resource 或 im:resource:upload(上传图片资源)。未开通时会改为卡片内「点击在系统中查看」链接。
审批机器人长连接由 service/pnx-management-feishu-approval-ws.service 管理,安装脚本默认安装并启动;通知机器人长连接由 service/pnx-management-feishu-ws.service 管理,需显式设置 ENABLE_FEISHU_WS=true。不需要审批回调时可设置 ENABLE_FEISHU_APPROVAL_WS=false。
采购审批卡片可用 npm run feishu:card-preview -- <orderId> 预览。该脚本默认 dry-run;真实发送必须额外设置 CONFIRM_SEND_FEISHU=true,且不能设置 NOTIFICATION_DELIVERY_DISABLED=true。
登录只解决「谁能进系统」;审批按钮取决于 UserRole 表里的角色分配。
- 自己先用飞书登录一次
- 在
prisma/seed.ts填入自己的openId为SUPER_ADMIN,执行npm run db:seed - 登录后访问
/admin权限管理:- 点击 「同步飞书通讯录」 将企业全员录入系统(无需对方先登录)
- 车组组长配置:为每个车组指定组长与报销员
- 技术组组长配置:为每个技术组指定组长
用户也可通过飞书登录自动写入/更新 User 表;分配角色前需先完成通讯录同步或让对方登录一次。
在应用 权限管理 中开通并由企业管理员授权(应用身份、全部成员):
| 权限 | scope |
|---|---|
| 获取用户基本信息 | contact:user.base:readonly |
| 获取部门基础信息 | contact:department.base:readonly |
| 获取通讯录部门组织架构信息 | contact:department.organize:readonly |
同步使用 tenant_access_token 调用通讯录 API,将 open_id、姓名、头像写入 User 表。
| 角色 | 范围 | 权限 |
|---|---|---|
| SUPER_ADMIN | 全局 | 访问 /admin,管理所有角色 |
| TEAM_ADMIN | 指定车组 | 管理审核阶段,车组组长通过 |
| TECH_GROUP_ADMIN | 指定技术组 | 管理审核阶段,技术组组长通过 |
| TEACHER | 全局 | 「老师审核」阶段通过 |
| FINANCE | 指定车组 | 上传报销截图 |
| PROJECT_MANAGER | 全局 | 项管:进度汇总、项目异常介入、任务/里程碑验收 |
同一人可拥有多个角色(如同时担任「英雄」管理员与「工程」报销员)。
进度管理中的任务可配置“验收清单”。清单允许为空;如果配置了清单,任务进入验收时审批人必须逐项勾选后才能通过。任务产生任意交付记录后,清单会锁定为只读,保证验收口径可追溯。
超级管理员可在 /admin 的 常用验收条例 卡片中新增或删除模板。模板只用于任务创建/编辑时快捷加入;加入任务后会保存为该任务自己的快照,后续删除模板不会影响已有任务。
首次部署或 schema 同步后可写入默认常用条例:
npm run db:seed-acceptance-checklists常见原因:
db:seed未执行:seed.ts里写了 SUPER_ADMIN 不等于已写入数据库,需运行npm run db:seed- openId 不一致:
UserRole.openId必须与User表中你登录账号的 openId 完全一致 - 旧数据残留:若曾配置过
TECH或ou_xxx_placeholder,schema 升级后会导致角色表异常,执行:npm run db:fix-roles
- 会话未刷新:修改角色后退出重新飞书登录一次
const seedRoles = [
{ openId: "ou_xxx", role: UserRoleType.SUPER_ADMIN },
{ openId: "ou_yyy", role: UserRoleType.TEACHER },
{ openId: "ou_zzz", role: UserRoleType.TEAM_ADMIN, team: "英雄" },
{ openId: "ou_aaa", role: UserRoleType.TECH_GROUP_ADMIN, techGroup: "机械" },
{ openId: "ou_bbb", role: UserRoleType.FINANCE, team: "英雄" },
];审批:
- 申请人 →
/procurement/new提交 - 管理审核(状态「管理审核」):车组组长、技术组组长均需通过(分别私信通知),全部通过后进入老师审核
- TEACHER → 「指导老师通过」
- 采购人 → 上传发票,并为每行明细上传实物照片;系统自动生成 Word 验收清单(状态「待上传凭证」)。车组组长、技术组组长、采购人需事先在「个人设置」上传电子签名图片。
- 报销员 → 上传报销截图(状态「待报销截图」)
- 采购人 → 「确认报销」(状态「待确认」)→ 完成
状态一览:
草稿 → 管理审核 → 老师审核 → 待上传凭证 → 待报销截图 → 待确认 → 已完成
在 .env 填写 FEISHU_WEBHOOK_URL 后,状态变更和采购日报会向群推送卡片。未配置则跳过群通知。群 Webhook 独立于私信机器人,不受 botKind 控制。
开通权限 im:message:send_as_bot 后,系统会在状态变更时向对应角色的所有 UserRole 用户私发卡片。发送机器人按消息性质区分:
- 审批机器人:只发待审批、待验收、待确认等需要处理的消息。
- 通知机器人:发审批结果、普通状态变更、提醒、反馈等其他私信消息。
- 未配置审批机器人时,审批消息自动回退通知机器人。
| 订单状态 | 私信通知 |
|---|---|
| 管理审核 | 车组组长 + 技术组组长(分别发送) |
| 老师审核 | TEACHER |
| 待上传凭证 / 待确认 | 采购发起人 |
| 待报销截图 | FINANCE(对应车组) |
前提:
- 审批人已在
UserRole表中配置正确的open_id - 审批人至少登录过本系统一次,或已通过通讯录同步写入
User.unionId .env中通知机器人和审批机器人凭证有效;未单独配置时至少FEISHU_APP_ID/FEISHU_APP_SECRET有效
群 Webhook 与私信独立:只配 App 凭证也可发私信;Webhook 仅影响群消息。
- 登录:访问
/login,飞书授权后跳转/procurement/list - 申请:
/procurement/new填写车组、技术组,添加明细,点击「提交申请」 - 通知:提交后通知群应收到飞书交互卡片(需配置 Webhook)
- 管理审核:车组组长、技术组组长分别点击「通过」
- 老师审批:TEACHER 点击「指导老师通过」
- 采购人上传:多张发票(每张 ≤20MB)+ 每行实物照片(自动生成验收清单 Word)
- 报销员:在详情页或弹窗中查看发票与清单后,上传报销截图
- 采购人确认:点击「确认报销」
- 定时汇总:
npm run cron(每天 09:00)
上传文件保存在私有目录下,浏览器仍使用 /uploads/... 兼容链接,实际读取由鉴权 route 校验权限后返回:
storage/uploads/<订单ID>/<文件名>
例如:storage/uploads/a1b2c3.../invoice-1-1712345678-abc.pdf
- 通过浏览器访问:
http://localhost:3000/uploads/<订单ID>/<文件名> - 服务器上直接查看:进入项目根目录,打开
storage/uploads/文件夹 - 生产环境备份时请一并备份
storage/uploads/与 PostgreSQL 数据库
| 项目 | 限制 |
|---|---|
| 单文件大小 | 20MB |
| 发票数量 | 最多 20 张(可多选) |
| 实物照片 | 每行明细 1 张(png/jpg/pdf),用于嵌入验收清单 |
| 验收清单 | 系统按学校模板自动生成 .docx,无需手填 |
| 报销截图 | 1 份 |
| 反馈图片 | 单张 20MB,单次合计 50MB |
Server Actions 总上传上限 100MB(采购附件或反馈图片合计)。
订单详情页「流程附件」按步骤展示:
| 步骤 | 内容 | 可查看 |
|---|---|---|
| 采购人上传 | 发票、自动生成的验收清单 | 采购人、对应车组报销员、超级管理员 |
| 报销员上传 | 报销截图 | 同上 |
报销员在「上传截图」弹窗内也会显示发票与清单链接。飞书私信会提示前往详情页查看附件。
修改 next.config.ts 中 serverActions.bodySizeLimit 可调整总上传上限(需重启 dev server)。
本机 IP 变化时可在 .env 设置 LAN_HOST=你的IP,或 ALLOWED_DEV_ORIGINS=ip1,ip2 追加多个主机。
npm run dev默认监听 0.0.0.0:3000,局域网内其他设备可访问 http://<本机IP>:3000(如 http://10.4.150.222:3000)。
仅本机调试可用 npm run dev:local(只绑定 localhost)。
从手机或其他电脑访问时,保留 AUTH_URL / NEXTAUTH_URL 未设置,并配置允许的入口:
NEXT_PUBLIC_APP_URL="https://pnx.demonmaster.cn"
LAN_HOST=10.4.150.222
ALLOWED_DEV_ORIGINS=pnx.demonmaster.cn,10.4.150.222,localhost,127.0.0.1
APP_ALLOWED_ORIGINS="https://pnx.demonmaster.cn,http://10.4.150.222:3000,http://localhost:3000,http://127.0.0.1:3000"在应用「安全设置 → 重定向 URL」中追加:
https://pnx.demonmaster.cn/api/auth/callback/feishu
http://10.4.150.222:3000/api/auth/callback/feishu
http://localhost:3000/api/auth/callback/feishu
修改 next.config.ts 或 .env 后需重启 npm run dev。
如果通过 Nginx Proxy Manager 将 https://pnx.demonmaster.cn 反代到本服务,Details 页建议:
Scheme:httpForward Hostname / IP: 实际能访问到 Next 服务的上游地址Forward Port: 实际上游端口,例如直连本机服务用3000;经 frp 时用 frp 暴露业务的remotePort- 打开
Websockets Support - 打开
Block Common Exploits可保留 Custom Nginx Configuration默认留空
Nginx Proxy Manager 打开 Websockets Support 后会自动写入 Upgrade 相关代理配置,通常不需要在 Custom Nginx Configuration 里重复设置 proxy_set_header Upgrade / Connection。如果你不是用 Nginx Proxy Manager,而是手写 Nginx/OpenResty 配置,才需要类似下面的 location 配置:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}保存后可以验证 WebSocket 是否被正确透传:
curl -i --http1.1 \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13' \
'https://pnx.demonmaster.cn/_next/webpack-hmr?id=manual-test'期望看到 HTTP/1.1 101 Switching Protocols。如果返回 404 Not Found,说明域名反代没有透传 WebSocket,next dev 下客户端组件可能无法正常接管页面,表现为搜索框、下拉框、按钮等交互异常。长期使用域名访问时更推荐生产模式:npm run build 后用 next start -H 0.0.0.0 运行。
见上文 Docker 快速部署,适合内网服务器一键运行。
不能。 GitHub Pages 只托管静态 HTML/JS,本项目需要:
- Node.js 运行时(Server Actions、API Routes)
- PostgreSQL 数据库持久化
- 服务端飞书 OAuth 与文件上传
- 独立 cron 进程
因此必须部署到能跑 Node 的服务器或 PaaS,不能直接用 github.io。
| 方案 | 适用场景 | 说明 |
|---|---|---|
| 学校/实验室内网服务器 | 长期、仅校内使用 | npm run build && npm start,PM2 保活 + cron;飞书回调填内网域名或 IP |
| Vercel / Railway / Fly.io | 需要公网访问 | 使用托管 PostgreSQL;cron 用平台定时任务或单独 worker |
| 内网穿透(ngrok / frp / Tailscale) | 临时给外网或手机测 | 获得公网 URL 后写入飞书重定向与 APP_ALLOWED_ORIGINS |
| 自有 VPS | 完全自控 | 同内网服务器,可绑域名 + HTTPS(飞书生产环境建议 HTTPS) |
npm run build
npm start生产环境 .env 示例:
NEXT_PUBLIC_APP_URL="https://your-domain.example.com"
APP_ALLOWED_ORIGINS="https://your-domain.example.com,http://10.4.150.222:3000"飞书重定向 URL:
https://your-domain.example.com/api/auth/callback/feishu
主应用与 cron 为独立进程,cron 不应在 Serverless 环境内运行:
pm2 start npm --name procurement-cron -- run cron首页 「进度管理」 或导航 /progress,与采购报销共用登录与飞书应用。
| 路径 | 功能 |
|---|---|
/progress |
进度首页 |
/progress/new |
新建项目(含验收里程碑) |
/progress/list |
进行中的项目列表 |
/progress/[id] |
项目详情、里程碑、挂载任务,以及风险、评论和最近动态侧栏 |
/progress/task/[id] |
任务详情、交付、周报、验收 |
/progress/dashboard |
任务看板(待办/进行中/待验收/已完成) |
/progress/archive |
已归档项目与任务 |
项目状态(须逐步推进,不可跳跃):
草稿 → 进行中 → 正常 / 异常 → 负责人介入 → 结果理想 / 不理想 → 归档
任务状态(须逐步推进):
待办 → 进行中 → 待验收 → 已完成 → 归档
- 执行人:开始任务、提交交付(飞书文档 + 可选视频)、填写周报
- 组长 / 项管:验收任务与里程碑;审批前请在飞书中打开文档核对内容
- 里程碑须按顺序逐一提交与验收,全部通过后项目方可进入「结果理想」并归档
| 时间 | 内容 |
|---|---|
| 每日 09:00 | 采购日报 + 任务逾期警报 + 当日截止提醒 |
| 每周一 09:00 | 活跃任务周报填写提醒(私信负责人) |
| 权限 | 用途 |
|---|---|
im:message:send_as_bot |
任务指派、逾期、周报等私信 |