87214b9441
Phase 1 (基础设施): - ThinkChain 思考链连续性 + 差异化思考提示词 (persistent) - AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional) - MessageScheduler 自适应消息节奏 (Idle/Available/Busy) - SessionEnrichmentStore 渐进式上下文丰富 (5层) - ConversationBus 事件总线 + ResponseCache (dedup) - pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf - NPE 守卫/链路优化/数据库表修复/Go workspace Phase 2 (人格交互): - EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减) - ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验) - Gateway↔ai-core 在线状态感知链路 (presence notification) - 离线思考频率控制 + 重连问候 + 离线消息排队 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
627 lines
16 KiB
Go
627 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/yourname/cyrene-ai/pkg/logger"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// DeviceType 设备类型
|
|
type DeviceType string
|
|
|
|
const (
|
|
TypeLight DeviceType = "light"
|
|
TypeAC DeviceType = "ac"
|
|
TypeCurtain DeviceType = "curtain"
|
|
TypeSensor DeviceType = "sensor"
|
|
TypeLock DeviceType = "lock"
|
|
)
|
|
|
|
// Device 设备状态
|
|
type Device struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type DeviceType `json:"type"`
|
|
Status string `json:"status"` // on/off/closed/open/locked/unlocked
|
|
Brightness int `json:"brightness,omitempty"`
|
|
Color string `json:"color,omitempty"`
|
|
Temperature float64 `json:"temperature,omitempty"`
|
|
Mode string `json:"mode,omitempty"` // cool/heat/auto
|
|
Position int `json:"position,omitempty"` // curtain position 0-100
|
|
Value float64 `json:"value,omitempty"` // sensor value
|
|
Unit string `json:"unit,omitempty"` // sensor unit
|
|
Battery int `json:"battery,omitempty"` // lock battery
|
|
LastUpdated string `json:"last_updated"`
|
|
History []HistoryEntry `json:"history,omitempty"`
|
|
}
|
|
|
|
// HistoryEntry 设备状态历史
|
|
type HistoryEntry struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Field string `json:"field"`
|
|
OldValue string `json:"old_value"`
|
|
NewValue string `json:"new_value"`
|
|
}
|
|
|
|
// DeviceStore 设备存储(线程安全)
|
|
type DeviceStore struct {
|
|
mu sync.RWMutex
|
|
devices map[string]*Device
|
|
history map[string][]HistoryEntry
|
|
}
|
|
|
|
func NewDeviceStore() *DeviceStore {
|
|
ds := &DeviceStore{
|
|
devices: make(map[string]*Device),
|
|
history: make(map[string][]HistoryEntry),
|
|
}
|
|
ds.initDevices()
|
|
return ds
|
|
}
|
|
|
|
func (ds *DeviceStore) initDevices() {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
devices := []*Device{
|
|
{ID: "light-livingroom", Name: "客厅灯", Type: TypeLight, Status: "on", Brightness: 80, Color: "warm_white", LastUpdated: now},
|
|
{ID: "light-bedroom", Name: "卧室灯", Type: TypeLight, Status: "off", Brightness: 0, Color: "warm_white", LastUpdated: now},
|
|
{ID: "ac-livingroom", Name: "客厅空调", Type: TypeAC, Status: "on", Temperature: 26, Mode: "cool", LastUpdated: now},
|
|
{ID: "ac-bedroom", Name: "卧室空调", Type: TypeAC, Status: "off", Temperature: 24, Mode: "auto", LastUpdated: now},
|
|
{ID: "curtain-livingroom", Name: "客厅窗帘", Type: TypeCurtain, Status: "closed", Position: 0, LastUpdated: now},
|
|
{ID: "sensor-temperature", Name: "温度传感器", Type: TypeSensor, Value: 25.5, Unit: "celsius", LastUpdated: now},
|
|
{ID: "sensor-humidity", Name: "湿度传感器", Type: TypeSensor, Value: 60, Unit: "percent", LastUpdated: now},
|
|
{ID: "lock-door", Name: "智能门锁", Type: TypeLock, Status: "locked", Battery: 85, LastUpdated: now},
|
|
}
|
|
|
|
for _, d := range devices {
|
|
ds.devices[d.ID] = d
|
|
ds.history[d.ID] = make([]HistoryEntry, 0)
|
|
}
|
|
}
|
|
|
|
// GetAll 获取所有设备
|
|
func (ds *DeviceStore) GetAll() []*Device {
|
|
ds.mu.RLock()
|
|
defer ds.mu.RUnlock()
|
|
|
|
result := make([]*Device, 0, len(ds.devices))
|
|
for _, d := range ds.devices {
|
|
cp := *d
|
|
cp.History = nil // 列表不返回历史
|
|
result = append(result, &cp)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Get 获取单个设备
|
|
func (ds *DeviceStore) Get(id string) *Device {
|
|
ds.mu.RLock()
|
|
defer ds.mu.RUnlock()
|
|
|
|
d, ok := ds.devices[id]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cp := *d
|
|
// 包含最近10条历史(RLock 可重入)
|
|
if h, ok := ds.history[id]; ok && len(h) > 0 {
|
|
start := 0
|
|
if len(h) > 10 {
|
|
start = len(h) - 10
|
|
}
|
|
cp.History = make([]HistoryEntry, len(h)-start)
|
|
copy(cp.History, h[start:])
|
|
} else {
|
|
cp.History = []HistoryEntry{}
|
|
}
|
|
return &cp
|
|
}
|
|
|
|
// GetHistory 获取设备状态历史(最近10条)
|
|
func (ds *DeviceStore) GetHistory(id string) []HistoryEntry {
|
|
ds.mu.RLock()
|
|
defer ds.mu.RUnlock()
|
|
|
|
h, ok := ds.history[id]
|
|
if !ok || len(h) == 0 {
|
|
return []HistoryEntry{}
|
|
}
|
|
|
|
start := 0
|
|
if len(h) > 10 {
|
|
start = len(h) - 10
|
|
}
|
|
result := make([]HistoryEntry, len(h[start:]))
|
|
copy(result, h[start:])
|
|
return result
|
|
}
|
|
|
|
// addHistory 添加历史记录
|
|
func (ds *DeviceStore) addHistory(id, field, oldVal, newVal string) {
|
|
entry := HistoryEntry{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
Field: field,
|
|
OldValue: oldVal,
|
|
NewValue: newVal,
|
|
}
|
|
ds.history[id] = append(ds.history[id], entry)
|
|
}
|
|
|
|
// Toggle 切换设备开关状态
|
|
func (ds *DeviceStore) Toggle(id string) (*Device, error) {
|
|
ds.mu.Lock()
|
|
defer ds.mu.Unlock()
|
|
|
|
d, ok := ds.devices[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("设备 %s 不存在", id)
|
|
}
|
|
|
|
switch d.Type {
|
|
case TypeLight:
|
|
oldStatus := d.Status
|
|
if d.Status == "on" {
|
|
d.Status = "off"
|
|
d.Brightness = 0
|
|
} else {
|
|
d.Status = "on"
|
|
d.Brightness = 80
|
|
}
|
|
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
|
ds.addHistory(id, "status", oldStatus, d.Status)
|
|
|
|
case TypeAC:
|
|
oldStatus := d.Status
|
|
if d.Status == "on" {
|
|
d.Status = "off"
|
|
} else {
|
|
d.Status = "on"
|
|
}
|
|
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
|
ds.addHistory(id, "status", oldStatus, d.Status)
|
|
|
|
case TypeCurtain:
|
|
oldStatus := d.Status
|
|
if d.Status == "closed" {
|
|
d.Status = "open"
|
|
d.Position = 100
|
|
} else {
|
|
d.Status = "closed"
|
|
d.Position = 0
|
|
}
|
|
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
|
ds.addHistory(id, "status", oldStatus, d.Status)
|
|
|
|
case TypeLock:
|
|
oldStatus := d.Status
|
|
if d.Status == "locked" {
|
|
d.Status = "unlocked"
|
|
} else {
|
|
d.Status = "locked"
|
|
}
|
|
d.LastUpdated = time.Now().UTC().Format(time.RFC3339)
|
|
ds.addHistory(id, "status", oldStatus, d.Status)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("设备类型 %s 不支持切换", d.Type)
|
|
}
|
|
|
|
cp := *d
|
|
// 不能调用 ds.GetHistory(id),因为当前已持有写锁,Go 的 RWMutex 不允许写锁重入读锁
|
|
if h, ok := ds.history[id]; ok && len(h) > 0 {
|
|
start := 0
|
|
if len(h) > 10 {
|
|
start = len(h) - 10
|
|
}
|
|
cp.History = make([]HistoryEntry, len(h)-start)
|
|
copy(cp.History, h[start:])
|
|
} else {
|
|
cp.History = []HistoryEntry{}
|
|
}
|
|
return &cp, nil
|
|
}
|
|
|
|
// SetProperty 设置设备属性(状态、温度、亮度、位置、模式、颜色等)
|
|
func (ds *DeviceStore) SetProperty(id, field string, value interface{}) (*Device, error) {
|
|
ds.mu.Lock()
|
|
defer ds.mu.Unlock()
|
|
|
|
d, ok := ds.devices[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("设备 %s 不存在", id)
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
switch field {
|
|
case "status", "power":
|
|
// 声明式电源控制:支持 "on"/"off"/"open"/"closed"/"locked"/"unlocked"
|
|
// 同时支持中文值: "开"/"关"/"打开"/"关闭"
|
|
// 支持布尔值: true/false → on/off
|
|
var newStatus string
|
|
switch v := value.(type) {
|
|
case string:
|
|
newStatus = normalizeStatus(v, d.Type)
|
|
if newStatus == "" {
|
|
return nil, fmt.Errorf("无效的状态值: %s (设备类型 %s 不支持此状态)", v, d.Type)
|
|
}
|
|
case bool:
|
|
if v {
|
|
newStatus = "on"
|
|
} else {
|
|
newStatus = "off"
|
|
}
|
|
case float64:
|
|
if v == 0 {
|
|
newStatus = "off"
|
|
} else {
|
|
newStatus = "on"
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("status 需要字符串、布尔值或数字")
|
|
}
|
|
// 对于非 on/off 设备类型(curtain/lock),转换为对应状态
|
|
switch d.Type {
|
|
case TypeCurtain:
|
|
if newStatus == "on" {
|
|
newStatus = "open"
|
|
} else if newStatus == "off" {
|
|
newStatus = "closed"
|
|
}
|
|
case TypeLock:
|
|
if newStatus == "on" {
|
|
newStatus = "unlocked"
|
|
} else if newStatus == "off" {
|
|
newStatus = "locked"
|
|
}
|
|
}
|
|
|
|
if d.Status == newStatus {
|
|
// 状态未变化,直接返回
|
|
cp := *d
|
|
return &cp, nil
|
|
}
|
|
oldStatus := d.Status
|
|
d.Status = newStatus
|
|
// 根据设备类型设置关联属性
|
|
switch d.Type {
|
|
case TypeLight:
|
|
if newStatus == "off" {
|
|
d.Brightness = 0
|
|
} else if d.Brightness == 0 {
|
|
d.Brightness = 80
|
|
}
|
|
case TypeCurtain:
|
|
if newStatus == "closed" {
|
|
d.Position = 0
|
|
} else {
|
|
d.Position = 100
|
|
}
|
|
}
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "status", oldStatus, newStatus)
|
|
|
|
case "temperature":
|
|
v, ok := toFloat64(value)
|
|
if !ok {
|
|
return nil, fmt.Errorf("temperature 需要数字值")
|
|
}
|
|
if d.Type != TypeAC {
|
|
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持温度调节", d.Name, d.Type)
|
|
}
|
|
oldVal := fmt.Sprintf("%.1f", d.Temperature)
|
|
d.Temperature = v
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "temperature", oldVal, fmt.Sprintf("%.1f", v))
|
|
|
|
case "brightness":
|
|
v, ok := toFloat64(value)
|
|
if !ok {
|
|
return nil, fmt.Errorf("brightness 需要数字值")
|
|
}
|
|
if d.Type != TypeLight {
|
|
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持亮度调节", d.Name, d.Type)
|
|
}
|
|
oldVal := fmt.Sprintf("%d", d.Brightness)
|
|
d.Brightness = clampInt(int(v), 0, 100)
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "brightness", oldVal, fmt.Sprintf("%d", d.Brightness))
|
|
|
|
case "position":
|
|
v, ok := toFloat64(value)
|
|
if !ok {
|
|
return nil, fmt.Errorf("position 需要数字值")
|
|
}
|
|
if d.Type != TypeCurtain {
|
|
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持位置调节", d.Name, d.Type)
|
|
}
|
|
oldVal := fmt.Sprintf("%d", d.Position)
|
|
d.Position = clampInt(int(v), 0, 100)
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "position", oldVal, fmt.Sprintf("%d", d.Position))
|
|
|
|
case "mode":
|
|
v, ok := value.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("mode 需要字符串值")
|
|
}
|
|
if d.Type != TypeAC {
|
|
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持模式切换", d.Name, d.Type)
|
|
}
|
|
oldVal := d.Mode
|
|
d.Mode = v
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "mode", oldVal, v)
|
|
|
|
case "color":
|
|
v, ok := value.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("color 需要字符串值")
|
|
}
|
|
if d.Type != TypeLight {
|
|
return nil, fmt.Errorf("设备 %s (类型 %s) 不支持颜色调节", d.Name, d.Type)
|
|
}
|
|
oldVal := d.Color
|
|
d.Color = v
|
|
d.LastUpdated = now
|
|
ds.addHistory(id, "color", oldVal, v)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("不支持的属性: %s", field)
|
|
}
|
|
|
|
cp := *d
|
|
if h, ok := ds.history[id]; ok && len(h) > 0 {
|
|
start := 0
|
|
if len(h) > 10 {
|
|
start = len(h) - 10
|
|
}
|
|
cp.History = make([]HistoryEntry, len(h)-start)
|
|
copy(cp.History, h[start:])
|
|
} else {
|
|
cp.History = []HistoryEntry{}
|
|
}
|
|
return &cp, nil
|
|
}
|
|
|
|
// normalizeStatus 标准化电源状态值
|
|
// 支持英文: "on"/"off"/"open"/"closed"/"locked"/"unlocked"
|
|
// 支持中文: "开"/"关"/"打开"/"关闭"/"开启"/"解锁"/"锁定"/"上锁"
|
|
// 支持布尔兼容: "true"/"false"
|
|
// 返回空字符串表示无效值
|
|
func normalizeStatus(v string, dType DeviceType) string {
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "on", "true", "开", "打开", "开启":
|
|
return "on"
|
|
case "off", "false", "关", "关闭":
|
|
return "off"
|
|
case "open":
|
|
if dType == TypeCurtain {
|
|
return "open"
|
|
}
|
|
return "on"
|
|
case "closed":
|
|
if dType == TypeCurtain {
|
|
return "closed"
|
|
}
|
|
return "off"
|
|
case "locked", "锁定", "上锁":
|
|
return "locked"
|
|
case "unlocked", "解锁":
|
|
return "unlocked"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// toFloat64 将 interface{} 转换为 float64
|
|
func toFloat64(v interface{}) (float64, bool) {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return val, true
|
|
case float32:
|
|
return float64(val), true
|
|
case int:
|
|
return float64(val), true
|
|
case int64:
|
|
return float64(val), true
|
|
case json.Number:
|
|
f, err := val.Float64()
|
|
return f, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// clampInt 限制整数在 [min, max] 范围内
|
|
func clampInt(v, min, max int) int {
|
|
if v < min {
|
|
return min
|
|
}
|
|
if v > max {
|
|
return max
|
|
}
|
|
return v
|
|
}
|
|
|
|
// SimulateFluctuation 模拟传感器随机波动
|
|
func (ds *DeviceStore) SimulateFluctuation() {
|
|
ds.mu.Lock()
|
|
defer ds.mu.Unlock()
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
// 温度传感器: ±0.2°C
|
|
if t, ok := ds.devices["sensor-temperature"]; ok {
|
|
oldVal := t.Value
|
|
t.Value += (rand.Float64()*0.4 - 0.2)
|
|
t.Value = float64(int(t.Value*10)) / 10 // 保留一位小数
|
|
t.LastUpdated = now
|
|
ds.addHistory("sensor-temperature", "value", fmt.Sprintf("%.1f", oldVal), fmt.Sprintf("%.1f", t.Value))
|
|
}
|
|
|
|
// 湿度传感器: ±1%
|
|
if h, ok := ds.devices["sensor-humidity"]; ok {
|
|
oldVal := h.Value
|
|
h.Value += float64(rand.Intn(3) - 1) // -1, 0, +1
|
|
if h.Value < 0 {
|
|
h.Value = 0
|
|
}
|
|
if h.Value > 100 {
|
|
h.Value = 100
|
|
}
|
|
h.LastUpdated = now
|
|
ds.addHistory("sensor-humidity", "value", fmt.Sprintf("%.0f", oldVal), fmt.Sprintf("%.0f", h.Value))
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
logger.SetDefault(logger.New("iot-debug"))
|
|
port := getEnv("IOT_DEBUG_PORT", "8083")
|
|
store := NewDeviceStore()
|
|
|
|
// 启动传感器波动模拟(每30秒)
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
store.SimulateFluctuation()
|
|
}
|
|
}()
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// GET /api/v1/devices - 列出所有设备
|
|
mux.HandleFunc("/api/v1/devices", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
devices := store.GetAll()
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"devices": devices,
|
|
"total": len(devices),
|
|
})
|
|
})
|
|
|
|
// GET /api/v1/devices/{id} - 获取单个设备
|
|
// POST /api/v1/devices/{id}/toggle - 切换设备
|
|
mux.HandleFunc("/api/v1/devices/", func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/v1/devices/")
|
|
parts := strings.Split(path, "/")
|
|
|
|
if len(parts) == 0 || parts[0] == "" {
|
|
http.Error(w, "缺少设备ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
deviceID := parts[0]
|
|
|
|
// POST /api/v1/devices/{id}/toggle
|
|
if len(parts) == 2 && parts[1] == "toggle" {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
device, err := store.Toggle(deviceID)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"device": device,
|
|
"action": "toggled",
|
|
})
|
|
return
|
|
}
|
|
|
|
// POST /api/v1/devices/{id}/set - 设置设备属性
|
|
if len(parts) == 2 && parts[1] == "set" {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var req struct {
|
|
Field string `json:"field"`
|
|
Value interface{} `json:"value"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请求格式错误"})
|
|
return
|
|
}
|
|
device, err := store.SetProperty(deviceID, req.Field, req.Value)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"device": device,
|
|
"action": "set_" + req.Field,
|
|
})
|
|
return
|
|
}
|
|
|
|
// GET /api/v1/devices/{id}/history
|
|
if len(parts) == 2 && parts[1] == "history" {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
history := store.GetHistory(deviceID)
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"device_id": deviceID,
|
|
"history": history,
|
|
})
|
|
return
|
|
}
|
|
|
|
// GET /api/v1/devices/{id}
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
device := store.Get(deviceID)
|
|
if device == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": fmt.Sprintf("设备 %s 不存在", deviceID)})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"device": device,
|
|
})
|
|
})
|
|
|
|
// 健康检查
|
|
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"status": "ok",
|
|
"service": "iot-debug-service",
|
|
})
|
|
})
|
|
|
|
logger.Printf("🔌 IoT 调试服务启动在端口 %s", port)
|
|
logger.Printf(" 模拟设备数: %d", len(store.GetAll()))
|
|
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
logger.Fatalf("服务启动失败: %v", err)
|
|
}
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|