Merge branch 'develop'

This commit is contained in:
mzl
2025-07-29 09:00:04 +08:00
parent a551507e4d
commit d74b374198
106 changed files with 6754 additions and 1346 deletions
+431
View File
@@ -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);
}
}
}
}
+555
View File
@@ -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": "&emsp;**'.$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": "&emsp;**'.$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": "&emsp;**'.$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;
}
}
+57
View File
@@ -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;
}
}
+191
View File
@@ -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' => [
]
];
}