kra-oa/app/system/data/repository/dictionary.go

468 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package system
import (
"context"
"errors"
"fmt"
"strings"
"time"
"kra/app/system/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
)
type dictionaryRepo struct{ data Provider }
func NewDictionaryRepo(data Provider) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
type dictionaryPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Type string
Status *bool
Desc string
ParentID *uint
}
func (dictionaryPO) TableName() string { return "sys_dictionaries" }
type dictionaryDetailPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Label string
Value string
Extend string
Status *bool
Sort int
DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
ParentID *uint
Level int
Path string
}
func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
}
func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
}
func parameterFromPO(po parameterPO) *biz.SystemParameter {
return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
}
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
return errors.New("存在相同的type不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.CreatedAt, v.UpdatedAt = po.CreatedAt, po.UpdatedAt
return nil
}
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", dictionary.Type).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type不允许导入")
}
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID}
if err := tx.Create(&po).Error; err != nil {
return err
}
dictionary.ID = po.ID
idMap := make(map[uint]uint, len(details))
created := make(map[uint]uint, len(details))
for index, item := range details {
// Incomplete detail rows are ignored to preserve import behavior.
if item.Label == "" || item.Value == "" {
continue
}
oldID := item.ID
detail := dictionaryDetailPO{Label: item.Label, Value: item.Value, Extend: item.Extend, Status: item.Status, Sort: item.Sort, DictionaryID: po.ID, Level: item.Level, Path: item.Path}
if err := tx.Create(&detail).Error; err != nil {
return err
}
created[uint(index)] = detail.ID
if oldID > 0 {
idMap[oldID] = detail.ID
}
}
for index, item := range details {
if item.ID == 0 || item.ParentID == nil || *item.ParentID == 0 {
continue
}
newID, exists := created[uint(index)]
newParentID, parentExists := idMap[*item.ParentID]
if !exists || !parentExists {
continue
}
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", newID).Update("parent_id", newParentID).Error; err != nil {
return err
}
}
return nil
})
}
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
db := r.data.DB().WithContext(ctx)
var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
return errors.New("查询字典数据失败")
}
if current.Type != v.Type {
var duplicate dictionaryPO
if err := db.Where("type = ?", v.Type).First(&duplicate).Error; err == nil {
return errors.New("存在相同的type不允许创建")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
if v.ParentID != nil && *v.ParentID != 0 {
if err := r.checkDictionaryCircularReference(ctx, v.ID, *v.ParentID); err != nil {
return err
}
}
return db.Model(&current).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
// checkDictionaryCircularReference mirrors the compatible recursive parent-chain
// check. A missing parent is allowed (the reference implementation permits a
// dangling parent ID); only a self-reference or an ancestor cycle is rejected.
func (r *dictionaryRepo) checkDictionaryCircularReference(ctx context.Context, currentID, parentID uint) error {
if currentID == parentID {
return errors.New("不能将字典设置为自己的父级")
}
var parent dictionaryPO
err := r.data.DB().WithContext(ctx).Where("id = ?", parentID).First(&parent).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if parent.ParentID != nil && *parent.ParentID != 0 {
return r.checkDictionaryCircularReference(ctx, currentID, *parent.ParentID)
}
return nil
}
func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
db := r.data.DB().WithContext(ctx)
var dictionary dictionaryPO
if err := db.First(&dictionary, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("请不要搞事")
}
return err
}
if err := db.Delete(&dictionary).Error; err != nil {
return err
}
return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
}
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO
db := r.data.DB().WithContext(ctx)
active := true
if status != nil {
active = *status
}
err := db.Where("(type = ? OR id = ?) AND status = ?", typ, id, active).First(&po).Error
if err != nil {
return nil, err
}
out := dictionaryFromPO(po)
if details {
var detailPOs []dictionaryDetailPO
if err = db.Where("sys_dictionary_id = ? AND status = ?", out.ID, true).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
out.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
out.Details = append(out.Details, detailFromPO(detail))
}
}
return out, err
}
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
var po dictionaryPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
value := dictionaryFromPO(po)
var detailPOs []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", id).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
value.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
value.Details = append(value.Details, detailFromPO(detail))
}
return value, nil
}
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
like := "%" + name + "%"
db = db.Where("name LIKE ? OR type LIKE ?", like, like)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryPO
// Both dictionary-list endpoints are intentionally unpaged. The page and
// size parameters remain in the repository seam for existing callers, but
// the administration contract ignores them.
if err := db.Find(&pos).Error; err != nil {
return nil, 0, err
}
parentIDs := make([]uint, 0, len(pos))
dictionaryIDs := make([]uint, 0, len(pos))
for _, po := range pos {
dictionaryIDs = append(dictionaryIDs, po.ID)
// The reference list-with-details endpoint only preloads details;
// its ordinary list endpoint preloads one level of dictionary children.
if !details {
parentIDs = append(parentIDs, po.ID)
}
}
childrenByParent := make(map[uint][]dictionaryPO)
if !details && len(parentIDs) > 0 {
var children []dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("parent_id IN ?", parentIDs).Find(&children).Error; err != nil {
return nil, 0, err
}
for _, child := range children {
if child.ParentID != nil {
childrenByParent[*child.ParentID] = append(childrenByParent[*child.ParentID], child)
}
}
}
detailsByDictionary := make(map[uint][]dictionaryDetailPO)
if details && len(dictionaryIDs) > 0 {
var values []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id IN ?", dictionaryIDs).Order("sort").Find(&values).Error; err != nil {
return nil, 0, err
}
for _, value := range values {
detailsByDictionary[value.DictionaryID] = append(detailsByDictionary[value.DictionaryID], value)
}
}
out := make([]*biz.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
if details {
v.Details = make([]*biz.DictionaryDetail, 0, len(detailsByDictionary[v.ID]))
for _, detail := range detailsByDictionary[v.ID] {
v.Details = append(v.Details, detailFromPO(detail))
}
} else {
v.Children = make([]*biz.Dictionary, 0, len(childrenByParent[po.ID]))
for _, child := range childrenByParent[po.ID] {
v.Children = append(v.Children, dictionaryFromPO(child))
}
}
out = append(out, v)
}
return out, total, nil
}
func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
po.Level = 0
po.Path = ""
if v.ParentID != nil {
var parent dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
return err
}
po.Level = parent.Level + 1
po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.Level = po.Level
v.Path = po.Path
return nil
}
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
var po dictionaryDetailPO
db := r.data.DB().WithContext(ctx)
if err := db.First(&po, v.ID).Error; err != nil {
return err
}
level, path := 0, ""
if v.ParentID != nil {
if *v.ParentID == v.ID {
return errors.New("不能将字典详情设置为自己或其子项的父级")
}
var parent dictionaryDetailPO
if err := db.First(&parent, *v.ParentID).Error; err != nil {
return err
}
if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
return errors.New("不能将字典详情设置为自己或其子项的父级")
}
level = parent.Level + 1
path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
}
if err := db.Model(&dictionaryDetailPO{}).Where("id = ?", v.ID).Updates(map[string]any{"label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sys_dictionary_id": v.DictionaryID, "parent_id": v.ParentID, "level": level, "path": path}).Error; err != nil {
return err
}
return r.updateDictionaryDetailChildren(db, v.ID, level, path)
}
// updateDictionaryDetailChildren recomputes descendants after a move. Updating
// level as well as path matters when a subtree is moved beneath a different depth.
func (r *dictionaryRepo) updateDictionaryDetailChildren(db *gorm.DB, parentID uint, parentLevel int, parentPath string) error {
var children []dictionaryDetailPO
if err := db.Where("parent_id = ?", parentID).Find(&children).Error; err != nil {
return err
}
for _, child := range children {
childPath := strings.Trim(strings.Join([]string{parentPath, fmt.Sprint(parentID)}, ","), ",")
if err := db.Model(&dictionaryDetailPO{}).Where("id = ?", child.ID).Updates(map[string]any{"level": parentLevel + 1, "path": childPath}).Error; err != nil {
return err
}
if err := r.updateDictionaryDetailChildren(db, child.ID, parentLevel+1, childPath); err != nil {
return err
}
}
return nil
}
func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
var count int64
if err := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("该字典详情下还有子项,无法删除")
}
return r.data.DB().WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
}
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
var po dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return detailFromPO(po), nil
}
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{})
if filter.DictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
}
if filter.Label != "" {
db = db.Where("label LIKE ?", "%"+filter.Label+"%")
}
if filter.Value != "" {
db = db.Where("value = ?", filter.Value)
}
if filter.Status != nil {
db = db.Where("status = ?", *filter.Status)
}
if filter.ParentID != nil {
db = db.Where("parent_id = ?", *filter.ParentID)
}
if filter.Level != nil {
db = db.Where("level = ?", *filter.Level)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryDetailPO
if err := pagination.ApplyRequired(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
for _, po := range pos {
out = append(out, detailFromPO(po))
}
return out, total, nil
}
func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
if dictionaryID == 0 {
// The tree-by-type endpoint resolves only by dictionary type. Unlike the
// public dictionary lookup, it does not require the dictionary itself to
// be enabled.
var dictionary dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", typ).First(&dictionary).Error; err != nil {
return nil, err
}
dictionaryID = dictionary.ID
}
var pos []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
roots := make([]*biz.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, item); err != nil {
return nil, err
}
roots = append(roots, item)
}
return roots, nil
}
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*biz.DictionaryDetail, error) {
db := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", dictionaryID)
if parentID == nil {
db = db.Where("parent_id IS NULL")
} else {
db = db.Where("parent_id = ?", *parentID)
}
var pos []dictionaryDetailPO
if err := db.Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
items := make([]*biz.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if includeChildren {
if err := r.loadDictionaryDetailChildren(ctx, item); err != nil {
return nil, err
}
}
items = append(items, item)
}
return items, nil
}
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error {
var pos []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("parent_id = ?", parent.ID).Order("sort").Find(&pos).Error; err != nil {
return err
}
parent.Children = make([]*biz.DictionaryDetail, 0, len(pos))
for _, po := range pos {
child := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, child); err != nil {
return err
}
parent.Children = append(parent.Children, child)
}
return nil
}