feat(metadata): 配置持久化

This commit is contained in:
xugo
2026-04-10 22:20:14 +08:00
parent 7f0e6cd107
commit 503af23ca0
14 changed files with 375 additions and 5 deletions
-5
View File
@@ -14,7 +14,6 @@ import (
"time" "time"
"github.com/gowvp/owl/internal/conf" "github.com/gowvp/owl/internal/conf"
"github.com/ixugo/goddd/domain/version/versionapi"
"github.com/ixugo/goddd/pkg/logger" "github.com/ixugo/goddd/pkg/logger"
"github.com/ixugo/goddd/pkg/server" "github.com/ixugo/goddd/pkg/server"
"github.com/ixugo/goddd/pkg/system" "github.com/ixugo/goddd/pkg/system"
@@ -51,10 +50,6 @@ func Run(bc *conf.Bootstrap) {
go setupAIClient(ctx, "http://127.0.0.1:15123/ai", bc.Debug) go setupAIClient(ctx, "http://127.0.0.1:15123/ai", bc.Debug)
} }
// 如果需要执行表迁移,递增此版本号和表更新说明
versionapi.DBVersion = "0.0.23"
versionapi.DBRemark = "onvif device support"
handler, cleanUp, err := wireApp(bc, log) handler, cleanUp, err := wireApp(bc, log)
if err != nil { if err != nil {
slog.Error("程序构建失败", "err", err) slog.Error("程序构建失败", "err", err)
+4
View File
@@ -8,6 +8,7 @@ package app
import ( import (
"github.com/gowvp/owl/internal/conf" "github.com/gowvp/owl/internal/conf"
"github.com/gowvp/owl/internal/core/metadata/metadataapi"
"github.com/gowvp/owl/internal/data" "github.com/gowvp/owl/internal/data"
"github.com/gowvp/owl/internal/web/api" "github.com/gowvp/owl/internal/web/api"
"github.com/gowvp/owl/pkg/gbs" "github.com/gowvp/owl/pkg/gbs"
@@ -43,6 +44,8 @@ func wireApp(bc *conf.Bootstrap, log *slog.Logger) (http.Handler, func(), error)
aiWebhookAPI := api.NewAIWebhookAPIWithDeps(bc, eventCore, ipcBundle) aiWebhookAPI := api.NewAIWebhookAPIWithDeps(bc, eventCore, ipcBundle)
eventAPI := api.NewEventAPI(eventCore, bc) eventAPI := api.NewEventAPI(eventCore, bc)
recordingAPI := api.NewRecordingAPI(recordingCore, bc) recordingAPI := api.NewRecordingAPI(recordingCore, bc)
metadataCore := metadataapi.NewMetadataCore(db)
metadataAPI := metadataapi.NewMetadataAPI(metadataCore)
usecase := &api.Usecase{ usecase := &api.Usecase{
Conf: bc, Conf: bc,
DB: db, DB: db,
@@ -57,6 +60,7 @@ func wireApp(bc *conf.Bootstrap, log *slog.Logger) (http.Handler, func(), error)
AIWebhookAPI: aiWebhookAPI, AIWebhookAPI: aiWebhookAPI,
EventAPI: eventAPI, EventAPI: eventAPI,
RecordingAPI: recordingAPI, RecordingAPI: recordingAPI,
MetadataAPI: metadataAPI,
} }
handler := api.NewHTTPHandler(usecase) handler := api.NewHTTPHandler(usecase)
return handler, func() { return handler, func() {
+17
View File
@@ -0,0 +1,17 @@
// Code generated by godddx, DO AVOID EDIT.
package metadata
// Storer data persistence
type Storer interface {
Metadata() MetadataStorer
}
// Core business domain
type Core struct {
store Storer
}
// NewCore create business domain
func NewCore(store Storer) Core {
return Core{store: store}
}
+4
View File
@@ -0,0 +1,4 @@
// Package metadata 通用数据持久化领域
// 提供 JSON 数据的存储能力,ID 由调用方指定,
// 仅支持创建、修改、按 ID 查询,不提供列表和删除接口
package metadata
+116
View File
@@ -0,0 +1,116 @@
// Code generated by godddx, DO AVOID EDIT.
package metadata
import (
"context"
"log/slog"
"github.com/ixugo/goddd/pkg/orm"
"github.com/ixugo/goddd/pkg/reason"
"github.com/jinzhu/copier"
"gorm.io/gorm"
)
// MetadataStorer Instantiation interface
type MetadataStorer interface {
Find(context.Context, *[]*Metadata, orm.Pager, ...orm.QueryOption) (int64, error)
Get(context.Context, *Metadata, ...orm.QueryOption) error
Add(context.Context, *Metadata) error
Edit(context.Context, *Metadata, func(*Metadata), ...orm.QueryOption) error
Del(context.Context, *Metadata, ...orm.QueryOption) error
Count(context.Context, ...orm.QueryOption) (int64, error)
Session(context.Context, ...func(*gorm.DB) error) error
EditWithSession(*gorm.DB, *Metadata, func(b *Metadata) error, ...orm.QueryOption) error
}
// FindMetadatas 分页查询(保留代码,当前不提供列表接口)
// func (c Core) FindMetadatas(ctx context.Context, in *FindMetadataInput) ([]*Metadata, int64, error) {
// query := orm.NewQuery(1).OrderBy("created_at DESC")
// items := make([]*Metadata, 0, in.Limit())
// total, err := c.store.Metadata().Find(ctx, &items, in, query.Encode()...)
// if err != nil {
// return nil, 0, reason.ErrDB.Withf(`Find in[%+v] err[%s]`, in, err.Error())
// }
// return items, total, nil
// }
// GetMetadata 按 ID 查询单条数据
func (c Core) GetMetadata(ctx context.Context, id string) (*Metadata, error) {
out := Metadata{ID: id}
if err := c.store.Metadata().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
if orm.IsErrRecordNotFound(err) {
return nil, reason.ErrNotFound.Withf(`Get id[%v] err[%s]`, id, err.Error())
}
return nil, reason.ErrDB.Withf(`Get id[%v] err[%s]`, id, err.Error())
}
return &out, nil
}
// AddMetadata 创建数据记录
func (c Core) AddMetadata(ctx context.Context, in *AddMetadataInput) (*Metadata, error) {
var out Metadata
if err := copier.Copy(&out, in); err != nil {
slog.ErrorContext(ctx, "Copy", "err", err)
}
if err := c.store.Metadata().Add(ctx, &out); err != nil {
if orm.IsDuplicatedKey(err) {
return nil, reason.ErrBadRequest.SetMsg("数据重复").Withf("key[%s]", in.ID)
}
return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
}
return &out, nil
}
// EditMetadata 更新数据记录
func (c Core) EditMetadata(ctx context.Context, in *EditMetadataInput, id string) (*Metadata, error) {
var out Metadata
if err := c.store.Metadata().Edit(ctx, &out, func(b *Metadata) {
if err := copier.Copy(b, in); err != nil {
slog.ErrorContext(ctx, "Copy", "err", err)
}
b.LastUpdatedBy = in.LastUpdatedBy
}, orm.Where("id=?", id)); err != nil {
return nil, reason.ErrDB.Withf(`Edit id[%v] err[%s]`, id, err.Error())
}
return &out, nil
}
// SaveMetadata 幂等保存:先尝试更新已有记录,不存在则创建
func (c Core) SaveMetadata(ctx context.Context, in *SaveMetadataInput, id string) (*Metadata, error) {
var out Metadata
err := c.store.Metadata().Edit(ctx, &out, func(b *Metadata) {
b.Ext = in.Ext
b.LastUpdatedBy = in.LastUpdatedBy
}, orm.Where("id=?", id))
if err == nil {
return &out, nil
}
if !orm.IsErrRecordNotFound(err) {
return nil, reason.ErrDB.Withf(`Edit id[%v] err[%s]`, id, err.Error())
}
out = Metadata{
ID: id,
Ext: in.Ext,
CreatedBy: in.CreatedBy,
LastUpdatedBy: in.LastUpdatedBy,
}
if err := c.store.Metadata().Add(ctx, &out); err != nil {
if orm.IsDuplicatedKey(err) {
return nil, reason.ErrBadRequest.SetMsg("数据重复").Withf("key[%s]", id)
}
return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
}
return &out, nil
}
// DelMetadata 删除数据(保留代码,当前不提供删除接口)
// func (c Core) DelMetadata(ctx context.Context, id string) (*Metadata, error) {
// var out Metadata
// if err := c.store.Metadata().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
// return nil, reason.ErrDB.Withf(`Del id[%v] err[%s]`, id, err.Error())
// }
// return &out, nil
// }
+26
View File
@@ -0,0 +1,26 @@
// Code generated by godddx, DO AVOID EDIT.
package metadata
import "github.com/ixugo/goddd/pkg/orm"
// Metadata domain model
type Metadata struct {
ID string `gorm:"primaryKey" json:"id"`
CreatedAt orm.Time `gorm:"column:created_at;notNull;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt orm.Time `gorm:"column:updated_at;notNull;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
CreatedBy string `gorm:"column:created_by;notNull;default:'';comment:创建者" json:"created_by"` // 创建者
LastUpdatedBy string `gorm:"column:last_updated_by;notNull;default:'';comment:最后更新者" json:"last_updated_by"` // 最后更新者
Ext string `gorm:"column:ext;type:text;notNull;default:'';comment:序列化的 JSON 字符串" json:"ext"` // 序列化的 JSON 字符串
}
// TableName database table name
func (*Metadata) TableName() string {
return "metadatas"
}
// CacheKey 缓存主键,必须唯一
// godddx 生成缓存代码时,依赖的主键
// 默认应该是 ID 字段,但也可以自定义
func (m *Metadata) CacheKey() string {
return m.ID
}
+31
View File
@@ -0,0 +1,31 @@
// Code generated by godddx, DO AVOID EDIT.
package metadata
// FindMetadataInput 查询数据参数(保留未使用)
// type FindMetadataInput struct {
// web.PagerFilter
// }
// AddMetadataInput 新增数据参数
type AddMetadataInput struct {
ID string `json:"id" binding:"required,max=64"` // 调用方指定的唯一标识
Ext string `json:"ext" binding:"required,max=131072"` // 序列化的 JSON 字符串
CreatedBy string `json:"-"` // 创建者(API层填充)
LastUpdatedBy string `json:"-"` // 最后更新者(API层填充)
}
// EditMetadataInput 编辑数据参数
type EditMetadataInput struct {
Ext string `json:"ext" binding:"required,max=131072"` // 序列化的 JSON 字符串
LastUpdatedBy string `json:"-"` // 最后更新者(API层填充)
}
// SaveMetadataInput 保存数据参数,合并创建与更新为一个幂等操作
type SaveMetadataInput struct {
Ext string `json:"ext" binding:"required,max=131072"` // 序列化的 JSON 字符串
CreatedBy string `json:"-"` // 创建者(API层填充)
LastUpdatedBy string `json:"-"` // 最后更新者(API层填充)
}
+61
View File
@@ -0,0 +1,61 @@
// Code generated by godddx, DO AVOID EDIT.
package metadataapi
import (
"github.com/gin-gonic/gin"
"github.com/gowvp/owl/internal/core/metadata"
"github.com/gowvp/owl/internal/core/metadata/store/metadatadb"
"github.com/ixugo/goddd/pkg/orm"
"github.com/ixugo/goddd/pkg/web"
"gorm.io/gorm"
)
// MetadataAPI 为 http 提供通用数据持久化的业务方法
type MetadataAPI struct {
metadataCore metadata.Core
}
func NewMetadataCore(db *gorm.DB) metadata.Core {
var store metadata.Storer
store = metadatadb.NewDB(db).AutoMigrate(orm.GetEnabledAutoMigrate())
return metadata.NewCore(store)
}
func NewMetadataAPI(core metadata.Core) MetadataAPI {
return MetadataAPI{metadataCore: core}
}
// RegisterMetadata 注册通用数据持久化路由
func RegisterMetadata(g gin.IRouter, api MetadataAPI, handler ...gin.HandlerFunc) {
group := g.Group("/metadatas", handler...)
group.GET("/:id", web.WrapH(api.getMetadata))
group.POST("/:id", web.WrapH(api.saveMetadata))
}
// >>> metadata >>>>>>>>>>>>>>>>>>>>
// findMetadatas 分页查询(保留代码,当前不提供)
// func (a MetadataAPI) findMetadatas(c *gin.Context, in *metadata.FindMetadataInput) (any, error) {
// items, total, err := a.metadataCore.FindMetadatas(c.Request.Context(), in)
// return gin.H{"items": items, "total": total}, err
// }
// getMetadata 按 ID 查询数据
func (a MetadataAPI) getMetadata(c *gin.Context, _ *struct{}) (*metadata.Metadata, error) {
metadataID := c.Param("id")
return a.metadataCore.GetMetadata(c.Request.Context(), metadataID)
}
// saveMetadata 幂等保存:已存在则更新,不存在则创建
func (a MetadataAPI) saveMetadata(c *gin.Context, in *metadata.SaveMetadataInput) (*metadata.Metadata, error) {
metadataID := c.Param("id")
in.CreatedBy = web.GetUsername(c)
in.LastUpdatedBy = in.CreatedBy
return a.metadataCore.SaveMetadata(c.Request.Context(), in, metadataID)
}
// delMetadata 删除数据(保留代码,当前不提供)
// func (a MetadataAPI) delMetadata(c *gin.Context, _ *struct{}) (*metadata.Metadata, error) {
// metadataID := c.Param("id")
// return a.metadataCore.DelMetadata(c.Request.Context(), metadataID)
// }
+2
View File
@@ -0,0 +1,2 @@
// Code generated by godddx, DO AVOID EDIT.
package metadata
+37
View File
@@ -0,0 +1,37 @@
// Code generated by godddx, DO AVOID EDIT.
package metadatadb
import (
"github.com/gowvp/owl/internal/core/metadata"
"gorm.io/gorm"
)
var _ metadata.Storer = DB{}
// DB Related business namespaces
type DB struct {
db *gorm.DB
}
// NewDB instance object
func NewDB(db *gorm.DB) DB {
return DB{db: db}
}
// Metadata Get business instance
func (d DB) Metadata() metadata.MetadataStorer {
return Metadata(d)
}
// AutoMigrate sync database
func (d DB) AutoMigrate(ok bool) DB {
if !ok {
return d
}
if err := d.db.AutoMigrate(
new(metadata.Metadata),
); err != nil {
panic(err)
}
return d
}
+67
View File
@@ -0,0 +1,67 @@
// Code generated by godddx, DO AVOID EDIT.
package metadatadb
import (
"context"
"github.com/gowvp/owl/internal/core/metadata"
"github.com/ixugo/goddd/pkg/orm"
"gorm.io/gorm"
)
var _ metadata.MetadataStorer = Metadata{}
// Metadata Related business namespaces
type Metadata DB
// NewMetadata instance object
func NewMetadata(db *gorm.DB) Metadata {
return Metadata{db: db}
}
// Find implements metadata.MetadataStorer.
func (d Metadata) Find(ctx context.Context, bs *[]*metadata.Metadata, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
return orm.FindWithContext(ctx, d.db, bs, page, opts...)
}
// Get implements metadata.MetadataStorer.
func (d Metadata) Get(ctx context.Context, model *metadata.Metadata, opts ...orm.QueryOption) error {
return orm.FirstWithContext(ctx, d.db, model, opts...)
}
// Add implements metadata.MetadataStorer.
func (d Metadata) Add(ctx context.Context, model *metadata.Metadata) error {
return d.db.WithContext(ctx).Create(model).Error
}
// Edit implements metadata.MetadataStorer.
func (d Metadata) Edit(ctx context.Context, model *metadata.Metadata, changeFn func(*metadata.Metadata), opts ...orm.QueryOption) error {
return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
}
// Del implements metadata.MetadataStorer.
func (d Metadata) Del(ctx context.Context, model *metadata.Metadata, opts ...orm.QueryOption) error {
return orm.DeleteWithContext(ctx, d.db, model, opts...)
}
// Count implements metadata.MetadataStorer.
func (d Metadata) Count(ctx context.Context, opts ...orm.QueryOption) (int64, error) {
return orm.CountWithContext[metadata.Metadata](ctx, d.db, opts...)
}
// Session 事务组合
func (d Metadata) Session(ctx context.Context, changeFns ...func(*gorm.DB) error) error {
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, fn := range changeFns {
if err := fn(tx); err != nil {
return err
}
}
return nil
})
}
// EditWithSession 修改事务
func (d Metadata) EditWithSession(tx *gorm.DB, model *metadata.Metadata, changeFn func(b *metadata.Metadata) error, opts ...orm.QueryOption) error {
return orm.UpdateWithSession(tx, model, changeFn, opts...)
}
+2
View File
@@ -18,6 +18,7 @@ import (
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gowvp/owl/internal/core/metadata/metadataapi"
"github.com/gowvp/owl/internal/core/sms" "github.com/gowvp/owl/internal/core/sms"
"github.com/gowvp/owl/pkg/ota" "github.com/gowvp/owl/pkg/ota"
"github.com/gowvp/owl/plugin/stat" "github.com/gowvp/owl/plugin/stat"
@@ -122,6 +123,7 @@ func setupRouter(r *gin.Engine, uc *Usecase) {
uc.AIWebhookAPI.StartAISyncLoop(context.Background(), uc.SMSAPI.smsCore) uc.AIWebhookAPI.StartAISyncLoop(context.Background(), uc.SMSAPI.smsCore)
RegisterEvent(r, uc.EventAPI, auth) RegisterEvent(r, uc.EventAPI, auth)
RegisterRecording(r, uc.RecordingAPI, auth) RegisterRecording(r, uc.RecordingAPI, auth)
metadataapi.RegisterMetadata(r, uc.MetadataAPI, auth)
} }
type playOutput struct { type playOutput struct {
+3
View File
@@ -15,6 +15,7 @@ import (
"github.com/gowvp/owl/internal/core/ipc" "github.com/gowvp/owl/internal/core/ipc"
"github.com/gowvp/owl/internal/core/ipc/store/ipccache" "github.com/gowvp/owl/internal/core/ipc/store/ipccache"
"github.com/gowvp/owl/internal/core/ipc/store/ipcdb" "github.com/gowvp/owl/internal/core/ipc/store/ipcdb"
"github.com/gowvp/owl/internal/core/metadata/metadataapi"
"github.com/gowvp/owl/internal/core/recording" "github.com/gowvp/owl/internal/core/recording"
"github.com/gowvp/owl/internal/core/recording/adapter" "github.com/gowvp/owl/internal/core/recording/adapter"
"github.com/gowvp/owl/internal/core/sms" "github.com/gowvp/owl/internal/core/sms"
@@ -49,6 +50,7 @@ var (
NewEventCore, NewEventAPI, NewEventCore, NewEventAPI,
// Recording: Store -> SMSProvider(adapter) -> Core -> API // Recording: Store -> SMSProvider(adapter) -> Core -> API
NewRecordingStore, NewSMSProviderAdapter, NewRecordingCore, NewRecordingAPI, NewRecordingStore, NewSMSProviderAdapter, NewRecordingCore, NewRecordingAPI,
metadataapi.NewMetadataCore, metadataapi.NewMetadataAPI,
) )
) )
@@ -69,6 +71,7 @@ type Usecase struct {
EventAPI EventAPI EventAPI EventAPI
RecordingAPI RecordingAPI RecordingAPI RecordingAPI
MetadataAPI metadataapi.MetadataAPI
} }
// NewHTTPHandler 生成Gin框架路由内容 // NewHTTPHandler 生成Gin框架路由内容
+5
View File
@@ -10,6 +10,7 @@ import (
"github.com/gowvp/owl/internal/app" "github.com/gowvp/owl/internal/app"
"github.com/gowvp/owl/internal/conf" "github.com/gowvp/owl/internal/conf"
"github.com/ixugo/goddd/domain/version/versionapi"
"github.com/ixugo/goddd/pkg/system" "github.com/ixugo/goddd/pkg/system"
) )
@@ -57,6 +58,10 @@ func main() {
})) }))
} }
// 如果需要执行表迁移,递增此版本号和表更新说明
versionapi.DBVersion = "0.0.25"
versionapi.DBRemark = "onvif device support"
app.Run(&bc) app.Run(&bc)
} }