feat(v1.3): 管理员(打手)颜色,客户归属打手,日历/待办按颜色标识

1) 管理员颜色
- users 表新增 color;设置 → 管理员 每行一个颜色选择器,保存即生效
- 新建管理员自动从 8 色调色板分配,尽量不重复;支持 #RRGGBB 校验,非法值拒绝

2) 客户归属打手
- customers 表新增 assignee_id(可空,指向 users)
- 新建/编辑客户时可选择「归属打手」,客户详情页也能直接改派
- 语义:一个客户一个打手,该客户之后的所有单子都归这个打手
- 停用的账户不出现在打手候选里;未指派显示灰色「未指派」

3) 日历
- 每个日期格左侧新增 5px 竖条,按当天涉及的打手颜色等分显示(几种颜色几段)
- 竖条按“当天全部任务”去重计算,不受格内色签 3 条上限影响
- 未指派的任务用中性灰段落

4) 待办与大屏
- 「当日待完成」每条任务左侧 3px 打手色条 + 圆点色标 + 打手名字,未指派灰字
- 大屏:今日待打列表、热力格竖条、悬停浮层(新增 who / who_color 字段)统一使用同一套颜色
- 加急样式改为红底,避免与打手色左边框冲突

数据库兼容性:
- 只做加法:users.color、customers.assignee_id 两列;老库打开时自动 ALTER,可重复迁移
- 原有表结构、字段、页面接口不变;未指派的存量数据照常工作

测试:
- 新增单测:颜色自动分配/规范化/非法值拒绝、客户指派与取消、订阅联表带出打手、停用账户不进候选
- 旧库迁移测试扩展:老 users / customers 表自动补列并可写
- 功能验证 16 项(颜色保存、指派、详情展示、日历多色竖条分段、大屏列表与浮层接口)全部通过
- 原有 119 项验收回归全绿;go vet / go test ./... 通过;版本号 1.3.0
This commit is contained in:
subtrack
2026-10-19 00:16:34 +08:00
parent 85f98dff5c
commit 8cfc9f5891
21 changed files with 542 additions and 43 deletions
+150
View File
@@ -764,6 +764,33 @@ func TestMigrateOldDatabaseAddsColumn(t *testing.T) {
)`,
`INSERT INTO packages(name,unit,default_amount,color,note,archived,sort_order,created_at,updated_at)
VALUES('老套餐','day',30,'#2F5D50','',0,1,'2026-01-01 00:00:00','2026-01-01 00:00:00')`,
`CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
last_login_at TEXT
)`,
`INSERT INTO users(username,display_name,password_hash,is_active,created_at)
VALUES('admin','老板','x',1,'2026-01-01 00:00:00')`,
`CREATE TABLE customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tail TEXT NOT NULL,
name TEXT NOT NULL,
game_name TEXT NOT NULL DEFAULT '',
game_server TEXT NOT NULL DEFAULT '',
contact TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (tail, name)
)`,
`INSERT INTO customers(tail,name,created_at,updated_at)
VALUES('1001','老客户','2026-01-01 00:00:00','2026-01-01 00:00:00')`,
}
for _, s := range stmts {
if _, err := old.Exec(s); err != nil {
@@ -797,6 +824,28 @@ func TestMigrateOldDatabaseAddsColumn(t *testing.T) {
if got, _ := db.Package(1); !got.CalendarSeparate {
t.Fatal("迁移后新列不可写")
}
// 老库的 users / customers 也补上了新列
var userColor string
if err := db.sql.QueryRow(`SELECT IFNULL(color,'') FROM users WHERE id=1`).Scan(&userColor); err != nil {
t.Fatalf("users.color 未补上: %v", err)
}
if err := db.SetUserColor(1, "#123456"); err != nil {
t.Fatalf("老库 users.color 不可写: %v", err)
}
c, err := db.Customer(1)
if err != nil {
t.Fatalf("老客户读取失败: %v", err)
}
if c.AssigneeID != 0 {
t.Fatalf("老客户不应有打手: %+v", c)
}
if err := db.SetCustomerAssignee(1, 1); err != nil {
t.Fatalf("老库 customers.assignee_id 不可写: %v", err)
}
if c2, _ := db.Customer(1); c2.AssigneeID != 1 {
t.Fatalf("指派未生效: %+v", c2)
}
// 新表 task_flags 已建立
var tbl string
if err := db.sql.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name='task_flags'`).Scan(&tbl); err != nil {
@@ -807,3 +856,104 @@ func TestMigrateOldDatabaseAddsColumn(t *testing.T) {
t.Fatalf("重复迁移失败: %v", err)
}
}
func TestUserColorAndAssignee(t *testing.T) {
db := newTestDB(t)
// 新建管理员自动分配调色板颜色
u1, err := db.CreateUser("dashou1", "阿强", "daili123")
if err != nil {
t.Fatal(err)
}
u2, err := db.CreateUser("dashou2", "小美", "daili123")
if err != nil {
t.Fatal(err)
}
if u1.Color == "" || u2.Color == "" || u1.Color == u2.Color {
t.Fatalf("自动颜色不合理: %q %q", u1.Color, u2.Color)
}
// 改色(小写转大写)
if err := db.SetUserColor(u2.ID, "#c0392b"); err != nil {
t.Fatal(err)
}
got, err := db.UserByID(u2.ID)
if err != nil || got.Color != "#C0392B" {
t.Fatalf("颜色未保存或未规范化: %+v err=%v", got, err)
}
// 非法颜色
if err := db.SetUserColor(u2.ID, "red"); err == nil {
t.Fatal("非法颜色应报错")
}
if err := db.SetUserColor(u2.ID, "#12345"); err == nil {
t.Fatal("长度不对应报错")
}
// 不存在的账户
if err := db.SetUserColor(9999, "#123456"); err == nil {
t.Fatal("不存在的账户应报错")
}
// 客户指派
cid := mustCustomer(t, db, "3001", "客户甲")
if err := db.SetCustomerAssignee(cid, u1.ID); err != nil {
t.Fatal(err)
}
c, err := db.Customer(cid)
if err != nil {
t.Fatal(err)
}
if c.AssigneeID != u1.ID || c.AssigneeName != "阿强" || c.AssigneeColor != "#2F5D50" {
t.Fatalf("客户指派信息不对: %+v", c)
}
// 取消指派
if err := db.SetCustomerAssignee(cid, 0); err != nil {
t.Fatal(err)
}
if c2, _ := db.Customer(cid); c2.AssigneeID != 0 || c2.AssigneeName != "" {
t.Fatalf("取消指派失败: %+v", c2)
}
// 指派给不存在的打手
if err := db.SetCustomerAssignee(cid, 9999); err == nil {
t.Fatal("不存在的打手应报错")
}
// 客户创建时直接带指派
cid2, err := db.CreateCustomer(&Customer{Tail: "3002", Name: "客户乙", AssigneeID: u2.ID})
if err != nil {
t.Fatal(err)
}
c3, _ := db.Customer(cid2)
if c3.AssigneeName != "小美" || c3.AssigneeColor != "#C0392B" {
t.Fatalf("新建客户的指派未生效: %+v", c3)
}
// 订阅联表能带出打手
pkg := mustPackage(t, db, "包月日常", domain.UnitDay, 30)
sub, err := db.CreateSub(CreateSubInput{
CustomerID: cid2, PackageID: pkg.ID, StartDate: domain.Date(domain.Today()), Days: 30,
})
if err != nil {
t.Fatal(err)
}
gotSub, err := db.Sub(sub.ID)
if err != nil {
t.Fatal(err)
}
if gotSub.AssigneeName != "小美" || gotSub.AssigneeColor != "#C0392B" {
t.Fatalf("订阅未带出打手: %+v", gotSub)
}
// 只有启用中的账户可作为打手候选
if err := db.SetUserActive(u1.ID, false); err != nil {
t.Fatal(err)
}
users, err := db.ActiveUsers()
if err != nil {
t.Fatal(err)
}
for _, u := range users {
if u.ID == u1.ID {
t.Fatal("停用账户不应出现在打手候选里")
}
}
}