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:
sagit
2026-02-15 23:43:41 +08:00
committed by GitHub
parent 1b4500202a
commit 45d7970177
8 changed files with 276 additions and 10 deletions
@@ -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
);