feat: assign IM user account numbers

This commit is contained in:
richjohnrambo
2026-09-10 21:51:23 +08:00
parent 51fee50343
commit 928a45ecfb
6 changed files with 112 additions and 27 deletions
+4 -3
View File
@@ -95,22 +95,23 @@ func TestGroupMessageQuerySupportsOperationalFilters(t *testing.T) {
}
func TestUserQuerySupportsKeywordAndUserIDs(t *testing.T) {
req := httptest.NewRequest("GET", "/?keyword=alice&user_ids=101,202", nil)
req := httptest.NewRequest("GET", "/?keyword=alice&user_ids=101,202&account_no=1000", nil)
query, countQuery, args, _, _, err := buildInternalAdminListQuery(req, usersInternalQuerySpec(), "")
if err != nil {
t.Fatal(err)
}
for _, clause := range []string{"CONCAT_WS(' ',u.username,p.nickname,u.phone) LIKE ?", "u.id IN (?,?)"} {
for _, clause := range []string{"CONCAT_WS(' ',u.username,p.nickname,u.phone,CAST(u.account_no AS CHAR)) LIKE ?", "u.id IN (?,?)", "u.account_no = ?"} {
if !strings.Contains(query, clause) || !strings.Contains(countQuery, clause) {
t.Fatalf("user query missing %q: %s", clause, query)
}
}
want := []any{int64(101), int64(202), "%alice%"}
want := []any{int64(101), int64(202), int64(1000), "%alice%"}
if !reflect.DeepEqual(args, want) {
t.Fatalf("unexpected filter arguments: got %#v, want %#v", args, want)
}
for _, field := range []string{
"AS avatar",
"u.account_no AS display_no",
"AS register_type",
"im_registration_invite_uses",
"im_fileuploads AS avatar",
+42 -1
View File
@@ -9,6 +9,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"sort"
"strconv"
@@ -763,6 +764,7 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE im_users(
id BIGINT NOT NULL,
account_no INT UNSIGNED NOT NULL DEFAULT 0,
username VARCHAR(32) NOT NULL,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
@@ -775,6 +777,7 @@ func (a *adapter) CreateDb(reset bool) error {
public JSON,
trusted JSON,
PRIMARY KEY(id),
UNIQUE INDEX uk_im_users_account_no(account_no),
UNIQUE INDEX users_username(username),
UNIQUE INDEX users_phone(phone),
INDEX users_state_stateat(state, stateat),
@@ -2230,8 +2233,18 @@ func (a *adapter) UserCreate(user *t.User) error {
user.Phone = canonicalPhone
phone = canonicalPhone
}
if _, err = tx.Exec("INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?,?)",
releaseAccountNoLock, err := lockUserAccountNoSequence(ctx, tx)
if err != nil {
return err
}
defer releaseAccountNoLock()
accountNo, err := nextUserAccountNo(ctx, tx)
if err != nil {
return err
}
if _, err = tx.Exec("INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?,?,?)",
decoded_uid,
accountNo,
user.Username,
user.CreatedAt,
user.UpdatedAt,
@@ -2260,6 +2273,34 @@ func (a *adapter) UserCreate(user *t.User) error {
return tx.Commit()
}
func lockUserAccountNoSequence(ctx context.Context, tx *sqlx.Tx) (func(), error) {
var locked sql.NullInt64
if err := tx.QueryRowContext(ctx, "SELECT GET_LOCK(CONCAT(DATABASE(), ':im_users_account_no'), 10)").Scan(&locked); err != nil {
return nil, err
}
if !locked.Valid || locked.Int64 != 1 {
return nil, fmt.Errorf("lock user account number sequence timeout")
}
return func() {
_, _ = tx.ExecContext(ctx, "SELECT RELEASE_LOCK(CONCAT(DATABASE(), ':im_users_account_no'))")
}, nil
}
func nextUserAccountNo(ctx context.Context, tx *sqlx.Tx) (int64, error) {
var current sql.NullInt64
err := tx.QueryRowContext(ctx, "SELECT account_no FROM im_users ORDER BY account_no DESC LIMIT 1 FOR UPDATE").Scan(&current)
if err == sql.ErrNoRows {
return 1000, nil
}
if err != nil {
return 0, err
}
if !current.Valid || current.Int64 < 999 {
return 1000, nil
}
return current.Int64 + 1, nil
}
// TenantAuthAddRecord adds the user's Basic authentication record.
func (a *adapter) TenantAuthAddRecord(tenantID t.TenantID, uid t.Uid,
authLvl auth.Level, secret []byte, expires time.Time) error {
+5 -5
View File
@@ -289,8 +289,8 @@ func TestTenantAuthIsolation(t *testing.T) {
userBID := userAID + 1
for index, userID := range []int64{userAID, userBID} {
tenantDB := quoteIdent(dbNames[index])
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) "+
"VALUES(?,?,?,?,?,?,?,?,?)", userID, "alice", now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil {
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted) "+
"VALUES(?,?,?,?,?,?,?,?,?,?)", userID, 1000+index, "alice", now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(userid,authlvl,secret) VALUES(?,?,?)",
@@ -360,9 +360,9 @@ func TestTenantUserSearchMatchesUsernamePhoneAndPublicAccount(t *testing.T) {
{targetID, "potter", "+8615500000002", `{"fn":"Potter"}`},
} {
if _, err := db.Exec(
`INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,public,trusted)
VALUES(?,?,?,?,?,?,?,?,?)`,
user.id, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, user.public, `{}`); err != nil {
`INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,public,trusted)
VALUES(?,?,?,?,?,?,?,?,?,?)`,
user.id, user.id%1000000, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, user.public, `{}`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(
+36
View File
@@ -88,6 +88,9 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
return err
}
}
if err = ensureUserAccountNoSchema(ctx, conn); err != nil {
return err
}
if profilesExist {
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "email"); err != nil {
return err
@@ -568,6 +571,39 @@ func identityCandidates(ctx context.Context, conn *sqlx.Conn, scoped bool, metho
return out, nil
}
func ensureUserAccountNoSchema(ctx context.Context, conn *sqlx.Conn) error {
if exists, err := identityColumnExists(ctx, conn, "im_users", "account_no"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_users ADD COLUMN account_no INT UNSIGNED NOT NULL DEFAULT 0 AFTER id"); err != nil {
return err
}
}
var start int64 = 999
if err := conn.QueryRowContext(ctx, "SELECT GREATEST(COALESCE(MAX(account_no), 999), 999) FROM im_users WHERE account_no>0").Scan(&start); err != nil {
return err
}
if _, err := conn.ExecContext(ctx, `
UPDATE im_users AS u
JOIN (
SELECT ordered.id, (@im_user_account_no := @im_user_account_no + 1) AS next_no
FROM (SELECT id FROM im_users WHERE account_no=0 ORDER BY createdat, id) AS ordered
CROSS JOIN (SELECT @im_user_account_no := ?) AS vars
) AS seq ON seq.id=u.id
SET u.account_no=seq.next_no
WHERE u.account_no=0`, start); err != nil {
return err
}
if exists, err := identityIndexExists(ctx, conn, "im_users", "uk_im_users_account_no"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "CREATE UNIQUE INDEX uk_im_users_account_no ON im_users(account_no)"); err != nil {
return fmt.Errorf("create unique user account number index: %w", err)
}
}
return nil
}
func identityTableExists(ctx context.Context, conn *sqlx.Conn, table string) (bool, error) {
var exists int
err := conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM information_schema.tables
+2 -2
View File
@@ -269,7 +269,7 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) {
func usersInternalQuerySpec() *internalQuerySpec {
return &internalQuerySpec{
Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted,
Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted,
p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,
COALESCE(
NULLIF(avatar.location,''),
@@ -288,7 +288,7 @@ func usersInternalQuerySpec() *internalQuerySpec {
From: "FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id LEFT JOIN im_user_security AS sec ON sec.user_id=u.id LEFT JOIN im_fileuploads AS avatar ON avatar.id=CAST(p.avatar_file_id AS UNSIGNED) AND p.avatar_file_id REGEXP '^[0-9]+$'",
DefaultSort: "ORDER BY u.id DESC",
DetailKey: "u.id",
Filters: []internalQueryFilter{{"user_id", "u.id", "=", "uid"}, {"user_ids", "u.id", "IN", "uid_list"}, {"keyword", "CONCAT_WS(' ',u.username,p.nickname,u.phone)", "LIKE", "string"}, {"nickname", "p.nickname", "LIKE", "string"}, {"auth_name", "u.username", "LIKE", "string"}, {"state", "u.state", "=", "int"}, {"created_at_start", "u.createdat", ">=", "time"}, {"created_at_end", "u.createdat", "<=", "time"}, {"last_seen_start", "u.lastseen", ">=", "time"}, {"last_seen_end", "u.lastseen", "<=", "time"}},
Filters: []internalQueryFilter{{"user_id", "u.id", "=", "uid"}, {"user_ids", "u.id", "IN", "uid_list"}, {"account_no", "u.account_no", "=", "int"}, {"keyword", "CONCAT_WS(' ',u.username,p.nickname,u.phone,CAST(u.account_no AS CHAR))", "LIKE", "string"}, {"nickname", "p.nickname", "LIKE", "string"}, {"auth_name", "u.username", "LIKE", "string"}, {"state", "u.state", "=", "int"}, {"created_at_start", "u.createdat", ">=", "time"}, {"created_at_end", "u.createdat", "<=", "time"}, {"last_seen_start", "u.lastseen", ">=", "time"}, {"last_seen_end", "u.lastseen", "<=", "time"}},
}
}
+7
View File
@@ -229,8 +229,15 @@ func serveInternalUserCreate(wrt http.ResponseWriter, req *http.Request, tenantI
writeInternalQueryError(wrt, req, err)
return
}
displayNo := int64(0)
if row, err := store.InternalAdminQueries.QueryOne(tenantID, "SELECT account_no FROM im_users WHERE id=?", []any{store.DecodeUid(created.Uid())}); err == nil {
displayNo, _ = numericID(row["account_no"])
} else {
logs.Warn.Printf("internal user create account number query failed tenant:%d user:%d err:%v", tenantID, store.DecodeUid(created.Uid()), err)
}
writeIMSuccess(wrt, req, map[string]any{
"user_id": store.DecodeUid(created.Uid()),
"display_no": displayNo,
"username": created.Username,
"nickname": body.Nickname,
"phone": created.Phone,