PawConnect API Documentation
Sprint 1 - Task 7: User & Pet Registration
Base URL
``
http://localhost:8000/api
`
Authentication
Most endpoints require authentication using Laravel Sanctum tokens. Include the token in the Authorization header:
`
Authorization: Bearer {your_token}
`
---
Authentication Endpoints
1. Register User (with optional pet)
Endpoint: POST /auth/register
Description: Register a new user account with optional pet profile creation.
Request Body:
`
json
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"password_confirmation": "password123",
"city": "Beijing",
"city_id": 1,
"latitude": 39.9042,
"longitude": 116.4074,
"pet": {
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"birthday": "2020-01-15",
"bio": "A friendly and playful dog",
"personality_tags": ["friendly", "playful", "energetic"],
"visibility": "public"
}
}
`
Supported Species:
dog
- 狗
cat
- 猫
bird
- 鸟
fish
- 鱼
reptile
- 爬宠
amphibian
- 两栖动物
turtle
- 龟鳖
small_mammal
- 小型哺乳动物
big_cat
- 大型猫科
other
- 其他
Response (201 Created):
`
json
{
"success": true,
"message": "User registered successfully. Please check your email to verify your account.",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"city": "Beijing",
"status": "active"
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"age": 4
},
"token": "1|abc123..."
}
}
`
---
2. Login
Endpoint: POST /auth/login
Request Body:
`
json
{
"email": "john@example.com",
"password": "password123"
}
`
Response (200 OK):
`
json
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"city": "Beijing",
"status": "active"
},
"token": "2|xyz789..."
}
}
`
---
3. Logout
Endpoint: POST /auth/logout
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"message": "Logged out successfully"
}
`
---
4. Get Current User
Endpoint: GET /auth/me
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"city": "Beijing",
"city_id": 1,
"latitude": "39.9042000",
"longitude": "116.4074000",
"status": "active",
"is_official": false,
"created_at": "2025-12-31T00:00:00.000000Z",
"updated_at": "2025-12-31T00:00:00.000000Z"
},
"pets": [
{
"id": 1,
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"birthday": "2020-01-15",
"bio": "A friendly and playful dog",
"personality_tags": ["friendly", "playful", "energetic"],
"visibility": "public",
"status": "active"
}
]
}
}
`
---
Pet Management Endpoints
5. List User's Pets
Endpoint: GET /pets
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"data": [
{
"id": 1,
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"birthday": "2020-01-15",
"age": 4,
"bio": "A friendly and playful dog",
"personality_tags": ["friendly", "playful", "energetic"],
"visibility": "public",
"status": "active",
"created_at": "2025-12-31T00:00:00.000000Z",
"updated_at": "2025-12-31T00:00:00.000000Z"
}
]
}
`
---
6. Create Pet
Endpoint: POST /pets
Headers: Authorization: Bearer {token}
Request Body:
`
json
{
"name": "Whiskers",
"species": "cat",
"breed": "Persian",
"gender": "female",
"birthday": "2021-05-20",
"bio": "A calm and elegant cat",
"personality_tags": ["calm", "elegant", "independent"],
"visibility": "public"
}
`
Response (201 Created):
`
json
{
"success": true,
"message": "Pet created successfully",
"data": {
"id": 2,
"user_id": 1,
"name": "Whiskers",
"species": "cat",
"breed": "Persian",
"gender": "female",
"birthday": "2021-05-20",
"bio": "A calm and elegant cat",
"personality_tags": ["calm", "elegant", "independent"],
"visibility": "public",
"status": "active",
"created_at": "2025-12-31T00:00:00.000000Z",
"updated_at": "2025-12-31T00:00:00.000000Z"
}
}
`
---
7. Get Pet Details
Endpoint: GET /pets/{id}
Headers: Authorization: Bearer {token}
(optional for public pets)
Response (200 OK):
`
json
{
"success": true,
"data": {
"id": 1,
"user_id": 1,
"name": "Buddy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"birthday": "2020-01-15",
"age": 4,
"bio": "A friendly and playful dog",
"personality_tags": ["friendly", "playful", "energetic"],
"visibility": "public",
"status": "active",
"user": {
"id": 1,
"name": "John Doe",
"city": "Beijing"
},
"created_at": "2025-12-31T00:00:00.000000Z",
"updated_at": "2025-12-31T00:00:00.000000Z"
}
}
`
---
8. Update Pet
Endpoint: PUT /pets/{id}
Headers: Authorization: Bearer {token}
Request Body:
`
json
{
"name": "Buddy Jr.",
"bio": "An updated bio",
"personality_tags": ["friendly", "playful", "energetic", "loyal"]
}
`
Response (200 OK):
`
json
{
"success": true,
"message": "Pet updated successfully",
"data": {
"id": 1,
"name": "Buddy Jr.",
"bio": "An updated bio",
"personality_tags": ["friendly", "playful", "energetic", "loyal"],
...
}
}
`
---
9. Delete Pet
Endpoint: DELETE /pets/{id}
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"message": "Pet deleted successfully"
}
`
---
Error Responses
Validation Error (422)
`
json
{
"success": false,
"errors": {
"email": ["The email has already been taken."],
"password": ["The password confirmation does not match."]
}
}
`
Unauthorized (401)
`
json
{
"success": false,
"message": "Invalid credentials"
}
`
Forbidden (403)
`
json
{
"success": false,
"message": "Unauthorized"
}
`
Not Found (404)
`
json
{
"success": false,
"message": "Pet not found"
}
`
Server Error (500)
`
json
{
"success": false,
"message": "Registration failed",
"error": "Error details..."
}
`
---
Testing with cURL
Register a new user with a pet:
`
bash
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"password": "password123",
"password_confirmation": "password123",
"city": "Beijing",
"pet": {
"name": "Fluffy",
"species": "dog",
"breed": "Golden Retriever",
"gender": "male",
"personality_tags": ["friendly", "playful"]
}
}'
`
Login:
`
bash
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'
`
Get current user (replace TOKEN with actual token):
`
bash
curl -X GET http://localhost:8000/api/auth/me \
-H "Authorization: Bearer TOKEN"
`
---
Database Schema
Users Table
id
- Primary key
name
- User name
email
- Unique email address
password
- Hashed password
avatar
- Profile picture URL (nullable)
bio
- User bio (nullable)
city
- City name (nullable)
city_id
- City ID for filtering (nullable)
latitude
- GPS latitude (nullable)
longitude
- GPS longitude (nullable)
status
- Enum: active, frozen, banned, deleted
is_official
- Boolean: official account flag
email_verified_at
- Email verification timestamp
remember_token
- Remember me token
created_at
- Creation timestamp
updated_at
- Update timestamp
deleted_at
- Soft delete timestamp
Pets Table
id
- Primary key
user_id
- Foreign key to users table
name
- Pet name
species
- Enum: dog, cat, bird, fish, reptile, amphibian, turtle, small_mammal, big_cat, other
breed
- Pet breed (nullable)
gender
- Enum: male, female, unknown (nullable)
birthday
- Pet birthday (nullable)
bio
- Pet bio (nullable)
avatar
- Pet picture URL (nullable)
personality_tags
- JSON array of personality traits
visibility
- Enum: public, friends, private
status
- Enum: active, lost, deceased, archived
created_at
- Creation timestamp
updated_at
- Update timestamp
deleted_at
- Soft delete timestamp
---
Next Steps
This completes Task 7 of Sprint 1. The following features are now available:
✅ User registration with email/password
✅ Optional pet profile creation during registration
✅ Multi-species support (10 species types)
✅ Pet basic information (gender, birthday, personality tags, visibility)
✅ User authentication (login/logout)
✅ Pet CRUD operations
✅ Soft deletes for users and pets
✅ Status management for users and pets
Validation:
New users can register successfully
Users can optionally add pet profiles during registration
All 10 species types are supported
Pet profiles include all required fields
Authentication works correctly
API endpoints are functional and tested
---
Feed System Endpoints (Sprint 1 - Task 8)
10. Get Posts Feed
Endpoint: GET /posts
Headers: Authorization: Bearer {token}
(optional)
Query Parameters:
page
- Page number (default: 1)
per_page
- Items per page (default: 20)
Response (200 OK):
`
json
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"user_id": 1,
"pet_id": 1,
"content": "Beautiful day at the park with Buddy!",
"media": ["https://example.com/image1.jpg"],
"location": "Beijing Park",
"latitude": "39.9042000",
"longitude": "116.4074000",
"visibility": "public",
"status": "active",
"is_featured": false,
"created_at": "2025-12-31T00:00:00.000000Z",
"user": {
"id": 1,
"name": "John Doe",
"avatar": null
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog",
"avatar": null
},
"stats": {
"likes_count": 15,
"comments_count": 3,
"shares_count": 2,
"views_count": 120
}
}
],
"per_page": 20,
"total": 50
}
}
`
---
11. Create Post
Endpoint: POST /posts
Headers: Authorization: Bearer {token}
Request Body:
`
json
{
"content": "Beautiful day at the park with Buddy!",
"pet_id": 1,
"media": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"location": "Beijing Park",
"latitude": 39.9042,
"longitude": 116.4074,
"visibility": "public"
}
`
Visibility Options:
public
- Visible to everyone (default)
friends
- Visible to friends only
group
- Visible to group members only
private
- Visible to user only
unlisted
- Not shown in public feed but accessible via direct link
Response (201 Created):
`
json
{
"success": true,
"message": "Post created successfully",
"data": {
"id": 1,
"user_id": 1,
"pet_id": 1,
"content": "Beautiful day at the park with Buddy!",
"media": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"location": "Beijing Park",
"latitude": "39.9042000",
"longitude": "116.4074000",
"visibility": "public",
"status": "active",
"created_at": "2025-12-31T00:00:00.000000Z",
"user": {
"id": 1,
"name": "John Doe"
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog"
},
"stats": {
"likes_count": 0,
"comments_count": 0,
"shares_count": 0,
"views_count": 0
}
}
}
`
Note: Creating a post automatically:
Initializes post_stats with zero counts
Publishes a post_created
event to the outbox for async processing
The event will be processed by OutboxWorker to update feed caches
---
12. Get Post Details
Endpoint: GET /posts/{id}
Headers: Authorization: Bearer {token}
(optional for public posts)
Response (200 OK):
`
json
{
"success": true,
"data": {
"id": 1,
"user_id": 1,
"pet_id": 1,
"content": "Beautiful day at the park with Buddy!",
"media": ["https://example.com/image1.jpg"],
"location": "Beijing Park",
"visibility": "public",
"status": "active",
"created_at": "2025-12-31T00:00:00.000000Z",
"user": {
"id": 1,
"name": "John Doe",
"avatar": null
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog"
},
"stats": {
"likes_count": 15,
"comments_count": 3,
"views_count": 121
},
"comments": [
{
"id": 1,
"user_id": 2,
"content": "So cute!",
"created_at": "2025-12-31T00:05:00.000000Z",
"user": {
"id": 2,
"name": "Jane Smith"
}
}
]
}
}
`
Note: Viewing a post automatically increments the views_count.
---
13. Like Post
Endpoint: POST /posts/{id}/like
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"message": "Post liked successfully",
"data": {
"likes_count": 16
}
}
`
Error Response (400 Bad Request):
`
json
{
"success": false,
"message": "Post already liked"
}
`
---
14. Unlike Post
Endpoint: DELETE /posts/{id}/like
Headers: Authorization: Bearer {token}
Response (200 OK):
`
json
{
"success": true,
"message": "Post unliked successfully",
"data": {
"likes_count": 15
}
}
`
Error Response (400 Bad Request):
`
json
{
"success": false,
"message": "Post not liked yet"
}
`
---
15. Comment on Post
Endpoint: POST /posts/{id}/comment
Headers: Authorization: Bearer {token}
Request Body:
`
json
{
"content": "So cute! Love this photo!",
"parent_id": null
}
`
Note: Set parent_id
to reply to another comment (nested comments).
Response (201 Created):
`
json
{
"success": true,
"message": "Comment added successfully",
"data": {
"id": 1,
"user_id": 2,
"post_id": 1,
"parent_id": null,
"content": "So cute! Love this photo!",
"status": "active",
"created_at": "2025-12-31T00:05:00.000000Z",
"user": {
"id": 2,
"name": "Jane Smith",
"avatar": null
}
}
}
`
---
16. Get Post Comments
Endpoint: GET /posts/{id}/comments
Query Parameters:
page
- Page number (default: 1)
per_page
- Items per page (default: 20)
Response (200 OK):
`
json
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": 1,
"user_id": 2,
"post_id": 1,
"parent_id": null,
"content": "So cute! Love this photo!",
"status": "active",
"created_at": "2025-12-31T00:05:00.000000Z",
"user": {
"id": 2,
"name": "Jane Smith",
"avatar": null
},
"replies": [
{
"id": 2,
"user_id": 1,
"parent_id": 1,
"content": "Thank you!",
"created_at": "2025-12-31T00:10:00.000000Z",
"user": {
"id": 1,
"name": "John Doe"
}
}
]
}
],
"per_page": 20,
"total": 3
}
}
`
---
Testing Feed System with cURL
Create a post:
`
bash
curl -X POST http://localhost:8000/api/posts \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Beautiful day at the park!",
"pet_id": 1,
"media": ["https://example.com/image1.jpg"],
"location": "Beijing Park",
"visibility": "public"
}'
`
Like a post:
`
bash
curl -X POST http://localhost:8000/api/posts/1/like \
-H "Authorization: Bearer TOKEN"
`
Comment on a post:
`
bash
curl -X POST http://localhost:8000/api/posts/1/comment \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "So cute!"
}'
`
Get posts feed:
`
bash
curl -X GET http://localhost:8000/api/posts
`
---
Feed System Database Schema
Posts Table
id
- Primary key
user_id
- Foreign key to users table
pet_id
- Foreign key to pets table (nullable)
content
- Post content (max 5000 chars)
media
- JSON array of media URLs
location
- Location name (nullable)
latitude
- GPS latitude (nullable)
longitude
- GPS longitude (nullable)
visibility
- Enum: public, friends, group, private, unlisted
status
- Enum: active, hidden, deleted
is_featured
- Boolean: featured post flag
created_at
- Creation timestamp
updated_at
- Update timestamp
deleted_at
- Soft delete timestamp
Post Stats Table
id
- Primary key
post_id
- Foreign key to posts table (unique)
likes_count
- Number of likes
comments_count
- Number of comments
shares_count
- Number of shares
views_count
- Number of views
created_at
- Creation timestamp
updated_at
- Update timestamp
Likes Table
id
- Primary key
user_id
- Foreign key to users table
post_id
- Foreign key to posts table
created_at
- Creation timestamp
updated_at
- Update timestamp
Unique constraint on (user_id, post_id)
Comments Table
id
- Primary key
user_id
- Foreign key to users table
post_id
- Foreign key to posts table
parent_id
- Foreign key to comments table (nullable, for nested comments)
content
- Comment content (max 1000 chars)
status
- Enum: active, hidden, deleted
created_at
- Creation timestamp
updated_at
- Update timestamp
deleted_at
- Soft delete timestamp
Feed Items Table
id
- Primary key
user_id
- Foreign key to users table
post_id
- Foreign key to posts table
event_id
- Foreign key to events table (unique, for idempotency)
score
- Feed ranking score
created_at
- Creation timestamp
updated_at
- Update timestamp
---
Event-Driven Architecture
Post Created Event
When a post is created, a post_created
event is published to the outbox:
Event Type: post_created
Aggregate Type: post
Aggregate ID: Post ID
Payload:
`
json
{
"post_id": 1,
"user_id": 1,
"pet_id": 1,
"visibility": "public",
"is_official": false,
"city_id": 1,
"created_at": "2025-12-31T00:00:00+00:00"
}
`
Handlers: PostCreatedHandler
(to be implemented in Task 13.2)
The handler will:
1. Initialize post_stats (already done in controller)
2. Write to feed_items table
3. Clear L2 city candidate cache
4. Increment user feed version
5. Write official content to L3 fallback cache
---
Task Completion Summary
Task 7 (Completed):
✅ User registration with email/password
✅ Optional pet profile creation during registration
✅ Multi-species support (10 species types)
✅ Pet basic information (gender, birthday, personality tags, visibility)
✅ User authentication (login/logout)
✅ Pet CRUD operations
✅ Soft deletes for users and pets
✅ Status management for users and pets
Task 8 (Completed):
✅ Posts data model with stats tracking
✅ Likes system with duplicate prevention
✅ Comments system with nested replies support
✅ Feed items table with event_id for idempotency
✅ Post creation with automatic stats initialization
✅ Like/unlike functionality with count updates
✅ Comment functionality with count updates
✅ View tracking
✅ Event publishing to outbox (post_created)
✅ All API endpoints tested and working
Validation:
Posts can be created with media, location, and visibility settings
Post stats are automatically initialized
Likes work correctly with duplicate prevention
Comments work correctly with nested replies
View counts increment properly
All interactions update counts correctly
post_created events are published to outbox
All 7 tests pass successfully
---
Feed System with Three-Level Cache (Sprint 1 - Task 9)
17. Get Personalized Feed
Endpoint: GET /feed
Headers: Authorization: Bearer {token}
(optional - works for both logged-in and anonymous users)
Query Parameters:
cursor
- Pagination cursor (optional)
limit
- Number of posts per page (default: 20, max: 50)
Response (200 OK):
`
json
{
"success": true,
"data": {
"posts": [
{
"id": 1,
"user_id": 1,
"pet_id": 1,
"content": "Beautiful day at the park!",
"media": ["https://example.com/image1.jpg"],
"location": "Beijing Park",
"visibility": "public",
"status": "active",
"created_at": "2025-12-31T00:00:00.000000Z",
"user": {
"id": 1,
"name": "John Doe",
"avatar": null
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog"
},
"stats": {
"likes_count": 15,
"comments_count": 3,
"views_count": 120
}
}
],
"next_cursor": "123",
"has_more": true
}
}
`
Three-Level Cache Architecture:
The Feed system uses a sophisticated three-level caching strategy for optimal performance:
L1 Cache - User Personalized Feed (TTL: 30-60s)
Key format: feed:user:{uid}:v:{ver}:cursor:*
Contains personalized feed for each user
Invalidated when user's feed version increments
Fastest response time
L2 Cache - City Candidate Pool (TTL: 3-5m)
Key format: feed:candidate:city:{city_id}
Contains up to 200 posts from the same city
Shared among users in the same city
Updated when new posts are created in the city
L3 Cache - Global Fallback (TTL: 30-60m)
Key format: feed:fallback:global
Contains featured and official content
Used when Redis is down or other caches miss
Ensures service availability
Cache Read Order:
1. Try L1 (user personalized) → if hit, return
2. Try L2 (city pool) → if hit, async update L1, return
3. Try L3 (global fallback) → if hit, return
4. Fallback to database → update all caches, return
Benefits:
✅ High performance with sub-100ms response time
✅ Graceful degradation when Redis fails
✅ Reduced database load
✅ Personalized content for logged-in users
✅ Works for anonymous users
---
Testing Feed Cache with cURL
Get feed (anonymous user):
`
bash
curl -X GET http://localhost:8000/api/feed
`
Get feed (logged-in user):
`
bash
curl -X GET http://localhost:8000/api/feed \
-H "Authorization: Bearer TOKEN"
`
Get feed with pagination:
`
bash
curl -X GET "http://localhost:8000/api/feed?cursor=123&limit=10" \
-H "Authorization: Bearer TOKEN"
`
---
Feed Cache Management
Cache Update Operations
Update L2 Cache (City Candidate Pool):
`
php
$feedService = app(\App\Services\FeedService::class);
$feedService->updateL2Cache($cityId);
`
Update L3 Cache (Global Fallback):
`
php
$feedService = app(\App\Services\FeedService::class);
$feedService->updateL3Cache();
`
Increment User Feed Version (Invalidate L1):
`
php
$feedService = app(\App\Services\FeedService::class);
$feedService->incrementUserFeedVersion($userId);
`
Clear City Cache:
`
php
$feedService = app(\App\Services\FeedService::class);
$feedService->clearL2Cache($cityId);
`
---
Task Completion Summary (Updated)
Task 7 (Completed):
✅ User registration with email/password
✅ Optional pet profile creation during registration
✅ Multi-species support (10 species types)
✅ Pet basic information (gender, birthday, personality tags, visibility)
✅ User authentication (login/logout)
✅ Pet CRUD operations
✅ Soft deletes for users and pets
✅ Status management for users and pets
Task 8 (Completed):
✅ Posts data model with stats tracking
✅ Likes system with duplicate prevention
✅ Comments system with nested replies support
✅ Feed items table with event_id for idempotency
✅ Post creation with automatic stats initialization
✅ Like/unlike functionality with count updates
✅ Comment functionality with count updates
✅ View tracking
✅ Event publishing to outbox (post_created)
✅ All API endpoints tested and working
Task 9 (Completed):
✅ L1 cache implementation (user personalized feed, TTL 30-60s)
✅ L2 cache implementation (city candidate pool, TTL 3-5m)
✅ L3 cache implementation (global fallback, TTL 30-60m)
✅ Cache read order (L1→L2→L3→DB)
✅ GET /api/feed endpoint
✅ Redis failure graceful degradation
✅ Pagination with cursor support
✅ Support for both logged-in and anonymous users
✅ All 8 tests pass successfully
Validation:
Three-level cache system works correctly
Feed can be retrieved from any cache level
Database fallback works when Redis is unavailable
Pagination works with cursor-based navigation
Cache TTLs are randomized to prevent thundering herd
User feed version management for cache invalidation
All tests pass (8/8) ✅
---
Cold Start Feed with Diversity Filter (Sprint 1 - Task 10)
18. Get Cold Start Feed
Endpoint: GET /feed/cold-start
Headers: Authorization: Bearer {token}
(optional - works for both logged-in and anonymous users)
Query Parameters:
candidate_size
- Candidate pool size (default: 200, max: 500)
return_size
- Number of posts to return (default: 20, max: 50)
Response (200 OK):
`
json
{
"success": true,
"data": {
"posts": [
{
"id": 1,
"user_id": 1,
"pet_id": 1,
"content": "Beautiful day at the park!",
"media": ["https://example.com/image1.jpg"],
"visibility": "public",
"status": "active",
"user": {
"id": 1,
"name": "John Doe"
},
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog"
},
"stats": {
"likes_count": 15,
"comments_count": 3
}
}
],
"next_cursor": null,
"has_more": false,
"source": "cold_start",
"diversity_applied": true,
"stats": {
"candidate_size": 200,
"return_size": 20,
"species_distribution": {
"dog": 5,
"cat": 4,
"bird": 3,
"fish": 3,
"reptile": 2,
"turtle": 2,
"amphibian": 1
}
}
}
}
`
Four-Way UNION Candidate Pool:
The cold start feed uses a sophisticated four-way UNION strategy to gather diverse content:
Path 1: Following Users' Posts (预留)
Posts from users you follow
Currently not implemented (following feature pending)
Will be enabled when follow system is implemented
Path 2: Group Posts (预留)
Posts from groups you're a member of
Currently not implemented (group feature pending)
Will be enabled when group system is implemented
Path 3: City Posts (50% weight)
Posts from users in the same city
Prioritizes local content
Helps users discover nearby pet owners
Path 4: Featured/Official Posts (25% weight)
Posts marked as featured
Posts from official accounts
High-quality curated content
Path 5: Global Hot Posts (25% weight)
Popular posts from all users
Ensures content diversity
Prevents echo chamber effect
Diversity Filter Rules:
The system applies intelligent diversity filtering to ensure varied content:
Rule 1: Minimum 3 Species
Each screen must contain at least 3 different species
Prevents feed from being dominated by single species
Ensures exposure to diverse pet types
Rule 2: Maximum 60% Per Species
No single species can exceed 60% of feed content
Prevents dog/cat content from overwhelming feed
Ensures fair representation
Rule 3: Rare Species Priority
Rare species get priority boost: reptile, amphibian, bird, fish, turtle, small_mammal, big_cat
Helps smaller communities get visibility
Encourages diverse pet ownership representation
Algorithm:
1. Gather candidates from four-way UNION (up to 200 posts)
2. Group posts by species
3. First round: Select at least 1 post from each rare species
4. Second round: Round-robin selection from all species
5. Enforce 60% maximum per species
6. Continue until reaching target size (20 posts)
Benefits:
✅ Diverse content across multiple species
✅ Fair representation for rare species
✅ Prevents echo chamber effect
✅ Encourages community diversity
✅ Better user experience for new users
---
Testing Cold Start Feed with cURL
Get cold start feed (anonymous user):
`
bash
curl -X GET http://localhost:8000/api/feed/cold-start
`
Get cold start feed (logged-in user):
`
bash
curl -X GET http://localhost:8000/api/feed/cold-start \
-H "Authorization: Bearer TOKEN"
`
Get cold start feed with custom sizes:
`
bash
curl -X GET "http://localhost:8000/api/feed/cold-start?candidate_size=300&return_size=30" \
-H "Authorization: Bearer TOKEN"
`
---
Task Completion Summary (Updated)
Task 7 (Completed):
✅ User registration with email/password
✅ Optional pet profile creation during registration
✅ Multi-species support (10 species types)
✅ Pet basic information (gender, birthday, personality tags, visibility)
✅ User authentication (login/logout)
✅ Pet CRUD operations
✅ Soft deletes for users and pets
✅ Status management for users and pets
Task 8 (Completed):
✅ Posts data model with stats tracking
✅ Likes system with duplicate prevention
✅ Comments system with nested replies support
✅ Feed items table with event_id for idempotency
✅ Post creation with automatic stats initialization
✅ Like/unlike functionality with count updates
✅ Comment functionality with count updates
✅ View tracking
✅ Event publishing to outbox (post_created)
✅ All API endpoints tested and working
Task 9 (Completed):
✅ L1 cache implementation (user personalized feed, TTL 30-60s)
✅ L2 cache implementation (city candidate pool, TTL 3-5m)
✅ L3 cache implementation (global fallback, TTL 30-60m)
✅ Cache read order (L1→L2→L3→DB)
✅ GET /api/feed endpoint
✅ Redis failure graceful degradation
✅ Pagination with cursor support
✅ Support for both logged-in and anonymous users
✅ All 8 tests pass successfully
Task 10 (Completed):
✅ Four-way UNION candidate pool (following/group/city/featured/global)
✅ Diversity filter implementation (min 3 species, max 60% per species)
✅ Rare species priority boost
✅ Round-robin species selection algorithm
✅ GET /api/feed/cold-start endpoint
✅ Species distribution statistics
✅ Support for both logged-in and anonymous users
✅ Configurable candidate pool and return sizes
✅ All 8 tests pass successfully
Validation:
Four-way UNION successfully gathers diverse candidates
Diversity filter ensures at least 3 species per screen
No single species exceeds 60% of feed content
Rare species (reptile, amphibian, bird, etc.) get priority
Cold start feed works for both logged-in and guest users
Species distribution is balanced and fair
All tests pass (8/8) ✅
---
Sprint 1 - Task 12: Pet Timeline
Timeline Endpoints
#### 1. Get Pet Timeline
Endpoint: GET /pets/{id}/timeline
Description: Get the timeline of events for a specific pet. Supports cursor-based pagination.
Authentication: Not required (public access)
Query Parameters:
limit
(optional): Number of events per page (default: 20, max: 100)
cursor
(optional): Cursor for pagination (ISO 8601 timestamp from previous page)
Example Request:
`
GET /api/pets/1/timeline?limit=20
GET /api/pets/1/timeline?limit=20&cursor=2025-12-31T02:00:00Z
`
Response (200 OK):
`
json
{
"success": true,
"data": {
"pet": {
"id": 1,
"name": "Buddy",
"species": "dog"
},
"events": [
{
"id": 1,
"event_type": "checkin",
"event_type_name": "打卡",
"title": "在中央公园打卡",
"description": "和主人一起散步",
"metadata": {
"poi_id": 123,
"poi_name": "中央公园",
"location": "北京市朝阳区"
},
"event_time": "2025-12-31T10:30:00Z",
"created_at": "2025-12-31T10:30:05Z"
},
{
"id": 2,
"event_type": "walk",
"event_type_name": "散步",
"title": "晨间散步",
"description": "在小区附近散步了30分钟",
"metadata": {
"duration": 30,
"distance": 2.5
},
"event_time": "2025-12-31T07:00:00Z",
"created_at": "2025-12-31T07:30:00Z"
}
],
"pagination": {
"has_more": true,
"next_cursor": "2025-12-31T07:00:00Z"
}
}
}
`
---
#### 2. Create Manual Timeline Event
Endpoint: POST /pets/{id}/timeline
Description: Create a manual timeline event for a pet. Only the pet owner can create events.
Authentication: Required (Bearer token)
Request Body:
`
json
{
"event_type": "walk",
"title": "晨间散步",
"description": "在公园散步了30分钟",
"metadata": {
"location": "中央公园",
"duration": 30
},
"event_time": "2025-12-31T07:00:00Z"
}
`
Event Types:
walk
- 散步
play
- 玩耍
grooming
- 洗护
training_complete
- 训练完成
first_friend
- 第一个朋友
travel
- 旅行
monthly_memory
- 月度回忆
checkin
- 打卡(通常由系统自动创建)
Response (201 Created):
`
json
{
"success": true,
"message": "时间轴事件创建成功",
"data": {
"id": 3,
"event_type": "walk",
"event_type_name": "散步",
"title": "晨间散步",
"description": "在公园散步了30分钟",
"metadata": {
"location": "中央公园",
"duration": 30
},
"event_time": "2025-12-31T07:00:00Z",
"created_at": "2025-12-31T07:05:00Z"
}
}
`
Error Response (403 Forbidden):
`
json
{
"success": false,
"message": "无权限操作此宠物"
}
`
Error Response (422 Unprocessable Entity):
`
json
{
"success": false,
"message": "参数验证失败",
"errors": {
"event_type": ["The event type field is required."],
"title": ["The title field is required."]
}
}
`
---
Timeline Features
1. Event Types: 8种事件类型,涵盖宠物日常生活的各个方面
2. Cursor Pagination: 使用游标分页,支持高效的时间轴浏览
3. Idempotency: source_event_id
唯一约束确保事件不重复
4. Automatic Events: 系统自动创建的事件(如打卡)会包含source_event_id
5. Manual Events: 用户手动创建的事件不需要source_event_id
6. Permission Control: 只有宠物主人可以创建手动事件
7. Metadata Support: 灵活的metadata字段支持各种额外信息
---
Timeline Integration
Timeline系统与其他系统的集成:
1. Checkin系统: 打卡成功后,通过Outbox机制自动创建Timeline事件
2. Feed系统: Timeline事件可以展示在Feed中
3. 月度回忆: 定时任务自动生成月度回忆事件
4. 权限控制: 根据宠物的visibility设置控制Timeline的可见性
---
---
Sprint 2 - Task 15: Official Announcement System
Announcement Endpoints
#### 1. Get Active Announcements
Endpoint: GET /announcements/active
Description: Get all active announcements for the current user or anonymous visitor. Announcements are filtered by city (if provided) and user dismissal status.
Authentication: Optional (works for both logged-in and anonymous users)
Query Parameters:
city_id
(optional): City ID for city-specific announcements (if not logged in)
Example Request:
`
GET /api/announcements/active
GET /api/announcements/active?city_id=1
`
Response (200 OK):
`
json
{
"success": true,
"data": [
{
"id": 1,
"title": "Emergency: Service Disruption",
"content": "We are experiencing technical difficulties. Our team is working to resolve the issue.",
"level": "emergency",
"dismissible": false,
"auto_expand": true,
"link_url": null,
"link_text": null,
"start_time": "2025-12-31T10:00:00Z",
"end_time": "2025-12-31T12:00:00Z"
},
{
"id": 2,
"title": "System Maintenance Notice",
"content": "We will perform system maintenance on Dec 31, 2025 from 2:00 AM to 4:00 AM.",
"level": "important",
"dismissible": true,
"auto_expand": true,
"link_url": "https://pawconnect.com/maintenance",
"link_text": "Learn More",
"start_time": "2025-12-30T00:00:00Z",
"end_time": "2026-01-02T00:00:00Z"
},
{
"id": 3,
"title": "Welcome to PawConnect!",
"content": "Thank you for joining our pet community.",
"level": "info",
"dismissible": true,
"auto_expand": false,
"link_url": null,
"link_text": null,
"start_time": "2025-12-30T00:00:00Z",
"end_time": "2026-01-07T00:00:00Z"
}
]
}
`
---
#### 2. Dismiss Announcement
Endpoint: POST /announcements/{id}/dismiss
Description: Dismiss an announcement for the current user. Emergency announcements and non-dismissible announcements cannot be dismissed.
Authentication: Required (Bearer token)
Example Request:
`
POST /api/announcements/3/dismiss
`
Response (200 OK):
`
json
{
"success": true,
"message": "Announcement dismissed successfully"
}
`
Error Response (400 Bad Request):
`
json
{
"success": false,
"message": "Cannot dismiss this announcement"
}
`
Error Response (401 Unauthorized):
`
json
{
"success": false,
"message": "Authentication required"
}
`
---
Announcement Levels
The system supports three levels of announcements with different behaviors:
#### 1. Info Level (信息)
Dismissible: Yes (can be closed by user)
Auto-expand: No (collapsed by default)
Use case: General information, tips, welcome messages
Example: "Welcome to PawConnect!"
#### 2. Important Level (重要)
Dismissible: Yes (can be closed by user)
Auto-expand: Yes (expanded by default)
Use case: System maintenance, feature updates, important notices
Example: "System Maintenance Notice"
#### 3. Emergency Level (紧急)
Dismissible: No (cannot be closed)
Auto-expand: Yes (always expanded)
Fixed position: Always shown at the top of feed
Use case: Service disruptions, critical issues, urgent alerts
Example: "Emergency: Service Disruption"
---
Announcement Features
1. Three-Level Priority System: Info, Important, Emergency with different behaviors
2. Dismissal Control: Users can dismiss info and important announcements
3. Emergency Protection: Emergency announcements cannot be dismissed
4. Time-based Activation: Announcements can be scheduled with start_time and end_time
5. City Targeting: Announcements can target specific cities or all cities (Task 16)
6. Redis Caching: Active announcements are cached for 5 minutes
7. Auto-expand Control: Important and emergency announcements auto-expand
8. Link Support: Announcements can include optional links for more information
9. Display Order: Announcements are ordered by priority (emergency > important > info)
---
Announcement Cache Strategy
Cache Key Format:
Global: announcement:active:global
City-specific: announcement:active:city:{city_id}
Cache TTL: 5 minutes (300 seconds)
Cache Invalidation:
Automatically expires after TTL
Can be manually cleared using AnnouncementService::clearCache()
Should be cleared when announcements are created/updated/deleted
Benefits:
✅ Reduces database load
✅ Fast response time
✅ Supports high traffic
✅ Graceful degradation if Redis fails
---
Announcement Workflow
`
1. Admin creates announcement
↓
2. Announcement stored in database
↓
3. Cache cleared (if needed)
↓
4. User requests active announcements
↓
5. Check Redis cache
├─ Cache hit → Return cached data
└─ Cache miss → Query database → Cache result → Return
↓
6. Filter by user dismissal status
↓
7. Return announcements to user
`
---
Testing Announcements with cURL
Get active announcements (anonymous):
`
bash
curl -X GET http://localhost:8000/api/announcements/active
`
Get active announcements (logged-in):
`
bash
curl -X GET http://localhost:8000/api/announcements/active \
-H "Authorization: Bearer TOKEN"
`
Get city-specific announcements:
`
bash
curl -X GET "http://localhost:8000/api/announcements/active?city_id=1"
`
Dismiss an announcement:
`
bash
curl -X POST http://localhost:8000/api/announcements/3/dismiss \
-H "Authorization: Bearer TOKEN"
`
---
Database Schema
#### official_announcements Table
id
- Primary key
title
- Announcement title (max 200 chars)
content
- Announcement content (text)
level
- Enum: info, important, emergency
dismissible
- Boolean: can user dismiss this announcement
auto_expand
- Boolean: should announcement be expanded by default
is_active
- Boolean: is announcement active
start_time
- Timestamp: when announcement becomes active (nullable)
end_time
- Timestamp: when announcement expires (nullable)
target_cities
- JSON: array of city IDs (null = all cities)
link_url
- String: optional link URL (nullable)
link_text
- String: optional link text (nullable)
display_order
- Integer: custom display order (default: 0)
created_at
- Creation timestamp
updated_at
- Update timestamp
#### announcement_dismiss Table
id
- Primary key
user_id
- Foreign key to users table
announcement_id
- Foreign key to official_announcements table
dismissed_at
- Timestamp: when user dismissed the announcement
Unique constraint on (user_id, announcement_id)
---
Task Completion Summary (Updated)
Sprint 1 (Completed):
✅ Task 7: User/Pet Registration (6 tests)
✅ Task 8: Feed System (7 tests)
✅ Task 9: Three-Level Cache (8 tests)
✅ Task 10: Cold Start Feed (8 tests)
✅ Task 11: Map System (28 tests)
✅ Task 12: Timeline System (7 tests)
✅ Task 13: Outbox Derivation (7 tests)
✅ Task 14: Checkpoint (71 tests)
Sprint 2 (In Progress):
✅ Task 15: Official Announcement System (10 tests)
Created official_announcements and announcement_dismiss tables
Implemented OfficialAnnouncement and AnnouncementDismiss models
Implemented AnnouncementService with Redis caching
Implemented AnnouncementController with 2 API endpoints
Three-level priority system (info/important/emergency)
Dismissal control with emergency protection
Time-based activation (start_time/end_time)
Redis caching with 5-minute TTL
All 10 tests pass successfully ✅
Validation:
Info announcements can be dismissed
Important announcements auto-expand and can be dismissed
Emergency announcements cannot be dismissed and always show at top
Expired announcements don't appear in active list
Future announcements don't appear until start_time
Redis cache improves performance
Dismissed announcements are filtered out for logged-in users
Anonymous users see all active announcements
All tests pass (10/10) ✅
---
Sprint 2 - Task 16: Announcement City Targeting
City Targeting Features
The announcement system now supports precise city targeting, allowing administrators to send announcements to specific cities or groups of cities.
#### City Targeting Modes
1. Global Announcements (No city targeting)
- Visible to all users regardless of their city
- No entries in announcement_cities
table
- Example: Platform-wide updates, global events
2. Single City Targeting
- Announcement targets one specific city
- Only users in that city see the announcement
- Example: Beijing-specific maintenance notice
3. Multi-City Targeting
- Announcement targets multiple cities
- Users in any of the target cities see the announcement
- Example: Regional event covering Beijing and Shanghai
#### How City Targeting Works
Database Structure:
`
official_announcements (1) ←→ (N) announcement_cities
`
Query Logic:
`
sql
WHERE (
-- Global announcements (no city mappings)
NOT EXISTS (SELECT 1 FROM announcement_cities WHERE announcement_id = ...)
OR
-- Announcements targeting this city
EXISTS (SELECT 1 FROM announcement_cities WHERE announcement_id = ... AND city_id = ?)
)
`
Cache Strategy:
Each city has its own cache: announcement:active:city:{city_id}
Global cache: announcement:active:global
Cache is automatically cleared when target cities are updated
#### Setting Target Cities
Using AnnouncementService:
`
php
$announcementService = app(\App\Services\AnnouncementService::class);
// Set single city
$announcementService->setTargetCities($announcementId, [1]); // Beijing only
// Set multiple cities
$announcementService->setTargetCities($announcementId, [1, 2]); // Beijing + Shanghai
// Make global (remove all city targeting)
$announcementService->setTargetCities($announcementId, []); // Global
`
Direct Database:
`
php
use App\Models\AnnouncementCity;
// Add city targeting
AnnouncementCity::create([
'announcement_id' => 1,
'city_id' => 1, // Beijing
]);
AnnouncementCity::create([
'announcement_id' => 1,
'city_id' => 2, // Shanghai
]);
`
#### City Targeting Examples
Example 1: Beijing Maintenance Notice
`
php
$announcement = OfficialAnnouncement::create([
'title' => 'Beijing Server Maintenance',
'content' => 'We will perform maintenance on Beijing servers tonight.',
'level' => 'important',
'is_active' => true,
]);
$announcementService->setTargetCities($announcement->id, [1]); // Beijing only
`
Example 2: Regional Event
`
php
$announcement = OfficialAnnouncement::create([
'title' => 'North China Pet Festival',
'content' => 'Join us for the biggest pet festival in North China!',
'level' => 'info',
'is_active' => true,
]);
// Target Beijing, Tianjin, Shijiazhuang
$announcementService->setTargetCities($announcement->id, [1, 4, 5]);
`
Example 3: Global Platform Update
`
php
$announcement = OfficialAnnouncement::create([
'title' => 'New Feature: Pet Timeline',
'content' => 'We just launched a new timeline feature for your pets!',
'level' => 'info',
'is_active' => true,
]);
// No city targeting = global
// Don't call setTargetCities() or call with empty array
`
#### Verification Examples
Beijing User:
`
GET /api/announcements/active?city_id=1
Response:
Global announcements
Beijing-specific announcements
Multi-city announcements that include Beijing
`
Shanghai User:
`
GET /api/announcements/active?city_id=2
Response:
Global announcements
Shanghai-specific announcements
Multi-city announcements that include Shanghai
NOT Beijing-specific announcements
`
Guangzhou User:
`
GET /api/announcements/active?city_id=3
Response:
Global announcements only
NOT Beijing or Shanghai specific announcements
`
#### Database Schema
announcement_cities Table:
id
- Primary key
announcement_id
- Foreign key to official_announcements
city_id
- City ID (integer)
created_at
- Creation timestamp
updated_at
- Update timestamp
Unique constraint on (announcement_id, city_id)
Index on city_id for fast lookups
#### Benefits
✅ Precise Targeting: Send announcements to specific cities or regions
✅ Reduced Noise: Users only see relevant announcements for their location
✅ Flexible Management: Easy to add/remove cities from targeting
✅ Performance: City-specific caching reduces database load
✅ Scalability: Supports any number of cities and targeting combinations
#### Testing City Targeting
Test Scenario 1: Beijing-only announcement
`
bash
Create announcement
curl -X POST http://localhost:8000/admin/api/announcements \
-H "Authorization: Bearer ADMIN_TOKEN" \
-d '{
"title": "Beijing Maintenance",
"content": "Server maintenance tonight",
"level": "important",
"target_cities": [1]
}'
Beijing user sees it
curl -X GET "http://localhost:8000/api/announcements/active?city_id=1"
Returns: Beijing Maintenance
Shanghai user doesn't see it
curl -X GET "http://localhost:8000/api/announcements/active?city_id=2"
Returns: (no Beijing Maintenance)
`
Test Scenario 2: Multi-city announcement
`
bash
Create announcement for Beijing + Shanghai
curl -X POST http://localhost:8000/admin/api/announcements \
-d '{
"title": "Regional Event",
"content": "Pet festival in North China",
"level": "info",
"target_cities": [1, 2]
}'
Both Beijing and Shanghai users see it
curl -X GET "http://localhost:8000/api/announcements/active?city_id=1"
curl -X GET "http://localhost:8000/api/announcements/active?city_id=2"
Both return: Regional Event
Guangzhou user doesn't see it
curl -X GET "http://localhost:8000/api/announcements/active?city_id=3"
Returns: (no Regional Event)
`
---
Task Completion Summary (Updated)
Sprint 2 (In Progress):
✅ Task 15: Official Announcement System (10 tests)
✅ Task 16: Announcement City Targeting (10 tests)
Created announcement_cities mapping table
Implemented AnnouncementCity model
Updated OfficialAnnouncement model with city relationships
Updated AnnouncementService with city filtering
Implemented setTargetCities() method
City-specific cache isolation
Support for global, single-city, and multi-city targeting
All 10 tests pass successfully ✅
Validation:
Global announcements visible to all cities
Beijing-specific announcements only visible to Beijing users
Shanghai-specific announcements only visible to Shanghai users
Multi-city announcements visible to all target cities
Guangzhou users only see global announcements
City cache isolation working correctly
Target cities can be updated dynamically
announcement_cities table stores mappings correctly
All tests pass (10/10) ✅
---
Sprint 2 - Task 17: Announcement → Notifications + Push (Event-Driven)
Notification System Overview
When an important or emergency announcement is published, the system automatically creates in-app notifications and push notifications for all active users. This is implemented using an event-driven architecture with the Outbox pattern.
#### Event-Driven Flow
`
1. Admin publishes announcement
↓
2. AnnouncementService::publishAnnouncement()
↓
3. Publish "announcement_published" event to Outbox
↓
4. OutboxWorker processes event
↓
5. AnnouncementPublishedHandler triggered
↓
6. Create in-app notifications for all users
↓
7. Queue push notifications (conditional)
↓
8. Push worker sends notifications to devices
`
#### Notification Rules by Level
Info Level:
❌ No in-app notifications
❌ No push notifications
Users see it in feed only
Important Level:
✅ In-app notifications for all users
✅ Push notifications ONLY for users with push enabled
Respects user push preferences
Emergency Level:
✅ In-app notifications for all users
✅ Push notifications for ALL users (forced)
Ignores user push preferences
High priority push
#### Notification Endpoints
Note: Notification management endpoints (list, read, mark as read) are not yet implemented. The current implementation focuses on automatic notification creation when announcements are published.
Future Endpoints (To be implemented):
GET /notifications
- List user notifications
POST /notifications/{id}/read
- Mark notification as read
POST /devices
- Register device for push notifications
DELETE /devices/{id}
- Unregister device
#### Notification Data Structure
In-App Notification:
`
json
{
"id": 1,
"user_id": 123,
"type": "announcement",
"title": "System Maintenance Notice",
"content": "We will perform system maintenance tonight.",
"data": {
"level": "important",
"announcement_id": 5,
"link_url": "https://pawconnect.com/maintenance"
},
"is_read": false,
"read_at": null,
"created_at": "2025-12-31T10:00:00Z"
}
`
Push Notification:
`
json
{
"id": 1,
"user_id": 123,
"device_id": 456,
"title": "System Maintenance Notice",
"body": "We will perform system maintenance tonight.",
"data": {
"type": "announcement",
"level": "important",
"announcement_id": 5
},
"priority": "high",
"status": "pending",
"sent_at": null,
"created_at": "2025-12-31T10:00:00Z"
}
`
#### Push Notification Priority
Normal Priority:
Used for important announcements
Respects device battery optimization
May be delayed if device is in low power mode
High Priority:
Used for emergency announcements
Bypasses battery optimization
Delivered immediately
Wakes up device if needed
#### Device Registration
Users must register their devices to receive push notifications:
Device Data Structure:
`
json
{
"id": 1,
"user_id": 123,
"device_token": "fcm_token_here",
"device_type": "ios",
"push_enabled": true,
"created_at": "2025-12-31T10:00:00Z",
"updated_at": "2025-12-31T10:00:00Z"
}
`
Device Types:
ios
- iOS devices (APNs)
android
- Android devices (FCM)
web
- Web browsers (Web Push)
#### Notification Service Methods
Create Bulk Notifications:
`
php
$notificationService = app(\App\Services\NotificationService::class);
$notificationService->createBulkNotifications(
userIds: [1, 2, 3],
type: 'announcement',
title: 'System Maintenance',
content: 'We will perform maintenance tonight.',
data: ['announcement_id' => 5]
);
`
Create Bulk Push Notifications:
`
php
$notificationService->createBulkPushNotifications(
userIds: [1, 2, 3],
title: 'System Maintenance',
body: 'We will perform maintenance tonight.',
data: ['type' => 'announcement', 'announcement_id' => 5],
priority: 'high'
);
`
Get Active Users by City:
`
php
$userIds = $notificationService->getActiveUsersByCity(cityId: 1);
// Returns: [1, 2, 3, ...]
`
Get Users with Push Enabled:
`
php
$userIds = $notificationService->getUsersWithPushEnabled(userIds: [1, 2, 3]);
// Returns: [1, 3] (user 2 has push disabled)
`
#### Publishing Announcements
Using AnnouncementService:
`
php
$announcementService = app(\App\Services\AnnouncementService::class);
// Publish announcement (triggers notifications)
$announcementService->publishAnnouncement($announcementId);
`
What happens:
1. Announcement is marked as active
2. announcement_published
event is published to Outbox
3. OutboxWorker processes the event
4. AnnouncementPublishedHandler creates notifications
5. Push notifications are queued
6. Push worker sends notifications to devices
#### City-Targeted Notifications
When an announcement targets specific cities, notifications are only sent to users in those cities:
Example:
`
php
// Create Beijing-only announcement
$announcement = OfficialAnnouncement::create([
'title' => 'Beijing Maintenance',
'content' => 'Server maintenance tonight',
'level' => 'important',
]);
$announcementService->setTargetCities($announcement->id, [1]); // Beijing
$announcementService->publishAnnouncement($announcement->id);
// Result: Only Beijing users receive notifications
`
#### Database Schema
notifications Table:
id
- Primary key
user_id
- Foreign key to users table
type
- Notification type (e.g., 'announcement', 'comment', 'like')
title
- Notification title
content
- Notification content (nullable)
data
- JSON metadata
is_read
- Boolean: has user read this notification
read_at
- Timestamp: when user read the notification (nullable)
created_at
- Creation timestamp
updated_at
- Update timestamp
Index on (user_id, is_read) for fast queries
user_devices Table:
id
- Primary key
user_id
- Foreign key to users table
device_token
- Device push token (max 255 chars)
device_type
- Enum: ios, android, web
push_enabled
- Boolean: is push enabled for this device
last_used_at
- Timestamp: last time device was active (nullable)
created_at
- Creation timestamp
updated_at
- Update timestamp
Index on user_id for fast lookups
push_queue Table:
id
- Primary key
user_id
- Foreign key to users table
device_id
- Foreign key to user_devices table
title
- Push notification title
body
- Push notification body
data
- JSON metadata
priority
- Enum: normal, high
status
- Enum: pending, sent, failed
sent_at
- Timestamp: when push was sent (nullable)
error_message
- Error message if failed (nullable)
created_at
- Creation timestamp
updated_at
- Update timestamp
Index on (status, created_at) for worker queries
#### Benefits
✅ Event-Driven: Decoupled architecture using Outbox pattern
✅ Reliable: Guaranteed delivery with retry mechanism
✅ Scalable: Async processing handles high volume
✅ Flexible: Different rules for different announcement levels
✅ User Control: Respects user push preferences (except emergencies)
✅ City Targeting: Notifications respect city targeting
✅ Idempotent: Prevents duplicate notifications
#### Testing Notifications
Test Scenario 1: Important Announcement
`
php
// Create important announcement
$announcement = OfficialAnnouncement::create([
'title' => 'System Maintenance',
'content' => 'We will perform maintenance tonight.',
'level' => 'important',
]);
// Publish (triggers notifications)
$announcementService->publishAnnouncement($announcement->id);
// Result:
// - All users get in-app notifications
// - Only users with push_enabled=true get push notifications
// - Push priority: normal
`
Test Scenario 2: Emergency Announcement
`
php
// Create emergency announcement
$announcement = OfficialAnnouncement::create([
'title' => 'Service Disruption',
'content' => 'We are experiencing technical difficulties.',
'level' => 'emergency',
]);
// Publish (triggers notifications)
$announcementService->publishAnnouncement($announcement->id);
// Result:
// - All users get in-app notifications
// - ALL users get push notifications (forced)
// - Push priority: high
// - Ignores push_enabled setting
`
Test Scenario 3: Info Announcement
`
php
// Create info announcement
$announcement = OfficialAnnouncement::create([
'title' => 'Welcome to PawConnect',
'content' => 'Thank you for joining our community.',
'level' => 'info',
]);
// Publish (no notifications)
$announcementService->publishAnnouncement($announcement->id);
// Result:
// - No in-app notifications
// - No push notifications
// - Users see it in feed only
`
#### Implementation Notes
1. Outbox Pattern: Events are stored in event_outbox
table and processed by OutboxWorker
2. Idempotency: Each event has a unique event_id` to prevent duplicate processing
3.
Bulk Operations: Notifications are created in bulk for performance
4.
Async Processing: Push notifications are queued and sent by a separate worker
5.
Error Handling: Failed push notifications are marked with error messages
6.
Retry Logic: Failed pushes can be retried by the push worker
---
Task Completion Summary (Updated)
Sprint 2 (In Progress):
✅ Task 15: Official Announcement System (10 tests)
✅ Task 16: Announcement City Targeting (10 tests)
✅ Task 17: Announcement → Notifications + Push (10 tests)
Created notifications, user_devices, and push_queue tables
Implemented Notification, UserDevice, PushQueue models
Implemented NotificationService with bulk operations
Implemented AnnouncementPublishedHandler (event-driven)
Updated AnnouncementService with publishAnnouncement()
Different notification rules for each announcement level
Push notifications respect user preferences (except emergencies)
City-targeted notifications
Event-driven architecture with Outbox pattern
All 10 tests pass successfully ✅
Validation:
Info announcements don't create notifications
Important announcements create notifications + conditional push
Emergency announcements create notifications + forced push
City-targeted announcements only notify users in target cities
Push notifications respect user push_enabled setting (except emergency)
Emergency push has high priority
Event-driven architecture working correctly
Bulk notification creation is efficient
All tests pass (10/10) ✅