Files
subtrack/README.md
T
chun_qiu 8b9c347649
build / build-and-test (push) Successful in 3m38s
feat(v1.10.0): 重建 Bootstrap 主题层,修复迁移遗留的明暗/缓存/布局问题
背景:v1.9.0 把界面迁到 Bootstrap 5 时保留了旧自研 CSS,v1.9.2 又叠加了一层
「墨绿渐变」覆盖层,形成三套令牌(--paper/--ink、prefers-color-scheme、
data-bs-theme)并存。系统偏好与 Cookie 不一致时,页面会出现半亮半暗、表单
标签/抽屉标题/订阅页签不可见、输入框变黑块等问题。

本次改动:
- 样式加载顺序改为 bootstrap → app.css;删除旧纸墨令牌与 v1.9.2 覆盖层,
  app.css 重写为单一主题层(:root/[data-bs-theme=light] 与 [data-bs-theme=dark]
  两套语义令牌)。组件只读 --app-*,解决 --ink-2 同时被当作「次级文字色」和
  「卡片底色」的语义冲突。
- 主题只认 <html data-bs-theme>:服务端按 Cookie 直出;没有 Cookie 时由 <head>
  内联脚本在首屏按系统偏好补齐,修好「跟随系统」并避免闪白。大屏同理。
- 修复 Bootstrap 覆盖自研设计的问题:正文 14px + 中文字体栈、22/16/14px 标题、
  按钮/输入框/焦点环/卡片回到设计值;普通按钮新增 .btn-soft,不再变透明文字。
- 布局修复:客户/套餐表单的 .grid-2 不再塞 3 个字段;包详情页把筛选独立成
  GET 表单(在搜索框回车不再误触批量开通);客户详情操作区与 meta 重排;
  抽屉时钟改为固定 46px 并用它对齐抽屉头的 sticky 偏移;追加面板在抽屉里
  两列月历、不再 1100px 竖排;手机端日历数字不再换行溢出,移动端表格隐藏
  低优先级列,不再被裁切。
- 大屏:KPI/面板去掉 Bootstrap utility,统一由 screen.css 控制;TOP/今日待打
  改为固定列宽 + grid-column 对齐;浅色主题的链接、热力、图例对比度补齐;
  热力格不再整体降透明度;当日浮层可滚动并支持点击/键盘打开。
- 配色与可读性:普通文本对比度全部达标(截图 lint 由 65 处降到 0);日历
  外月日期改为浅底深字,不再整格 opacity;archived 行改为中性底色而非降透明。
- 缓存:新增内嵌静态资源内容指纹 AssetVersion(12 位 sha256),所有 /static
  链接带 ?v=;带指纹的响应 public, max-age=31536000, immutable,不带指纹的
  回源 no-cache;HTML 继续 no-store。改样式重新编译即自动破缓存,不再依赖
  手改版本号(仍保留 v1.10.0 作为产品版本)。
- 认证页重做:登录/初始化页改用 .auth 卡片(品牌、主题切换、绿色焦点环),
  去掉 min-height:100vh 叠加 main padding 造成的 40px 纵向滚动。
- JS:修 toISOString 计算自然日差一天的时区 bug;主题切换读 data-bs-theme
  并同步 aria-pressed;全选支持半选;大屏切主题同时更新 data-bs-theme;
  提示条改为固定定位、淡出不再顶动页面。
- README 更新主题与缓存说明;版本号 1.9.5 → 1.10.0。

验证:gofmt / go vet / go test 全绿;1440、1100、390 三档,明暗两套,工作台/
客户/套餐/订阅/设置/日志/大屏/登录全部截图走查,无横向溢出与控制台错误。
2026-10-20 01:46:11 +08:00

316 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 代打台 · 代打管理平台
一个给代打工作室用的单文件管理程序:**套餐 → 订阅 → 追加时长 → 日历打卡 → 大屏**。
整个程序就是一个 `daili.exe` + 一个 `daili.ini`,数据存在同目录的 `daili.db` 里,局域网内手机、电视都能打开。界面支持亮色 / 暗色切换,默认跟随系统。
---
## 1. 快速开始
1. 把 `daili.exe` 放进任意文件夹(建议路径不含空格,例如 `D:\代打台\`);
2. 双击 `daili.exe`:
- 首次运行会自动生成配置文件 `daili.ini`;
- 弹出一个黑色窗口,打印访问地址(默认 **17777** 端口);
- 默认会自动打开浏览器;
3. 浏览器打开 `http://127.0.0.1:17777` → 第一次会让你**创建管理员账号**;
4. 之后按「套餐 → 客户 → 订阅」的顺序录入即可。
```
┌───────────────────────────────────────────────┐
│ 代 打 台 · 代打管理平台 │
└───────────────────────────────────────────────┘
版本 1.10.0 配置文件 daili.ini
数据库 D:\代打台\daili.db
访问地址:
本机 http://127.0.0.1:17777
局域网 http://192.168.1.23:17777
大屏 http://127.0.0.1:17777/screen(免登录)
注意:大屏免登录且监听所有网卡,同局域网内任何人都能打开 /screen
按 Ctrl+C 停止服务
```
> 关掉黑窗口 = 停止服务。数据已经落盘,下次打开继续用。
---
## 2. 配置文件 daili.ini
配置文件固定放在 **exe 同级目录**,首次运行自动生成(带中文注释),改完**重启生效**。
写法:`key = value`,`#` 或 `;` 开头的整行是注释,不支持分节;**相对路径相对于 exe 所在目录**。
| 配置项 | 默认值 | 说明 |
| --- | --- | --- |
| `listen_ip` | `0.0.0.0` | 监听 IP。`0.0.0.0` = 所有网卡(局域网可访问);`127.0.0.1` = 只允许本机 |
| `listen_port` | `17777` | 监听端口(1–65535) |
| `database_path` | `daili.db` | SQLite 数据库文件(单文件,含全部数据) |
| `backup_enabled` | `true` | 是否每日自动备份 |
| `backup_dir` | `backup` | 备份目录(留空则用 exe 同级 `backup/`) |
| `backup_keep` | `14` | 备份保留份数(1–365),超出自动清理 |
| `timezone` | 空 | 留空跟随系统;可固定 `Asia/Shanghai`。**影响所有“自然日”计算** |
| `open_browser` | `true` | 启动后自动打开浏览器 |
| `screen_public` | `true` | 大屏 `/screen` 免登录(投屏用);改 `false` 则需登录 |
| `log_enabled` | `true` | 运行日志写文件(控制台始终输出) |
| `log_dir` | `logs` | 日志目录,按天切分 `daili-YYYY-MM-DD.log` |
**行为约定**
- 文件不存在 → 自动生成默认配置,照常启动;
- 文件写错 → **报错退出并指出行号**,原文件不会被修改。例如:
```
[代打台] 配置文件有问题,已停止启动:
第 3 行: listen_port 必须在 1-65535 之间,当前是 99999
修好后重新启动即可(原文件未被修改)
```
- 未知配置项、重复配置项、布尔值写成 `maybe`、时区写错,都会带行号报错;
- 端口被占用不会自动换端口,而是明确报错并提示改 `listen_port`。
### 命令行参数(临时覆盖配置,优先级更高)
| 参数 | 说明 |
| --- | --- |
| `-config <路径>` | 指定别的配置文件(默认 exe 同级 `daili.ini`) |
| `-addr <ip:端口>` | 临时覆盖监听地址,如 `-addr 127.0.0.1:19000` |
| `-db <路径>` | 临时覆盖数据库路径 |
| `-backup-keep <n>` | 临时覆盖备份保留份数 |
| `-no-backup` | 本次运行关闭自动备份 |
| `-no-open` | 本次运行不自动打开浏览器 |
示例:`daili.exe -addr 127.0.0.1:19000 -no-open`
> 业务参数(到期预警天数、大屏刷新秒数)不在配置文件里,仍在网页「设置」中修改。
---
## 3. 第一次使用(5 分钟走完)
1. **建套餐**:`套餐 → 新增套餐`
- 「包月日常」按日 30 天,色签绿;
- 「深渊十次」按次 10 次,色签紫。
> 以后要加套餐 D/E/F,直接再加一行就行,**不会影响任何已有客户数据**。
2. **建客户**:`客户 → 新建客户`,填手机尾号 + 客户名(这两项组合唯一)。
3. **开通订阅**:进客户详情 → 选套餐、开始日、天数(按次则填次数)→ 开通订阅。
4. **日常使用**:打开「日历」,今天那格里就是这个客户今天要打的单,点「完成」打卡。
5. **续费**:客户转账后,点订阅上的「追加时长」→ 在日历上点新的终止日期 → 确认。
面板会实时显示「计算起点 / 原结束日 / 新结束日 / 本次追加几天 / 追加后剩余几天」,并在客户页留下追加记录。
> 想先看效果再录自己的数据?程序启动后运行 `powershell -ExecutionPolicy Bypass -File scripts\seed.ps1`,会写入 8 个演示客户和 4 个套餐(默认账号 `admin` / `daili123`)。
---
## 4. 业务规则(和 PRD 一致)
**剩余时长**
- 按日:`剩余天数 = 结束日 − 今天 + 1`,结束日当天仍算服务日;
- 按次:`剩余次数 = 总次数 − 已用次数`;按次套餐的「有效期」可留空,留空就一直有效直到次数用完。
**追加时长(核心)**
- 打开追加面板,**在月历上点选新的终止日期**,系统自动反算;
- 未到期的订阅:从原结束日的**次日**起算,追加天数 = 新结束日 − 原结束日;
- 已到期的订阅:**从今天起算**(不会把中间空档的天数白送给客户);
- 追加记录保留:计算起点、旧结束日、新结束日、天数、备注、操作人、时间。
**暂停 / 恢复**
- 暂停期间冻结剩余时长,日历上不出现待打任务;
- 恢复时按日套餐的结束日**自动顺延**暂停的自然日天数,时间轴上是灰色斜纹段。
**打卡**
- 按日套餐:每天一条「完成 / 跳过」,只做记录不扣天数(天数按自然日流逝);
- 按次套餐:打卡「完成」自动扣 1 次,撤销打卡返还 1 次;次数为 0 时不允许再打;
- 按次套餐如果没设有效期,它不会出现在日历里 —— 直接在客户详情点订阅卡片上的「今日打卡」扣次。
**预警**
- 剩余 ≤3 天橙色、今天到期红色、已过期灰色;顶栏常驻「今日到期 / 3 天内」两个数字。
**查询**
- 顶栏搜索框输入手机尾号即可定位客户(也支持姓名、游戏名、联系方式),按 `/` 快速聚焦。
---
## 4.1 工作台速用
- **按尾号快速完成**:日历右侧「当日待完成」顶部输入手机尾号 → 回车。
- 只匹配到 1 项:直接完成并提示;
- 同一尾号有多个客户,或一个客户今天有多个服务:弹出勾选面板,默认全选,可多选后一次完成。
- **加急**:每条任务右侧点「加急」,会加红色 `加急` 标签并排到最前;日历格子出现红点,大屏「今日待打」同步置顶。再点一次取消。
- **按套餐分日历**:套餐编辑页勾选「在日历中独立显示」后,工作台与大屏顶部会出现套餐标签(全部 / 每日任务 / …),可以只看某类委托的排单。
- **触摸小键盘**:日历右侧输入框下面有 3 列 4 行数字键盘,底行是 ⌫ / 0 / ✓,点 ✓ 直接提交;按键为自定义样式,不是浏览器原生按钮,触摸目标 52px 高;
- **日历格子**:不再显示排单缩略信息,只保留左侧打手色竖条 + 大字「待完成 / 总数」(如 1/2 = 还剩 1 单,共 2 单);
- **按钮收纳**:任务行只露出「完成」,跳过 / 加急 / 追加收进「⋯」菜单,点空白处自动收起;
- **停用客户**:客户详情页可「停用」(默认从列表、日历、大屏隐藏,订阅保留)或「彻底删除」(连订阅/打卡/追加记录一起删,需二次确认);列表右上角可切换到「显示已停用」再恢复;
- **7 天内到期**:顶栏新增徽标,固定统计未来 7 天内到期的订阅;
- **抽屉时钟**:右侧当日面板顶部有时钟,30px 大写、水平垂直居中,24 小时制精确到秒,下面一行是当天日期;
- **触控 / PC 模式**:尾号输入框右侧有「⌨ / 触控」开关。切到触控时展开 3×4 小键盘、输入框变只读并关闭系统输入法(`inputmode=none`),点键直接写值;切回 PC 收起键盘。选择记在浏览器里。
- **格子计数**:工作台和大屏的日历格都显示「已完成 / 总数」,例如 2/5 = 5 单里做完 2 单。
- **已完成折叠**:当日清单里已完成/已跳过的单子自动折叠成一行(尾号 + 姓名 + 套餐 + 状态),点这一行展开看剩余、打手,以及撤销/加急/追加;未完成的行不受影响;
- **打手颜色**:`设置 → 管理员` 里给每个管理员选一个颜色。客户详情或编辑页可以把客户指派给某个打手,之后这个客户的所有单子都用该颜色标识:
- 日历格左侧竖条 = 当天涉及的所有打手颜色(几种人几条,等分显示);
- 「当日待完成」每条前面有打手色条 + 名字,未指派的显示灰色「未指派」;
- 大屏的今日待打、悬停浮层、热力格竖条同样带颜色。
- **主题**:顶栏右上角 ☾ / ☀ 切换亮 / 暗,偏好记在浏览器里;没选过时由页面在首屏按系统偏好自动决定。整套界面只认 `<html data-bs-theme>` 一个开关,不会出现「半亮半暗」。
- **升级不缓存旧界面**:CSS/JS 链接带**内容指纹**(`app.css?v=<12 位哈希>`)。资源可以长缓存(一年、immutable),但样式或脚本一改、重新编译,URL 就会变,浏览器自动取新文件,不用手动清缓存;HTML 仍然 `no-store`。
---
## 4.3 包赛季与按次有效期
- **包赛季**:套餐计费方式新增第三种「包赛季」,套餐上直接填赛季开始日/结束日。客户开通后,日历上用一条横向色带标出整段赛季;赛季内任意一天点「完成」即整段完成,完成后不再出现在日历与待打,客户详情显示「赛季已完成 + 日期」。
- **赛季过期不可选**:今天已过赛季结束日的套餐,在客户开通下拉里会置灰并标「赛季已结束」,服务端也会拒绝开通。
- **按次默认有效期**:按次套餐可设「默认有效期(天)」,开单时自动算出到期日(开始日 + 天数 − 1),仍可手动改。
- **删除订阅**:已结束的订阅卡片上,在「重新启用」旁新增「删除」,二次确认后彻底删除该订阅及其打卡/追加记录。
---
## 4.2 套餐标签与批量开通
- **套餐标签**:套餐编辑页可以填一个标签(如「包月」)。日历/大屏顶部页签会**按标签合并**:同标签的套餐合成一个页签;勾了「在日历中独立显示」的套餐**额外**再开一个自己的页签。
- **批量开通**:`套餐 → 详情/批量`,勾选客户(表头有全选,已订阅该套餐的客户置灰),填开始日期与天数/次数后一次开通;重复的会自动跳过并提示。
- **客户月历**:客户详情页新增「当月排单」,显示该客户当月每天几单、完成几单,可翻月,点某天直接跳到日历那天。
- **按次到期**:按次订阅的「到期日」仍是选填;填了到期后状态显示「已到期」、不再进日历待排。
---
## 5. 数据、备份与导出
| 文件 | 说明 |
| --- | --- |
| `daili.db` | **全部数据都在这一个文件里**(SQLite,单文件回滚日志模式,运行中也不会产生 `-wal`/`-shm`) |
| `backup/daili-YYYYMMDD-HHMMSS.db` | 每天首次启动时自动备份一份,保留份数见 `backup_keep` |
| `logs/daili-YYYY-MM-DD.log` | 运行日志,按天切分 |
### 迁移与重置(只说一次,很重要)
- **迁移**:关闭程序 → 把 `daili.db` 复制到新电脑的新目录 → 放入 `daili.exe` 双击即可,账号、套餐、客户、订阅、追加记录、打卡、日志全部原样保留。
- **重置**:关闭程序 → 删除 `daili.db` → 重新打开,会回到「创建第一个管理员」的全新状态。
- 包赛季是 `packages.season/season_start/season_end` 与 `subscriptions.season/season_done`;套餐标签是 `packages.tag`;客户停用是 `customers.archived`(0/1);打手颜色存在 `users.color`,客户归属打手是 `customers.assignee_id`;加急标记存在 `daili.db` 的 `task_flags` 表;套餐的「独立日历」是 `packages.calendar_separate` 列,老版本数据库打开时会**自动加列**,不用手工迁移;
- `daili.ini`(监听/备份/日志配置)与 `backup/`、`logs/` 都是**独立于数据**的,迁移时可以不拷,程序会按默认值重新生成。
- 手动备份:`设置 → 立即备份`(同样遵守保留份数),备份文件本身也是可直接打开的单文件库;
- 导出:`设置 → 导出客户 / 订阅 / 日志 CSV`,UTF-8 BOM,Excel 直接打开不乱码。
---
## 6. 大屏
- 地址:`http://<本机IP>:17777/screen`
- 默认 `screen_public = true`:**免登录**,电视/副屏直接打开即可(仅建议局域网使用);
- 改成 `screen_public = false` 后大屏回到登录保护;
- 「记分板」风格,亮 / 暗主题同样可切(右上角 ☾ / ☀):
- 顶部 KPI:客户 / 活跃订阅 / 今日到期 / 7 天内到期;
- 中部:**热力月历 + 即将到期 TOP10**。热力默认统计「每日待完成单量」,点右上角按钮可切回「到期热力」;鼠标悬停任意格子会浮出当日待办明细(含加急标记);
- 底部:**今日到期**(左)+ **今日待打**(右,加急置顶带红 tag);
- 最下方细条:最近追加流水;
- 套餐勾选「在日历中独立显示」后,大屏顶部会出现套餐标签,可切换只看某类委托;
- 默认 30 秒自动刷新(设置页可改),按 `F` 全屏。
---
## 7. 从源码构建
环境:Windows + Go 1.25(本项目在 `go1.25.6` 上验证)。
```powershell
.\build.ps1
```
`build.ps1` 会设置 `GOPROXY=off`、`GOCACHE=<项目>\.gocache`,用本地模块缓存离线构建,产物为 `daili.exe`(约 13.5 MB,内含 SQLite 引擎、时区库、模板与静态资源,不依赖任何 DLL)。
> **为什么有 `third_party/fileutil`**:SQLite 驱动 `modernc.org/sqlite` 的**测试代码**引用了 `modernc.org/fileutil`,但编译产物并不依赖它。本机无法访问任何 Go 模块源,因此用一个空模块占位,仅用于满足模块图解析。
>
> **为什么内嵌时区库**:Windows 没有 `/usr/share/zoneinfo`,为了 `timezone = Asia/Shanghai` 在任何机器上都可用,引入了 `time/tzdata`(约 0.5 MB)。
> 改了依赖之后,请在 Windows 上额外跑一次 `GOOS=linux go build ./...` 再提交 `go.sum`:
> 有些依赖(例如 `modernc.org/libc` 的 Linux 专用文件)只在 Linux 构建路径下被编译,
> 只在本机 Windows 构建会漏掉它们的 `go.sum` 条目,导致 CI 报 `missing go.sum entry`。
测试:
```powershell
$env:GOFLAGS='-mod=mod'; $env:GOPROXY='off'; $env:GOCACHE="$PWD\.gocache"
go test ./...
```
目录结构:
```
main.go 程序入口(读配置、日志、时区、监听、备份)
internal/config daili.ini 的生成/解析/校验(含单测)
internal/domain 纯业务计算(日期、剩余天数、状态、追加换算)+ 单元测试
internal/store SQLite 连接、迁移、全部读写 + 单元测试
internal/logx 控制台 + 按天切分的运行日志
internal/pw 口令散列(PBKDF2-SHA256,标准库实现)
internal/web 路由、会话、模板渲染、处理器
internal/web/templates 页面与布局(embed 进二进制)
internal/web/static app.css / screen.css / app.js / screen.js
scripts/acceptance.ps1 冒烟清单执行器(119 条断言)
scripts/config-test.ps1 配置文件专项测试(14 条断言)
scripts/smoke.ps1 端到端冒烟(早期版本,保留)
scripts/seed.ps1 写入演示数据
scripts/dbtool.py 测试预处理(暂停回溯、批量造数、完整性检查)
.gitea/workflows/build.yml Gitea Actions:vet → test → 交叉编译 → 上传产物
.gitattributes 统一换行(.go/.yml 用 LF,.ps1 用 CRLF)
PRD.md 产品需求文档(本地文档,不入库)
TEST-CHECKLIST.md 冒烟测试清单 + 执行结果 + 缺陷记录(本地,不入库)
test-evidence/ 测试证据(本地,不入库)
daili.ini 首次运行自动生成
```
### 持续集成(Gitea Actions)
仓库:<https://git.code.cq.cn/chun_qiu/subtrack.git>,CI 配置在 `.gitea/workflows/build.yml`。
- 触发:推送到 `main` / `master`、打 `v*` 标签、PR、手动执行;
- 运行环境:`runs-on: ubuntu-latest`;Go 版本由 `actions/setup-go@v5` 读取 `go.mod`(1.25);
- 步骤:`go vet ./...` → `go test ./...` → 交叉编译 Windows / Linux 两个二进制 → `sha256sum` → 用 `actions/upload-artifact@v4` 上传;
- 上传步骤带 `env: ACTIONS_RESULTS_URL: https://git.code.cq.cn/`;
- 产物:`subtrack-<run_number>`,内含 `daili-windows-amd64.exe`、`daili-linux-amd64`、`SHA256SUMS.txt`,保留 14 天;
- 模块代理默认 `https://goproxy.cn,direct`,如在海外或内网构建,改 workflow 顶部的 `GOPROXY` 即可。
> 仓库只入库源码、构建脚本、CI 配置和本 README;PRD、测试清单、截图与测试证据都在 `.gitignore` 里排除。
---
## 8. 常见问题
**Windows 提示「已保护你的电脑」**
点「更多信息 → 仍要运行」。这是 Windows 对未签名 exe 的默认拦截。
**端口被占用 / 想换端口**
改 `daili.ini` 里的 `listen_port` 后重启;临时换端口可用 `daili.exe -addr :19000`。
**只想自己用,不让局域网访问**
把 `listen_ip` 改成 `127.0.0.1`,启动横幅会提示「仅本机可访问」。
**手机打不开局域网地址**
1. 确认 `listen_ip` 是 `0.0.0.0`;
2. 确认手机和电脑在同一个 WiFi;
3. Windows 防火墙首次会弹窗,选「允许访问」(专用网络);
4. 用启动时打印的那个 `192.168.x.x` 地址,不要用 `127.0.0.1`。
**忘记密码**
让另一位管理员在 `设置 → 管理员` 里新增一个账户即可;如果只剩一个管理员且忘了密码,只能删掉 `daili.db` 重来。
**数据会不会因为程序崩溃丢?**
单文件 SQLite + 每次操作即时落盘,正常使用不会丢;每天的自动备份是双保险。仍然建议定期把 `backup/` 拷到别的盘。
**能放到公网吗?**
不建议。程序按「局域网内可信环境」设计:会话 Cookie 有 HttpOnly / SameSite 保护,但没有 HTTPS、没有登录失败锁定,且大屏默认免登录。要放公网请套一层反向代理 + HTTPS + 访问控制,并把 `screen_public` 改成 `false`。
---
## 9. 安全说明
- **初始管理员**(第一个创建的账号,`id=1`)在 `设置 → 管理员` 每行可以点「编辑」,修改其他管理员的用户名、昵称,或重置密码;
- 重置密码会立刻清除对方的登录会话,他需要用新密码重新登录;昵称/用户名的修改不影响会话;
- 自己那一行不显示编辑(改自己的密码用「修改我的密码」),避免误操作把自己锁在门外;
- 其余管理员权限相同,但只能改自己的密码。
- 口令使用 PBKDF2-HMAC-SHA256(12 万次迭代 + 随机盐)存储,不可逆;
- 所有写操作记录操作人、时间、IP 与摘要,可在「操作日志」里查询;
- 服务端模板全部走 `html/template` 自动转义;
- 所有 POST 校验 `Origin`,会话 Cookie 使用 `SameSite=Lax` + `HttpOnly`;
- 配置文件里的数据库/备份/日志路径请自行确保目录权限,程序不会覆盖已有文件(备份重名会跳过)。