package app import ( "database/sql" "errors" "net/http" "net/url" "strconv" "strings" ) type adminListResult struct { Items []map[string]any `json:"items"` Total int `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` Pages int `json:"pages"` } func adminPaging(q url.Values, defaultSize int) (adminListResult, error) { p := adminListResult{Items: []map[string]any{}, Page: 1, PageSize: defaultSize, Pages: 1} for key, dest := range map[string]*int{"page": &p.Page, "page_size": &p.PageSize} { if raw := q.Get(key); raw != "" { n, err := strconv.Atoi(raw) if err != nil || n < 1 || (key == "page_size" && n > 100) { return p, errors.New("页码须为正整数,每页条数须为1至100") } *dest = n } } return p, nil } func (p *adminListResult) setTotal(total int) { p.Total = total p.Pages = max(1, (total+p.PageSize-1)/p.PageSize) p.Page = min(p.Page, p.Pages) } func adminFilter(q url.Values, key string, limit int) (string, error) { v := strings.TrimSpace(q.Get(key)) if len(v) > limit { return "", errors.New("搜索条件过长") } return v, nil } func (a *App) adminParcels(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } q := r.URL.Query() result, err := adminPaging(q, 10) if err != nil { fail(w, 400, err.Error()) return } keyword, err := adminFilter(q, "q", 256) if err != nil { fail(w, 400, err.Error()) return } state, err := adminFilter(q, "state", 32) if err != nil { fail(w, 400, err.Error()) return } if state != "" && state != "uploading" && state != "ready" && state != "revoked" && state != "expired" && state != "exhausted" && state != "restored" { fail(w, 400, "包裹状态无效") return } where := " WHERE 1=1" args := []any{} if keyword != "" { where += " AND (instr(lower(p.id),lower(?))>0 OR EXISTS(SELECT 1 FROM files f WHERE f.parcel=p.id AND instr(lower(f.name),lower(?))>0))" args = append(args, keyword, keyword) } if state != "" { where += " AND p.state=?" args = append(args, state) } // File details use the same connection after the rows are closed. Prevent // cleanup/publication from changing the selected parcels between those reads. a.mu.Lock() defer a.mu.Unlock() var total int if err = a.db.QueryRowContext(r.Context(), "SELECT count(*) FROM parcels p"+where, args...).Scan(&total); err != nil { a.serverError(w, err) return } result.setTotal(total) listArgs := append(append([]any{}, args...), result.PageSize, (result.Page-1)*result.PageSize) rows, err := a.db.QueryContext(r.Context(), "SELECT p.id,p.state,p.ip,p.created,p.expires,p.renewal_days,p.renewal_mode,(SELECT count(*) FROM files WHERE parcel=p.id),(SELECT COALESCE(sum(size),0) FROM files WHERE parcel=p.id) FROM parcels p"+where+" ORDER BY p.created DESC,p.id DESC LIMIT ? OFFSET ?", listArgs...) if err != nil { a.serverError(w, err) return } for rows.Next() { var id, state, ip, mode string var created, expiry, size int64 var renewal, count int if err = rows.Scan(&id, &state, &ip, &created, &expiry, &renewal, &mode, &count, &size); err != nil { rows.Close() a.serverError(w, err) return } result.Items = append(result.Items, map[string]any{"id": id, "state": state, "ip": ip, "created": created, "expires": expiry, "renewal_days": renewal, "renewal_mode": mode, "count": count, "size": size}) } err = rows.Err() rows.Close() if err != nil { a.serverError(w, err) return } for _, p := range result.Items { files, err := a.fileList(p["id"].(string)) if err != nil { a.serverError(w, err) return } p["files"] = files } respond(w, 200, result) } func (a *App) adminAudit(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } q := r.URL.Query() result, err := adminPaging(q, 20) if err != nil { fail(w, 400, err.Error()) return } where := " WHERE 1=1" args := []any{} for _, f := range []struct { key string limit int }{{"action", 64}, {"source", 128}, {"target", 256}} { value, err := adminFilter(q, f.key, f.limit) if err != nil { fail(w, 400, err.Error()) return } if value == "" { continue } if f.key == "action" { where += " AND action=?" } else { where += " AND instr(lower(" + f.key + "),lower(?))>0" } args = append(args, value) } var from, to int64 for _, bound := range []struct { key, op string dest *int64 }{{"from", ">=", &from}, {"to", "<=", &to}} { if raw := q.Get(bound.key); raw != "" { value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value < 0 { fail(w, 400, "时间须为非负Unix秒数") return } *bound.dest = value where += " AND created" + bound.op + "?" args = append(args, value) } } if q.Get("from") != "" && q.Get("to") != "" && from > to { fail(w, 400, "开始时间不能晚于结束时间") return } // Count and items share a snapshot so concurrent audit writes or retention do // not change the response's total and page boundaries mid-query. tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{ReadOnly: true}) if err != nil { a.serverError(w, err) return } defer tx.Rollback() var total int if err = tx.QueryRowContext(r.Context(), "SELECT count(*) FROM audit"+where, args...).Scan(&total); err != nil { a.serverError(w, err) return } result.setTotal(total) listArgs := append(append([]any{}, args...), result.PageSize, (result.Page-1)*result.PageSize) rows, err := tx.QueryContext(r.Context(), "SELECT id,created,action,target,source FROM audit"+where+" ORDER BY id DESC LIMIT ? OFFSET ?", listArgs...) if err != nil { a.serverError(w, err) return } for rows.Next() { var id, created int64 var action, target, source string if err = rows.Scan(&id, &created, &action, &target, &source); err != nil { rows.Close() a.serverError(w, err) return } result.Items = append(result.Items, map[string]any{"id": id, "created": created, "action": action, "target": target, "source": source}) } err = rows.Err() rows.Close() if err != nil { a.serverError(w, err) return } if err = tx.Commit(); err != nil { a.serverError(w, err) return } respond(w, 200, result) }