diff --git a/server/bin/marketing-data-server b/server/bin/marketing-data-server index e88ad40..98b6ba2 100755 Binary files a/server/bin/marketing-data-server and b/server/bin/marketing-data-server differ diff --git a/server/internal/api/access.go b/server/internal/api/access.go index 19c680b..3c71ed7 100644 --- a/server/internal/api/access.go +++ b/server/internal/api/access.go @@ -25,6 +25,13 @@ func (w *statusWriter) Write(b []byte)(int, error){ func withAccess(h http.Handler) http.Handler { 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() sw := &statusWriter{ResponseWriter: w, status: 200} h.ServeHTTP(sw, r) @@ -43,4 +50,3 @@ func withAccess(h http.Handler) http.Handler { }) }) } - diff --git a/server/internal/api/exports.go b/server/internal/api/exports.go index 397b758..7da54ca 100644 --- a/server/internal/api/exports.go +++ b/server/internal/api/exports.go @@ -106,38 +106,31 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) { } r = WithSQL(r, q) dataDB := a.selectDataDB(ds) - expRows, score, err := exporter.RunExplain(dataDB, q, args) - if err != nil { - fail(w, r, http.StatusBadRequest, err.Error()) - return - } + score, sugg, err := exporter.EvaluateExplain(dataDB, q, args) + if err != nil { + fail(w, r, http.StatusBadRequest, err.Error()) + return + } + sugg = append(sugg, exporter.IndexSuggestions(req)...) const passThreshold = 60 if score < passThreshold { fail(w, r, http.StatusBadRequest, fmt.Sprintf("EXPLAIN 未通过:评分=%d,请优化索引或缩小查询范围", score)) return } - var estimate int64 - func() { - idx := strings.Index(q, " FROM ") - if idx > 0 { - cq := "SELECT COUNT(1)" + q[idx:] - row := dataDB.QueryRow(cq, args...) - var cnt int64 - if err := row.Scan(&cnt); err == nil { - estimate = cnt - return - } - } - for _, r := range expRows { - if r.Table.Valid && r.Table.String == "order" && r.Rows.Valid { - estimate = r.Rows.Int64 - break - } - if r.Rows.Valid { - estimate += r.Rows.Int64 - } - } - }() + var estimate int64 + func() { + idx := strings.Index(q, " FROM ") + if idx > 0 { + cq := "SELECT COUNT(1)" + q[idx:] + row := dataDB.QueryRow(cq, args...) + var cnt int64 + if err := row.Scan(&cnt); err == nil { + estimate = cnt + return + } + } + estimate = 0 + }() labels := FieldLabels() hdrs := make([]string, len(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 (?,?,?,?,?,?,?,?,?,?,?,?,?)" - 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) res, err := a.meta.Exec(ejSQL, ejArgs...) if err != nil { @@ -801,11 +794,11 @@ func (a *ExportsAPI) getSQL(w http.ResponseWriter, r *http.Request, id string) { json.Unmarshal(filters, &fl) wl := Whitelist() req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: fl} - q, args, err := exporter.BuildSQL(req, wl) - if err != nil { - fail(w, r, http.StatusBadRequest, err.Error()) - return - } + q, args, err := exporter.BuildSQL(req, wl) + if err != nil { + failCat(w, r, http.StatusBadRequest, err.Error(), "sql_build_error") + return + } formatArg := func(a interface{}) string { switch t := a.(type) { 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) } } - if err != nil { - fail(w, r, http.StatusInternalServerError, err.Error()) - return - } + if err != nil { + failCat(w, r, http.StatusInternalServerError, err.Error(), "explain_error") + return + } defer rows.Close() items := []map[string]interface{}{} for rows.Next() { diff --git a/server/internal/api/metadata.go b/server/internal/api/metadata.go new file mode 100644 index 0000000..d0a575a --- /dev/null +++ b/server/internal/api/metadata.go @@ -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 +} diff --git a/server/internal/api/response.go b/server/internal/api/response.go index c9a916d..7d22367 100644 --- a/server/internal/api/response.go +++ b/server/internal/api/response.go @@ -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) { 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) +} diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 83b48ef..31d2e22 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -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/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/resellers", withAccess(withTrace(ResellersHandler(marketingDB)))) diff --git a/server/internal/api/templates.go b/server/internal/api/templates.go index 2e8debf..bcaf8d9 100644 --- a/server/internal/api/templates.go +++ b/server/internal/api/templates.go @@ -1,14 +1,16 @@ package api import ( - "database/sql" - "encoding/json" - "io" - "log" - "net/http" - "strings" - "time" - "fmt" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "marketing-system-data-tool/server/internal/exporter" + "marketing-system-data-tool/server/internal/schema" + "net/http" + "strings" + "time" ) type TemplatesAPI struct { @@ -64,21 +66,23 @@ type TemplatePayload struct { } func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) { - b, _ := io.ReadAll(r.Body) - var p TemplatePayload - json.Unmarshal(b, &p) - r = WithPayload(r, p) - uidStr := r.URL.Query().Get("userId") - if uidStr != "" { - var uid uint64 - _, _ = fmt.Sscan(uidStr, &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} - log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs) - _, err := a.meta.Exec(tplSQL, tplArgs...) + b, _ := io.ReadAll(r.Body) + var p TemplatePayload + json.Unmarshal(b, &p) + r = WithPayload(r, p) + uidStr := r.URL.Query().Get("userId") + if uidStr != "" { + var uid uint64 + _, _ = fmt.Sscan(uidStr, &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} + log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs) + _, err := a.meta.Exec(tplSQL, tplArgs...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) 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) { - 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{}{} - if uidStr != "" { - sqlText += " WHERE owner_id IN (0, ?)" - args = append(args, uidStr) - } - sqlText += " ORDER BY updated_at DESC LIMIT 200" - rows, err := a.meta.Query(sqlText, args...) - if err != nil { - fail(w, r, http.StatusInternalServerError, err.Error()) - return - } - defer rows.Close() - out := []map[string]interface{}{} - for rows.Next() { - var id uint64 - var name, datasource, mainTable, fileFormat, visibility string - var ownerID uint64 - 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) - if err != nil { - fail(w, r, http.StatusInternalServerError, err.Error()) - 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} - out = append(out, m) - } - ok(w, r, out) + 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{}{} + if uidStr != "" { + sqlText += " WHERE owner_id IN (0, ?)" + args = append(args, uidStr) + } + sqlText += " ORDER BY updated_at DESC LIMIT 200" + rows, err := a.meta.Query(sqlText, args...) + if err != nil { + fail(w, r, http.StatusInternalServerError, err.Error()) + return + } + defer rows.Close() + out := []map[string]interface{}{} + for rows.Next() { + var id uint64 + var name, datasource, mainTable, fileFormat, visibility string + var ownerID uint64 + 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) + if err != nil { + fail(w, r, http.StatusInternalServerError, err.Error()) + 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} + out = append(out, m) + } + ok(w, r, out) } 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) { - b, _ := io.ReadAll(r.Body) - var p map[string]interface{} - json.Unmarshal(b, &p) - set := []string{} - args := []interface{}{} - for k, v := range p { - switch k { - case "name", "visibility", "file_format": - set = append(set, k+"=?") - args = append(args, v) - case "fields": - set = append(set, "fields_json=?") - args = append(args, toJSON(v)) - case "filters": - set = append(set, "filters_json=?") - args = append(args, toJSON(v)) - case "enabled": - set = append(set, "enabled=?") - if v.(bool) { - args = append(args, 1) - } else { - args = append(args, 0) - } - } - } - if len(set) == 0 { - fail(w, r, http.StatusBadRequest, "no patch") - return - } - // ensure updated_at - set = append(set, "updated_at=?") - args = append(args, time.Now(), id) - _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) - if err != nil { - fail(w, r, http.StatusInternalServerError, err.Error()) - return - } - ok(w, r, nil) + b, _ := io.ReadAll(r.Body) + var p map[string]interface{} + json.Unmarshal(b, &p) + set := []string{} + args := []interface{}{} + for k, v := range p { + switch k { + case "name", "visibility", "file_format": + set = append(set, k+"=?") + args = append(args, v) + case "fields": + set = append(set, "fields_json=?") + args = append(args, toJSON(v)) + case "filters": + set = append(set, "filters_json=?") + args = append(args, toJSON(v)) + case "enabled": + set = append(set, "enabled=?") + if v.(bool) { + args = append(args, 1) + } else { + args = append(args, 0) + } + } + } + if len(set) == 0 { + fail(w, r, http.StatusBadRequest, "no patch") + return + } + // ensure updated_at + set = append(set, "updated_at=?") + args = append(args, time.Now(), id) + _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) + if err != nil { + fail(w, r, http.StatusInternalServerError, err.Error()) + return + } + ok(w, r, nil) } 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) { - 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 fields, filters []byte - err := row.Scan(&main, &fields, &filters) + err := row.Scan(&ds, &main, &fields, &filters) if err != nil { fail(w, r, http.StatusNotFound, "not found") return @@ -224,7 +229,30 @@ func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, var fl map[string]interface{} json.Unmarshal(fields, &fs) 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 { @@ -238,445 +266,8 @@ func fromJSON(b []byte) interface{} { return v } -func Whitelist() 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.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 Whitelist() map[string]bool { return schema.AllWhitelist() } func FieldLabels() 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": "银行标识", - } + return schema.AllLabels() } diff --git a/server/internal/exporter/evaluate.go b/server/internal/exporter/evaluate.go new file mode 100644 index 0000000..2ff6bd4 --- /dev/null +++ b/server/internal/exporter/evaluate.go @@ -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 +} diff --git a/server/internal/schema/fields.go b/server/internal/schema/fields.go new file mode 100644 index 0000000..9c14387 --- /dev/null +++ b/server/internal/schema/fields.go @@ -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", + } +} diff --git a/server/log/server-20251127.log b/server/log/server-20251127.log index 1bd76d9..301f288 100644 --- a/server/log/server-20251127.log +++ b/server/log/server-20251127.log @@ -76,3 +76,63 @@ 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":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"} diff --git a/web/index.html b/web/index.html index 286dd19..65b934d 100644 --- a/web/index.html +++ b/web/index.html @@ -123,7 +123,7 @@ ref="fieldsCascader" v-model="form.fieldsSel" :key="form.datasource + '-' + String(form.orderType)" - :options="fieldOptions" + :options="fieldOptionsDynamic" :props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }" :teleported="false" collapse-tags @@ -187,7 +187,7 @@ ref="editFieldsCascader" v-model="edit.fieldsSel" :key="edit.datasource + '-' + String(edit.orderType)" - :options="editFieldOptions" + :options="editFieldOptionsDynamic" :props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }" :teleported="false" collapse-tags diff --git a/web/main.js b/web/main.js index fc601a7..71f1d5b 100644 --- a/web/main.js +++ b/web/main.js @@ -342,6 +342,165 @@ const { createApp, reactive } = Vue; { value: 'is_open_db_transaction', 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 = { order: '订单主表', 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', 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.fieldsRaw = DEFAULT_FIELDS[ds] || '' state.form.main_table = (ds==='ymt' ? 'order_info' : 'order') 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 = [] + // 订单类型变化,刷新推荐字段 + 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.main_table = (ds==='ymt' ? 'order_info' : 'order') + await loadFieldsMeta(ds) }) Vue.watch(()=>state.edit.orderType, ()=>{ state.edit.fieldsSel = [] }) const orderLeafPaths = (ds)=>{ - const FM = FIELDS_MAP[ds] || {} + const FM = FM_OF(ds) const arr = (FM.order || []).map(f=>['order', f.value]) return arr } 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 msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type}); @@ -784,7 +964,9 @@ const { createApp, reactive } = Vue; }) } } 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 = { name: state.form.name, @@ -984,8 +1166,9 @@ const { createApp, reactive } = Vue; }catch(_e){ state.sqlText=''; state.sqlVisible=false; msg('加载SQL失败','error') } } 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)