server.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. package web
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "os"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "ftp-server/config"
  16. "ftp-server/database"
  17. "ftp-server/ftp"
  18. "github.com/golang-jwt/jwt/v5"
  19. )
  20. // Server Web管理服务器
  21. type Server struct {
  22. config *config.Config
  23. db *database.DB
  24. ftpServer *ftp.Server
  25. jwtSecret []byte
  26. }
  27. // NewServer 创建Web服务器
  28. func NewServer(cfg *config.Config, db *database.DB, ftpSrv *ftp.Server) *Server {
  29. secret := make([]byte, 32)
  30. rand.Read(secret)
  31. return &Server{
  32. config: cfg,
  33. db: db,
  34. ftpServer: ftpSrv,
  35. jwtSecret: secret,
  36. }
  37. }
  38. // Start 启动Web服务
  39. func (s *Server) Start() error {
  40. webCfg := s.config.Get().Web
  41. mux := http.NewServeMux()
  42. // 静态文件
  43. fs := http.FileServer(http.Dir("./static"))
  44. mux.Handle("/", fs)
  45. // API路由
  46. mux.HandleFunc("/api/login", s.handleLogin)
  47. mux.HandleFunc("/api/dashboard", s.authMiddleware(s.handleDashboard))
  48. mux.HandleFunc("/api/users", s.authMiddleware(s.handleUsers))
  49. mux.HandleFunc("/api/users/", s.authMiddleware(s.handleUserOperation))
  50. mux.HandleFunc("/api/files", s.authMiddleware(s.handleFileBrowse))
  51. mux.HandleFunc("/api/files/", s.authMiddleware(s.handleFileBrowse))
  52. mux.HandleFunc("/api/logs", s.authMiddleware(s.handleLogs))
  53. mux.HandleFunc("/api/online", s.authMiddleware(s.handleOnline))
  54. mux.HandleFunc("/api/config", s.authMiddleware(s.handleConfig))
  55. mux.HandleFunc("/api/upload", s.authMiddleware(s.handleUpload))
  56. mux.HandleFunc("/api/ip-rules", s.authMiddleware(s.handleIPRules))
  57. mux.HandleFunc("/api/ip-rules/", s.authMiddleware(s.handleIPRuleOperation))
  58. addr := fmt.Sprintf("%s:%d", webCfg.Host, webCfg.Port)
  59. log.Printf("Web管理界面已启动: http://localhost:%d", webCfg.Port)
  60. server := &http.Server{
  61. Addr: addr,
  62. Handler: mux,
  63. ReadTimeout: 30 * time.Second,
  64. WriteTimeout: 30 * time.Second,
  65. }
  66. return server.ListenAndServe()
  67. }
  68. // --- 中间件 ---
  69. func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
  70. return func(w http.ResponseWriter, r *http.Request) {
  71. tokenStr := r.Header.Get("Authorization")
  72. if tokenStr == "" {
  73. // 也支持 cookie
  74. if cookie, err := r.Cookie("token"); err == nil {
  75. tokenStr = cookie.Value
  76. }
  77. }
  78. if strings.HasPrefix(tokenStr, "Bearer ") {
  79. tokenStr = tokenStr[7:]
  80. }
  81. if tokenStr == "" {
  82. s.jsonError(w, "未登录", http.StatusUnauthorized)
  83. return
  84. }
  85. token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
  86. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  87. return nil, fmt.Errorf("无效的签名方法")
  88. }
  89. return s.jwtSecret, nil
  90. })
  91. if err != nil || !token.Valid {
  92. s.jsonError(w, "登录已过期", http.StatusUnauthorized)
  93. return
  94. }
  95. next(w, r)
  96. }
  97. }
  98. // --- 请求处理 ---
  99. // handleLogin 登录
  100. func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
  101. if r.Method != http.MethodPost {
  102. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  103. return
  104. }
  105. var req struct {
  106. Username string `json:"username"`
  107. Password string `json:"password"`
  108. }
  109. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  110. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  111. return
  112. }
  113. adminCfg := s.config.Get().Admin
  114. if req.Username != adminCfg.Username || req.Password != adminCfg.Password {
  115. s.jsonError(w, "用户名或密码错误", http.StatusUnauthorized)
  116. return
  117. }
  118. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  119. "username": req.Username,
  120. "exp": time.Now().Add(24 * time.Hour).Unix(),
  121. })
  122. tokenStr, err := token.SignedString(s.jwtSecret)
  123. if err != nil {
  124. s.jsonError(w, "生成令牌失败", http.StatusInternalServerError)
  125. return
  126. }
  127. s.jsonResponse(w, http.StatusOK, map[string]interface{}{
  128. "token": tokenStr,
  129. })
  130. }
  131. // handleDashboard 仪表盘
  132. func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
  133. stats, err := s.db.GetLogStats()
  134. if err != nil {
  135. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  136. return
  137. }
  138. // 添加在线用户数
  139. if s.ftpServer != nil {
  140. online := s.ftpServer.GetOnlineUsers()
  141. stats["online_users"] = len(online)
  142. } else {
  143. stats["online_users"] = 0
  144. }
  145. s.jsonResponse(w, http.StatusOK, stats)
  146. }
  147. // handleUsers 用户管理
  148. func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
  149. switch r.Method {
  150. case http.MethodGet:
  151. users, err := s.db.ListUsers()
  152. if err != nil {
  153. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  154. return
  155. }
  156. if users == nil {
  157. users = []database.FTPUser{}
  158. }
  159. s.jsonResponse(w, http.StatusOK, users)
  160. case http.MethodPost:
  161. var user database.FTPUser
  162. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  163. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  164. return
  165. }
  166. if user.Username == "" || user.Password == "" {
  167. s.jsonError(w, "用户名和密码不能为空", http.StatusBadRequest)
  168. return
  169. }
  170. if user.HomeDir == "" {
  171. ftpCfg := s.config.Get().FTP
  172. user.HomeDir = filepath.Join(ftpCfg.RootDir, user.Username)
  173. }
  174. // 自动创建用户目录(如果不存在)
  175. if err := os.MkdirAll(user.HomeDir, 0755); err != nil {
  176. s.jsonError(w, fmt.Sprintf("创建用户目录失败: %v", err), http.StatusInternalServerError)
  177. return
  178. }
  179. if user.Permissions == "" {
  180. user.Permissions = "read,write"
  181. }
  182. user.Enabled = true
  183. if err := s.db.CreateUser(&user); err != nil {
  184. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  185. return
  186. }
  187. s.jsonResponse(w, http.StatusOK, user)
  188. default:
  189. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  190. }
  191. }
  192. // handleUserOperation 单个用户操作
  193. func (s *Server) handleUserOperation(w http.ResponseWriter, r *http.Request) {
  194. // 解析路径中的用户名: /api/users/{username} 或 /api/users/{username}/password
  195. pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/users/"), "/")
  196. username := pathParts[0]
  197. if username == "" {
  198. s.jsonError(w, "用户名不能为空", http.StatusBadRequest)
  199. return
  200. }
  201. // 密码修改: PUT /api/users/{username}/password
  202. if len(pathParts) > 1 && pathParts[1] == "password" && r.Method == http.MethodPut {
  203. var req struct {
  204. Password string `json:"password"`
  205. }
  206. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  207. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  208. return
  209. }
  210. if req.Password == "" {
  211. s.jsonError(w, "密码不能为空", http.StatusBadRequest)
  212. return
  213. }
  214. if err := s.db.UpdateUserPassword(username, req.Password); err != nil {
  215. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  216. return
  217. }
  218. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "密码已更新"})
  219. return
  220. }
  221. switch r.Method {
  222. case http.MethodGet:
  223. user, err := s.db.GetUser(username)
  224. if err != nil {
  225. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  226. return
  227. }
  228. if user == nil {
  229. s.jsonError(w, "用户不存在", http.StatusNotFound)
  230. return
  231. }
  232. s.jsonResponse(w, http.StatusOK, user)
  233. case http.MethodPut:
  234. var user database.FTPUser
  235. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  236. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  237. return
  238. }
  239. // 如果目录有变化,自动创建新目录
  240. if user.HomeDir != "" {
  241. if err := os.MkdirAll(user.HomeDir, 0755); err != nil {
  242. s.jsonError(w, fmt.Sprintf("创建目录失败: %v", err), http.StatusInternalServerError)
  243. return
  244. }
  245. }
  246. if err := s.db.UpdateUser(&user); err != nil {
  247. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  248. return
  249. }
  250. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已更新"})
  251. case http.MethodDelete:
  252. if err := s.db.DeleteUser(username); err != nil {
  253. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  254. return
  255. }
  256. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已删除"})
  257. default:
  258. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  259. }
  260. }
  261. // FileInfo 文件信息
  262. type FileInfo struct {
  263. Name string `json:"name"`
  264. Size int64 `json:"size"`
  265. IsDir bool `json:"is_dir"`
  266. ModTime time.Time `json:"mod_time"`
  267. Path string `json:"path"`
  268. }
  269. // handleFileBrowse 文件浏览
  270. func (s *Server) handleFileBrowse(w http.ResponseWriter, r *http.Request) {
  271. if r.Method != http.MethodGet {
  272. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  273. return
  274. }
  275. dir := r.URL.Query().Get("path")
  276. if dir == "" {
  277. dir = s.config.Get().FTP.RootDir
  278. }
  279. // 安全检查:确保路径在FTP根目录内
  280. rootDir, err := filepath.Abs(s.config.Get().FTP.RootDir)
  281. if err != nil {
  282. s.jsonError(w, "无效的根目录", http.StatusInternalServerError)
  283. return
  284. }
  285. absPath, err := filepath.Abs(dir)
  286. if err != nil {
  287. s.jsonError(w, "无效的路径", http.StatusBadRequest)
  288. return
  289. }
  290. if !strings.HasPrefix(absPath, rootDir) {
  291. s.jsonError(w, "路径超出允许范围", http.StatusForbidden)
  292. return
  293. }
  294. entries, err := os.ReadDir(absPath)
  295. if err != nil {
  296. if os.IsNotExist(err) {
  297. s.jsonResponse(w, http.StatusOK, []FileInfo{})
  298. return
  299. }
  300. s.jsonError(w, fmt.Sprintf("读取目录失败: %v", err), http.StatusInternalServerError)
  301. return
  302. }
  303. var files []FileInfo
  304. for _, entry := range entries {
  305. info, err := entry.Info()
  306. if err != nil {
  307. continue
  308. }
  309. files = append(files, FileInfo{
  310. Name: entry.Name(),
  311. Size: info.Size(),
  312. IsDir: entry.IsDir(),
  313. ModTime: info.ModTime(),
  314. Path: filepath.Join(absPath, entry.Name()),
  315. })
  316. }
  317. if files == nil {
  318. files = []FileInfo{}
  319. }
  320. s.jsonResponse(w, http.StatusOK, map[string]interface{}{
  321. "path": absPath,
  322. "files": files,
  323. })
  324. }
  325. // handleUpload 文件上传
  326. func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
  327. if r.Method != http.MethodPost {
  328. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  329. return
  330. }
  331. targetDir := r.URL.Query().Get("path")
  332. if targetDir == "" {
  333. targetDir = s.config.Get().FTP.RootDir
  334. }
  335. // 安全检查
  336. rootDir, _ := filepath.Abs(s.config.Get().FTP.RootDir)
  337. absDir, err := filepath.Abs(targetDir)
  338. if err != nil || !strings.HasPrefix(absDir, rootDir) {
  339. s.jsonError(w, "路径超出允许范围", http.StatusForbidden)
  340. return
  341. }
  342. r.ParseMultipartForm(100 << 20) // 100MB最大
  343. file, header, err := r.FormFile("file")
  344. if err != nil {
  345. s.jsonError(w, "读取文件失败", http.StatusBadRequest)
  346. return
  347. }
  348. defer file.Close()
  349. targetPath := filepath.Join(absDir, header.Filename)
  350. dst, err := os.Create(targetPath)
  351. if err != nil {
  352. s.jsonError(w, "创建文件失败", http.StatusInternalServerError)
  353. return
  354. }
  355. defer dst.Close()
  356. written, err := io.Copy(dst, file)
  357. if err != nil {
  358. s.jsonError(w, "写入文件失败", http.StatusInternalServerError)
  359. return
  360. }
  361. s.jsonResponse(w, http.StatusOK, map[string]interface{}{
  362. "message": "上传成功",
  363. "size": written,
  364. "path": targetPath,
  365. })
  366. }
  367. // handleLogs 日志查询
  368. func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
  369. if r.Method != http.MethodGet {
  370. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  371. return
  372. }
  373. username := r.URL.Query().Get("username")
  374. action := r.URL.Query().Get("action")
  375. page, _ := strconv.Atoi(r.URL.Query().Get("page"))
  376. pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
  377. if page <= 0 {
  378. page = 1
  379. }
  380. if pageSize <= 0 {
  381. pageSize = 20
  382. }
  383. logs, total, err := s.db.QueryLogs(username, action, page, pageSize)
  384. if err != nil {
  385. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  386. return
  387. }
  388. if logs == nil {
  389. logs = []database.FTPLog{}
  390. }
  391. s.jsonResponse(w, http.StatusOK, map[string]interface{}{
  392. "logs": logs,
  393. "total": total,
  394. "page": page,
  395. "page_size": pageSize,
  396. })
  397. }
  398. // handleOnline 在线用户
  399. func (s *Server) handleOnline(w http.ResponseWriter, r *http.Request) {
  400. if r.Method != http.MethodGet {
  401. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  402. return
  403. }
  404. var users []database.OnlineUser
  405. if s.ftpServer != nil {
  406. users = s.ftpServer.GetOnlineUsers()
  407. }
  408. if users == nil {
  409. users = []database.OnlineUser{}
  410. }
  411. s.jsonResponse(w, http.StatusOK, users)
  412. }
  413. // handleConfig 配置管理
  414. func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
  415. switch r.Method {
  416. case http.MethodGet:
  417. cfg := s.config.Get()
  418. // 隐藏敏感信息
  419. safeCfg := map[string]interface{}{
  420. "ftp": map[string]interface{}{
  421. "host": cfg.FTP.Host,
  422. "port": cfg.FTP.Port,
  423. "passive_port_min": cfg.FTP.PassivePortMin,
  424. "passive_port_max": cfg.FTP.PassivePortMax,
  425. "root_dir": cfg.FTP.RootDir,
  426. "enable_anonymous": cfg.FTP.EnableAnonymous,
  427. "max_connections": cfg.FTP.MaxConnections,
  428. "idle_timeout": cfg.FTP.IdleTimeout,
  429. },
  430. "web": map[string]interface{}{
  431. "host": cfg.Web.Host,
  432. "port": cfg.Web.Port,
  433. },
  434. }
  435. s.jsonResponse(w, http.StatusOK, safeCfg)
  436. case http.MethodPut:
  437. var update map[string]interface{}
  438. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  439. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  440. return
  441. }
  442. s.config.Update(func(cfg *config.Config) {
  443. if ftpCfg, ok := update["ftp"].(map[string]interface{}); ok {
  444. if v, ok := ftpCfg["port"].(float64); ok {
  445. cfg.FTP.Port = int(v)
  446. }
  447. if v, ok := ftpCfg["passive_port_min"].(float64); ok {
  448. cfg.FTP.PassivePortMin = int(v)
  449. }
  450. if v, ok := ftpCfg["passive_port_max"].(float64); ok {
  451. cfg.FTP.PassivePortMax = int(v)
  452. }
  453. if v, ok := ftpCfg["enable_anonymous"].(bool); ok {
  454. cfg.FTP.EnableAnonymous = v
  455. }
  456. if v, ok := ftpCfg["max_connections"].(float64); ok {
  457. cfg.FTP.MaxConnections = int(v)
  458. }
  459. if v, ok := ftpCfg["idle_timeout"].(float64); ok {
  460. cfg.FTP.IdleTimeout = int(v)
  461. }
  462. }
  463. if adminCfg, ok := update["admin"].(map[string]interface{}); ok {
  464. if v, ok := adminCfg["username"].(string); ok && v != "" {
  465. cfg.Admin.Username = v
  466. }
  467. if v, ok := adminCfg["password"].(string); ok && v != "" {
  468. cfg.Admin.Password = v
  469. }
  470. }
  471. })
  472. if err := s.config.Save("config.json"); err != nil {
  473. s.jsonError(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError)
  474. return
  475. }
  476. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "配置已保存,部分设置需要重启生效"})
  477. default:
  478. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  479. }
  480. }
  481. // --- 工具方法 ---
  482. func (s *Server) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
  483. w.Header().Set("Content-Type", "application/json")
  484. w.WriteHeader(status)
  485. json.NewEncoder(w).Encode(map[string]interface{}{
  486. "code": status,
  487. "data": data,
  488. })
  489. }
  490. func (s *Server) jsonError(w http.ResponseWriter, msg string, status int) {
  491. w.Header().Set("Content-Type", "application/json")
  492. w.WriteHeader(status)
  493. json.NewEncoder(w).Encode(map[string]interface{}{
  494. "code": status,
  495. "error": msg,
  496. })
  497. }
  498. // GenerateToken 生成随机令牌(用于JWT密钥)
  499. func GenerateToken() string {
  500. b := make([]byte, 16)
  501. rand.Read(b)
  502. return hex.EncodeToString(b)
  503. }
  504. // handleIPRules IP规则列表和创建
  505. func (s *Server) handleIPRules(w http.ResponseWriter, r *http.Request) {
  506. switch r.Method {
  507. case http.MethodGet:
  508. ruleType := r.URL.Query().Get("type")
  509. username := r.URL.Query().Get("username")
  510. rules, err := s.db.ListIPRules(ruleType, username)
  511. if err != nil {
  512. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  513. return
  514. }
  515. if rules == nil {
  516. rules = []database.IPAccessRule{}
  517. }
  518. s.jsonResponse(w, http.StatusOK, rules)
  519. case http.MethodPost:
  520. var rule database.IPAccessRule
  521. if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
  522. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  523. return
  524. }
  525. if rule.IP == "" {
  526. s.jsonError(w, "IP不能为空", http.StatusBadRequest)
  527. return
  528. }
  529. if rule.Type != "whitelist" && rule.Type != "blacklist" {
  530. rule.Type = "blacklist"
  531. }
  532. rule.Enabled = true
  533. if err := s.db.CreateIPRule(&rule); err != nil {
  534. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  535. return
  536. }
  537. s.jsonResponse(w, http.StatusOK, rule)
  538. default:
  539. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  540. }
  541. }
  542. // handleIPRuleOperation 单条IP规则操作
  543. func (s *Server) handleIPRuleOperation(w http.ResponseWriter, r *http.Request) {
  544. pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/ip-rules/"), "/")
  545. idStr := pathParts[0]
  546. if idStr == "" {
  547. s.jsonError(w, "ID不能为空", http.StatusBadRequest)
  548. return
  549. }
  550. id, err := strconv.ParseInt(idStr, 10, 64)
  551. if err != nil {
  552. s.jsonError(w, "无效的ID", http.StatusBadRequest)
  553. return
  554. }
  555. switch r.Method {
  556. case http.MethodPut:
  557. var rule database.IPAccessRule
  558. if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
  559. s.jsonError(w, "请求格式错误", http.StatusBadRequest)
  560. return
  561. }
  562. rule.ID = id
  563. if err := s.db.UpdateIPRule(&rule); err != nil {
  564. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  565. return
  566. }
  567. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "规则已更新"})
  568. case http.MethodDelete:
  569. if err := s.db.DeleteIPRule(id); err != nil {
  570. s.jsonError(w, err.Error(), http.StatusInternalServerError)
  571. return
  572. }
  573. s.jsonResponse(w, http.StatusOK, map[string]string{"message": "规则已删除"})
  574. default:
  575. s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
  576. }
  577. }