Files
-Fonu/internal/certificate/store.go
T
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

169 lines
4.7 KiB
Go

package certificate
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
type Record struct {
ID int64 `json:"id"`
Domain string `json:"domain"`
Domains []string `json:"domains,omitempty"`
Wildcard bool `json:"wildcard"`
ACMECA string `json:"acme_ca,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
LastRenewAt *time.Time `json:"last_renew_at,omitempty"`
Status string `json:"status"`
LastError string `json:"last_error,omitempty"`
DaysLeft int `json:"days_left"`
}
type Store struct {
db *sql.DB
}
func NewStore(db *sql.DB) *Store {
return &Store{db: db}
}
func (s *Store) List(ctx context.Context) ([]Record, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, domain, COALESCE(domains, ''), wildcard, COALESCE(acme_ca, ''), COALESCE(cert_path, ''), COALESCE(key_path, ''),
expires_at, last_renew_at, status, COALESCE(last_error, '')
FROM certificates ORDER BY id ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var records []Record
for rows.Next() {
rec, err := scanRecord(rows)
if err != nil {
return nil, err
}
records = append(records, rec)
}
return records, rows.Err()
}
func (s *Store) Upsert(ctx context.Context, domain string, wildcard bool, domainsJSON, acmeCA, certPath, keyPath string, expiresAt time.Time, status string, lastError string) (Record, error) {
_, err := s.db.ExecContext(ctx, `
INSERT INTO certificates(domain, domains, wildcard, acme_ca, cert_path, key_path, expires_at, last_renew_at, status, last_error, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, datetime('now'))
ON CONFLICT(domain) DO UPDATE SET
domains = excluded.domains,
wildcard = excluded.wildcard,
acme_ca = excluded.acme_ca,
cert_path = excluded.cert_path,
key_path = excluded.key_path,
expires_at = excluded.expires_at,
last_renew_at = datetime('now'),
status = excluded.status,
last_error = excluded.last_error,
updated_at = datetime('now')
`, domain, domainsJSON, boolInt(wildcard), acmeCA, certPath, keyPath, expiresAt.UTC().Format(time.RFC3339), status, lastError)
if err != nil {
return Record{}, err
}
return s.GetByDomain(ctx, domain)
}
func (s *Store) UpdateStatus(ctx context.Context, domain, status, lastError string) error {
_, err := s.db.ExecContext(ctx, `
UPDATE certificates SET status = ?, last_error = ?, updated_at = datetime('now') WHERE domain = ?
`, status, lastError, domain)
return err
}
func (s *Store) Delete(ctx context.Context, domain string) error {
res, err := s.db.ExecContext(ctx, `DELETE FROM certificates WHERE domain = ?`, domain)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("证书不存在")
}
return nil
}
func (s *Store) GetByDomain(ctx context.Context, domain string) (Record, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, domain, COALESCE(domains, ''), wildcard, COALESCE(acme_ca, ''), COALESCE(cert_path, ''), COALESCE(key_path, ''),
expires_at, last_renew_at, status, COALESCE(last_error, '')
FROM certificates WHERE domain = ?
`, domain)
rec, err := scanRecord(row)
if errors.Is(err, sql.ErrNoRows) {
return Record{}, fmt.Errorf("证书不存在")
}
return rec, err
}
func scanRecord(row interface{ Scan(dest ...any) error }) (Record, error) {
var rec Record
var wildcard int
var domainsJSON string
var expiresAt, lastRenewAt sql.NullString
if err := row.Scan(&rec.ID, &rec.Domain, &domainsJSON, &wildcard, &rec.ACMECA, &rec.CertPath, &rec.KeyPath, &expiresAt, &lastRenewAt, &rec.Status, &rec.LastError); err != nil {
return Record{}, err
}
rec.Wildcard = wildcard == 1
rec.Domains = decodeDomains(domainsJSON, rec.Domain, rec.Wildcard)
if expiresAt.Valid {
t := parseTime(expiresAt.String)
rec.ExpiresAt = &t
rec.DaysLeft = int(time.Until(t).Hours() / 24)
}
if lastRenewAt.Valid {
t := parseTime(lastRenewAt.String)
rec.LastRenewAt = &t
}
return rec, nil
}
func boolInt(v bool) int {
if v {
return 1
}
return 0
}
func decodeDomains(raw string, domain string, wildcard bool) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
if wildcard {
return []string{domain, "*." + domain}
}
return []string{domain}
}
var domains []string
if err := json.Unmarshal([]byte(raw), &domains); err != nil || len(domains) == 0 {
if wildcard {
return []string{domain, "*." + domain}
}
return []string{domain}
}
return domains
}
func parseTime(v string) time.Time {
t, err := time.Parse(time.RFC3339, v)
if err != nil {
t, _ = time.Parse("2006-01-02 15:04:05", v)
}
return t.UTC()
}