feat: implement first batch im enhancements

This commit is contained in:
richjohnrambo
2026-08-21 11:42:45 +08:00
parent 14abe58b88
commit c89c466387
16 changed files with 2629 additions and 130 deletions
+349
View File
@@ -2307,6 +2307,74 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
return members, nextCursor, nil
}
func (a *adapter) TenantOrganizationMembersAll(id t.TenantID, uid t.Uid, departmentID, queryText, cursor string, limit int) ([]t.OrganizationMember, string, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, "", err
}
if tenantAdp != a {
return tenantAdp.TenantOrganizationMembersAll(id, uid, departmentID, queryText, cursor, limit)
}
}
if err := validTenant(id); err != nil || uid.IsZero() {
return nil, "", t.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
where := []string{"m.employment_state='active'"}
args := []any{}
if strings.TrimSpace(departmentID) != "" {
where = append(where, "m.department_id=?")
args = append(args, strings.TrimSpace(departmentID))
}
if q := strings.TrimSpace(queryText); q != "" {
like := "%" + q + "%"
where = append(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)
}
if cursor != "" {
parsed, err := strconv.ParseInt(cursor, 10, 64)
if err != nil || parsed <= 0 {
return nil, "", t.ErrMalformed
}
where = append(where, "m.id>?")
args = append(args, parsed)
}
args = append(args, limit+1)
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
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
WHERE `+strings.Join(where, " AND ")+`
ORDER BY m.sort_order ASC,m.id ASC LIMIT ?`, args...)
if err != nil {
return nil, "", err
}
defer rows.Close()
members := make([]t.OrganizationMember, 0, limit)
nextCursor := ""
for rows.Next() {
member, err := scanOrganizationMember(rows)
if err != nil {
return nil, "", err
}
if len(members) == limit {
nextCursor = strconv.FormatInt(member.ID, 10)
continue
}
members = append(members, *member)
}
if err = rows.Err(); err != nil {
return nil, "", err
}
return members, nextCursor, nil
}
func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText, resultTypes, cursor string, limit int) ([]t.OrganizationSearchItem, string, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
@@ -3547,6 +3615,37 @@ func (a *adapter) TenantMonthlyReportSubmit(id t.TenantID, uid t.Uid, reportID s
return a.TenantMonthlyReportGet(id, uid, reportID)
}
func (a *adapter) TenantMonthlyReportReopen(id t.TenantID, uid t.Uid, reportID string, version int) (*t.MonthlyReport, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantMonthlyReportReopen(id, uid, reportID, version)
}
}
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
res, err := a.db.ExecContext(ctx, `UPDATE im_monthly_reports
SET status=?,submitted_at=NULL,version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE report_id=? AND author_id=? AND status=? AND version=?`,
t.DailyReportStatusDraft, reportID, decodeUID(uid), t.DailyReportStatusSubmitted, version)
if err != nil {
return nil, err
}
affected, _ := res.RowsAffected()
if affected == 0 {
return nil, t.ErrInvalidResponse
}
return a.TenantMonthlyReportGet(id, uid, reportID)
}
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ string) (*t.MonthlyReportStats, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
@@ -4627,6 +4726,213 @@ func kvMapFromJSON(value any) t.KVMap {
}
}
func (a *adapter) TenantConversationSettingsGet(id t.TenantID, uid t.Uid, topic string) (*t.ConversationSettings, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantConversationSettingsGet(id, uid, topic)
}
}
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
settings, err := a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
if err == sql.ErrNoRows {
settingsID, idErr := nextSnowflakeID()
if idErr != nil {
return nil, idErr
}
now := t.TimeNow()
_, err = a.db.ExecContext(ctx, `INSERT IGNORE INTO im_conversation_settings
(id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?)`,
settingsID, decodeUID(uid), strings.TrimSpace(topic), false, false, "", "default", 1, now, now)
if err != nil {
return nil, err
}
return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
}
if err != nil {
return nil, err
}
return settings, nil
}
func (a *adapter) TenantConversationSettingsUpdate(id t.TenantID, uid t.Uid, topic string, patch t.ConversationSettingsPatch) (*t.ConversationSettings, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantConversationSettingsUpdate(id, uid, topic, patch)
}
}
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" || patch.Version <= 0 {
return nil, t.ErrMalformed
}
if _, err := a.TenantConversationSettingsGet(id, uid, topic); err != nil {
return nil, err
}
sets := []string{}
args := []any{}
if patch.Pinned != nil {
sets = append(sets, "pinned=?")
args = append(args, *patch.Pinned)
}
if patch.Muted != nil {
sets = append(sets, "muted=?")
args = append(args, *patch.Muted)
}
if patch.Remark != nil {
sets = append(sets, "remark=?")
args = append(args, strings.TrimSpace(*patch.Remark))
}
if patch.MessageRetention != nil {
sets = append(sets, "message_retention=?")
args = append(args, strings.TrimSpace(*patch.MessageRetention))
}
if len(sets) == 0 {
return a.TenantConversationSettingsGet(id, uid, topic)
}
sets = append(sets, "version=version+1", "updated_at=CURRENT_TIMESTAMP(3)")
args = append(args, decodeUID(uid), strings.TrimSpace(topic), patch.Version)
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
res, err := a.db.ExecContext(ctx, `UPDATE im_conversation_settings SET `+strings.Join(sets, ",")+`
WHERE user_id=? AND topic=? AND version=?`, args...)
if err != nil {
return nil, err
}
affected, _ := res.RowsAffected()
if affected == 0 {
return nil, t.ErrInvalidResponse
}
return a.conversationSettingsByKey(ctx, uid, strings.TrimSpace(topic))
}
func (a *adapter) TenantTopicDraftGet(id t.TenantID, uid t.Uid, topic string) (*t.TopicDraft, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantTopicDraftGet(id, uid, topic)
}
}
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
draft, err := a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
if err == sql.ErrNoRows {
return nil, t.ErrNotFound
}
return draft, err
}
func (a *adapter) TenantTopicDraftPut(id t.TenantID, uid t.Uid, topic, content string, entities []byte, version int) (*t.TopicDraft, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantTopicDraftPut(id, uid, topic, content, entities, version)
}
}
if err := validTenant(id); err != nil || uid.IsZero() || strings.TrimSpace(topic) == "" || version < 0 {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
if version == 0 {
draftID, err := nextSnowflakeID()
if err != nil {
return nil, err
}
now := t.TimeNow()
_, err = a.db.ExecContext(ctx, `INSERT INTO im_topic_drafts
(id,user_id,topic,content,entities_json,version,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE content=VALUES(content),entities_json=VALUES(entities_json),
version=version+1,updated_at=VALUES(updated_at)`,
draftID, decodeUID(uid), strings.TrimSpace(topic), content, entities, 1, now, now)
if err != nil {
return nil, err
}
return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
}
res, err := a.db.ExecContext(ctx, `UPDATE im_topic_drafts
SET content=?,entities_json=?,version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE user_id=? AND topic=? AND version=?`,
content, entities, decodeUID(uid), strings.TrimSpace(topic), version)
if err != nil {
return nil, err
}
affected, _ := res.RowsAffected()
if affected == 0 {
return nil, t.ErrInvalidResponse
}
return a.topicDraftByKey(ctx, uid, strings.TrimSpace(topic))
}
func (a *adapter) conversationSettingsByKey(ctx context.Context, uid t.Uid, topic string) (*t.ConversationSettings, error) {
row := struct {
ID int64
UserID int64 `db:"user_id"`
Topic string `db:"topic"`
Pinned bool
Muted bool
Remark string
MessageRetention string `db:"message_retention"`
Version int `db:"version"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}{}
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,pinned,muted,remark,message_retention,version,created_at,updated_at
FROM im_conversation_settings WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil {
return nil, err
}
return &t.ConversationSettings{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic,
Pinned: row.Pinned, Muted: row.Muted, Remark: row.Remark, MessageRetention: row.MessageRetention,
Version: row.Version, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
}
func (a *adapter) topicDraftByKey(ctx context.Context, uid t.Uid, topic string) (*t.TopicDraft, error) {
row := struct {
ID int64
UserID int64 `db:"user_id"`
Topic string `db:"topic"`
Content string
EntitiesJSON []byte `db:"entities_json"`
Version int `db:"version"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}{}
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT id,user_id,topic,content,entities_json,version,created_at,updated_at
FROM im_topic_drafts WHERE user_id=? AND topic=?`, decodeUID(uid), topic); err != nil {
return nil, err
}
return &t.TopicDraft{ID: row.ID, UserID: store.EncodeUid(row.UserID), UserIDRaw: row.UserID, Topic: row.Topic,
Content: row.Content, EntitiesJSON: row.EntitiesJSON, Version: row.Version, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, nil
}
func (a *adapter) TenantFavoriteCreate(id t.TenantID, fav *t.FavoriteItem) (*t.FavoriteItem, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
@@ -5821,6 +6127,49 @@ func (a *adapter) TenantRealNameCreate(id t.TenantID, rv *t.RealNameVerification
return err
}
func (a *adapter) TenantRealNameAppealCreate(id t.TenantID, appeal *t.RealNameAppeal) (*t.RealNameAppeal, error) {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)
if err != nil {
return nil, err
}
if tenantAdp != a {
return tenantAdp.TenantRealNameAppealCreate(id, appeal)
}
}
if err := validTenant(id); err != nil || appeal == nil || appeal.TenantID != id || appeal.UserID.IsZero() ||
strings.TrimSpace(appeal.AppealID) == "" || strings.TrimSpace(appeal.Reason) == "" {
return nil, t.ErrMalformed
}
appealID, err := nextSnowflakeID()
if err != nil {
return nil, err
}
now := t.TimeNow()
if appeal.Status == "" {
appeal.Status = "pending"
}
appeal.ID = appealID
appeal.CreatedAt = now
appeal.UpdatedAt = now
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
_, err = a.db.ExecContext(ctx, `INSERT INTO im_realname_appeals
(id,appeal_id,user_id,reason,attachments_json,status,review_comment,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?)`,
appeal.ID, appeal.AppealID, decodeUID(appeal.UserID), appeal.Reason, appeal.AttachmentsJSON,
appeal.Status, appeal.ReviewComment, appeal.CreatedAt, appeal.UpdatedAt)
if isDupe(err) {
return nil, t.ErrDuplicate
}
if err != nil {
return nil, err
}
return appeal, nil
}
func (a *adapter) TenantActionTokenCreate(id t.TenantID, token *t.ActionToken) error {
if !a.tenantScopedDB {
tenantAdp, err := a.routedTenantAdapter(id)