feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)
This commit is contained in:
+222
-70
@@ -612,6 +612,15 @@ func ValidateNodeDeploy(d *Data, node *Node) error {
|
||||
return orchNotFound("出口服务器")
|
||||
}
|
||||
}
|
||||
targets := []*Server{server}
|
||||
if !direct {
|
||||
targets = append(targets, exitServer)
|
||||
}
|
||||
for _, target := range targets {
|
||||
if target.Enabled != nil && !*target.Enabled {
|
||||
return httpErr(409, "设备 %s 已停用", target.Name)
|
||||
}
|
||||
}
|
||||
if len(activeNodeClients(d, node)) == 0 {
|
||||
return httpErr(400, "请先给节点分配至少一个客户再部署")
|
||||
}
|
||||
@@ -720,6 +729,10 @@ func reconcileServerData(d *Data, serverID, actor string, opts ReconcileOptions)
|
||||
if server == nil {
|
||||
return 0, orchNotFound("服务器")
|
||||
}
|
||||
// A disabled device receives no new deployments, including reconciles.
|
||||
if server.Enabled != nil && !*server.Enabled {
|
||||
return 0, nil
|
||||
}
|
||||
failInFlight := true
|
||||
if opts.FailInFlight != nil {
|
||||
failInFlight = *opts.FailInFlight
|
||||
@@ -935,61 +948,178 @@ func (o *Orchestrator) RepairNode(nodeID, actor string) (bool, error) {
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// assignCustomerData is the transaction-free core of AssignCustomer,
|
||||
// shared with the customer form's node alignment (AlignCustomerNodes).
|
||||
func assignCustomerData(d *Data, nodeID, customerID, actor string) (*NodeClient, error) {
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil {
|
||||
return nil, orchNotFound("节点")
|
||||
}
|
||||
customer := orchFindCustomer(d, customerID)
|
||||
if customer == nil {
|
||||
return nil, orchNotFound("客户")
|
||||
}
|
||||
if customer.Status != "active" {
|
||||
return nil, httpErr(409, "只能给状态正常的客户分配节点")
|
||||
}
|
||||
var nc *NodeClient
|
||||
for _, entry := range d.NodeClients {
|
||||
if entry.NodeID == nodeID && entry.CustomerID == customerID {
|
||||
nc = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if nc != nil && nc.Status == "active" {
|
||||
return nil, httpErr(409, "该客户已分配到此节点")
|
||||
}
|
||||
if nc != nil {
|
||||
nc.Status = "active"
|
||||
nc.UpdatedAt = NowISO()
|
||||
} else {
|
||||
now := NowISO()
|
||||
nc = &NodeClient{
|
||||
ID: NewID("ncl"),
|
||||
NodeID: nodeID,
|
||||
CustomerID: customerID,
|
||||
Credentials: NewNodeClientCredentials(node.Protocol),
|
||||
Status: "active",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
d.NodeClients = append(d.NodeClients, nc)
|
||||
}
|
||||
if (node.Status == "removing" || node.Status == "draft") && !boolVal(node.AutoRemoving) {
|
||||
// Record the assignment only.
|
||||
} else {
|
||||
if node.Status == "removing" && boolVal(node.AutoRemoving) {
|
||||
cancelQueuedDeletes(d, node.ID, "已取消:节点恢复服务")
|
||||
node.AutoRemoving = BoolPtr(false)
|
||||
}
|
||||
if err := RebuildNode(d, node, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customerID, "customer": customer.Name})
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
func unassignCustomerData(d *Data, nodeID, customerID, actor string) error {
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil {
|
||||
return orchNotFound("节点")
|
||||
}
|
||||
index := -1
|
||||
for i, entry := range d.NodeClients {
|
||||
if entry.NodeID == nodeID && entry.CustomerID == customerID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
return httpErr(404, "该客户未分配到此节点")
|
||||
}
|
||||
d.NodeClients = append(d.NodeClients[:index], d.NodeClients[index+1:]...)
|
||||
if node.Status != "draft" && node.Status != "removing" {
|
||||
if err := RebuildNode(d, node, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Audit(d, actor, "unassign_customer", node.ID, map[string]any{"customerId": customerID})
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlignCustomerNodes mirrors alignCustomerNodes in lib/orchestrator.js:
|
||||
// full-set alignment of one customer's node bindings (customer form's
|
||||
// 关联入站 multi-select). Suspended customers get suspended bindings.
|
||||
func AlignCustomerNodes(d *Data, customer *Customer, desiredIds []string, actor string) error {
|
||||
desired := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, nodeID := range desiredIds {
|
||||
if nodeID == "" || seen[nodeID] {
|
||||
continue
|
||||
}
|
||||
seen[nodeID] = true
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil || node.PendingDelete {
|
||||
return httpErr(400, "节点不存在")
|
||||
}
|
||||
desired = append(desired, nodeID)
|
||||
}
|
||||
rebuild := map[string]bool{}
|
||||
kept := []*NodeClient{}
|
||||
for _, nc := range d.NodeClients {
|
||||
if nc.CustomerID != customer.ID {
|
||||
kept = append(kept, nc)
|
||||
continue
|
||||
}
|
||||
if seen[nc.NodeID] {
|
||||
kept = append(kept, nc)
|
||||
continue
|
||||
}
|
||||
Audit(d, actor, "unassign_customer", nc.NodeID, map[string]any{"customerId": customer.ID})
|
||||
rebuild[nc.NodeID] = true
|
||||
}
|
||||
d.NodeClients = kept
|
||||
for _, nodeID := range desired {
|
||||
exists := false
|
||||
for _, nc := range d.NodeClients {
|
||||
if nc.CustomerID == customer.ID && nc.NodeID == nodeID {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
node := orchFindNode(d, nodeID)
|
||||
status := "suspended"
|
||||
if customer.Status == "active" {
|
||||
status = "active"
|
||||
}
|
||||
now := NowISO()
|
||||
d.NodeClients = append(d.NodeClients, &NodeClient{
|
||||
ID: NewID("ncl"),
|
||||
NodeID: nodeID,
|
||||
CustomerID: customer.ID,
|
||||
Credentials: NewNodeClientCredentials(node.Protocol),
|
||||
Status: status,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customer.ID, "customer": customer.Name})
|
||||
if status == "active" {
|
||||
rebuild[nodeID] = true
|
||||
}
|
||||
}
|
||||
for nodeID := range rebuild {
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
if (node.Status == "removing" || node.Status == "draft") && !boolVal(node.AutoRemoving) {
|
||||
continue
|
||||
}
|
||||
if node.Status == "removing" && boolVal(node.AutoRemoving) {
|
||||
cancelQueuedDeletes(d, node.ID, "已取消:节点恢复服务")
|
||||
node.AutoRemoving = BoolPtr(false)
|
||||
}
|
||||
if err := RebuildNode(d, node, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Orchestrator) AssignCustomer(nodeID, customerID, actor string) (*NodeClient, error) {
|
||||
if actor == "" {
|
||||
actor = "admin"
|
||||
}
|
||||
var result *NodeClient
|
||||
err := o.tx(func(d *Data) error {
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil {
|
||||
return orchNotFound("节点")
|
||||
nc, err := assignCustomerData(d, nodeID, customerID, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
customer := orchFindCustomer(d, customerID)
|
||||
if customer == nil {
|
||||
return orchNotFound("客户")
|
||||
}
|
||||
if customer.Status != "active" {
|
||||
return httpErr(409, "只能给状态正常的客户分配节点")
|
||||
}
|
||||
var nc *NodeClient
|
||||
for _, entry := range d.NodeClients {
|
||||
if entry.NodeID == nodeID && entry.CustomerID == customerID {
|
||||
nc = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if nc != nil && nc.Status == "active" {
|
||||
return httpErr(409, "该客户已分配到此节点")
|
||||
}
|
||||
if nc != nil {
|
||||
nc.Status = "active"
|
||||
nc.UpdatedAt = NowISO()
|
||||
} else {
|
||||
now := NowISO()
|
||||
nc = &NodeClient{
|
||||
ID: NewID("ncl"),
|
||||
NodeID: nodeID,
|
||||
CustomerID: customerID,
|
||||
Credentials: NewNodeClientCredentials(node.Protocol),
|
||||
Status: "active",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
d.NodeClients = append(d.NodeClients, nc)
|
||||
}
|
||||
if (node.Status == "removing" || node.Status == "draft") && !boolVal(node.AutoRemoving) {
|
||||
// Record the assignment only.
|
||||
} else {
|
||||
if node.Status == "removing" && boolVal(node.AutoRemoving) {
|
||||
cancelQueuedDeletes(d, node.ID, "已取消:节点恢复服务")
|
||||
node.AutoRemoving = BoolPtr(false)
|
||||
}
|
||||
if err := RebuildNode(d, node, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customerID, "customer": customer.Name})
|
||||
result = cloneNodeClientRecord(nc)
|
||||
return nil
|
||||
})
|
||||
@@ -1001,28 +1131,7 @@ func (o *Orchestrator) UnassignCustomer(nodeID, customerID, actor string) (bool,
|
||||
actor = "admin"
|
||||
}
|
||||
err := o.tx(func(d *Data) error {
|
||||
node := orchFindNode(d, nodeID)
|
||||
if node == nil {
|
||||
return orchNotFound("节点")
|
||||
}
|
||||
index := -1
|
||||
for i, entry := range d.NodeClients {
|
||||
if entry.NodeID == nodeID && entry.CustomerID == customerID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
return httpErr(404, "该客户未分配到此节点")
|
||||
}
|
||||
d.NodeClients = append(d.NodeClients[:index], d.NodeClients[index+1:]...)
|
||||
if node.Status != "draft" && node.Status != "removing" {
|
||||
if err := RebuildNode(d, node, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Audit(d, actor, "unassign_customer", node.ID, map[string]any{"customerId": customerID})
|
||||
return nil
|
||||
return unassignCustomerData(d, nodeID, customerID, actor)
|
||||
})
|
||||
return err == nil, err
|
||||
}
|
||||
@@ -1298,6 +1407,9 @@ func (o *Orchestrator) RecordUsage(serverID string, samples []UsageSample, errTe
|
||||
slot = &UsageSlot{}
|
||||
customer.ServerUsage[key] = slot
|
||||
}
|
||||
prevCounters := slot.UsageCounters
|
||||
prevSampleISO := slot.LastSampleAt
|
||||
restarted := prevCounters != nil && prevCounters.Epoch != nil && sampleEpoch != nil && *sampleEpoch != *prevCounters.Epoch
|
||||
delta := ApplyDelta(SlotHolder(slot), uplink, downlink, sampleEpoch)
|
||||
if delta == nil {
|
||||
continue
|
||||
@@ -1307,11 +1419,33 @@ func (o *Orchestrator) RecordUsage(serverID string, samples []UsageSample, errTe
|
||||
customer.UsedBytes += delta.UpDelta + delta.DownDelta
|
||||
customer.LastUsageAt = StrPtr(reportedAt)
|
||||
customer.UpdatedAt = reportedAt
|
||||
slot.LastSampleAt = StrPtr(reportedAt)
|
||||
if delta.UpDelta > 0 || delta.DownDelta > 0 {
|
||||
slot.LastActiveAt = StrPtr(reportedAt)
|
||||
customer.LastOnlineAt = StrPtr(reportedAt)
|
||||
}
|
||||
if !restarted && prevCounters != nil && prevSampleISO != nil {
|
||||
if prevMs, ok1 := ParseISOMs(*prevSampleISO); ok1 {
|
||||
if nowMs, ok2 := ParseISOMs(reportedAt); ok2 {
|
||||
dt := float64(nowMs-prevMs) / 1000
|
||||
if dt >= 5 {
|
||||
up := float64(int64(math.Round(float64(uplink-prevCounters.Uplink) / dt)))
|
||||
down := float64(int64(math.Round(float64(downlink-prevCounters.Downlink) / dt)))
|
||||
slot.SpeedUpBps = &up
|
||||
slot.SpeedDownBps = &down
|
||||
slot.SpeedAt = StrPtr(reportedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "inbound":
|
||||
node := orchFindNode(d, sample.ID)
|
||||
if node == nil || node.ServerID != serverID {
|
||||
continue
|
||||
}
|
||||
prevCounters := node.UsageCounters
|
||||
prevSampleISO := node.LastSampleAt
|
||||
restarted := prevCounters != nil && prevCounters.Epoch != nil && sampleEpoch != nil && *sampleEpoch != *prevCounters.Epoch
|
||||
delta := ApplyDelta(NodeHolder(node), uplink, downlink, sampleEpoch)
|
||||
if delta == nil {
|
||||
continue
|
||||
@@ -1323,6 +1457,24 @@ func (o *Orchestrator) RecordUsage(serverID string, samples []UsageSample, errTe
|
||||
}
|
||||
node.MeteredTraffic = &MeteredTraffic{Uplink: mu + delta.UpDelta, Downlink: md + delta.DownDelta}
|
||||
node.LastUsageAt = StrPtr(reportedAt)
|
||||
node.LastSampleAt = StrPtr(reportedAt)
|
||||
if delta.UpDelta > 0 || delta.DownDelta > 0 {
|
||||
node.LastActiveAt = StrPtr(reportedAt)
|
||||
}
|
||||
if !restarted && prevCounters != nil && prevSampleISO != nil {
|
||||
if prevMs, ok1 := ParseISOMs(*prevSampleISO); ok1 {
|
||||
if nowMs, ok2 := ParseISOMs(reportedAt); ok2 {
|
||||
dt := float64(nowMs-prevMs) / 1000
|
||||
if dt >= 5 {
|
||||
up := float64(int64(math.Round(float64(uplink-prevCounters.Uplink) / dt)))
|
||||
down := float64(int64(math.Round(float64(downlink-prevCounters.Downlink) / dt)))
|
||||
node.SpeedUpBps = &up
|
||||
node.SpeedDownBps = &down
|
||||
node.SpeedAt = StrPtr(reportedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user