feat(media-supply): persist favorites with multi-group membership
Move media favorites from client state to PostgreSQL with server-side search and pagination. - Store favorites keyed by (tenant, user, group, resource) so a resource can belong to multiple groups; the 500-resource cap counts distinct resources, not memberships. - Add favorite-group CRUD and group/global removal routes, with silent cleanup of delisted or invisible resources on read. - Serve favorite resource details server-side, 10 per page, with name/ID search; favorites-page removal is group-scoped, resources-page removal is global. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
@@ -326,24 +327,107 @@ func (s *MediaSupplyService) SetResourcePrice(ctx context.Context, actor *Actor,
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) SetResourceVisibility(ctx context.Context, actor *Actor, resourceID int64, input SetMediaSupplyVisibilityInput) error {
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE supplier_media_resources
|
||||
SET customer_visible = $3, updated_at = NOW()
|
||||
WHERE id = $1 AND supplier = $2 AND deleted_at IS NULL
|
||||
`, resourceID, opsMediaSupplySupplierMeijiequan, input.CustomerVisible)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if !input.CustomerVisible {
|
||||
ownerRows, err := tx.Query(ctx, `
|
||||
SELECT favorite_owner.tenant_id, favorite_owner.user_id
|
||||
FROM media_supply_favorite_owners favorite_owner
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM media_supply_favorite_resources favorite
|
||||
WHERE favorite.tenant_id = favorite_owner.tenant_id
|
||||
AND favorite.user_id = favorite_owner.user_id
|
||||
AND favorite.resource_id = $1
|
||||
)
|
||||
ORDER BY favorite_owner.tenant_id, favorite_owner.user_id
|
||||
FOR UPDATE OF favorite_owner
|
||||
`, resourceID)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
for ownerRows.Next() {
|
||||
var tenantID, userID int64
|
||||
if err := ownerRows.Scan(&tenantID, &userID); err != nil {
|
||||
ownerRows.Close()
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
}
|
||||
if err := ownerRows.Err(); err != nil {
|
||||
ownerRows.Close()
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
ownerRows.Close()
|
||||
}
|
||||
var lockedResourceID int64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT id
|
||||
FROM supplier_media_resources
|
||||
WHERE id = $1 AND supplier = $2 AND deleted_at IS NULL
|
||||
FOR UPDATE
|
||||
`, resourceID, opsMediaSupplySupplierMeijiequan).Scan(&lockedResourceID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return response.ErrNotFound(40460, "media_supply_resource_not_found", "媒体资源不存在或已下架")
|
||||
}
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
|
||||
var updated, removedFavorites int
|
||||
err = tx.QueryRow(ctx, `
|
||||
WITH updated_resource AS (
|
||||
UPDATE supplier_media_resources
|
||||
SET customer_visible = $3, updated_at = NOW()
|
||||
WHERE id = $1 AND supplier = $2 AND deleted_at IS NULL
|
||||
RETURNING id
|
||||
), deleted_favorites AS (
|
||||
DELETE FROM media_supply_favorite_resources favorite
|
||||
USING updated_resource resource
|
||||
WHERE $3 = FALSE AND favorite.resource_id = resource.id
|
||||
RETURNING favorite.tenant_id, favorite.user_id, favorite.group_key
|
||||
), touched_groups AS (
|
||||
UPDATE media_supply_favorite_groups favorite_group
|
||||
SET updated_at = NOW()
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM deleted_favorites deleted
|
||||
WHERE deleted.tenant_id = favorite_group.tenant_id
|
||||
AND deleted.user_id = favorite_group.user_id
|
||||
AND deleted.group_key = favorite_group.group_key
|
||||
)
|
||||
RETURNING favorite_group.tenant_id, favorite_group.user_id
|
||||
), touched_owners AS (
|
||||
UPDATE media_supply_favorite_owners favorite_owner
|
||||
SET revision = revision + 1, updated_at = NOW()
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM deleted_favorites deleted
|
||||
WHERE deleted.tenant_id = favorite_owner.tenant_id
|
||||
AND deleted.user_id = favorite_owner.user_id
|
||||
)
|
||||
RETURNING favorite_owner.tenant_id
|
||||
)
|
||||
SELECT (SELECT COUNT(*) FROM updated_resource),
|
||||
(SELECT COUNT(*) FROM deleted_favorites)
|
||||
`, resourceID, opsMediaSupplySupplierMeijiequan, input.CustomerVisible).Scan(&updated, &removedFavorites)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
if updated == 0 {
|
||||
return response.ErrNotFound(40460, "media_supply_resource_not_found", "媒体资源不存在或已下架")
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return response.ErrInternal(50087, "media_supply_visibility_update_failed", "媒体展示状态更新失败")
|
||||
}
|
||||
if s.audits != nil && actor != nil {
|
||||
action := "media_supply.resource_show"
|
||||
if !input.CustomerVisible {
|
||||
action = "media_supply.resource_hide"
|
||||
}
|
||||
_ = s.audits.Append(ctx, actor.audit(action, "supplier_media_resource", resourceID, map[string]any{
|
||||
"customer_visible": input.CustomerVisible,
|
||||
"customer_visible": input.CustomerVisible,
|
||||
"removed_favorite_count": removedFavorites,
|
||||
}))
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user