chore: 添加静态资源文件和版本配置文件

添加大量图片、字体、音频等静态资源文件,包括用户界面图片、模板文件和安装界面素材
添加系统版本配置文件(.version)和常量定义文件(.constant)
更新供应商依赖文件,包括Symfony VarDumper组件和相关许可证文件
添加验证码配置文件(captcha.php)和.gitignore规则
This commit is contained in:
2026-03-06 20:14:23 +08:00
commit c9a98f342c
4485 changed files with 490986 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\user\label;
use app\services\BaseServices;
use app\services\user\UserServices;
use app\jobs\user\UserAutoLabelJob;
use think\facade\Log;
use crmeb\traits\ServicesTrait;
/**
* 用户标签批量处理服务
* Class UserLabelBatchProcessServices
* @package app\services\user\label
*/
class UserLabelBatchProcessServices extends BaseServices
{
use ServicesTrait;
/**
* 批量大小
*/
const BATCH_SIZE = 1000;
/**
* 处理标签变更后的用户批量检查
* @param array $labelIds 标签ID数组
* @param string $action 操作类型:create(新建) 或 update(修改)
* @return bool
*/
public function processLabelChange(array $labelIds, string $action = 'update')
{
try {
Log::info('开始处理标签变更批量用户检查:' . var_export([
'label_ids' => $labelIds,
'action' => $action
], true));
// 获取所有用户ID
$userIds = $this->getAllUserIds();
if (empty($userIds)) {
Log::info('没有找到用户,跳过处理');
return true;
}
// 初始化处理状态缓存
$cacheKey = "label_batch_process_" . implode('_', $labelIds);
$statusData = [
'label_ids' => $labelIds,
'status' => 'processing',
'processed_count' => 0,
'total_count' => count($userIds),
'start_time' => date('Y-m-d H:i:s'),
'end_time' => null,
'message' => '正在处理中...'
];
cache($cacheKey, $statusData, 3600); // 缓存1小时
// 分批处理用户
$batches = array_chunk($userIds, self::BATCH_SIZE);
$totalBatches = count($batches);
Log::info('开始分批处理用户:' . var_export([
'total_users' => count($userIds),
'batch_size' => self::BATCH_SIZE,
'total_batches' => $totalBatches
], true));
$processedCount = 0;
foreach ($batches as $index => $batchUserIds) {
$this->processBatchUsers($batchUserIds, $labelIds, $action, $index + 1, $totalBatches);
$processedCount += count($batchUserIds);
// 更新处理状态
$statusData['processed_count'] = $processedCount;
$statusData['message'] = "已处理 {$processedCount}/{$statusData['total_count']} 个用户";
cache($cacheKey, $statusData, 3600);
}
// 更新最终状态
$statusData['status'] = 'completed';
$statusData['end_time'] = date('Y-m-d H:i:s');
$statusData['message'] = '处理完成';
cache($cacheKey, $statusData, 3600);
Log::info('标签变更批量用户检查处理完成:' . var_export([
'label_ids' => $labelIds,
'total_users' => count($userIds)
], true));
return true;
} catch (\Throwable $e) {
// 更新错误状态
$cacheKey = "label_batch_process_" . implode('_', $labelIds);
$errorStatus = [
'label_ids' => $labelIds,
'status' => 'error',
'processed_count' => 0,
'total_count' => 0,
'start_time' => date('Y-m-d H:i:s'),
'end_time' => date('Y-m-d H:i:s'),
'message' => '处理失败:' . $e->getMessage()
];
cache($cacheKey, $errorStatus, 3600);
Log::error('标签变更批量用户检查处理失败:' . var_export([
'label_ids' => $labelIds,
'action' => $action,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
], true));
return false;
}
}
/**
* 获取所有用户ID
* @return array
*/
protected function getAllUserIds(): array
{
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
// 获取所有启用的用户ID
$where = [
'status' => 1, // 启用状态
'is_del' => 0 // 未删除
];
// 分批获取用户ID,避免内存溢出
$userIds = [];
$page = 1;
$limit = 5000; // 每次获取5000个用户ID
do {
$users = $userServices->getList($where, 'uid', $page, $limit);
$batchIds = array_column($users, 'uid');
$userIds = array_merge($userIds, $batchIds);
$page++;
} while (count($batchIds) === $limit);
return array_unique($userIds);
}
/**
* 处理批次用户
* @param array $userIds 用户ID数组
* @param array $labelIds 标签ID数组
* @param string $action 操作类型
* @param int $batchIndex 当前批次索引
* @param int $totalBatches 总批次数
*/
protected function processBatchUsers(array $userIds, array $labelIds, string $action, int $batchIndex, int $totalBatches)
{
try {
Log::info('处理用户批次:' . var_export([
'batch_index' => $batchIndex,
'total_batches' => $totalBatches,
'user_ids' => $userIds,
'user_count' => count($userIds),
'label_ids' => $labelIds
], true));
// 批量分发自动标签检查任务,每100个用户一个任务
$batchSize = 100;
$userBatches = array_chunk($userIds, $batchSize);
foreach ($userBatches as $userBatch) {
// 分发批量处理任务,传入用户数组而不是单个用户
UserAutoLabelJob::dispatch([
$userBatch,
'label_change', // 触发类型:标签变更
[
'action' => $action,
'changed_labels' => $labelIds,
'timestamp' => time()
],
$labelIds // 只检查变更的标签
]);
}
Log::info('批次用户处理完成:' . var_export([
'batch_index' => $batchIndex,
'user_batches' => count($userBatches),
'dispatched_jobs' => count($userBatches),
'total_users' => count($userIds)
], true));
} catch (\Throwable $e) {
Log::error('处理用户批次失败:' . var_export([
'batch_index' => $batchIndex,
'user_count' => count($userIds),
'error' => $e->getMessage()
], true));
}
}
/**
* 处理单个标签的用户检查
* @param int $labelId 标签ID
* @param string $action 操作类型
* @return bool
*/
public function processSingleLabel(int $labelId, string $action = 'update')
{
return $this->processLabelChange([$labelId], $action);
}
/**
* 获取批量处理状态
* @param int $labelId
* @return array
*/
public function getBatchProcessStatus(int $labelId): array
{
try {
// 从缓存中获取处理状态
$cacheKey = "label_batch_process_{$labelId}";
$status = cache($cacheKey);
if (!$status) {
return [
'label_id' => $labelId,
'status' => 'not_started',
'processed_count' => 0,
'total_count' => 0,
'start_time' => null,
'end_time' => null,
'message' => '未找到处理记录'
];
}
return $status;
} catch (\Exception $e) {
Log::error('获取批量处理状态失败:' . var_export([
'label_id' => $labelId,
'error' => $e->getMessage()
], true));
return [
'label_id' => $labelId,
'status' => 'error',
'processed_count' => 0,
'total_count' => 0,
'start_time' => null,
'end_time' => null,
'message' => '获取状态失败:' . $e->getMessage()
];
}
}
}

View File

@@ -0,0 +1,175 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\user\label;
use app\dao\other\CategoryDao;
use app\services\BaseServices;
use crmeb\services\CacheService;
use crmeb\services\FormBuilder;
use think\annotation\Inject;
use think\exception\ValidateException;
/**
* 用户标签分类
* Class UserLabelCateServices
* @package app\services\user\label
* @mixin CategoryDao
*/
class UserLabelCateServices extends BaseServices
{
/**
* 标签分类缓存
* @var string
*/
protected string $cacheName = 'label_list_all';
/**
* @var CategoryDao
*/
#[Inject]
protected CategoryDao $dao;
/**
* 获取标签分类
* @param array $where
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLabelList(array $where)
{
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getCateList($where, $page, $limit);
$count = $this->dao->count($where);
return compact('list', 'count');
}
/**
* 获取tree数据
* @param array $where
* @param array|string[] $field
* @param array $with
* @return array
*/
public function getLabelTree(array $where, array $field = ['*'], array $with = [])
{
return $this->dao->getCateList($where, 0, 0, $field, $with);
}
/**
* 删除分类缓存
* @param int $type
* @param int $relation_id
* @return bool
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function deleteCateCache(int $type = 0, int $relation_id = 0)
{
$key = $this->cacheName . '_' . $type . '_' . $relation_id;
return CacheService::delete($key);
}
/**
* 获取标签全部分类
* @param int $type
* @param int $relation_id
* @return bool|mixed|null
*/
public function getLabelCateAll(int $type = 0, int $relation_id = 0)
{
$key = $this->cacheName . '_' . $type . '_' . $relation_id;
return CacheService::get($key, function () use ($type, $relation_id) {
return $this->dao->getCateList(['type' => $type, 'owner_id' => 0, 'relation_id' => $relation_id, 'group' => 0]);
});
}
/**
* 标签分类表单
* @param array $cataData
* @return mixed
*/
public function labelCateForm(array $cataData = [])
{
$f[] = FormBuilder::input('name', '分类名称:', $cataData['name'] ?? '')->maxlength(20)->required();
$f[] = FormBuilder::number('sort', '排序:', (int)($cataData['sort'] ?? 0))->min(0);
return $f;
}
/**
* 创建表单
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
*/
public function createForm()
{
return create_form('添加标签分类', $this->labelCateForm(), $this->url('/user/user_label_cate'), 'POST');
}
/**
* 修改分类标签表单
* @param int $id
* @return array
* @throws \FormBuilder\Exception\FormBuilderException
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function updateForm(int $id)
{
$labelCate = $this->dao->get($id);
if (!$labelCate) {
throw new ValidateException('分类标签没有查到');
}
return create_form('编辑标签分类', $this->labelCateForm($labelCate->toArray()), $this->url('user/user_label_cate/' . $id), 'PUT');
}
/**
* 用户标签列表
* @param int $uid
* @param int $type
* @param int $relation_id
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getUserLabel(int $uid = 0, int $type = 0, int $relation_id = 0)
{
$list = $this->dao->getAll(['group' => 0, 'type' => $type, 'relation_id' => $relation_id], ['label']);
$labelIds = [];
if ($uid) {
/** @var UserLabelRelationServices $services */
$services = app()->make(UserLabelRelationServices::class);
$labelIds = $services->getUserLabels($uid, $type, $relation_id);
}
foreach ($list as $key => &$item) {
if (is_array($item['label'])) {
if (!$item['label']) {
unset($list[$key]);
continue;
}
foreach ($item['label'] as &$value) {
if (in_array($value['id'], $labelIds)) {
$value['disabled'] = true;
} else {
$value['disabled'] = false;
}
}
}
}
return array_merge($list);
}
}

View File

@@ -0,0 +1,250 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\services\user\label;
use app\services\BaseServices;
use app\dao\user\label\UserLabelExtendRelationDao;
use crmeb\exceptions\AdminException;
use crmeb\services\FormBuilder as Form;
use FormBuilder\Factory\Iview;
use think\annotation\Inject;
use think\exception\ValidateException;
/**
* 标签规则扩展表
* Class UserLabelExtendRelationServices
* @package app\services\user\label
* @mixin UserLabelExtendRelationDao
*/
class UserLabelExtendRelationServices extends BaseServices
{
/**
* @var UserLabelExtendRelationDao
*/
#[Inject]
protected UserLabelExtendRelationDao $dao;
/**
* 获取某一条标签扩展关联关系
* @param $id
* @return array|\think\Model|null
*/
public function getLabelExtendRelation($id)
{
return $this->dao->get($id);
}
/**
* 获取所有标签扩展关联关系
* @param array $where
* @param array|string[] $field
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLabelExtendRelationList(array $where = [], array $field = ['*'])
{
return $this->dao->getList(0, 0, $where, $field);
}
/**
* 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getList(array $where)
{
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getList($page, $limit, $where);
$count = $this->dao->count($where);
return compact('list', 'count');
}
/**
* 根据左侧ID获取关联关系
* @param int $leftId
* @param int $type
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByLeftId(int $leftId, int $type = 0): array
{
return $this->dao->getByLeftId($leftId, $type);
}
/**
* 根据右侧ID获取关联关系
* @param int $rightId
* @param int $type
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByRightId(int $rightId, int $type = 0): array
{
return $this->dao->getByRightId($rightId, $type);
}
/**
* 根据类型获取关联关系
* @param int $type
* @param array $where
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByType(int $type, array $where = []): array
{
return $this->dao->getByType($type, $where);
}
/**
* 添加修改标签扩展关联关系表单
* @param int $id
* @param int $leftId
* @return mixed
*/
public function add(int $id, int $leftId = 0)
{
$labelExtendRelation = $this->getLabelExtendRelation($id);
$field = array();
// 类型选项
$typeOptions = [
['value' => 1, 'label' => '商品'],
['value' => 2, 'label' => '分类'],
['value' => 3, 'label' => '标签']
];
/** @var UserLabelExtendServices $labelExtendService */
$labelExtendService = app()->make(UserLabelExtendServices::class);
$labelExtendOptions = [];
foreach ($labelExtendService->getLabelExtendList() as $item) {
$labelExtendOptions[] = ['value' => $item['id'], 'label' => '标签扩展规则-' . $item['id']];
}
if (!$labelExtendRelation) {
$title = '添加标签扩展关联关系';
$field[] = Form::select('left_id', '关联标签扩展', $leftId)->setOptions($labelExtendOptions)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择关联标签扩展')->required());
} else {
$title = '编辑标签扩展关联关系';
$field[] = Form::select('left_id', '关联标签扩展', $labelExtendRelation['left_id'])->setOptions($labelExtendOptions)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择关联标签扩展')->required());
}
$field[] = Form::select('type', '类型', $labelExtendRelation['type'] ?? 0)->setOptions($typeOptions)->appendValidate(Iview::validateInt()->message('请选择类型')->required());
$field[] = Form::number('right_id', '关联ID', $labelExtendRelation['right_id'] ?? 0)->appendValidate(Iview::validateInt()->message('请输入关联ID')->required());
return create_form($title, $field, '/admin/user/label_extend_relation/save/' . $id, 'POST');
}
/**
* 保存标签扩展关联关系
* @param int $id
* @param array $data
* @return mixed
*/
public function save(int $id, array $data)
{
if ($id) {
$res = $this->dao->update($id, $data);
if (!$res) throw new AdminException('修改失败');
} else {
$res = $this->dao->save($data);
if (!$res) throw new AdminException('添加失败');
}
return $res;
}
/**
* 删除标签扩展关联关系
* @param int $id
* @return bool
*/
public function del(int $id)
{
$labelExtendRelation = $this->getLabelExtendRelation($id);
if (!$labelExtendRelation) {
throw new AdminException('标签扩展关联关系不存在');
}
return $this->dao->delete($id);
}
/**
* 批量保存关联关系
* @param int $leftId
* @param int $type
* @param array $rightIds
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function saveBatch(int $leftId, int $type, array $rightIds): bool
{
// 先删除原有关联关系
$this->dao->deleteByLeftId($leftId, $type);
// 批量保存新的关联关系
if (!empty($rightIds)) {
$data = [];
foreach ($rightIds as $rightId) {
$data[] = [
'type' => $type,
'left_id' => $leftId,
'right_id' => $rightId
];
}
return $this->dao->saveAll($data);
}
return true;
}
/**
* 获取关联的右侧ID列表
* @param int $leftId
* @param int $type
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getRightIds(int $leftId, int $type = 0): array
{
$list = $this->getByLeftId($leftId, $type);
return array_column($list, 'right_id');
}
/**
* 获取关联的左侧ID列表
* @param int $rightId
* @param int $type
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLeftIds(int $rightId, int $type = 0): array
{
$list = $this->getByRightId($rightId, $type);
return array_column($list, 'left_id');
}
}

View File

@@ -0,0 +1,242 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\services\user\label;
use app\services\BaseServices;
use app\dao\user\label\UserLabelExtendDao;
use app\services\product\category\StoreProductCategoryServices;
use app\services\product\label\StoreProductLabelServices;
use app\services\product\product\StoreProductServices;
use crmeb\exceptions\AdminException;
use crmeb\services\FormBuilder as Form;
use FormBuilder\Factory\Iview;
use think\annotation\Inject;
use think\exception\ValidateException;
/**
* 标签规则关联表
* Class UserLabelExtendServices
* @package app\services\user\label
* @mixin UserLabelExtendDao
*/
class UserLabelExtendServices extends BaseServices
{
/**
* @var UserLabelExtendDao
*/
#[Inject]
protected UserLabelExtendDao $dao;
/**
* 获取某一条标签扩展规则
* @param $id
* @return array|\think\Model|null
*/
public function getLabelExtend($id)
{
return $this->dao->get($id);
}
/**
* 获取所有标签扩展规则
* @param array $where
* @param array|string[] $field
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLabelExtendList(array $where = [], array $field = ['*'])
{
return $this->dao->getList(0, 0, $where, $field);
}
/**
* 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getList(array $where)
{
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getList($page, $limit, $where);
$count = $this->dao->count($where);
return compact('list', 'count');
}
/**
* 根据标签ID获取扩展规则
* @param int $labelId
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByLabelId(int $labelId): array
{
return $this->dao->getByLabelId($labelId);
}
/**
* 根据规则类型获取扩展规则
* @param int $ruleType
* @param array $where
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getByRuleType(int $ruleType, array $where = []): array
{
return $this->dao->getByRuleType($ruleType, $where);
}
/**
* 添加修改标签扩展规则表单
* @param int $id
* @param int $labelId
* @return mixed
*/
public function add(int $id, int $labelId = 0)
{
$labelExtend = $this->getLabelExtend($id);
$field = array();
/** @var UserLabelServices $labelService */
$labelService = app()->make(UserLabelServices::class);
$labelOptions = [];
foreach ($labelService->getLabelList() as $item) {
$labelOptions[] = ['value' => $item['id'], 'label' => $item['label_name']];
}
// 规则类型选项
$ruleTypeOptions = [
['value' => 1, 'label' => '商品'],
['value' => 2, 'label' => '资产'],
['value' => 3, 'label' => '交易'],
['value' => 4, 'label' => '客户(所有)']
];
// 规则子类型选项
$subTypeOptions = [
['value' => 1, 'label' => '积分'],
['value' => 2, 'label' => '余额']
];
// 余额类型选项
$balanceTypeOptions = [
['value' => 1, 'label' => '充值'],
['value' => 2, 'label' => '消耗']
];
// 次数/金额选项
$operationTypeOptions = [
['value' => 1, 'label' => '次数'],
['value' => 2, 'label' => '金额']
];
// 时间维度选项
$timeDimensionOptions = [
['value' => 1, 'label' => '历史'],
['value' => 2, 'label' => '最近'],
['value' => 3, 'label' => '累计']
];
// 商品维度选项
$specifyDimensionOptions = [
['value' => 1, 'label' => '商品'],
['value' => 2, 'label' => '分类'],
['value' => 3, 'label' => '标签']
];
// 客户身份选项
$customerIdentityOptions = [
['value' => 1, 'label' => '注册时间'],
['value' => 2, 'label' => '访问时间'],
['value' => 3, 'label' => '用户等级'],
['value' => 4, 'label' => '客户身份']
];
if (!$labelExtend) {
$title = '添加标签扩展规则';
$field[] = Form::select('label_id', '关联标签', $labelId)->setOptions($labelOptions)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择关联标签')->required());
} else {
$title = '编辑标签扩展规则';
$field[] = Form::select('label_id', '关联标签', $labelExtend['label_id'])->setOptions($labelOptions)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择关联标签')->required());
}
$field[] = Form::select('rule_type', '规则类型', $labelExtend['rule_type'] ?? 0)->setOptions($ruleTypeOptions)->appendValidate(Iview::validateInt()->message('请选择规则类型')->required());
$field[] = Form::select('sub_type', '规则子类型', $labelExtend['sub_type'] ?? 0)->setOptions($subTypeOptions);
$field[] = Form::select('balance_type', '余额类型', $labelExtend['balance_type'] ?? 0)->setOptions($balanceTypeOptions);
$field[] = Form::select('operation_type', '次数/金额', $labelExtend['operation_type'] ?? 0)->setOptions($operationTypeOptions);
$field[] = Form::select('time_dimension', '时间维度', $labelExtend['time_dimension'] ?? 0)->setOptions($timeDimensionOptions);
$field[] = Form::number('time_value', '时间值(天)', $labelExtend['time_value'] ?? 0);
$field[] = Form::select('specify_dimension', '商品维度', $labelExtend['specify_dimension'] ?? 0)->setOptions($specifyDimensionOptions);
$field[] = Form::number('amount_value_min', '金额/数值最小', $labelExtend['amount_value_min'] ?? 0)->precision(2);
$field[] = Form::number('amount_value_max', '金额/数值最大', $labelExtend['amount_value_max'] ?? 0)->precision(2);
$field[] = Form::number('operation_times_min', '操作次数最小', $labelExtend['operation_times_min'] ?? 0);
$field[] = Form::number('operation_times_max', '操作次数最大', $labelExtend['operation_times_max'] ?? 0);
$field[] = Form::select('customer_identity', '客户身份', $labelExtend['customer_identity'] ?? 0)->setOptions($customerIdentityOptions);
$field[] = Form::number('customer_num', '客户身份数据', $labelExtend['customer_num'] ?? 0);
return create_form($title, $field, '/admin/user/label_extend/save/' . $id, 'POST');
}
/**
* 删除标签扩展规则
* @param int $id
* @return bool
*/
public function del(int $id)
{
$labelExtend = $this->getLabelExtend($id);
if (!$labelExtend) {
throw new AdminException('标签扩展规则不存在');
}
return $this->dao->delete($id);
}
/**
* 根据商品维度获取商品/分类/标签信息
* @param int $id 标签扩展ID
* @return array
*/
public function getLabelSpecify(int $id)
{
$labelExtend = $this->getLabelExtend($id);
if (!$labelExtend) {
return [];
}
$product_ids = $labelExtend['product_ids'] ? explode(',', $labelExtend['product_ids']) : [];
$data = [];
switch ($labelExtend['specify_dimension']) {
case 1:
//商品
$data = app()->make(StoreProductServices::class)->search([])->whereIn('id', $product_ids)->column('id,store_name,image');
break;
case 2:
//分类
$data = app()->make(StoreProductCategoryServices::class)->search([])->whereIn('id', $product_ids)->column('id,cate_name');
break;
case 3:
//标签
$data = app()->make(StoreProductLabelServices::class)->search([])->whereIn('id', $product_ids)->column('id,label_name');
break;
}
return $data;
}
}

View File

@@ -0,0 +1,174 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\services\user\label;
use app\services\BaseServices;
use app\dao\user\label\UserLabelRelationDao;
use crmeb\exceptions\AdminException;
use think\annotation\Inject;
/**
* 用户关联标签
* Class UserLabelRelationServices
* @package app\services\user\label
* @mixin UserLabelRelationDao
*/
class UserLabelRelationServices extends BaseServices
{
/**
* @var UserLabelRelationDao
*/
#[Inject]
protected UserLabelRelationDao $dao;
/**
* 获取某个用户标签ids
* @param int $uid
* @param int $type
* @param int $relation_id
* @return array
*/
public function getUserLabels(int $uid, int $type = 0, int $relation_id = 0)
{
return $this->dao->getColumn(['uid' => $uid, 'type' => $type, 'relation_id' => $relation_id], 'label_id', '');
}
/**
* 用户设置标签
* @param $uids
* @param array $labels
* @param int $type
* @param int $relation_id
* @param bool $group
* @return bool
*/
public function setUserLable($uids, array $labels, int $type = 0, int $relation_id = 0, bool $group = false, $label_type = 0)
{
if (!$uids) {
return true;
}
if (!is_array($uids)) {
$uids = [$uids];
}
//增加标签
$data = [];
foreach ($uids as $uid) {
if ($label_type == 0) {
//用户已经存在的标签
$user_label_ids = $this->dao->delete([
['uid', '=', $uid],
['type', '=', $type],
['relation_id', '=', $relation_id]
]);
if ($labels) {
foreach ($labels as $label) {
$label = (int)$label;
if (!$label) continue;
$data[] = ['uid' => $uid, 'label_id' => $label, 'type' => $type, 'relation_id' => $relation_id];
}
}
} elseif ($label_type == 1) {
foreach ($labels as $label) {
$label = (int)$label;
if (!$label) continue;
$data[] = ['uid' => $uid, 'label_id' => $label, 'type' => $type, 'relation_id' => $relation_id];
}
} else {
$this->dao->delete([
['uid', '=', $uid],
['label_id', 'in', $labels],
['type', '=', $type],
['relation_id', '=', $relation_id]
]);
}
}
if ($data) {
if (!$this->dao->saveAll($data))
throw new AdminException('设置标签失败');
}
return true;
}
/**
* 取消用户标签
* @param int $uid
* @param array $labels
* @param int $type
* @param int $relation_id
* @return bool
*/
public function unUserLabel(int $uid, array $labels = [], int $type = 0, int $relation_id = 0)
{
$where = [
['uid', '=', $uid],
['type', '=', $type],
['relation_id', '=', $relation_id]
];
//不传入 清空用户所有标签
if (count($labels)) {
$where[] = ['label_id', 'in', $labels];
}
$this->dao->delete($where);
return true;
}
/**
* 获取用户标签
* @param array $uids
* @param int $type
* @param int $relation_id
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getUserLabelList(array $uids, int $type = 0, int $relation_id = 0)
{
return $this->dao->getLabelList($uids, $type, $relation_id);
}
/**
* 批量获取用户标签关系映射
* @param array $uids 用户ID数组
* @param int $type 类型
* @param int $relation_id 关联ID
* @return array 返回格式:[uid => [label_id1, label_id2, ...]]
*/
public function getUserLabelsMap(array $uids, int $type = 0, int $relation_id = 0): array
{
if (empty($uids)) {
return [];
}
$where = [
'uid' => $uids,
'type' => $type,
'relation_id' => $relation_id
];
$relations = $this->dao->search($where)->field(['uid', 'label_id'])->select()->toArray();
$userLabelsMap = [];
foreach ($uids as $uid) {
$userLabelsMap[$uid] = [];
}
foreach ($relations as $relation) {
$userLabelsMap[$relation['uid']][] = $relation['label_id'];
}
return $userLabelsMap;
}
}

View File

@@ -0,0 +1,554 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\services\user\label;
use app\jobs\user\UserLabelJob;
use app\services\BaseServices;
use app\dao\user\label\UserLabelDao;
use crmeb\exceptions\AdminException;
use crmeb\services\FormBuilder as Form;
use crmeb\services\wechat\Work;
use FormBuilder\Factory\Iview;
use think\annotation\Inject;
use think\exception\ValidateException;
use think\facade\Route as Url;
/**
* 用户标签
* Class UserLabelServices
* @package app\services\user\label
* @mixin UserLabelDao
*/
class UserLabelServices extends BaseServices
{
/**
* @var UserLabelDao
*/
#[Inject]
protected UserLabelDao $dao;
/**
* 获取某一本标签
* @param $id
* @return array|\think\Model|null
*/
public function getLable($id)
{
return $this->dao->get($id);
}
/**
* 获取所有用户标签
* @param array $where
* @param array|string[] $field
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getLabelList(array $where = [], array $field = ['*'])
{
return $this->dao->getList(0, 0, $where, $field);
}
/**
* 获取列表
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getList(array $where)
{
[$page, $limit] = $this->getPageValue();
$list = $this->dao->getList($page, $limit, $where, ['*'], ['category']);
$ids = array_column($list, 'id');
$userCount = app()->make(UserLabelRelationServices::class)->getLabelUserCount($ids ?: []);
$count = $this->dao->count($where);
foreach ($list as &$item) {
$item['user_count'] = $userCount[$item['id']] ?? 0;
}
return compact('list', 'count');
}
/**
* 添加修改标签表单
* @param int $id
* @param int $type
* @param int $relation_id
* @param int $label_cate
* @return mixed
*/
public function add(int $id, int $type = 0, int $relation_id = 0, int $label_cate = 0)
{
$label = $this->getLable($id);
$field = array();
/** @var UserLabelCateServices $service */
$service = app()->make(UserLabelCateServices::class);
$options = [];
foreach ($service->getLabelCateAll($type, $relation_id) as $item) {
$options[] = ['value' => $item['id'], 'label' => $item['name']];
}
if (!$label) {
$title = '添加标签';
$field[] = Form::select('label_cate', '标签分类', $label_cate)->setOptions($options)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择标签分类')->required());
$field[] = Form::input('label_name', '标签名称', '')->maxlength(20)->required('请填写标签名称');
} else {
$title = '修改标签';
$field[] = Form::select('label_cate', '分类', (int)$label->getData('label_cate'))->setOptions($options)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择标签分类')->required());
$field[] = Form::hidden('id', $label->getData('id'));
$field[] = Form::input('label_name', '标签名称', $label->getData('label_name'))->maxlength(20)->required('请填写标签名称');
}
return create_form($title, $field, Url::buildUrl('/user/user_label/save'), 'POST');
}
/**
* 保存标签表单数据
* @param int $id
* @param array $data
* @param int $type
* @param int $relation_id
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function saveData(int $id, array $data, int $type = 0, int $relation_id = 0)
{
if (!$data['label_cate']) {
throw new ValidateException('请选择标签分类');
}
$processedIds = [];
$action = $id ? 'update' : 'create';
$this->transaction(function () use ($id, $data, $type, $relation_id, &$processedIds) {
$data['type'] = $type;
$data['relation_id'] = $relation_id;
unset($data['id']);
if ($id) {
if (!$this->getLable($id)) {
throw new AdminException('数据不存在');
}
$this->dao->update($id, $data);
//删除旧标签
app()->make(UserLabelExtendServices::class)->searchDel(['label_id' => $id]);
app()->make(UserLabelExtendRelationServices::class)->search(['left_id' => $id])->delete();
$processedIds = [$id];
} else {
//组合标签数据
$processedIds = [];
foreach ($data['label_name'] as $key => $item) {
$allLabelData = [
'label_name' => $item,
'label_cate' => $data['label_cate'],
'type' => $data['type'],
'relation_id' => $data['relation_id'],
'label_type' => $data['label_type'],
'is_product' => $data['is_product'],
'is_property' => $data['is_property'],
'is_trade' => $data['is_trade'],
'is_customer' => $data['is_customer'],
'is_condition' => $data['is_condition'],
'status' => $data['status'],
'add_time' => time(),
];
$processedIds[] = $this->dao->save($allLabelData)->id;
}
}
//批量保存
$this->saveExtend($processedIds, $data);
return true;
});
// 如果是自动标签,触发批量用户处理
if (!empty($processedIds) && isset($data['label_type']) && $data['label_type'] == 2) {
try {
/** @var UserLabelBatchProcessServices $batchProcessServices */
$batchProcessServices = app()->make(UserLabelBatchProcessServices::class);
$batchProcessServices->processLabelChange($processedIds, $action);
} catch (\Throwable $e) {
// 记录错误但不影响标签保存
\think\facade\Log::error('触发标签批量处理失败', [
'label_ids' => $processedIds,
'action' => $action,
'error' => $e->getMessage()
]);
}
}
return true;
}
/**
* 获取标签详情
* @param int $id 标签ID
* @return array
* @throws AdminException
*/
public function getInfo($id)
{
$info = $this->getLable($id);
if (!$info) {
throw new AdminException('数据不存在');
}
$info = $info->toArray();
$userLabelExtendServices = app()->make(UserLabelExtendServices::class);
$extendList = $userLabelExtendServices->getLabelExtendList(['label_id' => $id]);
// getLabelSpecify
$specifyId = 0;
foreach ($extendList as &$item) {
switch ($item['rule_type']) {
case 1:
$specifyId = $item['id'];
$product_ids = $item['product_ids'] ? explode(',', $item['product_ids']) : [];
$info['product'] = [
'specify_dimension' => $item['specify_dimension'],
'time_dimension' => $item['time_dimension'],
'time_value' => $item['time_value'],
'ids' => $product_ids,
];
break;
case 2:
$info['property']['property_rule'][] = $item;
break;
case 3:
$info['trade']['trade_rule'][] = $item;
break;
case 4:
if ($item['customer_time_start']) {
$item['customer_time_start'] = date('Y-m-d', $item['customer_time_start']);
}
if ($item['customer_time_end']) {
$item['customer_time_end'] = date('Y-m-d', $item['customer_time_end']);
}
$item['time_val'] = [$item['customer_time_start'], $item['customer_time_end']];
$info['customer']['customer_rule'][] = $item;
break;
}
}
$info['specifyData'] = [];
if ($specifyId) {
$info['specifyData'] = $userLabelExtendServices->getLabelSpecify($specifyId);
}
return $info;
}
/**
* 保存标签扩展规则
* @param array $ids 标签ID数组
* @param array $data 扩展规则数据
* @return bool
*/
public function saveExtend(array $ids, array $data)
{
if (count($ids) == 0 || count($data) == 0) {
return true;
}
$extendData = [];
$relationData = [];
foreach ($ids as $id) {
//组合商品数据
if ($data['is_product'] == 1) {
$product = $data['product'];
foreach ($product['ids'] as $productId) {
$relationData[] = [
'type' => $product['specify_dimension'],
'left_id' => $id,
'right_id' => $productId,
];
}
$extendData[] = [
'label_id' => $id,
'rule_type' => 1,
'time_dimension' => $product['time_dimension'],
'time_value' => $product['time_value'],
'specify_dimension' => $product['specify_dimension'],
'product_ids' => $product['ids'] ? implode(',', $product['ids']) : '',
];
}
//资产
if ($data['is_property'] == 1) {
$property = $data['property'];
foreach ($property['property_rule'] as $item) {
$extendData[] = [
'label_id' => $id,
'rule_type' => 2,
'sub_type' => $item['sub_type'],
'balance_type' => $item['balance_type'],
'operation_type' => $item['operation_type'],
'time_dimension' => $item['time_dimension'],
'amount_value_max' => $item['amount_value_max'],
'amount_value_min' => $item['amount_value_min'],
'operation_times_max' => $item['operation_times_max'],
'operation_times_min' => $item['operation_times_min'],
];
}
}
//交易
if ($data['is_trade'] == 1) {
$trade_rule = $data['trade']['trade_rule'];
foreach ($trade_rule as $item) {
$extendData[] = [
'label_id' => $id,
'rule_type' => 3,
'amount_value_max' => $item['amount_value_max'],
'amount_value_min' => $item['amount_value_min'],
'operation_times_max' => $item['operation_times_max'],
'operation_times_min' => $item['operation_times_min'],
'operation_type' => $item['operation_type'],
'time_dimension' => $item['time_dimension'],
];
}
}
//客户
if ($data['is_customer'] == 1) {
$customer_rule = $data['customer']['customer_rule'];
foreach ($customer_rule as $item) {
$time_val = $item['time_val'];
$customer_time_start = isset($item['time_val'][0]) && $item['time_val'][0] ? strtotime($time_val[0]) : 0;
$customer_time_end = isset($item['time_val'][1]) && $item['time_val'][1] ? strtotime($time_val[1]) + 86399 : 0;
$extendData[] = [
'label_id' => $id,
'rule_type' => 4,
'customer_identity' => $item['customer_identity'],
'customer_num' => $item['customer_num'],
'customer_time_start' => $customer_time_start,
'customer_time_end' => $customer_time_end,
];
}
}
}
foreach ($extendData as $v) {
app()->make(UserLabelExtendServices::class)->save($v);
}
if (count($relationData) > 0) {
app()->make(UserLabelExtendRelationServices::class)->saveAll($relationData);
}
return true;
}
/**
* 删除
* @param $id
* @throws \Exception
*/
public function delLabel(int $id)
{
if ($this->getLable($id)) {
//删除旧标签
app()->make(UserLabelExtendServices::class)->searchDel(['label_id' => $id]);
app()->make(UserLabelExtendRelationServices::class)->search(['left_id' => $id])->delete();
app()->make(UserLabelRelationServices::class)->search(['label_id' => $id])->delete();
if (!$this->dao->delete($id)) {
throw new AdminException('删除失败,请稍候再试!');
}
}
return true;
}
/**
* 同步标签
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function authWorkClientLabel()
{
/** @var UserLabelCateServices $cateService */
$cateService = app()->make(UserLabelCateServices::class);
$data = $cateService->getLabelList(['group' => 0, 'owner_id' => 0]);
if ($data['list']) {
foreach ($data['list'] as $item) {
UserLabelJob::dispatchDo('authLabel', [$item['id'], $item['name']]);
}
}
UserLabelJob::dispatchSece(count($data['list']) + 1, 'authWorkLabel');
return true;
}
/**
* 同步平台标签到企业微信客户
* @param int $cateId
* @param string $groupName
* @return bool
*/
public function addCorpClientLabel(int $cateId, string $groupName)
{
try {
$list = $this->dao->getList(0, 0, ['not_tag_id' => 1, 'type' => 0, 'label_cate' => $cateId], ['label_name as name', 'id']);
if (!$list) {
return true;
}
$data = [];
foreach ($list as $item) {
$data[] = ['name' => $item['name']];
}
$res = Work::addCorpTag($groupName, $data);
/** @var UserLabelCateServices $categoryService */
$categoryService = app()->make(UserLabelCateServices::class);
$categoryService->update($cateId, ['other' => $res['tag_group']['group_id']]);
foreach ($res['tag_group']['tag'] ?? [] as $item) {
foreach ($list as $value) {
if ($item['name'] == $value['name']) {
$this->dao->update($value['id'], ['tag_id' => $item['id']]);
}
}
}
return true;
} catch (\Throwable $e) {
return false;
}
}
/**
* 客户标签同步到平台
* @param array $tagIds
* @param array $group
* @return bool
*/
public function authWorkLabel(array $tagIds = [], array $group = [])
{
$res = Work::getCorpTags($tagIds, $group);
$tagGroup = $res['tag_group'] ?? [];
$cateData = [];
$labelData = [];
$groupIds = [];
/** @var UserLabelCateServices $cateService */
$cateService = app()->make(UserLabelCateServices::class);
$this->transaction(function () use ($tagGroup, $cateData, $cateService, $labelData, $groupIds) {
foreach ($tagGroup as $item) {
if ($id = $cateService->value(['other' => $item['group_id']], 'id')) {
$cateService->update(['id' => $id], ['name' => $item['group_name'], 'other' => $item['group_id'], 'sort' => $item['order']]);
} else {
$cateData[] = [
'name' => $item['group_name'],
'sort' => $item['order'],
'add_time' => $item['create_time'],
'other' => $item['group_id'],
'group' => 0
];
}
$groupIds[] = $item['group_id'];
foreach ($item['tag'] as $tag) {
if ($labelId = $this->dao->value(['tag_id' => $tag['id']], 'id')) {
$this->dao->update($labelId, ['tag_id' => $tag['id']]);
} else {
$labelData[$item['group_id']][] = [
'label_name' => $tag['name'],
'type' => 0,
'tag_id' => $tag['id'],
];
}
}
}
if ($cateData) {
$cateService->saveAll($cateData);
}
$cateIds = $cateService->getColumn([
['other', 'in', $groupIds],
['type', '=', 0],
['owner_id', '=', 0],
['group', '=', 0],
], 'id', 'other');
if ($labelData) {
$saveData = [];
foreach ($labelData as $groupId => $labels) {
$cateId = $cateIds[$groupId];
foreach ($labels as $label) {
$label['label_cate'] = $cateId;
$saveData[] = $label;
}
}
$this->dao->saveAll($saveData);
}
});
$cateService->deleteCateCache();
return true;
}
/**
* 获取同步企业微信的标签数据
* @return array
*/
public function getWorkLabel()
{
/** @var UserLabelCateServices $cateService */
$cateService = app()->make(UserLabelCateServices::class);
$list = $cateService->getLabelTree(['type' => 0, 'owner_id' => 0, 'group' => 0, 'other' => true], ['name', 'id', 'other as value'], [
'label' => function ($query) {
$query->where('tag_id', '<>', '')->where('type', 0)->field(['id', 'label_cate', 'tag_id as value', 'label_name as label']);
}
]);
foreach ($list as &$item) {
$label = $item['label'];
$item['children'] = $label;
unset($item['label']);
$item['label'] = $item['name'];
}
return $list;
}
/**
* 企业微信创建客户标签事件
* @param string $corpId
* @param string $strId
* @param string $type
* @return bool
*/
public function createUserLabel(string $corpId, string $strId, string $type)
{
return $this->authWorkLabel($type === 'tag' ? [$strId] : [], $type === 'tag_group' ? [$strId] : []);
}
/**
* 企业微信更新客户标签事件
* @param string $corpId
* @param string $strId
* @param string $type
* @return bool
*/
public function updateUserLabel(string $corpId, string $strId, string $type)
{
return $this->authWorkLabel($type === 'tag' ? [$strId] : [], $type === 'tag_group' ? [$strId] : []);
}
/**
* 删除标签
* @param string $corpId
* @param string $strId
* @param string $type
*/
public function deleteUserLabel(string $corpId, string $strId, string $type)
{
if ('tag' === $type) {
$this->dao->delete(['tag_id' => $strId]);
} else if ('tag_group' === $type) {
/** @var UserLabelCateServices $cateService */
$cateService = app()->make(UserLabelCateServices::class);
$cateInfo = $cateService->get(['type' => 0, 'owner_id' => 0, 'group' => 0, 'other' => $strId]);
if ($cateInfo) {
$this->dao->delete(['label_cate' => $cateInfo->id, 'type' => 0]);
$cateInfo->delete();
}
}
}
}