package data import ( "context" "errors" "fmt" "gorm.io/gorm" "kra/internal/biz" "regexp" "sort" "strconv" "strings" "time" ) type exportTemplatePO struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` DBName string Name string DBTableName string `gorm:"column:table_name"` TemplateID string `gorm:"uniqueIndex"` TemplateInfo string `gorm:"type:text"` SQL string `gorm:"type:text"` ImportSQL string `gorm:"type:text"` Limit *int Order string } func (exportTemplatePO) TableName() string { return "sys_export_templates" } type exportConditionPO struct { ID uint `gorm:"primaryKey"` TemplateID string `gorm:"index"` From string Column string Operator string } func (exportConditionPO) TableName() string { return "sys_export_template_condition" } type exportJoinPO struct { ID uint `gorm:"primaryKey"` TemplateID string `gorm:"index"` Join string `gorm:"column:joins"` Table string On string `gorm:"column:on"` } func (exportJoinPO) TableName() string { return "sys_export_template_join" } type exportRepo struct{ data *Data } func NewExportRepo(data *Data) biz.ExportRepo { return &exportRepo{data: data} } func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate { v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order} for _, x := range conditions { v.Conditions = append(v.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator}) } for _, x := range joins { v.Joins = append(v.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On}) } return v } func exportToPO(v *biz.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} } func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error { if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil { return err } if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportJoinPO{}).Error; err != nil { return err } conditions := make([]exportConditionPO, 0, len(v.Conditions)) for _, x := range v.Conditions { conditions = append(conditions, exportConditionPO{TemplateID: v.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator}) } if len(conditions) > 0 { if err := tx.Create(&conditions).Error; err != nil { return err } } joins := make([]exportJoinPO, 0, len(v.Joins)) for _, x := range v.Joins { joins = append(joins, exportJoinPO{TemplateID: v.TemplateID, Join: x.Join, Table: x.Table, On: x.On}) } if len(joins) > 0 { return tx.Create(&joins).Error } return nil } func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { po := exportToPO(v) if err := tx.Create(&po).Error; err != nil { return err } v.ID = po.ID return r.saveRelations(tx, v) }) } func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { po := exportToPO(v) if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil { return err } return r.saveRelations(tx, v) }) } func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var tids []string if err := tx.Model(&exportTemplatePO{}).Where("id IN ?", ids).Pluck("template_id", &tids).Error; err != nil { return err } if len(tids) > 0 { if err := tx.Where("template_id IN ?", tids).Delete(&exportConditionPO{}).Error; err != nil { return err } if err := tx.Where("template_id IN ?", tids).Delete(&exportJoinPO{}).Error; err != nil { return err } } return tx.Delete(&exportTemplatePO{}, ids).Error }) } func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) { var po exportTemplatePO db := r.data.gormDB.WithContext(ctx) var err error if id != 0 { err = db.First(&po, id).Error } else { err = db.Where("template_id = ?", tid).First(&po).Error } if err != nil { return nil, err } var conditions []exportConditionPO var joins []exportJoinPO if err = db.Where("template_id = ?", po.TemplateID).Find(&conditions).Error; err != nil { return nil, err } if err = db.Where("template_id = ?", po.TemplateID).Find(&joins).Error; err != nil { return nil, err } return exportFromPO(po, conditions, joins), nil } func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) { if page < 1 { page = 1 } if size < 1 { size = 10 } db := r.data.gormDB.WithContext(ctx).Model(&exportTemplatePO{}) if q != nil { if q.Name != "" { db = db.Where("name LIKE ?", "%"+q.Name+"%") } if q.TableName != "" { db = db.Where("table_name = ?", q.TableName) } if q.TemplateID != "" { db = db.Where("template_id = ?", q.TemplateID) } } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var pos []exportTemplatePO if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil { return nil, 0, err } out := make([]*biz.ExportTemplate, 0, len(pos)) for _, po := range pos { v, err := r.FindExportTemplate(ctx, po.ID, "") if err != nil { return nil, 0, err } out = append(out, v) } return out, total, nil } var safeIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.]*$`) var allowedOperator = map[string]bool{"=": true, "!=": true, ">": true, ">=": true, "<": true, "<=": true, "LIKE": true, "IN": true, "NOT IN": true, "BETWEEN": true} func validateSelectSQL(sql string) error { normalized := strings.ToLower(strings.TrimSpace(sql)) if !strings.HasPrefix(normalized, "select ") || strings.Contains(normalized, ";") { return errors.New("仅允许单条 SELECT 查询") } return nil } func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) { db := r.data.gormDB.WithContext(ctx) var rows []map[string]any if t.SQL != "" { if err := validateSelectSQL(t.SQL); err != nil { return nil, "", err } 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 } if !safeIdentifier.MatchString(t.TableName) { return nil, "", errors.New("表名不合法") } columns := make([]string, 0) for key := range parseTemplateColumns(t.TemplateInfo) { if !safeIdentifier.MatchString(key) { return nil, "", errors.New("列名不合法") } columns = append(columns, key) } sort.Strings(columns) if len(columns) == 0 { return nil, "", errors.New("模板列为空") } query := db.Table(t.TableName).Select(strings.Join(columns, ",")) for _, j := range t.Joins { joinType := strings.ToUpper(strings.TrimSpace(j.Join)) if joinType != "LEFT JOIN" && joinType != "RIGHT JOIN" && joinType != "INNER JOIN" && joinType != "JOIN" { return nil, "", errors.New("关联类型不合法") } if !safeIdentifier.MatchString(j.Table) { return nil, "", errors.New("关联表不合法") } query = query.Joins(joinType + " " + j.Table + " ON " + j.On) } for _, condition := range t.Conditions { operator := strings.ToUpper(strings.TrimSpace(condition.Operator)) if !allowedOperator[operator] || !safeIdentifier.MatchString(condition.Column) { return nil, "", errors.New("查询条件不合法") } value := params[condition.From] if value == "" { continue } switch operator { case "LIKE": query = query.Where(condition.Column+" LIKE ?", "%"+value+"%") case "IN", "NOT IN": query = query.Where(condition.Column+" "+operator+" ?", strings.Split(value, ",")) case "BETWEEN": start, end := params["start"+condition.From], params["end"+condition.From] if start != "" && end != "" { query = query.Where(condition.Column+" BETWEEN ? AND ?", start, end) } default: query = query.Where(condition.Column+" "+operator+" ?", value) } } limit := 0 if raw := params["limit"]; raw != "" { limit, _ = strconv.Atoi(raw) } else if t.Limit != nil { limit = *t.Limit } if limit > 0 { query = query.Limit(limit) } if t.Order != "" { parts := strings.Fields(t.Order) if len(parts) < 1 || len(parts) > 2 || !safeIdentifier.MatchString(parts[0]) || (len(parts) == 2 && strings.ToUpper(parts[1]) != "ASC" && strings.ToUpper(parts[1]) != "DESC") { return nil, "", errors.New("排序不合法") } query = query.Order(t.Order) } 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 parseTemplateColumns(raw string) map[string]string { out := map[string]string{} parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",") for _, part := range parts { pair := strings.SplitN(part, ":", 2) if len(pair) == 2 { out[strings.Trim(strings.TrimSpace(pair[0]), `"`)] = strings.Trim(strings.TrimSpace(pair[1]), `"`) } } return out } func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error { sql := strings.TrimSpace(t.ImportSQL) lower := strings.ToLower(sql) if sql == "" || strings.Contains(sql, ";") || (!strings.HasPrefix(lower, "insert ") && !strings.HasPrefix(lower, "update ")) { return errors.New("导入 SQL 仅允许单条 INSERT 或 UPDATE") } return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for index, row := range rows { if err := tx.Exec(sql, row).Error; err != nil { return fmt.Errorf("第%d行导入失败: %w", index+2, err) } } return nil }) }