初始化蓝鸟文件快递柜源码与部署文档

This commit is contained in:
lhc committed 2026-10-05 23:41:07 +08:00
commit 5643d22242
70 files changed
+18669

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
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)
}