feat(api): 增强模板列表查询以支持字段去重和JSON字段处理

在templates.go中优化listTemplates函数,新增字段去重逻辑和JSON字段处理,确保返回的字段数量准确且符合预期。同时,调整SQL查询以包含fields_json字段,提升数据的准确性和可读性。
This commit is contained in:
zhouyonggao 2025-12-15 17:01:07 +08:00
parent c4f674ec5b
commit b16746c048
2 changed files with 99 additions and 36 deletions

View File

@ -91,25 +91,42 @@ func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) {
}
func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) {
uidStr := r.URL.Query().Get("userId")
sqlText := "SELECT id,name,datasource,main_table,file_format,visibility,owner_id,enabled,last_validated_at,created_at,updated_at, COALESCE(JSON_LENGTH(fields_json),0) AS field_count, (SELECT COUNT(1) FROM export_jobs ej WHERE ej.template_id = export_templates.id) AS exec_count FROM export_templates"
args := []interface{}{}
conds := []string{}
if uidStr != "" {
conds = append(conds, "owner_id IN (0, ?)")
args = append(args, uidStr)
}
conds = append(conds, "enabled = 1")
if len(conds) > 0 {
sqlText += " WHERE " + strings.Join(conds, " AND ")
}
sqlText += " ORDER BY datasource ASC, id DESC LIMIT 200"
uidStr := r.URL.Query().Get("userId")
sqlText := "SELECT id,name,datasource,main_table,file_format,visibility,owner_id,enabled,last_validated_at,created_at,updated_at,fields_json, (SELECT COUNT(1) FROM export_jobs ej WHERE ej.template_id = export_templates.id) AS exec_count FROM export_templates"
args := []interface{}{}
conds := []string{}
if uidStr != "" {
conds = append(conds, "owner_id IN (0, ?)")
args = append(args, uidStr)
}
conds = append(conds, "enabled = 1")
if len(conds) > 0 {
sqlText += " WHERE " + strings.Join(conds, " AND ")
}
sqlText += " ORDER BY datasource ASC, id DESC LIMIT 200"
rows, err := a.meta.Query(sqlText, args...)
if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
wl := Whitelist()
countFields := func(ds, main string, fs []string) int64 {
seen := map[string]struct{}{}
for _, tf := range fs {
if ds == "ymt" && strings.HasPrefix(tf, "order_info.") {
tf = strings.Replace(tf, "order_info.", "order.", 1)
}
if !wl[tf] {
continue
}
if _, ok := seen[tf]; ok {
continue
}
seen[tf] = struct{}{}
}
return int64(len(seen))
}
out := []map[string]interface{}{}
for rows.Next() {
var id uint64
@ -118,12 +135,16 @@ func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) {
var enabled int
var lastValidatedAt sql.NullTime
var createdAt, updatedAt time.Time
var fieldCount, execCount int64
err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldCount, &execCount)
var execCount int64
var fieldsRaw []byte
err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldsRaw, &execCount)
if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error())
return
}
var fs []string
_ = json.Unmarshal(fieldsRaw, &fs)
fieldCount := countFields(datasource, mainTable, fs)
m := map[string]interface{}{"id": id, "name": name, "datasource": datasource, "main_table": mainTable, "file_format": fileFormat, "visibility": visibility, "owner_id": ownerID, "enabled": enabled == 1, "last_validated_at": lastValidatedAt.Time, "created_at": createdAt, "updated_at": updatedAt, "field_count": fieldCount, "exec_count": execCount}
out = append(out, m)
}
@ -249,25 +270,25 @@ func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id
}
func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) {
var cnt int64
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id=?", id)
_ = row.Scan(&cnt)
if cnt > 0 {
soft := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("soft")))
if soft == "1" || soft == "true" || soft == "yes" {
a.meta.Exec("UPDATE export_templates SET enabled=?, updated_at=? WHERE id= ?", 0, time.Now(), id)
ok(w, r, nil)
return
}
fail(w, r, http.StatusBadRequest, "template in use")
return
}
_, err := a.meta.Exec("DELETE FROM export_templates WHERE id= ?", id)
if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error())
return
}
ok(w, r, nil)
var cnt int64
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id=?", id)
_ = row.Scan(&cnt)
if cnt > 0 {
soft := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("soft")))
if soft == "1" || soft == "true" || soft == "yes" {
a.meta.Exec("UPDATE export_templates SET enabled=?, updated_at=? WHERE id= ?", 0, time.Now(), id)
ok(w, r, nil)
return
}
fail(w, r, http.StatusBadRequest, "template in use")
return
}
_, err := a.meta.Exec("DELETE FROM export_templates WHERE id= ?", id)
if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error())
return
}
ok(w, r, nil)
}
func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, id string) {

View File

@ -712,52 +712,94 @@ const app = createApp({
const isGroupPath = (ds, path) => Array.isArray(path) && path.length >= 1 && tableKeys(ds).includes(path[path.length - 1]);
const convertFieldsToPaths = (fields, ds, mainTable) => {
const FM = getFieldsMap(ds);
const hasField = (tbl, fld) => {
const arr = FM[tbl] || [];
return arr.some(i => i.value === fld);
};
const actualMainTable = (ds === 'ymt' && mainTable === 'order_info') ? 'order' : mainTable;
const toPath = (tf) => {
const parts = String(tf || '').split('.');
if (parts.length !== 2) return null;
let table = parts[0];
const field = parts[1];
let targetTable = table;
if (ds === 'ymt' && table === 'order_info') {
table = 'order';
targetTable = table;
}
if (table === actualMainTable) {
targetTable = actualMainTable;
return [actualMainTable, field];
} else if (table === 'order_detail') {
targetTable = 'order_detail';
return [actualMainTable, 'order_detail', field];
} else if (table === 'plan') {
targetTable = 'plan';
return [actualMainTable, 'plan', field];
} else if (table === 'key_batch') {
targetTable = 'key_batch';
return [actualMainTable, 'plan', 'key_batch', field];
} else if (table === 'code_batch') {
targetTable = 'code_batch';
return [actualMainTable, 'plan', 'key_batch', 'code_batch', field];
} else if (table === 'order_voucher') {
targetTable = 'order_voucher';
return [actualMainTable, 'order_voucher', field];
} else if (table === 'voucher') {
targetTable = 'voucher';
return [actualMainTable, 'order_voucher', 'voucher', field];
} else if (table === 'voucher_batch') {
targetTable = 'voucher_batch';
return [actualMainTable, 'order_voucher', 'voucher', 'voucher_batch', field];
} else if (table === 'merchant_key_send') {
targetTable = 'merchant_key_send';
return [actualMainTable, 'merchant_key_send', field];
} else if (table === 'order_cash') {
targetTable = 'order_cash';
return [actualMainTable, 'order_cash', field];
} else if (table === 'order_digit') {
targetTable = 'order_digit';
return [actualMainTable, 'order_digit', field];
} else if (table === 'goods_voucher_batch') {
targetTable = 'goods_voucher_batch';
return [actualMainTable, 'goods_voucher_batch', field];
} else if (table === 'goods_voucher_subject_config') {
targetTable = 'goods_voucher_subject_config';
return [actualMainTable, 'goods_voucher_subject_config', field];
} else if (table === 'merchant') {
targetTable = 'merchant';
return [actualMainTable, 'merchant', field];
} else if (table === 'activity') {
targetTable = 'activity';
return [actualMainTable, 'activity', field];
}
return null;
};
return fields.map(toPath).filter(p => Array.isArray(p) && p.length >= 2);
return fields
.map(toPath)
.filter(p => Array.isArray(p) && p.length >= 2)
.filter(path => {
const tbl = path[path.length - 2];
const fld = path[path.length - 1];
return hasField(tbl, fld);
});
};
const dedupPaths = (paths = []) => {
const seen = new Set();
const out = [];
for (const p of paths) {
if (!Array.isArray(p)) continue;
const key = p.join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push(p);
}
return out;
};
const convertPathsToFields = (paths, ds, mainTable) => {
@ -877,7 +919,7 @@ const app = createApp({
await Vue.nextTick();
const mainTable = state.edit.main_table || 'order';
const paths = convertFieldsToPaths(fields, state.edit.datasource, mainTable);
const paths = dedupPaths(convertFieldsToPaths(fields, state.edit.datasource, mainTable));
state.edit.fieldsSel = paths;
// 设置树形选择器的选中状态(延迟确保树已渲染)
await Vue.nextTick();