Fix: ListChats scan NULL в string → 500

pgx не мог сканировать NULL из COALESCE(...,'') когда
OtherPhoto был *string. После преобразования subquery
photo в строки и OtherPhoto в string, scan работает.

Проверено: Алиса и Тест получают свои чаты 200, без 500.
This commit is contained in:
ga 2026-08-21 12:31:11 +00:00
parent aee85c0087
commit c1396ffb6b
2 changed files with 25 additions and 21 deletions

View File

@ -93,18 +93,17 @@ func (r *Repo) ListChats(ctx context.Context, me uuid.UUID, limit, offset int) (
} }
rows, err := r.pool.Query(ctx, ` rows, err := r.pool.Query(ctx, `
SELECT c.id, c.user_a, c.user_b, c.created_at, c.last_msg_at, SELECT c.id, c.user_a, c.user_b, c.created_at, c.last_msg_at,
CASE WHEN c.user_a = $1 THEN c.user_b ELSE c.user_a END AS other_id, CASE WHEN c.user_a = $1::uuid THEN c.user_b ELSE c.user_a END AS other_id,
u.name, COALESCE((SELECT name FROM users WHERE id = CASE WHEN c.user_a = $1::uuid THEN c.user_b ELSE c.user_a END), '') AS other_name,
u.photo_url, COALESCE((SELECT photo_url FROM users WHERE id = CASE WHEN c.user_a = $1::uuid THEN c.user_b ELSE c.user_a END), '') AS other_photo,
(SELECT COALESCE(body, '') FROM messages m COALESCE((SELECT body FROM messages m
WHERE m.chat_id = c.id AND m.deleted = FALSE WHERE m.chat_id = c.id AND m.deleted = FALSE
ORDER BY m.created_at DESC LIMIT 1) AS last_body, ORDER BY m.created_at DESC LIMIT 1), '') AS last_body,
(SELECT COUNT(*) FROM messages m (SELECT COUNT(*) FROM messages m
WHERE m.chat_id = c.id AND m.read = FALSE WHERE m.chat_id = c.id AND m.read = FALSE
AND m.sender_id != $1) AS unread AND m.sender_id != $1::uuid) AS unread
FROM chats c FROM chats c
JOIN users u ON u.id = CASE WHEN c.user_a = $1 THEN c.user_b ELSE c.user_a END WHERE c.user_a = $1::uuid OR c.user_b = $1::uuid
WHERE c.user_a = $1 OR c.user_b = $1
ORDER BY c.last_msg_at DESC ORDER BY c.last_msg_at DESC
LIMIT $2 OFFSET $3`, LIMIT $2 OFFSET $3`,
me, limit, offset, me, limit, offset,
@ -117,14 +116,10 @@ func (r *Repo) ListChats(ctx context.Context, me uuid.UUID, limit, offset int) (
var out []ChatListItem var out []ChatListItem
for rows.Next() { for rows.Next() {
var c ChatListItem var c ChatListItem
var photo *string
if err := rows.Scan(&c.Chat.ID, &c.Chat.UserA, &c.Chat.UserB, &c.Chat.CreatedAt, &c.Chat.LastMsgAt, if err := rows.Scan(&c.Chat.ID, &c.Chat.UserA, &c.Chat.UserB, &c.Chat.CreatedAt, &c.Chat.LastMsgAt,
&c.OtherID, &c.OtherName, &photo, &c.LastMessage, &c.UnreadCount); err != nil { &c.OtherID, &c.OtherName, &c.OtherPhoto, &c.LastMessage, &c.UnreadCount); err != nil {
return nil, err return nil, err
} }
if photo != nil {
c.OtherPhoto = *photo
}
c.LastMsgAt = c.Chat.LastMsgAt c.LastMsgAt = c.Chat.LastMsgAt
out = append(out, c) out = append(out, c)
} }

View File

@ -335,6 +335,10 @@ type debugLogReq struct {
UA string `json:"ua"` UA string `json:"ua"`
Err string `json:"err,omitempty"` Err string `json:"err,omitempty"`
IsTelegram bool `json:"is_telegram"` IsTelegram bool `json:"is_telegram"`
Kind string `json:"kind,omitempty"`
UserID string `json:"userId,omitempty"`
Msg string `json:"msg,omitempty"`
Stack string `json:"stack,omitempty"`
} }
func (h *Handlers) DebugLog(c *fiber.Ctx) error { func (h *Handlers) DebugLog(c *fiber.Ctx) error {
@ -342,7 +346,7 @@ func (h *Handlers) DebugLog(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad body"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad body"})
} }
slog.Info("client-debug", attrs := []any{
"path", req.Path, "path", req.Path,
"status", req.Status, "status", req.Status,
"ms", req.Ms, "ms", req.Ms,
@ -350,6 +354,11 @@ func (h *Handlers) DebugLog(c *fiber.Ctx) error {
"ua", req.UA, "ua", req.UA,
"err", req.Err, "err", req.Err,
"ip", c.IP(), "ip", c.IP(),
) }
if req.Kind != "" {
attrs = append(attrs, "kind", req.Kind, "userId", req.UserID, "msg", req.Msg)
if req.Stack != "" { attrs = append(attrs, "stack", req.Stack) }
}
slog.Info("client-debug", attrs...)
return c.JSON(fiber.Map{"ok": true}) return c.JSON(fiber.Map{"ok": true})
} }