优化结构
This commit is contained in:
+85
-183
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type exportTemplatePO struct {
|
||||
@@ -78,9 +77,9 @@ func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []e
|
||||
return v
|
||||
}
|
||||
func exportToPO(v *system.ExportTemplate) exportTemplatePO {
|
||||
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
|
||||
return exportTemplatePO{DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
|
||||
}
|
||||
func (r *exportRepo) saveRelations(tx *gorm.DB, v *system.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
|
||||
func (r *exportRepo) saveRelations(tx *gorm.DB, v *system.ExportTemplate, replace bool) error {
|
||||
if replace {
|
||||
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
|
||||
return err
|
||||
@@ -91,50 +90,19 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *system.ExportTemplate, resetI
|
||||
}
|
||||
conditions := make([]exportConditionPO, 0, len(v.Conditions))
|
||||
for _, x := range v.Conditions {
|
||||
id := x.ID
|
||||
if resetIDs {
|
||||
id = 0
|
||||
}
|
||||
templateID := x.TemplateID
|
||||
// GORM's has-many association on Create always writes the parent's
|
||||
// TemplateID into the child foreign key (references:TemplateID).
|
||||
// Preserve that observable behavior for creates; updates intentionally
|
||||
// keep the request's value after the compatible update deletes and
|
||||
// recreates relations.
|
||||
if forceTemplateID || templateID == "" {
|
||||
templateID = v.TemplateID
|
||||
}
|
||||
conditions = append(conditions, exportConditionPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
conditions = append(conditions, exportConditionPO{TemplateID: v.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
|
||||
}
|
||||
if len(conditions) > 0 {
|
||||
create := tx
|
||||
if !resetIDs {
|
||||
// GORM saves Create associations with ON CONFLICT DO NOTHING.
|
||||
// Preserve that behavior for copied templates carrying relation IDs.
|
||||
create = create.Clauses(clause.OnConflict{DoNothing: true})
|
||||
}
|
||||
if err := create.Create(&conditions).Error; err != nil {
|
||||
if err := tx.Create(&conditions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
joins := make([]exportJoinPO, 0, len(v.Joins))
|
||||
for _, x := range v.Joins {
|
||||
id := x.ID
|
||||
if resetIDs {
|
||||
id = 0
|
||||
}
|
||||
templateID := x.TemplateID
|
||||
if forceTemplateID || templateID == "" {
|
||||
templateID = v.TemplateID
|
||||
}
|
||||
joins = append(joins, exportJoinPO{ID: id, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: templateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
joins = append(joins, exportJoinPO{TemplateID: v.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
|
||||
}
|
||||
if len(joins) > 0 {
|
||||
create := tx
|
||||
if !resetIDs {
|
||||
create = create.Clauses(clause.OnConflict{DoNothing: true})
|
||||
}
|
||||
return create.Create(&joins).Error
|
||||
return tx.Create(&joins).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -145,7 +113,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *system.ExportT
|
||||
return err
|
||||
}
|
||||
v.ID = po.ID
|
||||
return r.saveRelations(tx, v, false, false, true)
|
||||
return r.saveRelations(tx, v, false)
|
||||
})
|
||||
}
|
||||
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *system.ExportTemplate) error {
|
||||
@@ -154,7 +122,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *system.ExportT
|
||||
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.saveRelations(tx, v, true, true, false)
|
||||
return r.saveRelations(tx, v, true)
|
||||
})
|
||||
}
|
||||
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
|
||||
@@ -219,49 +187,70 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
|
||||
}
|
||||
|
||||
func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
|
||||
selected, err := r.data.Database(t.DBName)
|
||||
query, err := r.buildExportQuery(ctx, t, params)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
db := selected.WithContext(ctx)
|
||||
var rows []map[string]any
|
||||
if t.SQL != "" {
|
||||
args := map[string]any{}
|
||||
for k, v := range params {
|
||||
args[k] = v
|
||||
}
|
||||
err := db.Raw(t.SQL, args).Scan(&rows).Error
|
||||
return rows, t.SQL, err
|
||||
}
|
||||
preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
|
||||
err = query.Find(&rows).Error
|
||||
return rows, preview, err
|
||||
}
|
||||
|
||||
func exportColumns(raw string) ([]string, error) {
|
||||
columns := make([]string, 0)
|
||||
if err := json.Unmarshal([]byte(t.TemplateInfo), &map[string]string{}); err != nil {
|
||||
return nil, "", err
|
||||
decoder := json.NewDecoder(strings.NewReader(raw))
|
||||
if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
|
||||
return nil, errors.New("导出模板列定义必须是 JSON 对象")
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
|
||||
_, _ = decoder.Token()
|
||||
for decoder.More() {
|
||||
key, _ := decoder.Token()
|
||||
columns = append(columns, key.(string))
|
||||
key, err := decoder.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
column, ok := key.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("导出模板列名无效")
|
||||
}
|
||||
columns = append(columns, column)
|
||||
var ignored any
|
||||
_ = decoder.Decode(&ignored)
|
||||
if err = decoder.Decode(&ignored); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return nil, "", errors.New("模板列为空")
|
||||
return nil, errors.New("模板列为空")
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func (r *exportRepo) buildExportQuery(ctx context.Context, t *system.ExportTemplate, params map[string]string) (*gorm.DB, error) {
|
||||
if err := system.ValidateExportTemplate(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selected, err := r.data.Database(t.DBName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db := selected.WithContext(ctx)
|
||||
columns, err := exportColumns(t.TemplateInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := db.Table(t.TableName).Select(strings.Join(columns, ","))
|
||||
for _, j := range t.Joins {
|
||||
query = query.Joins(j.Join + " " + j.Table + " ON " + j.On)
|
||||
query = query.Joins(strings.ToUpper(strings.Join(strings.Fields(j.Join), " ")) + " " + j.Table + " ON " + j.On)
|
||||
}
|
||||
if params["filterDeleted"] == "true" {
|
||||
query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
|
||||
query = query.Where(t.TableName + ".deleted_at IS NULL")
|
||||
for _, join := range t.Joins {
|
||||
if db.Migrator().HasColumn(join.Table, "deleted_at") {
|
||||
query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
||||
query = query.Where(join.Table + ".deleted_at IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, condition := range t.Conditions {
|
||||
operator := condition.Operator
|
||||
operator := strings.ToUpper(strings.Join(strings.Fields(condition.Operator), " "))
|
||||
value := params[condition.From]
|
||||
switch operator {
|
||||
case "LIKE":
|
||||
@@ -270,7 +259,11 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate,
|
||||
}
|
||||
case "IN", "NOT IN":
|
||||
if value != "" {
|
||||
query = query.Where(condition.Column+" "+operator+" (?)", value)
|
||||
values := strings.Split(value, ",")
|
||||
for index := range values {
|
||||
values[index] = strings.TrimSpace(values[index])
|
||||
}
|
||||
query = query.Where(condition.Column+" "+operator+" ?", values)
|
||||
}
|
||||
case "BETWEEN":
|
||||
start, end := params["start"+condition.From], params["end"+condition.From]
|
||||
@@ -285,7 +278,10 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate,
|
||||
}
|
||||
limit := 0
|
||||
if raw := params["limit"]; raw != "" {
|
||||
limit, _ = strconv.Atoi(raw)
|
||||
limit, err = strconv.Atoi(raw)
|
||||
if err != nil || limit < 0 || limit > 100000 {
|
||||
return nil, errors.New("导出行数限制不合法")
|
||||
}
|
||||
} else if t.Limit != nil {
|
||||
limit = *t.Limit
|
||||
}
|
||||
@@ -293,139 +289,54 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate,
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if raw := params["offset"]; raw != "" {
|
||||
if offset, parseErr := strconv.Atoi(raw); parseErr == nil {
|
||||
query = query.Offset(offset)
|
||||
offset, parseErr := strconv.Atoi(raw)
|
||||
if parseErr != nil || offset < 0 {
|
||||
return nil, errors.New("导出偏移量不合法")
|
||||
}
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
order := params["order"]
|
||||
if order == "" {
|
||||
order = t.Order
|
||||
}
|
||||
if order != "" {
|
||||
parts := strings.Split(order, " ")
|
||||
parts := strings.Fields(order)
|
||||
if len(parts) > 2 {
|
||||
return nil, errors.New("导出排序不合法")
|
||||
}
|
||||
fields := map[string]bool{}
|
||||
columnTypes, columnErr := db.Migrator().ColumnTypes(t.TableName)
|
||||
if columnErr != nil {
|
||||
return nil, "", columnErr
|
||||
return nil, columnErr
|
||||
}
|
||||
for _, column := range columnTypes {
|
||||
fields[column.Name()] = true
|
||||
}
|
||||
if !fields[parts[0]] {
|
||||
return nil, "", fmt.Errorf("order by %s is not in the fields", order)
|
||||
columnName := parts[0]
|
||||
if dot := strings.LastIndex(columnName, "."); dot >= 0 {
|
||||
columnName = columnName[dot+1:]
|
||||
}
|
||||
if !fields[columnName] {
|
||||
return nil, fmt.Errorf("order by %s is not in the fields", order)
|
||||
}
|
||||
orderSQL := parts[0]
|
||||
if len(parts) > 1 {
|
||||
if parts[1] != "asc" && parts[1] != "desc" {
|
||||
return nil, "", fmt.Errorf("order by %s is not secure", order)
|
||||
if !strings.EqualFold(parts[1], "asc") && !strings.EqualFold(parts[1], "desc") {
|
||||
return nil, fmt.Errorf("order by %s is not secure", order)
|
||||
}
|
||||
orderSQL += " " + parts[1]
|
||||
orderSQL += " " + strings.ToUpper(parts[1])
|
||||
}
|
||||
query = query.Order(orderSQL)
|
||||
}
|
||||
preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
|
||||
err = query.Find(&rows).Error
|
||||
return rows, preview, err
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (r *exportRepo) PreviewExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) (string, error) {
|
||||
columns := make([]string, 0)
|
||||
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
|
||||
if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
|
||||
query, err := r.buildExportQuery(ctx, t, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for decoder.More() {
|
||||
key, err := decoder.Token()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
columns = append(columns, key.(string))
|
||||
var ignored any
|
||||
if err = decoder.Decode(&ignored); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.WriteString("SELECT ")
|
||||
builder.WriteString(strings.Join(columns, ", "))
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(t.TableName)
|
||||
for _, join := range t.Joins {
|
||||
builder.WriteString(" " + join.Join + " " + join.Table + " ON " + join.On)
|
||||
}
|
||||
wheres := make([]string, 0)
|
||||
if params["filterDeleted"] == "true" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
|
||||
selected, err := r.data.Database(t.DBName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
db := selected.WithContext(ctx)
|
||||
for _, join := range t.Joins {
|
||||
if db.Migrator().HasColumn(join.Table, "deleted_at") {
|
||||
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, condition := range t.Conditions {
|
||||
op, column, value := strings.ToUpper(strings.TrimSpace(condition.Operator)), strings.TrimSpace(condition.Column), params[condition.From]
|
||||
switch op {
|
||||
case "BETWEEN":
|
||||
start, end := params["start"+condition.From], params["end"+condition.From]
|
||||
if start != "" && end != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", column, start, end))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", column, condition.From, condition.From))
|
||||
}
|
||||
case "IN", "NOT IN":
|
||||
if value != "" {
|
||||
parts := strings.Split(value, ",")
|
||||
for index := range parts {
|
||||
parts[index] = strings.TrimSpace(parts[index])
|
||||
}
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", column, op, strings.Join(parts, "','")))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", column, op, condition.From))
|
||||
}
|
||||
case "LIKE":
|
||||
if value != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", column, value))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", column, condition.From))
|
||||
}
|
||||
default:
|
||||
if value != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s '%s'", column, op, value))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s {%s}", column, op, condition.From))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(wheres) > 0 {
|
||||
builder.WriteString(" WHERE " + strings.Join(wheres, " AND "))
|
||||
}
|
||||
order := params["order"]
|
||||
if order == "" {
|
||||
order = t.Order
|
||||
}
|
||||
if order != "" {
|
||||
builder.WriteString(" ORDER BY " + order)
|
||||
}
|
||||
limitRaw, offsetRaw := params["limit"], params["offset"]
|
||||
if limitRaw == "" && t.Limit != nil && *t.Limit != 0 {
|
||||
limitRaw = strconv.Itoa(*t.Limit)
|
||||
}
|
||||
limit, _ := strconv.Atoi(limitRaw)
|
||||
offset, _ := strconv.Atoi(offsetRaw)
|
||||
if limit > 0 {
|
||||
builder.WriteString(" LIMIT " + strconv.Itoa(limit))
|
||||
if offset > 0 {
|
||||
builder.WriteString(" OFFSET " + strconv.Itoa(offset))
|
||||
}
|
||||
} else if offset > 0 {
|
||||
builder.WriteString(" OFFSET " + strconv.Itoa(offset))
|
||||
}
|
||||
return builder.String(), nil
|
||||
return query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) }), nil
|
||||
}
|
||||
func parseTemplateColumns(raw string) map[string]string {
|
||||
out := map[string]string{}
|
||||
@@ -439,23 +350,14 @@ func parseTemplateColumns(raw string) map[string]string {
|
||||
return out
|
||||
}
|
||||
func (r *exportRepo) ImportExportRows(ctx context.Context, t *system.ExportTemplate, rows []map[string]any) error {
|
||||
// ImportSQL is checked verbatim. In particular, a whitespace-only value
|
||||
// is still treated as custom SQL and is allowed to return the driver's
|
||||
// native error instead of silently falling back to GORM insertion.
|
||||
sql := t.ImportSQL
|
||||
if err := system.ValidateExportTemplate(t); err != nil {
|
||||
return err
|
||||
}
|
||||
selected, err := r.data.Database(t.DBName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return selected.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if sql != "" {
|
||||
for _, row := range rows {
|
||||
if err := tx.Exec(sql, row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
needCreated := tx.Migrator().HasColumn(t.TableName, "created_at")
|
||||
needUpdated := tx.Migrator().HasColumn(t.TableName, "updated_at")
|
||||
for _, row := range rows {
|
||||
|
||||
Reference in New Issue
Block a user