package exporter import ( "encoding/json" "errors" "fmt" "server/internal/constants" "server/internal/schema" "server/internal/utils" "strings" ) // isZeroID returns true when the value is effectively empty/zero for ID filters. func isZeroID(v interface{}) bool { switch t := v.(type) { case nil: return true case string: s := strings.TrimSpace(t) return s == "" || s == "0" case int, int32, int64: return fmt.Sprint(t) == "0" case float32, float64: return fmt.Sprint(t) == "0" || fmt.Sprint(t) == "0.0" } return false } 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, constants.BuildCaseWhen("order", "status", constants.MarketingOrderStatus, "order.status")) continue } if f == "type" { cols = append(cols, constants.BuildCaseWhen("order", "type", constants.MarketingOrderType, "order.type")) continue } if f == "pay_type" { cols = append(cols, constants.BuildCaseWhen("order", "pay_type", constants.MarketingPayType, "order.pay_type")) continue } if f == "pay_status" { cols = append(cols, constants.BuildCaseWhen("order", "pay_status", constants.MarketingPayStatus, "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, constants.BuildCaseWhen(mt, "type", constants.YMTOrderType, "order.type")) continue } if f == "recharge_suc_time" { // 仅在充值成功状态下展示充值成功时间,其余状态展示为空 cols = append(cols, "CASE WHEN `"+mt+"`.status = 3 THEN `"+mt+"`.recharge_suc_time ELSE NULL END AS `order.recharge_suc_time`") continue } if f == "status" { cols = append(cols, constants.BuildCaseWhen(mt, "status", constants.YMTOrderStatus, "order.status")) continue } if f == "pay_status" { cols = append(cols, constants.BuildCaseWhen(mt, "pay_status", constants.YMTPayStatus, "order.pay_status")) continue } if f == "is_retry" { cols = append(cols, constants.BuildCaseWhen(mt, "is_retry", constants.YMTIsRetry, "order.is_retry")) continue } if f == "supplier_name" { need["supplier"] = true cols = append(cols, "`supplier`.name AS `order.supplier_name`") continue } if f == "is_inner" { cols = append(cols, constants.BuildCaseWhen(mt, "is_inner", constants.YMTIsInner, "order.is_inner")) continue } } if req.Datasource == "ymt" && t == "activity" { if f == "settlement_type" { cols = append(cols, constants.BuildCaseWhen(mt, "settlement_type", constants.YMTSettlementType, "activity.settlement_type")) continue } } if t == "merchant" { if f == "third_party" { cols = append(cols, constants.BuildCaseWhen(mt, "third_party", constants.ThirdPartyType, "merchant.third_party")) continue } } // Generic mapping for order.is_retry across datasources if t == "order" && f == "is_retry" { cols = append(cols, constants.BuildCaseWhen(mt, "is_retry", constants.YMTIsRetry, "order.is_retry")) continue } // Generic mapping for order.is_inner across datasources if t == "order" && f == "is_inner" { cols = append(cols, constants.BuildCaseWhen(mt, "is_inner", constants.YMTIsInner, "order.is_inner")) continue } if req.Datasource == "ymt" && t == "order_digit" { if f == "order_type" { cols = append(cols, constants.BuildCaseWhen(mt, "order_type", constants.OrderDigitOrderType, "order_digit.order_type")) continue } if f == "sms_channel" { // 短信渠道枚举:1=官方,2=专票 cols = append(cols, constants.BuildCaseWhen(mt, "sms_channel", constants.OrderDigitSmsChannel, "order_digit.sms_channel")) 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 } // normalize merchant_id_eq: skip 0/空,非零转为 merchant_id_in 以便和 creator_in 做 OR if v, ok := req.Filters["merchant_id_eq"]; ok { if isZeroID(v) { delete(req.Filters, "merchant_id_eq") } else { if _, has := req.Filters["merchant_id_in"]; !has { req.Filters["merchant_id_in"] = []interface{}{v} } delete(req.Filters, "merchant_id_eq") } } // normalize reseller_id_eq (YMT 直连 merchant_id) if req.Datasource == "ymt" && (req.MainTable == "order" || req.MainTable == "order_info") { if v, ok := req.Filters["reseller_id_eq"]; ok { if isZeroID(v) { delete(req.Filters, "reseller_id_eq") } else { if _, has := req.Filters["merchant_id_in"]; !has { req.Filters["merchant_id_in"] = []interface{}{v} } delete(req.Filters, "reseller_id_eq") } } } // helper: treat "0"/0/空 as未提供的 ID isZeroID := func(v interface{}) bool { switch t := v.(type) { case nil: return true case string: s := strings.TrimSpace(t) return s == "" || s == "0" case int, int32, int64: return fmt.Sprint(t) == "0" case float32, float64: return fmt.Sprint(t) == "0" || fmt.Sprint(t) == "0.0" } return false } // 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{}: for _, x := range t { if !isZeroID(x) { creatorArgs = append(creatorArgs, x) } } case []int: for _, x := range t { if !isZeroID(x) { creatorArgs = append(creatorArgs, x) } } case []string: for _, x := range t { if !isZeroID(x) { 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{}: for _, x := range t { if !isZeroID(x) { merchantArgs = append(merchantArgs, x) } } case []int: for _, x := range t { if !isZeroID(x) { merchantArgs = append(merchantArgs, x) } } case []string: for _, x := range t { if !isZeroID(x) { 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]) } var tv int if v, ok := req.Filters["type_eq"]; ok { 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 := utils.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 := utils.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 := utils.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 := utils.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 := utils.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), if _, hasIn := req.Filters["merchant_id_in"]; !hasIn { s := utils.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 := utils.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 := utils.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 := utils.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 := utils.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 := utils.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 := false if req.Datasource == "marketing" && req.MainTable == "order" && (need["order_voucher"] || need["voucher"] || need["voucher_batch"] || need["key_batch"] || need["code_batch"]) { needDedupe = true } if req.Datasource == "ymt" && (req.MainTable == "order" || req.MainTable == "order_info") && (need["order_digit"] || need["order_voucher"] || need["voucher"] || need["voucher_batch"]) { needDedupe = true } 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) } // 聚合时按别名分组:主表字段别名始终是 req.MainTable.order_number pkAlias := 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.`" + pkAlias + "`") 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 } // 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 } } // normalize merchant_id_eq / reseller_id_eq for YMT: skip 0/空, 非零转为 merchant_id_in 以便与 creator_in 做 OR if v, ok := req.Filters["merchant_id_eq"]; ok { if isZeroID(v) { delete(req.Filters, "merchant_id_eq") } else { if _, has := req.Filters["merchant_id_in"]; !has { req.Filters["merchant_id_in"] = []interface{}{v} } delete(req.Filters, "merchant_id_eq") } } if req.Datasource == "ymt" && (req.MainTable == "order" || req.MainTable == "order_info") { if v, ok := req.Filters["reseller_id_eq"]; ok { if isZeroID(v) { delete(req.Filters, "reseller_id_eq") } else { if _, has := req.Filters["merchant_id_in"]; !has { req.Filters["merchant_id_in"] = []interface{}{v} } delete(req.Filters, "reseller_id_eq") } } } // 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{}: for _, x := range t { if !isZeroID(x) { creatorArgs = append(creatorArgs, x) } } case []int: for _, x := range t { if !isZeroID(x) { creatorArgs = append(creatorArgs, x) } } case []string: for _, x := range t { if !isZeroID(x) { 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{}: for _, x := range t { if !isZeroID(x) { merchantArgs = append(merchantArgs, x) } } case []int: for _, x := range t { if !isZeroID(x) { merchantArgs = append(merchantArgs, x) } } case []string: for _, x := range t { if !isZeroID(x) { 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" && utils.ToString(v) == "0" { continue } if k == "merchant_id_eq" && isZeroID(v) { 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 := utils.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 }