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,173 @@
|
||||
//! 同步载荷应用:按业务键 upsert,仅当载荷新于本地时更新(last-write-wins)。 // Logan.Li
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/internal/dto"
|
||||
"backend/internal/model"
|
||||
)
|
||||
|
||||
// upsertProduct 按产品 code 定位 // Logan.Li
|
||||
func (s *Service) upsertProduct(p dto.SyncProductPayload) bool {
|
||||
if strings.TrimSpace(p.Code) == "" {
|
||||
return false
|
||||
}
|
||||
var local model.Product
|
||||
err := s.db.Where("code = ?", p.Code).First(&local).Error
|
||||
if err != nil {
|
||||
local = model.Product{
|
||||
Code: p.Code, Name: p.Name, Version: p.Version, Description: p.Description,
|
||||
Features: p.Features, Price: p.Price, PriceModel: p.PriceModel,
|
||||
Status: p.Status, CreatedAt: p.UpdatedAt, UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
return s.db.Create(&local).Error == nil
|
||||
}
|
||||
if !p.UpdatedAt.After(local.UpdatedAt) {
|
||||
return false // 本地已是最新的,跳过 // Logan.Li
|
||||
}
|
||||
local.Name = p.Name
|
||||
local.Version = p.Version
|
||||
local.Description = p.Description
|
||||
local.Features = p.Features
|
||||
local.Price = p.Price
|
||||
local.PriceModel = p.PriceModel
|
||||
local.Status = p.Status
|
||||
return s.db.Save(&local).Error == nil
|
||||
}
|
||||
|
||||
// upsertPlan 按 产品code+plan_type+duration 定位 // Logan.Li
|
||||
func (s *Service) upsertPlan(pl dto.SyncPlanPayload) bool {
|
||||
if pl.ProductCode == "" {
|
||||
return false
|
||||
}
|
||||
var product model.Product
|
||||
if err := s.db.Where("code = ?", pl.ProductCode).First(&product).Error; err != nil {
|
||||
return false // 产品依赖未就绪,等下一轮 // Logan.Li
|
||||
}
|
||||
var local model.Plan
|
||||
err := s.db.Where("product_id = ? AND plan_type = ? AND duration = ?", product.ID, pl.PlanType, pl.Duration).First(&local).Error
|
||||
if err != nil {
|
||||
local = model.Plan{
|
||||
ProductID: product.ID, Name: pl.Name, PlanType: pl.PlanType,
|
||||
Price: pl.Price, Currency: pl.Currency, Duration: pl.Duration,
|
||||
MaxDevices: pl.MaxDevices, Features: pl.Features, SortOrder: pl.SortOrder,
|
||||
Status: pl.Status, CreatedAt: pl.UpdatedAt, UpdatedAt: pl.UpdatedAt,
|
||||
}
|
||||
return s.db.Create(&local).Error == nil
|
||||
}
|
||||
if !pl.UpdatedAt.After(local.UpdatedAt) {
|
||||
return false
|
||||
}
|
||||
local.Name = pl.Name
|
||||
local.Price = pl.Price
|
||||
local.Currency = pl.Currency
|
||||
local.MaxDevices = pl.MaxDevices
|
||||
local.Features = pl.Features
|
||||
local.SortOrder = pl.SortOrder
|
||||
local.Status = pl.Status
|
||||
return s.db.Save(&local).Error == nil
|
||||
}
|
||||
|
||||
// upsertCustomer 按 email 定位(空/占位邮箱回退 name),合规最小字段 // Logan.Li
|
||||
func (s *Service) upsertCustomer(c dto.SyncCustomerPayload) bool {
|
||||
email := strings.TrimSpace(c.Email)
|
||||
name := strings.TrimSpace(c.Name)
|
||||
var local model.Customer
|
||||
var err error
|
||||
if email != "" && !strings.HasSuffix(email, "@local.invalid") {
|
||||
err = s.db.Where("email = ?", email).First(&local).Error
|
||||
} else {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
err = s.db.Where("name = ?", name).Order("id ASC").First(&local).Error
|
||||
}
|
||||
if err != nil {
|
||||
if email == "" {
|
||||
email = fmt.Sprintf("sync+%d@local.invalid", time.Now().UnixNano())
|
||||
}
|
||||
local = model.Customer{
|
||||
Name: name, Company: c.Company, Email: email,
|
||||
Status: c.Status, CreatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
return s.db.Create(&local).Error == nil
|
||||
}
|
||||
if !c.UpdatedAt.After(local.UpdatedAt) {
|
||||
return false
|
||||
}
|
||||
local.Name = name
|
||||
local.Company = c.Company
|
||||
if email != "" && !strings.HasSuffix(email, "@local.invalid") {
|
||||
local.Email = email
|
||||
}
|
||||
local.Status = c.Status
|
||||
return s.db.Save(&local).Error == nil
|
||||
}
|
||||
|
||||
// upsertLicense 按 license_key 定位,跨表解析产品与客户 // Logan.Li
|
||||
func (s *Service) upsertLicense(l dto.SyncLicensePayload) bool {
|
||||
if strings.TrimSpace(l.LicenseKey) == "" || l.ProductCode == "" {
|
||||
return false
|
||||
}
|
||||
var product model.Product
|
||||
if err := s.db.Where("code = ?", l.ProductCode).First(&product).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
customerID, err := s.resolveCustomer(l.CustomerEmail, l.CustomerName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var local model.License
|
||||
err = s.db.Where("license_key = ?", l.LicenseKey).First(&local).Error
|
||||
if err != nil {
|
||||
local = model.License{
|
||||
LicenseKey: l.LicenseKey, ProductID: product.ID, CustomerID: customerID,
|
||||
Status: l.Status, StartDate: l.StartDate, EndDate: l.EndDate,
|
||||
Duration: l.Duration, MaxActivations: l.MaxActivations,
|
||||
CurrentActivations: l.CurrentActivations,
|
||||
IsTrial: l.IsTrial, TrialDays: l.TrialDays,
|
||||
CreatedAt: l.UpdatedAt, UpdatedAt: l.UpdatedAt,
|
||||
}
|
||||
return s.db.Create(&local).Error == nil
|
||||
}
|
||||
if !l.UpdatedAt.After(local.UpdatedAt) {
|
||||
return false
|
||||
}
|
||||
local.ProductID = product.ID
|
||||
local.CustomerID = customerID
|
||||
local.Status = l.Status
|
||||
local.StartDate = l.StartDate
|
||||
local.EndDate = l.EndDate
|
||||
local.Duration = l.Duration
|
||||
local.MaxActivations = l.MaxActivations
|
||||
local.CurrentActivations = l.CurrentActivations
|
||||
local.IsTrial = l.IsTrial
|
||||
local.TrialDays = l.TrialDays
|
||||
return s.db.Save(&local).Error == nil
|
||||
}
|
||||
|
||||
// resolveCustomer 按邮箱解析或创建客户(离线占位客户按名字复用) // Logan.Li
|
||||
func (s *Service) resolveCustomer(email, name string) (uint, error) {
|
||||
email = strings.TrimSpace(email)
|
||||
name = strings.TrimSpace(name)
|
||||
if email == "" {
|
||||
email = fmt.Sprintf("sync+%d@local.invalid", time.Now().UnixNano())
|
||||
}
|
||||
var customer model.Customer
|
||||
if err := s.db.Where("email = ?", email).First(&customer).Error; err == nil {
|
||||
return customer.ID, nil
|
||||
}
|
||||
customer = model.Customer{Name: name, Email: email, Status: 1}
|
||||
if err := s.db.Create(&customer).Error; err != nil {
|
||||
// 并发下可能已存在,再查一次 // Logan.Li
|
||||
if err2 := s.db.Where("email = ?", email).First(&customer).Error; err2 == nil {
|
||||
return customer.ID, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return customer.ID, nil
|
||||
}
|
||||
Reference in new issue
Block a user