feat: switch client API branding to MeetingIM

This commit is contained in:
richjohnrambo
2026-08-31 01:22:53 +08:00
parent c6a243ee59
commit 9f647de4d0
48 changed files with 717 additions and 271 deletions
+35
View File
@@ -275,6 +275,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
if err = ensureIMFileReviewTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure file review schema: " + err.Error())
}
if err = ensureIMOrganizationVisibilityTables(a.db); err != nil {
return errors.New("mysql adapter failed to ensure organization visibility schema: " + err.Error())
}
if err = ensureSnowflakePrimaryKeys(a.db); err != nil {
return errors.New("mysql adapter failed to ensure snowflake primary keys: " + err.Error())
}
@@ -507,6 +510,10 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
db.Close()
return nil, nil, err
}
if err = ensureIMOrganizationVisibilityTables(db); err != nil {
db.Close()
return nil, nil, err
}
if err = ensureSnowflakePrimaryKeys(db); err != nil {
db.Close()
return nil, nil, err
@@ -1187,6 +1194,18 @@ func createIMOrganizationTables(tx *sql.Tx) error {
INDEX idx_im_org_members_display_name(display_name),
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES im_org_departments(department_id) ON DELETE CASCADE
)`,
`CREATE TABLE im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`,
}
for _, stmt := range statements {
if _, err := tx.Exec(stmt); err != nil {
@@ -1196,6 +1215,22 @@ func createIMOrganizationTables(tx *sql.Tx) error {
return nil
}
func ensureIMOrganizationVisibilityTables(exec sqlExecutor) error {
_, err := exec.Exec(`CREATE TABLE IF NOT EXISTS im_org_visibility_overrides(
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_org_visibility_user(user_id),
INDEX idx_im_org_visibility_scope(visibility_scope,state)
)`)
return err
}
func createIMMeetingTables(tx *sql.Tx) error {
statements := []string{
`CREATE TABLE im_meetings(
+174 -24
View File
@@ -2681,12 +2681,28 @@ func (a *adapter) TenantOrganizationDepartments(id t.TenantID, uid t.Uid, parent
if cancel != nil {
defer cancel()
}
rows, err := a.db.QueryxContext(ctx, `SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
cte, cteArgs := visibility.departmentCTE()
memberCountSQL, memberCountArgs := visibility.memberCountSQL("m")
if visibility.Scope != orgVisibilityAll {
where = append(where, "d.department_id IN (SELECT department_id FROM visible_depts)")
}
queryArgs := append([]any{}, cteArgs...)
queryArgs = append(queryArgs, memberCountArgs...)
queryArgs = append(queryArgs, args...)
childCountSQL := `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active')`
if visibility.Scope != orgVisibilityAll {
childCountSQL = `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active' AND child.department_id IN (SELECT department_id FROM visible_depts))`
}
rows, err := a.db.QueryxContext(ctx, cte+`SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
`+childCountSQL+` AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id AND m.employment_state='active'`+memberCountSQL+`) AS member_count
FROM im_org_departments AS d
WHERE `+strings.Join(where, " AND ")+`
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, args...)
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, queryArgs...)
if err != nil {
return nil, "", err
}
@@ -2726,7 +2742,7 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
if limit <= 0 || limit > 100 {
limit = 50
}
where := []string{"m.department_id=?"}
where := []string{"m.department_id=?", "m.employment_state='active'"}
args := []any{strings.TrimSpace(departmentID)}
if cursor != "" {
parsed, err := strconv.ParseInt(cursor, 10, 64)
@@ -2741,6 +2757,15 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
if cancel != nil {
defer cancel()
}
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
where = append(where, visibilitySQL)
args = append(args, visibilityArgs...)
}
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
FROM im_org_members AS m
@@ -2809,6 +2834,15 @@ func (a *adapter) TenantOrganizationMembersAll(id t.TenantID, uid t.Uid, departm
if cancel != nil {
defer cancel()
}
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
where = append(where, visibilitySQL)
args = append(args, visibilityArgs...)
}
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
FROM im_org_members AS m
@@ -2866,29 +2900,45 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
return nil, "", t.ErrMalformed
}
like := "%" + strings.TrimSpace(queryText) + "%"
parts := make([]string, 0, 2)
args := make([]any, 0, 6)
if includeDepartments {
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
FROM im_org_departments AS d WHERE d.state='active' AND d.name LIKE ?`)
args = append(args, like)
}
if includeUsers {
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
FROM im_org_members AS m
WHERE m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?`)
args = append(args, like, like, like, like)
}
args = append(args, offset, limit+1)
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
visibility, err := a.organizationVisibility(ctx, uid)
if err != nil {
return nil, "", err
}
cte, cteArgs := visibility.departmentCTE()
parts := make([]string, 0, 2)
args := make([]any, 0, 10)
args = append(args, cteArgs...)
if includeDepartments {
departmentWhere := "d.state='active' AND d.name LIKE ?"
if visibility.Scope != orgVisibilityAll {
departmentWhere += " AND d.department_id IN (SELECT department_id FROM visible_depts)"
}
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
FROM im_org_departments AS d WHERE `+departmentWhere)
args = append(args, like)
}
if includeUsers {
userWhere := "(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?) AND m.employment_state='active'"
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
if visibilitySQL != "" {
userWhere += " AND " + visibilitySQL
}
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
FROM im_org_members AS m
WHERE `+userWhere)
args = append(args, like, like, like, like)
args = append(args, visibilityArgs...)
}
args = append(args, offset, limit+1)
rows, err := a.db.QueryxContext(ctx, cte+`SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
ORDER BY result_type ASC,id ASC LIMIT ?,?`, args...)
if err != nil {
return nil, "", err
@@ -2913,6 +2963,106 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
return items, nextCursor, nil
}
const (
orgVisibilityAll = "all"
orgVisibilityFriends = "friends"
orgVisibilitySelf = "self"
)
type organizationVisibility struct {
Scope string
UserID int64
}
func (a *adapter) organizationVisibility(ctx context.Context, uid t.Uid) (organizationVisibility, error) {
userID := decodeUID(uid)
visibility := organizationVisibility{Scope: orgVisibilitySelf, UserID: userID}
if userID <= 0 {
return visibility, t.ErrMalformed
}
var override string
err := a.db.QueryRowxContext(ctx, `SELECT visibility_scope FROM im_org_visibility_overrides
WHERE user_id=? AND state='active' LIMIT 1`, userID).Scan(&override)
if err != nil && err != sql.ErrNoRows {
return visibility, err
}
if err == nil {
switch strings.TrimSpace(override) {
case orgVisibilityAll, orgVisibilityFriends, orgVisibilitySelf:
visibility.Scope = strings.TrimSpace(override)
return visibility, nil
default:
return visibility, t.ErrMalformed
}
}
var phone sql.NullString
err = a.db.QueryRowxContext(ctx, `SELECT phone FROM im_users WHERE id=? AND state<>? LIMIT 1`, userID, t.StateDeleted).Scan(&phone)
if err == sql.ErrNoRows {
return visibility, t.ErrNotFound
}
if err != nil {
return visibility, err
}
var inviteUsed int
err = a.db.QueryRowxContext(ctx, `SELECT EXISTS(SELECT 1 FROM im_registration_invite_uses WHERE user_id=?)`, userID).Scan(&inviteUsed)
if err != nil {
return visibility, err
}
if inviteUsed > 0 {
visibility.Scope = orgVisibilitySelf
return visibility, nil
}
if phone.Valid && strings.TrimSpace(phone.String) != "" {
visibility.Scope = orgVisibilityFriends
}
return visibility, nil
}
func (v organizationVisibility) memberWhereSQL(alias string) (string, []any) {
alias = strings.TrimSpace(alias)
if alias == "" {
alias = "m"
}
switch v.Scope {
case orgVisibilityAll:
return "", nil
case orgVisibilityFriends:
return "(" + alias + ".user_id=? OR EXISTS (SELECT 1 FROM im_friendships AS fs WHERE fs.user_id=? AND fs.peer_id=" + alias + ".user_id AND fs.status=?))",
[]any{v.UserID, v.UserID, t.RelationshipFriend}
default:
return alias + ".user_id=?", []any{v.UserID}
}
}
func (v organizationVisibility) memberCountSQL(alias string) (string, []any) {
where, args := v.memberWhereSQL(alias)
if where == "" {
return "", nil
}
return " AND " + where, args
}
func (v organizationVisibility) departmentCTE() (string, []any) {
if v.Scope == orgVisibilityAll {
return "", nil
}
memberWhere, args := v.memberWhereSQL("m")
return `WITH RECURSIVE visible_member_depts AS (
SELECT DISTINCT m.department_id FROM im_org_members AS m
WHERE m.employment_state='active' AND ` + memberWhere + `
), visible_depts AS (
SELECT d.department_id,d.parent_id FROM im_org_departments AS d
JOIN visible_member_depts AS vmd ON vmd.department_id=d.department_id
WHERE d.state='active'
UNION DISTINCT
SELECT parent.department_id,parent.parent_id FROM im_org_departments AS parent
JOIN visible_depts AS child ON child.parent_id=parent.department_id
WHERE parent.state='active'
) `, args
}
func organizationSearchIncludes(resultTypes string) (bool, bool) {
if strings.TrimSpace(resultTypes) == "" {
return true, true