MarketingSystemDataExportTool/server/internal/exporter/sqlbuilder.go

632 lines
20 KiB
Go

package exporter
import (
"encoding/json"
"errors"
"fmt"
"server/internal/schema"
"strconv"
"strings"
"time"
)
type BuildRequest struct {
MainTable string
Datasource string
Fields []string // table.field
Filters map[string]interface{}
}
func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{}, error) {
if req.MainTable != "order" && req.MainTable != "order_info" {
return "", nil, errors.New("unsupported main table")
}
sch := schema.Get(req.Datasource, req.MainTable)
if req.Datasource == "marketing" && req.MainTable == "order" {
if v, ok := req.Filters["create_time_between"]; ok {
switch t := v.(type) {
case []interface{}:
if len(t) != 2 {
return "", nil, errors.New("create_time_between 需要两个时间值")
}
case []string:
if len(t) != 2 {
return "", nil, errors.New("create_time_between 需要两个时间值")
}
default:
return "", nil, errors.New("create_time_between 格式错误")
}
} else {
return "", nil, errors.New("缺少时间过滤:必须提供 create_time_between")
}
}
cols := []string{}
need := map[string]bool{}
for _, tf := range req.Fields {
// normalize YMT physical names saved previously to logical names
if req.Datasource == "ymt" && strings.HasPrefix(tf, "order_info.") {
tf = strings.Replace(tf, "order_info.", "order.", 1)
}
if whitelist != nil && !whitelist[tf] {
continue
}
parts := strings.Split(tf, ".")
if len(parts) != 2 {
return "", nil, errors.New("invalid field format")
}
t, f := parts[0], parts[1]
if req.Datasource == "marketing" && t == "order_voucher" && f == "channel_batch_no" {
f = "channel_activity_id"
}
if req.Datasource == "ymt" && t == "order_voucher" && f == "channel_activity_id" {
f = "channel_batch_no"
}
need[t] = true
mt := sch.TableName(t)
mf, _ := sch.MapField(t, f)
if req.Datasource == "marketing" && t == "order" && req.MainTable == "order" {
if f == "status" {
cols = append(cols, "CASE `order`.status WHEN 0 THEN '待充值' WHEN 1 THEN '充值中' WHEN 2 THEN '已完成' WHEN 3 THEN '充值失败' WHEN 4 THEN '已取消' WHEN 5 THEN '已过期' WHEN 6 THEN '待支付' END AS `order.status`")
continue
}
if f == "type" {
cols = append(cols, "CASE `order`.type WHEN 1 THEN '直充卡密' WHEN 2 THEN '立减金' WHEN 3 THEN '红包' ELSE '' END AS `order.type`")
continue
}
if f == "pay_type" {
cols = append(cols, "CASE `order`.pay_type WHEN 1 THEN '支付宝' WHEN 5 THEN '微信' ELSE '' END AS `order.pay_type`")
continue
}
if f == "pay_status" {
cols = append(cols, "CASE `order`.pay_status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已退款' ELSE '' END AS `order.pay_status`")
continue
}
if req.Datasource == "marketing" && f == "card_code" {
cols = append(cols, "CASE WHEN LENGTH(`order`.card_code) > 10 THEN CONCAT(SUBSTRING(`order`.card_code,1,6),'****',SUBSTRING(`order`.card_code, LENGTH(`order`.card_code)-3, 4)) ELSE `order`.card_code END AS `order.card_code`")
continue
}
}
if req.Datasource == "ymt" && t == "order" {
if f == "type" {
cols = append(cols, "CASE `"+mt+"`.type WHEN 1 THEN '红包订单' WHEN 2 THEN '直充卡密订单' WHEN 3 THEN '立减金订单' ELSE '' END AS `order.type`")
continue
}
if f == "status" {
cols = append(cols, "CASE `"+mt+"`.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 '订单重置' WHEN 10 THEN '卡单' ELSE '' END AS `order.status`")
continue
}
if f == "pay_status" {
cols = append(cols, "CASE `"+mt+"`.pay_status WHEN 1 THEN '待支付' WHEN 2 THEN '支付中' WHEN 3 THEN '已支付' WHEN 4 THEN '取消支付' WHEN 5 THEN '退款中' WHEN 6 THEN '退款成功' ELSE '' END AS `order.pay_status`")
continue
}
}
if req.Datasource == "ymt" && t == "activity" {
if f == "settlement_type" {
cols = append(cols, "CASE `"+mt+"`.settlement_type WHEN 1 THEN '发放结算' WHEN 2 THEN '打开结算' WHEN 3 THEN '领用结算' WHEN 4 THEN '核销结算' ELSE '' END AS `activity.settlement_type`")
continue
}
}
if req.Datasource == "ymt" && t == "order_digit" {
if f == "order_type" {
cols = append(cols, "CASE `"+mt+"`.order_type WHEN 1 THEN '直充' WHEN 2 THEN '卡密' ELSE '' END AS `order_digit.order_type`")
continue
}
}
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 `order_cash.receive_status`")
continue
}
// YMT 的 order_cash 表无 is_confirm 字段,输出占位常量
if req.Datasource == "ymt" && t == "order_cash" && f == "is_confirm" {
cols = append(cols, "0 AS `order_cash.is_confirm`")
continue
}
if t == "order_cash" && f == "channel" {
cols = append(cols, "CASE `order_cash`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' WHEN 3 THEN '云闪付' ELSE '' END AS `order_cash.channel`")
continue
}
if t == "order_voucher" && f == "channel" {
cols = append(cols, "CASE `order_voucher`.channel WHEN 1 THEN '支付宝' WHEN 2 THEN '微信' WHEN 3 THEN '云闪付' ELSE '' END AS `order_voucher.channel`")
continue
}
if req.Datasource == "ymt" && 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 '已退款' ELSE '' END AS `order_voucher.status`")
continue
}
if t == "order_voucher" && f == "status" {
cols = append(cols, "CASE `order_voucher`.status WHEN 1 THEN '可用' WHEN 2 THEN '已实扣' WHEN 3 THEN '已过期' WHEN 4 THEN '已退款' WHEN 5 THEN '领取失败' WHEN 6 THEN '发放中' WHEN 7 THEN '部分退款' WHEN 8 THEN '已退回' WHEN 9 THEN '发放失败' ELSE '' END AS `order_voucher.status`")
continue
}
if t == "order_voucher" && f == "receive_mode" {
cols = append(cols, "CASE `order_voucher`.receive_mode WHEN 1 THEN '渠道授权用户id' WHEN 2 THEN '手机号或邮箱' ELSE '' END AS `order_voucher.receive_mode`")
continue
}
if t == "order_voucher" && f == "out_biz_no" {
cols = append(cols, "'' AS `order_voucher.out_biz_no`")
continue
}
// Fallback for YMT tables that are not joined in schema: voucher, voucher_batch, merchant_key_send
if req.Datasource == "ymt" && (t == "voucher" || t == "voucher_batch" || t == "merchant_key_send") {
cols = append(cols, "'' AS `"+t+"."+f+"`")
continue
}
cols = append(cols, "`"+mt+"`."+escape(mf)+" AS `"+t+"."+f+"`")
}
if len(cols) == 0 {
return "", nil, errors.New("no fields")
}
sb := strings.Builder{}
baseCols := strings.Join(cols, ",")
sb.WriteString("SELECT ")
sb.WriteString(baseCols)
sb.WriteString(" FROM `" + sch.TableName(req.MainTable) + "`")
args := []interface{}{}
where := []string{}
// 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_voucher_channel_activity_id_eq"]; ok {
need["order_voucher"] = true
}
if _, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok {
need["voucher_batch"] = true
need["voucher"] = true
need["order_voucher"] = true
}
if _, ok := req.Filters["merchant_out_biz_no_eq"]; ok {
need["merchant_key_send"] = true
}
// Handle creator_in and merchant_id_in with OR logic if both exist
var creatorArgs []interface{}
hasCreator := false
if v, ok := req.Filters["creator_in"]; ok {
switch t := v.(type) {
case []interface{}:
creatorArgs = t
case []int:
for _, x := range t {
creatorArgs = append(creatorArgs, x)
}
case []string:
for _, x := range t {
creatorArgs = append(creatorArgs, x)
}
}
if len(creatorArgs) > 0 {
hasCreator = true
}
}
var merchantArgs []interface{}
hasMerchant := false
if v, ok := req.Filters["merchant_id_in"]; ok {
switch t := v.(type) {
case []interface{}:
merchantArgs = t
case []int:
for _, x := range t {
merchantArgs = append(merchantArgs, x)
}
case []string:
for _, x := range t {
merchantArgs = append(merchantArgs, x)
}
}
if len(merchantArgs) > 0 {
hasMerchant = true
}
}
// Apply the logic: if both present, use OR. Else use individual.
if hasCreator && hasMerchant {
cTbl, cCol, cOk := sch.FilterColumn("creator_in")
mTbl, mCol, mOk := sch.FilterColumn("merchant_id_in")
if cOk && mOk {
cPh := strings.Repeat("?,", len(creatorArgs))
cPh = strings.TrimSuffix(cPh, ",")
mPh := strings.Repeat("?,", len(merchantArgs))
mPh = strings.TrimSuffix(mPh, ",")
where = append(where, fmt.Sprintf("(`%s`.%s IN (%s) OR `%s`.%s IN (%s))",
sch.TableName(cTbl), escape(cCol), cPh,
sch.TableName(mTbl), escape(mCol), mPh))
args = append(args, creatorArgs...)
args = append(args, merchantArgs...)
} else if cOk {
// Fallback: only creator valid (e.g. marketing system)
ph := strings.Repeat("?,", len(creatorArgs))
ph = strings.TrimSuffix(ph, ",")
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(cTbl), escape(cCol), ph))
args = append(args, creatorArgs...)
}
} else if hasCreator {
if tbl, col, ok := sch.FilterColumn("creator_in"); ok {
ph := strings.Repeat("?,", len(creatorArgs))
ph = strings.TrimSuffix(ph, ",")
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(tbl), escape(col), ph))
args = append(args, creatorArgs...)
}
}
if v, ok := req.Filters["create_time_between"]; ok {
var arr []interface{}
b, _ := json.Marshal(v)
json.Unmarshal(b, &arr)
if len(arr) != 2 {
return "", nil, errors.New("create_time_between requires 2 values")
}
if tbl, col, ok := sch.FilterColumn("create_time_between"); ok {
where = append(where, fmt.Sprintf("`%s`.%s BETWEEN ? AND ?", sch.TableName(tbl), escape(col)))
}
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 and label mapping
_s := strings.TrimSpace(t)
for i := 0; i < len(_s); i++ {
c := _s[i]
if c < '0' || c > '9' {
continue
}
tv = tv*10 + int(c-'0')
}
if tv == 0 {
if req.Datasource == "ymt" {
if _s == "红包订单" {
tv = 1
}
if _s == "直充卡密订单" {
tv = 2
}
if _s == "立减金订单" {
tv = 3
}
} else {
if _s == "直充卡密" {
tv = 1
}
if _s == "立减金" {
tv = 2
}
if _s == "红包" {
tv = 3
}
}
}
}
if tv == 1 || tv == 2 || tv == 3 {
if tbl, col, ok := sch.FilterColumn("type_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, tv)
}
}
if v, ok := req.Filters["out_trade_no_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("out_trade_no_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["account_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("account_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["plan_id_eq"]; ok {
s := toString(v)
if s != "" && s != "0" {
if tbl, col, ok := sch.FilterColumn("plan_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["key_batch_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("key_batch_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["product_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("product_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["reseller_id_eq"]; ok {
// If merchant_id_in is present, it handles the merchant_id logic (via OR condition),
// so we should skip this strict equality filter to avoid generating "AND merchant_id = '0'".
if _, hasIn := req.Filters["merchant_id_in"]; !hasIn {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("reseller_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
}
if v, ok := req.Filters["code_batch_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("code_batch_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["order_cash_cash_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("order_cash_cash_activity_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["order_voucher_channel_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("order_voucher_channel_activity_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
if v, ok := req.Filters["voucher_batch_channel_activity_id_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("voucher_batch_channel_activity_id_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
args = append(args, s)
}
}
}
if v, ok := req.Filters["merchant_out_biz_no_eq"]; ok {
s := toString(v)
if s != "" {
if tbl, col, ok := sch.FilterColumn("merchant_out_biz_no_eq"); ok {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
}
args = append(args, s)
}
}
// append necessary joins after collecting filter-driven needs
joins := sch.BuildJoins(need, req.MainTable)
for _, j := range joins {
sb.WriteString(j)
}
if len(where) > 0 {
sb.WriteString(" WHERE ")
sb.WriteString(strings.Join(where, " AND "))
}
needDedupe := req.Datasource == "marketing" && req.MainTable == "order" && (need["order_voucher"] || need["voucher"] || need["voucher_batch"] || need["key_batch"] || need["code_batch"])
if needDedupe {
// Extract alias names in order
aliases := make([]string, 0, len(cols))
sources := make([]string, 0, len(cols))
for _, c := range cols {
pos := strings.LastIndex(c, " AS `")
if pos < 0 {
continue
}
alias := c[pos+5:]
if len(alias) == 0 {
continue
}
if alias[len(alias)-1] == '`' {
alias = alias[:len(alias)-1]
}
aliases = append(aliases, alias)
parts := strings.Split(alias, ".")
src := ""
if len(parts) >= 1 {
src = parts[0]
}
sources = append(sources, src)
}
mt := sch.TableName(req.MainTable)
pkCol, _ := sch.MapField(req.MainTable, "order_number")
var out strings.Builder
out.WriteString("SELECT ")
for i := range aliases {
if i > 0 {
out.WriteString(",")
}
alias := aliases[i]
src := sources[i]
if src == "order" {
out.WriteString("sub.`" + alias + "`")
} else {
out.WriteString("MIN(sub.`" + alias + "`) AS `" + alias + "`")
}
}
out.WriteString(" FROM (")
out.WriteString(sb.String())
out.WriteString(") AS sub GROUP BY sub.`" + mt + "." + pkCol + "`")
return out.String(), args, nil
}
return sb.String(), args, nil
}
func escape(s string) string {
if s == "key" {
return "`key`"
}
if s == "index" {
return "`index`"
}
return s
}
func toString(v interface{}) string {
switch t := v.(type) {
case []byte:
return string(t)
case string:
return t
case int64:
return strconv.FormatInt(t, 10)
case int:
return strconv.Itoa(t)
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case time.Time:
return t.Format("2006-01-02 15:04:05")
default:
return ""
}
}
// BuildCountSQL: minimal COUNT for filters-only joins, counting distinct main PK to avoid 1:N duplication
func BuildCountSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{}, error) {
if req.MainTable != "order" && req.MainTable != "order_info" {
return "", nil, errors.New("unsupported main table")
}
sch := schema.Get(req.Datasource, req.MainTable)
mt := sch.TableName(req.MainTable)
pkCol, _ := sch.MapField(req.MainTable, "order_number")
args := []interface{}{}
where := []string{}
need := map[string]bool{}
// mark joins only needed by filters
for k, _ := range req.Filters {
if tbl, _, ok := sch.FilterColumn(k); ok {
need[tbl] = true
}
}
// Handle creator_in and merchant_id_in with OR logic if both exist
var creatorArgs []interface{}
hasCreator := false
if v, ok := req.Filters["creator_in"]; ok {
switch t := v.(type) {
case []interface{}:
creatorArgs = t
case []int:
for _, x := range t {
creatorArgs = append(creatorArgs, x)
}
case []string:
for _, x := range t {
creatorArgs = append(creatorArgs, x)
}
}
if len(creatorArgs) > 0 {
hasCreator = true
}
}
var merchantArgs []interface{}
hasMerchant := false
if v, ok := req.Filters["merchant_id_in"]; ok {
switch t := v.(type) {
case []interface{}:
merchantArgs = t
case []int:
for _, x := range t {
merchantArgs = append(merchantArgs, x)
}
case []string:
for _, x := range t {
merchantArgs = append(merchantArgs, x)
}
}
if len(merchantArgs) > 0 {
hasMerchant = true
}
}
if hasCreator && hasMerchant {
cTbl, cCol, cOk := sch.FilterColumn("creator_in")
mTbl, mCol, mOk := sch.FilterColumn("merchant_id_in")
if cOk && mOk {
cPh := strings.Repeat("?,", len(creatorArgs))
cPh = strings.TrimSuffix(cPh, ",")
mPh := strings.Repeat("?,", len(merchantArgs))
mPh = strings.TrimSuffix(mPh, ",")
where = append(where, fmt.Sprintf("(`%s`.%s IN (%s) OR `%s`.%s IN (%s))",
sch.TableName(cTbl), escape(cCol), cPh,
sch.TableName(mTbl), escape(mCol), mPh))
args = append(args, creatorArgs...)
args = append(args, merchantArgs...)
} else if cOk {
ph := strings.Repeat("?,", len(creatorArgs))
ph = strings.TrimSuffix(ph, ",")
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(cTbl), escape(cCol), ph))
args = append(args, creatorArgs...)
}
} else if hasCreator {
if tbl, col, ok := sch.FilterColumn("creator_in"); ok {
ph := strings.Repeat("?,", len(creatorArgs))
ph = strings.TrimSuffix(ph, ",")
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(tbl), escape(col), ph))
args = append(args, creatorArgs...)
}
}
// build WHERE from other filters
for k, v := range req.Filters {
if k == "creator_in" || k == "merchant_id_in" {
continue
}
if k == "reseller_id_eq" {
if _, has := req.Filters["merchant_id_in"]; has {
continue
}
}
if k == "plan_id_eq" && toString(v) == "0" {
continue
}
if tbl, col, ok := sch.FilterColumn(k); ok {
switch k {
case "create_time_between":
var arr []interface{}
b, _ := json.Marshal(v)
_ = json.Unmarshal(b, &arr)
if len(arr) == 2 {
where = append(where, fmt.Sprintf("`%s`.%s BETWEEN ? AND ?", sch.TableName(tbl), escape(col)))
args = append(args, arr[0], arr[1])
}
default:
s := toString(v)
if s != "" {
where = append(where, fmt.Sprintf("`%s`.%s = ?", sch.TableName(tbl), escape(col)))
args = append(args, s)
}
}
}
}
sb := strings.Builder{}
sb.WriteString("SELECT COUNT(DISTINCT `" + mt + "`." + pkCol + ") FROM `" + mt + "`")
for _, j := range sch.BuildJoins(need, req.MainTable) {
sb.WriteString(j)
}
if len(where) > 0 {
sb.WriteString(" WHERE ")
sb.WriteString(strings.Join(where, " AND "))
}
return sb.String(), args, nil
}