o2o-site-AEO/solution/backend/crud/social_crud.py
hbyang 58d6249102 Merge remote-tracking branch 'origin/main' into feature/social-post
# Conflicts:
#	docs/DECISIONS.md
#	docs/DEVLOG.md
#	solution/backend/requirements.txt
#	solution/backend/scheduler/__init__.py
#	solution/backend/worker/handlers.py
#	solution/site/src/pages/HomePage.tsx
#	solution/site/src/sections/index.ts
2026-09-16 08:46:48 +09:00

68 lines
2.5 KiB
Python

"""승인 CAS와 큐 적재를 같은 트랜잭션으로 묶어 승인 후 잡 유실을 막는다."""
import json
from sqlalchemy import text
from common.enums import JobType
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import place_social_posts as Post
async def transaction(fn):
return await DB_SESSION_MNG.execute_lambda_write(Post.DBType(), fn)
async def enqueue(s, post_id, job_type):
await s.execute(
text("""INSERT INTO jobs(job_type, payload, dedupe_key, max_attempts)
VALUES (:type, CAST(:payload AS jsonb), :key, 1)
ON CONFLICT (dedupe_key) WHERE status IN (1,2) AND dedupe_key IS NOT NULL DO NOTHING"""),
{
"type": job_type,
"payload": json.dumps({"post_id": str(post_id)}),
"key": f"social:{job_type}:{post_id}",
},
)
await s.execute(text("SELECT pg_notify('web4ai_job', '')"))
async def decide(s, post_id, sha, approve, via):
row = (
await s.execute(
text("""UPDATE place_social_posts
SET status=:status, decided_at=now(), decided_via=:via, updated_at=now()
WHERE post_id=:id AND deleted=false AND status='PENDING_APPROVAL'
AND approval_token_sha=:sha AND approval_expires_at>now()
RETURNING post_id, account_id"""),
{
"status": "APPROVED" if approve else "DECLINED",
"via": via,
"id": post_id,
"sha": sha,
},
)
).first()
if row and approve and row.account_id:
await enqueue(s, post_id, JobType.SOCIAL_POST.value)
return bool(row)
async def sweep():
async def run(s):
await s.execute(
text("""UPDATE place_social_posts SET status='EXPIRED', updated_at=now()
WHERE deleted=false AND status='PENDING_APPROVAL' AND approval_expires_at<=now()""")
)
# POSTING은 외부가 받았을 수 있다. 시간을 근거로 APPROVED로 돌리지 않는다.
await s.execute(
text("""UPDATE place_social_posts SET status='UNKNOWN',
last_error='POST_RESULT_UNKNOWN', updated_at=now()
WHERE deleted=false AND status='POSTING' AND updated_at<now()-interval '10 minutes'""")
)
await s.execute(
text("""UPDATE place_social_posts SET status='FAILED',
last_error='DRAFT_INTERRUPTED', updated_at=now()
WHERE deleted=false AND status='DRAFTING' AND updated_at<now()-interval '10 minutes'""")
)
await transaction(run)