Compare commits

...

5 Commits

Author SHA1 Message Date
zhouyonggao 00ebfdd24f feat(api): 修改模板列表查询排序规则为按数据源和ID降序
refactor(web): 重构字段元数据加载逻辑,支持传入订单类型参数
fix(web): 修复订单类型变化时字段加载不更新的问题
feat(web): 新增对order_digit等表的字段路径映射支持
2025-11-27 16:39:06 +08:00
zhouyonggao 5980ee19a8 feat(api): 添加元数据字段接口并优化字段选择逻辑
添加/metadata/fields接口获取字段元数据
重构前端字段选择器使用动态字段选项
优化SQL执行错误日志记录和CORS配置
新增字段推荐功能并根据订单类型动态加载
2025-11-27 16:38:19 +08:00
zhouyonggao a193605e5c fix: 修复服务器日志中重复的监听端口信息 2025-11-27 15:02:04 +08:00
zhouyonggao e78a9836b4 docs(日志): 更新服务器日志文件,添加新的数据库连接记录 2025-11-27 14:21:46 +08:00
zhouyonggao 2ac2d61551 refactor(exporter): 重构SQL构建逻辑以支持多数据源
将SQL构建逻辑重构为基于schema接口的实现,支持不同数据源的字段映射和表连接
修复重复转换行数据的问题
公开Whitelist和FieldLabels函数以供外部调用
2025-11-27 14:21:29 +08:00
16 changed files with 1534 additions and 859 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

@ -18,22 +18,22 @@ import (
) )
type ExportsAPI struct { type ExportsAPI struct {
meta *sql.DB meta *sql.DB
marketing *sql.DB marketing *sql.DB
} }
func ExportsHandler(meta, marketing *sql.DB) http.Handler { func ExportsHandler(meta, marketing *sql.DB) http.Handler {
api := &ExportsAPI{meta: meta, marketing: marketing} api := &ExportsAPI{meta: meta, marketing: marketing}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/api/exports") p := strings.TrimPrefix(r.URL.Path, "/api/exports")
if r.Method == http.MethodPost && p == "" { if r.Method == http.MethodPost && p == "" {
api.create(w, r) api.create(w, r)
return return
} }
if r.Method == http.MethodGet && p == "" { if r.Method == http.MethodGet && p == "" {
api.list(w, r) api.list(w, r)
return return
} }
if strings.HasPrefix(p, "/") { if strings.HasPrefix(p, "/") {
id := strings.TrimPrefix(p, "/") id := strings.TrimPrefix(p, "/")
if r.Method == http.MethodGet && !strings.HasSuffix(p, "/download") { if r.Method == http.MethodGet && !strings.HasSuffix(p, "/download") {
@ -56,27 +56,27 @@ func ExportsHandler(meta, marketing *sql.DB) http.Handler {
return return
} }
} }
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
}) })
} }
func (a *ExportsAPI) ensureOwnerColumn() { func (a *ExportsAPI) ensureOwnerColumn() {
// Try to add owner_id column if not exists; ignore errors // Try to add owner_id column if not exists; ignore errors
_, _ = a.meta.Exec("ALTER TABLE export_jobs ADD COLUMN owner_id BIGINT UNSIGNED NOT NULL DEFAULT 0") _, _ = a.meta.Exec("ALTER TABLE export_jobs ADD COLUMN owner_id BIGINT UNSIGNED NOT NULL DEFAULT 0")
} }
type ExportPayload struct { type ExportPayload struct {
TemplateID uint64 `json:"template_id"` TemplateID uint64 `json:"template_id"`
RequestedBy uint64 `json:"requested_by"` RequestedBy uint64 `json:"requested_by"`
Permission map[string]interface{} `json:"permission"` Permission map[string]interface{} `json:"permission"`
Options map[string]interface{} `json:"options"` Options map[string]interface{} `json:"options"`
FileFormat string `json:"file_format"` FileFormat string `json:"file_format"`
Filters map[string]interface{} `json:"filters"` Filters map[string]interface{} `json:"filters"`
Datasource string `json:"datasource"` Datasource string `json:"datasource"`
} }
func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) { func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
a.ensureOwnerColumn() a.ensureOwnerColumn()
b, _ := io.ReadAll(r.Body) b, _ := io.ReadAll(r.Body)
var p ExportPayload var p ExportPayload
json.Unmarshal(b, &p) json.Unmarshal(b, &p)
@ -96,8 +96,8 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
} }
var fs []string var fs []string
json.Unmarshal(fields, &fs) json.Unmarshal(fields, &fs)
wl := whitelist() wl := Whitelist()
req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: p.Filters} req := exporter.BuildRequest{MainTable: main, Datasource: ds, Fields: fs, Filters: p.Filters}
q, args, err := exporter.BuildSQL(req, wl) q, args, err := exporter.BuildSQL(req, wl)
if err != nil { if err != nil {
r = WithSQL(r, q) r = WithSQL(r, q)
@ -106,39 +106,32 @@ 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 labels := FieldLabels()
break
}
if r.Rows.Valid {
estimate += r.Rows.Int64
}
}
}()
labels := fieldLabels()
hdrs := make([]string, len(fs)) hdrs := make([]string, len(fs))
for i, tf := range fs { for i, tf := range fs {
if v, ok := labels[tf]; ok { if v, ok := labels[tf]; ok {
@ -147,13 +140,15 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
hdrs[i] = tf hdrs[i] = tf
} }
} }
// owner from query userId if provided // owner from query userId if provided
owner := uint64(0) owner := uint64(0)
if uidStr := r.URL.Query().Get("userId"); uidStr != "" { if uidStr := r.URL.Query().Get("userId"); uidStr != "" {
if n, err := strconv.ParseUint(uidStr, 10, 64); err == nil { owner = n } if n, err := strconv.ParseUint(uidStr, 10, 64); err == nil {
} owner = n
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()} }
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(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 {
@ -182,16 +177,16 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
var filtersJSON []byte var filtersJSON []byte
row := a.meta.QueryRow("SELECT template_id, filters_json FROM export_jobs WHERE id=?", id) row := a.meta.QueryRow("SELECT template_id, filters_json FROM export_jobs WHERE id=?", id)
_ = row.Scan(&tplID, &filtersJSON) _ = row.Scan(&tplID, &filtersJSON)
var tplDS string var tplDS string
var main string var main string
var fieldsJSON []byte var fieldsJSON []byte
tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID) tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID)
_ = tr.Scan(&tplDS, &main, &fieldsJSON) _ = tr.Scan(&tplDS, &main, &fieldsJSON)
var fs []string var fs []string
var fl map[string]interface{} var fl map[string]interface{}
json.Unmarshal(fieldsJSON, &fs) json.Unmarshal(fieldsJSON, &fs)
json.Unmarshal(filtersJSON, &fl) json.Unmarshal(filtersJSON, &fl)
wl := whitelist() wl := Whitelist()
var chunks [][2]string var chunks [][2]string
if v, ok := fl["create_time_between"]; ok { if v, ok := fl["create_time_between"]; ok {
if arr, ok2 := v.([]interface{}); ok2 && len(arr) == 2 { if arr, ok2 := v.([]interface{}); ok2 && len(arr) == 2 {
@ -212,7 +207,7 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
var tick int64 var tick int64
for _, rg := range chunks { for _, rg := range chunks {
fl["create_time_between"] = []string{rg[0], rg[1]} fl["create_time_between"] = []string{rg[0], rg[1]}
req := exporter.BuildRequest{MainTable: main, Datasource: tplDS, Fields: fs, Filters: fl} req := exporter.BuildRequest{MainTable: main, Datasource: tplDS, Fields: fs, Filters: fl}
cq, cargs, err := exporter.BuildSQL(req, wl) cq, cargs, err := exporter.BuildSQL(req, wl)
if err != nil { if err != nil {
continue continue
@ -244,9 +239,6 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
} }
} }
vals = transformRow(fs, vals) vals = transformRow(fs, vals)
vals = transformRow(fields, vals)
vals = transformRow(fields, vals)
vals = transformRow(fields, vals)
w.WriteRow(vals) w.WriteRow(vals)
count++ count++
partCount++ partCount++
@ -516,16 +508,16 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
var filtersJSON []byte var filtersJSON []byte
row := a.meta.QueryRow("SELECT template_id, filters_json FROM export_jobs WHERE id=?", id) row := a.meta.QueryRow("SELECT template_id, filters_json FROM export_jobs WHERE id=?", id)
_ = row.Scan(&tplID, &filtersJSON) _ = row.Scan(&tplID, &filtersJSON)
var tplDS string var tplDS string
var main string var main string
var fieldsJSON []byte var fieldsJSON []byte
tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID) tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID)
_ = tr.Scan(&tplDS, &main, &fieldsJSON) _ = tr.Scan(&tplDS, &main, &fieldsJSON)
var fs []string var fs []string
var fl map[string]interface{} var fl map[string]interface{}
json.Unmarshal(fieldsJSON, &fs) json.Unmarshal(fieldsJSON, &fs)
json.Unmarshal(filtersJSON, &fl) json.Unmarshal(filtersJSON, &fl)
wl := whitelist() wl := Whitelist()
var chunks [][2]string var chunks [][2]string
if v, ok := fl["create_time_between"]; ok { if v, ok := fl["create_time_between"]; ok {
if arr, ok2 := v.([]interface{}); ok2 && len(arr) == 2 { if arr, ok2 := v.([]interface{}); ok2 && len(arr) == 2 {
@ -546,7 +538,7 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
var tick int64 var tick int64
for _, rg := range chunks { for _, rg := range chunks {
fl["create_time_between"] = []string{rg[0], rg[1]} fl["create_time_between"] = []string{rg[0], rg[1]}
req := exporter.BuildRequest{MainTable: main, Datasource: tplDS, Fields: fs, Filters: fl} req := exporter.BuildRequest{MainTable: main, Datasource: tplDS, Fields: fs, Filters: fl}
cq, cargs, err := exporter.BuildSQL(req, wl) cq, cargs, err := exporter.BuildSQL(req, wl)
if err != nil { if err != nil {
continue continue
@ -578,10 +570,6 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
} }
} }
vals = transformRow(fs, vals) vals = transformRow(fs, vals)
vals = transformRow(fs, vals)
vals = transformRow(fs, vals)
vals = transformRow(fs, vals)
vals = transformRow(fields, vals)
x.WriteRow(vals) x.WriteRow(vals)
count++ count++
partCount++ partCount++
@ -792,11 +780,11 @@ func (a *ExportsAPI) getSQL(w http.ResponseWriter, r *http.Request, id string) {
fail(w, r, http.StatusNotFound, "not found") fail(w, r, http.StatusNotFound, "not found")
return return
} }
tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID) tr := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id=?", tplID)
var ds string var ds string
var main string var main string
var fields []byte var fields []byte
if err := tr.Scan(&ds, &main, &fields); err != nil { if err := tr.Scan(&ds, &main, &fields); err != nil {
fail(w, r, http.StatusBadRequest, "template not found") fail(w, r, http.StatusBadRequest, "template not found")
return return
} }
@ -804,13 +792,13 @@ func (a *ExportsAPI) getSQL(w http.ResponseWriter, r *http.Request, id string) {
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)
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:
@ -931,10 +919,10 @@ func toString(v interface{}) string {
} }
} }
func (a *ExportsAPI) list(w http.ResponseWriter, r *http.Request) { func (a *ExportsAPI) list(w http.ResponseWriter, r *http.Request) {
a.ensureOwnerColumn() a.ensureOwnerColumn()
q := r.URL.Query() q := r.URL.Query()
page := 1 page := 1
size := 15 size := 15
if p := q.Get("page"); p != "" { if p := q.Get("page"); p != "" {
if n, err := strconv.Atoi(p); err == nil && n > 0 { if n, err := strconv.Atoi(p); err == nil && n > 0 {
page = n page = n
@ -953,44 +941,44 @@ func (a *ExportsAPI) list(w http.ResponseWriter, r *http.Request) {
} }
} }
offset := (page - 1) * size offset := (page - 1) * size
var totalCount int64 var totalCount int64
uidStr := q.Get("userId") uidStr := q.Get("userId")
if tplID > 0 { if tplID > 0 {
if uidStr != "" { if uidStr != "" {
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id = ? AND owner_id = ?", tplID, uidStr) row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id = ? AND owner_id = ?", tplID, uidStr)
_ = row.Scan(&totalCount) _ = row.Scan(&totalCount)
} else { } else {
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id = ?", tplID) row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id = ?", tplID)
_ = row.Scan(&totalCount) _ = row.Scan(&totalCount)
} }
} else { } else {
if uidStr != "" { if uidStr != "" {
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE owner_id = ?", uidStr) row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE owner_id = ?", uidStr)
_ = row.Scan(&totalCount) _ = row.Scan(&totalCount)
} else { } else {
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs") row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs")
_ = row.Scan(&totalCount) _ = row.Scan(&totalCount)
} }
} }
var rows *sql.Rows var rows *sql.Rows
var err error var err error
if tplID > 0 { if tplID > 0 {
if uidStr != "" { if uidStr != "" {
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 WHERE template_id = ? AND owner_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", tplID, uidStr, 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 WHERE template_id = ? AND owner_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", tplID, uidStr, size, offset)
} else { } else {
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 WHERE template_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", tplID, 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 WHERE template_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", tplID, size, offset)
} }
} else { } else {
if uidStr != "" { if uidStr != "" {
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 WHERE owner_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", uidStr, 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 WHERE owner_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", uidStr, size, offset)
} else { } else {
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 {
fail(w, r, http.StatusInternalServerError, err.Error())
return
} }
if err != nil {
failCat(w, r, http.StatusInternalServerError, err.Error(), "explain_error")
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 datasource ASC, id 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

@ -1,10 +1,12 @@
package exporter package exporter
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"strconv" "fmt"
"strings" "marketing-system-data-tool/server/internal/schema"
"strconv"
"strings"
) )
type BuildRequest struct { type BuildRequest struct {
@ -18,69 +20,66 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if req.MainTable != "order" && req.MainTable != "order_info" { if req.MainTable != "order" && req.MainTable != "order_info" {
return "", nil, errors.New("unsupported main table") return "", nil, errors.New("unsupported main table")
} }
cols := []string{} sch := schema.Get(req.Datasource, req.MainTable)
need := map[string]bool{} cols := []string{}
for _, tf := range req.Fields { need := map[string]bool{}
if !whitelist[tf] { for _, tf := range req.Fields {
return "", nil, errors.New("field not allowed") if !whitelist[tf] {
} return "", nil, errors.New("field not allowed")
parts := strings.Split(tf, ".") }
if len(parts) != 2 { parts := strings.Split(tf, ".")
return "", nil, errors.New("invalid field format") if len(parts) != 2 {
} return "", nil, errors.New("invalid field format")
t, f := parts[0], parts[1] }
need[t] = true t, f := parts[0], parts[1]
if t == "order" { need[t] = true
if req.MainTable == "order_info" { mt := sch.TableName(t)
switch f { mf, _ := sch.MapField(t, f)
case "order_number": f = "order_no" if t == "order" && req.MainTable == "order" {
case "key": f = "key_code" if f == "status" {
case "creator": f = "user_id" cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS type")
case "out_trade_no": f = "out_order_no" cols = append(cols, "CASE `order`.status WHEN 0 THEN '待充值' WHEN 1 THEN '充值中' WHEN 2 THEN '已完成' WHEN 3 THEN '充值失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '待支付' END AS status")
case "plan_id": f = "activity_id"
case "reseller_id": f = "merchant_id"
case "product_id": f = "goods_id"
case "pay_amount": f = "pay_price"
case "key_batch_id": f = "key_batch_name"
}
cols = append(cols, "`order_info`."+escape(f))
continue continue
} }
if f == "status" { if f == "type" {
cols = append(cols, "CASE `order`.type " +
"WHEN 1 THEN (CASE `order`.status WHEN 0 THEN '待充值' WHEN 1 THEN '充值中' WHEN 2 THEN '已完成' WHEN 3 THEN '充值失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '待支付' END) " +
"WHEN 2 THEN (CASE `order`.status WHEN 0 THEN '待领取' WHEN 1 THEN '待领取' WHEN 2 THEN '已领取' WHEN 3 THEN '领取失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '待支付' END) " +
"WHEN 3 THEN (CASE `order`.status WHEN 0 THEN '待领取' WHEN 1 THEN '待领取' WHEN 2 THEN '已核销' WHEN 3 THEN '领取失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '' END) " +
"ELSE (CASE `order`.status WHEN 0 THEN '待充值' WHEN 1 THEN '充值中' WHEN 2 THEN '已完成' WHEN 3 THEN '充值失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '待支付' END) END AS status")
} else if f == "type" {
cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS type") cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS type")
} else if f == "type" { continue
cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS type") }
} else if f == "pay_type" { if f == "pay_type" {
cols = append(cols, "CASE `order`.pay_type WHEN 1 THEN '支付宝' WHEN 5 THEN '微信' ELSE '' END AS pay_type") cols = append(cols, "CASE `order`.pay_type WHEN 1 THEN '支付宝' WHEN 5 THEN '微信' ELSE '' END AS pay_type")
} else if f == "pay_status" { continue
cols = append(cols, "CASE `order`.pay_status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已退款' ELSE '' END AS pay_status") }
} else { if f == "pay_status" {
cols = append(cols, "`order`."+escape(f)) cols = append(cols, "CASE `order`.pay_status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已退款' ELSE '' END AS pay_status")
} continue
} else {
if t == "order_cash" && f == "receive_status" {
cols = append(cols, "CASE `order_cash`.receive_status WHEN 0 THEN '待领取' WHEN 1 THEN '领取中' WHEN 2 THEN '领取成功' WHEN 3 THEN '领取失败' ELSE '' END AS receive_status")
} else if t == "order_cash" && f == "channel" {
cols = append(cols, "CASE `order_cash`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' WHEN 3 THEN '云闪付' ELSE '' END AS channel")
} else if t == "order_voucher" && f == "channel" {
cols = append(cols, "CASE `order_voucher`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' ELSE '' END AS channel")
} else if t == "order_voucher" && f == "status" {
cols = append(cols, "CASE `order_voucher`.status WHEN 1 THEN '可用' WHEN 2 THEN '已实扣' WHEN 3 THEN '已过期' WHEN 4 THEN '已退款' WHEN 5 THEN '领取失败' WHEN 6 THEN '发放中' WHEN 7 THEN '部分退款' WHEN 8 THEN '已退回' WHEN 9 THEN '发放失败' ELSE '' END AS status")
} else if t == "order_voucher" && f == "receive_mode" {
cols = append(cols, "CASE `order_voucher`.receive_mode WHEN 1 THEN '渠道授权用户id' WHEN 2 THEN '手机号或邮箱' ELSE '' END AS receive_mode")
} else if t == "order_voucher" && f == "out_biz_no" {
cols = append(cols, "'' AS out_biz_no")
} else {
cols = append(cols, "`"+t+"`."+escape(f))
} }
} }
} if t == "order_cash" && f == "receive_status" {
cols = append(cols, "CASE `order_cash`.receive_status WHEN 0 THEN '待领取' WHEN 1 THEN '领取中' WHEN 2 THEN '领取成功' WHEN 3 THEN '领取失败' ELSE '' END AS receive_status")
continue
}
if t == "order_cash" && f == "channel" {
cols = append(cols, "CASE `order_cash`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' WHEN 3 THEN '云闪付' ELSE '' END AS channel")
continue
}
if t == "order_voucher" && f == "channel" {
cols = append(cols, "CASE `order_voucher`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' ELSE '' END AS channel")
continue
}
if t == "order_voucher" && f == "status" {
cols = append(cols, "CASE `order_voucher`.status WHEN 1 THEN '可用' WHEN 2 THEN '已实扣' WHEN 3 THEN '已过期' WHEN 4 THEN '已退款' WHEN 5 THEN '领取失败' WHEN 6 THEN '发放中' WHEN 7 THEN '部分退款' WHEN 8 THEN '已退回' WHEN 9 THEN '发放失败' ELSE '' END AS status")
continue
}
if t == "order_voucher" && f == "receive_mode" {
cols = append(cols, "CASE `order_voucher`.receive_mode WHEN 1 THEN '渠道授权用户id' WHEN 2 THEN '手机号或邮箱' ELSE '' END AS receive_mode")
continue
}
if t == "order_voucher" && f == "out_biz_no" {
cols = append(cols, "'' AS out_biz_no")
continue
}
cols = append(cols, "`"+mt+"`."+escape(mf))
}
if len(cols) == 0 { if len(cols) == 0 {
return "", nil, errors.New("no fields") return "", nil, errors.New("no fields")
} }
@ -88,67 +87,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
sb.WriteString("SELECT ") sb.WriteString("SELECT ")
sb.WriteString(strings.Join(cols, ",")) sb.WriteString(strings.Join(cols, ","))
sb.WriteString(" FROM `" + req.MainTable + "`") sb.WriteString(" FROM `" + req.MainTable + "`")
// JOINs based on need for _, j := range sch.BuildJoins(need, req.MainTable) {
// order_detail sb.WriteString(j)
if need["order_detail"] && req.MainTable == "order" {
sb.WriteString(" LEFT JOIN `order_detail` ON `order_detail`.order_number = `order`.order_number")
}
// order_cash
if need["order_cash"] {
if req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `order_cash` ON `order_cash`.order_no = `order_info`.order_no")
} else {
sb.WriteString(" LEFT JOIN `order_cash` ON `order_cash`.order_number = `order`.order_number")
}
}
// order_voucher
if need["order_voucher"] {
if req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `order_voucher` ON `order_voucher`.order_no = `order_info`.order_no")
} else {
sb.WriteString(" LEFT JOIN `order_voucher` ON `order_voucher`.order_number = `order`.order_number")
}
}
// order_digit (ymt only)
if need["order_digit"] && req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `order_digit` ON `order_digit`.order_no = `order_info`.order_no")
}
// goods_voucher_batch (ymt)
if need["goods_voucher_batch"] && req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `goods_voucher_batch` ON `goods_voucher_batch`.channel_batch_no = `order_voucher`.channel_batch_no")
}
// goods_voucher_subject_config (ymt)
if need["goods_voucher_subject_config"] && req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `goods_voucher_subject_config` ON `goods_voucher_subject_config`.id = `goods_voucher_batch`.voucher_subject_id")
}
// merchant (ymt)
if need["merchant"] && req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `merchant` ON `merchant`.id = `order_info`.merchant_id")
}
// activity (ymt)
if need["activity"] && req.MainTable == "order_info" {
sb.WriteString(" LEFT JOIN `activity` ON `activity`.id = `order_info`.activity_id")
}
// plan
if req.MainTable == "order" {
if need["plan"] || need["key_batch"] {
sb.WriteString(" LEFT JOIN `plan` ON `plan`.id = `order`.plan_id")
}
if need["key_batch"] {
sb.WriteString(" LEFT JOIN `key_batch` ON `key_batch`.plan_id = `plan`.id")
}
if need["code_batch"] {
sb.WriteString(" LEFT JOIN `code_batch` ON `code_batch`.key_batch_id = `key_batch`.id")
}
if need["voucher"] {
sb.WriteString(" LEFT JOIN `voucher` ON `voucher`.channel_activity_id = `order_voucher`.channel_activity_id")
}
if need["voucher_batch"] {
sb.WriteString(" LEFT JOIN `voucher_batch` ON `voucher_batch`.voucher_id = `voucher`.id")
}
if need["merchant_key_send"] {
sb.WriteString(" LEFT JOIN `merchant_key_send` ON `order`." + escape("key") + " = `merchant_key_send`.key")
}
} }
args := []interface{}{} args := []interface{}{}
@ -185,12 +125,10 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if len(ids) == 0 { if len(ids) == 0 {
return "", nil, errors.New("creator_in required") return "", nil, errors.New("creator_in required")
} }
ph := strings.Repeat("?,", len(ids)) ph := strings.Repeat("?,", len(ids))
ph = strings.TrimSuffix(ph, ",") ph = strings.TrimSuffix(ph, ",")
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("creator_in"); ok {
where = append(where, "`order_info`.user_id IN ("+ph+")") where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(tbl), escape(col), ph))
} else {
where = append(where, "`order`.creator IN ("+ph+")")
} }
args = append(args, ids...) args = append(args, ids...)
} }
@ -201,10 +139,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if len(arr) != 2 { if len(arr) != 2 {
return "", nil, errors.New("create_time_between requires 2 values") return "", nil, errors.New("create_time_between requires 2 values")
} }
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("create_time_between"); ok {
where = append(where, "`order_info`.create_time BETWEEN ? AND ?") where = append(where, fmt.Sprintf("`%s`.%s BETWEEN ? AND ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.create_time BETWEEN ? AND ?")
} }
args = append(args, arr[0], arr[1]) args = append(args, arr[0], arr[1])
} }
@ -226,10 +162,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
} }
} }
if tv == 1 || tv == 2 || tv == 3 { if tv == 1 || tv == 2 || tv == 3 {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("type_eq"); ok {
where = append(where, "`order_info`.type = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.type = ?")
} }
args = append(args, tv) args = append(args, tv)
} }
@ -237,10 +171,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["out_trade_no_eq"]; ok { if v, ok := req.Filters["out_trade_no_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("out_trade_no_eq"); ok {
where = append(where, "`order_info`.out_order_no = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.out_trade_no = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -248,10 +180,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["account_eq"]; ok { if v, ok := req.Filters["account_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("account_eq"); ok {
where = append(where, "`order_info`.account = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.account = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -259,10 +189,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["plan_id_eq"]; ok { if v, ok := req.Filters["plan_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("plan_id_eq"); ok {
where = append(where, "`order_info`.activity_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.plan_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -270,10 +198,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["key_batch_id_eq"]; ok { if v, ok := req.Filters["key_batch_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("key_batch_id_eq"); ok {
where = append(where, "`order_info`.key_batch_name = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.key_batch_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -281,10 +207,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["product_id_eq"]; ok { if v, ok := req.Filters["product_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("product_id_eq"); ok {
where = append(where, "`order_info`.goods_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.product_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -292,10 +216,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["reseller_id_eq"]; ok { if v, ok := req.Filters["reseller_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("reseller_id_eq"); ok {
where = append(where, "`order_info`.merchant_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.reseller_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -303,10 +225,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["code_batch_id_eq"]; ok { if v, ok := req.Filters["code_batch_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("code_batch_id_eq"); ok {
where = append(where, "`order_info`.supplier_product_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order`.code_batch_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -314,10 +234,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok { if v, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("order_cash_cash_activity_id_eq"); ok {
where = append(where, "`order_cash`.activity_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order_cash`.cash_activity_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -325,10 +243,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok { if v, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order_info" { if tbl, col, ok := sch.FilterColumn("order_voucher_channel_activity_id_eq"); ok {
where = append(where, "`order_voucher`.channel_batch_no = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} else {
where = append(where, "`order_voucher`.channel_activity_id = ?")
} }
args = append(args, s) args = append(args, s)
} }
@ -336,8 +252,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok { if v, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order" { // only marketing schema has voucher_batch if tbl, col, ok := sch.FilterColumn("voucher_batch_channel_activity_id_eq"); ok {
where = append(where, "`voucher_batch`.channel_activity_id = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
args = append(args, s) args = append(args, s)
} }
} }
@ -345,8 +261,8 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
if v, ok := req.Filters["merchant_out_biz_no_eq"]; ok { if v, ok := req.Filters["merchant_out_biz_no_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { if s != "" {
if req.MainTable == "order" { // marketing only if tbl, col, ok := sch.FilterColumn("merchant_out_biz_no_eq"); ok {
where = append(where, "`merchant_key_send`.out_biz_no = ?") where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
} }
args = append(args, s) args = append(args, s)
} }

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

@ -0,0 +1,60 @@
package schema
type marketingSchema struct{}
func (marketingSchema) TableName(t string) string { return t }
func (marketingSchema) MapField(t, f string) (string, bool) { return f, true }
func (marketingSchema) BuildJoins(need map[string]bool, main string) []string {
out := []string{}
if need["order_detail"] {
out = append(out, " LEFT JOIN `order_detail` ON `order_detail`.order_number = `order`.order_number")
}
if need["order_cash"] {
out = append(out, " LEFT JOIN `order_cash` ON `order_cash`.order_number = `order`.order_number")
}
if need["order_voucher"] {
out = append(out, " LEFT JOIN `order_voucher` ON `order_voucher`.order_number = `order`.order_number")
}
if need["plan"] || need["key_batch"] {
out = append(out, " LEFT JOIN `plan` ON `plan`.id = `order`.plan_id")
}
if need["key_batch"] {
out = append(out, " LEFT JOIN `key_batch` ON `key_batch`.plan_id = `plan`.id")
}
if need["code_batch"] {
out = append(out, " LEFT JOIN `code_batch` ON `code_batch`.key_batch_id = `key_batch`.id")
}
if need["voucher"] {
out = append(out, " LEFT JOIN `voucher` ON `voucher`.channel_activity_id = `order_voucher`.channel_activity_id")
}
if need["voucher_batch"] {
out = append(out, " LEFT JOIN `voucher_batch` ON `voucher_batch`.voucher_id = `voucher`.id")
}
if need["merchant_key_send"] {
out = append(out, " LEFT JOIN `merchant_key_send` ON `order`.`key` = `merchant_key_send`.key")
}
return out
}
func (marketingSchema) FilterColumn(key string) (string, string, bool) {
switch key {
case "creator_in": return "order", "creator", true
case "create_time_between": return "order", "create_time", true
case "type_eq": return "order", "type", true
case "out_trade_no_eq": return "order", "out_trade_no", true
case "account_eq": return "order", "account", true
case "plan_id_eq": return "order", "plan_id", true
case "key_batch_id_eq": return "order", "key_batch_id", true
case "product_id_eq": return "order", "product_id", true
case "reseller_id_eq": return "order", "reseller_id", true
case "code_batch_id_eq": return "order", "code_batch_id", true
case "order_cash_cash_activity_id_eq": return "order_cash", "cash_activity_id", true
case "order_voucher_channel_activity_id_eq": return "order_voucher", "channel_activity_id", true
case "voucher_batch_channel_activity_id_eq": return "voucher_batch", "channel_activity_id", true
case "merchant_out_biz_no_eq": return "merchant_key_send", "out_biz_no", true
default:
return "", "", false
}
}

View File

@ -0,0 +1,15 @@
package schema
type Schema interface {
TableName(string) string
MapField(string, string) (string, bool)
BuildJoins(map[string]bool, string) []string
FilterColumn(string) (string, string, bool)
}
func Get(datasource string, main string) Schema {
if datasource == "ymt" || main == "order_info" {
return ymtSchema{}
}
return marketingSchema{}
}

View File

@ -0,0 +1,74 @@
package schema
type ymtSchema struct{}
func (ymtSchema) TableName(t string) string {
if t == "order" {
return "order_info"
}
return t
}
func (s ymtSchema) MapField(t, f string) (string, bool) {
if t == "order" {
switch f {
case "order_number": return "order_no", true
case "key": return "key_code", true
case "creator": return "user_id", true
case "out_trade_no": return "out_order_no", true
case "plan_id": return "activity_id", true
case "reseller_id": return "merchant_id", true
case "product_id": return "goods_id", true
case "pay_amount": return "pay_price", true
case "key_batch_id": return "key_batch_name", true
default:
return f, true
}
}
return f, true
}
func (s ymtSchema) BuildJoins(need map[string]bool, main string) []string {
out := []string{}
if need["order_cash"] {
out = append(out, " LEFT JOIN `order_cash` ON `order_cash`.order_no = `order_info`.order_no")
}
if need["order_voucher"] {
out = append(out, " LEFT JOIN `order_voucher` ON `order_voucher`.order_no = `order_info`.order_no")
}
if need["order_digit"] {
out = append(out, " LEFT JOIN `order_digit` ON `order_digit`.order_no = `order_info`.order_no")
}
if need["goods_voucher_batch"] {
out = append(out, " LEFT JOIN `goods_voucher_batch` ON `goods_voucher_batch`.channel_batch_no = `order_voucher`.channel_batch_no")
}
if need["goods_voucher_subject_config"] {
out = append(out, " LEFT JOIN `goods_voucher_subject_config` ON `goods_voucher_subject_config`.id = `goods_voucher_batch`.voucher_subject_id")
}
if need["merchant"] {
out = append(out, " LEFT JOIN `merchant` ON `merchant`.id = `order_info`.merchant_id")
}
if need["activity"] {
out = append(out, " LEFT JOIN `activity` ON `activity`.id = `order_info`.activity_id")
}
return out
}
func (s ymtSchema) FilterColumn(key string) (string, string, bool) {
switch key {
case "creator_in": return "order", "user_id", true
case "create_time_between": return "order", "create_time", true
case "type_eq": return "order", "type", true
case "out_trade_no_eq": return "order", "out_order_no", true
case "account_eq": return "order", "account", true
case "plan_id_eq": return "order", "activity_id", true
case "key_batch_id_eq": return "order", "key_batch_name", true
case "product_id_eq": return "order", "goods_id", true
case "reseller_id_eq": return "order", "merchant_id", true
case "code_batch_id_eq": return "order", "supplier_product_id", true
case "order_cash_cash_activity_id_eq": return "order_cash", "activity_id", true
case "order_voucher_channel_activity_id_eq": return "order_voucher", "channel_batch_no", true
default:
return "", "", false
}
}

File diff suppressed because one or more lines are too long

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, type)=>{
try{
const res = await fetch(API_BASE + '/api/metadata/fields?datasource=' + encodeURIComponent(ds) + '&order_type=' + encodeURIComponent(String(type||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,53 @@ 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, state.form.orderType)
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
await loadFieldsMeta(ds, state.form.orderType)
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, state.edit.orderType)
}) })
Vue.watch(()=>state.edit.orderType, ()=>{ Vue.watch(()=>state.edit.orderType, async ()=>{
state.edit.fieldsSel = [] state.edit.fieldsSel = []
await loadFieldsMeta(state.edit.datasource, state.edit.orderType)
}) })
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 +966,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,
@ -894,6 +1078,7 @@ const { createApp, reactive } = Vue;
} else { } else {
state.edit.orderType = 1 state.edit.orderType = 1
} }
await loadFieldsMeta(state.edit.datasource, state.edit.orderType)
const fields = Array.isArray(tpl.fields) ? tpl.fields : [] const fields = Array.isArray(tpl.fields) ? tpl.fields : []
const toPath = (tf)=>{ const toPath = (tf)=>{
const parts = String(tf||'').split('.') const parts = String(tf||'').split('.')
@ -910,6 +1095,11 @@ const { createApp, reactive } = Vue;
if(table==='voucher_batch') return ['order','order_voucher','voucher','voucher_batch',field] if(table==='voucher_batch') return ['order','order_voucher','voucher','voucher_batch',field]
if(table==='merchant_key_send') return ['order','merchant_key_send',field] if(table==='merchant_key_send') return ['order','merchant_key_send',field]
if(table==='order_cash') return ['order','order_cash',field] if(table==='order_cash') return ['order','order_cash',field]
if(table==='order_digit') return ['order','order_digit',field]
if(table==='goods_voucher_batch') return ['order','goods_voucher_batch',field]
if(table==='goods_voucher_subject_config') return ['order','goods_voucher_subject_config',field]
if(table==='merchant') return ['order','merchant',field]
if(table==='activity') return ['order','activity',field]
return null return null
} }
state.edit.fieldsSel = fields.map(toPath).filter(p=>Array.isArray(p) && p.length>=2) state.edit.fieldsSel = fields.map(toPath).filter(p=>Array.isArray(p) && p.length>=2)
@ -984,8 +1174,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, state.form.orderType)
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)