feat(postcard): profile country, country_from, per-user card number uniqueness, public page overhaul - Profile: add country field for default origin country - Postcard: add country_from field, auto-fill from profile.country on create - card_number uniqueness: change from global to per-user (profile_id + card_number) - Public pages: remove card_number display, only show cards with image_front - Public pages: show country as from→to format - Index page: no time display, add register button, fix lightbox with prev/next - Infinite scroll: add /api/public/cards endpoints and IntersectionObserver - Internal pages: update postcards list/detail for country_from display - i18n: add profiles.country_ph, pc_form.country_from, pc_detail.route

This commit is contained in:
2026-07-07 12:32:24 +08:00
parent bd664d23be
commit a071b3dde4
12 changed files with 414 additions and 90 deletions

View File

@@ -81,13 +81,16 @@ def get_api_user_optional(
class ProfileCreate(BaseModel):
nickname: str
country: str | None = None
class ProfileUpdate(BaseModel):
nickname: str | None = None
country: str | None = None
class ProfileOut(BaseModel):
id: int
nickname: str
country: str | None
showcase_enabled: bool
showcase_visible: str
notes: str
@@ -101,6 +104,7 @@ class PostcardCreate(BaseModel):
status: str
recipient_name: str | None = None
country: str | None = None
country_from: str | None = None
send_time: str | None = None
arrival_time: str | None = None
sender_name: str | None = None
@@ -112,6 +116,7 @@ class PostcardUpdate(BaseModel):
status: str | None = None
recipient_name: str | None = None
country: str | None = None
country_from: str | None = None
send_time: str | None = None
arrival_time: str | None = None
sender_name: str | None = None
@@ -125,6 +130,7 @@ class PostcardOut(BaseModel):
status: str
recipient_name: str | None
country: str | None
country_from: str | None
send_time: datetime | None
arrival_time: datetime | None
sender_name: str | None
@@ -205,7 +211,8 @@ def create_profile(
db: DbSession = Depends(get_db),
ak: ApiKey = Depends(require_scopes(PERM_PROFILES_WRITE)),
):
p = Profile(nickname=body.nickname.strip(), user_id=ak.user_id)
c = body.country.strip().upper() if body.country else None
p = Profile(nickname=body.nickname.strip(), user_id=ak.user_id, country=c)
db.add(p)
db.commit()
db.refresh(p)
@@ -231,6 +238,8 @@ def update_profile(
p = _profile_or_404(ak.user, profile_id, db)
if body.nickname is not None:
p.nickname = body.nickname.strip()
if body.country is not None:
p.country = body.country.strip().upper() if body.country else None
db.commit()
db.refresh(p)
return p
@@ -275,7 +284,10 @@ def create_postcard(
):
_profile_or_404(ak.user, profile_id, db)
cn = body.card_number.strip()
if db.query(Postcard).filter(Postcard.card_number == cn).first():
dup = db.query(Postcard).join(Profile).filter(
Postcard.card_number == cn, Profile.user_id == ak.user.id
).first()
if dup:
raise HTTPException(status_code=409, detail="Card number already exists")
pc = Postcard(
profile_id=profile_id,
@@ -283,6 +295,7 @@ def create_postcard(
status=body.status,
recipient_name=body.recipient_name,
country=body.country.strip().upper() if body.country else None,
country_from=body.country_from.strip().upper() if body.country_from else None,
send_time=_parse_date(body.send_time),
arrival_time=_parse_date(body.arrival_time),
sender_name=body.sender_name,
@@ -320,12 +333,17 @@ def update_postcard(
setattr(pc, field, val.strip() if isinstance(val, str) else val)
if body.card_number is not None:
cn = body.card_number.strip()
dup = db.query(Postcard).filter(Postcard.card_number == cn, Postcard.id != postcard_id).first()
dup = db.query(Postcard).join(Profile).filter(
Postcard.card_number == cn, Postcard.id != postcard_id,
Profile.user_id == ak.user.id
).first()
if dup:
raise HTTPException(status_code=409, detail="Card number already exists")
pc.card_number = cn
if body.country is not None:
pc.country = body.country.strip().upper() if body.country else None
if body.country_from is not None:
pc.country_from = body.country_from.strip().upper() if body.country_from else None
for field in ("send_time", "arrival_time", "receive_time"):
val = getattr(body, field)
if val is not None: