Перейти к основному содержимому

Social Share

Описание

Модуль social-share в apps/backend отвечает за репосты платформы в соцсетях. Админ заводит посты, пользователь публикует их у себя, а факт публикации подтверждается переходом по реферальной ссылке из поста.

Поддерживаются восемь соцсетей: twitter, facebook, telegram, whatsapp, vk, ok, pinterest, linkedin.

Полное ТЗ — tz/active/social-share.

Логика работы

админ создаёт пост

GET /social-share/posts?network=whatsapp → пост + готовый shareUrl

пользователь жмёт кнопку → POST /posts/:id/click (счётчик) → уходит по shareUrl

соцсеть открывает диалог публикации с текстом и реферальной ссылкой

кто-то переходит по реферальной ссылке

лендинг видит sp/sn/st в query → POST /social-share/track

backend: подпись → бот? → автор? → кулдаун? → зачёт

1. Ссылка, которая уходит в пост

Backend строит реферальную ссылку по тем же правилам, что и фронтовый хук useReferralLink:

utils/social-share-link.util.ts
export const resolveReferralCode = (user: ReferralLinkOwner): ReferralCode | null => {
if (!user.hashedUid) {
return null;
}

if (user.useNotHashedLink && user.link) {
return { param: 'link', value: user.link };
}

return { param: 'refUid', value: user.hashedUid };
};

Дальше к ней дописываются параметры трекинга — после реферального кода:

https://magnet.xyz/?refUid=AB12CD3&sp=7&sn=whatsapp&st=9f2a1c4b8e0d
ПараметрЗначение
spid поста
snсоцсеть
stподпись HMAC (12 hex)
Не трогайте реферальный код

Лендинг разбирает код регулярками, требующими ровно 8 hex-символов для link и 7 base62 для refUid, а getParentByLink в user.service.ts режет код по - и трактует хвост как индексы матриц P2/P3.

Поэтому: дефис внутри кода недопустим, метаданные идут отдельными &-параметрами.

2. Диплинк соцсети

Шаблоны собраны в одном месте — social-share.constants.ts. Пустые опциональные параметры выбрасываются, значения кодируются encodeURIComponent:

social-share.constants.ts
[SocialNetwork.Whatsapp]: ({ text, pageUrl }) =>
`https://wa.me/?${buildQuery({ text: `${text}\n${pageUrl}` })}`,

Facebook и LinkedIn не принимают текст — их диплинки берут только url и тянут заголовок с картинкой из OG-разметки страницы. Реферальная ссылка при этом передаётся, поэтому трекинг работает.

3. Подпись st

key = HMAC-SHA256(SOCIAL_SHARE_SECRET ?? JWT_SECRET, 'magnet:social-share:v1')
st = HMAC-SHA256(key, `${referralCode}:${postId}:${network}`).hex.slice(0, 12)

Ключ выводится из секрета, а не равен ему: компрометация share-подписи ничего не даёт для подделки JWT. Сверка идёт через timingSafeEqual. Подпись не даёт подменить sp/sn.

4. Зачёт публикации

Единица учёта — связка (автор, пост, соцсеть). Одна строка в social_share, уникальный индекс (user_id, post_id, network).

Зачёт происходит не по клику, а по живому переходу по ссылке. Правило: одна и та же связка засчитывается не чаще раза в cooldown_hours (по умолчанию 48).

social-share.service.ts
return this.dataSource.transaction(async (manager) => {
const locked = await this.lockShare(manager, authorId, post.id, network);
const cooldownEndsAt = resolveNextAvailableAt(locked.lastCreditedAt, post.cooldownHours);
const skipReason = this.resolveSkipReason(isSelfVisit, Boolean(cooldownEndsAt));
const credited = !skipReason;
// ...
});

lockShare делает SELECT, при отсутствии строки — INSERT ... IGNORE, затем SELECT ... FOR UPDATE. Блокировка строки не даёт двум одновременным переходам пройти проверку кулдауна дважды.

5. Почему переход может не засчитаться

skipReasonПишется в БДКогда
INVALID_SIGNATUREнетпараметры ссылки подменены
BOTнеткраулер соцсети тянет превью, либо пустой UA
REFERRER_NOT_FOUNDнеткод не резолвится в пользователя
POST_INACTIVEнетпост выключен или мягко удалён
NETWORK_NOT_ALLOWEDнетпост больше не публикуется в эту соцсеть
SELF_VISITдапо ссылке перешёл сам автор
COOLDOWNдакулдаун ещё не истёк

Первые пять отсекаются до транзакции: краулеры соцсетей дёргают ссылку сразу после публикации, и без этого фильтра пост засчитывался бы сам собой.

Схема данных

ТаблицаРоль
social_share_postпосты админа; networks и hashtags — JSON, soft delete
social_shareагрегат по связке; last_credited_at — источник истины кулдауна
social_share_visitappend-only лог переходов: credited, skip_reason, ip, user_agent

Миграция: src/migrations/1782769264315-create-social-share.ts, зарегистрирована в BACKEND_MIGRATIONS (app.module.ts).

API

Пользовательские — JwtAuthGuard

МетодПутьОтвет
GET/social-share/networks[{ network, supportsText }]
GET/social-share/posts?network={ referralAvailable, referralUrl, posts[] }
POST/social-share/posts/:postId/click{ shareUrl, trackedReferralUrl, status, nextAvailableAt }
GET/social-share/status?network={ totalClicks, totalVisits, totalCredits, totalRewardPoints, byNetwork[], items[] }

Публичный — без guard'ов

МетодПутьТелоОтвет
POST/social-share/track{ link | refUid, sp, sn, st }{ credited, skipReason, status, nextAvailableAt }

Админские — JwtAuthGuard + RolesGuard

Роли: AdminSocialShare, SocialShareRead, SocialShareWrite.

POST|GET /admin/posts, GET|PATCH|DELETE /admin/posts/:id, GET /admin/shares, GET /admin/stats.

Переменные окружения

ПеременнаяОбязательнаНазначение
NEXT_PUBLIC_BASE_URLдабаза реферальных ссылок
SOCIAL_SHARE_SECRETнетсекрет подписи st; если пуст — используется JWT_SECRET

Известные ограничения

  • Факт публикации подтверждается только переходом по ссылке — API соцсетей мы не используем. Автор может засчитать себе публикацию, открыв ссылку из другого браузера. Кулдаун ограничивает потолок такой накрутки.
  • SELF_VISIT ловит автора, только если он залогинен в том же браузере (проверяется Authorization-заголовок).
  • Rate limiting отсутствует: в backend нет @nestjs/throttler.
  • Вознаграждения не начисляются, reward_points только хранится.