mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-13 04:36:37 +08:00
feat: 添加公告系统(支持SQLite和PostgreSQL) (#129)
* feat(announcement): add database schema for SQLite and PostgreSQL Add announcement table with id, title, content, enabled, created_at, updated_at columns to both SQLite and PostgreSQL schemas to support announcement system. * feat(announcement): implement SQLite repository for announcement management Add announcement CRUD operations in SQLite repository including create, read, update, delete, and list methods with proper error handling. * feat(announcement): add HTTP handlers and auth middleware for announcement API Implement admin-only update endpoint and public read endpoint for announcements. Add auth middleware to enforce admin-only access for update operations. * feat(announcement): add frontend API client for announcement endpoints Implement API client methods for fetching announcements and updating announcement settings with proper error handling. * feat(announcement): add announcement display component to dashboard Implement announcement display section in dashboard with real-time updates and proper styling using HeroUI components. * feat(announcement): add announcement management UI to config page Implement announcement settings panel with enable/disable toggle and content editor for admin users to manage announcements.
This commit is contained in:
@@ -177,6 +177,8 @@ func (h *Handler) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/federation/runtime/diagnose", h.authPeer(h.federationRuntimeDiagnose))
|
||||
mux.HandleFunc("/api/v1/federation/runtime/command", h.authPeer(h.federationRuntimeCommand))
|
||||
mux.HandleFunc("/api/v1/federation/node/import", h.nodeImport)
|
||||
mux.HandleFunc("/api/v1/announcement/get", h.getAnnouncement)
|
||||
mux.HandleFunc("/api/v1/announcement/update", h.updateAnnouncement)
|
||||
|
||||
mux.HandleFunc("/flow/test", h.flowTest)
|
||||
mux.HandleFunc("/flow/config", h.flowConfig)
|
||||
@@ -1228,3 +1230,53 @@ func (h *Handler) backupImport(w http.ResponseWriter, r *http.Request) {
|
||||
result.AutoBackup = autoBackup
|
||||
response.WriteJSON(w, response.OK(result))
|
||||
}
|
||||
|
||||
func (h *Handler) getAnnouncement(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
ann, err := h.repo.GetAnnouncement()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-1, fmt.Sprintf("获取公告失败: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if ann == nil {
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": "",
|
||||
"enabled": 0,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": ann.Content,
|
||||
"enabled": ann.Enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handler) updateAnnouncement(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
Enabled int `json:"enabled"`
|
||||
}
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.Err(500, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
if err := h.repo.UpsertAnnouncement(req.Content, req.Enabled, now); err != nil {
|
||||
response.WriteJSON(w, response.Err(-1, fmt.Sprintf("更新公告失败: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ func requiresAdmin(path string) bool {
|
||||
return true
|
||||
case "/api/v1/config/update", "/api/v1/config/update-single":
|
||||
return true
|
||||
case "/api/v1/announcement/update":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -239,3 +239,11 @@ CREATE TABLE IF NOT EXISTS federation_tunnel_binding (
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_federation_tunnel_binding_unique ON federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx);
|
||||
CREATE INDEX IF NOT EXISTS idx_federation_tunnel_binding_tunnel ON federation_tunnel_binding(tunnel_id, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS announcement (
|
||||
id SERIAL PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_time BIGINT NOT NULL,
|
||||
updated_time BIGINT
|
||||
);
|
||||
|
||||
@@ -66,6 +66,14 @@ type ViteConfig struct {
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
type Announcement struct {
|
||||
ID int64 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Enabled int `json:"enabled"`
|
||||
CreatedTime int64 `json:"created_time"`
|
||||
UpdatedTime sql.NullInt64 `json:"updated_time,omitempty"`
|
||||
}
|
||||
|
||||
type UserTunnelDetail struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
@@ -319,6 +327,46 @@ func (r *Repository) UpsertConfig(name, value string, now int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetAnnouncement() (*Announcement, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
}
|
||||
|
||||
row := r.db.QueryRow(`SELECT id, content, enabled, created_time, updated_time FROM announcement ORDER BY id DESC LIMIT 1`)
|
||||
ann := &Announcement{}
|
||||
if err := row.Scan(&ann.ID, &ann.Content, &ann.Enabled, &ann.CreatedTime, &ann.UpdatedTime); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ann, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertAnnouncement(content string, enabled int, now int64) error {
|
||||
if r == nil || r.db == nil {
|
||||
return errors.New("repository not initialized")
|
||||
}
|
||||
|
||||
var count int
|
||||
err := r.db.QueryRow(`SELECT COUNT(*) FROM announcement`).Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
_, err = r.db.Exec(`
|
||||
INSERT INTO announcement(content, enabled, created_time, updated_time)
|
||||
VALUES(?, ?, ?, ?)
|
||||
`, content, enabled, now, now)
|
||||
} else {
|
||||
_, err = r.db.Exec(`
|
||||
UPDATE announcement SET content = ?, enabled = ?, updated_time = ?
|
||||
`, content, enabled, now)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserByID(id int64) (*User, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("repository not initialized")
|
||||
|
||||
@@ -244,3 +244,11 @@ CREATE TABLE IF NOT EXISTS federation_tunnel_binding (
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_federation_tunnel_binding_unique ON federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx);
|
||||
CREATE INDEX IF NOT EXISTS idx_federation_tunnel_binding_tunnel ON federation_tunnel_binding(tunnel_id, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS announcement (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user