feat: 增强导出工具功能并优化界面
- 在导出工具中新增模板删除功能 - 重构SQL构建器以支持多表关联查询 - 扩展字段白名单配置,支持更多业务字段 - 优化前端界面,新增模板编辑和导出配置对话框 - 改进静态文件路径查找逻辑 - 为导出任务添加执行分析和过滤条件支持
This commit is contained in:
parent
dde8bdb05f
commit
d0e1488815
|
|
@ -1,16 +1,15 @@
|
||||||
{
|
{
|
||||||
"tables": ["order"],
|
"tables": ["order"],
|
||||||
"fields": [
|
"fields": [
|
||||||
"order.order_number",
|
"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","order.update_time",
|
||||||
"order.creator",
|
"order_detail.plan_title","order_detail.reseller_name","order_detail.product_name","order_detail.show_url","order_detail.official_price","order_detail.cost_price","order_detail.create_time","order_detail.update_time",
|
||||||
"order.out_trade_no",
|
"order_cash.channel","order_cash.cash_activity_id","order_cash.receive_status","order_cash.receive_time","order_cash.cash_packet_id","order_cash.cash_id","order_cash.amount","order_cash.status","order_cash.expire_time","order_cash.update_time",
|
||||||
"order.type",
|
"order_voucher.channel","order_voucher.channel_activity_id","order_voucher.channel_voucher_id","order_voucher.status","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",
|
||||||
"order.status",
|
"plan.id","plan.title","plan.status","plan.begin_time","plan.end_time",
|
||||||
"order.contract_price",
|
"key_batch.id","key_batch.batch_name","key_batch.bind_object","key_batch.quantity","key_batch.stock","key_batch.begin_time","key_batch.end_time",
|
||||||
"order.num",
|
"code_batch.id","code_batch.title","code_batch.status","code_batch.begin_time","code_batch.end_time","code_batch.quantity","code_batch.usage","code_batch.stock",
|
||||||
"order.total",
|
"voucher.channel","voucher.channel_activity_id","voucher.price","voucher.balance","voucher.used_amount","voucher.denomination",
|
||||||
"order.pay_amount",
|
"voucher_batch.channel_activity_id","voucher_batch.temp_no","voucher_batch.provider","voucher_batch.weight",
|
||||||
"order.create_time",
|
"merchant_key_send.merchant_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.update_time"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type ExportPayload struct{
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
|
func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -58,40 +59,31 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
|
||||||
var p ExportPayload
|
var p ExportPayload
|
||||||
json.Unmarshal(b, &p)
|
json.Unmarshal(b, &p)
|
||||||
var main string
|
var main string
|
||||||
var fields, filters []byte
|
var fields []byte
|
||||||
row := a.meta.QueryRow("SELECT main_table, fields_json, filters_json FROM export_templates WHERE id=?", p.TemplateID)
|
row := a.meta.QueryRow("SELECT main_table, fields_json FROM export_templates WHERE id=?", p.TemplateID)
|
||||||
err := row.Scan(&main, &fields, &filters)
|
err := row.Scan(&main, &fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte("invalid template"))
|
w.Write([]byte("invalid template"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var fs []string
|
var fs []string
|
||||||
var fl map[string]interface{}
|
|
||||||
json.Unmarshal(fields, &fs)
|
json.Unmarshal(fields, &fs)
|
||||||
json.Unmarshal(filters, &fl)
|
wl := whitelist()
|
||||||
wl := map[string]bool{
|
req := exporter.BuildRequest{MainTable: main, Fields: fs, Filters: p.Filters}
|
||||||
"order.order_number": true,
|
|
||||||
"order.creator": true,
|
|
||||||
"order.out_trade_no": true,
|
|
||||||
"order.type": true,
|
|
||||||
"order.status": true,
|
|
||||||
"order.contract_price": true,
|
|
||||||
"order.num": true,
|
|
||||||
"order.total": true,
|
|
||||||
"order.pay_amount": true,
|
|
||||||
"order.create_time": true,
|
|
||||||
"order.update_time": true,
|
|
||||||
}
|
|
||||||
req := exporter.BuildRequest{MainTable: main, Fields: fs, Filters: fl}
|
|
||||||
q, args, err := exporter.BuildSQL(req, wl)
|
q, args, err := exporter.BuildSQL(req, wl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _, _ = exporter.RunExplain(a.marketing, q, args)
|
expRows, score, err := exporter.RunExplain(a.marketing, q, args)
|
||||||
res, err := a.meta.Exec("INSERT INTO export_jobs (template_id, status, requested_by, permission_scope_json, options_json, file_format, created_at) VALUES (?,?,?,?,?,?,?)", p.TemplateID, "queued", p.RequestedBy, toJSON(p.Permission), toJSON(p.Options), p.FileFormat, time.Now())
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := a.meta.Exec("INSERT INTO export_jobs (template_id, status, requested_by, permission_scope_json, filters_json, options_json, explain_json, explain_score, file_format, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)", p.TemplateID, "queued", p.RequestedBy, toJSON(p.Permission), toJSON(p.Filters), toJSON(p.Options), toJSON(expRows), score, p.FileFormat, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
|
func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
|
||||||
|
|
@ -11,6 +12,17 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
|
||||||
mux.Handle("/api/templates/", TemplatesHandler(metaDB, marketingDB))
|
mux.Handle("/api/templates/", TemplatesHandler(metaDB, marketingDB))
|
||||||
mux.Handle("/api/exports", ExportsHandler(metaDB, marketingDB))
|
mux.Handle("/api/exports", ExportsHandler(metaDB, marketingDB))
|
||||||
mux.Handle("/api/exports/", ExportsHandler(metaDB, marketingDB))
|
mux.Handle("/api/exports/", ExportsHandler(metaDB, marketingDB))
|
||||||
mux.Handle("/", http.FileServer(http.Dir("web")))
|
sd := staticDir()
|
||||||
|
mux.Handle("/", http.FileServer(http.Dir(sd)))
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func staticDir() string {
|
||||||
|
if _, err := os.Stat("web/index.html"); err == nil {
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("../web/index.html"); err == nil {
|
||||||
|
return "../web"
|
||||||
|
}
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"marketing-system-data-tool/server/internal/exporter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplatesAPI struct{
|
type TemplatesAPI struct{
|
||||||
|
|
@ -37,6 +36,10 @@ func TemplatesHandler(meta, marketing *sql.DB) http.Handler {
|
||||||
api.patchTemplate(w, r, id)
|
api.patchTemplate(w, r, id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
api.deleteTemplate(w, r, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
if r.Method == http.MethodPost && strings.HasSuffix(p, "/validate") {
|
if r.Method == http.MethodPost && strings.HasSuffix(p, "/validate") {
|
||||||
id = strings.TrimSuffix(id, "/validate")
|
id = strings.TrimSuffix(id, "/validate")
|
||||||
api.validateTemplate(w, r, id)
|
api.validateTemplate(w, r, id)
|
||||||
|
|
@ -62,36 +65,10 @@ 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)
|
||||||
wl := map[string]bool{
|
|
||||||
"order.order_number": true,
|
|
||||||
"order.creator": true,
|
|
||||||
"order.out_trade_no": true,
|
|
||||||
"order.type": true,
|
|
||||||
"order.status": true,
|
|
||||||
"order.contract_price": true,
|
|
||||||
"order.num": true,
|
|
||||||
"order.total": true,
|
|
||||||
"order.pay_amount": true,
|
|
||||||
"order.create_time": true,
|
|
||||||
"order.update_time": true,
|
|
||||||
}
|
|
||||||
req := exporter.BuildRequest{MainTable: p.MainTable, Fields: p.Fields, Filters: p.Filters}
|
|
||||||
q, args, err := exporter.BuildSQL(req, wl)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, score, err := exporter.RunExplain(a.marketing, q, args)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err = a.meta.Exec(
|
_, err := a.meta.Exec(
|
||||||
"INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, explain_score, last_validated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, last_validated_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||||
p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), toJSON(p.Filters), p.FileFormat, p.Visibility, p.OwnerID, 1, score, now,
|
p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), nil, p.FileFormat, p.Visibility, p.OwnerID, 1, now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
@ -204,6 +181,24 @@ func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
var cnt int64
|
||||||
|
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id=?", id)
|
||||||
|
_ = row.Scan(&cnt)
|
||||||
|
if cnt > 0 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("template in use"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := a.meta.Exec("DELETE FROM export_templates WHERE id=?", id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
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 main_table, fields_json, filters_json FROM export_templates WHERE id=?", id)
|
||||||
var main string
|
var main string
|
||||||
|
|
@ -218,39 +213,7 @@ 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)
|
||||||
wl := map[string]bool{
|
// 模板不再记录 EXPLAIN,返回成功
|
||||||
"order.order_number": true,
|
|
||||||
"order.creator": true,
|
|
||||||
"order.out_trade_no": true,
|
|
||||||
"order.type": true,
|
|
||||||
"order.status": true,
|
|
||||||
"order.contract_price": true,
|
|
||||||
"order.num": true,
|
|
||||||
"order.total": true,
|
|
||||||
"order.pay_amount": true,
|
|
||||||
"order.create_time": true,
|
|
||||||
"order.update_time": true,
|
|
||||||
}
|
|
||||||
req := exporter.BuildRequest{MainTable: main, Fields: fs, Filters: fl}
|
|
||||||
q, args, err := exporter.BuildSQL(req, wl)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, score, err := exporter.RunExplain(a.marketing, q, args)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
_, err = a.meta.Exec("UPDATE export_templates SET explain_score=?, last_validated_at=? WHERE id=?", score, now, id)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,3 +227,87 @@ func fromJSON(b []byte) interface{} {
|
||||||
json.Unmarshal(b, &v)
|
json.Unmarshal(b, &v)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func whitelist() map[string]bool {
|
||||||
|
m := map[string]bool{
|
||||||
|
"order.order_number": true,
|
||||||
|
"order.creator": true,
|
||||||
|
"order.out_trade_no": true,
|
||||||
|
"order.type": true,
|
||||||
|
"order.status": true,
|
||||||
|
"order.contract_price": true,
|
||||||
|
"order.num": true,
|
||||||
|
"order.total": true,
|
||||||
|
"order.pay_amount": true,
|
||||||
|
"order.create_time": true,
|
||||||
|
"order.update_time": 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.cash_activity_id": true,
|
||||||
|
"order_cash.receive_status": true,
|
||||||
|
"order_cash.receive_time": true,
|
||||||
|
"order_cash.cash_packet_id": true,
|
||||||
|
"order_cash.cash_id": true,
|
||||||
|
"order_cash.amount": true,
|
||||||
|
"order_cash.status": true,
|
||||||
|
"order_cash.expire_time": true,
|
||||||
|
"order_cash.update_time": true,
|
||||||
|
"order_voucher.channel": true,
|
||||||
|
"order_voucher.channel_activity_id": true,
|
||||||
|
"order_voucher.channel_voucher_id": true,
|
||||||
|
"order_voucher.status": 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,
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
type BuildRequest struct {
|
type BuildRequest struct {
|
||||||
MainTable string
|
MainTable string
|
||||||
Fields []string
|
Fields []string // table.field
|
||||||
Filters map[string]interface{}
|
Filters map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,12 +17,19 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
|
||||||
return "", nil, errors.New("unsupported main table")
|
return "", nil, errors.New("unsupported main table")
|
||||||
}
|
}
|
||||||
cols := []string{}
|
cols := []string{}
|
||||||
for _, f := range req.Fields {
|
need := map[string]bool{}
|
||||||
if !whitelist["order."+f] {
|
for _, tf := range req.Fields {
|
||||||
|
if !whitelist[tf] {
|
||||||
return "", nil, errors.New("field not allowed")
|
return "", nil, errors.New("field not allowed")
|
||||||
}
|
}
|
||||||
if f == "key" || req.MainTable == "order" {
|
parts := strings.Split(tf, ".")
|
||||||
|
if len(parts) != 2 { return "", nil, errors.New("invalid field format") }
|
||||||
|
t, f := parts[0], parts[1]
|
||||||
|
need[t] = true
|
||||||
|
if t == "order" {
|
||||||
cols = append(cols, "`order`."+escape(f))
|
cols = append(cols, "`order`."+escape(f))
|
||||||
|
} else {
|
||||||
|
cols = append(cols, "`"+t+"`."+escape(f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
|
|
@ -32,6 +39,26 @@ 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 `order`")
|
sb.WriteString(" FROM `order`")
|
||||||
|
// JOINs based on need
|
||||||
|
// order_detail
|
||||||
|
if need["order_detail"] { sb.WriteString(" LEFT JOIN `order_detail` ON `order_detail`.order_number = `order`.order_number") }
|
||||||
|
// order_cash
|
||||||
|
if need["order_cash"] { sb.WriteString(" LEFT JOIN `order_cash` ON `order_cash`.order_number = `order`.order_number") }
|
||||||
|
// order_voucher
|
||||||
|
if need["order_voucher"] { sb.WriteString(" LEFT JOIN `order_voucher` ON `order_voucher`.order_number = `order`.order_number") }
|
||||||
|
// plan
|
||||||
|
if need["plan"] || need["key_batch"] { sb.WriteString(" LEFT JOIN `plan` ON `plan`.id = `order`.plan_id") }
|
||||||
|
// key_batch depends on plan
|
||||||
|
if need["key_batch"] { sb.WriteString(" LEFT JOIN `key_batch` ON `key_batch`.plan_id = `plan`.id") }
|
||||||
|
// code_batch depends on key_batch
|
||||||
|
if need["code_batch"] { sb.WriteString(" LEFT JOIN `code_batch` ON `code_batch`.key_batch_id = `key_batch`.id") }
|
||||||
|
// voucher depends on order_voucher
|
||||||
|
if need["voucher"] { sb.WriteString(" LEFT JOIN `voucher` ON `voucher`.channel_activity_id = `order_voucher`.channel_activity_id") }
|
||||||
|
// voucher_batch depends on voucher
|
||||||
|
if need["voucher_batch"] { sb.WriteString(" LEFT JOIN `voucher_batch` ON `voucher_batch`.voucher_id = `voucher`.id") }
|
||||||
|
// merchant_key_send depends on order.key
|
||||||
|
if need["merchant_key_send"] { sb.WriteString(" LEFT JOIN `merchant_key_send` ON `order`." + escape("key") + " = `merchant_key_send`.key") }
|
||||||
|
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
where := []string{}
|
where := []string{}
|
||||||
if v, ok := req.Filters["creator_in"]; ok {
|
if v, ok := req.Filters["creator_in"]; ok {
|
||||||
|
|
@ -40,34 +67,40 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
ids = t
|
ids = t
|
||||||
case []int:
|
case []int:
|
||||||
for _, x := range t {
|
for _, x := range t { ids = append(ids, x) }
|
||||||
ids = append(ids, x)
|
|
||||||
}
|
|
||||||
case []string:
|
case []string:
|
||||||
for _, x := range t {
|
for _, x := range t { ids = append(ids, x) }
|
||||||
ids = append(ids, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return "", nil, errors.New("creator_in required")
|
|
||||||
}
|
}
|
||||||
|
if len(ids) == 0 { 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, ",")
|
||||||
where = append(where, "`order`.creator IN ("+ph+")")
|
where = append(where, "`order`.creator IN ("+ph+")")
|
||||||
args = append(args, ids...)
|
args = append(args, ids...)
|
||||||
} else {
|
|
||||||
return "", nil, errors.New("creator_in required")
|
|
||||||
}
|
}
|
||||||
if v, ok := req.Filters["create_time_between"]; ok {
|
if v, ok := req.Filters["create_time_between"]; ok {
|
||||||
var arr []interface{}
|
var arr []interface{}
|
||||||
b, _ := json.Marshal(v)
|
b, _ := json.Marshal(v)
|
||||||
json.Unmarshal(b, &arr)
|
json.Unmarshal(b, &arr)
|
||||||
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")
|
|
||||||
}
|
|
||||||
where = append(where, "`order`.create_time BETWEEN ? AND ?")
|
where = append(where, "`order`.create_time BETWEEN ? AND ?")
|
||||||
args = append(args, arr[0], arr[1])
|
args = append(args, arr[0], arr[1])
|
||||||
}
|
}
|
||||||
|
if v, ok := req.Filters["type_eq"]; ok {
|
||||||
|
var tv int
|
||||||
|
switch t := v.(type) {
|
||||||
|
case float64:
|
||||||
|
tv = int(t)
|
||||||
|
case int:
|
||||||
|
tv = t
|
||||||
|
case string:
|
||||||
|
// simple digits parsing
|
||||||
|
for i := 0; i < len(t); i++ { c := t[i]; if c<'0'||c>'9' { continue }; tv = tv*10 + int(c-'0') }
|
||||||
|
}
|
||||||
|
if tv == 1 || tv == 2 || tv == 3 {
|
||||||
|
where = append(where, "`order`.type = ?")
|
||||||
|
args = append(args, tv)
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(where) > 0 {
|
if len(where) > 0 {
|
||||||
sb.WriteString(" WHERE ")
|
sb.WriteString(" WHERE ")
|
||||||
sb.WriteString(strings.Join(where, " AND "))
|
sb.WriteString(strings.Join(where, " AND "))
|
||||||
|
|
@ -76,8 +109,6 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
|
||||||
}
|
}
|
||||||
|
|
||||||
func escape(s string) string {
|
func escape(s string) string {
|
||||||
if s == "key" {
|
if s == "key" { return "`key`" }
|
||||||
return "`key`"
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,5 +76,16 @@ func Apply(db *sql.DB) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
optional := []string{
|
||||||
|
"ALTER TABLE export_jobs ADD COLUMN explain_json JSON",
|
||||||
|
"ALTER TABLE export_jobs ADD COLUMN explain_score INT",
|
||||||
|
"ALTER TABLE export_jobs ADD COLUMN filters_json JSON",
|
||||||
|
}
|
||||||
|
for _, s := range optional {
|
||||||
|
if _, err := db.Exec(s); err != nil {
|
||||||
|
// ignore if column exists or syntax not supported
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
186
web/index.html
186
web/index.html
|
|
@ -12,70 +12,37 @@
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header height="56px">
|
<el-header height="56px">
|
||||||
<el-row align="middle" justify="space-between">
|
<el-row align="middle" justify="space-between">
|
||||||
<el-col :span="8">
|
<el-col :span="24">
|
||||||
<div class="title">导出工具</div>
|
<div class="title">导出工具</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="14">
|
<el-col :span="24">
|
||||||
<el-card header="模板列表">
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span>模板列表</span>
|
||||||
|
<el-button type="primary" size="small" @click="createVisible=true">新增模板</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<el-table :data="templates" size="small" stripe>
|
<el-table :data="templates" size="small" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||||
<el-table-column prop="name" label="名称" />
|
<el-table-column prop="name" label="名称"></el-table-column>
|
||||||
<el-table-column prop="datasource" label="数据源" width="120" />
|
<el-table-column prop="datasource" label="数据源" width="120"></el-table-column>
|
||||||
<el-table-column prop="file_format" label="格式" width="100" />
|
<el-table-column prop="file_format" label="格式" width="100"></el-table-column>
|
||||||
<el-table-column prop="explain_score" label="评分" width="100" />
|
<el-table-column prop="explain_score" label="评分" width="100"></el-table-column>
|
||||||
<el-table-column label="操作" width="140">
|
<el-table-column label="操作" width="260">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button size="small" type="primary" @click="runExport(scope.row.id)">执行导出</el-button>
|
<el-button size="small" type="primary" @click="openExport(scope.row)">导出任务</el-button>
|
||||||
|
<el-button size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="removeTemplate(scope.row.id)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="10">
|
|
||||||
<el-card header="新增模板">
|
|
||||||
<el-form :model="form" label-width="110px">
|
|
||||||
<el-form-item label="模板名称"><el-input v-model="form.name" placeholder="模板名称" /></el-form-item>
|
|
||||||
<el-form-item label="数据源">
|
|
||||||
<el-select v-model="form.datasource" placeholder="选择">
|
|
||||||
<el-option label="营销系统" value="marketing" />
|
|
||||||
<el-option label="易码通" value="ymt" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="主表"><el-input v-model="form.main_table" /></el-form-item>
|
|
||||||
<el-form-item label="字段(逗号分隔)"><el-input v-model="form.fieldsRaw" /></el-form-item>
|
|
||||||
<el-form-item label="creator 列表"><el-input v-model="form.creatorRaw" placeholder="如:1,2,3" /></el-form-item>
|
|
||||||
<el-form-item label="时间范围">
|
|
||||||
<el-date-picker v-model="form.timeRange" type="datetimerange" range-separator="至" start-placeholder="开始" end-placeholder="结束" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-row :gutter="8">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="输出格式">
|
|
||||||
<el-select v-model="form.file_format">
|
|
||||||
<el-option label="CSV" value="csv" />
|
|
||||||
<el-option label="XLSX" value="xlsx" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="可见性">
|
|
||||||
<el-select v-model="form.visibility">
|
|
||||||
<el-option label="个人" value="private" />
|
|
||||||
<el-option label="公共" value="public" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-form-item label="所有者ID"><el-input v-model="form.owner_id" /></el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="createTemplate">创建并校验</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-card header="导出任务">
|
<el-card header="导出任务">
|
||||||
<div v-if="job.id" class="job">
|
<div v-if="job.id" class="job">
|
||||||
|
|
@ -88,6 +55,123 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
<el-dialog v-model="createVisible" title="新增模板" :width="createWidth">
|
||||||
|
<el-form :model="form" label-width="110px">
|
||||||
|
<el-form-item label="模板名称"><el-input v-model="form.name" placeholder="模板名称" /></el-form-item>
|
||||||
|
<el-form-item label="数据源">
|
||||||
|
<el-select v-model="form.datasource" placeholder="选择">
|
||||||
|
<el-option label="营销系统" value="marketing" />
|
||||||
|
<el-option label="易码通" value="ymt" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="导出场景">
|
||||||
|
<el-select v-model="form.main_table" placeholder="选择场景">
|
||||||
|
<el-option label="订单数据" value="order" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单类型">
|
||||||
|
<el-radio-group v-model="form.orderType">
|
||||||
|
<el-radio :label="1">直充卡密</el-radio>
|
||||||
|
<el-radio :label="2">立减金</el-radio>
|
||||||
|
<el-radio :label="3">红包</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字段选择">
|
||||||
|
<el-cascader
|
||||||
|
v-model="form.fieldsSel"
|
||||||
|
:options="fieldOptions"
|
||||||
|
:props="{ multiple: true, checkStrictly: true }"
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
placeholder="按场景逐级选择,可多选"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据权限">
|
||||||
|
<el-select v-model="form.permissionMode" style="width:160px">
|
||||||
|
<el-option label="所有" value="all" />
|
||||||
|
<el-option label="按创建者" value="creator" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.permissionMode==='creator'" label="创建者列表"><el-input v-model="form.creatorRaw" placeholder="如:1,2,3" /></el-form-item>
|
||||||
|
<el-form-item label="时间范围">
|
||||||
|
<el-date-picker v-model="form.timeRange" type="datetimerange" range-separator="至" start-placeholder="开始" end-placeholder="结束" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="8">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="输出格式">
|
||||||
|
<el-select v-model="form.file_format" :teleported="false" placeholder="请选择" style="width:160px">
|
||||||
|
<el-option v-for="opt in formatOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="可见性">
|
||||||
|
<el-select v-model="form.visibility" clearable :teleported="false" style="width:160px" placeholder="请选择">
|
||||||
|
<el-option v-for="opt in visibilityOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="所有者ID"><el-input v-model="form.owner_id" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="resizeDialog('create', -100)">缩小</el-button>
|
||||||
|
<el-button @click="resizeDialog('create', 100)">放大</el-button>
|
||||||
|
<el-button @click="createVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="createTemplate">创建并校验</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog v-model="editVisible" title="编辑模板" :width="editWidth">
|
||||||
|
<el-form :model="edit" label-width="110px">
|
||||||
|
<el-form-item label="模板名称"><el-input v-model="edit.name" /></el-form-item>
|
||||||
|
<el-form-item label="输出格式">
|
||||||
|
<el-select v-model="edit.file_format" :teleported="false" placeholder="请选择" style="width:160px">
|
||||||
|
<el-option v-for="opt in formatOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="可见性">
|
||||||
|
<el-select v-model="edit.visibility" clearable :teleported="false" style="width:160px" placeholder="请选择">
|
||||||
|
<el-option v-for="opt in visibilityOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="resizeDialog('edit', -100)">缩小</el-button>
|
||||||
|
<el-button @click="resizeDialog('edit', 100)">放大</el-button>
|
||||||
|
<el-button @click="editVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveEdit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog v-model="exportVisible" title="执行导出" width="700px">
|
||||||
|
<el-form :model="export" label-width="110px">
|
||||||
|
<el-form-item label="订单类型">
|
||||||
|
<el-radio-group v-model="export.orderType">
|
||||||
|
<el-radio :label="1">直充卡密</el-radio>
|
||||||
|
<el-radio :label="2">立减金</el-radio>
|
||||||
|
<el-radio :label="3">红包</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数据权限">
|
||||||
|
<el-select v-model="export.permissionMode" style="width:160px">
|
||||||
|
<el-option label="所有" value="all" />
|
||||||
|
<el-option label="按创建者" value="creator" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="export.permissionMode==='creator'" label="创建者列表"><el-input v-model="export.creatorRaw" placeholder="如:1,2,3" /></el-form-item>
|
||||||
|
<el-form-item label="时间范围">
|
||||||
|
<el-date-picker v-model="export.timeRange" type="datetimerange" range-separator="至" start-placeholder="开始" end-placeholder="结束" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="输出格式">
|
||||||
|
<el-select v-model="export.file_format" :teleported="false" placeholder="请选择" style="width:160px">
|
||||||
|
<el-option v-for="opt in formatOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="exportVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitExport">执行并分析</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script>
|
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script>
|
||||||
|
|
|
||||||
262
web/main.js
262
web/main.js
|
|
@ -8,15 +8,200 @@ const app = createApp({
|
||||||
name: '',
|
name: '',
|
||||||
datasource: 'marketing',
|
datasource: 'marketing',
|
||||||
main_table: 'order',
|
main_table: 'order',
|
||||||
|
orderType: 1,
|
||||||
fieldsRaw: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time',
|
fieldsRaw: 'order_number,creator,out_trade_no,type,status,contract_price,num,total,pay_amount,create_time',
|
||||||
|
fieldsSel: [],
|
||||||
creatorRaw: '',
|
creatorRaw: '',
|
||||||
|
permissionMode: 'all',
|
||||||
timeRange: [],
|
timeRange: [],
|
||||||
file_format: 'csv',
|
file_format: 'xlsx',
|
||||||
visibility: 'private',
|
visibility: 'private',
|
||||||
owner_id: '1'
|
owner_id: '1'
|
||||||
|
},
|
||||||
|
createVisible: false,
|
||||||
|
editVisible: false,
|
||||||
|
exportVisible: false,
|
||||||
|
createWidth: (localStorage.getItem('tplDialogWidth') || '900px'),
|
||||||
|
editWidth: (localStorage.getItem('tplEditDialogWidth') || '600px'),
|
||||||
|
edit: { id: null, name: '', visibility: 'private', file_format: 'csv' },
|
||||||
|
export: { tplId: null, orderType: 1, permissionMode: 'all', creatorRaw: '', timeRange: [], file_format: 'xlsx' }
|
||||||
|
})
|
||||||
|
const FIELDS_MAP = {
|
||||||
|
marketing: {
|
||||||
|
order: [
|
||||||
|
{ value: 'order_number', label: '订单编号' },
|
||||||
|
{ value: 'creator', label: '创建者ID' },
|
||||||
|
{ value: 'out_trade_no', label: '支付流水号' },
|
||||||
|
{ value: 'type', label: '订单类型' },
|
||||||
|
{ value: 'status', label: '订单状态' },
|
||||||
|
{ value: 'contract_price', label: '合同单价' },
|
||||||
|
{ value: 'num', label: '数量' },
|
||||||
|
{ value: 'total', label: '总金额' },
|
||||||
|
{ value: 'pay_amount', label: '支付金额' },
|
||||||
|
{ value: 'create_time', label: '创建时间' },
|
||||||
|
{ value: 'update_time', label: '更新时间' }
|
||||||
|
]
|
||||||
|
,
|
||||||
|
order_detail: [
|
||||||
|
{ value: 'plan_title', label: '计划标题' },
|
||||||
|
{ value: 'reseller_name', label: '分销商名称' },
|
||||||
|
{ value: 'product_name', label: '商品名称' },
|
||||||
|
{ value: 'show_url', label: '商品图片URL' },
|
||||||
|
{ value: 'official_price', label: '官方价' },
|
||||||
|
{ value: 'cost_price', label: '成本价' },
|
||||||
|
{ value: 'create_time', label: '创建时间' },
|
||||||
|
{ value: 'update_time', label: '更新时间' }
|
||||||
|
],
|
||||||
|
order_cash: [
|
||||||
|
{ value: 'channel', label: '渠道' },
|
||||||
|
{ value: 'cash_activity_id', label: '红包批次号' },
|
||||||
|
{ value: 'receive_status', label: '领取状态' },
|
||||||
|
{ value: 'receive_time', label: '拆红包时间' },
|
||||||
|
{ value: 'cash_packet_id', label: '红包ID' },
|
||||||
|
{ value: 'cash_id', label: '红包规则ID' },
|
||||||
|
{ value: 'amount', label: '红包额度' },
|
||||||
|
{ value: 'status', label: '状态' },
|
||||||
|
{ value: 'expire_time', label: '过期时间' },
|
||||||
|
{ value: 'update_time', label: '更新时间' }
|
||||||
|
],
|
||||||
|
order_voucher: [
|
||||||
|
{ value: 'channel', label: '渠道' },
|
||||||
|
{ value: 'channel_activity_id', label: '渠道立减金批次' },
|
||||||
|
{ value: 'channel_voucher_id', label: '渠道立减金ID' },
|
||||||
|
{ value: 'status', label: '状态' },
|
||||||
|
{ value: 'grant_time', label: '领取时间' },
|
||||||
|
{ value: 'usage_time', label: '核销时间' },
|
||||||
|
{ value: 'refund_time', label: '退款时间' },
|
||||||
|
{ value: 'status_modify_time', label: '状态更新时间' },
|
||||||
|
{ value: 'overdue_time', label: '过期时间' },
|
||||||
|
{ value: 'refund_amount', label: '退款金额' },
|
||||||
|
{ value: 'official_price', label: '官方价' },
|
||||||
|
{ value: 'out_biz_no', label: '外部业务号' },
|
||||||
|
{ value: 'account_no', label: '账户号' }
|
||||||
|
],
|
||||||
|
plan: [
|
||||||
|
{ value: 'id', label: '计划ID' },
|
||||||
|
{ value: 'title', label: '计划标题' },
|
||||||
|
{ value: 'status', label: '状态' },
|
||||||
|
{ value: 'begin_time', label: '开始时间' },
|
||||||
|
{ value: 'end_time', label: '结束时间' }
|
||||||
|
],
|
||||||
|
key_batch: [
|
||||||
|
{ value: 'id', label: '批次ID' },
|
||||||
|
{ value: 'batch_name', label: '批次名称' },
|
||||||
|
{ value: 'bind_object', label: '绑定对象' },
|
||||||
|
{ value: 'quantity', label: '发放数量' },
|
||||||
|
{ value: 'stock', label: '剩余库存' },
|
||||||
|
{ value: 'begin_time', label: '开始时间' },
|
||||||
|
{ value: 'end_time', label: '结束时间' }
|
||||||
|
],
|
||||||
|
code_batch: [
|
||||||
|
{ value: 'id', label: '兑换批次ID' },
|
||||||
|
{ value: 'title', label: '标题' },
|
||||||
|
{ value: 'status', label: '状态' },
|
||||||
|
{ value: 'begin_time', label: '开始时间' },
|
||||||
|
{ value: 'end_time', label: '结束时间' },
|
||||||
|
{ value: 'quantity', label: '数量' },
|
||||||
|
{ value: 'usage', label: '使用数' },
|
||||||
|
{ value: 'stock', label: '库存' }
|
||||||
|
],
|
||||||
|
voucher: [
|
||||||
|
{ value: 'channel', label: '渠道' },
|
||||||
|
{ value: 'channel_activity_id', label: '渠道批次号' },
|
||||||
|
{ value: 'price', label: '合同单价' },
|
||||||
|
{ value: 'balance', label: '剩余额度' },
|
||||||
|
{ value: 'used_amount', label: '已用额度' },
|
||||||
|
{ value: 'denomination', label: '面额' }
|
||||||
|
],
|
||||||
|
voucher_batch: [
|
||||||
|
{ value: 'channel_activity_id', label: '渠道批次号' },
|
||||||
|
{ value: 'temp_no', label: '模板编号' },
|
||||||
|
{ value: 'provider', label: '服务商' },
|
||||||
|
{ value: 'weight', label: '权重' }
|
||||||
|
],
|
||||||
|
merchant_key_send: [
|
||||||
|
{ value: 'merchant_id', label: '商户ID' },
|
||||||
|
{ value: 'out_biz_no', label: '商户业务号' },
|
||||||
|
{ value: 'key', label: '券码' },
|
||||||
|
{ value: 'status', label: '状态' },
|
||||||
|
{ value: 'usage_time', label: '核销时间' },
|
||||||
|
{ value: 'create_time', label: '创建时间' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const TABLE_LABELS = {
|
||||||
|
order: '订单主表',
|
||||||
|
order_detail: '订单详情',
|
||||||
|
order_cash: '红包订单',
|
||||||
|
order_voucher: '立减金订单',
|
||||||
|
plan: '活动计划',
|
||||||
|
key_batch: 'KEY批次',
|
||||||
|
code_batch: '兑换码批次',
|
||||||
|
voucher: '立减金',
|
||||||
|
voucher_batch: '立减金批次',
|
||||||
|
merchant_key_send: '开放平台发放记录'
|
||||||
|
}
|
||||||
|
const fieldOptions = Vue.computed(()=>{
|
||||||
|
const ds = state.form.datasource
|
||||||
|
const FM = FIELDS_MAP[ds] || {}
|
||||||
|
const node = (table, children=[])=>({ value: table, label: TABLE_LABELS[table]||table, children })
|
||||||
|
const fieldsNode = (table)=> (FM[table]||[])
|
||||||
|
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 = (type)=>{
|
||||||
|
const ch = [...orderChildrenBase]
|
||||||
|
if(type===1){
|
||||||
|
// 直充卡密:排除红包与立减金
|
||||||
|
ch.push(node('plan', planChildren))
|
||||||
|
ch.push(node('merchant_key_send', fieldsNode('merchant_key_send')))
|
||||||
|
} else if(type===2){
|
||||||
|
// 立减金:排除红包,保留立减金链
|
||||||
|
ch.push(node('order_voucher', voucherChildren))
|
||||||
|
ch.push(node('plan', planChildren))
|
||||||
|
} else if(type===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 type = Number(state.form.orderType || 0)
|
||||||
|
const orderNode = node('order', orderChildrenFor(type))
|
||||||
|
if(type){
|
||||||
|
return [ orderNode ]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: 'scene_order', label: '订单数据', children: [ orderNode ] }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
const msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type});
|
const msg = (t, type='success')=>ElementPlus.ElMessage({message:t,type});
|
||||||
|
const visibilityOptions = [
|
||||||
|
{ label: '个人', value: 'private' },
|
||||||
|
{ label: '公共', value: 'public' }
|
||||||
|
]
|
||||||
|
const formatOptions = [
|
||||||
|
{ label: 'XLSX', value: 'xlsx' },
|
||||||
|
{ label: 'CSV', value: 'csv' }
|
||||||
|
]
|
||||||
const fmtDT = (d)=>{
|
const fmtDT = (d)=>{
|
||||||
const pad=(n)=>String(n).padStart(2,'0');
|
const pad=(n)=>String(n).padStart(2,'0');
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
|
@ -26,38 +211,93 @@ const app = createApp({
|
||||||
state.templates = await res.json();
|
state.templates = await res.json();
|
||||||
}
|
}
|
||||||
const createTemplate = async ()=>{
|
const createTemplate = async ()=>{
|
||||||
const fields = state.form.fieldsRaw.split(',').map(s=>s.trim()).filter(Boolean);
|
let fields = []
|
||||||
const filters = {};
|
if(state.form.fieldsSel && state.form.fieldsSel.length){
|
||||||
if(state.form.creatorRaw){ filters.creator_in = state.form.creatorRaw.split(',').map(s=>Number(s.trim())).filter(x=>!isNaN(x)) }
|
fields = state.form.fieldsSel.map(path=>{
|
||||||
if(state.form.timeRange && state.form.timeRange.length===2){
|
if(Array.isArray(path)){
|
||||||
filters.create_time_between = [fmtDT(new Date(state.form.timeRange[0])), fmtDT(new Date(state.form.timeRange[1]))]
|
if(path.length===4){ return `${path[2]}.${path[3]}` }
|
||||||
|
if(path.length===3){ return `${path[1]}.${path[2]}` }
|
||||||
|
if(path.length===2){ return `${path[0]}.${path[1]}` }
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fields = state.form.fieldsRaw.split(',').map(s=>s.trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
name: state.form.name,
|
name: state.form.name,
|
||||||
datasource: state.form.datasource,
|
datasource: state.form.datasource,
|
||||||
main_table: state.form.main_table,
|
main_table: state.form.main_table,
|
||||||
fields,
|
fields,
|
||||||
filters,
|
|
||||||
file_format: state.form.file_format,
|
file_format: state.form.file_format,
|
||||||
owner_id: Number(state.form.owner_id),
|
owner_id: Number(state.form.owner_id),
|
||||||
visibility: state.form.visibility
|
visibility: state.form.visibility
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/templates',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
const res = await fetch('/api/templates',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||||||
if(res.ok){ msg('创建成功'); loadTemplates() } else { msg(await res.text(),'error') }
|
if(res.ok){ msg('创建成功'); state.createVisible=false; loadTemplates() } else { msg(await res.text(),'error') }
|
||||||
}
|
}
|
||||||
const runExport = async (id)=>{
|
const openExport = (row)=>{
|
||||||
const payload={template_id:Number(id),requested_by:1,permission:{},options:{},file_format:'csv'};
|
state.export.tplId = row.id
|
||||||
|
state.export.file_format = row.file_format || 'xlsx'
|
||||||
|
state.exportVisible = true
|
||||||
|
}
|
||||||
|
const submitExport = async ()=>{
|
||||||
|
const id = state.export.tplId
|
||||||
|
const filters = {}
|
||||||
|
if(state.export.permissionMode==='creator' && state.export.creatorRaw){
|
||||||
|
filters.creator_in = state.export.creatorRaw.split(',').map(s=>Number(s.trim())).filter(x=>!isNaN(x))
|
||||||
|
}
|
||||||
|
if(state.export.orderType){ filters.type_eq = Number(state.export.orderType) }
|
||||||
|
if(state.export.timeRange && state.export.timeRange.length===2){
|
||||||
|
filters.create_time_between = [fmtDT(new Date(state.export.timeRange[0])), fmtDT(new Date(state.export.timeRange[1]))]
|
||||||
|
}
|
||||||
|
const payload={template_id:Number(id),requested_by:1,permission:{},options:{},filters, file_format: state.export.file_format};
|
||||||
const r=await fetch('/api/exports',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
const r=await fetch('/api/exports',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||||||
const j=await r.json();
|
const j=await r.json();
|
||||||
|
state.exportVisible=false
|
||||||
loadJob(j.id);
|
loadJob(j.id);
|
||||||
}
|
}
|
||||||
|
const clampWidth = (w)=>{
|
||||||
|
const n = Math.max(500, Math.min(1400, w))
|
||||||
|
return n + 'px'
|
||||||
|
}
|
||||||
|
const resizeDialog = (kind, delta)=>{
|
||||||
|
if(kind==='create'){
|
||||||
|
const cur = parseInt(String(state.createWidth).replace('px','')||'900',10)
|
||||||
|
const next = clampWidth(cur + delta)
|
||||||
|
state.createWidth = next
|
||||||
|
localStorage.setItem('tplDialogWidth', next)
|
||||||
|
} else if(kind==='edit'){
|
||||||
|
const cur = parseInt(String(state.editWidth).replace('px','')||'600',10)
|
||||||
|
const next = clampWidth(cur + delta)
|
||||||
|
state.editWidth = next
|
||||||
|
localStorage.setItem('tplEditDialogWidth', next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const openEdit = (row)=>{
|
||||||
|
state.edit.id = row.id
|
||||||
|
state.edit.name = row.name
|
||||||
|
state.edit.visibility = row.visibility
|
||||||
|
state.edit.file_format = row.file_format
|
||||||
|
state.editVisible = true
|
||||||
|
}
|
||||||
|
const saveEdit = async ()=>{
|
||||||
|
const id = state.edit.id
|
||||||
|
const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format }
|
||||||
|
const res = await fetch('/api/templates/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
||||||
|
if(res.ok){ msg('保存成功'); state.editVisible=false; loadTemplates() } else { msg(await res.text(),'error') }
|
||||||
|
}
|
||||||
|
const removeTemplate = async (id)=>{
|
||||||
|
const r = await fetch('/api/templates/'+id,{method:'DELETE'})
|
||||||
|
if(r.ok){ msg('删除成功'); loadTemplates() } else { msg(await r.text(),'error') }
|
||||||
|
}
|
||||||
const loadJob = async (id)=>{
|
const loadJob = async (id)=>{
|
||||||
const res=await fetch('/api/exports/'+id);
|
const res=await fetch('/api/exports/'+id);
|
||||||
state.job = await res.json();
|
state.job = await res.json();
|
||||||
}
|
}
|
||||||
const download = (id)=>{ window.open('/api/exports/'+id+'/download','_blank') }
|
const download = (id)=>{ window.open('/api/exports/'+id+'/download','_blank') }
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
return { ...state, loadTemplates, createTemplate, runExport, loadJob, download }
|
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, fieldOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, download, openEdit, saveEdit, removeTemplate, resizeDialog }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue