feat(notifications): add real-time notification system

Add database tables for notifications and WebSocket tickets
Implement REST API and WebSocket server for real-time delivery
Add NotificationBell component with dropdown and unread badge
Trigger alerts for comments, reactions, and AI moderation results
This commit is contained in:
2026-05-04 10:40:14 +08:00
parent 579d092020
commit a25f1661b5
12 changed files with 1811 additions and 0 deletions

View File

@@ -1214,6 +1214,70 @@ ALTER TABLE entity_discussion_comments
entity_type IN ('pokemon', 'items', 'recipes', 'habitats', 'ancient-artifacts')
);
CREATE TABLE IF NOT EXISTS notifications (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
recipient_user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
actor_user_id integer REFERENCES users(id) ON DELETE SET NULL,
type text NOT NULL CHECK (
type IN (
'life_post_comment',
'life_comment_reply',
'discussion_comment_reply',
'life_post_reaction',
'moderation_result'
)
),
life_post_id integer REFERENCES life_posts(id) ON DELETE CASCADE,
life_comment_id integer REFERENCES life_post_comments(id) ON DELETE CASCADE,
parent_life_comment_id integer REFERENCES life_post_comments(id) ON DELETE SET NULL,
discussion_comment_id integer REFERENCES entity_discussion_comments(id) ON DELETE CASCADE,
parent_discussion_comment_id integer REFERENCES entity_discussion_comments(id) ON DELETE SET NULL,
entity_type text CHECK (
entity_type IS NULL OR entity_type IN ('pokemon', 'items', 'recipes', 'habitats', 'ancient-artifacts')
),
entity_id integer,
reaction_type text CHECK (reaction_type IS NULL OR reaction_type IN ('like', 'helpful', 'fun', 'thanks')),
moderation_status text CHECK (moderation_status IS NULL OR moderation_status IN ('approved', 'rejected', 'failed')),
read_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS notifications_recipient_created_idx
ON notifications(recipient_user_id, created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS notifications_recipient_unread_idx
ON notifications(recipient_user_id, created_at DESC, id DESC)
WHERE read_at IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS notifications_life_post_comment_unique_idx
ON notifications(recipient_user_id, life_comment_id)
WHERE type = 'life_post_comment' AND life_comment_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS notifications_life_comment_reply_unique_idx
ON notifications(recipient_user_id, life_comment_id)
WHERE type = 'life_comment_reply' AND life_comment_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS notifications_discussion_comment_reply_unique_idx
ON notifications(recipient_user_id, discussion_comment_id)
WHERE type = 'discussion_comment_reply' AND discussion_comment_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS notifications_life_post_reaction_unique_idx
ON notifications(recipient_user_id, actor_user_id, life_post_id)
WHERE type = 'life_post_reaction' AND actor_user_id IS NOT NULL AND life_post_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS notification_ws_tickets (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash text NOT NULL UNIQUE,
expires_at timestamptz NOT NULL,
used_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS notification_ws_tickets_user_idx
ON notification_ws_tickets(user_id, expires_at DESC);
ALTER TABLE life_tags
ADD COLUMN IF NOT EXISTS is_rateable boolean NOT NULL DEFAULT false;