118 lines
3.8 KiB
Go

package user
import (
"context"
"database/sql"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
userservice "hyapp/services/user-service/internal/service/user"
)
// SetUserStatus 原子更新用户主状态,并在非 active 状态下吊销该用户所有 active refresh session。
func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) {
if r == nil || r.db == nil {
return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.Normalize(command.AppCode)
if appCode == "" {
appCode = appcode.FromContext(ctx)
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
defer tx.Rollback()
var oldStatus string
if err := tx.QueryRowContext(ctx, `
SELECT status
FROM users
WHERE app_code = ? AND user_id = ?
FOR UPDATE
`, appCode, command.TargetUserID).Scan(&oldStatus); err != nil {
if err == sql.ErrNoRows {
return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.NotFound, "user not found")
}
return userservice.UserStatusPersistenceResult{}, err
}
_, err = tx.ExecContext(ctx, `
UPDATE users
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND user_id = ?
`, string(command.Status), command.NowMs, appCode, command.TargetUserID)
if err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_status_change_logs (
app_code, target_user_id, old_status, new_status, operator_type, operator_user_id, reason, request_id, created_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, appCode, command.TargetUserID, oldStatus, string(command.Status), command.OperatorType, command.OperatorUserID, command.Reason, command.RequestID, command.NowMs); err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
sessionIDs := make([]string, 0)
if command.Status != userdomain.StatusActive {
rows, err := tx.QueryContext(ctx, `
SELECT session_id
FROM auth_sessions
WHERE app_code = ? AND user_id = ? AND expires_at_ms > ?
FOR UPDATE
`, appCode, command.TargetUserID, command.NowMs)
if err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
for rows.Next() {
var sessionID string
if err := rows.Scan(&sessionID); err != nil {
_ = rows.Close()
return userservice.UserStatusPersistenceResult{}, err
}
sessionIDs = append(sessionIDs, sessionID)
}
if err := rows.Close(); err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
if _, err := tx.ExecContext(ctx, `
UPDATE auth_sessions
SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL
`, command.NowMs, userStatusRevokeReason(command.Status), command.RequestID, revokedBy(command.OperatorUserID), command.NowMs, appCode, command.TargetUserID); err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
}
if err := tx.Commit(); err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
user, err := r.GetUser(appcode.WithContext(ctx, appCode), command.TargetUserID)
if err != nil {
return userservice.UserStatusPersistenceResult{}, err
}
return userservice.UserStatusPersistenceResult{User: user, RevokedSessionIDs: sessionIDs}, nil
}
func userStatusRevokeReason(status userdomain.Status) string {
switch status {
case userdomain.StatusBanned:
return "USER_BANNED"
case userdomain.StatusDisabled:
return "USER_DISABLED"
default:
return "USER_STATUS_CHANGED"
}
}
func revokedBy(operatorUserID int64) string {
if operatorUserID > 0 {
return "user_status_operator"
}
return "user_status"
}