团队管理:成员擅长标签
- team_members 新增 specialty 列(老库自动 ALTER 迁移,幂等) - 新接口:GET /api/teams/:id/members 成员列表(仅成员可看)、PUT /api/teams/:id/members/:uid 更新擅长(创建者可改他人,成员仅改自己);邀请接口支持带擅长 - TeamDialog 重构:团队卡片内成员列表(头像/名称/邮箱/角色/彩色擅长 chip,超 3 个折叠 +N)、行内擅长编辑器(10 个预设点选 + 自定义输入)、邀请时顺手点选擅长预设 - 文档 20-account 同步更新(含活动栏新导航说明) - curl E2E:邀请带擅长/创建者改他人/自改/越权与非成员 403 全部通过;53 前端测试 + Go 测试全绿
This commit is contained in:
1 parent
6d72264793
commit
c6868b0ddd
9 files changed
+451
-48
No files matched your search
@@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS team_members (
|
||||
team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
specialty TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (team_id, user_id)
|
||||
);
|
||||
@@ -107,6 +108,8 @@ CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_id);
|
||||
if _, err := s.db.Exec(`ALTER TABLE workspaces ADD COLUMN mock_token TEXT`); err == nil {
|
||||
_, _ = s.db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_mock_token ON workspaces(mock_token)`)
|
||||
}
|
||||
// 老库补擅长列(幂等)
|
||||
_, _ = s.db.Exec(`ALTER TABLE team_members ADD COLUMN specialty TEXT NOT NULL DEFAULT ''`)
|
||||
rows, err := s.db.Query(`SELECT id FROM workspaces WHERE mock_token IS NULL OR mock_token = ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -289,14 +292,68 @@ WHERE m.user_id = ? ORDER BY t.id`, userID)
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) AddTeamMember(teamID int64, email string) (*User, error) {
|
||||
// TeamMember 团队成员(含擅长标签,多个以空白或顿号分隔)
|
||||
type TeamMember struct {
|
||||
UserID int64 `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Specialty string `json:"specialty"`
|
||||
}
|
||||
|
||||
// TeamMembers 团队成员列表:创建者在最前,其余按加入时间
|
||||
func (s *Store) TeamMembers(teamID int64) ([]TeamMember, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT m.user_id, IFNULL(u.name, ''), IFNULL(u.email, ''), m.role, m.specialty
|
||||
FROM team_members m LEFT JOIN users u ON u.id = m.user_id
|
||||
WHERE m.team_id = ?
|
||||
ORDER BY CASE m.role WHEN 'owner' THEN 0 ELSE 1 END, m.created_at`, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []TeamMember
|
||||
for rows.Next() {
|
||||
var m TeamMember
|
||||
if err := rows.Scan(&m.UserID, &m.Name, &m.Email, &m.Role, &m.Specialty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// IsTeamMember 是否为团队成员(创建者亦为成员)
|
||||
func (s *Store) IsTeamMember(teamID, userID int64) (bool, error) {
|
||||
var one int
|
||||
err := s.db.QueryRow(`SELECT 1 FROM team_members WHERE team_id = ? AND user_id = ?`, teamID, userID).Scan(&one)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// UpdateMemberSpecialty 更新成员擅长(须先确认调用者是创建者或本人)
|
||||
func (s *Store) UpdateMemberSpecialty(teamID, userID int64, specialty string) error {
|
||||
res, err := s.db.Exec(`UPDATE team_members SET specialty = ? WHERE team_id = ? AND user_id = ?`, specialty, teamID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) AddTeamMember(teamID int64, email string, specialty string) (*User, error) {
|
||||
u, err := s.UserByEmail(email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = s.db.Exec(
|
||||
`INSERT OR IGNORE INTO team_members(team_id, user_id, role, created_at) VALUES(?,?,'member',?)`,
|
||||
teamID, u.ID, time.Now().Unix(),
|
||||
`INSERT OR IGNORE INTO team_members(team_id, user_id, role, specialty, created_at) VALUES(?,?,'member',?,?)`,
|
||||
teamID, u.ID, specialty, time.Now().Unix(),
|
||||
)
|
||||
return u, err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user