feat(api): 添加元数据字段接口并优化字段选择逻辑

添加/metadata/fields接口获取字段元数据
重构前端字段选择器使用动态字段选项
优化SQL执行错误日志记录和CORS配置
新增字段推荐功能并根据订单类型动态加载
This commit is contained in:
zhouyonggao 2025-11-27 16:38:19 +08:00
parent a193605e5c
commit 5980ee19a8
12 changed files with 1082 additions and 583 deletions

Binary file not shown.

View File

@ -25,6 +25,13 @@ func (w *statusWriter) Write(b []byte)(int, error){
func withAccess(h http.Handler) http.Handler { func withAccess(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
start := time.Now() start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: 200} sw := &statusWriter{ResponseWriter: w, status: 200}
h.ServeHTTP(sw, r) h.ServeHTTP(sw, r)
@ -43,4 +50,3 @@ func withAccess(h http.Handler) http.Handler {
}) })
}) })
} }

View File

@ -106,38 +106,31 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
} }
r = WithSQL(r, q) r = WithSQL(r, q)
dataDB := a.selectDataDB(ds) dataDB := a.selectDataDB(ds)
expRows, score, err := exporter.RunExplain(dataDB, q, args) score, sugg, err := exporter.EvaluateExplain(dataDB, q, args)
if err != nil { if err != nil {
fail(w, r, http.StatusBadRequest, err.Error()) fail(w, r, http.StatusBadRequest, err.Error())
return return
} }
sugg = append(sugg, exporter.IndexSuggestions(req)...)
const passThreshold = 60 const passThreshold = 60
if score < passThreshold { if score < passThreshold {
fail(w, r, http.StatusBadRequest, fmt.Sprintf("EXPLAIN 未通过:评分=%d请优化索引或缩小查询范围", score)) fail(w, r, http.StatusBadRequest, fmt.Sprintf("EXPLAIN 未通过:评分=%d请优化索引或缩小查询范围", score))
return return
} }
var estimate int64 var estimate int64
func() { func() {
idx := strings.Index(q, " FROM ") idx := strings.Index(q, " FROM ")
if idx > 0 { if idx > 0 {
cq := "SELECT COUNT(1)" + q[idx:] cq := "SELECT COUNT(1)" + q[idx:]
row := dataDB.QueryRow(cq, args...) row := dataDB.QueryRow(cq, args...)
var cnt int64 var cnt int64
if err := row.Scan(&cnt); err == nil { if err := row.Scan(&cnt); err == nil {
estimate = cnt estimate = cnt
return return
} }
} }
for _, r := range expRows { estimate = 0
if r.Table.Valid && r.Table.String == "order" && r.Rows.Valid { }()
estimate = r.Rows.Int64
break
}
if r.Rows.Valid {
estimate += r.Rows.Int64
}
}
}()
labels := FieldLabels() labels := FieldLabels()
hdrs := make([]string, len(fs)) hdrs := make([]string, len(fs))
for i, tf := range fs { for i, tf := range fs {
@ -155,7 +148,7 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
} }
} }
ejSQL := "INSERT INTO export_jobs (template_id, status, requested_by, owner_id, permission_scope_json, filters_json, options_json, explain_json, explain_score, row_estimate, file_format, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" ejSQL := "INSERT INTO export_jobs (template_id, status, requested_by, owner_id, permission_scope_json, filters_json, options_json, explain_json, explain_score, row_estimate, file_format, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
ejArgs := []interface{}{p.TemplateID, "queued", p.RequestedBy, owner, toJSON(p.Permission), toJSON(p.Filters), toJSON(p.Options), toJSON(expRows), score, estimate, p.FileFormat, time.Now(), time.Now()} ejArgs := []interface{}{p.TemplateID, "queued", p.RequestedBy, owner, toJSON(p.Permission), toJSON(p.Filters), toJSON(p.Options), toJSON(map[string]interface{}{"sql": q, "suggestions": sugg}), score, estimate, p.FileFormat, time.Now(), time.Now()}
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), ejSQL, ejArgs) log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), ejSQL, ejArgs)
res, err := a.meta.Exec(ejSQL, ejArgs...) res, err := a.meta.Exec(ejSQL, ejArgs...)
if err != nil { if err != nil {
@ -801,11 +794,11 @@ func (a *ExportsAPI) getSQL(w http.ResponseWriter, r *http.Request, id string) {
json.Unmarshal(filters, &fl) json.Unmarshal(filters, &fl)
wl := Whitelist() wl := Whitelist()
req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: fl} req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: fl}
q, args, err := exporter.BuildSQL(req, wl) q, args, err := exporter.BuildSQL(req, wl)
if err != nil { if err != nil {
fail(w, r, http.StatusBadRequest, err.Error()) failCat(w, r, http.StatusBadRequest, err.Error(), "sql_build_error")
return return
} }
formatArg := func(a interface{}) string { formatArg := func(a interface{}) string {
switch t := a.(type) { switch t := a.(type) {
case nil: case nil:
@ -982,10 +975,10 @@ func (a *ExportsAPI) list(w http.ResponseWriter, r *http.Request) {
rows, err = a.meta.Query("SELECT id, template_id, status, requested_by, row_estimate, total_rows, file_format, created_at, updated_at, explain_score, explain_json FROM export_jobs ORDER BY id DESC LIMIT ? OFFSET ?", size, offset) rows, err = a.meta.Query("SELECT id, template_id, status, requested_by, row_estimate, total_rows, file_format, created_at, updated_at, explain_score, explain_json FROM export_jobs ORDER BY id DESC LIMIT ? OFFSET ?", size, offset)
} }
} }
if err != nil { if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error()) failCat(w, r, http.StatusInternalServerError, err.Error(), "explain_error")
return return
} }
defer rows.Close() defer rows.Close()
items := []map[string]interface{}{} items := []map[string]interface{}{}
for rows.Next() { for rows.Next() {

View File

@ -0,0 +1,127 @@
package api
import (
"database/sql"
"net/http"
"sort"
)
func MetadataHandler(meta, marketing *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ds := r.URL.Query().Get("datasource")
ot := r.URL.Query().Get("order_type")
db := marketing
if ds == "ymt" { db = meta }
tables := []string{}
if ds == "ymt" {
tables = []string{"order_info","order_cash","order_voucher","order_digit","goods_voucher_batch","goods_voucher_subject_config","merchant","activity"}
} else {
tables = []string{"order","order_detail","order_cash","order_voucher","plan","key_batch","code_batch","voucher","voucher_batch","merchant_key_send"}
}
out := []map[string]interface{}{}
for _, tbl := range tables {
cols := getColumns(db, tbl)
fields := []map[string]string{}
for _, c := range cols {
tCanonical, fCanonical := canonicalField(ds, tbl, c.Name)
if tCanonical == "" || fCanonical == "" { continue }
lab := c.Comment
if lab == "" { lab = fCanonical }
fields = append(fields, map[string]string{"key": tCanonical+"."+fCanonical, "field": fCanonical, "label": lab})
}
tDisplay := displayTable(ds, tbl)
out = append(out, map[string]interface{}{"table": tDisplay, "label": tableLabel(tDisplay), "fields": fields})
}
sort.Slice(out, func(i, j int) bool { return out[i]["table"].(string) < out[j]["table"].(string) })
rec := recommendedDefaults(ds, ot)
ok(w, r, map[string]interface{}{"datasource": ds, "tables": out, "recommended": rec})
})
}
func tableLabel(t string) string {
switch t {
case "order": return "订单主表"
case "order_detail": return "订单详情"
case "order_cash": return "红包订单"
case "order_voucher": return "立减金订单"
case "order_digit": return "直充卡密订单"
case "plan": return "活动计划"
case "key_batch": return "key批次"
case "code_batch": return "兑换码批次"
case "voucher": return "立减金"
case "voucher_batch": return "立减金批次"
case "merchant_key_send": return "开放平台发放记录"
case "goods_voucher_batch": return "立减金批次表"
case "goods_voucher_subject_config": return "立减金主体配置"
case "merchant": return "客户"
case "activity": return "活动"
default: return t
}
}
func displayTable(ds, tbl string) string {
if ds == "ymt" && tbl == "order_info" { return "order" }
return tbl
}
func canonicalField(ds, tbl, col string) (string, string) {
if ds == "ymt" && tbl == "order_info" {
switch col {
case "order_no": return "order", "order_number"
case "key_code": return "order", "key"
case "user_id": return "order", "creator"
case "out_order_no": return "order", "out_trade_no"
case "activity_id": return "order", "plan_id"
case "merchant_id": return "order", "reseller_id"
case "goods_id": return "order", "product_id"
case "pay_price": return "order", "pay_amount"
case "key_batch_name": return "order", "key_batch_id"
default:
return "order", col
}
}
// other tables: canonical equals actual
return tbl, col
}
type columnMeta struct{ Name string; Comment string }
func getColumns(db *sql.DB, tbl string) []columnMeta {
rows, err := db.Query("SELECT COLUMN_NAME, COLUMN_COMMENT FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? ORDER BY ORDINAL_POSITION", tbl)
if err != nil { return []columnMeta{} }
defer rows.Close()
cols := []columnMeta{}
for rows.Next() {
var name, comment string
if err := rows.Scan(&name, &comment); err == nil {
cols = append(cols, columnMeta{Name: name, Comment: comment})
}
}
return cols
}
func recommendedDefaults(ds, orderType string) []string {
base := []string{"order.order_number","order.creator","order.out_trade_no","order.type","order.status","order.contract_price","order.num","order.pay_amount","order.create_time"}
if ds != "ymt" { base = []string{"order.order_number","order.creator","order.out_trade_no","order.type","order.status","order.contract_price","order.num","order.total","order.pay_amount","order.create_time"} }
t := orderType
if t == "1" { // 直充卡密
if ds == "ymt" {
base = append(base, "order_digit.order_no","order_digit.account","order_digit.success_time")
} else {
base = append(base, "plan.title")
}
} else if t == "2" { // 立减金
if ds == "ymt" {
base = append(base, "order_voucher.channel","order_voucher.status","goods_voucher_batch.channel_batch_no")
} else {
base = append(base, "order_voucher.channel","voucher.denomination")
}
} else if t == "3" { // 红包
if ds == "ymt" {
base = append(base, "order_cash.channel","order_cash.receive_status","order_cash.denomination")
} else {
base = append(base, "order_cash.channel","order_cash.receive_status","order_cash.amount")
}
}
return base
}

View File

@ -42,3 +42,10 @@ func ok(w http.ResponseWriter, r *http.Request, data interface{}) {
func fail(w http.ResponseWriter, r *http.Request, status int, msg string) { func fail(w http.ResponseWriter, r *http.Request, status int, msg string) {
writeJSON(w, r, status, 1, msg, nil) writeJSON(w, r, status, 1, msg, nil)
} }
func failCat(w http.ResponseWriter, r *http.Request, status int, msg string, kind string) {
writeJSON(w, r, status, 1, msg, nil)
tid := TraceIDFrom(r)
meta := MetaFrom(r)
log.Printf("kind=%s trace_id=%s status=%d method=%s path=%s msg=%s", kind, tid, status, meta.Method, meta.Path, msg)
}

View File

@ -12,6 +12,7 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
mux.Handle("/api/templates/", withAccess(withTrace(TemplatesHandler(metaDB, marketingDB)))) mux.Handle("/api/templates/", withAccess(withTrace(TemplatesHandler(metaDB, marketingDB))))
mux.Handle("/api/exports", withAccess(withTrace(ExportsHandler(metaDB, marketingDB)))) mux.Handle("/api/exports", withAccess(withTrace(ExportsHandler(metaDB, marketingDB))))
mux.Handle("/api/exports/", withAccess(withTrace(ExportsHandler(metaDB, marketingDB)))) mux.Handle("/api/exports/", withAccess(withTrace(ExportsHandler(metaDB, marketingDB))))
mux.Handle("/api/metadata/fields", withAccess(withTrace(MetadataHandler(metaDB, marketingDB))))
mux.Handle("/api/creators", withAccess(withTrace(CreatorsHandler(marketingDB)))) mux.Handle("/api/creators", withAccess(withTrace(CreatorsHandler(marketingDB))))
mux.Handle("/api/creators/", withAccess(withTrace(CreatorsHandler(marketingDB)))) mux.Handle("/api/creators/", withAccess(withTrace(CreatorsHandler(marketingDB))))
mux.Handle("/api/resellers", withAccess(withTrace(ResellersHandler(marketingDB)))) mux.Handle("/api/resellers", withAccess(withTrace(ResellersHandler(marketingDB))))

View File

@ -1,14 +1,16 @@
package api package api
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"io" "fmt"
"log" "io"
"net/http" "log"
"strings" "marketing-system-data-tool/server/internal/exporter"
"time" "marketing-system-data-tool/server/internal/schema"
"fmt" "net/http"
"strings"
"time"
) )
type TemplatesAPI struct { type TemplatesAPI struct {
@ -64,21 +66,23 @@ type TemplatePayload struct {
} }
func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) { func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body) b, _ := io.ReadAll(r.Body)
var p TemplatePayload var p TemplatePayload
json.Unmarshal(b, &p) json.Unmarshal(b, &p)
r = WithPayload(r, p) r = WithPayload(r, p)
uidStr := r.URL.Query().Get("userId") uidStr := r.URL.Query().Get("userId")
if uidStr != "" { if uidStr != "" {
var uid uint64 var uid uint64
_, _ = fmt.Sscan(uidStr, &uid) _, _ = fmt.Sscan(uidStr, &uid)
if uid > 0 { p.OwnerID = uid } if uid > 0 {
} p.OwnerID = uid
now := time.Now() }
tplSQL := "INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, stats_enabled, last_validated_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" }
tplArgs := []interface{}{p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), toJSON(p.Filters), p.FileFormat, p.Visibility, p.OwnerID, 1, 0, now, now, now} now := time.Now()
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs) tplSQL := "INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, stats_enabled, last_validated_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
_, err := a.meta.Exec(tplSQL, tplArgs...) tplArgs := []interface{}{p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), toJSON(p.Filters), p.FileFormat, p.Visibility, p.OwnerID, 1, 0, now, now, now}
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs)
_, err := a.meta.Exec(tplSQL, tplArgs...)
if err != nil { if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error()) fail(w, r, http.StatusInternalServerError, err.Error())
return return
@ -87,38 +91,38 @@ func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) {
} }
func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) { func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) {
uidStr := r.URL.Query().Get("userId") 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" 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{}{} args := []interface{}{}
if uidStr != "" { if uidStr != "" {
sqlText += " WHERE owner_id IN (0, ?)" sqlText += " WHERE owner_id IN (0, ?)"
args = append(args, uidStr) args = append(args, uidStr)
} }
sqlText += " ORDER BY updated_at DESC LIMIT 200" sqlText += " ORDER BY updated_at DESC LIMIT 200"
rows, err := a.meta.Query(sqlText, args...) rows, err := a.meta.Query(sqlText, args...)
if err != nil { if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error()) fail(w, r, http.StatusInternalServerError, err.Error())
return return
} }
defer rows.Close() defer rows.Close()
out := []map[string]interface{}{} out := []map[string]interface{}{}
for rows.Next() { for rows.Next() {
var id uint64 var id uint64
var name, datasource, mainTable, fileFormat, visibility string var name, datasource, mainTable, fileFormat, visibility string
var ownerID uint64 var ownerID uint64
var enabled int var enabled int
var lastValidatedAt sql.NullTime var lastValidatedAt sql.NullTime
var createdAt, updatedAt time.Time var createdAt, updatedAt time.Time
var fieldCount, execCount int64 var fieldCount, execCount int64
err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldCount, &execCount) err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldCount, &execCount)
if err != nil { if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error()) fail(w, r, http.StatusInternalServerError, err.Error())
return return
} }
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} 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) out = append(out, m)
} }
ok(w, r, out) ok(w, r, out)
} }
func (a *TemplatesAPI) getTemplate(w http.ResponseWriter, r *http.Request, id string) { func (a *TemplatesAPI) getTemplate(w http.ResponseWriter, r *http.Request, id string) {
@ -155,44 +159,44 @@ func (a *TemplatesAPI) getTemplate(w http.ResponseWriter, r *http.Request, id st
} }
func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id string) { func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id string) {
b, _ := io.ReadAll(r.Body) b, _ := io.ReadAll(r.Body)
var p map[string]interface{} var p map[string]interface{}
json.Unmarshal(b, &p) json.Unmarshal(b, &p)
set := []string{} set := []string{}
args := []interface{}{} args := []interface{}{}
for k, v := range p { for k, v := range p {
switch k { switch k {
case "name", "visibility", "file_format": case "name", "visibility", "file_format":
set = append(set, k+"=?") set = append(set, k+"=?")
args = append(args, v) args = append(args, v)
case "fields": case "fields":
set = append(set, "fields_json=?") set = append(set, "fields_json=?")
args = append(args, toJSON(v)) args = append(args, toJSON(v))
case "filters": case "filters":
set = append(set, "filters_json=?") set = append(set, "filters_json=?")
args = append(args, toJSON(v)) args = append(args, toJSON(v))
case "enabled": case "enabled":
set = append(set, "enabled=?") set = append(set, "enabled=?")
if v.(bool) { if v.(bool) {
args = append(args, 1) args = append(args, 1)
} else { } else {
args = append(args, 0) args = append(args, 0)
} }
} }
} }
if len(set) == 0 { if len(set) == 0 {
fail(w, r, http.StatusBadRequest, "no patch") fail(w, r, http.StatusBadRequest, "no patch")
return return
} }
// ensure updated_at // ensure updated_at
set = append(set, "updated_at=?") set = append(set, "updated_at=?")
args = append(args, time.Now(), id) args = append(args, time.Now(), id)
_, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...)
if err != nil { if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error()) fail(w, r, http.StatusInternalServerError, err.Error())
return return
} }
ok(w, r, nil) ok(w, r, nil)
} }
func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) { func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) {
@ -212,10 +216,11 @@ func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id
} }
func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, id string) { func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, id string) {
row := a.meta.QueryRow("SELECT main_table, fields_json, filters_json FROM export_templates WHERE id=?", id) row := a.meta.QueryRow("SELECT datasource, main_table, fields_json, filters_json FROM export_templates WHERE id= ?", id)
var ds string
var main string var main string
var fields, filters []byte var fields, filters []byte
err := row.Scan(&main, &fields, &filters) err := row.Scan(&ds, &main, &fields, &filters)
if err != nil { if err != nil {
fail(w, r, http.StatusNotFound, "not found") fail(w, r, http.StatusNotFound, "not found")
return return
@ -224,7 +229,30 @@ func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request,
var fl map[string]interface{} var fl map[string]interface{}
json.Unmarshal(fields, &fs) json.Unmarshal(fields, &fs)
json.Unmarshal(filters, &fl) json.Unmarshal(filters, &fl)
ok(w, r, nil) wl := Whitelist()
req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: fl}
q, args, err := exporter.BuildSQL(req, wl)
if err != nil {
failCat(w, r, http.StatusBadRequest, err.Error(), "sql_build_error")
return
}
dataDB := a.selectDataDB(ds)
score, sugg, err := exporter.EvaluateExplain(dataDB, q, args)
if err != nil {
failCat(w, r, http.StatusBadRequest, err.Error(), "explain_error")
return
}
idxSugg := exporter.IndexSuggestions(req)
sugg = append(sugg, idxSugg...)
_, _ = a.meta.Exec("UPDATE export_templates SET explain_json=?, explain_score=?, last_validated_at=?, updated_at=? WHERE id=?", toJSON(map[string]interface{}{"sql": q, "suggestions": sugg}), score, time.Now(), time.Now(), id)
ok(w, r, map[string]interface{}{"score": score, "suggestions": sugg})
}
func (a *TemplatesAPI) selectDataDB(ds string) *sql.DB {
if ds == "ymt" {
return a.meta
}
return a.marketing
} }
func toJSON(v interface{}) []byte { func toJSON(v interface{}) []byte {
@ -238,445 +266,8 @@ func fromJSON(b []byte) interface{} {
return v return v
} }
func Whitelist() map[string]bool { func Whitelist() map[string]bool { return schema.AllWhitelist() }
m := map[string]bool{
"order.order_number": true,
"order.key": true,
"order.creator": true,
"order.out_trade_no": true,
"order.type": true,
"order.status": true,
"order.account": true,
"order.product_id": true,
"order.reseller_id": true,
"order.plan_id": true,
"order.key_batch_id": true,
"order.code_batch_id": true,
"order.pay_type": true,
"order.pay_status": true,
"order.use_coupon": true,
"order.deliver_status": true,
"order.expire_time": true,
"order.recharge_time": true,
"order.contract_price": true,
"order.num": true,
"order.total": true,
"order.pay_amount": true,
"order.create_time": true,
"order.update_time": true,
"order.official_price": true,
"order.merchant_name": true,
"order.activity_name": true,
"order.goods_name": true,
"order.pay_time": true,
"order.coupon_id": true,
"order.discount_amount": true,
"order.supplier_product_name": true,
"order.is_inner": true,
"order.icon": true,
"order.cost_price": true,
"order.success_num": true,
"order.is_reset": true,
"order.is_retry": true,
"order.channel": true,
"order.is_store": true,
"order.trace_id": true,
"order.out_order_no": true,
"order.next_retry_time": true,
"order.recharge_suc_time": true,
"order.supplier_id": true,
"order.supplier_product_id": true,
"order.merchant_id": true,
"order.goods_id": true,
"order.activity_id": true,
"order.key_batch_name": true,
"order_detail.plan_title": true,
"order_detail.reseller_name": true,
"order_detail.product_name": true,
"order_detail.show_url": true,
"order_detail.official_price": true,
"order_detail.cost_price": true,
"order_detail.create_time": true,
"order_detail.update_time": true,
"order_cash.channel": true,
"order_cash.order_no": true,
"order_cash.trade_no": true,
"order_cash.wechat_detail_id": true,
"order_cash.denomination": true,
"order_cash.account": true,
"order_cash.receive_name": true,
"order_cash.app_id": true,
"order_cash.cash_activity_id": true,
"order_cash.receive_status": true,
"order_cash.receive_time": true,
"order_cash.success_time": true,
"order_cash.cash_packet_id": true,
"order_cash.cash_id": true,
"order_cash.amount": true,
"order_cash.activity_id": true,
"order_cash.goods_id": true,
"order_cash.merchant_id": true,
"order_cash.supplier_id": true,
"order_cash.user_id": true,
"order_cash.status": true,
"order_cash.expire_time": true,
"order_cash.create_time": true,
"order_cash.update_time": true,
"order_cash.version": true,
"order_cash.is_confirm": true,
"order_voucher.channel": true,
"order_voucher.channel_activity_id": true,
"order_voucher.channel_voucher_id": true,
"order_voucher.status": true,
"order_voucher.receive_mode": true,
"order_voucher.grant_time": true,
"order_voucher.usage_time": true,
"order_voucher.refund_time": true,
"order_voucher.status_modify_time": true,
"order_voucher.overdue_time": true,
"order_voucher.refund_amount": true,
"order_voucher.official_price": true,
"order_voucher.out_biz_no": true,
"order_voucher.account_no": true,
"plan.id": true,
"plan.title": true,
"plan.status": true,
"plan.begin_time": true,
"plan.end_time": true,
"key_batch.id": true,
"key_batch.batch_name": true,
"key_batch.bind_object": true,
"key_batch.quantity": true,
"key_batch.stock": true,
"key_batch.begin_time": true,
"key_batch.end_time": true,
"code_batch.id": true,
"code_batch.title": true,
"code_batch.status": true,
"code_batch.begin_time": true,
"code_batch.end_time": true,
"code_batch.quantity": true,
"code_batch.usage": true,
"code_batch.stock": true,
"voucher.channel": true,
"voucher.channel_activity_id": true,
"voucher.price": true,
"voucher.balance": true,
"voucher.used_amount": true,
"voucher.denomination": true,
"voucher_batch.channel_activity_id": true,
"voucher_batch.temp_no": true,
"voucher_batch.provider": true,
"voucher_batch.weight": true,
"merchant_key_send.merchant_id": true,
"merchant_key_send.out_biz_no": true,
"merchant_key_send.key": true,
"merchant_key_send.status": true,
"merchant_key_send.usage_time": true,
"merchant_key_send.create_time": true,
"order_digit.order_no": true,
"order_digit.card_no": true,
"order_digit.account": true,
"order_digit.goods_id": true,
"order_digit.merchant_id": true,
"order_digit.supplier_id": true,
"order_digit.activity_id": true,
"order_digit.user_id": true,
"order_digit.success_time": true,
"order_digit.supplier_product_no": true,
"order_digit.order_type": true,
"order_digit.end_time": true,
"order_digit.create_time": true,
"order_digit.update_time": true,
"order_digit.code": true,
"order_digit.sms_channel": true,
"goods_voucher_batch.channel_batch_no": true,
"goods_voucher_batch.voucher_subject_id": true,
"goods_voucher_batch.id": true,
"goods_voucher_batch.goods_voucher_id": true,
"goods_voucher_batch.supplier_id": true,
"goods_voucher_batch.temp_no": true,
"goods_voucher_batch.index": true,
"goods_voucher_batch.create_time": true,
"goods_voucher_batch.update_time": true,
"goods_voucher_subject_config.id": true,
"goods_voucher_subject_config.name": true,
"goods_voucher_subject_config.type": true,
"goods_voucher_subject_config.create_time": true,
"merchant.id": true,
"merchant.name": true,
"merchant.user_id": true,
"merchant.merchant_no": true,
"merchant.subject": true,
"merchant.third_party": true,
"merchant.status": true,
"merchant.balance": true,
"merchant.total_consumption": true,
"merchant.contact_name": true,
"merchant.contact_phone": true,
"merchant.contact_email": true,
"merchant.create_time": true,
"merchant.update_time": true,
"activity.id": true,
"activity.name": true,
"activity.user_id": true,
"activity.merchant_id": true,
"activity.user_name": true,
"activity.activity_no": true,
"activity.status": true,
"activity.key_total_num": true,
"activity.key_generate_num": true,
"activity.key_usable_num": true,
"activity.domain_url": true,
"activity.theme_login_id": true,
"activity.theme_list_id": true,
"activity.theme_verify_id": true,
"activity.settlement_type": true,
"activity.key_expire_type": true,
"activity.key_valid_day": true,
"activity.key_begin_time": true,
"activity.key_end_time": true,
"activity.key_style": true,
"activity.begin_time": true,
"activity.end_time": true,
"activity.is_retry": true,
"activity.create_time": true,
"activity.update_time": true,
"activity.discard_time": true,
"activity.delete_time": true,
"activity.auto_charge": true,
"activity.stock": true,
"activity.approval_trade_no": true,
"activity.amount": true,
"activity.channels": true,
"activity.key_begin": true,
"activity.key_end": true,
"activity.key_unit": true,
"activity.key_pay_button_text": true,
"activity.goods_pay_button_text": true,
"activity.is_open_db_transaction": true,
"activity.bank_tag": true,
}
return m
}
func FieldLabels() map[string]string { func FieldLabels() map[string]string {
return map[string]string{ return schema.AllLabels()
"order.order_number": "订单编号",
"order.key": "KEY",
"order.creator": "创建者ID",
"order.out_trade_no": "支付流水号",
"order.type": "订单类型",
"order.status": "订单状态",
"order.account": "账号",
"order.product_id": "商品ID",
"order.reseller_id": "分销商ID",
"order.plan_id": "计划ID",
"order.key_batch_id": "KEY批次ID",
"order.code_batch_id": "兑换批次ID",
"order.pay_type": "支付方式",
"order.pay_status": "支付状态",
"order.use_coupon": "是否使用优惠券",
"order.deliver_status": "投递状态",
"order.expire_time": "过期处理时间",
"order.recharge_time": "充值时间",
"order.contract_price": "合同单价",
"order.num": "数量",
"order.total": "总金额",
"order.pay_amount": "支付金额",
"order.create_time": "创建时间",
"order.update_time": "更新时间",
"order.official_price": "官方价",
"order.merchant_name": "分销商名称",
"order.activity_name": "活动名称",
"order.goods_name": "商品名称",
"order.pay_time": "支付时间",
"order.coupon_id": "优惠券ID",
"order.discount_amount": "优惠金额",
"order.supplier_product_name": "供应商产品名称",
"order.is_inner": "内部供应商订单",
"order.icon": "订单图片",
"order.cost_price": "成本价",
"order.success_num": "到账数量",
"order.is_reset": "是否重置",
"order.is_retry": "是否重试",
"order.channel": "支付渠道",
"order.is_store": "是否退还库存",
"order.trace_id": "TraceID",
"order.out_order_no": "外部订单号",
"order.next_retry_time": "下次重试时间",
"order.recharge_suc_time": "充值成功时间",
"order.supplier_id": "供应商ID",
"order.supplier_product_id": "供应商产品ID",
"order.merchant_id": "分销商ID",
"order.goods_id": "商品ID",
"order.activity_id": "活动ID",
"order.key_batch_name": "key批次名称",
"order_detail.plan_title": "计划标题",
"order_detail.reseller_name": "分销商名称",
"order_detail.product_name": "商品名称",
"order_detail.show_url": "商品图片URL",
"order_detail.official_price": "官方价",
"order_detail.cost_price": "成本价",
"order_detail.create_time": "创建时间",
"order_detail.update_time": "更新时间",
"order_cash.order_no": "订单号",
"order_cash.trade_no": "交易号",
"order_cash.wechat_detail_id": "微信明细单号",
"order_cash.channel": "渠道",
"order_cash.denomination": "红包面额",
"order_cash.account": "领取账号",
"order_cash.receive_name": "真实姓名",
"order_cash.app_id": "转账AppID",
"order_cash.cash_activity_id": "红包批次号",
"order_cash.receive_status": "领取状态",
"order_cash.receive_time": "拆红包时间",
"order_cash.success_time": "成功时间",
"order_cash.cash_packet_id": "红包ID",
"order_cash.cash_id": "红包规则ID",
"order_cash.amount": "红包额度",
"order_cash.activity_id": "活动ID",
"order_cash.goods_id": "商品ID",
"order_cash.merchant_id": "分销商ID",
"order_cash.supplier_id": "供应商ID",
"order_cash.user_id": "创建者ID",
"order_cash.status": "状态",
"order_cash.expire_time": "过期时间",
"order_cash.create_time": "创建时间",
"order_cash.update_time": "更新时间",
"order_cash.version": "版本",
"order_cash.is_confirm": "是否确认",
"order_voucher.channel": "渠道",
"order_voucher.channel_activity_id": "渠道立减金批次",
"order_voucher.channel_voucher_id": "渠道立减金ID",
"order_voucher.status": "状态",
"order_voucher.receive_mode": "领取方式",
"order_voucher.grant_time": "领取时间",
"order_voucher.usage_time": "核销时间",
"order_voucher.refund_time": "退款时间",
"order_voucher.status_modify_time": "状态更新时间",
"order_voucher.overdue_time": "过期时间",
"order_voucher.refund_amount": "退款金额",
"order_voucher.official_price": "官方价",
"order_voucher.out_biz_no": "外部业务号",
"order_voucher.account_no": "账户号",
"plan.id": "计划ID",
"plan.title": "计划标题",
"plan.status": "状态",
"plan.begin_time": "开始时间",
"plan.end_time": "结束时间",
"key_batch.id": "批次ID",
"key_batch.batch_name": "批次名称",
"key_batch.bind_object": "绑定对象",
"key_batch.quantity": "发放数量",
"key_batch.stock": "剩余库存",
"key_batch.begin_time": "开始时间",
"key_batch.end_time": "结束时间",
"code_batch.id": "兑换批次ID",
"code_batch.title": "标题",
"code_batch.status": "状态",
"code_batch.begin_time": "开始时间",
"code_batch.end_time": "结束时间",
"code_batch.quantity": "数量",
"code_batch.usage": "使用数",
"code_batch.stock": "库存",
"voucher.channel": "渠道",
"voucher.channel_activity_id": "渠道批次号",
"voucher.price": "合同单价",
"voucher.balance": "剩余额度",
"voucher.used_amount": "已用额度",
"voucher.denomination": "面额",
"voucher_batch.channel_activity_id": "渠道批次号",
"voucher_batch.temp_no": "模板编号",
"voucher_batch.provider": "服务商",
"voucher_batch.weight": "权重",
"merchant_key_send.merchant_id": "商户ID",
"merchant_key_send.out_biz_no": "商户业务号",
"merchant_key_send.key": "券码",
"merchant_key_send.status": "状态",
"merchant_key_send.usage_time": "核销时间",
"merchant_key_send.create_time": "创建时间",
"order_digit.order_no": "订单号",
"order_digit.card_no": "卡号",
"order_digit.account": "充值账号",
"order_digit.goods_id": "商品ID",
"order_digit.merchant_id": "分销商ID",
"order_digit.supplier_id": "供应商ID",
"order_digit.activity_id": "活动ID",
"order_digit.user_id": "创建者ID",
"order_digit.success_time": "到账时间",
"order_digit.supplier_product_no": "供应商产品编码",
"order_digit.order_type": "订单类型",
"order_digit.end_time": "卡密有效期",
"order_digit.create_time": "创建时间",
"order_digit.update_time": "更新时间",
"order_digit.code": "验证码",
"order_digit.sms_channel": "短信渠道",
"goods_voucher_batch.channel_batch_no": "渠道批次号",
"goods_voucher_batch.voucher_subject_id": "主体配置ID",
"goods_voucher_batch.id": "ID",
"goods_voucher_batch.goods_voucher_id": "立减金ID",
"goods_voucher_batch.supplier_id": "供应商ID",
"goods_voucher_batch.temp_no": "模板编号",
"goods_voucher_batch.index": "权重",
"goods_voucher_batch.create_time": "创建时间",
"goods_voucher_batch.update_time": "更新时间",
"goods_voucher_subject_config.id": "主体配置ID",
"goods_voucher_subject_config.name": "主体名称",
"goods_voucher_subject_config.type": "主体类型",
"goods_voucher_subject_config.create_time": "创建时间",
"merchant.id": "客户ID",
"merchant.name": "客户名称",
"merchant.user_id": "用户中心ID",
"merchant.merchant_no": "商户编码",
"merchant.subject": "客户主体",
"merchant.third_party": "来源类型",
"merchant.status": "状态",
"merchant.balance": "客户余额",
"merchant.total_consumption": "累计消费",
"merchant.contact_name": "联系人名称",
"merchant.contact_phone": "联系人电话",
"merchant.contact_email": "联系人Email",
"merchant.create_time": "创建时间",
"merchant.update_time": "编辑时间",
"activity.id": "活动ID",
"activity.name": "活动名称",
"activity.user_id": "创建者ID",
"activity.merchant_id": "客户ID",
"activity.user_name": "创建者名称",
"activity.activity_no": "活动编号",
"activity.status": "状态",
"activity.key_total_num": "Key码总量",
"activity.key_generate_num": "Key码已生成数量",
"activity.key_usable_num": "Key可使用次数",
"activity.domain_url": "域名",
"activity.theme_login_id": "登录模版ID",
"activity.theme_list_id": "列表模版ID",
"activity.theme_verify_id": "验证模版ID",
"activity.settlement_type": "结算方式",
"activity.key_expire_type": "Key有效期类型",
"activity.key_valid_day": "有效天数",
"activity.key_begin_time": "Key有效开始时间",
"activity.key_end_time": "Key有效结束时间",
"activity.key_style": "Key样式",
"activity.begin_time": "开始时间",
"activity.end_time": "结束时间",
"activity.is_retry": "是否自动重试",
"activity.create_time": "创建时间",
"activity.update_time": "修改时间",
"activity.discard_time": "作废时间",
"activity.delete_time": "删除时间",
"activity.auto_charge": "是否充值到账",
"activity.stock": "已使用库存",
"activity.approval_trade_no": "审批交易号",
"activity.amount": "支付金额",
"activity.channels": "支付渠道",
"activity.key_begin": "开始月份",
"activity.key_end": "截止月份",
"activity.key_unit": "时间单位",
"activity.key_pay_button_text": "Key支付按钮文本",
"activity.goods_pay_button_text": "商品支付按钮文本",
"activity.is_open_db_transaction": "是否开启事务",
"activity.bank_tag": "银行标识",
}
} }

View File

@ -0,0 +1,76 @@
package exporter
import (
"database/sql"
"fmt"
"strings"
"marketing-system-data-tool/server/internal/schema"
)
func EvaluateExplain(db *sql.DB, q string, args []interface{}) (int, []string, error) {
rows, score, err := RunExplain(db, q, args)
if err != nil { return 0, nil, err }
sugg := []string{}
for _, r := range rows {
// tbl := r.Table.String
typ := r.Type.String
if typ == "" && r.SelectType.Valid { typ = r.SelectType.String }
if typ == "ALL" {
sugg = append(sugg, "出现全表扫描(ALL),请在过滤或连接列上建立索引")
}
if r.Extra.Valid {
e := r.Extra.String
if contains(e, "Using temporary") || contains(e, "Using filesort") {
sugg = append(sugg, "出现临时表或文件排序,请优化排序列及索引覆盖")
}
}
}
return score, sugg, nil
}
func IndexSuggestions(req BuildRequest) []string {
sugg := []string{}
sch := schema.Get(req.Datasource, req.MainTable)
// Filter-based suggestions
has := func(k string) bool { _, ok := req.Filters[k]; return ok }
add := func(s string){ if s != "" { sugg = append(sugg, s) } }
if has("creator_in") && has("create_time_between") {
add(fmt.Sprintf("建议在 `%s`(create_time, %s) 建立复合索引以覆盖权限与时间范围", sch.TableName("order"), colName(sch, "creator_in")))
} else {
if has("creator_in") { add(fmt.Sprintf("建议在 `%s`(%s) 建立索引以覆盖权限过滤", sch.TableName("order"), colName(sch, "creator_in"))) }
if has("create_time_between") { add(fmt.Sprintf("建议在 `%s`(create_time) 建立索引以覆盖时间范围", sch.TableName("order"))) }
}
if has("plan_id_eq") { add(fmt.Sprintf("建议在 `%s`(%s) 建立索引以覆盖活动/计划过滤", sch.TableName("order"), colName(sch, "plan_id_eq"))) }
if has("reseller_id_eq") { add(fmt.Sprintf("建议在 `%s`(%s) 建立索引以覆盖分销商过滤", sch.TableName("order"), colName(sch, "reseller_id_eq"))) }
if has("product_id_eq") { add(fmt.Sprintf("建议在 `%s`(%s) 建立索引以覆盖商品过滤", sch.TableName("order"), colName(sch, "product_id_eq"))) }
if has("out_trade_no_eq") { add(fmt.Sprintf("建议在 `%s`(%s) 建立索引以覆盖支付流水过滤", sch.TableName("order"), colName(sch, "out_trade_no_eq"))) }
// Table usage-based join suggestions
usedTables := map[string]bool{}
for _, tf := range req.Fields {
parts := strings.Split(tf, ".")
if len(parts)==2 { usedTables[parts[0]] = true }
}
if req.MainTable == "order_info" {
add("建议在 `order_info`(order_no) 建立索引以优化与子表的连接")
if usedTables["order_cash"] { add("建议在 `order_cash`(order_no) 建立索引以优化与主表的连接") }
if usedTables["order_voucher"] { add("建议在 `order_voucher`(order_no) 建立索引以优化与主表的连接") }
if usedTables["order_digit"] { add("建议在 `order_digit`(order_no) 建立索引以优化与主表的连接") }
if usedTables["goods_voucher_batch"] { add("建议在 `goods_voucher_batch`(channel_batch_no) 建立索引以优化与订单立减金的连接") }
if usedTables["goods_voucher_subject_config"] { add("建议在 `goods_voucher_subject_config`(id) 上确保主键索引以优化连接") }
if usedTables["merchant"] { add("建议在 `merchant`(id) 上确保主键索引以优化连接") }
if usedTables["activity"] { add("建议在 `activity`(id) 上确保主键索引以优化连接") }
}
return dedup(sugg)
}
func colName(sch schema.Schema, key string) string {
if _, col, ok := sch.FilterColumn(key); ok { return col }
return ""
}
func dedup(arr []string) []string {
m := map[string]bool{}
out := []string{}
for _, s := range arr { if !m[s] { m[s]=true; out = append(out, s) } }
return out
}

View File

@ -0,0 +1,455 @@
package schema
func AllWhitelist() map[string]bool {
m := map[string]bool{
"order.order_number": true,
"order.key": true,
"order.creator": true,
"order.out_trade_no": true,
"order.type": true,
"order.status": true,
"order.account": true,
"order.product_id": true,
"order.reseller_id": true,
"order.plan_id": true,
"order.key_batch_id": true,
"order.code_batch_id": true,
"order.pay_type": true,
"order.pay_status": true,
"order.use_coupon": true,
"order.deliver_status": true,
"order.expire_time": true,
"order.recharge_time": true,
"order.contract_price": true,
"order.num": true,
"order.total": true,
"order.pay_amount": true,
"order.create_time": true,
"order.update_time": true,
"order.official_price": true,
"order.merchant_name": true,
"order.activity_name": true,
"order.goods_name": true,
"order.pay_time": true,
"order.coupon_id": true,
"order.discount_amount": true,
"order.supplier_product_name": true,
"order.is_inner": true,
"order.icon": true,
"order.cost_price": true,
"order.success_num": true,
"order.is_reset": true,
"order.is_retry": true,
"order.channel": true,
"order.is_store": true,
"order.trace_id": true,
"order.out_order_no": true,
"order.next_retry_time": true,
"order.recharge_suc_time": true,
"order.supplier_id": true,
"order.supplier_product_id": true,
"order.merchant_id": true,
"order.goods_id": true,
"order.activity_id": true,
"order.key_batch_name": true,
"order_detail.plan_title": true,
"order_detail.reseller_name": true,
"order_detail.product_name": true,
"order_detail.show_url": true,
"order_detail.official_price": true,
"order_detail.cost_price": true,
"order_detail.create_time": true,
"order_detail.update_time": true,
"order_cash.order_no": true,
"order_cash.trade_no": true,
"order_cash.wechat_detail_id": true,
"order_cash.channel": true,
"order_cash.denomination": true,
"order_cash.account": true,
"order_cash.receive_name": true,
"order_cash.app_id": true,
"order_cash.cash_activity_id": true,
"order_cash.receive_status": true,
"order_cash.receive_time": true,
"order_cash.success_time": true,
"order_cash.cash_packet_id": true,
"order_cash.cash_id": true,
"order_cash.amount": true,
"order_cash.activity_id": true,
"order_cash.goods_id": true,
"order_cash.merchant_id": true,
"order_cash.supplier_id": true,
"order_cash.user_id": true,
"order_cash.status": true,
"order_cash.expire_time": true,
"order_cash.create_time": true,
"order_cash.update_time": true,
"order_cash.version": true,
"order_cash.is_confirm": true,
"order_voucher.channel": true,
"order_voucher.channel_activity_id": true,
"order_voucher.channel_voucher_id": true,
"order_voucher.status": true,
"order_voucher.receive_mode": true,
"order_voucher.grant_time": true,
"order_voucher.usage_time": true,
"order_voucher.refund_time": true,
"order_voucher.status_modify_time": true,
"order_voucher.overdue_time": true,
"order_voucher.refund_amount": true,
"order_voucher.official_price": true,
"order_voucher.out_biz_no": true,
"order_voucher.account_no": true,
"plan.id": true,
"plan.title": true,
"plan.status": true,
"plan.begin_time": true,
"plan.end_time": true,
"key_batch.id": true,
"key_batch.batch_name": true,
"key_batch.bind_object": true,
"key_batch.quantity": true,
"key_batch.stock": true,
"key_batch.begin_time": true,
"key_batch.end_time": true,
"code_batch.id": true,
"code_batch.title": true,
"code_batch.status": true,
"code_batch.begin_time": true,
"code_batch.end_time": true,
"code_batch.quantity": true,
"code_batch.usage": true,
"code_batch.stock": true,
"voucher.channel": true,
"voucher.channel_activity_id": true,
"voucher.price": true,
"voucher.balance": true,
"voucher.used_amount": true,
"voucher.denomination": true,
"voucher_batch.channel_activity_id": true,
"voucher_batch.temp_no": true,
"voucher_batch.provider": true,
"voucher_batch.weight": true,
"merchant_key_send.merchant_id": true,
"merchant_key_send.out_biz_no": true,
"merchant_key_send.key": true,
"merchant_key_send.status": true,
"merchant_key_send.usage_time": true,
"merchant_key_send.create_time": true,
"order_digit.order_no": true,
"order_digit.card_no": true,
"order_digit.account": true,
"order_digit.goods_id": true,
"order_digit.merchant_id": true,
"order_digit.supplier_id": true,
"order_digit.activity_id": true,
"order_digit.user_id": true,
"order_digit.success_time": true,
"order_digit.supplier_product_no": true,
"order_digit.order_type": true,
"order_digit.end_time": true,
"order_digit.create_time": true,
"order_digit.update_time": true,
"order_digit.code": true,
"order_digit.sms_channel": true,
"goods_voucher_batch.channel_batch_no": true,
"goods_voucher_batch.voucher_subject_id": true,
"goods_voucher_batch.id": true,
"goods_voucher_batch.goods_voucher_id": true,
"goods_voucher_batch.supplier_id": true,
"goods_voucher_batch.temp_no": true,
"goods_voucher_batch.index": true,
"goods_voucher_batch.create_time": true,
"goods_voucher_batch.update_time": true,
"goods_voucher_subject_config.id": true,
"goods_voucher_subject_config.name": true,
"goods_voucher_subject_config.type": true,
"goods_voucher_subject_config.create_time": true,
"merchant.id": true,
"merchant.name": true,
"merchant.user_id": true,
"merchant.merchant_no": true,
"merchant.subject": true,
"merchant.third_party": true,
"merchant.status": true,
"merchant.balance": true,
"merchant.total_consumption": true,
"merchant.contact_name": true,
"merchant.contact_phone": true,
"merchant.contact_email": true,
"merchant.create_time": true,
"merchant.update_time": true,
"activity.id": true,
"activity.name": true,
"activity.user_id": true,
"activity.merchant_id": true,
"activity.user_name": true,
"activity.activity_no": true,
"activity.status": true,
"activity.key_total_num": true,
"activity.key_generate_num": true,
"activity.key_usable_num": true,
"activity.domain_url": true,
"activity.theme_login_id": true,
"activity.theme_list_id": true,
"activity.theme_verify_id": true,
"activity.settlement_type": true,
"activity.key_expire_type": true,
"activity.key_valid_day": true,
"activity.key_begin_time": true,
"activity.key_end_time": true,
"activity.key_style": true,
"activity.begin_time": true,
"activity.end_time": true,
"activity.is_retry": true,
"activity.create_time": true,
"activity.update_time": true,
"activity.discard_time": true,
"activity.delete_time": true,
"activity.auto_charge": true,
"activity.stock": true,
"activity.approval_trade_no": true,
"activity.amount": true,
"activity.channels": true,
"activity.key_begin": true,
"activity.key_end": true,
"activity.key_unit": true,
"activity.key_pay_button_text": true,
"activity.goods_pay_button_text": true,
"activity.is_open_db_transaction": true,
"activity.bank_tag": true,
}
return m
}
func AllLabels() map[string]string {
return map[string]string{
"order.order_number": "订单编号",
"order.key": "KEY",
"order.creator": "创建者ID",
"order.out_trade_no": "支付流水号",
"order.type": "订单类型",
"order.status": "订单状态",
"order.account": "账号",
"order.product_id": "商品ID",
"order.reseller_id": "分销商ID",
"order.plan_id": "计划ID",
"order.key_batch_id": "KEY批次ID",
"order.code_batch_id": "兑换批次ID",
"order.pay_type": "支付方式",
"order.pay_status": "支付状态",
"order.use_coupon": "是否使用优惠券",
"order.deliver_status": "投递状态",
"order.expire_time": "过期处理时间",
"order.recharge_time": "充值时间",
"order.contract_price": "合同单价",
"order.num": "数量",
"order.total": "总金额",
"order.pay_amount": "支付金额",
"order.create_time": "创建时间",
"order.update_time": "更新时间",
"order.official_price": "官方价",
"order.merchant_name": "分销商名称",
"order.activity_name": "活动名称",
"order.goods_name": "商品名称",
"order.pay_time": "支付时间",
"order.coupon_id": "优惠券ID",
"order.discount_amount": "优惠金额",
"order.supplier_product_name": "供应商产品名称",
"order.is_inner": "内部供应商订单",
"order.icon": "订单图片",
"order.cost_price": "成本价",
"order.success_num": "到账数量",
"order.is_reset": "是否重置",
"order.is_retry": "是否重试",
"order.channel": "支付渠道",
"order.is_store": "是否退还库存",
"order.trace_id": "TraceID",
"order.out_order_no": "外部订单号",
"order.next_retry_time": "下次重试时间",
"order.recharge_suc_time": "充值成功时间",
"order.supplier_id": "供应商ID",
"order.supplier_product_id": "供应商产品ID",
"order.merchant_id": "分销商ID",
"order.goods_id": "商品ID",
"order.activity_id": "活动ID",
"order.key_batch_name": "key批次名称",
"order_detail.plan_title": "计划标题",
"order_detail.reseller_name": "分销商名称",
"order_detail.product_name": "商品名称",
"order_detail.show_url": "商品图片URL",
"order_detail.official_price": "官方价",
"order_detail.cost_price": "成本价",
"order_detail.create_time": "创建时间",
"order_detail.update_time": "更新时间",
"order_cash.order_no": "订单号",
"order_cash.trade_no": "交易号",
"order_cash.wechat_detail_id": "微信明细单号",
"order_cash.channel": "渠道",
"order_cash.denomination": "红包面额",
"order_cash.account": "领取账号",
"order_cash.receive_name": "真实姓名",
"order_cash.app_id": "转账AppID",
"order_cash.cash_activity_id": "红包批次号",
"order_cash.receive_status": "领取状态",
"order_cash.receive_time": "拆红包时间",
"order_cash.success_time": "成功时间",
"order_cash.cash_packet_id": "红包ID",
"order_cash.cash_id": "红包规则ID",
"order_cash.amount": "红包额度",
"order_cash.activity_id": "活动ID",
"order_cash.goods_id": "商品ID",
"order_cash.merchant_id": "分销商ID",
"order_cash.supplier_id": "供应商ID",
"order_cash.user_id": "创建者ID",
"order_cash.status": "状态",
"order_cash.expire_time": "过期时间",
"order_cash.create_time": "创建时间",
"order_cash.update_time": "更新时间",
"order_cash.version": "版本",
"order_cash.is_confirm": "是否确认",
"order_voucher.channel": "渠道",
"order_voucher.channel_activity_id": "渠道立减金批次",
"order_voucher.channel_voucher_id": "渠道立减金ID",
"order_voucher.status": "状态",
"order_voucher.receive_mode": "领取方式",
"order_voucher.grant_time": "领取时间",
"order_voucher.usage_time": "核销时间",
"order_voucher.refund_time": "退款时间",
"order_voucher.status_modify_time": "状态更新时间",
"order_voucher.overdue_time": "过期时间",
"order_voucher.refund_amount": "退款金额",
"order_voucher.official_price": "官方价",
"order_voucher.out_biz_no": "外部业务号",
"order_voucher.account_no": "账户号",
"plan.id": "计划ID",
"plan.title": "计划标题",
"plan.status": "状态",
"plan.begin_time": "开始时间",
"plan.end_time": "结束时间",
"key_batch.id": "批次ID",
"key_batch.batch_name": "批次名称",
"key_batch.bind_object": "绑定对象",
"key_batch.quantity": "发放数量",
"key_batch.stock": "剩余库存",
"key_batch.begin_time": "开始时间",
"key_batch.end_time": "结束时间",
"code_batch.id": "兑换批次ID",
"code_batch.title": "标题",
"code_batch.status": "状态",
"code_batch.begin_time": "开始时间",
"code_batch.end_time": "结束时间",
"code_batch.quantity": "数量",
"code_batch.usage": "使用数",
"code_batch.stock": "库存",
"voucher.channel": "渠道",
"voucher.channel_activity_id": "渠道批次号",
"voucher.price": "合同单价",
"voucher.balance": "剩余额度",
"voucher.used_amount": "已用额度",
"voucher.denomination": "面额",
"voucher_batch.channel_activity_id": "渠道批次号",
"voucher_batch.temp_no": "模板编号",
"voucher_batch.provider": "服务商",
"voucher_batch.weight": "权重",
"merchant_key_send.merchant_id": "商户ID",
"merchant_key_send.out_biz_no": "商户业务号",
"merchant_key_send.key": "券码",
"merchant_key_send.status": "状态",
"merchant_key_send.usage_time": "核销时间",
"merchant_key_send.create_time": "创建时间",
"order_digit.order_no": "订单号",
"order_digit.card_no": "卡号",
"order_digit.account": "充值账号",
"order_digit.goods_id": "商品ID",
"order_digit.merchant_id": "分销商ID",
"order_digit.supplier_id": "供应商ID",
"order_digit.activity_id": "活动ID",
"order_digit.user_id": "创建者ID",
"order_digit.success_time": "到账时间",
"order_digit.supplier_product_no": "供应商产品编码",
"order_digit.order_type": "订单类型",
"order_digit.end_time": "卡密有效期",
"order_digit.create_time": "创建时间",
"order_digit.update_time": "更新时间",
"order_digit.code": "验证码",
"order_digit.sms_channel": "短信渠道",
"goods_voucher_batch.channel_batch_no": "渠道批次号",
"goods_voucher_batch.voucher_subject_id": "主体配置ID",
"goods_voucher_batch.id": "ID",
"goods_voucher_batch.goods_voucher_id": "立减金ID",
"goods_voucher_batch.supplier_id": "供应商ID",
"goods_voucher_batch.temp_no": "模板编号",
"goods_voucher_batch.index": "权重",
"goods_voucher_batch.create_time": "创建时间",
"goods_voucher_batch.update_time": "更新时间",
"goods_voucher_subject_config.id": "主体配置ID",
"goods_voucher_subject_config.name": "主体名称",
"goods_voucher_subject_config.type": "主体类型",
"goods_voucher_subject_config.create_time": "创建时间",
"merchant.id": "客户ID",
"merchant.name": "客户名称",
"merchant.user_id": "用户中心ID",
"merchant.merchant_no": "商户编码",
"merchant.subject": "客户主体",
"merchant.third_party": "来源类型",
"merchant.status": "状态",
"merchant.balance": "客户余额",
"merchant.total_consumption": "累计消费",
"merchant.contact_name": "联系人名称",
"merchant.contact_phone": "联系人电话",
"merchant.contact_email": "联系人Email",
"merchant.create_time": "创建时间",
"merchant.update_time": "编辑时间",
"activity.id": "活动ID",
"activity.name": "活动名称",
"activity.user_id": "创建者ID",
"activity.merchant_id": "客户ID",
"activity.user_name": "创建者名称",
"activity.activity_no": "活动编号",
"activity.status": "状态",
"activity.key_total_num": "Key码总量",
"activity.key_generate_num": "Key码已生成数量",
"activity.key_usable_num": "Key可使用次数",
"activity.domain_url": "域名",
"activity.theme_login_id": "登录模版ID",
"activity.theme_list_id": "列表模版ID",
"activity.theme_verify_id": "验证模版ID",
"activity.settlement_type": "结算方式",
"activity.key_expire_type": "Key有效期类型",
"activity.key_valid_day": "有效天数",
"activity.key_begin_time": "Key有效开始时间",
"activity.key_end_time": "Key有效结束时间",
"activity.key_style": "Key样式",
"activity.begin_time": "开始时间",
"activity.end_time": "结束时间",
"activity.is_retry": "是否自动重试",
"activity.create_time": "创建时间",
"activity.update_time": "修改时间",
"activity.discard_time": "作废时间",
"activity.delete_time": "删除时间",
"activity.auto_charge": "是否充值到账",
"activity.stock": "已使用库存",
"activity.approval_trade_no": "审批交易号",
"activity.amount": "支付金额",
"activity.channels": "支付渠道",
"activity.key_begin": "开始月份",
"activity.key_end": "截止月份",
"activity.key_unit": "时间单位",
"activity.key_pay_button_text": "Key支付按钮文本",
"activity.goods_pay_button_text": "商品支付按钮文本",
"activity.is_open_db_transaction": "是否开启事务",
"activity.bank_tag": "银行标识",
}
}
func RecommendedDefaultFields(ds string) []string {
if ds == "ymt" {
return []string{
"order.order_number", "order.creator", "order.out_trade_no", "order.type", "order.status", "order.contract_price", "order.num", "order.pay_amount", "order.create_time",
}
}
return []string{
"order.order_number", "order.creator", "order.out_trade_no", "order.type", "order.status", "order.contract_price", "order.num", "order.total", "order.pay_amount", "order.create_time",
}
}

View File

@ -76,3 +76,63 @@ server listening on :8077
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077 server listening on :8077
{"bytes":1371,"duration_ms":57,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:24+08:00"}
{"bytes":121,"duration_ms":272,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:26+08:00"}
{"bytes":121,"duration_ms":333,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:27+08:00"}
{"bytes":3055,"duration_ms":319,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:30+08:00"}
{"bytes":3055,"duration_ms":346,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:31+08:00"}
{"bytes":3055,"duration_ms":317,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:22:32+08:00"}
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
{"bytes":13700,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:26:32+08:00"}
{"bytes":1371,"duration_ms":50,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:26:40+08:00"}
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
{"bytes":10579,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:32:10+08:00"}
{"bytes":1371,"duration_ms":45,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:32:11+08:00"}
{"bytes":10579,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:36:14+08:00"}
{"bytes":1371,"duration_ms":49,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:36:14+08:00"}
{"bytes":1371,"duration_ms":93,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:36:23+08:00"}
{"bytes":1272,"duration_ms":145,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:36:51+08:00"}
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:42:50+08:00"}
{"bytes":1371,"duration_ms":48,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:42:50+08:00"}
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:03+08:00"}
{"bytes":1371,"duration_ms":50,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:03+08:00"}
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:05+08:00"}
{"bytes":1371,"duration_ms":47,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:05+08:00"}
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:06+08:00"}
{"bytes":1371,"duration_ms":46,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:06+08:00"}
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:41+08:00"}
{"bytes":1371,"duration_ms":48,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:43:41+08:00"}
{"bytes":10769,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:44:39+08:00"}
{"bytes":1371,"duration_ms":45,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:44:39+08:00"}
{"bytes":1272,"duration_ms":91,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:44:41+08:00"}
{"bytes":13876,"duration_ms":0,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:44:41+08:00"}
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
{"bytes":13650,"duration_ms":820,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:50:43+08:00"}
{"bytes":1371,"duration_ms":50,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:05+08:00"}
{"bytes":17269,"duration_ms":124,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:05+08:00"}
{"bytes":1272,"duration_ms":106,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:07+08:00"}
{"bytes":13650,"duration_ms":866,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:08+08:00"}
{"bytes":1207,"duration_ms":127,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:46+08:00"}
{"bytes":17269,"duration_ms":198,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:51:46+08:00"}
connecting YMT MySQL: 47.97.27.195:3306 db merketing user root
connecting Marketing MySQL: 192.168.6.92:3306 db market user root
server listening on :8077
{"bytes":14981,"duration_ms":814,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:53:53+08:00"}
{"bytes":1371,"duration_ms":55,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:56:18+08:00"}
{"bytes":19367,"duration_ms":83,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:56:18+08:00"}
{"bytes":1272,"duration_ms":96,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:56:20+08:00"}
{"bytes":14967,"duration_ms":833,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T14:56:21+08:00"}
{"bytes":1371,"duration_ms":53,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T15:00:47+08:00"}
{"bytes":19367,"duration_ms":81,"kind":"access","level":"INFO","method":"","path":"","query":"","remote":"","status":200,"trace_id":"","ts":"2025-11-27T15:00:47+08:00"}

View File

@ -123,7 +123,7 @@
ref="fieldsCascader" ref="fieldsCascader"
v-model="form.fieldsSel" v-model="form.fieldsSel"
:key="form.datasource + '-' + String(form.orderType)" :key="form.datasource + '-' + String(form.orderType)"
:options="fieldOptions" :options="fieldOptionsDynamic"
:props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }" :props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }"
:teleported="false" :teleported="false"
collapse-tags collapse-tags
@ -187,7 +187,7 @@
ref="editFieldsCascader" ref="editFieldsCascader"
v-model="edit.fieldsSel" v-model="edit.fieldsSel"
:key="edit.datasource + '-' + String(edit.orderType)" :key="edit.datasource + '-' + String(edit.orderType)"
:options="editFieldOptions" :options="editFieldOptionsDynamic"
:props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }" :props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }"
:teleported="false" :teleported="false"
collapse-tags collapse-tags

View File

@ -342,6 +342,165 @@ const { createApp, reactive } = Vue;
{ value: 'is_open_db_transaction', label: '是否开启事务' }, { value: 'is_open_db_transaction', label: '是否开启事务' },
{ value: 'bank_tag', label: '银行标识' } { value: 'bank_tag', label: '银行标识' }
] ]
FIELDS_MAP.marketing = {}
FIELDS_MAP.ymt = {}
const metaFM = Vue.ref({})
const loadFieldsMeta = async (ds)=>{
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(state.form.orderType||0)))
const data = await res.json()
const tables = Array.isArray(data?.data?.tables) ? data.data.tables : (Array.isArray(data?.tables)? data.tables: [])
const m = {}
tables.forEach(t=>{
const arr = Array.isArray(t.fields) ? t.fields : []
m[t.table] = arr.map(it=>({ value: it.field, label: it.label }))
})
metaFM.value = m
}catch(_e){ metaFM.value = {} }
}
const FM_OF = (ds)=>{ return Object.keys(metaFM.value||{}).length ? (metaFM.value||{}) : (FIELDS_MAP[ds]||{}) }
const fieldOptionsDynamic = Vue.computed(()=>{
const ds = state.form.datasource
const FM = FM_OF(ds)
const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children })
const fieldsNode = (table)=> (FM[table]||[])
const type = Number(state.form.orderType || 0)
if(ds === 'ymt'){
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
}
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
orderChildrenBase.push(node('order_detail', fieldsNode('order_detail')))
const planChildren = []
planChildren.push(...fieldsNode('plan'))
planChildren.push(node('key_batch', [
...fieldsNode('key_batch'),
node('code_batch', fieldsNode('code_batch'))
]))
const voucherChildren = []
voucherChildren.push(...fieldsNode('order_voucher'))
voucherChildren.push(node('voucher', [
...fieldsNode('voucher'),
node('voucher_batch', fieldsNode('voucher_batch'))
]))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('plan', planChildren))
} else {
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
if(type){ return [ orderNode ] }
return [ { value: 'scene_order', label: '订单数据', children: [ orderNode ] } ]
})
const editFieldOptionsDynamic = Vue.computed(()=>{
const ds = state.edit.datasource
const FM = FM_OF(ds)
const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children })
const fieldsNode = (table)=> (FM[table]||[])
const type = Number(state.edit.orderType || 0)
if(ds === 'ymt'){
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
ch.push(node('merchant', fieldsNode('merchant')))
ch.push(node('activity', fieldsNode('activity')))
if(t===2){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
} else if(t===1){
ch.push(node('order_digit', fieldsNode('order_digit')))
} else if(!t){
ch.push(node('order_voucher', fieldsNode('order_voucher')))
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_digit', fieldsNode('order_digit')))
ch.push(node('goods_voucher_batch', fieldsNode('goods_voucher_batch')))
ch.push(node('goods_voucher_subject_config', fieldsNode('goods_voucher_subject_config')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
return [ orderNode ]
}
const orderChildrenBase = []
orderChildrenBase.push(...fieldsNode('order'))
orderChildrenBase.push(node('order_detail', fieldsNode('order_detail')))
const planChildren = []
planChildren.push(...fieldsNode('plan'))
planChildren.push(node('key_batch', [
...fieldsNode('key_batch'),
node('code_batch', fieldsNode('code_batch'))
]))
const voucherChildren = []
voucherChildren.push(...fieldsNode('order_voucher'))
voucherChildren.push(node('voucher', [
...fieldsNode('voucher'),
node('voucher_batch', fieldsNode('voucher_batch'))
]))
const orderChildrenFor = (t)=>{
const ch = [...orderChildrenBase]
if(t===1){
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
} else if(t===2){
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
} else if(t===3){
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('plan', planChildren))
} else {
ch.push(node('order_cash', fieldsNode('order_cash')))
ch.push(node('order_voucher', voucherChildren))
ch.push(node('plan', planChildren))
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
}
return ch
}
const orderNode = node('order', orderChildrenFor(type))
return [ orderNode ]
})
const TABLE_LABELS = { const TABLE_LABELS = {
order: '订单主表', order: '订单主表',
order_detail: '订单详情', order_detail: '订单详情',
@ -443,30 +602,51 @@ const { createApp, reactive } = Vue;
marketing: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time', marketing: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time',
ymt: 'order_number,creator,out_trade_no,type,status,contract_price,num,pay_amount,create_time' ymt: 'order_number,creator,out_trade_no,type,status,contract_price,num,pay_amount,create_time'
} }
Vue.watch(()=>state.form.datasource, (ds)=>{ const recommendedMeta = Vue.ref([])
Vue.watch(()=>state.form.datasource, async (ds)=>{
state.form.fieldsSel = [] state.form.fieldsSel = []
state.form.fieldsRaw = DEFAULT_FIELDS[ds] || ''
state.form.main_table = (ds==='ymt' ? 'order_info' : 'order') state.form.main_table = (ds==='ymt' ? 'order_info' : 'order')
state.form.orderType = 1 state.form.orderType = 1
await loadFieldsMeta(ds)
recommendedMeta.value = []
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(state.form.orderType||0)))
const data = await res.json()
const rec = Array.isArray(data?.data?.recommended) ? data.data.recommended : (Array.isArray(data?.recommended)? data.recommended: [])
recommendedMeta.value = rec
}catch(_e){ recommendedMeta.value = [] }
const recOrderFields = (recommendedMeta.value||[]).filter(k=>String(k).startsWith('order.')).map(k=>String(k).split('.')[1])
state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || ''))
}) })
Vue.watch(()=>state.form.orderType, ()=>{ Vue.watch(()=>state.form.orderType, async ()=>{
state.form.fieldsSel = [] state.form.fieldsSel = []
// 订单类型变化,刷新推荐字段
const ds = state.form.datasource
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(state.form.orderType||0)))
const data = await res.json()
const rec = Array.isArray(data?.data?.recommended) ? data.data.recommended : (Array.isArray(data?.recommended)? data.recommended: [])
recommendedMeta.value = rec
}catch(_e){ recommendedMeta.value = [] }
const recOrderFields = (recommendedMeta.value||[]).filter(k=>String(k).startsWith('order.')).map(k=>String(k).split('.')[1])
state.form.fieldsRaw = (recOrderFields.length ? recOrderFields.join(',') : (DEFAULT_FIELDS[ds] || ''))
}) })
Vue.watch(()=>state.edit.datasource, (ds)=>{ Vue.watch(()=>state.edit.datasource, async (ds)=>{
state.edit.fieldsSel = [] state.edit.fieldsSel = []
state.edit.main_table = (ds==='ymt' ? 'order_info' : 'order') state.edit.main_table = (ds==='ymt' ? 'order_info' : 'order')
await loadFieldsMeta(ds)
}) })
Vue.watch(()=>state.edit.orderType, ()=>{ Vue.watch(()=>state.edit.orderType, ()=>{
state.edit.fieldsSel = [] state.edit.fieldsSel = []
}) })
const orderLeafPaths = (ds)=>{ const orderLeafPaths = (ds)=>{
const FM = FIELDS_MAP[ds] || {} const FM = FM_OF(ds)
const arr = (FM.order || []).map(f=>['order', f.value]) const arr = (FM.order || []).map(f=>['order', f.value])
return arr return arr
} }
const hasOrderPath = (arr)=> Array.isArray(arr) && arr.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order') const hasOrderPath = (arr)=> Array.isArray(arr) && arr.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order')
const tableKeys = (ds)=> Object.keys(FIELDS_MAP[ds] || {}) const tableKeys = (ds)=> Object.keys(FM_OF(ds) || {})
const isGroupPath = (ds, path)=> Array.isArray(path) && path.length>=1 && tableKeys(ds).includes(path[path.length-1]) const isGroupPath = (ds, path)=> Array.isArray(path) && path.length>=1 && tableKeys(ds).includes(path[path.length-1])
const msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type}); const msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type});
@ -784,7 +964,9 @@ const { createApp, reactive } = Vue;
}) })
} }
} else { } else {
fields = state.form.fieldsRaw.split(',').map(s=>s.trim()).filter(Boolean) const rec = (recommendedMeta.value||[])
if(Array.isArray(rec) && rec.length){ fields = rec }
else { fields = state.form.fieldsRaw.split(',').map(s=>s.trim()).filter(Boolean).map(f=>('order.'+f)) }
} }
const payload = { const payload = {
name: state.form.name, name: state.form.name,
@ -984,8 +1166,9 @@ const { createApp, reactive } = Vue;
}catch(_e){ state.sqlText=''; state.sqlVisible=false; msg('加载SQL失败','error') } }catch(_e){ state.sqlText=''; state.sqlVisible=false; msg('加载SQL失败','error') }
} }
loadTemplates() loadTemplates()
loadFieldsMeta(state.form.datasource)
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, editFieldOptions, sceneOptions, editSceneOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, loadJobs, openJobs, closeJobs, download, openSQL, openEdit, saveEdit, removeTemplate, resizeDialog, createRules, exportRules, editRules, createFormRef, exportFormRef, editFormRef, dsLabel, exportType, isOrder, exportTitle, creatorOptions, resellerOptions, planOptions, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT, fieldsCascader, editFieldsCascader, createCascaderRoot, editCascaderRoot, onCascaderVisible, onFieldsSelChange, hasUserId, currentUserId } return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptionsDynamic, editFieldOptionsDynamic, sceneOptions, editSceneOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, loadJobs, openJobs, closeJobs, download, openSQL, openEdit, saveEdit, removeTemplate, resizeDialog, createRules, exportRules, editRules, createFormRef, exportFormRef, editFormRef, dsLabel, exportType, isOrder, exportTitle, creatorOptions, resellerOptions, planOptions, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT, fieldsCascader, editFieldsCascader, createCascaderRoot, editCascaderRoot, onCascaderVisible, onFieldsSelChange, hasUserId, currentUserId }
} }
}) })
app.use(ElementPlus) app.use(ElementPlus)