feat(导出功能): 增强订单导出功能并修复相关问题

新增订单KEY解码功能及字段转换逻辑
扩展订单字段支持并优化SQL生成
修复导出文件处理逻辑及模板编辑界面
This commit is contained in:
zhouyonggao 2025-11-25 18:21:12 +08:00
parent 950fa758e1
commit 23bfdfc645
30 changed files with 3158 additions and 244 deletions

View File

@ -8,6 +8,7 @@ import (
"io" "io"
"log" "log"
"marketing-system-data-tool/server/internal/exporter" "marketing-system-data-tool/server/internal/exporter"
"math/big"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -149,11 +150,11 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
return return
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
go a.runJob(uint64(id), dataDB, q, args, hdrs, p.FileFormat) go a.runJob(uint64(id), dataDB, q, args, fs, hdrs, p.FileFormat)
ok(w, r, map[string]interface{}{"id": id}) ok(w, r, map[string]interface{}{"id": id})
} }
func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{}, cols []string, fmt string) { func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{}, fields []string, cols []string, fmt string) {
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, started_at=? WHERE id= ?", []interface{}{"running", time.Now(), id}) log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, started_at=? WHERE id= ?", []interface{}{"running", time.Now(), id})
a.meta.Exec("UPDATE export_jobs SET status=?, started_at=?, updated_at=? WHERE id= ?", "running", time.Now(), time.Now(), id) a.meta.Exec("UPDATE export_jobs SET status=?, started_at=?, updated_at=? WHERE id= ?", "running", time.Now(), time.Now(), id)
if fmt == "csv" { if fmt == "csv" {
@ -230,6 +231,10 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
vals[i] = toString(out[i]) vals[i] = toString(out[i])
} }
} }
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++
@ -559,6 +564,11 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
vals[i] = toString(out[i]) vals[i] = toString(out[i])
} }
} }
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++
@ -586,10 +596,49 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
} }
} }
} }
if count == 0 {
rows, err := db.Query(q, args...)
if err == nil {
defer rows.Close()
out2 := make([]interface{}, len(cols))
dest2 := make([]interface{}, len(cols))
for i := range out2 {
dest2[i] = &out2[i]
}
var tick2 int64
for rows.Next() {
if err := rows.Scan(dest2...); err != nil {
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id?", "failed", time.Now(), id)
return
}
vals := make([]string, len(cols))
for i := range out2 {
if b, ok := out2[i].([]byte); ok {
vals[i] = string(b)
} else if out2[i] == nil {
vals[i] = ""
} else {
vals[i] = toString(out2[i])
}
}
x.WriteRow(vals)
count++
tick2++
if tick2%50 == 0 {
a.meta.Exec("UPDATE export_jobs SET total_rows=?, updated_at=? WHERE id= ?", count, time.Now(), id)
}
}
}
}
p, size, _ := x.Close(path) p, size, _ := x.Close(path)
if partCount > 0 || len(files) == 0 { if partCount > 0 || len(files) == 0 {
files = append(files, p) files = append(files, p)
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, p, partCount, size, time.Now(), time.Now()) a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, p, func() int64 {
if count > 0 {
return count
}
return partCount
}(), size, time.Now(), time.Now())
} }
if count == 0 { if count == 0 {
row := db.QueryRow("SELECT COUNT(1) FROM ("+q+") AS sub", args...) row := db.QueryRow("SELECT COUNT(1) FROM ("+q+") AS sub", args...)
@ -597,7 +646,7 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
_ = row.Scan(&c) _ = row.Scan(&c)
count = c count = c
} }
if len(files) > 1 { if len(files) >= 1 {
zipPath, zipSize := createZip(id, files) zipPath, zipSize := createZip(id, files)
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, zipPath, count, zipSize, time.Now(), time.Now()) a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, zipPath, count, zipSize, time.Now(), time.Now())
} }
@ -644,6 +693,8 @@ func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{},
p, size, _ := x.Close(path) p, size, _ := x.Close(path)
log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", []interface{}{id, p, count, size, time.Now(), time.Now()}) log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", []interface{}{id, p, count, size, time.Now(), time.Now()})
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, p, count, size, time.Now(), time.Now()) a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, p, count, size, time.Now(), time.Now())
zipPath, zipSize := createZip(id, []string{p})
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, zipPath, count, zipSize, time.Now(), time.Now())
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", []interface{}{"completed", time.Now(), count, time.Now(), id}) log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", []interface{}{"completed", time.Now(), count, time.Now(), id})
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", "completed", time.Now(), count, time.Now(), id) a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", "completed", time.Now(), count, time.Now(), id)
return return
@ -794,6 +845,49 @@ func (a *ExportsAPI) download(w http.ResponseWriter, r *http.Request, id string)
http.ServeFile(w, r, uri) http.ServeFile(w, r, uri)
} }
func transformRow(fields []string, vals []string) []string {
for i := range fields {
if i >= len(vals) {
break
}
f := fields[i]
if f == "order.key" {
vals[i] = decodeOrderKey(vals[i])
}
}
return vals
}
func decodeOrderKey(s string) string {
if s == "" {
return s
}
if len(s) > 2 && s[len(s)-2:] == "_1" {
s = s[:len(s)-2]
}
var n big.Int
if _, ok := n.SetString(s, 10); !ok {
return s
}
base := []rune{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
baseCount := big.NewInt(int64(len(base)))
zero := big.NewInt(0)
var out []rune
for n.Cmp(zero) > 0 {
var mod big.Int
mod.Mod(&n, baseCount)
out = append(out, base[mod.Int64()])
n.Div(&n, baseCount)
}
for len(out) < 16 {
out = append(out, base[0])
}
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return string(out)
}
func (a *ExportsAPI) cancel(w http.ResponseWriter, r *http.Request, id string) { func (a *ExportsAPI) cancel(w http.ResponseWriter, r *http.Request, id string) {
a.meta.Exec("UPDATE export_jobs SET status=?, updated_at=? WHERE id=? AND status IN ('queued','running')", "canceled", time.Now(), id) a.meta.Exec("UPDATE export_jobs SET status=?, updated_at=? WHERE id=? AND status IN ('queued','running')", "canceled", time.Now(), id)
w.Write([]byte("ok")) w.Write([]byte("ok"))

View File

@ -16,6 +16,17 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
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))))
mux.Handle("/api/resellers/", withAccess(withTrace(ResellersHandler(marketingDB)))) mux.Handle("/api/resellers/", withAccess(withTrace(ResellersHandler(marketingDB))))
mux.HandleFunc("/api/utils/decode_key", func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get("v")
if v == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("missing v"))
return
}
d := decodeOrderKey(v)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{\"decoded\":\"" + d + "\"}"))
})
sd := staticDir() sd := staticDir()
mux.Handle("/", http.FileServer(http.Dir(sd))) mux.Handle("/", http.FileServer(http.Dir(sd)))
return mux return mux

View File

@ -140,36 +140,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 "enabled": case "fields":
set = append(set, "enabled=?") set = append(set, "fields_json=?")
if v.(bool) { args = append(args, toJSON(v))
args = append(args, 1) case "filters":
} else { set = append(set, "filters_json=?")
args = append(args, 0) args = append(args, toJSON(v))
} case "enabled":
} set = append(set, "enabled=?")
} if v.(bool) {
if len(set) == 0 { args = append(args, 1)
fail(w, r, http.StatusBadRequest, "no patch") } else {
return args = append(args, 0)
} }
args = append(args, id) }
_, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) }
if err != nil { if len(set) == 0 {
fail(w, r, http.StatusInternalServerError, err.Error()) fail(w, r, http.StatusBadRequest, "no patch")
return return
} }
ok(w, r, nil) // ensure updated_at
set = append(set, "updated_at=?")
args = append(args, time.Now(), id)
_, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...)
if err != nil {
fail(w, r, http.StatusInternalServerError, err.Error())
return
}
ok(w, r, nil)
} }
func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) { func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) {
@ -216,18 +224,31 @@ func fromJSON(b []byte) interface{} {
} }
func whitelist() map[string]bool { func whitelist() map[string]bool {
m := map[string]bool{ m := map[string]bool{
"order.order_number": true, "order.order_number": true,
"order.creator": true, "order.key": true,
"order.out_trade_no": true, "order.creator": true,
"order.type": true, "order.out_trade_no": true,
"order.status": true, "order.type": true,
"order.contract_price": true, "order.status": true,
"order.num": true, "order.account": true,
"order.total": true, "order.product_id": true,
"order.pay_amount": true, "order.reseller_id": true,
"order.create_time": true, "order.plan_id": true,
"order.update_time": 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_detail.plan_title": true, "order_detail.plan_title": true,
"order_detail.reseller_name": true, "order_detail.reseller_name": true,
"order_detail.product_name": true, "order_detail.product_name": true,
@ -248,13 +269,14 @@ func whitelist() map[string]bool {
"order_cash.update_time": true, "order_cash.update_time": true,
"order_voucher.channel": true, "order_voucher.channel": true,
"order_voucher.channel_activity_id": true, "order_voucher.channel_activity_id": true,
"order_voucher.channel_voucher_id": true, "order_voucher.channel_voucher_id": true,
"order_voucher.status": true, "order_voucher.status": true,
"order_voucher.grant_time": true, "order_voucher.receive_mode": true,
"order_voucher.usage_time": true, "order_voucher.grant_time": true,
"order_voucher.refund_time": true, "order_voucher.usage_time": true,
"order_voucher.status_modify_time": true, "order_voucher.refund_time": true,
"order_voucher.overdue_time": true, "order_voucher.status_modify_time": true,
"order_voucher.overdue_time": true,
"order_voucher.refund_amount": true, "order_voucher.refund_amount": true,
"order_voucher.official_price": true, "order_voucher.official_price": true,
"order_voucher.out_biz_no": true, "order_voucher.out_biz_no": true,
@ -302,10 +324,23 @@ func whitelist() map[string]bool {
func fieldLabels() map[string]string { func fieldLabels() map[string]string {
return map[string]string{ return map[string]string{
"order.order_number": "订单编号", "order.order_number": "订单编号",
"order.key": "KEY",
"order.creator": "创建者ID", "order.creator": "创建者ID",
"order.out_trade_no": "支付流水号", "order.out_trade_no": "支付流水号",
"order.type": "订单类型", "order.type": "订单类型",
"order.status": "订单状态", "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.contract_price": "合同单价",
"order.num": "数量", "order.num": "数量",
"order.total": "总金额", "order.total": "总金额",
@ -334,6 +369,7 @@ func fieldLabels() map[string]string {
"order_voucher.channel_activity_id": "渠道立减金批次", "order_voucher.channel_activity_id": "渠道立减金批次",
"order_voucher.channel_voucher_id": "渠道立减金ID", "order_voucher.channel_voucher_id": "渠道立减金ID",
"order_voucher.status": "状态", "order_voucher.status": "状态",
"order_voucher.receive_mode": "领取方式",
"order_voucher.grant_time": "领取时间", "order_voucher.grant_time": "领取时间",
"order_voucher.usage_time": "核销时间", "order_voucher.usage_time": "核销时间",
"order_voucher.refund_time": "退款时间", "order_voucher.refund_time": "退款时间",

View File

@ -1,185 +1,290 @@
package exporter package exporter
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"strings" "strconv"
"strconv" "strings"
) )
type BuildRequest struct { type BuildRequest struct {
MainTable string MainTable string
Fields []string // table.field Fields []string // table.field
Filters map[string]interface{} Filters map[string]interface{}
} }
func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{}, error) { func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{}, error) {
if req.MainTable != "order" { if req.MainTable != "order" {
return "", nil, errors.New("unsupported main table") return "", nil, errors.New("unsupported main table")
} }
cols := []string{} cols := []string{}
need := map[string]bool{} need := map[string]bool{}
for _, tf := range req.Fields { for _, tf := range req.Fields {
if !whitelist[tf] { if !whitelist[tf] {
return "", nil, errors.New("field not allowed") return "", nil, errors.New("field not allowed")
} }
parts := strings.Split(tf, ".") parts := strings.Split(tf, ".")
if len(parts) != 2 { return "", nil, errors.New("invalid field format") } if len(parts) != 2 {
t, f := parts[0], parts[1] return "", nil, errors.New("invalid field format")
need[t] = true }
t, f := parts[0], parts[1]
need[t] = true
if t == "order" { if t == "order" {
cols = append(cols, "`order`."+escape(f)) if f == "status" {
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")
} else if f == "type" {
cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS type")
} else if f == "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" {
cols = append(cols, "CASE `order`.pay_status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已退款' ELSE '' END AS pay_status")
} else {
cols = append(cols, "`order`."+escape(f))
}
} else { } 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")
} 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 len(cols) == 0 { if len(cols) == 0 {
return "", nil, errors.New("no fields") return "", nil, errors.New("no fields")
} }
sb := strings.Builder{} sb := strings.Builder{}
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 // JOINs based on need
// order_detail // order_detail
if need["order_detail"] { sb.WriteString(" LEFT JOIN `order_detail` ON `order_detail`.order_number = `order`.order_number") } if need["order_detail"] {
// order_cash sb.WriteString(" LEFT JOIN `order_detail` ON `order_detail`.order_number = `order`.order_number")
if need["order_cash"] { sb.WriteString(" LEFT JOIN `order_cash` ON `order_cash`.order_number = `order`.order_number") } }
// order_voucher // order_cash
if need["order_voucher"] { sb.WriteString(" LEFT JOIN `order_voucher` ON `order_voucher`.order_number = `order`.order_number") } if need["order_cash"] {
// plan sb.WriteString(" LEFT JOIN `order_cash` ON `order_cash`.order_number = `order`.order_number")
if need["plan"] || need["key_batch"] { sb.WriteString(" LEFT JOIN `plan` ON `plan`.id = `order`.plan_id") } }
// key_batch depends on plan // order_voucher
if need["key_batch"] { sb.WriteString(" LEFT JOIN `key_batch` ON `key_batch`.plan_id = `plan`.id") } if need["order_voucher"] {
// code_batch depends on key_batch sb.WriteString(" LEFT JOIN `order_voucher` ON `order_voucher`.order_number = `order`.order_number")
if need["code_batch"] { sb.WriteString(" LEFT JOIN `code_batch` ON `code_batch`.key_batch_id = `key_batch`.id") } }
// voucher depends on order_voucher // plan
if need["voucher"] { sb.WriteString(" LEFT JOIN `voucher` ON `voucher`.channel_activity_id = `order_voucher`.channel_activity_id") } if need["plan"] || need["key_batch"] {
// voucher_batch depends on voucher sb.WriteString(" LEFT JOIN `plan` ON `plan`.id = `order`.plan_id")
if need["voucher_batch"] { sb.WriteString(" LEFT JOIN `voucher_batch` ON `voucher_batch`.voucher_id = `voucher`.id") } }
// merchant_key_send depends on order.key // key_batch depends on plan
if need["merchant_key_send"] { sb.WriteString(" LEFT JOIN `merchant_key_send` ON `order`." + escape("key") + " = `merchant_key_send`.key") } 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{}
// collect need from filters referencing related tables // collect need from filters referencing related tables
if _, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok { need["order_cash"] = true } if _, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok {
if _, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok { need["order_voucher"] = true } need["order_cash"] = true
if _, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok { }
need["voucher_batch"] = true if _, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok {
need["voucher"] = true need["order_voucher"] = true
need["order_voucher"] = true }
} if _, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok {
if _, ok := req.Filters["merchant_out_biz_no_eq"]; ok { need["merchant_key_send"] = true } need["voucher_batch"] = true
if v, ok := req.Filters["creator_in"]; ok { need["voucher"] = true
ids := []interface{}{} need["order_voucher"] = true
switch t := v.(type) { }
case []interface{}: if _, ok := req.Filters["merchant_out_biz_no_eq"]; ok {
ids = t need["merchant_key_send"] = true
case []int: }
for _, x := range t { ids = append(ids, x) } if v, ok := req.Filters["creator_in"]; ok {
case []string: ids := []interface{}{}
for _, x := range t { ids = append(ids, x) } switch t := v.(type) {
} case []interface{}:
if len(ids) == 0 { return "", nil, errors.New("creator_in required") } ids = t
ph := strings.Repeat("?,", len(ids)) case []int:
ph = strings.TrimSuffix(ph, ",") for _, x := range t {
where = append(where, "`order`.creator IN ("+ph+")") ids = append(ids, x)
args = append(args, ids...) }
} case []string:
if v, ok := req.Filters["create_time_between"]; ok { for _, x := range t {
var arr []interface{} ids = append(ids, x)
b, _ := json.Marshal(v) }
json.Unmarshal(b, &arr) }
if len(arr) != 2 { return "", nil, errors.New("create_time_between requires 2 values") } if len(ids) == 0 {
where = append(where, "`order`.create_time BETWEEN ? AND ?") return "", nil, errors.New("creator_in required")
args = append(args, arr[0], arr[1]) }
} ph := strings.Repeat("?,", len(ids))
if v, ok := req.Filters["type_eq"]; ok { ph = strings.TrimSuffix(ph, ",")
var tv int where = append(where, "`order`.creator IN ("+ph+")")
switch t := v.(type) { args = append(args, ids...)
case float64: }
tv = int(t) if v, ok := req.Filters["create_time_between"]; ok {
case int: var arr []interface{}
tv = t b, _ := json.Marshal(v)
case string: json.Unmarshal(b, &arr)
// simple digits parsing if len(arr) != 2 {
for i := 0; i < len(t); i++ { c := t[i]; if c<'0'||c>'9' { continue }; tv = tv*10 + int(c-'0') } return "", nil, errors.New("create_time_between requires 2 values")
} }
if tv == 1 || tv == 2 || tv == 3 { where = append(where, "`order`.create_time BETWEEN ? AND ?")
where = append(where, "`order`.type = ?") args = append(args, arr[0], arr[1])
args = append(args, tv) }
} if v, ok := req.Filters["type_eq"]; ok {
} var tv int
if v, ok := req.Filters["out_trade_no_eq"]; ok { switch t := v.(type) {
s := toString(v) case float64:
if s != "" { where = append(where, "`order`.out_trade_no = ?"); args = append(args, s) } tv = int(t)
} case int:
if v, ok := req.Filters["account_eq"]; ok { tv = t
s := toString(v) case string:
if s != "" { where = append(where, "`order`.account = ?"); args = append(args, s) } // simple digits parsing
} for i := 0; i < len(t); i++ {
if v, ok := req.Filters["plan_id_eq"]; ok { c := t[i]
s := toString(v) if c < '0' || c > '9' {
if s != "" { where = append(where, "`order`.plan_id = ?"); args = append(args, s) } continue
} }
if v, ok := req.Filters["key_batch_id_eq"]; ok { tv = tv*10 + int(c-'0')
s := toString(v) }
if s != "" { where = append(where, "`order`.key_batch_id = ?"); args = append(args, s) } }
} if tv == 1 || tv == 2 || tv == 3 {
if v, ok := req.Filters["product_id_eq"]; ok { where = append(where, "`order`.type = ?")
s := toString(v) args = append(args, tv)
if s != "" { where = append(where, "`order`.product_id = ?"); args = append(args, s) } }
} }
if v, ok := req.Filters["reseller_id_eq"]; ok { if v, ok := req.Filters["out_trade_no_eq"]; ok {
s := toString(v) s := toString(v)
if s != "" { where = append(where, "`order`.reseller_id = ?"); args = append(args, s) } if s != "" {
} where = append(where, "`order`.out_trade_no = ?")
if v, ok := req.Filters["code_batch_id_eq"]; ok { args = append(args, s)
s := toString(v) }
if s != "" { where = append(where, "`order`.code_batch_id = ?"); args = append(args, s) } }
} if v, ok := req.Filters["account_eq"]; ok {
if v, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok { s := toString(v)
s := toString(v) if s != "" {
if s != "" { where = append(where, "`order_cash`.cash_activity_id = ?"); args = append(args, s) } where = append(where, "`order`.account = ?")
} args = append(args, s)
if v, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok { }
s := toString(v) }
if s != "" { where = append(where, "`order_voucher`.channel_activity_id = ?"); args = append(args, s) } if v, ok := req.Filters["plan_id_eq"]; ok {
} s := toString(v)
if v, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok { if s != "" {
s := toString(v) where = append(where, "`order`.plan_id = ?")
if s != "" { where = append(where, "`voucher_batch`.channel_activity_id = ?"); args = append(args, s) } args = append(args, s)
} }
if v, ok := req.Filters["merchant_out_biz_no_eq"]; ok { }
s := toString(v) if v, ok := req.Filters["key_batch_id_eq"]; ok {
if s != "" { where = append(where, "`merchant_key_send`.out_biz_no = ?"); args = append(args, s) } s := toString(v)
} if s != "" {
if len(where) > 0 { where = append(where, "`order`.key_batch_id = ?")
sb.WriteString(" WHERE ") args = append(args, s)
sb.WriteString(strings.Join(where, " AND ")) }
} }
return sb.String(), args, nil if v, ok := req.Filters["product_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`order`.product_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["reseller_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`order`.reseller_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["code_batch_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`order`.code_batch_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`order_cash`.cash_activity_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`order_voucher`.channel_activity_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`voucher_batch`.channel_activity_id = ?")
args = append(args, s)
}
}
if v, ok := req.Filters["merchant_out_biz_no_eq"]; ok {
s := toString(v)
if s != "" {
where = append(where, "`merchant_key_send`.out_biz_no = ?")
args = append(args, s)
}
}
if len(where) > 0 {
sb.WriteString(" WHERE ")
sb.WriteString(strings.Join(where, " AND "))
}
return sb.String(), args, nil
} }
func escape(s string) string { func escape(s string) string {
if s == "key" { return "`key`" } if s == "key" {
return s return "`key`"
}
return s
} }
func toString(v interface{}) string { func toString(v interface{}) string {
switch t := v.(type) { switch t := v.(type) {
case []byte: case []byte:
return string(t) return string(t)
case string: case string:
return t return t
case int64: case int64:
return strconv.FormatInt(t, 10) return strconv.FormatInt(t, 10)
case int: case int:
return strconv.Itoa(t) return strconv.Itoa(t)
case float64: case float64:
return strconv.FormatFloat(t, 'f', -1, 64) return strconv.FormatFloat(t, 'f', -1, 64)
default: default:
return "" return ""
} }
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
订单编号,创建者ID,支付流水号,订单类型,订单状态,合同单价,数量,总金额,支付金额,创建时间,更新时间,计划标题,分销商名称,商品名称,商品图片URL,官方价,成本价,创建时间,更新时间,计划ID,计划标题,状态,开始时间,结束时间,批次ID,批次名称,绑定对象,发放数量,剩余库存,开始时间,结束时间,兑换批次ID,标题,状态,开始时间,结束时间,数量,使用数,库存,商户ID,商户业务号,券码,状态,核销时间,创建时间
1 订单编号 创建者ID 支付流水号 订单类型 订单状态 合同单价 数量 总金额 支付金额 创建时间 更新时间 计划标题 分销商名称 商品名称 商品图片URL 官方价 成本价 创建时间 更新时间 计划ID 计划标题 状态 开始时间 结束时间 批次ID 批次名称 绑定对象 发放数量 剩余库存 开始时间 结束时间 兑换批次ID 标题 状态 开始时间 结束时间 数量 使用数 库存 商户ID 商户业务号 券码 状态 核销时间 创建时间

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -111,15 +111,20 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="字段选择" required show-message prop="fieldsSel"> <el-form-item label="字段选择" required show-message prop="fieldsSel">
<el-cascader <div ref="createCascaderRoot">
ref="fieldsCascader" <el-cascader
v-model="form.fieldsSel" ref="fieldsCascader"
:options="fieldOptions" v-model="form.fieldsSel"
:props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }" :options="fieldOptions"
collapse-tags :props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }"
collapse-tags-tooltip :teleported="false"
placeholder="按场景逐级选择,可多选" collapse-tags
/> collapse-tags-tooltip
placeholder="按场景逐级选择,可多选"
@visible-change="onCascaderVisible('create', $event)"
@change="onFieldsSelChange('create')"
/>
</div>
</el-form-item> </el-form-item>
<el-form-item label="数据权限" required show-message prop="permissionMode"> <el-form-item label="数据权限" required show-message prop="permissionMode">
<el-select v-model="form.permissionMode" style="width:160px"> <el-select v-model="form.permissionMode" style="width:160px">
@ -156,17 +161,56 @@
</el-dialog> </el-dialog>
<el-dialog v-model="editVisible" title="编辑模板" :width="editWidth"> <el-dialog v-model="editVisible" title="编辑模板" :width="editWidth">
<el-form ref="editFormRef" :model="edit" :rules="editRules" label-width="110px"> <el-form ref="editFormRef" :model="edit" :rules="editRules" label-width="110px">
<el-form-item label="模板名称" required show-message prop="name"><el-input v-model="edit.name" /></el-form-item> <el-form-item label="模板名称" required show-message prop="name"><el-input v-model="edit.name" /></el-input></el-form-item>
<el-form-item label="输出格式"> <el-form-item label="数据源">
<el-select v-model="edit.file_format" :teleported="false" placeholder="请选择" style="width:160px"> <el-select v-model="edit.datasource" placeholder="选择" :teleported="false" style="width:160px">
<el-option v-for="opt in formatOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option v-for="opt in datasourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="可见性"> <el-form-item label="导出场景">
<el-select v-model="edit.visibility" clearable :teleported="false" style="width:160px" placeholder="请选择"> <el-select v-model="edit.main_table" placeholder="选择场景" style="width:160px">
<el-option v-for="opt in visibilityOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> <el-option label="订单数据" value="order" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="订单类型" required show-message prop="orderType">
<el-radio-group v-model="edit.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="字段选择" required show-message prop="fieldsSel">
<div ref="editCascaderRoot">
<el-cascader
ref="editFieldsCascader"
v-model="edit.fieldsSel"
:options="editFieldOptions"
:props="{ multiple: true, checkStrictly: false, expandTrigger: 'hover', checkOnClickNode: true, checkOnClickLeaf: true }"
:teleported="false"
collapse-tags
collapse-tags-tooltip
placeholder="按场景逐级选择,可多选"
@visible-change="onCascaderVisible('edit', $event)"
@change="onFieldsSelChange('edit')"
/>
</div>
</el-form-item>
<el-row :gutter="8">
<el-col :span="12">
<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-col>
<el-col :span="12">
<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-col>
</el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="resizeDialog('edit', -100)">缩小</el-button> <el-button @click="resizeDialog('edit', -100)">缩小</el-button>

View File

@ -29,8 +29,8 @@ const { createApp, reactive } = Vue;
editVisible: false, editVisible: false,
exportVisible: false, exportVisible: false,
createWidth: (localStorage.getItem('tplDialogWidth') || '900px'), createWidth: (localStorage.getItem('tplDialogWidth') || '900px'),
editWidth: (localStorage.getItem('tplEditDialogWidth') || '600px'), editWidth: (localStorage.getItem('tplEditDialogWidth') || '900px'),
edit: { id: null, name: '', visibility: 'private', file_format: 'csv' }, edit: { id: null, name: '', datasource: 'marketing', main_table: 'order', orderType: 1, fieldsSel: [], visibility: 'private', file_format: 'xlsx' },
exportForm: { tplId: null, datasource: 'marketing', file_format: 'xlsx', dateRange: [], creatorIds: [], creatorIdsRaw: '', resellerId: null, planId: null, keyBatchId: null, codeBatchId: null, productId: null, outTradeNo: '', account: '', cashActivityId: '', voucherChannelActivityId: '', voucherBatchChannelActivityId: '', outBizNo: '' }, exportForm: { tplId: null, datasource: 'marketing', file_format: 'xlsx', dateRange: [], creatorIds: [], creatorIdsRaw: '', resellerId: null, planId: null, keyBatchId: null, codeBatchId: null, productId: null, outTradeNo: '', account: '', cashActivityId: '', voucherChannelActivityId: '', voucherBatchChannelActivityId: '', outBizNo: '' },
exportTpl: { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' } exportTpl: { id: null, filters: {}, main_table: '', fields: [], datasource: '', file_format: '' }
}) })
@ -40,14 +40,27 @@ const { createApp, reactive } = Vue;
marketing: { marketing: {
order: [ order: [
{ value: 'order_number', label: '订单编号' }, { value: 'order_number', label: '订单编号' },
{ value: 'key', label: 'KEY' },
{ value: 'creator', label: '创建者ID' }, { value: 'creator', label: '创建者ID' },
{ value: 'out_trade_no', label: '支付流水号' }, { value: 'out_trade_no', label: '支付流水号' },
{ value: 'type', label: '订单类型' }, { value: 'type', label: '订单类型' },
{ value: 'status', label: '订单状态' }, { value: 'status', label: '订单状态' },
{ value: 'account', label: '账号' },
{ value: 'product_id', label: '商品ID' },
{ value: 'reseller_id', label: '分销商ID' },
{ value: 'plan_id', label: '计划ID' },
{ value: 'key_batch_id', label: 'KEY批次ID' },
{ value: 'code_batch_id', label: '兑换批次ID' },
{ value: 'contract_price', label: '合同单价' }, { value: 'contract_price', label: '合同单价' },
{ value: 'num', label: '数量' }, { value: 'num', label: '数量' },
{ value: 'total', label: '总金额' }, { value: 'total', label: '总金额' },
{ value: 'pay_amount', label: '支付金额' }, { value: 'pay_amount', label: '支付金额' },
{ value: 'pay_type', label: '支付方式' },
{ value: 'pay_status', label: '支付状态' },
{ value: 'use_coupon', label: '是否使用优惠券' },
{ value: 'deliver_status', label: '投递状态' },
{ value: 'expire_time', label: '过期处理时间' },
{ value: 'recharge_time', label: '充值时间' },
{ value: 'create_time', label: '创建时间' }, { value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' } { value: 'update_time', label: '更新时间' }
] ]
@ -79,6 +92,7 @@ const { createApp, reactive } = Vue;
{ value: 'channel_activity_id', label: '渠道立减金批次' }, { value: 'channel_activity_id', label: '渠道立减金批次' },
{ value: 'channel_voucher_id', label: '渠道立减金ID' }, { value: 'channel_voucher_id', label: '渠道立减金ID' },
{ value: 'status', label: '状态' }, { value: 'status', label: '状态' },
{ value: 'receive_mode', label: '领取方式' },
{ value: 'grant_time', label: '领取时间' }, { value: 'grant_time', label: '领取时间' },
{ value: 'usage_time', label: '核销时间' }, { value: 'usage_time', label: '核销时间' },
{ value: 'refund_time', label: '退款时间' }, { value: 'refund_time', label: '退款时间' },
@ -141,14 +155,27 @@ const { createApp, reactive } = Vue;
ymt: { ymt: {
order: [ order: [
{ value: 'order_number', label: '订单编号' }, { value: 'order_number', label: '订单编号' },
{ value: 'key', label: 'KEY' },
{ value: 'creator', label: '创建者ID' }, { value: 'creator', label: '创建者ID' },
{ value: 'out_trade_no', label: '支付流水号' }, { value: 'out_trade_no', label: '支付流水号' },
{ value: 'type', label: '订单类型' }, { value: 'type', label: '订单类型' },
{ value: 'status', label: '订单状态' }, { value: 'status', label: '订单状态' },
{ value: 'account', label: '账号' },
{ value: 'product_id', label: '商品ID' },
{ value: 'reseller_id', label: '分销商ID' },
{ value: 'plan_id', label: '计划ID' },
{ value: 'key_batch_id', label: 'KEY批次ID' },
{ value: 'code_batch_id', label: '兑换批次ID' },
{ value: 'contract_price', label: '合同单价' }, { value: 'contract_price', label: '合同单价' },
{ value: 'num', label: '数量' }, { value: 'num', label: '数量' },
{ value: 'total', label: '总金额' }, { value: 'total', label: '总金额' },
{ value: 'pay_amount', label: '支付金额' }, { value: 'pay_amount', label: '支付金额' },
{ value: 'pay_type', label: '支付方式' },
{ value: 'pay_status', label: '支付状态' },
{ value: 'use_coupon', label: '是否使用优惠券' },
{ value: 'deliver_status', label: '投递状态' },
{ value: 'expire_time', label: '过期处理时间' },
{ value: 'recharge_time', label: '充值时间' },
{ value: 'create_time', label: '创建时间' }, { value: 'create_time', label: '创建时间' },
{ value: 'update_time', label: '更新时间' } { value: 'update_time', label: '更新时间' }
], ],
@ -179,6 +206,7 @@ const { createApp, reactive } = Vue;
{ value: 'channel_activity_id', label: '渠道立减金批次' }, { value: 'channel_activity_id', label: '渠道立减金批次' },
{ value: 'channel_voucher_id', label: '渠道立减金ID' }, { value: 'channel_voucher_id', label: '渠道立减金ID' },
{ value: 'status', label: '状态' }, { value: 'status', label: '状态' },
{ value: 'receive_mode', label: '领取方式' },
{ value: 'grant_time', label: '领取时间' }, { value: 'grant_time', label: '领取时间' },
{ value: 'usage_time', label: '核销时间' }, { value: 'usage_time', label: '核销时间' },
{ value: 'refund_time', label: '退款时间' }, { value: 'refund_time', label: '退款时间' },
@ -317,6 +345,44 @@ const { createApp, reactive } = Vue;
const createFormRef = Vue.ref(null) const createFormRef = Vue.ref(null)
const exportFormRef = Vue.ref(null) const exportFormRef = Vue.ref(null)
const editFormRef = Vue.ref(null) const editFormRef = Vue.ref(null)
const fieldsCascader = Vue.ref(null)
const editFieldsCascader = Vue.ref(null)
const createCascaderRoot = Vue.ref(null)
const editCascaderRoot = Vue.ref(null)
const cascaderScroll = { create: [], edit: [] }
const getWraps = (kind)=>{
const r = kind==='create' ? createCascaderRoot.value : editCascaderRoot.value
const el = r && r.$el ? r.$el : r
if(!el || typeof el.querySelectorAll !== 'function') return []
return Array.from(el.querySelectorAll('.el-cascader__panel .el-scrollbar__wrap'))
}
const onCascaderVisible = (kind, v)=>{
if(!v) return
Vue.nextTick(()=>{
const wraps = getWraps(kind)
cascaderScroll[kind] = wraps.map(w=>w.scrollTop)
wraps.forEach((wrap, idx)=>{
wrap.addEventListener('scroll', (e)=>{ cascaderScroll[kind][idx] = e.target.scrollTop }, { passive: true })
})
const r = kind==='create' ? createCascaderRoot.value : editCascaderRoot.value
const el = r && r.$el ? r.$el : r
if(el && typeof el.querySelectorAll === 'function'){
el.querySelectorAll('.el-cascader-node').forEach(node=>{
node.addEventListener('click', ()=>{
const ws = getWraps(kind)
cascaderScroll[kind] = ws.map(w=>w.scrollTop)
}, { passive: true })
})
}
})
}
const onFieldsSelChange = (kind)=>{
const tops = cascaderScroll[kind] || []
Vue.nextTick(()=>{
const wraps = getWraps(kind)
wraps.forEach((w, idx)=>{ w.scrollTop = tops[idx] || 0 })
})
}
const createRules = { const createRules = {
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
datasource: [{ required: true, message: '请选择数据源', trigger: 'change' }], datasource: [{ required: true, message: '请选择数据源', trigger: 'change' }],
@ -329,12 +395,57 @@ const { createApp, reactive } = Vue;
visibility: [{ required: true, message: '请选择可见性', trigger: 'change' }] visibility: [{ required: true, message: '请选择可见性', trigger: 'change' }]
} }
const editRules = { const editRules = {
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }] name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
orderType: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
fieldsSel: [{ validator: (_rule, val, cb)=>{ if(Array.isArray(val) && val.length>0){ cb() } else { cb(new Error('请至少选择一个字段')) } }, trigger: 'change' }]
} }
const exportRules = { const exportRules = {
tplId: [{ required: true, message: '请选择模板', trigger: 'change' }], tplId: [{ required: true, message: '请选择模板', trigger: 'change' }],
dateRange: [{ validator: (_r, v, cb)=>{ if(Array.isArray(v) && v.length===2){ cb() } else { cb(new Error('请选择时间范围')) } }, trigger: 'change' }] dateRange: [{ validator: (_r, v, cb)=>{ if(Array.isArray(v) && v.length===2){ cb() } else { cb(new Error('请选择时间范围')) } }, trigger: 'change' }]
} }
const editFieldOptions = Vue.computed(()=>{
const ds = state.edit.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.edit.orderType || 0)
const orderNode = node('order', orderChildrenFor(type))
return [ orderNode ]
})
const visibilityOptions = [ const visibilityOptions = [
{ label: '个人', value: 'private' }, { label: '个人', value: 'private' },
{ label: '公共', value: 'public' } { label: '公共', value: 'public' }
@ -545,7 +656,8 @@ const { createApp, reactive } = Vue;
if(!ok){ msg('请完善必填项','error'); return } if(!ok){ msg('请完善必填项','error'); return }
const id = state.exportForm.tplId const id = state.exportForm.tplId
const filters = {} const filters = {}
// 订单类型与权限按模板配置,此处不显示不提交 const tVal = exportType.value
if(tVal != null){ filters.type_eq = Number(tVal) }
if(Array.isArray(state.exportForm.dateRange) && state.exportForm.dateRange.length===2){ filters.create_time_between = [ state.exportForm.dateRange[0], state.exportForm.dateRange[1] ] } if(Array.isArray(state.exportForm.dateRange) && state.exportForm.dateRange.length===2){ filters.create_time_between = [ state.exportForm.dateRange[0], state.exportForm.dateRange[1] ] }
if(state.exportForm.outTradeNo){ filters.out_trade_no_eq = state.exportForm.outTradeNo } if(state.exportForm.outTradeNo){ filters.out_trade_no_eq = state.exportForm.outTradeNo }
if(state.exportForm.account){ filters.account_eq = state.exportForm.account } if(state.exportForm.account){ filters.account_eq = state.exportForm.account }
@ -595,11 +707,54 @@ const { createApp, reactive } = Vue;
localStorage.setItem('tplEditDialogWidth', next) localStorage.setItem('tplEditDialogWidth', next)
} }
} }
const openEdit = (row)=>{ const openEdit = async (row)=>{
state.edit.id = row.id state.edit.id = row.id
state.edit.name = row.name // 加载模板详情以便回填字段
state.edit.visibility = row.visibility try{
state.edit.file_format = row.file_format const res = await fetch(API_BASE + '/api/templates/'+row.id)
const data = await res.json()
const tpl = data?.data || {}
state.edit.name = tpl.name || row.name || ''
state.edit.datasource = tpl.datasource || row.datasource || 'marketing'
state.edit.main_table = tpl.main_table || row.main_table || 'order'
state.edit.file_format = tpl.file_format || row.file_format || 'xlsx'
state.edit.visibility = tpl.visibility || row.visibility || 'private'
const filters = tpl.filters || {}
if(filters && (filters.type_eq != null)){
state.edit.orderType = Number(filters.type_eq)
} else if(Array.isArray(filters?.type_in) && filters.type_in.length===1){
state.edit.orderType = Number(filters.type_in[0])
} else {
state.edit.orderType = 1
}
const fields = Array.isArray(tpl.fields) ? tpl.fields : []
const toPath = (tf)=>{
const parts = String(tf||'').split('.')
if(parts.length!==2) return null
const table = parts[0]
const field = parts[1]
if(table==='order') return ['order', field]
if(table==='order_detail') return ['order','order_detail',field]
if(table==='plan') return ['order','plan',field]
if(table==='key_batch') return ['order','plan','key_batch',field]
if(table==='code_batch') return ['order','plan','key_batch','code_batch',field]
if(table==='order_voucher') return ['order','order_voucher',field]
if(table==='voucher') return ['order','order_voucher','voucher',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==='order_cash') return ['order','order_cash',field]
return null
}
state.edit.fieldsSel = fields.map(toPath).filter(p=>Array.isArray(p) && p.length>=2)
}catch(_e){
state.edit.name = row.name
state.edit.datasource = row.datasource || 'marketing'
state.edit.main_table = row.main_table || 'order'
state.edit.file_format = row.file_format || 'xlsx'
state.edit.visibility = row.visibility || 'private'
state.edit.orderType = 1
state.edit.fieldsSel = []
}
state.editVisible = true state.editVisible = true
} }
const saveEdit = async ()=>{ const saveEdit = async ()=>{
@ -607,7 +762,28 @@ const { createApp, reactive } = Vue;
const ok = formRef ? await formRef.validate().catch(()=>false) : true const ok = formRef ? await formRef.validate().catch(()=>false) : true
if(!ok){ msg('请完善必填项','error'); return } if(!ok){ msg('请完善必填项','error'); return }
const id = state.edit.id const id = state.edit.id
const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format } // 构建字段与过滤
let fields = []
const ds = state.edit.datasource
if(state.edit.fieldsSel && state.edit.fieldsSel.length){
const hasOrderOnly = state.edit.fieldsSel.some(p=>Array.isArray(p) && p.length===1 && p[0]==='order')
if(hasOrderOnly){
fields = orderLeafPaths(ds).map(p=>`${p[0]}.${p[1]}`)
} else {
fields = state.edit.fieldsSel.flatMap(path=>{
if(!Array.isArray(path)) return []
if(isGroupPath(ds, path)) return []
if(path.length>=2){
const t = path[path.length-2]
const f = path[path.length-1]
return [`${t}.${f}`]
}
return []
})
}
}
const filters = { type_eq: Number(state.edit.orderType || 1) }
const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format, fields, filters }
const res = await fetch(API_BASE + '/api/templates/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}) const res = await fetch(API_BASE + '/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') } if(res.ok){ msg('保存成功'); state.editVisible=false; loadTemplates() } else { msg(await res.text(),'error') }
} }
@ -642,7 +818,7 @@ const { createApp, reactive } = Vue;
} }
loadTemplates() loadTemplates()
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, 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, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT } return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, editFieldOptions, 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, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent, fmtDT, fieldsCascader, editFieldsCascader, createCascaderRoot, editCascaderRoot, onCascaderVisible, onFieldsSelChange }
} }
}) })
app.use(ElementPlus) app.use(ElementPlus)