feat(导出): 添加创建者筛选功能并优化层级筛选逻辑
添加创建者筛选接口和前端组件 在SQL构建器中增加key_batch_id_eq条件 实现创建者-分销商-计划-批次-商品的层级联动筛选 优化前端表单布局和字段禁用逻辑
This commit is contained in:
parent
fb3666acb3
commit
24891fa208
|
|
@ -0,0 +1,83 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CreatorsAPI struct {
|
||||
marketing *sql.DB
|
||||
}
|
||||
|
||||
func CreatorsHandler(marketing *sql.DB) http.Handler {
|
||||
api := &CreatorsAPI{marketing: marketing}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/api/creators")
|
||||
if r.Method == http.MethodGet && p == "" {
|
||||
api.list(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *CreatorsAPI) list(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 2000
|
||||
if limitStr != "" {
|
||||
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 && n <= 10000 { limit = n }
|
||||
}
|
||||
// Try plan table first (creator, creator_name)
|
||||
sql1 := "SELECT DISTINCT creator, COALESCE(creator_name, '') AS name FROM plan WHERE creator IS NOT NULL"
|
||||
args := []interface{}{}
|
||||
if q != "" {
|
||||
sql1 += " AND (CAST(creator AS CHAR) LIKE ? OR creator_name LIKE ?)"
|
||||
like := "%" + q + "%"
|
||||
args = append(args, like, like)
|
||||
}
|
||||
sql1 += " ORDER BY creator ASC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
rows, err := a.marketing.Query(sql1, args...)
|
||||
out := []map[string]interface{}{}
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id sql.NullInt64
|
||||
var name sql.NullString
|
||||
if err := rows.Scan(&id, &name); err != nil { continue }
|
||||
if !id.Valid { continue }
|
||||
m := map[string]interface{}{"id": id.Int64, "name": name.String}
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
// Fallback to order table if empty or error
|
||||
if err != nil || len(out) == 0 {
|
||||
sql2 := "SELECT DISTINCT creator, '' AS name FROM `order` WHERE creator IS NOT NULL"
|
||||
args2 := []interface{}{}
|
||||
if q != "" {
|
||||
sql2 += " AND CAST(creator AS CHAR) LIKE ?"
|
||||
args2 = append(args2, "%"+q+"%")
|
||||
}
|
||||
sql2 += " ORDER BY creator ASC LIMIT ?"
|
||||
args2 = append(args2, limit)
|
||||
rows2, err2 := a.marketing.Query(sql2, args2...)
|
||||
if err2 != nil {
|
||||
fail(w, r, http.StatusInternalServerError, err2.Error())
|
||||
return
|
||||
}
|
||||
defer rows2.Close()
|
||||
out = out[:0]
|
||||
for rows2.Next() {
|
||||
var id sql.NullInt64
|
||||
var name sql.NullString
|
||||
if err := rows2.Scan(&id, &name); err != nil { continue }
|
||||
if !id.Valid { continue }
|
||||
m := map[string]interface{}{"id": id.Int64, "name": name.String}
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
ok(w, r, out)
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
|
|||
mux.Handle("/api/templates/", withAccess(withTrace(TemplatesHandler(metaDB, marketingDB))))
|
||||
mux.Handle("/api/exports", withAccess(withTrace(ExportsHandler(metaDB, marketingDB))))
|
||||
mux.Handle("/api/exports/", withAccess(withTrace(ExportsHandler(metaDB, marketingDB))))
|
||||
mux.Handle("/api/creators", withAccess(withTrace(CreatorsHandler(marketingDB))))
|
||||
mux.Handle("/api/creators/", withAccess(withTrace(CreatorsHandler(marketingDB))))
|
||||
sd := staticDir()
|
||||
mux.Handle("/", http.FileServer(http.Dir(sd)))
|
||||
return mux
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
|
|||
s := toString(v)
|
||||
if s != "" { where = append(where, "`order`.plan_id = ?"); args = append(args, s) }
|
||||
}
|
||||
if v, ok := req.Filters["key_batch_id_eq"]; ok {
|
||||
s := toString(v)
|
||||
if s != "" { where = append(where, "`order`.key_batch_id = ?"); args = append(args, s) }
|
||||
}
|
||||
if v, ok := req.Filters["product_id_eq"]; ok {
|
||||
s := toString(v)
|
||||
if s != "" { where = append(where, "`order`.product_id = ?"); args = append(args, s) }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -153,6 +153,45 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">层级筛选</el-divider>
|
||||
<el-row :gutter="8" v-if="isOrder">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="创建者" prop="creator">
|
||||
<el-select v-model="exportForm.creatorIds" multiple filterable :teleported="false" placeholder="请选择创建者" style="width:100%">
|
||||
<el-option v-for="opt in creatorOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分销商ID" prop="resellerId">
|
||||
<el-input v-model.number="exportForm.resellerId" :disabled="!hasCreators" placeholder="reseller_id" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8" v-if="isOrder">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划ID" prop="planId">
|
||||
<el-input v-model.number="exportForm.planId" :disabled="!hasReseller" placeholder="plan_id" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="KEY批次ID" prop="keyBatchId">
|
||||
<el-input v-model.number="exportForm.keyBatchId" :disabled="!hasPlan" placeholder="key_batch_id" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8" v-if="isOrder">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="兑换批次ID" prop="codeBatchId">
|
||||
<el-input v-model.number="exportForm.codeBatchId" :disabled="!hasKeyBatch" placeholder="code_batch_id" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品ID" prop="productId">
|
||||
<el-input v-model.number="exportForm.productId" :disabled="!hasCodeBatch" placeholder="product_id" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支付流水号" prop="outTradeNo"><el-input v-model="exportForm.outTradeNo" placeholder="out_trade_no" /></el-input></el-form-item>
|
||||
|
|
@ -161,22 +200,7 @@
|
|||
<el-form-item label="账户" prop="account"><el-input v-model="exportForm.account" placeholder="account" /></el-input></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划ID" prop="planId"><el-input v-model.number="exportForm.planId" placeholder="plan_id" /></el-input></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品ID" prop="productId"><el-input v-model.number="exportForm.productId" placeholder="product_id" /></el-input></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分销商ID" prop="resellerId"><el-input v-model.number="exportForm.resellerId" placeholder="reseller_id" /></el-input></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="兑换批次ID" prop="codeBatchId"><el-input v-model.number="exportForm.codeBatchId" placeholder="code_batch_id" /></el-input></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="8" v-if="isOrder">
|
||||
<el-col :span="12" v-if="exportType===3">
|
||||
<el-form-item label="红包批次号" prop="cashActivityId"><el-input v-model="exportForm.cashActivityId" placeholder="order_cash.cash_activity_id" /></el-input></el-form-item>
|
||||
|
|
|
|||
25
web/main.js
25
web/main.js
|
|
@ -23,7 +23,7 @@ const { createApp, reactive } = Vue;
|
|||
createWidth: (localStorage.getItem('tplDialogWidth') || '900px'),
|
||||
editWidth: (localStorage.getItem('tplEditDialogWidth') || '600px'),
|
||||
edit: { id: null, name: '', visibility: 'private', file_format: 'csv' },
|
||||
exportForm: { tplId: null, datasource: 'marketing', file_format: 'xlsx', dateRange: [], outTradeNo: '', account: '', planId: null, productId: null, resellerId: null, codeBatchId: null, 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: '' }
|
||||
})
|
||||
|
||||
|
|
@ -344,6 +344,20 @@ const { createApp, reactive } = Vue;
|
|||
if(v==='ymt') return '易码通'
|
||||
return v || ''
|
||||
}
|
||||
const creatorOptions = Vue.ref([])
|
||||
const hasCreators = Vue.computed(()=> Array.isArray(state.exportForm.creatorIds) && state.exportForm.creatorIds.length>0 )
|
||||
const hasReseller = Vue.computed(()=> !!state.exportForm.resellerId)
|
||||
const hasPlan = Vue.computed(()=> !!state.exportForm.planId)
|
||||
const hasKeyBatch = Vue.computed(()=> !!state.exportForm.keyBatchId)
|
||||
const hasCodeBatch = Vue.computed(()=> !!state.exportForm.codeBatchId)
|
||||
const loadCreators = async ()=>{
|
||||
try{
|
||||
const res = await fetch(API_BASE + '/api/creators')
|
||||
const data = await res.json()
|
||||
const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : [])
|
||||
creatorOptions.value = arr.map(it=>({label: it.name || String(it.id), value: Number(it.id)}))
|
||||
}catch(_e){ creatorOptions.value = [] }
|
||||
}
|
||||
const exportType = Vue.computed(()=>{
|
||||
const f = state.exportTpl && state.exportTpl.filters
|
||||
if(!f) return null
|
||||
|
|
@ -448,6 +462,7 @@ const { createApp, reactive } = Vue;
|
|||
await loadTemplateDetail(row.id)
|
||||
state.exportForm.datasource = state.exportTpl.datasource || row.datasource || 'marketing'
|
||||
state.exportForm.file_format = state.exportTpl.file_format || row.file_format || 'xlsx'
|
||||
if(state.exportForm.datasource==='marketing'){ loadCreators() }
|
||||
state.exportVisible = true
|
||||
}
|
||||
const loadTemplateDetail = async (id)=>{
|
||||
|
|
@ -472,11 +487,14 @@ const { createApp, reactive } = Vue;
|
|||
if(state.exportForm.planId){ filters.plan_id_eq = Number(state.exportForm.planId) }
|
||||
if(state.exportForm.productId){ filters.product_id_eq = Number(state.exportForm.productId) }
|
||||
if(state.exportForm.resellerId){ filters.reseller_id_eq = Number(state.exportForm.resellerId) }
|
||||
if(state.exportForm.keyBatchId){ filters.key_batch_id_eq = Number(state.exportForm.keyBatchId) }
|
||||
if(state.exportForm.codeBatchId){ filters.code_batch_id_eq = Number(state.exportForm.codeBatchId) }
|
||||
if(state.exportForm.cashActivityId){ filters.order_cash_cash_activity_id_eq = state.exportForm.cashActivityId }
|
||||
if(state.exportForm.voucherChannelActivityId){ filters.order_voucher_channel_activity_id_eq = state.exportForm.voucherChannelActivityId }
|
||||
if(state.exportForm.voucherBatchChannelActivityId){ filters.voucher_batch_channel_activity_id_eq = state.exportForm.voucherBatchChannelActivityId }
|
||||
if(state.exportForm.outBizNo){ filters.merchant_out_biz_no_eq = state.exportForm.outBizNo }
|
||||
if(Array.isArray(state.exportForm.creatorIds) && state.exportForm.creatorIds.length){ filters.creator_in = state.exportForm.creatorIds.map(Number) }
|
||||
else if(state.exportForm.creatorIdsRaw){ const arr = String(state.exportForm.creatorIdsRaw).split(',').map(s=>s.trim()).filter(Boolean); if(arr.length){ filters.creator_in = arr } }
|
||||
|
||||
const payload={template_id:Number(id),requested_by:1,permission:{},options:{},filters, file_format: state.exportForm.file_format, datasource: state.exportForm.datasource};
|
||||
const r=await fetch(API_BASE + '/api/exports',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
||||
|
|
@ -485,6 +503,11 @@ const { createApp, reactive } = Vue;
|
|||
state.exportVisible=false
|
||||
if(jid){ loadJob(jid) } else { msg('任务创建返回异常','error') }
|
||||
}
|
||||
Vue.watch(()=>state.exportForm.creatorIds, ()=>{ state.exportForm.resellerId=null; state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.resellerId, ()=>{ state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.planId, ()=>{ state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.keyBatchId, ()=>{ state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.codeBatchId, ()=>{ state.exportForm.productId=null })
|
||||
const clampWidth = (w)=>{
|
||||
const n = Math.max(500, Math.min(1400, w))
|
||||
return n + 'px'
|
||||
|
|
|
|||
Loading…
Reference in New Issue