fix: align admin behavior and Kratos integrations
This commit is contained in:
+212
-68
@@ -2,12 +2,11 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -151,14 +150,11 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
|
||||
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.StartCreatedAt != nil && q.EndCreatedAt != nil {
|
||||
db = db.Where("created_at BETWEEN ? AND ?", q.StartCreatedAt, q.EndCreatedAt)
|
||||
}
|
||||
if q.Name != "" {
|
||||
db = db.Where("name LIKE ?", "%"+q.Name+"%")
|
||||
}
|
||||
@@ -174,37 +170,46 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []exportTemplatePO
|
||||
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
|
||||
if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
templateIDs := make([]string, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
templateIDs = append(templateIDs, po.TemplateID)
|
||||
}
|
||||
var conditions []exportConditionPO
|
||||
var joins []exportJoinPO
|
||||
if len(templateIDs) > 0 {
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
|
||||
for _, condition := range conditions {
|
||||
conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
|
||||
}
|
||||
joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
|
||||
for _, join := range joins {
|
||||
joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
|
||||
}
|
||||
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)
|
||||
out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID]))
|
||||
}
|
||||
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)
|
||||
selected, err := r.data.database(t.DBName)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
db := selected.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
|
||||
@@ -212,52 +217,54 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
|
||||
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)
|
||||
if err := json.Unmarshal([]byte(t.TemplateInfo), &map[string]string{}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
|
||||
_, _ = decoder.Token()
|
||||
for decoder.More() {
|
||||
key, _ := decoder.Token()
|
||||
columns = append(columns, key.(string))
|
||||
var ignored any
|
||||
_ = decoder.Decode(&ignored)
|
||||
}
|
||||
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("关联类型不合法")
|
||||
query = query.Joins(j.Join + " " + j.Table + " ON " + j.On)
|
||||
}
|
||||
if params["filterDeleted"] == "true" {
|
||||
query = query.Where(fmt.Sprintf("%s.deleted_at IS NULL", t.TableName))
|
||||
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))
|
||||
}
|
||||
}
|
||||
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("查询条件不合法")
|
||||
}
|
||||
operator := condition.Operator
|
||||
value := params[condition.From]
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
switch operator {
|
||||
case "LIKE":
|
||||
query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
|
||||
if value != "" {
|
||||
query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
|
||||
}
|
||||
case "IN", "NOT IN":
|
||||
query = query.Where(condition.Column+" "+operator+" ?", strings.Split(value, ","))
|
||||
if value != "" {
|
||||
query = query.Where(condition.Column+" "+operator+" (?)", 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)
|
||||
if value != "" {
|
||||
query = query.Where(condition.Column+" "+operator+" ?", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
limit := 0
|
||||
@@ -269,17 +276,141 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
|
||||
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("排序不合法")
|
||||
if raw := params["offset"]; raw != "" {
|
||||
if offset, parseErr := strconv.Atoi(raw); parseErr == nil {
|
||||
query = query.Offset(offset)
|
||||
}
|
||||
query = query.Order(t.Order)
|
||||
}
|
||||
order := params["order"]
|
||||
if order == "" {
|
||||
order = t.Order
|
||||
}
|
||||
if order != "" {
|
||||
parts := strings.Split(order, " ")
|
||||
fields := map[string]bool{}
|
||||
columnTypes, columnErr := db.Migrator().ColumnTypes(t.TableName)
|
||||
if columnErr != nil {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
orderSQL += " " + 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
|
||||
err = query.Find(&rows).Error
|
||||
return rows, preview, err
|
||||
}
|
||||
|
||||
func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.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('{') {
|
||||
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
|
||||
}
|
||||
func parseTemplateColumns(raw string) map[string]string {
|
||||
out := map[string]string{}
|
||||
parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",")
|
||||
@@ -293,16 +424,29 @@ func parseTemplateColumns(raw string) map[string]string {
|
||||
}
|
||||
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")
|
||||
selected, err := r.data.database(t.DBName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 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 {
|
||||
if row["created_at"] == nil && needCreated {
|
||||
row["created_at"] = time.Now()
|
||||
}
|
||||
if row["updated_at"] == nil && needUpdated {
|
||||
row["updated_at"] = time.Now()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return tx.Table(t.TableName).CreateInBatches(&rows, 1000).Error
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user