133 lines
3.6 KiB
Go
133 lines
3.6 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
|
|||
|
|
"nl-pms-api/internal/commonservice"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// SyncService 个人待办/工单/记事本的推拉(LWW)。
|
|||
|
|
type SyncService struct {
|
|||
|
|
DB *gorm.DB
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type syncTableMeta struct {
|
|||
|
|
remote string
|
|||
|
|
cols []string // 不含 user_id;含 uuid … updated_at deleted
|
|||
|
|
hasProject bool
|
|||
|
|
hasTimes bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var syncTables = map[string]syncTableMeta{
|
|||
|
|
"todos": {
|
|||
|
|
remote: "sync_todos",
|
|||
|
|
hasProject: true,
|
|||
|
|
hasTimes: true,
|
|||
|
|
cols: []string{"uuid", "title", "content", "due_at", "priority", "status", "history", "team_id", "project_name", "created_at", "updated_at", "deleted"},
|
|||
|
|
},
|
|||
|
|
"tickets": {
|
|||
|
|
remote: "sync_tickets",
|
|||
|
|
hasProject: true,
|
|||
|
|
hasTimes: true,
|
|||
|
|
cols: []string{"uuid", "title", "description", "type", "start_at", "due_at", "status", "priority", "history", "team_id", "project_name", "created_at", "updated_at", "deleted"},
|
|||
|
|
},
|
|||
|
|
"notes": {
|
|||
|
|
remote: "sync_notes",
|
|||
|
|
cols: []string{"uuid", "content", "updated_at", "deleted"},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Push 对指定表做 LWW upsert;row 中不含 user_id(取自 JWT)。
|
|||
|
|
func (s *SyncService) Push(table string, userID int64, rows []map[string]any) (int, error) {
|
|||
|
|
meta, ok := syncTables[table]
|
|||
|
|
if !ok {
|
|||
|
|
return 0, commonservice.BadRequest("SYNC_BAD_TABLE")
|
|||
|
|
}
|
|||
|
|
if userID <= 0 {
|
|||
|
|
return 0, commonservice.Unauthorized("UNAUTHORIZED")
|
|||
|
|
}
|
|||
|
|
remoteCols := append([]string{"user_id"}, meta.cols...)
|
|||
|
|
set := make([]string, 0, len(meta.cols))
|
|||
|
|
for _, c := range meta.cols {
|
|||
|
|
if c == "uuid" || c == "updated_at" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
set = append(set, fmt.Sprintf("%s=IF(VALUES(updated_at)>updated_at,VALUES(%s),%s)", c, c, c))
|
|||
|
|
}
|
|||
|
|
set = append(set, "updated_at=IF(VALUES(updated_at)>updated_at,VALUES(updated_at),updated_at)")
|
|||
|
|
placeholders := strings.TrimRight(strings.Repeat("?,", len(remoteCols)), ",")
|
|||
|
|
q := fmt.Sprintf("INSERT INTO %s(%s) VALUES(%s) ON DUPLICATE KEY UPDATE %s",
|
|||
|
|
meta.remote, strings.Join(remoteCols, ","), placeholders, strings.Join(set, ","))
|
|||
|
|
|
|||
|
|
n := 0
|
|||
|
|
for _, row := range rows {
|
|||
|
|
uuid := commonservice.MapStr(row, "uuid")
|
|||
|
|
if uuid == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
args := make([]any, 0, len(remoteCols))
|
|||
|
|
args = append(args, userID)
|
|||
|
|
for _, c := range meta.cols {
|
|||
|
|
switch c {
|
|||
|
|
case "deleted", "team_id":
|
|||
|
|
args = append(args, commonservice.MapInt64(row, c))
|
|||
|
|
default:
|
|||
|
|
args = append(args, commonservice.MapStr(row, c))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if err := s.DB.Exec(q, args...).Error; err != nil {
|
|||
|
|
return n, commonservice.Internal("SYNC_PUSH_FAILED")
|
|||
|
|
}
|
|||
|
|
n++
|
|||
|
|
}
|
|||
|
|
return n, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Pull 按 updated_at 游标增量拉取,最多 2000 条。
|
|||
|
|
func (s *SyncService) Pull(table string, userID int64, cursor string) ([]map[string]any, error) {
|
|||
|
|
meta, ok := syncTables[table]
|
|||
|
|
if !ok {
|
|||
|
|
return nil, commonservice.BadRequest("SYNC_BAD_TABLE")
|
|||
|
|
}
|
|||
|
|
if userID <= 0 {
|
|||
|
|
return nil, commonservice.Unauthorized("UNAUTHORIZED")
|
|||
|
|
}
|
|||
|
|
q := fmt.Sprintf("SELECT %s FROM %s WHERE user_id=? AND updated_at>? ORDER BY updated_at LIMIT 2000",
|
|||
|
|
strings.Join(meta.cols, ","), meta.remote)
|
|||
|
|
rawRows, err := s.DB.Raw(q, userID, cursor).Rows()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, commonservice.Internal("SYNC_PULL_FAILED")
|
|||
|
|
}
|
|||
|
|
defer rawRows.Close()
|
|||
|
|
|
|||
|
|
out := []map[string]any{}
|
|||
|
|
for rawRows.Next() {
|
|||
|
|
vals := make([]any, len(meta.cols))
|
|||
|
|
ptrs := make([]any, len(meta.cols))
|
|||
|
|
for i := range vals {
|
|||
|
|
ptrs[i] = &vals[i]
|
|||
|
|
}
|
|||
|
|
if err := rawRows.Scan(ptrs...); err != nil {
|
|||
|
|
return nil, commonservice.Internal("SYNC_PULL_FAILED")
|
|||
|
|
}
|
|||
|
|
m := map[string]any{}
|
|||
|
|
for i, c := range meta.cols {
|
|||
|
|
m[c] = coerceSQL(vals[i])
|
|||
|
|
}
|
|||
|
|
out = append(out, m)
|
|||
|
|
}
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func coerceSQL(v any) any {
|
|||
|
|
switch x := v.(type) {
|
|||
|
|
case []byte:
|
|||
|
|
return string(x)
|
|||
|
|
default:
|
|||
|
|
return x
|
|||
|
|
}
|
|||
|
|
}
|