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:
@@ -23,6 +23,11 @@ type Customer struct {
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
|
||||
// 归属打手(管理员)
|
||||
AssigneeID int64
|
||||
AssigneeName string
|
||||
AssigneeColor string
|
||||
|
||||
// 列表聚合
|
||||
SubCount int
|
||||
Summary string
|
||||
@@ -41,12 +46,13 @@ func (c *Customer) TagList() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
const customerCols = `id,tail,name,game_name,game_server,contact,tags,source,note,created_at,updated_at`
|
||||
const customerCols = `id,tail,name,game_name,game_server,contact,tags,source,note,created_at,updated_at,IFNULL(assignee_id,0),IFNULL((SELECT u.display_name FROM users u WHERE u.id=customers.assignee_id),''),IFNULL((SELECT u.color FROM users u WHERE u.id=customers.assignee_id),'')`
|
||||
|
||||
func scanCustomer(row interface{ Scan(dest ...any) error }) (*Customer, error) {
|
||||
var c Customer
|
||||
err := row.Scan(&c.ID, &c.Tail, &c.Name, &c.GameName, &c.GameServer, &c.Contact,
|
||||
&c.Tags, &c.Source, &c.Note, &c.CreatedAt, &c.UpdatedAt)
|
||||
&c.Tags, &c.Source, &c.Note, &c.CreatedAt, &c.UpdatedAt,
|
||||
&c.AssigneeID, &c.AssigneeName, &c.AssigneeColor)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
@@ -59,10 +65,10 @@ func scanCustomer(row interface{ Scan(dest ...any) error }) (*Customer, error) {
|
||||
// CreateCustomer 新建客户(尾号+姓名唯一)。
|
||||
func (db *DB) CreateCustomer(c *Customer) (int64, error) {
|
||||
now := nowStamp()
|
||||
res, err := db.sql.Exec(`INSERT INTO customers(tail,name,game_name,game_server,contact,tags,source,note,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
res, err := db.sql.Exec(`INSERT INTO customers(tail,name,game_name,game_server,contact,tags,source,note,assignee_id,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
strings.TrimSpace(c.Tail), strings.TrimSpace(c.Name), c.GameName, c.GameServer, c.Contact,
|
||||
c.Tags, c.Source, c.Note, now, now)
|
||||
c.Tags, c.Source, c.Note, nullIfZero(c.AssigneeID), now, now)
|
||||
if err != nil {
|
||||
if isUnique(err) {
|
||||
return 0, ErrDuplicate
|
||||
@@ -74,10 +80,10 @@ func (db *DB) CreateCustomer(c *Customer) (int64, error) {
|
||||
|
||||
// UpdateCustomer 更新客户。
|
||||
func (db *DB) UpdateCustomer(c *Customer) error {
|
||||
_, err := db.sql.Exec(`UPDATE customers SET tail=?,name=?,game_name=?,game_server=?,contact=?,tags=?,source=?,note=?,updated_at=?
|
||||
_, err := db.sql.Exec(`UPDATE customers SET tail=?,name=?,game_name=?,game_server=?,contact=?,tags=?,source=?,note=?,assignee_id=?,updated_at=?
|
||||
WHERE id=?`,
|
||||
strings.TrimSpace(c.Tail), strings.TrimSpace(c.Name), c.GameName, c.GameServer, c.Contact,
|
||||
c.Tags, c.Source, c.Note, nowStamp(), c.ID)
|
||||
c.Tags, c.Source, c.Note, nullIfZero(c.AssigneeID), nowStamp(), c.ID)
|
||||
if err != nil && isUnique(err) {
|
||||
return ErrDuplicate
|
||||
}
|
||||
|
||||
@@ -121,6 +121,15 @@ func (db *DB) Migrate() error {
|
||||
"ALTER TABLE packages ADD COLUMN calendar_separate INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
return err
|
||||
}
|
||||
// v1.3:管理员颜色 + 客户归属打手
|
||||
if err := db.ensureColumn("users", "color",
|
||||
"ALTER TABLE users ADD COLUMN color TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.ensureColumn("customers", "assignee_id",
|
||||
"ALTER TABLE customers ADD COLUMN assignee_id INTEGER REFERENCES users(id)"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -161,6 +170,7 @@ var schemaStatements = []string{
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
password_hash TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
color TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
last_login_at TEXT
|
||||
)`,
|
||||
@@ -195,6 +205,7 @@ var schemaStatements = []string{
|
||||
tags TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
assignee_id INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (tail, name)
|
||||
|
||||
@@ -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("停用账户不应出现在打手候选里")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ type Subscription struct {
|
||||
CustomerTail string
|
||||
CustomerName string
|
||||
CustomerContact string
|
||||
|
||||
// 客户归属的打手(管理员)
|
||||
AssigneeID int64
|
||||
AssigneeName string
|
||||
AssigneeColor string
|
||||
}
|
||||
|
||||
// Eval 计算展示状态。
|
||||
@@ -93,7 +98,10 @@ func (a *Adjustment) KindLabel() string {
|
||||
|
||||
const subSelect = `SELECT s.id,s.customer_id,s.package_id,s.unit,s.status,s.start_date,IFNULL(s.end_date,''),
|
||||
s.total_count,s.used_count,s.note,IFNULL(s.created_by,0),s.created_at,s.updated_at,IFNULL(s.closed_at,''),
|
||||
p.name,p.color,c.tail,c.name,IFNULL(c.contact,'')
|
||||
p.name,p.color,c.tail,c.name,IFNULL(c.contact,''),
|
||||
IFNULL(c.assignee_id,0),
|
||||
IFNULL((SELECT u.display_name FROM users u WHERE u.id=c.assignee_id),''),
|
||||
IFNULL((SELECT u.color FROM users u WHERE u.id=c.assignee_id),'')
|
||||
FROM subscriptions s
|
||||
JOIN packages p ON p.id=s.package_id
|
||||
JOIN customers c ON c.id=s.customer_id`
|
||||
@@ -102,7 +110,8 @@ func scanSub(row interface{ Scan(dest ...any) error }) (*Subscription, error) {
|
||||
var s Subscription
|
||||
err := row.Scan(&s.ID, &s.CustomerID, &s.PackageID, &s.Unit, &s.Status, &s.StartDate, &s.EndDate,
|
||||
&s.TotalCount, &s.UsedCount, &s.Note, &s.CreatedBy, &s.CreatedAt, &s.UpdatedAt, &s.ClosedAt,
|
||||
&s.PackageName, &s.PackageColor, &s.CustomerTail, &s.CustomerName, &s.CustomerContact)
|
||||
&s.PackageName, &s.PackageColor, &s.CustomerTail, &s.CustomerName, &s.CustomerContact,
|
||||
&s.AssigneeID, &s.AssigneeName, &s.AssigneeColor)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
|
||||
+90
-5
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"daili/internal/pw"
|
||||
@@ -14,17 +15,41 @@ type User struct {
|
||||
Username string
|
||||
DisplayName string
|
||||
IsActive bool
|
||||
Color string
|
||||
CreatedAt string
|
||||
LastLoginAt string
|
||||
}
|
||||
|
||||
// AssigneePalette 是新建管理员时自动分配的颜色(区分度较高的深色系)。
|
||||
var AssigneePalette = []string{
|
||||
"#2F5D50", "#8C4A3C", "#3E5C8A", "#7A5AA6",
|
||||
"#A66A1E", "#2C7A7B", "#8A3B5C", "#5B6B2F",
|
||||
}
|
||||
|
||||
// NormalizeColor 校验并规范化 #RRGGBB;空值返回空。
|
||||
func NormalizeColor(c string) (string, error) {
|
||||
c = strings.TrimSpace(c)
|
||||
if c == "" {
|
||||
return "", nil
|
||||
}
|
||||
if len(c) != 7 || c[0] != '#' {
|
||||
return "", errors.New("颜色格式应为 #RRGGBB")
|
||||
}
|
||||
for _, r := range c[1:] {
|
||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||
return "", errors.New("颜色格式应为 #RRGGBB")
|
||||
}
|
||||
}
|
||||
return strings.ToUpper(c), nil
|
||||
}
|
||||
|
||||
func scanUser(row interface {
|
||||
Scan(dest ...any) error
|
||||
}) (*User, error) {
|
||||
var u User
|
||||
var active int
|
||||
var last sql.NullString
|
||||
if err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &active, &u.CreatedAt, &last); err != nil {
|
||||
if err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &active, &u.CreatedAt, &last, &u.Color); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -35,7 +60,7 @@ func scanUser(row interface {
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
const userCols = `id, username, display_name, is_active, created_at, IFNULL(last_login_at,'')`
|
||||
const userCols = `id, username, display_name, is_active, created_at, IFNULL(last_login_at,''), IFNULL(color,'')`
|
||||
|
||||
// CountUsers 返回账户数量。
|
||||
func (db *DB) CountUsers() (int, error) {
|
||||
@@ -51,8 +76,9 @@ func (db *DB) CreateUser(username, displayName, password string) (*User, error)
|
||||
return nil, err
|
||||
}
|
||||
now := nowStamp()
|
||||
res, err := db.sql.Exec(`INSERT INTO users(username, display_name, password_hash, is_active, created_at)
|
||||
VALUES(?,?,?,1,?)`, username, displayName, hash, now)
|
||||
color := db.nextAssigneeColor()
|
||||
res, err := db.sql.Exec(`INSERT INTO users(username, display_name, password_hash, is_active, color, created_at)
|
||||
VALUES(?,?,?,1,?,?)`, username, displayName, hash, color, now)
|
||||
if err != nil {
|
||||
if isUnique(err) {
|
||||
return nil, ErrDuplicate
|
||||
@@ -60,7 +86,66 @@ func (db *DB) CreateUser(username, displayName, password string) (*User, error)
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return &User{ID: id, Username: username, DisplayName: displayName, IsActive: true, CreatedAt: now}, nil
|
||||
return &User{ID: id, Username: username, DisplayName: displayName, IsActive: true, Color: color, CreatedAt: now}, nil
|
||||
}
|
||||
|
||||
// nextAssigneeColor 依据现有账户数循环取调色板,尽量不重复。
|
||||
func (db *DB) nextAssigneeColor() string {
|
||||
n, _ := db.CountUsers()
|
||||
return AssigneePalette[n%len(AssigneePalette)]
|
||||
}
|
||||
|
||||
// SetUserColor 设置管理员颜色(空字符串表示清除)。
|
||||
func (db *DB) SetUserColor(userID int64, color string) error {
|
||||
c, err := NormalizeColor(color)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := db.sql.Exec(`UPDATE users SET color=? WHERE id=?`, c, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCustomerAssignee 把客户指派给某个打手;userID=0 表示取消指派。
|
||||
func (db *DB) SetCustomerAssignee(customerID, userID int64) error {
|
||||
var v any
|
||||
if userID > 0 {
|
||||
if _, err := db.UserByID(userID); err != nil {
|
||||
return errors.New("打手不存在")
|
||||
}
|
||||
v = userID
|
||||
}
|
||||
res, err := db.sql.Exec(`UPDATE customers SET assignee_id=?, updated_at=? WHERE id=?`, v, nowStamp(), customerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveUsers 返回在用的管理员(打手候选)。
|
||||
func (db *DB) ActiveUsers() ([]User, error) {
|
||||
rows, err := db.sql.Query(`SELECT ` + userCols + ` FROM users WHERE is_active=1 ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []User{}
|
||||
for rows.Next() {
|
||||
u, err := scanUser(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Authenticate 校验用户名口令。
|
||||
|
||||
Reference in New Issue
Block a user