feat: auto-friend invite bound users

This commit is contained in:
richjohnrambo
2026-09-11 10:46:20 +08:00
parent 926b4bbd81
commit b9dc56eccf
4 changed files with 104 additions and 23 deletions
+54 -11
View File
@@ -5,6 +5,7 @@ package mysql
import (
"database/sql"
"strconv"
"strings"
"time"
@@ -60,8 +61,8 @@ func (a *adapter) TenantRegistrationInviteCreate(id t.TenantID, code *t.Registra
defer cancel()
}
_, err = a.registrationInviteControlDB().ExecContext(ctx, `INSERT INTO im_registration_invite_codes
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,?,?,0,NULL,?,?,?,?)`, code.ID, id, code.BatchID, code.Code, code.Status, code.ExpiresAt,
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,?,?,1,0,NULL,?,?,?,?)`, code.ID, id, code.BatchID, code.Code, code.Status, code.ExpiresAt,
code.CreatedBy, code.Remark, code.CreatedAt, code.UpdatedAt)
if isDupe(err) {
return t.ErrDuplicate
@@ -128,8 +129,8 @@ func (a *adapter) TenantRegistrationInviteCreateBatch(id t.TenantID, batch *t.Re
codes[i].Remark = batch.Remark
codes[i].CreatedAt, codes[i].UpdatedAt = now, now
if _, err = tx.ExecContext(ctx, `INSERT INTO im_registration_invite_codes
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,?,?,0,NULL,?,?,?,?)`, codes[i].ID, id, batch.ID, codes[i].Code, codes[i].Status,
(id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,?,?,0,0,NULL,?,?,?,?)`, codes[i].ID, id, batch.ID, codes[i].Code, codes[i].Status,
codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, now, now); err != nil {
if isDupe(err) {
return t.ErrDuplicate
@@ -156,7 +157,7 @@ func (a *adapter) TenantRegistrationInviteGetByCode(id t.TenantID, value string)
defer cancel()
}
var code t.RegistrationInviteCode
err = a.registrationInviteControlDB().GetContext(ctx, &code, `SELECT c.id,c.tenant_id,c.batch_id,c.code,c.status,c.expires_at,
err = a.registrationInviteControlDB().GetContext(ctx, &code, `SELECT c.id,c.tenant_id,c.batch_id,c.code,c.status,c.expires_at,c.max_uses,c.bound_user_id,
c.created_by,c.remark,c.created_at,c.updated_at,c.usage_count,c.last_used_at
FROM im_registration_invite_codes AS c
WHERE c.tenant_id=? AND c.code=? LIMIT 1`, id, value)
@@ -227,7 +228,7 @@ func (a *adapter) TenantRegistrationInviteList(id t.TenantID, opts t.Registratio
}
queryArgs := append(append([]any{}, args...), opts.Limit, opts.Offset)
var items []t.RegistrationInviteCode
err = controlDB.SelectContext(ctx, &items, `SELECT c.id,c.tenant_id,c.batch_id,c.code,c.status,c.expires_at,
err = controlDB.SelectContext(ctx, &items, `SELECT c.id,c.tenant_id,c.batch_id,c.code,c.status,c.expires_at,c.max_uses,c.bound_user_id,
c.created_by,c.remark,c.created_at,c.updated_at,c.usage_count,c.last_used_at
FROM im_registration_invite_codes AS c
WHERE `+filter+` ORDER BY c.created_at DESC,c.id DESC LIMIT ? OFFSET ?`, queryArgs...)
@@ -345,15 +346,17 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string,
return t.ErrMalformed
}
var row struct {
ID int64 `db:"id"`
Status t.RegistrationInviteStatus `db:"status"`
ExpiresAt time.Time `db:"expires_at"`
ID int64 `db:"id"`
Status t.RegistrationInviteStatus `db:"status"`
ExpiresAt time.Time `db:"expires_at"`
MaxUses int `db:"max_uses"`
BoundUserID *int64 `db:"bound_user_id"`
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
err = a.registrationInviteControlDB().GetContext(ctx, &row, `SELECT id,status,expires_at
err = a.registrationInviteControlDB().GetContext(ctx, &row, `SELECT id,status,expires_at,max_uses,bound_user_id
FROM im_registration_invite_codes WHERE tenant_id=? AND code=? LIMIT 1`, id, value)
if err == sql.ErrNoRows {
return t.ErrInviteInvalid
@@ -384,6 +387,16 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string,
_ = tx.Rollback()
}
}()
if row.MaxUses > 0 {
var usedCount int
if err = tx.GetContext(txCtx, &usedCount,
"SELECT COUNT(*) FROM im_registration_invite_uses WHERE invite_code_id=?", row.ID); err != nil {
return err
}
if usedCount >= row.MaxUses {
return t.ErrInviteInvalid
}
}
useID, err := nextSnowflakeID()
if err != nil {
return err
@@ -403,9 +416,17 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string,
return err
}
}
if row.BoundUserID != nil && *row.BoundUserID > 0 {
boundUID := t.ParseUserId(strconv.FormatInt(*row.BoundUserID, 10))
if !boundUID.IsZero() && boundUID != uid {
if err = upsertFriendshipPairTx(tx, uid, boundUID, uid.P2PName(boundUID), now); err != nil {
return err
}
}
}
eventID := t.StableEventID("registration-invite-use", id, row.ID, store.DecodeUid(uid))
if err = insertBusinessEventTx(tx, eventID, "user", uid.UserId(), uid, "registration.invite_used", map[string]any{
"tenant_id": id, "invite_code_id": row.ID, "user_id": store.DecodeUid(uid),
"tenant_id": id, "invite_code_id": row.ID, "user_id": store.DecodeUid(uid), "bound_user_id": row.BoundUserID,
"request_id": requestID, "used_at": now,
}); err != nil {
return err
@@ -413,6 +434,28 @@ func (a *adapter) TenantRegistrationInviteRecordUse(id t.TenantID, value string,
return tx.Commit()
}
func upsertFriendshipPairTx(tx *sqlx.Tx, uid1, uid2 t.Uid, topic string, now time.Time) error {
if uid1.IsZero() || uid2.IsZero() || uid1 == uid2 {
return t.ErrMalformed
}
if topic == "" {
topic = uid1.P2PName(uid2)
}
for _, pair := range [][2]t.Uid{{uid1, uid2}, {uid2, uid1}} {
friendshipID, err := nextSnowflakeID()
if err != nil {
return err
}
if _, err = tx.Exec(
"INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil {
return err
}
}
return nil
}
// TenantRegistrationInviteSyncUsage projects the tenant usage aggregate to the
// control database. The overwrite is idempotent, so outbox retries are safe.
func (a *adapter) TenantRegistrationInviteSyncUsage(id t.TenantID, codeID int64) error {