// Package device implements user-service push token persistence. package device import ( "context" "database/sql" "strings" "hyapp/pkg/appcode" userdomain "hyapp/services/user-service/internal/domain/user" ) // Repository stores App push tokens in MySQL. type Repository struct { db *sql.DB } // New creates a device repository over the shared user-service connection pool. func New(db *sql.DB) *Repository { return &Repository{db: db} } // BindPushToken binds a push token to the current user/device and removes stale ownership of the same token. func (r *Repository) BindPushToken(ctx context.Context, command userdomain.PushTokenCommand) error { appCode := normalizedAppCode(ctx, command.AppCode) tx, err := r.db.BeginTx(ctx, nil) if err != nil { return err } // 同一个系统 push token 只能投递一次;重新登录其他账号时删除旧归属,避免离线消息双发。 if _, err := tx.ExecContext(ctx, `DELETE FROM user_push_tokens WHERE app_code = ? AND provider = ? AND push_token = ? AND NOT (user_id = ? AND device_id = ?)`, appCode, command.Provider, command.PushToken, command.UserID, command.DeviceID, ); err != nil { _ = tx.Rollback() return err } if _, err := tx.ExecContext(ctx, `INSERT INTO user_push_tokens ( app_code, user_id, device_id, push_token, provider, platform, app_version, language, timezone, status, bound_at_ms, updated_at_ms, last_request_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE push_token = VALUES(push_token), provider = VALUES(provider), platform = VALUES(platform), app_version = VALUES(app_version), language = VALUES(language), timezone = VALUES(timezone), status = VALUES(status), updated_at_ms = VALUES(updated_at_ms), last_request_id = VALUES(last_request_id)`, appCode, command.UserID, command.DeviceID, command.PushToken, command.Provider, command.Platform, command.AppVersion, command.Language, command.Timezone, userdomain.PushTokenStatusActive, command.UpdatedAtMs, command.UpdatedAtMs, command.RequestID, ); err != nil { _ = tx.Rollback() return err } return tx.Commit() } // DeletePushToken marks one user's device token inactive. func (r *Repository) DeletePushToken(ctx context.Context, command userdomain.DeletePushTokenCommand) (bool, error) { appCode := normalizedAppCode(ctx, command.AppCode) query := `UPDATE user_push_tokens SET status = ?, updated_at_ms = ?, last_request_id = ? WHERE app_code = ? AND user_id = ? AND device_id = ? AND status = ?` args := []any{ userdomain.PushTokenStatusInactive, command.UpdatedAtMs, command.RequestID, appCode, command.UserID, command.DeviceID, userdomain.PushTokenStatusActive, } if strings.TrimSpace(command.PushToken) != "" { query += ` AND push_token = ?` args = append(args, command.PushToken) } result, err := r.db.ExecContext(ctx, query, args...) if err != nil { return false, err } affected, err := result.RowsAffected() if err != nil { return false, err } return affected > 0, nil } func normalizedAppCode(ctx context.Context, value string) string { value = strings.TrimSpace(value) if value == "" { return appcode.FromContext(ctx) } return appcode.Normalize(value) }