feat(notification): add target_mode enum constraint and merge register-notifications script

- Add NotificationTargetMode enum (new_users/exist_users/all_users/user_ids)
- Create Alembic migrations: drop duplicate indexes, add target_mode column
- Merge register-notifications.sh into dev-migrate.sh sync-notifications subcommand
- Shorten notification config path: static/notification/notifications -> static/notifications
- Update registration flow to dispatch notifications by target_mode
- Add is_first_registration to RegisterBonusResult for first-time user detection
- Remove dead code: link_published_notifications_to_user
- Update welcome_points.yaml to target new_users only
- Add 44 unit tests + 1 integration test, all passing
This commit is contained in:
qzl
2026-04-16 17:48:36 +08:00
parent d91064835b
commit c79c773d67
26 changed files with 1011 additions and 49 deletions
+4 -3
View File
@@ -73,13 +73,14 @@ async def create_email_session(
)
result = await service.create_email_session(payload)
points_service = PointsService(repository=PointsRepository(session))
await points_service.grant_register_bonus_if_eligible(
bonus_result = await points_service.grant_register_bonus_if_eligible(
user_id=UUID(result.user.id),
user_email=result.user.email,
)
notification_service = NotificationService(NotificationRepository(session))
linked_count = await notification_service.link_published_notifications_to_user(
user_id=UUID(result.user.id)
linked_count = await notification_service.link_notifications_for_registered_user(
user_id=UUID(result.user.id),
is_first_registration=bonus_result.is_first_registration,
)
await session.commit()
logger.info(
+15 -3
View File
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from models.notification import Notification
from models.user_notification import UserNotification
from schemas.enums import NotificationTargetMode
class NotificationRepository:
@@ -116,13 +117,24 @@ class NotificationRepository:
async def commit(self) -> None:
await self._session.commit()
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
async def link_notifications_for_registered_user(
self, *, user_id: UUID, is_first_registration: bool
) -> int:
target_modes: list[NotificationTargetMode]
if is_first_registration:
target_modes = [
NotificationTargetMode.NEW_USERS,
NotificationTargetMode.ALL_USERS,
]
else:
target_modes = [NotificationTargetMode.ALL_USERS]
notification_ids = list(
(
await self._session.execute(
select(Notification.id).where(
Notification.status == "published",
Notification.deleted_at.is_(None),
Notification.target_mode.in_(target_modes),
)
)
)
@@ -136,8 +148,8 @@ class NotificationRepository:
insert(UserNotification)
.values(
[
{"user_id": user_id, "notification_id": notification_id}
for notification_id in notification_ids
{"user_id": user_id, "notification_id": nid}
for nid in notification_ids
]
)
.on_conflict_do_nothing(index_elements=["user_id", "notification_id"])
+5 -3
View File
@@ -123,9 +123,11 @@ class NotificationService:
await self._repository.commit()
return updated_count
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
return await self._repository.link_published_notifications_to_user(
user_id=user_id
async def link_notifications_for_registered_user(
self, *, user_id: UUID, is_first_registration: bool
) -> int:
return await self._repository.link_notifications_for_registered_user(
user_id=user_id, is_first_registration=is_first_registration
)
+9 -3
View File
@@ -61,6 +61,7 @@ class RegisterBonusResult:
amount: int
balance_after: int
event_id: str
is_first_registration: bool = False
@dataclass(frozen=True)
@@ -122,14 +123,17 @@ class PointsService:
account = await self._repository.get_or_create_user_points_for_update(
user_id=user_id
)
if claim is not None and claim.balance_snapshot is not None:
account.balance = max(int(claim.balance_snapshot), 0)
account.version = int(account.version) + 1
if claim is not None:
is_first_registration = claim.first_user_id_snapshot is None
if claim.balance_snapshot is not None:
account.balance = max(int(claim.balance_snapshot), 0)
account.version = int(account.version) + 1
return RegisterBonusResult(
granted=False,
amount=0,
balance_after=int(account.balance),
event_id=event_id,
is_first_registration=is_first_registration,
)
claimed = await self._repository.claim_register_bonus(
@@ -144,6 +148,7 @@ class PointsService:
amount=0,
balance_after=int(account.balance),
event_id=event_id,
is_first_registration=False,
)
balance = int(account.balance)
@@ -197,6 +202,7 @@ class PointsService:
amount=bonus_points,
balance_after=int(account.balance),
event_id=event_id,
is_first_registration=True,
)
async def ensure_run_points_available(