161 lines
4.8 KiB
Go
161 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"freight-sfa-server/internal/auth"
|
|
)
|
|
|
|
// backupPayload 备份结构,与前端 exportBackup 的 JSON 保持一致
|
|
type backupPayload struct {
|
|
App string `json:"app"`
|
|
Version string `json:"version"`
|
|
Exported string `json:"exported"`
|
|
Clients []map[string]any `json:"clients"`
|
|
Inquiries []map[string]any `json:"inquiries"`
|
|
Bookings []map[string]any `json:"bookings"`
|
|
Followups []map[string]any `json:"followups"`
|
|
Options map[string][]string `json:"options"`
|
|
Owner string `json:"owner"`
|
|
Extra map[string]json.RawMessage `json:"extra,omitempty"`
|
|
}
|
|
|
|
// exportBackup 导出全量数据(团队共享账套)
|
|
func (s *Server) exportBackup(w http.ResponseWriter, r *http.Request) {
|
|
clients, _ := s.loadAllRecords("client")
|
|
inquiries, _ := s.loadAllRecords("inquiry")
|
|
bookings, _ := s.loadAllRecords("booking")
|
|
followups, _ := s.loadAllRecords("followup")
|
|
|
|
opts, _ := s.loadOptions()
|
|
|
|
p := backupPayload{
|
|
App: "freight-sfa",
|
|
Version: "1.0.0",
|
|
Exported: nowStr(),
|
|
Clients: clients,
|
|
Inquiries: inquiries,
|
|
Bookings: bookings,
|
|
Followups: followups,
|
|
Options: opts,
|
|
Owner: auth.Username(r),
|
|
}
|
|
writeJSON(w, http.StatusOK, p)
|
|
}
|
|
|
|
func (s *Server) loadOptions() (map[string][]string, error) {
|
|
var raw string
|
|
err := s.db.QueryRow(`SELECT data_json FROM options WHERE id=1`).Scan(&raw)
|
|
if err != nil {
|
|
return defaultOptions, nil
|
|
}
|
|
m := map[string][]string{}
|
|
if json.Unmarshal([]byte(raw), &m) != nil {
|
|
return defaultOptions, nil
|
|
}
|
|
for k, v := range defaultOptions {
|
|
if _, ok := m[k]; !ok || m[k] == nil {
|
|
m[k] = v
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// importBackup 导入备份(全量覆盖团队数据)
|
|
func (s *Server) importBackup(w http.ResponseWriter, r *http.Request) {
|
|
var p backupPayload
|
|
if err := decodeJSON(r, &p); err != nil {
|
|
writeErr(w, http.StatusBadRequest, "备份格式错误")
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "事务启动失败")
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
now := nowStr()
|
|
clearAndInsert := func(kind string, list []map[string]any) error {
|
|
if _, err := tx.Exec(`UPDATE records SET deleted_at=? WHERE kind=?`, now, kind); err != nil {
|
|
return err
|
|
}
|
|
for _, m := range list {
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id := genID(kind)
|
|
uid := auth.UserID(r)
|
|
if _, err := tx.Exec(`INSERT INTO records (id, kind, owner_id, data_json, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?)`, id, kind, uid, string(raw), now, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := clearAndInsert("client", p.Clients); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "导入客户失败")
|
|
return
|
|
}
|
|
if err := clearAndInsert("inquiry", p.Inquiries); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "导入商机失败")
|
|
return
|
|
}
|
|
if err := clearAndInsert("booking", p.Bookings); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "导入订舱失败")
|
|
return
|
|
}
|
|
if err := clearAndInsert("followup", p.Followups); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "导入跟进失败")
|
|
return
|
|
}
|
|
if p.Options != nil {
|
|
raw, _ := json.Marshal(p.Options)
|
|
if _, err := tx.Exec(`UPDATE options SET data_json=?, updated_at=? WHERE id=1`, string(raw), now); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "导入提交失败")
|
|
return
|
|
}
|
|
s.auditLog(r, "import", "backup", "", "导入备份")
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "counts": map[string]int{
|
|
"clients": len(p.Clients), "inquiries": len(p.Inquiries),
|
|
"bookings": len(p.Bookings), "followups": len(p.Followups),
|
|
}})
|
|
}
|
|
|
|
// listAuditLogs 审计日志(仅 admin 可见)
|
|
func (s *Server) listAuditLogs(w http.ResponseWriter, r *http.Request) {
|
|
if auth.Role(r) != "admin" {
|
|
writeErr(w, http.StatusForbidden, "仅管理员可查看审计日志")
|
|
return
|
|
}
|
|
rows, err := s.db.Query(`SELECT id, user_id, username, action, entity, entity_id, detail, created_at
|
|
FROM audit_logs ORDER BY id DESC LIMIT 200`)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "查询失败")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
out := []map[string]any{}
|
|
for rows.Next() {
|
|
var id int64
|
|
var uid int64
|
|
var user, action, entity, entityID, detail, created string
|
|
if err := rows.Scan(&id, &uid, &user, &action, &entity, &entityID, &detail, &created); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "数据读取失败")
|
|
return
|
|
}
|
|
out = append(out, map[string]any{
|
|
"id": id, "user_id": uid, "username": user, "action": action,
|
|
"entity": entity, "entity_id": entityID, "detail": detail, "created_at": created,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|