302 lines
8.3 KiB
Go
302 lines
8.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.asio.asia/nixevol/NixMsg/internal/httpx"
|
|
)
|
|
|
|
// 消息查询只读 messages / deliveries,不读 message_bodies,响应不含 body/正文。
|
|
|
|
func (h *Handler) handleMessageList(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
limit, offset, ok := parsePage(w, q.Get("limit"), q.Get("cursor"))
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
where, args, ok := buildMessageFilter(w, q)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var total int
|
|
if err := h.db.Read.QueryRowContext(r.Context(),
|
|
`SELECT COUNT(*) FROM messages m WHERE `+where, args...).Scan(&total); err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
|
|
listArgs := append(append([]any{}, args...), limit, offset)
|
|
rows, err := h.db.Read.QueryContext(r.Context(), `
|
|
SELECT m.seq, m.id, m.sender_id, m.dest_kind, m.dest_id, m.state, m.reason,
|
|
m.send_at, m.created_at, m.keep, m.receipt, m.content_type
|
|
FROM messages m
|
|
WHERE `+where+`
|
|
ORDER BY m.seq DESC
|
|
LIMIT ? OFFSET ?`, listArgs...)
|
|
if err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
items := make([]map[string]any, 0)
|
|
seqs := make([]int64, 0)
|
|
for rows.Next() {
|
|
var seq int64
|
|
var id, sender, destKind, destID, state, reason, contentType string
|
|
var sendAt, created int64
|
|
var keep, receipt int
|
|
if scanErr := rows.Scan(&seq, &id, &sender, &destKind, &destID, &state, &reason,
|
|
&sendAt, &created, &keep, &receipt, &contentType); scanErr != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
seqs = append(seqs, seq)
|
|
items = append(items, map[string]any{
|
|
"seq": seq,
|
|
"id": id,
|
|
"sender_id": sender,
|
|
"dest_kind": destKind,
|
|
"dest_id": destID,
|
|
"state": state,
|
|
"reason": reason,
|
|
"send_at_ms": sendAt,
|
|
"created_at_ms": created,
|
|
"keep": keep != 0,
|
|
"receipt": receipt != 0,
|
|
"content_type": contentType,
|
|
})
|
|
}
|
|
if rowsErr := rows.Err(); rowsErr != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
|
|
counts, err := h.deliveryCounts(r.Context(), seqs)
|
|
if err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
for i, seq := range seqs {
|
|
c := counts[seq]
|
|
if c == nil {
|
|
c = emptyDeliveryCounts()
|
|
}
|
|
items[i]["delivery_counts"] = c
|
|
}
|
|
|
|
next := ""
|
|
if offset+len(items) < total {
|
|
next = strconv.Itoa(offset + len(items))
|
|
}
|
|
httpx.WriteOK(w, map[string]any{"items": items, "next_cursor": next, "total": total})
|
|
}
|
|
|
|
func (h *Handler) handleMessageGet(w http.ResponseWriter, r *http.Request) {
|
|
seqStr := r.PathValue("seq")
|
|
seq, err := strconv.ParseInt(seqStr, 10, 64)
|
|
if err != nil || seq < 1 {
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "seq 无效")
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
limit, offset, ok := parsePage(w, q.Get("limit"), q.Get("cursor"))
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var id, sender, destKind, destID, state, reason, contentType, meta string
|
|
var sendAt, created int64
|
|
err = h.db.Read.QueryRowContext(r.Context(), `
|
|
SELECT id, sender_id, dest_kind, dest_id, state, reason, send_at, created_at,
|
|
content_type, meta
|
|
FROM messages WHERE seq = ?`, seq).Scan(
|
|
&id, &sender, &destKind, &destID, &state, &reason, &sendAt, &created,
|
|
&contentType, &meta,
|
|
)
|
|
if isNoRows(err) {
|
|
httpx.WriteError(w, http.StatusNotFound, "not_found", "消息不存在")
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
|
|
var total int
|
|
_ = h.db.Read.QueryRowContext(r.Context(),
|
|
`SELECT COUNT(*) FROM deliveries WHERE seq = ?`, seq).Scan(&total)
|
|
|
|
rows, err := h.db.Read.QueryContext(r.Context(), `
|
|
SELECT endpoint_id, state, reason, attempts, pushed_at, updated_at
|
|
FROM deliveries WHERE seq = ?
|
|
ORDER BY endpoint_id ASC
|
|
LIMIT ? OFFSET ?`, seq, limit, offset)
|
|
if err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
deliveries := make([]map[string]any, 0)
|
|
for rows.Next() {
|
|
var endpointID, dState, dReason string
|
|
var attempts int
|
|
var pushedAt, updatedAt sql.NullInt64
|
|
if scanErr := rows.Scan(&endpointID, &dState, &dReason, &attempts, &pushedAt, &updatedAt); scanErr != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
deliveries = append(deliveries, map[string]any{
|
|
"endpoint_id": endpointID,
|
|
"state": dState,
|
|
"reason": dReason,
|
|
"attempts": attempts,
|
|
"pushed_at_ms": nullInt64API(pushedAt),
|
|
"updated_at_ms": updatedAt.Int64,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
|
|
next := ""
|
|
if offset+len(deliveries) < total {
|
|
next = strconv.Itoa(offset + len(deliveries))
|
|
}
|
|
|
|
metaObj := any(map[string]any{})
|
|
if strings.TrimSpace(meta) != "" && meta != "{}" {
|
|
metaObj = jsonRawOrObject(meta)
|
|
}
|
|
|
|
httpx.WriteOK(w, map[string]any{
|
|
"seq": seq,
|
|
"id": id,
|
|
"sender_id": sender,
|
|
"dest_kind": destKind,
|
|
"dest_id": destID,
|
|
"state": state,
|
|
"reason": reason,
|
|
"send_at_ms": sendAt,
|
|
"created_at_ms": created,
|
|
"meta": metaObj,
|
|
"content_type": contentType,
|
|
"deliveries": deliveries,
|
|
"next_cursor": next,
|
|
})
|
|
}
|
|
|
|
func buildMessageFilter(w http.ResponseWriter, q interface{ Get(string) string }) (where string, args []any, ok bool) {
|
|
conds := []string{"1=1"}
|
|
args = make([]any, 0, 8)
|
|
|
|
if v := strings.TrimSpace(q.Get("sender_id")); v != "" {
|
|
conds = append(conds, "m.sender_id = ?")
|
|
args = append(args, v)
|
|
}
|
|
if v := strings.TrimSpace(q.Get("group_id")); v != "" {
|
|
conds = append(conds, "m.dest_kind = 'group' AND m.dest_id = ?")
|
|
args = append(args, v)
|
|
}
|
|
if v := strings.TrimSpace(q.Get("state")); v != "" {
|
|
switch v {
|
|
case "scheduled", "dispatched", "completed":
|
|
conds = append(conds, "m.state = ?")
|
|
args = append(args, v)
|
|
default:
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "state 无效")
|
|
return "", nil, false
|
|
}
|
|
}
|
|
if v := strings.TrimSpace(q.Get("from_ms")); v != "" {
|
|
n, okParse := parseInt64Query(v)
|
|
if !okParse {
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "from_ms 无效")
|
|
return "", nil, false
|
|
}
|
|
conds = append(conds, "m.created_at >= ?")
|
|
args = append(args, n)
|
|
}
|
|
if v := strings.TrimSpace(q.Get("to_ms")); v != "" {
|
|
n, okParse := parseInt64Query(v)
|
|
if !okParse {
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "to_ms 无效")
|
|
return "", nil, false
|
|
}
|
|
conds = append(conds, "m.created_at <= ?")
|
|
args = append(args, n)
|
|
}
|
|
if v := strings.TrimSpace(q.Get("endpoint_id")); v != "" {
|
|
conds = append(conds, `EXISTS (
|
|
SELECT 1 FROM deliveries d WHERE d.seq = m.seq AND d.endpoint_id = ?)`)
|
|
args = append(args, v)
|
|
}
|
|
|
|
return strings.Join(conds, " AND "), args, true
|
|
}
|
|
|
|
func (h *Handler) deliveryCounts(ctx context.Context, seqs []int64) (map[int64]map[string]int, error) {
|
|
out := make(map[int64]map[string]int, len(seqs))
|
|
if len(seqs) == 0 {
|
|
return out, nil
|
|
}
|
|
placeholders := make([]string, len(seqs))
|
|
args := make([]any, len(seqs))
|
|
for i, s := range seqs {
|
|
placeholders[i] = "?"
|
|
args[i] = s
|
|
out[s] = emptyDeliveryCounts()
|
|
}
|
|
rows, err := h.db.Read.QueryContext(ctx, `
|
|
SELECT seq, state, COUNT(*) FROM deliveries
|
|
WHERE seq IN (`+strings.Join(placeholders, ",")+`)
|
|
GROUP BY seq, state`, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
for rows.Next() {
|
|
var seq int64
|
|
var state string
|
|
var n int
|
|
if scanErr := rows.Scan(&seq, &state, &n); scanErr != nil {
|
|
return nil, scanErr
|
|
}
|
|
c := out[seq]
|
|
switch state {
|
|
case "pending", "accepted", "recalled", "expired", "dropped", "rejected":
|
|
c[state] = n
|
|
}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func emptyDeliveryCounts() map[string]int {
|
|
return map[string]int{
|
|
"pending": 0,
|
|
"accepted": 0,
|
|
"recalled": 0,
|
|
"expired": 0,
|
|
"dropped": 0,
|
|
"rejected": 0,
|
|
}
|
|
}
|
|
|
|
func jsonRawOrObject(s string) any {
|
|
// meta 存的是 JSON 文本;解析失败时返回空对象,避免把原始串当正文泄漏路径
|
|
var v any
|
|
if err := json.Unmarshal([]byte(s), &v); err != nil || v == nil {
|
|
return map[string]any{}
|
|
}
|
|
return v
|
|
}
|