Merge branch 'develop'
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use plugin\admin\app\common\Auth;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 构建查询工具类
|
||||
* | 方法 | 说明 |
|
||||
* | ------------------------------- | --------------------- |
|
||||
* | `buildQuery($config)` | 构建动态查询(关联加载/字段/过滤/排序) |
|
||||
* | `paginate($q,$per,$page)` | 分页返回 Paginator |
|
||||
* | `getAll($q)` | 不分页直接 `get()` |
|
||||
* | `findByPk($model,$id)` | 根据主键查询单条,找不到抛 404 |
|
||||
* | `findOneBy($model,$where)` | 根据任意条件查询单条,找不到抛 404 |
|
||||
* | `deleteByPk($model,$ids)` | 根据主键删除(可批量),返回删除条数 |
|
||||
* | `deleteBy($model,$where)` | 根据任意条件删除,返回删除条数 |
|
||||
* | `updateByPk($model,$id,$data)` | 根据主键更新,找不到抛 404 |
|
||||
* | `updateBy($model,$where,$data)` | 根据任意条件更新,返回影响行数 |
|
||||
*/
|
||||
|
||||
class CrudUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* 构建查询,支持:
|
||||
* - 本表字段过滤 where
|
||||
* - 关联加载 with
|
||||
* - 关联表字段选择 withFields
|
||||
* - 关联表字段过滤 where with dot-notation
|
||||
* - 排序 order
|
||||
* - 本表字段选择 fields
|
||||
*
|
||||
* $config = [
|
||||
* 'model' => User::class,
|
||||
* 'where' => [
|
||||
* 'status' => 1,
|
||||
* 'name' => ['like','%张%'],
|
||||
* 'roles.name' => '管理员', // 关联表字段过滤
|
||||
* 'posts.created_at'=> ['>','2025-01-01'],
|
||||
* ],
|
||||
* 'order' => ['id'=>'desc'],
|
||||
* 'fields' => ['id','name','email'],
|
||||
* 'with' => ['roles','posts'],
|
||||
* 'withFields' => [
|
||||
* 'roles' => ['id','name'], // 关联表字段选择
|
||||
* 'posts' => ['id','title','user_id'],
|
||||
* ],
|
||||
* ];
|
||||
*/
|
||||
public static function buildQuery(array $config): Builder
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = new $config['model'];
|
||||
$q = $model->newQuery();
|
||||
|
||||
// eager-load:支持 ['rel','rel2'] 和 ['rel'=>fields[], ...]
|
||||
if (! empty($config['with'])) {
|
||||
foreach ($config['with'] as $rel => $fields) {
|
||||
if (is_int($rel)) {
|
||||
// 普通关联
|
||||
$q->with($fields);
|
||||
} else {
|
||||
// 带字段筛选的关联
|
||||
$q->with([$rel => function ($qr) use ($fields) {
|
||||
$key = $qr->getModel()->getKeyName();
|
||||
if (! in_array($key, $fields, true)) {
|
||||
$fields[] = $key;
|
||||
}
|
||||
$qr->select($fields);
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表字段
|
||||
if (!empty($config['fields'])) {
|
||||
$q->select($config['fields']);
|
||||
}
|
||||
|
||||
// where
|
||||
if (!empty($config['where'])) {
|
||||
foreach ($config['where'] as $col => $val) {
|
||||
if (strpos($col, '.') !== false) {
|
||||
// 多层支持
|
||||
$segments = explode('.', $col);
|
||||
$leaf = array_pop($segments);
|
||||
self::applyNestedWhere($q, $segments, $leaf, $val);
|
||||
} else {
|
||||
self::applyWhere($q, $col, $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// or where
|
||||
if (!empty($config['orWhere'])) {
|
||||
$q->where(function($q) use($config) {
|
||||
foreach ($config['orWhere'] as $cond) {
|
||||
[$col,$op,$val] = $cond;
|
||||
$q->orWhere($col,$op,$val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (!empty($config['order'])) {
|
||||
foreach ($config['order'] as $col => $dir) {
|
||||
$q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
// 数据权限过滤
|
||||
if (!empty($config['dataLimit'])) {
|
||||
self::applyDataLimit($q, $config['dataLimit']);
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param int $perPage
|
||||
* @param int $page
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public static function paginate(Builder $q, int $perPage, int $page)
|
||||
{
|
||||
return $q->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getAll(Builder $q)
|
||||
{
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行复杂查询——仅在显式调用时生效
|
||||
*
|
||||
* @param string $modelClass 模型类
|
||||
* @param callable $callback function(Builder $q) { ... }
|
||||
* @return mixed 由 callback 返回的结果
|
||||
*/
|
||||
public static function executeComplex(string $modelClass, callable $cb)
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = new $modelClass;
|
||||
return $cb($model->newQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键单条查询
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param mixed $id
|
||||
* @param array $with
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function findByPk(string $modelClass, $id, array $with = []): Model
|
||||
{
|
||||
$m = $modelClass::with($with)->find($id);
|
||||
if (!$m) {
|
||||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||||
}
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件单条查询,支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function findOneBy(Builder $q): Model
|
||||
{
|
||||
return $q->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建,支持 dataLimit
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param array $data
|
||||
* @param array $dataLimit
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function create(
|
||||
string $modelClass,
|
||||
array $data,
|
||||
array $dataLimit = []
|
||||
): Model {
|
||||
// dataLimit 检查/注入
|
||||
if (!empty($dataLimit)) {
|
||||
self::applyDataLimitOnMutate($data, $dataLimit);
|
||||
}
|
||||
return $modelClass::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键更新,支持 dataLimit
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param mixed $id
|
||||
* @param array $data
|
||||
* @param array $dataLimit
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function updateByPk(
|
||||
string $modelClass,
|
||||
$id,
|
||||
array $data,
|
||||
array $dataLimit = []
|
||||
): Model {
|
||||
/** @var Model $m */
|
||||
$m = $modelClass::find($id);
|
||||
if (!$m) {
|
||||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||||
}
|
||||
// dataLimit 校验
|
||||
if (!empty($dataLimit)) {
|
||||
self::applyDataLimitOnMutate($m, $dataLimit, $data);
|
||||
}
|
||||
$m->fill($data)->save();
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 非主键条件更新(批量),支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array $data
|
||||
* @return int 受影响行数
|
||||
*/
|
||||
public static function updateBy(Builder $q, array $data): int
|
||||
{
|
||||
return $q->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键删除(支持批量)
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param array $ids
|
||||
* @return int 删除条数
|
||||
*/
|
||||
public static function deleteByPk(string $modelClass, array $ids): int
|
||||
{
|
||||
return $modelClass::destroy($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非主键条件删除,支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return int 删除条数
|
||||
*/
|
||||
public static function deleteBy(Builder $q): int
|
||||
{
|
||||
return $q->delete();
|
||||
}
|
||||
|
||||
|
||||
//=================== Internal Methods ===================//
|
||||
|
||||
|
||||
/**
|
||||
* 支持多层的 whereHas:$relations=['roles','permissions'],$leaf='name'
|
||||
*/
|
||||
protected static function applyNestedWhere(Builder $q, array $relations, string $leaf, $val): void
|
||||
{
|
||||
$rel = array_shift($relations);
|
||||
$q->whereHas($rel, function (Builder $qr) use ($relations, $leaf, $val) {
|
||||
if ($relations) {
|
||||
// 还有更深层,继续递归
|
||||
self::applyNestedWhere($qr, $relations, $leaf, $val);
|
||||
} else {
|
||||
// 最后一层,使用普通 applyWhere
|
||||
self::applyWhere($qr, $leaf, $val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 单字段 where 支持:
|
||||
* - 多条件数组 [ [op1,val1], [op2,val2], … ]
|
||||
* - 单条件 ['op', val]
|
||||
* - 直接值
|
||||
*
|
||||
* @param Builder $q
|
||||
* @param string $col
|
||||
* @param mixed $val
|
||||
* @return void
|
||||
*/
|
||||
protected static function applyWhere(Builder $q, string $col, $val): void
|
||||
{
|
||||
// 多条件数组
|
||||
if (is_array($val) && isset($val[0]) && is_array($val[0])) {
|
||||
foreach ($val as $pair) {
|
||||
if (is_array($pair) && count($pair) === 2) {
|
||||
self::applySingleWhere($q, $col, $pair[0], $pair[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 单条件
|
||||
elseif (is_array($val) && count($val) === 2) {
|
||||
self::applySingleWhere($q, $col, $val[0], $val[1]);
|
||||
}
|
||||
// 直接等值
|
||||
else {
|
||||
$q->where($col, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行条件 where
|
||||
*/
|
||||
protected static function applySingleWhere(Builder $q, string $col, string $op, $v): void
|
||||
{
|
||||
switch (strtolower($op)) {
|
||||
case 'like':
|
||||
$q->where($col, 'like', $v);
|
||||
break;
|
||||
case 'in':
|
||||
$q->whereIn($col, (array)$v);
|
||||
break;
|
||||
case 'not in':
|
||||
$q->whereNotIn($col, (array)$v);
|
||||
break;
|
||||
case 'null':
|
||||
$q->whereNull($col);
|
||||
break;
|
||||
case 'not null':
|
||||
$q->whereNotNull($col);
|
||||
break;
|
||||
case 'between':
|
||||
$q->whereBetween($col, (array)$v);
|
||||
break;
|
||||
default:
|
||||
$q->where($col, $op, $v);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据权限过滤到查询(buildQuery 场景)
|
||||
*/
|
||||
protected static function applyDataLimit(Builder $q, array $dataLimit): void
|
||||
{
|
||||
if (Auth::isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
$type = $dataLimit['type'] ?? null;
|
||||
$field = $dataLimit['field'] ?? 'admin_id';
|
||||
|
||||
if ($type === 'personal') {
|
||||
$q->where($field, admin_id());
|
||||
} elseif ($type === 'auth') {
|
||||
$q->whereIn($field, Auth::getScopeAdminIds(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据权限到“创建/更新”操作
|
||||
*
|
||||
* - $target 可能是 array|Model 对象
|
||||
* - $newData 仅在 updateByPk 场景时传入,用以校验变更归属
|
||||
*/
|
||||
protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void
|
||||
{
|
||||
if (Auth::isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
$type = $dataLimit['type'] ?? null;
|
||||
$field = $dataLimit['field'] ?? 'admin_id';
|
||||
|
||||
// 读取当前归属:create 时 $target 是 array,update 时是 Model
|
||||
$current = is_array($target) ? null : $target->{$field};
|
||||
|
||||
// 1. personal → 只能自己
|
||||
if ($type === 'personal') {
|
||||
// create: 强制设置
|
||||
if (is_array($target)) {
|
||||
$target[$field] = admin_id();
|
||||
}
|
||||
// update: 校验当前归属
|
||||
elseif ($current !== admin_id()) {
|
||||
throw new BusinessException("无权操作该记录", 403);
|
||||
}
|
||||
// 若要修改归属,也必须和自己一致
|
||||
if (isset($newData[$field]) && $newData[$field] !== admin_id()) {
|
||||
throw new BusinessException("无权变更归属字段", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. auth → 自己及下属
|
||||
if ($type === 'auth') {
|
||||
$scope = Auth::getScopeAdminIds(true);
|
||||
// create: 若前端指定归属,则校验范围;否则默认自己
|
||||
if (is_array($target)) {
|
||||
if (isset($target[$field])) {
|
||||
if (!in_array($target[$field], $scope)) {
|
||||
throw new BusinessException("无权为指定用户操作", 403);
|
||||
}
|
||||
} else {
|
||||
$target[$field] = admin_id();
|
||||
}
|
||||
}
|
||||
// update: 校验当前归属
|
||||
elseif (!in_array($current, $scope)) {
|
||||
throw new BusinessException("无权操作该记录", 403);
|
||||
}
|
||||
// 校验更新中指定的新归属
|
||||
if (isset($newData[$field]) && !in_array($newData[$field], $scope)) {
|
||||
throw new BusinessException("无权变更归属字段", 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use Webman\Exception\BusinessException;
|
||||
|
||||
class FeishuMessage
|
||||
{
|
||||
|
||||
protected $app_id = 'cli_a72f13d6c022500d';
|
||||
protected $app_secret = 'Kxtbq70OQWrEhpltrufRZc76z7kjDqPT';
|
||||
protected $class = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->class = new Feishu($this->app_id, $this->app_secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书消息模板
|
||||
* @param string $scene
|
||||
* @param array $options
|
||||
*/
|
||||
public function messageTemplates(string $scene = 'group-send', array $options = [])
|
||||
{
|
||||
try {
|
||||
$template = match ($scene) {
|
||||
'inquiry-group-send' => $this->groupNewTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time']
|
||||
),
|
||||
'inquiry-group-update' => $this->groupUpdateTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time'],
|
||||
$options['t_time']
|
||||
),
|
||||
'inquiry-person-allocation' => $this->personAllocationTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time'],
|
||||
$options['t_time']
|
||||
),
|
||||
'crm-person-follow-up' => $this->personFollowUpTemplates(
|
||||
$options['contacts'],
|
||||
$options['follow_next'],
|
||||
$options['follow_last']
|
||||
)
|
||||
};
|
||||
} catch (\UnhandledMatchError $e) {
|
||||
throw new BusinessException($e->getMessage());
|
||||
}
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送飞书消息
|
||||
* @param array $options
|
||||
*/
|
||||
public function messageSend(string $scene = 'group-send', array $options = [])
|
||||
{
|
||||
$template = $this->messageTemplates($scene, $options);
|
||||
$result = $this->class->sendMessages($options['receive_id'], $template, $options['msg_type'], $options['receive_id_type']);
|
||||
if (!isset($result['code']) || $result['code'] !== 0) {
|
||||
throw new BusinessException('发送飞书消息失败');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索消息发送
|
||||
*/
|
||||
public function groupNewTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **发布人:** '.$enter_user.'\n- **发布时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "form",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"flex_mode": "stretch",
|
||||
"horizontal_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "立即跟踪"
|
||||
},
|
||||
"type": "primary_filled",
|
||||
"width": "fill",
|
||||
"size": "medium",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "list-check-bold_outlined"
|
||||
},
|
||||
"confirm": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "确认跟踪?"
|
||||
},
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "客户线索:'.$order_no.'"
|
||||
}
|
||||
},
|
||||
"behaviors": [
|
||||
{
|
||||
"type": "callback",
|
||||
"value": "'.$order_no.'"
|
||||
}
|
||||
],
|
||||
"form_action_type": "submit",
|
||||
"name": "Button_m7d0zqa2",
|
||||
"element_id": "track"
|
||||
}
|
||||
],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top"
|
||||
},
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top"
|
||||
},
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [],
|
||||
"padding": "0px 0px 0px 0px",
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"margin": "4px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "12px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 0px 0px 0px",
|
||||
"margin": "0px 0px 0px 0px",
|
||||
"name": "Form_m7d0zqa1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "新的【公海】客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"template": "default",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "ai-common_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索消息更新
|
||||
*/
|
||||
public function groupUpdateTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23', $t_time = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **发布人:** '.$enter_user.'\n- **发布时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "跟踪时间:'.$t_time.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "green"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat-done_outlined",
|
||||
"color": "green"
|
||||
},
|
||||
"element_id": "track_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "【公海】客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"text_tag_list": [
|
||||
{
|
||||
"tag": "text_tag",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "已跟踪"
|
||||
},
|
||||
"color": "orange"
|
||||
}
|
||||
],
|
||||
"template": "green",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "ai-common_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索分配消息
|
||||
*/
|
||||
public function personAllocationTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23', $t_time = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **录单人:** '.$enter_user.'\n- **录入时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "分配时间:'.$t_time.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "green"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat-done_outlined",
|
||||
"color": "green"
|
||||
},
|
||||
"element_id": "allocation_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "新的客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"text_tag_list": [
|
||||
{
|
||||
"tag": "text_tag",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "指定"
|
||||
},
|
||||
"color": "yellow"
|
||||
}
|
||||
],
|
||||
"template": "blue",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "todo_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟进提醒消息
|
||||
*/
|
||||
public function personFollowUpTemplates($contacts = '木子李', $follow_next = '2025-07-24 14:41:23', $follow_last = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "- **客户名称:** '.$contacts.'\n- **跟进时间:** '.$follow_next.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "最近跟进:'.$follow_last.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "grey"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat_outlined",
|
||||
"color": "grey"
|
||||
},
|
||||
"element_id": "track_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "跟进提醒"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"template": "green",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "calendar_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class TableInspector
|
||||
{
|
||||
/**
|
||||
* 获取某个表的所有字段信息
|
||||
*/
|
||||
public static function getColumns(string $table, ?string $database = null): array
|
||||
{
|
||||
$database = $database ?: Db::connection()->getDatabaseName();
|
||||
|
||||
$columns = Db::select("
|
||||
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE,
|
||||
COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION
|
||||
", [$database, $table]);
|
||||
|
||||
return array_map(function ($col) {
|
||||
return [
|
||||
'name' => $col->COLUMN_NAME,
|
||||
'type' => $col->DATA_TYPE,
|
||||
'column_type' => $col->COLUMN_TYPE,
|
||||
'nullable' => $col->IS_NULLABLE === 'YES',
|
||||
'default' => $col->COLUMN_DEFAULT,
|
||||
'comment' => $col->COLUMN_COMMENT,
|
||||
'extra' => $col->EXTRA,
|
||||
];
|
||||
}, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段名列表
|
||||
*/
|
||||
public static function getColumnNames(string $table, ?string $database = null): array
|
||||
{
|
||||
return array_column(self::getColumns($table, $database), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段注释 map:字段名 => 注释
|
||||
*/
|
||||
public static function getColumnComments(string $table, ?string $database = null): array
|
||||
{
|
||||
$columns = self::getColumns($table, $database);
|
||||
$map = [];
|
||||
foreach ($columns as $col) {
|
||||
$map[$col['name']] = $col['comment'];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\scaffold;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use app\common\utils\TableInspector;
|
||||
|
||||
class Generate
|
||||
{
|
||||
|
||||
// 配置项
|
||||
protected $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (empty($options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function doGenerate()
|
||||
{
|
||||
$fs = new Filesystem();
|
||||
|
||||
$templateDir = __DIR__ . '/templates/';
|
||||
|
||||
$resultStr = '';
|
||||
|
||||
if (!empty($this->options['controller'])) {
|
||||
$controllerPath = __DIR__ . "/../../../{$this->options['controller']['module']}/controller/".$this->rName($this->options['controller']['name'])."Controller.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['controller']['module']}/controller");
|
||||
$this->deleteFileIfExists($controllerPath);
|
||||
$ControllerStub = $this->generateFile($templateDir, 'ControllerStub.stub', $controllerPath, 'controller');
|
||||
$resultStr .= $ControllerStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['service'])) {
|
||||
$servicePath = __DIR__ . "/../../../{$this->options['service']['module']}/service/".$this->rName($this->options['service']['name'])."Service.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['service']['module']}/service");
|
||||
$this->deleteFileIfExists($servicePath);
|
||||
$ServiceStub = $this->generateFile($templateDir, 'ServiceStub.stub', $servicePath, 'service');
|
||||
$resultStr .= $ServiceStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['logic'])) {
|
||||
$logicPath = __DIR__ . "/../../../{$this->options['logic']['module']}/logic/".$this->rName($this->options['logic']['name'])."Logic.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['logic']['module']}/logic");
|
||||
$this->deleteFileIfExists($logicPath);
|
||||
$LogicStub = $this->generateFile($templateDir, 'LogicStub.stub', $logicPath, 'logic');
|
||||
$resultStr .= $LogicStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['model'])) {
|
||||
$modelPath = __DIR__ . "/../../../{$this->options['model']['module']}/model/".$this->rName($this->options['model']['name']).".php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['model']['module']}/model");
|
||||
$this->deleteFileIfExists($modelPath);
|
||||
$ModelStub = $this->generateFile($templateDir, 'ModelStub.stub', $modelPath, 'model');
|
||||
$resultStr .= $ModelStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['validate'])) {
|
||||
$validatePath = __DIR__ . "/../../../{$this->options['validate']['module']}/validate/".$this->rName($this->options['validate']['name'])."Validate.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['validate']['module']}/validate");
|
||||
$this->deleteFileIfExists($validatePath);
|
||||
$ValidatorStub = $this->generateFile($templateDir, 'ValidateStub.stub', $validatePath, 'validate');
|
||||
$resultStr .= $ValidatorStub;
|
||||
}
|
||||
|
||||
return $resultStr ? $resultStr . "<info>文件生成成功!</info> <br/>" : '未生成任何文件';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模板生成文件
|
||||
*
|
||||
* @param string $templateDir 模板目录
|
||||
* @param string $templateFile 模板文件名
|
||||
* @param string $outputPath 输出文件路径
|
||||
*/
|
||||
protected function generateFile($templateDir, $templateFile, $outputPath, $fileType)
|
||||
{
|
||||
$templatePath = $templateDir . $templateFile;
|
||||
|
||||
if (!file_exists($templatePath)) {
|
||||
echo "<error>模板文件 {$templateFile} 不存在!</error> \n";
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($templatePath);
|
||||
|
||||
switch ($fileType) {
|
||||
case 'controller':
|
||||
$content = str_replace('{{controllerModule}}', $this->options['controller']['module'], $content);
|
||||
$content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content);
|
||||
$content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content);
|
||||
$content = str_replace('{{controllerName}}', $this->rName($this->options['controller']['name']), $content);
|
||||
break;
|
||||
case 'service':
|
||||
$content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content);
|
||||
$content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content);
|
||||
$content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content);
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content);
|
||||
break;
|
||||
case 'logic':
|
||||
$content = str_replace('{{logicModule}}', $this->options['logic']['module'], $content);
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{logicName}}', $this->rName($this->options['logic']['name']), $content);
|
||||
break;
|
||||
case 'model':
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{table}}', $this->options['table'], $content);
|
||||
$content = str_replace('{{fillable}}', $this->fillableFields(), $content);
|
||||
break;
|
||||
case 'validate':
|
||||
$content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content);
|
||||
$content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content);
|
||||
break;
|
||||
}
|
||||
|
||||
file_put_contents($outputPath, $content);
|
||||
|
||||
return "生成文件: <info>{$outputPath}</info> <br/>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字段
|
||||
*
|
||||
* @param array $fields 字段数组
|
||||
*/
|
||||
public function fillableFields()
|
||||
{
|
||||
$tableName = $this->options['table'];
|
||||
$fields = TableInspector::getColumnNames($tableName);
|
||||
$str = '';
|
||||
foreach ($fields as $key => $val) {
|
||||
if ($key < (count($fields)-1)) {
|
||||
$str .= "'{$val}', ";
|
||||
} else {
|
||||
$str .= "'{$val}'";
|
||||
}
|
||||
}
|
||||
$fields = trim($str, '"');
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果文件存在,删除它
|
||||
*
|
||||
* @param string $path 文件路径
|
||||
*/
|
||||
protected function deleteFileIfExists($path)
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
echo "删除已存在文件: <comment>{$path}</comment> \n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置名称
|
||||
*/
|
||||
public function rName($name = '')
|
||||
{
|
||||
return ucfirst(camelize($name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* 思路:
|
||||
* step1.原字符串转小写,原字符串中的分隔符用空格替换,在字符串开头加上分隔符
|
||||
* step2.将字符串中每个单词的首字母转换为大写,再去空格,去字符串首部附加的分隔符.
|
||||
*/
|
||||
function camelize($uncamelized_words, $separator='_') {
|
||||
$uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words));
|
||||
return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator );
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰命名转下划线命名
|
||||
* 思路:
|
||||
* 小写和大写紧挨一起的地方,加上分隔符,然后全部转小写
|
||||
*/
|
||||
function uncamelize($camelCaps, $separator='_') {
|
||||
return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\{{controllerModule}}\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\{{serviceModule}}\service\{{serviceName}}Service;
|
||||
|
||||
class {{controllerName}}Controller extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new {{serviceName}}Service();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function indexFormat($data)
|
||||
// {
|
||||
// if (isset($data['created_at']))
|
||||
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
// if (isset($data['updated_at']))
|
||||
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{logicModule}}\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\{{modelModule}}\model\{{modelName}};
|
||||
|
||||
class {{logicName}}Logic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = {{modelName}}::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{modelModule}}\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class {{modelName}} extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '{{table}}';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = [{{fillable}}];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{serviceModule}}\service;
|
||||
|
||||
use app\{{validateModule}}\validate\{{validateName}}Validate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\{{modelModule}}\model\{{modelName}};
|
||||
|
||||
class {{serviceName}}Service
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = {{modelName}}::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = {{validateName}}Validate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{validateModule}}\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class {{validateName}}Validate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user