feat(导出): 添加创建者筛选功能并优化层级筛选逻辑

添加创建者筛选接口和前端组件
在SQL构建器中增加key_batch_id_eq条件
实现创建者-分销商-计划-批次-商品的层级联动筛选
优化前端表单布局和字段禁用逻辑
This commit is contained in:
zhouyonggao 2025-11-25 14:33:54 +08:00
parent fb3666acb3
commit 24891fa208
6 changed files with 185 additions and 17 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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'