一、原生用户标签体系拆解

1.1 原生标签数据模型

LikeShop 的用户标签功能以“手动标签”为核心。标签本身的定义通过 user.user_label 相关接口管理,核心字段包括 name(标签名称)、remark(备注)、label_type(标签类型:0-手动标签,1-自动标签)。标签的创建和编辑通过 /adminapi/user.user_label/edit 接口完成,参数中 label_type 字段用于标识标签类型。https://www.kancloud.cn/yi_xiang_42/likeshopb2cplus/2505241https://www.kancloud.cn/yi_xiang_42/likeshopb2cplus/2505242

用户与标签的关联通过批量设置接口 /adminapi/user.user/setLabel 实现,请求参数为 user_ids(用户ID数组)和 label_ids(标签ID数组),运营人员在用户管理列表中勾选用户后手动打标。https://www.kancloud.cn/yi_xiang_42/likeshopb2cplus/2505246

在数据存储层面,ls_user 表包含 level(用户等级)字段,但用户标签与用户的关联关系存储在一张独立的关联表中(通常命名为 ls_user_label_relation 或类似),通过 user_id 和 label_id 建立多对多关系。用户搜索条件接口 /adminapi/user.user/otherList 返回的 user_label_list 中包含自动标签和手动标签的混合数据,例如 {"4": "手动标签", "5": "自动标签", "7": "高消费人群"}。https://www.kancloud.cn/yi_xiang_42/likeshopb2cplus/2505245

1.2 原生标签体系的四个核心缺口

缺口一:自动打标未落地。虽然 label_type 字段已预留了“1-自动标签”的枚举,且官方文档中也明确标注了标签类型支持自动标签,但原生系统的用户标签功能定位是“根据用户特征、消费行为等要素给用户分配标签,同一用户可打上多个标签,暂时不支持自动标签”。label_type = 1 的标签在当前版本中仅是一个分类标识,没有配套的自动计算引擎。https://juejin.cn/post/7159369051205500936

缺口二:无规则配置能力。原生标签没有任何规则配置字段,无法定义“什么样的用户应该被打上这个标签”。运营人员只能逐个勾选用户手动打标,无法实现“消费金额超过5000元的用户自动打上高消费标签”这样的自动化逻辑。

缺口三:无标签人群定向能力。原生系统支持按标签筛选用户,但标签仅用于“查看”和“手动操作”,没有与优惠券发放、消息推送等营销动作打通。运营人员需要先按标签筛选出用户列表,再逐个或批量手动发券,流程割裂。

缺口四:无标签更新机制。手动标签一旦打上就永久存在,用户状态发生变化后(如从高消费变为沉默用户),标签不会自动更新或移除,导致标签体系逐渐失真。

1.3 二开策略定位

遵循 LikeShop 的“扩展优先”原则,标签体系二开采用以下策略:

  1. 不修改原生 user.user_label 接口和标签关联表结构,新建独立的自动标签规则表和服务层
  2. 自动标签通过定时任务驱动计算,复用 LikeShop 已有的定时任务体系(后台可配置、php think crontab 调用)
  3. 标签人群定向营销通过扩展优惠券发放接口和消息推送服务实现,在原生接口基础上增加“按标签筛选”能力
  4. 自动标签的规则配置采用“字段 + 运算符 + 值 + 时间窗口”的结构化模型,运营可在后台可视化配置

二、数据库扩展设计

2.1 自动标签规则配置表

这是自动打标的核心配置表,定义每个自动标签的计算规则:

sql

CREATE TABLE `ls_user_label_rule` (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `label_id` int UNSIGNED NOT NULL COMMENT '关联的标签ID(ls_user_label.id)',
  `rule_name` varchar(50) NOT NULL DEFAULT '' COMMENT '规则名称',
  `rule_group` tinyint NOT NULL DEFAULT 1 COMMENT '规则组:同一组内的条件按match_type组合',
  `rule_field` varchar(50) NOT NULL COMMENT '规则字段:total_consume/order_count/avg_order_amount/last_order_time/register_days/province/city/level_id/points',
  `rule_operator` varchar(20) NOT NULL DEFAULT '>=' COMMENT '运算符:>=,<=,>,<,=,!=,in,not_in,between,days_ago',
  `rule_value` varchar(500) NOT NULL DEFAULT '' COMMENT '规则值',
  `match_type` tinyint NOT NULL DEFAULT 1 COMMENT '组内匹配方式:1-满足全部条件(AND),2-满足任一条件(OR)',
  `auto_remove` tinyint NOT NULL DEFAULT 1 COMMENT '不满足条件时是否自动移除标签:0-否,1-是',
  `update_frequency` tinyint NOT NULL DEFAULT 1 COMMENT '更新频率:1-每日,2-每周,3-每月',
  `last_calc_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '上次计算时间',
  `last_calc_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '上次计算命中人数',
  `is_enable` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用',
  `create_time` int UNSIGNED NOT NULL DEFAULT 0,
  `update_time` int UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_label_id` (`label_id`),
  KEY `idx_enable` (`is_enable`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户自动标签规则配置表';

rule_field 支持的核心字段包括:

字段标识含义数据来源
total_consume累计消费金额ls_order 聚合
order_count累计订单数ls_order 聚合
avg_order_amount客单价ls_order 聚合
last_order_time最后下单时间ls_order 聚合
register_days注册天数ls_user.create_time
province / city省份 / 城市ls_user 地址信息
level_id会员等级ls_user.level
points积分余额ls_user.user_integral

2.2 标签计算日志表

记录每次自动标签计算的结果,便于追溯和调试:

sql

CREATE TABLE `ls_user_label_calc_log` (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `label_id` int UNSIGNED NOT NULL COMMENT '标签ID',
  `rule_id` int UNSIGNED NOT NULL COMMENT '规则ID',
  `calc_type` tinyint NOT NULL DEFAULT 1 COMMENT '计算类型:1-全量计算,2-增量计算',
  `matched_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '命中人数',
  `added_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '新增打标人数',
  `removed_count` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '移除标签人数',
  `calc_duration` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '计算耗时(秒)',
  `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-成功,2-失败',
  `error_msg` varchar(500) NOT NULL DEFAULT '' COMMENT '错误信息',
  `create_time` int UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_label_id` (`label_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自动标签计算日志表';

三、自动打标规则引擎实现

3.1 规则解析与 SQL 构建

自动打标的核心是将结构化的规则配置翻译为可执行的 SQL 查询。参考业界自动化标签体系的实践,规则引擎负责将标签定义翻译为高效 SQL。https://developer.aliyun.com/article/1736219

php

class LabelRuleEngine
{
    /**
     * 解析规则并生成SQL查询条件
     */
    public function buildQuery($labelId)
    {
        $rules = UserLabelRuleModel::where('label_id', $labelId)
            ->where('is_enable', 1)
            ->order('rule_group', 'asc')
            ->select();

        if ($rules->isEmpty()) {
            throw new Exception('标签规则不存在或已禁用');
        }

        // 按规则组分组
        $groups = [];
        foreach ($rules as $rule) {
            $groups[$rule['rule_group']][] = $rule;
        }

        $groupQueries = [];
        foreach ($groups as $groupRules) {
            $matchType = $groupRules[0]['match_type'];
            $conditions = [];

            foreach ($groupRules as $rule) {
                $condition = $this->translateRule($rule);
                if ($condition) {
                    $conditions[] = $condition;
                }
            }

            if (!empty($conditions)) {
                $connector = ($matchType == 1) ? ' AND ' : ' OR ';
                $groupQueries[] = '(' . implode($connector, $conditions) . ')';
            }
        }

        if (empty($groupQueries)) return null;

        // 多个规则组之间默认按 AND 组合
        return implode(' AND ', $groupQueries);
    }

    /**
     * 将单条规则翻译为SQL条件片段
     */
    private function translateRule($rule)
    {
        $field = $rule['rule_field'];
        $operator = $rule['rule_operator'];
        $value = $rule['rule_value'];

        // 直接映射到 ls_user 表的字段
        $userFields = ['level_id' => 'level', 'points' => 'user_integral',
            'register_days' => 'create_time'];

        if (isset($userFields[$field])) {
            return $this->buildUserFieldCondition($userFields[$field], $operator, $value);
        }

        // 需要聚合查询的字段(消费行为)
        $aggFields = ['total_consume', 'order_count', 'avg_order_amount', 'last_order_time'];
        if (in_array($field, $aggFields)) {
            return $this->buildAggregateCondition($field, $operator, $value);
        }

        // 地域字段
        if ($field == 'province' || $field == 'city') {
            return $this->buildLocationCondition($field, $operator, $value);
        }

        return null;
    }

    private function buildAggregateCondition($field, $operator, $value)
    {
        $subQuery = $this->getAggregateSubQuery($field);
        return $this->buildOperatorCondition("({$subQuery})", $operator, $value);
    }

    /**
     * 构建聚合子查询
     */
    private function getAggregateSubQuery($field)
    {
        switch ($field) {
            case 'total_consume':
                return "SELECT COALESCE(SUM(order_amount),0) FROM ls_order WHERE user_id = ls_user.id AND pay_status = 1 AND refund_status <> 2";
            case 'order_count':
                return "SELECT COUNT(*) FROM ls_order WHERE user_id = ls_user.id AND pay_status = 1 AND refund_status <> 2";
            case 'avg_order_amount':
                return "SELECT COALESCE(AVG(order_amount),0) FROM ls_order WHERE user_id = ls_user.id AND pay_status = 1 AND refund_status <> 2";
            case 'last_order_time':
                return "SELECT COALESCE(MAX(pay_time),0) FROM ls_order WHERE user_id = ls_user.id AND pay_status = 1";
        }
        return '0';
    }

    private function buildOperatorCondition($expr, $operator, $value)
    {
        switch ($operator) {
            case '>=': return "{$expr} >= " . floatval($value);
            case '<=': return "{$expr} <= " . floatval($value);
            case '>':  return "{$expr} > " . floatval($value);
            case '<':  return "{$expr} < " . floatval($value);
            case '=':  return "{$expr} = " . floatval($value);
            case '!=': return "{$expr} != " . floatval($value);
            case 'days_ago':
                $threshold = time() - (int)$value * 86400;
                return "{$expr} >= {$threshold}";
            case 'in':
                $items = array_map('intval', explode(',', $value));
                return "{$expr} IN (" . implode(',', $items) . ")";
            default: return null;
        }
    }
}

3.2 消费行为自动打标

消费行为打标是自动标签体系的核心场景。常见规则包括:

标签名称规则条件自动移除
高消费人群累计消费 ≥ 5000 元是
高频购买用户订单数 ≥ 10 单且近 30 天有下单是
客单价高端客单价 ≥ 300 元是
沉默用户最后下单时间 ≥ 90 天前是
新客注册天数 ≤ 7 天是

消费行为的打标计算通过聚合查询实现。以“高消费人群”为例,规则配置为 rule_field = 'total_consume'、rule_operator = '>='、rule_value = '5000',生成的 SQL 条件为:

sql

(SELECT COALESCE(SUM(order_amount),0) 
 FROM ls_order 
 WHERE user_id = ls_user.id AND pay_status = 1 AND refund_status <> 2
) >= 5000

3.3 地域自动打标

地域标签基于用户的收货地址省份或城市进行打标。需要在 ls_user 表中关联用户的默认收货地址,或者通过 ls_user_address 表获取用户的常用地址:

php

private function buildLocationCondition($field, $operator, $value)
{
    // 从用户默认地址中获取省市信息
    $addrField = ($field == 'province') ? 'province' : 'city';
    $subQuery = "SELECT COALESCE((SELECT {$addrField} FROM ls_user_address 
        WHERE user_id = ls_user.id AND is_default = 1 LIMIT 1), '')";

    if ($operator == 'in') {
        $items = array_map(function($v) { return "'" . addslashes(trim($v)) . "'"; },
            explode(',', $value));
        return "{$subQuery} IN (" . implode(',', $items) . ")";
    }
    return "{$subQuery} = '" . addslashes($value) . "'";
}

3.4 会员等级自动打标

会员等级标签直接基于 ls_user.level 字段进行打标,计算逻辑最简单:

php

// 规则配置:rule_field = 'level_id', rule_operator = 'in', rule_value = '2,3'
// 生成的SQL条件:ls_user.level IN (2,3)

3.5 定时任务驱动打标

自动标签的计算通过定时任务驱动,复用 LikeShop 已有的定时任务体系。LikeShop 的定时任务可以在管理后台配置和开关,设置路径是「系统设置 → 系统维护 → 定时任务」,后台提供了完整的定时任务 CRUD 接口,包括添加任务(/crontab.crontab/add)。

在 Crontab.php 中新增自动标签计算子任务:

php

class AutoLabelCrontab
{
    /**
     * 自动标签计算任务(每日凌晨执行)
     */
    public function calcAutoLabels()
    {
        $today = date('Y-m-d');
        $now = time();

        // 查询所有启用的自动标签规则
        $rules = UserLabelRuleModel::where('is_enable', 1)
            ->group('label_id')
            ->select();

        foreach ($rules as $rule) {
            $labelId = $rule['label_id'];

            // 判断是否到了该标签的更新周期
            $frequency = $rule['update_frequency'];
            $lastCalcTime = $rule['last_calc_time'];

            if (!$this->shouldCalc($frequency, $lastCalcTime, $now)) {
                continue;
            }

            $startTime = microtime(true);

            try {
                // 1. 构建规则SQL
                $engine = new LabelRuleEngine();
                $whereCondition = $engine->buildQuery($labelId);

                if (!$whereCondition) {
                    continue;
                }

                // 2. 查询命中用户
                $matchedUsers = Db::name('user')
                    ->alias('u')
                    ->whereRaw($whereCondition)
                    ->where('u.status', 1)
                    ->column('u.id');

                $matchedCount = count($matchedUsers);

                // 3. 查询当前已打标用户
                $existingUsers = Db::name('user_label_relation')
                    ->where('label_id', $labelId)
                    ->column('user_id');

                // 4. 计算新增和移除
                $toAdd = array_diff($matchedUsers, $existingUsers);
                $toRemove = array_diff($existingUsers, $matchedUsers);

                // 5. 执行打标和移除
                Db::startTrans();
                try {
                    // 批量新增
                    if (!empty($toAdd)) {
                        $insertData = [];
                        foreach ($toAdd as $userId) {
                            $insertData[] = [
                                'user_id'     => $userId,
                                'label_id'    => $labelId,
                                'create_time' => $now,
                            ];
                        }
                        Db::name('user_label_relation')->insertAll($insertData);
                    }

                    // 自动移除(仅当 auto_remove = 1)
                    if ($rule['auto_remove'] == 1 && !empty($toRemove)) {
                        Db::name('user_label_relation')
                            ->where('label_id', $labelId)
                            ->whereIn('user_id', $toRemove)
                            ->delete();
                    }

                    // 更新规则的最后计算时间
                    UserLabelRuleModel::where('label_id', $labelId)
                        ->update([
                            'last_calc_time'  => $now,
                            'last_calc_count' => $matchedCount,
                        ]);

                    Db::commit();
                } catch (\Exception $e) {
                    Db::rollback();
                    throw $e;
                }

                $duration = round(microtime(true) - $startTime, 2);

                // 6. 记录计算日志
                UserLabelCalcLogModel::create([
                    'label_id'      => $labelId,
                    'rule_id'       => $rule['id'],
                    'calc_type'     => 1,
                    'matched_count' => $matchedCount,
                    'added_count'   => count($toAdd),
                    'removed_count' => $rule['auto_remove'] ? count($toRemove) : 0,
                    'calc_duration' => $duration,
                    'status'        => 1,
                    'create_time'   => $now,
                ]);

            } catch (\Exception $e) {
                UserLabelCalcLogModel::create([
                    'label_id'    => $labelId,
                    'rule_id'     => $rule['id'],
                    'status'      => 2,
                    'error_msg'   => mb_substr($e->getMessage(), 0, 500),
                    'create_time' => $now,
                ]);
                Log::error("自动标签计算失败 label_id={$labelId}: " . $e->getMessage());
            }
        }
    }

    private function shouldCalc($frequency, $lastCalcTime, $now)
    {
        if ($lastCalcTime == 0) return true;
        $lastDate = date('Y-m-d', $lastCalcTime);
        $today = date('Y-m-d');

        switch ($frequency) {
            case 1: return $lastDate != $today; // 每日
            case 2: return (time() - $lastCalcTime) >= 7 * 86400; // 每周
            case 3: return (time() - $lastCalcTime) >= 30 * 86400; // 每月
            default: return true;
        }
    }
}

3.6 后台配置

在“用户管理 → 用户标签 → 自动标签规则”中增加规则配置页面:

配置项类型说明
关联标签下拉选择从已有标签中选择,仅显示 label_type=1 的标签
规则字段下拉选择累计消费/订单数/客单价/最后下单时间/注册天数/省份/城市/会员等级/积分
运算符下拉选择≥、≤、>、<、=、≠、包含于、N天内
规则值文本/数字输入根据字段类型动态调整输入控件
匹配方式单选满足全部条件 / 满足任一条件
自动移除开关不满足条件时是否移除标签
更新频率单选每日/每周/每月
启用状态开关是否启用该规则

四、标签人群定向营销落地

4.1 按标签筛选目标人群

定向营销的第一步是根据标签筛选目标用户。新增标签人群筛选接口,支持多标签组合筛选(同时拥有/拥有任一):

php

class LabelAudienceService
{
    /**
     * 按标签筛选用户
     * @param array $labelIds  标签ID数组
     * @param string $matchType 匹配方式:all-同时拥有所有标签,any-拥有任一标签
     * @param int   $limit      最大返回数量
     */
    public function getUsersByLabels($labelIds, $matchType = 'all', $limit = 0)
    {
        if (empty($labelIds)) {
            throw new Exception('请至少选择一个标签');
        }

        $query = Db::name('user_label_relation')
            ->alias('r')
            ->join('user u', 'r.user_id = u.id')
            ->whereIn('r.label_id', $labelIds)
            ->where('u.status', 1);

        if ($matchType == 'all') {
            // 同时拥有所有标签
            $query->group('r.user_id')
                ->having('COUNT(DISTINCT r.label_id) = ' . count($labelIds));
        } else {
            // 拥有任一标签
            $query->group('r.user_id');
        }

        $userIds = $query->column('r.user_id');

        if ($limit > 0) {
            $userIds = array_slice($userIds, 0, $limit);
        }

        return $userIds;
    }

    /**
     * 预览标签人群数量和分布
     */
    public function previewAudience($labelIds, $matchType = 'all')
    {
        $userIds = $this->getUsersByLabels($labelIds, $matchType);

        // 统计人群的等级分布
        $levelDist = Db::name('user')
            ->whereIn('id', $userIds)
            ->group('level')
            ->field('level, COUNT(*) as cnt')
            ->select();

        // 统计人群的消费总额分布
        $consumeDist = Db::name('order')
            ->whereIn('user_id', $userIds)
            ->where('pay_status', 1)
            ->group('user_id')
            ->field('SUM(order_amount) as total')
            ->select();

        return [
            'total_count'   => count($userIds),
            'level_dist'    => $levelDist,
            'avg_consume'   => count($consumeDist) > 0
                ? round(array_sum(array_column($consumeDist, 'total')) / count($consumeDist), 2) : 0,
        ];
    }
}

4.2 标签人群定向发券

原生优惠券发放接口 marketing.coupon/send 支持传入 send_user(用户ID数组)。在原生接口基础上封装“按标签发券”的方法:

php

class LabelCouponService
{
    /**
     * 按标签人群定向发券
     */
    public function sendByLabel($couponId, $labelIds, $matchType = 'all', $sendNum = 1)
    {
        // 1. 查询目标人群
        $audienceService = new LabelAudienceService();
        $userIds = $audienceService->getUsersByLabels($labelIds, $matchType);

        if (empty($userIds)) {
            throw new Exception('该标签组合下没有匹配的用户');
        }

        // 2. 过滤已达领取上限的用户
        $coupon = CouponModel::find($couponId);
        if (!$coupon) {
            throw new Exception('优惠券不存在');
        }

        $userIds = $this->filterReachedLimitUsers($couponId, $userIds, $coupon);

        if (empty($userIds)) {
            return ['success_count' => 0, 'msg' => '所有用户均已达到该券的领取上限'];
        }

        // 3. 分批发放(每批500人)
        $totalSuccess = 0;
        foreach (array_chunk($userIds, 500) as $batch) {
            $result = CouponService::batchSend($couponId, $batch, $sendNum);
            $totalSuccess += $result['success_count'];
        }

        // 4. 记录发放日志
        LabelMarketingLogModel::create([
            'label_ids'     => implode(',', $labelIds),
            'match_type'    => $matchType,
            'coupon_id'     => $couponId,
            'target_count'  => count($userIds),
            'success_count' => $totalSuccess,
            'create_time'   => time(),
        ]);

        return [
            'target_count'  => count($userIds),
            'success_count' => $totalSuccess,
        ];
    }

    private function filterReachedLimitUsers($couponId, $userIds, $coupon)
    {
        if ($coupon['get_num_type'] == 1) return $userIds; // 不限次数

        // 查询已领取数量
        $receivedCounts = Db::name('coupon_record')
            ->where('coupon_id', $couponId)
            ->whereIn('user_id', $userIds)
            ->group('user_id')
            ->column('COUNT(*) as cnt', 'user_id');

        return array_filter($userIds, function($userId) use ($receivedCounts, $coupon) {
            $count = $receivedCounts[$userId] ?? 0;
            return $count < $coupon['get_num'];
        });
    }
}

4.3 标签人群定向消息推送

定向推送支持站内消息、短信、微信模板消息三种渠道,根据标签人群的规模选择推送策略:

php

class LabelPushService
{
    /**
     * 按标签人群推送消息
     */
    public function pushByLabel($labelIds, $matchType, $pushData, $channel = 'message')
    {
        $audienceService = new LabelAudienceService();
        $userIds = $audienceService->getUsersByLabels($labelIds, $matchType);

        if (empty($userIds)) {
            throw new Exception('目标人群为空');
        }

        // 大规规模人群走队列异步推送
        if (count($userIds) > 100) {
            Queue::push(BatchPushJob::class, [
                'user_ids'  => $userIds,
                'push_data' => $pushData,
                'channel'   => $channel,
            ]);
            return ['target_count' => count($userIds), 'status' => 'queued'];
        }

        // 小规模直接推送
        $successCount = 0;
        foreach ($userIds as $userId) {
            try {
                switch ($channel) {
                    case 'message':
                        MessageService::send($userId, $pushData['title'], $pushData['content']);
                        break;
                    case 'sms':
                        $mobile = UserModel::where('id', $userId)->value('mobile');
                        if ($mobile) {
                            SmsService::send($mobile, $pushData['content']);
                        }
                        break;
                    case 'wechat':
                        WxTemplateService::send($userId, $pushData['template_id'], $pushData['data']);
                        break;
                }
                $successCount++;
            } catch (\Exception $e) {
                Log::error("标签定向推送失败 user_id={$userId}: " . $e->getMessage());
            }
        }

        return ['target_count' => count($userIds), 'success_count' => $successCount];
    }
}

4.4 后台配置

在“营销 → 定向营销”中增加标签人群定向管理页面:

功能说明
人群预览选择标签组合后,实时预览匹配人数、等级分布、平均消费
定向发券选择优惠券和目标标签,一键批量发放
定向推送选择推送渠道和目标标签,编辑推送内容后发送
营销记录查看历史定向营销记录(发券/推送的时间、目标人群、成功数)

五、完整开发步骤总结

5.1 数据库变更清单

操作表名说明
CREATEls_user_label_rule自动标签规则配置表
CREATEls_user_label_calc_log自动标签计算日志表
CREATEls_label_marketing_log标签定向营销记录表

5.2 后端开发清单

模块文件/类工作内容
数据层UserLabelRuleModel规则配置 Model
数据层UserLabelCalcLogModel计算日志 Model
数据层LabelMarketingLogModel营销记录 Model
业务层LabelRuleEngine规则解析与 SQL 构建
业务层LabelAudienceService标签人群筛选与预览
业务层LabelCouponService按标签定向发券
业务层LabelPushService按标签定向推送
命令AutoLabelCrontab自动标签计算定时任务
后台接口UserLabelRuleController规则 CRUD、手动触发计算
后台接口LabelMarketingController人群预览、定向发券、定向推送

5.3 后台前端开发清单

页面工作内容
用户标签 → 自动标签规则规则列表、新建规则(字段+运算符+值)、启用/禁用、手动触发计算
用户标签 → 标签详情展示标签下的用户数、计算日志、命中人数趋势
营销 → 定向营销标签人群预览、定向发券、定向推送、营销记录
系统设置 → 定时任务新增“自动标签计算”任务记录

5.4 开发注意事项

标签计算性能:聚合子查询(如 SUM(order_amount))在大数据量下性能较差。建议为 ls_order 表的 user_id + pay_status 建立联合索引,并考虑将用户的消费统计数据预聚合到 ls_user 表中(如新增 total_consume、order_count 等冗余字段),定时任务定期刷新,标签计算直接读取冗余字段而非实时聚合。

自动移除的边界:auto_remove = 1 时,不满足条件的用户会被移除标签。但需注意:如果用户同时拥有手动标签和自动标签,自动规则的移除操作只应移除自动标签的关联,不应影响手动标签。在关联表中增加 source 字段(1-手动,2-自动)来区分标签来源。

规则冲突处理:同一用户可能同时满足“高消费人群”和“沉默用户”两个标签的规则。标签之间不应互斥,用户可以同时拥有多个标签。但在定向营销时,运营需要理解标签的语义,避免向沉默用户推荐高频购买类内容。

兼容性:新增表均为独立表,不修改原生 user.user_label 相关接口和表结构。标签关联表的 source 字段新增时设置默认值 1(手动),保证存量数据兼容。自动标签计算任务作为定时任务的新增子任务接入,不影响已有的订单超时关闭、退款扫描等任务。