feat: 节点互联双向同步——授权/计划/客户(邮箱)按业务键LWW增量同步,HTTPS+HMAC互联密钥,默认10分钟宽松节奏
This commit is contained in:
1 parent
06a9dac99b
commit
44387835e1
13 files changed
+1180
-1
No files matched your search
@@ -0,0 +1,163 @@
|
||||
//! 节点互联控制器:管理端点(JWT)+ 节点间入站端点(HMAC)。 // Logan.Li
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"backend/config"
|
||||
"backend/internal/dto"
|
||||
"backend/internal/model"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
syncsvc "backend/internal/service/sync"
|
||||
)
|
||||
|
||||
// Controller 节点互联控制器 // Logan.Li
|
||||
type Controller struct {
|
||||
svc *syncsvc.Service
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewController 创建控制器实例 // Logan.Li
|
||||
func NewController(db *gorm.DB, cfg *config.Config) *Controller {
|
||||
return &Controller{svc: syncsvc.NewService(db, cfg), db: db}
|
||||
}
|
||||
|
||||
// Svc 暴露 service 供路由层启动定时循环 // Logan.Li
|
||||
func (ctrl *Controller) Svc() *syncsvc.Service { return ctrl.svc }
|
||||
|
||||
// ============ 管理端点(JWT 保护) ============
|
||||
|
||||
// ListPeers 节点列表 // Logan.Li
|
||||
func (ctrl *Controller) ListPeers(c *gin.Context) {
|
||||
peers, err := ctrl.svc.ListPeers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{
|
||||
"list": peers, "node_id": ctrl.svc.NodeID(),
|
||||
}})
|
||||
}
|
||||
|
||||
// SavePeer 创建/更新节点 // Logan.Li
|
||||
func (ctrl *Controller) SavePeer(c *gin.Context) {
|
||||
var req dto.PeerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
peer, err := ctrl.svc.SavePeer(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "已保存,请在对端配置相同互联密钥后测试握手", "data": peer})
|
||||
}
|
||||
|
||||
// DeletePeer 删除节点 // Logan.Li
|
||||
func (ctrl *Controller) DeletePeer(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err := ctrl.svc.DeletePeer(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "已删除"})
|
||||
}
|
||||
|
||||
// TestHandshake 手动握手测试 // Logan.Li
|
||||
func (ctrl *Controller) TestHandshake(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
peer, err := ctrl.svc.TestHandshake(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "握手成功,互联已激活", "data": peer})
|
||||
}
|
||||
|
||||
// SyncNow 手动触发一轮双向同步 // Logan.Li
|
||||
func (ctrl *Controller) SyncNow(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var peer model.PeerNode
|
||||
if err := ctrl.db.First(&peer, id).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": "节点不存在"})
|
||||
return
|
||||
}
|
||||
result, err := ctrl.svc.ExchangeOut(&peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "同步完成: " + result})
|
||||
}
|
||||
|
||||
// GenerateKey 生成互联密钥 // Logan.Li
|
||||
func (ctrl *Controller) GenerateKey(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"shared_key": syncsvc.GenerateSharedKey()}})
|
||||
}
|
||||
|
||||
// ============ 节点间入站端点(HMAC 保护,供对端调用) ============
|
||||
|
||||
const maxSyncBody = 32 << 20
|
||||
|
||||
// handshakeIn POST /api/v1/sync/handshake // Logan.Li
|
||||
func (ctrl *Controller) HandshakeIn(c *gin.Context) {
|
||||
body, peer := ctrl.readAndVerify(c)
|
||||
if peer == nil {
|
||||
return // readAndVerify 已写响应 // Logan.Li
|
||||
}
|
||||
var req dto.SyncHandshakeRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": "请求体非法"})
|
||||
return
|
||||
}
|
||||
resp, err := ctrl.svc.HandshakeIn(peer, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// exchangeIn POST /api/v1/sync/exchange // Logan.Li
|
||||
func (ctrl *Controller) ExchangeIn(c *gin.Context) {
|
||||
body, peer := ctrl.readAndVerify(c)
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
var req dto.SyncExchangeRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": "请求体非法"})
|
||||
return
|
||||
}
|
||||
resp, err := ctrl.svc.ExchangeIn(peer, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// readAndVerify 读取请求体并按互联密钥验签定位对端;失败时已写响应 // Logan.Li
|
||||
func (ctrl *Controller) readAndVerify(c *gin.Context) ([]byte, *model.PeerNode) {
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxSyncBody))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "message": "读取请求体失败"})
|
||||
return nil, nil
|
||||
}
|
||||
nodeID := c.Request.Header.Get("X-Sync-Node")
|
||||
peer, err := ctrl.svc.ResolvePeer(nodeID, func(p *model.PeerNode) error {
|
||||
return syncsvc.VerifySignature(p.SharedKey, c.Request, body)
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "message": err.Error()})
|
||||
return nil, nil
|
||||
}
|
||||
return body, peer
|
||||
}
|
||||
Reference in new issue
Block a user