feat(导出): 支持商户ID筛选并优化用户ID筛选逻辑

- 在URL参数中添加merchantId支持,并处理多个商户ID的情况
- 在SQL构建器中实现creator_in和merchant_id_in的OR逻辑组合查询
- 优化reseller_id_eq和plan_id_eq的过滤条件处理
- 前端添加merchantId参数拼接功能
- 移除docker部署脚本中的固定镜像ID逻辑
This commit is contained in:
zhouyonggao 2025-12-09 18:29:14 +08:00
parent 86a0cc696a
commit 9b77801b04
8 changed files with 2016 additions and 122 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -6,23 +6,18 @@ IMAGE="marketing-system-data-tool"
TAG="$ENV_NAME"
PORT="${PORT:-8077}"
cd "$ROOT_DIR"
FIXED_IMG_ID="254602263cd6"
if docker image inspect "$FIXED_IMG_ID" >/dev/null 2>&1; then
USE_IMAGE="$FIXED_IMG_ID"
# 如果镜像存在,则直接使用;否则构建
if docker image inspect "$IMAGE:$TAG" >/dev/null 2>&1; then
echo "镜像 $IMAGE:$TAG 已存在,跳过构建。"
else
if docker image inspect "$IMAGE:$TAG" >/dev/null 2>&1; then
DOCKER_BUILDKIT=1 docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" \
--cache-from "$IMAGE:$TAG" -t "$IMAGE:$TAG" -f Dockerfile .
else
DOCKER_BUILDKIT=1 docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" \
-t "$IMAGE:$TAG" -f Dockerfile .
fi
USE_IMAGE="$IMAGE:$TAG"
DOCKER_BUILDKIT=1 docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" \
-t "$IMAGE:$TAG" -f Dockerfile .
fi
USE_IMAGE="$IMAGE:$TAG"
mkdir -p log storage/export
CID_NAME="marketing-data-$ENV_NAME"
RUNNING=$(docker inspect -f '{{.State.Running}}' "$CID_NAME" 2>/dev/null || echo false)

View File

@ -117,12 +117,56 @@ func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
}
}
if len(ids) > 0 {
// FORCE set creator_in if URL params are present, even if p.Filters had something else (which is unlikely if mergePermission worked, but let's be safe)
// Actually, we should probably append or merge? For now, let's assume URL overrides or merges if key missing.
// Logic before was: if _, exists := p.Filters["creator_in"]; !exists { ... }
// But if user passed userId in URL, they probably want it to be used.
// If p.Filters["creator_in"] came from `Permission`, it might be the logged-in user.
// If the user is an admin acting as another user (passed in URL), we should probably use the URL one?
// But `mergePermissionIntoFilters` logic is strict.
// Let's keep existing logic: if permission set it, don't override.
// Wait, if permission is empty (e.g. admin), then `creator_in` is missing.
if _, exists := p.Filters["creator_in"]; !exists {
p.Filters["creator_in"] = ids
} else {
// If it exists, should we merge?
// If the existing one is from permission, it's a boundary.
// If we are admin, permission might be empty.
// Let's trust `mergePermissionIntoFilters`.
}
}
}
}
// support multiple merchantId in query: e.g., merchantId=1,2,3 → filters.merchant_id_in
{
midStr := r.URL.Query().Get("merchantId")
if midStr != "" {
parts := strings.Split(midStr, ",")
ids := make([]interface{}, 0, len(parts))
for _, s := range parts {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if n, err := strconv.ParseUint(s, 10, 64); err == nil {
ids = append(ids, n)
}
}
if len(ids) > 0 {
if _, exists := p.Filters["merchant_id_in"]; !exists {
p.Filters["merchant_id_in"] = ids
}
}
}
}
// DEBUG LOGGING
logging.JSON("INFO", map[string]interface{}{
"event": "export_filters_debug",
"filters": p.Filters,
"has_creator_in": hasNonEmptyIDs(p.Filters["creator_in"]),
"has_merchant_id_in": hasNonEmptyIDs(p.Filters["merchant_id_in"]),
})
if ds == "marketing" && (main == "order" || main == "order_info") {
if v, ok := p.Filters["create_time_between"]; ok {
switch t := v.(type) {

View File

@ -178,29 +178,78 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
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 {
ids := []interface{}{}
switch t := v.(type) {
case []interface{}:
ids = t
creatorArgs = t
case []int:
for _, x := range t {
ids = append(ids, x)
creatorArgs = append(creatorArgs, x)
}
case []string:
for _, x := range t {
ids = append(ids, x)
creatorArgs = append(creatorArgs, x)
}
}
if len(ids) > 0 {
ph := strings.Repeat("?,", len(ids))
ph = strings.TrimSuffix(ph, ",")
if tbl, col, ok := sch.FilterColumn("creator_in"); ok {
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(tbl), escape(col), ph))
}
args = append(args, ids...)
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)
@ -281,7 +330,7 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
}
if v, ok := req.Filters["plan_id_eq"]; ok {
s := toString(v)
if s != "" {
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)))
}
@ -307,12 +356,16 @@ func BuildSQL(req BuildRequest, whitelist map[string]bool) (string, []interface{
}
}
if v, ok := req.Filters["reseller_id_eq"]; ok {
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)))
// 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)
}
args = append(args, s)
}
}
if v, ok := req.Filters["code_batch_id_eq"]; ok {
@ -464,30 +517,90 @@ func BuildCountSQL(req BuildRequest, whitelist map[string]bool) (string, []inter
need[tbl] = true
}
}
// build WHERE from filters
// 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 "creator_in":
ids := []interface{}{}
switch t := v.(type) {
case []interface{}:
ids = t
case []int:
for _, x := range t {
ids = append(ids, x)
}
case []string:
for _, x := range t {
ids = append(ids, x)
}
}
if len(ids) > 0 {
ph := strings.Repeat("?,", len(ids))
ph = strings.TrimSuffix(ph, ",")
where = append(where, fmt.Sprintf("`%s`.%s IN (%s)", sch.TableName(tbl), escape(col), ph))
args = append(args, ids...)
}
case "create_time_between":
var arr []interface{}
b, _ := json.Marshal(v)

View File

@ -3,81 +3,107 @@ package schema
type ymtSchema struct{}
func (ymtSchema) TableName(t string) string {
if t == "order" {
return "order_info"
}
return t
if t == "order" {
return "order_info"
}
return t
}
func (s ymtSchema) MapField(t, f string) (string, bool) {
if t == "order" {
switch f {
case "order_number": return "order_no", true
case "key": return "key_code", true
case "creator": return "user_id", true
case "out_trade_no": return "out_order_no", true
case "plan_id": return "activity_id", true
case "reseller_id": return "merchant_id", true
case "product_id": return "goods_id", true
case "pay_amount": return "pay_price", true
case "key_batch_id": return "key_batch_name", true
default:
return f, true
}
}
if t == "order_voucher" {
switch f {
case "channel_activity_id": return "channel_batch_no", true
case "overdue_time": return "expire_time", true
case "account_no": return "account", true
default:
return f, true
}
}
return f, true
if t == "order" {
switch f {
case "order_number":
return "order_no", true
case "key":
return "key_code", true
case "creator":
return "user_id", true
case "out_trade_no":
return "out_order_no", true
case "plan_id":
return "activity_id", true
case "reseller_id":
return "merchant_id", true
case "product_id":
return "goods_id", true
case "pay_amount":
return "pay_price", true
case "key_batch_id":
return "key_batch_name", true
default:
return f, true
}
}
if t == "order_voucher" {
switch f {
case "channel_activity_id":
return "channel_batch_no", true
case "overdue_time":
return "expire_time", true
case "account_no":
return "account", true
default:
return f, true
}
}
return f, true
}
func (s ymtSchema) BuildJoins(need map[string]bool, main string) []string {
out := []string{}
if need["order_cash"] {
out = append(out, " LEFT JOIN `order_cash` ON `order_cash`.order_no = `order_info`.order_no")
}
if need["order_voucher"] {
out = append(out, " LEFT JOIN `order_voucher` ON `order_voucher`.order_no = `order_info`.order_no")
}
if need["order_digit"] {
out = append(out, " LEFT JOIN `order_digit` ON `order_digit`.order_no = `order_info`.order_no")
}
if need["goods_voucher_batch"] {
out = append(out, " LEFT JOIN `goods_voucher_batch` ON `goods_voucher_batch`.channel_batch_no = `order_voucher`.channel_batch_no")
}
if need["goods_voucher_subject_config"] {
out = append(out, " LEFT JOIN `goods_voucher_subject_config` ON `goods_voucher_subject_config`.id = `goods_voucher_batch`.voucher_subject_id")
}
if need["merchant"] {
out = append(out, " LEFT JOIN `merchant` ON `merchant`.id = `order_info`.merchant_id")
}
if need["activity"] {
out = append(out, " LEFT JOIN `activity` ON `activity`.id = `order_info`.activity_id")
}
return out
out := []string{}
if need["order_cash"] {
out = append(out, " LEFT JOIN `order_cash` ON `order_cash`.order_no = `order_info`.order_no")
}
if need["order_voucher"] {
out = append(out, " LEFT JOIN `order_voucher` ON `order_voucher`.order_no = `order_info`.order_no")
}
if need["order_digit"] {
out = append(out, " LEFT JOIN `order_digit` ON `order_digit`.order_no = `order_info`.order_no")
}
if need["goods_voucher_batch"] {
out = append(out, " LEFT JOIN `goods_voucher_batch` ON `goods_voucher_batch`.channel_batch_no = `order_voucher`.channel_batch_no")
}
if need["goods_voucher_subject_config"] {
out = append(out, " LEFT JOIN `goods_voucher_subject_config` ON `goods_voucher_subject_config`.id = `goods_voucher_batch`.voucher_subject_id")
}
if need["merchant"] {
out = append(out, " LEFT JOIN `merchant` ON `merchant`.id = `order_info`.merchant_id")
}
if need["activity"] {
out = append(out, " LEFT JOIN `activity` ON `activity`.id = `order_info`.activity_id")
}
return out
}
func (s ymtSchema) FilterColumn(key string) (string, string, bool) {
switch key {
case "creator_in": return "order", "user_id", true
case "create_time_between": return "order", "create_time", true
case "type_eq": return "order", "type", true
case "out_trade_no_eq": return "order", "out_order_no", true
case "account_eq": return "order", "account", true
case "plan_id_eq": return "order", "activity_id", true
case "key_batch_id_eq": return "order", "key_batch_name", true
case "product_id_eq": return "order", "goods_id", true
case "reseller_id_eq": return "order", "merchant_id", true
case "code_batch_id_eq": return "order", "supplier_product_id", true
case "order_cash_cash_activity_id_eq": return "order_cash", "activity_id", true
case "order_voucher_channel_activity_id_eq": return "order_voucher", "channel_batch_no", true
default:
return "", "", false
}
switch key {
case "creator_in":
return "order", "user_id", true
case "merchant_id_in":
return "order", "merchant_id", true
case "create_time_between":
return "order", "create_time", true
case "type_eq":
return "order", "type", true
case "out_trade_no_eq":
return "order", "out_order_no", true
case "account_eq":
return "order", "account", true
case "plan_id_eq":
return "order", "activity_id", true
case "key_batch_id_eq":
return "order", "key_batch_name", true
case "product_id_eq":
return "order", "goods_id", true
case "reseller_id_eq":
return "order", "merchant_id", true
case "code_batch_id_eq":
return "order", "supplier_product_id", true
case "order_cash_cash_activity_id_eq":
return "order_cash", "activity_id", true
case "order_voucher_channel_activity_id_eq":
return "order_voucher", "channel_batch_no", true
default:
return "", "", false
}
}

1698
server/server.log Normal file

File diff suppressed because one or more lines are too long

BIN
tool.tar

Binary file not shown.

View File

@ -12,10 +12,20 @@ const app = createApp({
const v = sp.get('userId') || sp.get('userid') || sp.get('user_id');
return v && String(v).trim() ? String(v).trim() : '';
};
const getMerchantId = () => {
const sp = new URLSearchParams(window.location.search || '');
const v = sp.get('merchantId') || sp.get('merchantid') || sp.get('merchant_id');
return v && String(v).trim() ? String(v).trim() : '';
};
const qsUser = () => {
const uid = getUserId();
return uid ? ('?userId=' + encodeURIComponent(uid)) : '';
const mid = getMerchantId();
const parts = [];
if (uid) parts.push('userId=' + encodeURIComponent(uid));
if (mid) parts.push('merchantId=' + encodeURIComponent(mid));
return parts.length ? ('?' + parts.join('&')) : '';
};
const msg = (text, type = 'success') => ElementPlus.ElMessage({ message: text, type });