This commit is contained in:
wangjinlei
2022-04-06 18:02:49 +08:00
parent e34f87de36
commit c1885928ff
262 changed files with 18633 additions and 0 deletions

172
vendor/hg/apidoc/src/Auth.php vendored Normal file
View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc;
use think\facade\Config;
use think\facade\Request;
use hg\apidoc\exception\AuthException;
class Auth
{
protected $config = [];
public function __construct()
{
$this->config = Config::get('apidoc')?Config::get('apidoc'):Config::get('apidoc.');
}
/**
* 验证密码
* @param $password
* @return false|string
*/
public function verifyAuth(string $password, string $appKey)
{
if (!empty($appKey)) {
$currentApps = (new Utils())->getCurrentApps($appKey);
$currentApp = $currentApps[count($currentApps) - 1];
if (!empty($currentApp) && !empty($currentApp['password'])) {
// 应用密码
if (md5($currentApp['password']) === $password) {
return $this->createToken($currentApp['password']);
}
throw new AuthException("password error");
}
}
if ($this->config['auth']['enable']) {
// 密码验证
if (md5($this->config['auth']['password']) === $password) {
return $this->createToken($this->config['auth']['password']);
}
throw new AuthException("password error");
}
return false;
}
/**
* 获取tokencode
* @param string $password
* @return string
*/
protected function getTokenCode(string $password): string
{
return md5(md5($password) . strtotime(date('Y-m-d', time())));
}
/**
* 创建token
* @param string $password
* @return string
*/
public function createToken(string $password): string
{
return $this->handleToken($this->getTokenCode($password), "CE");
}
/**
* 验证token
* @param $token
* @return bool
*/
public function checkToken(string $token, string $password): bool
{
if (empty($password)) {
$password = $this->config['auth']['password'];
}
$decode = $this->handleToken($token, "DE");
if ($decode === $this->getTokenCode($password)) {
return true;
}
return false;
}
/**
* @param $request
* @return bool
*/
public function checkAuth(string $appKey): bool
{
$config = $this->config;
$request = Request::instance();
$token = $request->param("apidocToken");
if (!empty($appKey)) {
$currentApps = (new Utils())->getCurrentApps($appKey);
$currentApp = $currentApps[count($currentApps) - 1];
if (!empty($currentApp) && !empty($currentApp['password'])) {
if (empty($token)) {
throw new AuthException("token not found");
}
// 应用密码
if ($this->checkToken($token, $currentApp['password'])) {
return true;
} else {
throw new AuthException("token error");
}
} else if (!(!empty($config['auth']) && $config['auth']['enable'])) {
return true;
}
}
if(!empty($config['auth']) && $config['auth']['enable'] && empty($token)){
throw new AuthException("token not found");
}else if (!empty($token) && !$this->checkToken($token, "")) {
throw new AuthException("token error");
}
return true;
}
/**
* 处理token
* @param $string
* @param string $operation
* @param string $key
* @param int $expiry
* @return false|string
*/
protected function handleToken(string $string, string $operation = 'DE', string $key = '', int $expiry = 0):string
{
$ckey_length = 4;
$key = md5($key ? $key : $this->config['auth']['secret_key']);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if ($operation == 'DE') {
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc . str_replace('=', '', base64_encode($result));
}
}
}

259
vendor/hg/apidoc/src/Controller.php vendored Normal file
View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc;
use hg\apidoc\exception\AuthException;
use hg\apidoc\exception\ErrorException;
use hg\apidoc\parseApi\CacheApiData;
use hg\apidoc\parseApi\ParseAnnotation;
use hg\apidoc\parseApi\ParseMarkdown;
use think\App;
use think\facade\Config;
use think\facade\Request;
use hg\apidoc\crud\CreateCrud;
class Controller
{
protected $app;
protected $config;
protected $defaultConfig=[
'crud'=>[
'model'=>[
'fields_types'=>[
"int",
"tinyint",
"smallint",
"mediumint",
"integer",
"bigint",
"bit",
"real",
"float",
"decimal",
"numeric",
"char",
"varchar",
"date",
"time",
"year",
"timestamp",
"datetime",
"tinyblob",
"blob",
"mediumblob",
"longblob",
"tinytext",
"text",
"mediumtext",
"longtext",
"enum",
"set",
"binary",
"varbinary",
"point",
"linestring",
"polygon",
"geometry",
"multipoint",
"multilinestring",
"multipolygon",
"geometrycollection"
]
]
]
];
public function __construct(App $app)
{
$this->app = $app;
$this->config = Config::get("apidoc")?Config::get("apidoc"):Config::get("apidoc.");
}
/**
* 获取配置
* @return \think\response\Json
*/
public function getConfig(){
$config = $this->config;
if (!empty($config['auth'])){
unset($config['auth']['auth_password']);
unset($config['auth']['password']);
unset($config['auth']['key']);
}
// 处理统一返回信息
if (!empty($config['responses']) && is_string($config['responses'])){
// 兼容原配置
$config['responses'] = [
'jsonStr'=>$config['responses']
];
}else if (!empty($config['responses']) && isset($config['responses']['show_responses']) && !$config['responses']['show_responses'] && !empty($config['responses']['data'])){
// 显示在提示中
$responsesStr = '{'."\r\n";
$responsesMain = "";
foreach ($config['responses']['data'] as $item){
$responsesStr.='"'.$item['name'].'":"'.$item['desc'].'",'."\r\n";
if (!empty($item['main']) && $item['main']==true){
$responsesMain = $item;
}
}
$responsesStr.= '}';
$config['responses']['jsonStr']=$responsesStr;
$config['responses']['main']=$responsesMain;
}
$config['debug']=app()->isDebug();
if (!empty($config['crud'])){
// 无配置可选字段类型,使用默认的
if (!empty($config['crud']['model']) && empty($config['crud']['model']['fields_types'])){
$config['crud']['model']['fields_types'] = $this->defaultConfig['crud']['model']['fields_types'];
}
// 过滤route文件配置
if (!empty($config['crud']['route'])){
unset($config['crud']['route']);
}
}
// 清除apps配置中的password
$config['apps'] = (new Utils())->handleAppsConfig($config['apps']);
return Utils::showJson(0,"",$config);
}
/**
* 验证密码
* @return false|\think\response\Json
* @throws \think\Exception
*/
public function verifyAuth(){
$config = $this->config;
$request = Request::instance();
$params = $request->param();
$password = $params['password'];
if (empty($password)){
throw new AuthException( "password not found");
}
$appKey = !empty($params['appKey'])?$params['appKey']:"";
if (!$appKey && !(!empty($config['auth']) && $config['auth']['enable'])) {
return false;
}
try {
$hasAuth = (new Auth())->verifyAuth($password,$appKey);
return Utils::showJson(0,"",$hasAuth);
} catch (AuthException $e) {
return Utils::showJson($e->getCode(),$e->getMessage());
}
}
/**
* 获取文档数据
* @return \think\response\Json
*/
public function getApidoc(){
$config = $this->config;
$request = Request::instance();
$params = $request->param();
(new Auth())->checkAuth($params['appKey']);
$cacheData=null;
if (!empty($config['cache']) && $config['cache']['enable']){
$cacheData = (new CacheApiData())->get($params['appKey'],$params['cacheFileName']);
if ($cacheData && $params['reload']=='false'){
$apiData = $cacheData['data'];
}else{
// 生成数据并缓存
$apiData = (new ParseAnnotation())->renderApiData($params['appKey']);
$cacheData =(new CacheApiData())->set($params['appKey'],$apiData);
}
}else{
// 生成数据
$apiData = (new ParseAnnotation())->renderApiData($params['appKey']);
}
$groups=[['title'=>'全部','name'=>0]];
// 获取md
$docs = (new ParseMarkdown())->getDocsMenu();
if (count($docs)>0){
$menu_title = !empty($config['docs']) && !empty($config['docs']['menu_title'])?$config['docs']['menu_title']:'文档';
$groups[]=['title'=>$menu_title,'name'=>'markdown_doc'];
}
if (!empty($config['groups']) && count($config['groups'])>0 && !empty($apiData['groups']) && count($apiData['groups'])>0){
$configGroups=[];
foreach ($config['groups'] as $group) {
if (in_array($group['name'],$apiData['groups'])){
$configGroups[]=$group;
}
}
$groups = array_merge($groups,$configGroups);
}
$json=[
'groups'=>$groups,
'list'=>$apiData['data'],
'docs'=>$docs,
'tags'=>$apiData['tags']
];
if ($cacheData && !empty($cacheData['list'])){
$json['cacheFiles']=$cacheData['list'];
$json['cacheName']=$cacheData['name'];
}
return Utils::showJson(0,"",$json);
}
/**
* 获取md文档内容
* @return \think\response\Json
*/
public function getMdDetail(){
$request = Request::instance();
$params = $request->param();
try {
if (empty($params['path'])){
throw new ErrorException("mdPath not found");
}
if (empty($params['appKey'])){
throw new ErrorException("appkey not found");
}
(new Auth())->checkAuth($params['appKey']);
$res = (new ParseMarkdown())->getContent($params['appKey'],$params['path']);
return Utils::showJson(0,"",$res);
} catch (ErrorException $e) {
return Utils::showJson($e->getCode(),$e->getMessage());
}
}
/**
* 创建Crud
* @return \think\response\Json
*/
public function createCrud()
{
if (!\think\facade\App::isDebug()) {
throw new ErrorException("no debug",501);
}
$request = Request::instance();
$params = $request->param();
if (!isset($params['appKey'])){
throw new ErrorException("appkey not found");
}
(new Auth())->checkAuth($params['appKey']);
$res = (new CreateCrud())->create($params);
return Utils::showJson(0,"",$res);
}
}

35
vendor/hg/apidoc/src/Service.php vendored Normal file
View File

@@ -0,0 +1,35 @@
<?php
namespace hg\apidoc;
use think\facade\Config;
use think\facade\Route;
class Service extends \think\Service
{
public function boot()
{
$this->registerRoutes(function (){
$route_prefix = 'apidoc';
$apidocConfig = Config::get("apidoc")?Config::get("apidoc"):Config::get("apidoc.");
$routes = function () {
$controller_namespace = '\hg\apidoc\Controller@';
Route::get('config' , $controller_namespace . 'getConfig');
Route::get('apiData' , $controller_namespace . 'getApidoc');
Route::get('mdDetail' , $controller_namespace . 'getMdDetail');
Route::post('verifyAuth' , $controller_namespace . 'verifyAuth');
Route::post('createCrud' , $controller_namespace . 'createCrud');
};
if (!empty($apidocConfig['allowCrossDomain'])){
Route::group($route_prefix, $routes)->allowCrossDomain();
}
Route::group($route_prefix, $routes);
});
}
}

330
vendor/hg/apidoc/src/Utils.php vendored Normal file
View File

@@ -0,0 +1,330 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc;
use hg\apidoc\exception\ErrorException;
use think\facade\Config;
use think\response\Json;
class Utils
{
protected static $snakeCache = [];
/**
* 统一返回json格式
* @param int $code
* @param string $msg
* @param string $data
* @return \think\response\Json
*/
public static function showJson(int $code = 0, string $msg = "", $data = ""):Json
{
$res = [
'code' => $code,
'msg' => $msg,
'data' => $data,
];
return json($res);
}
/**
* 过滤参数字段
* @param $data
* @param $fields
* @param string $type
* @return array
*/
public function filterParamsField(array $data, $fields, string $type = "field"): array
{
if ($fields && strpos($fields, ',') !== false){
$fieldArr = explode(',', $fields);
}else{
$fieldArr = [$fields];
}
$dataList = [];
foreach ($data as $item) {
if (!empty($item['name']) && in_array($item['name'], $fieldArr) && $type === 'field') {
$dataList[] = $item;
} else if (!(!empty($item['name']) && in_array($item['name'], $fieldArr)) && $type == "withoutField") {
$dataList[] = $item;
}
}
return $dataList;
}
/**
* 读取文件内容
* @param $fileName
* @return false|string
*/
public static function getFileContent(string $fileName): string
{
$content = "";
if (file_exists($fileName)) {
$handle = fopen($fileName, "r");
$content = fread($handle, filesize($fileName));
fclose($handle);
}
return $content;
}
/**
* 保存文件
* @param $path
* @param $str_tmp
* @return bool
*/
public static function createFile(string $path, string $str_tmp): bool
{
$pathArr = explode("/", $path);
unset($pathArr[count($pathArr) - 1]);
$dir = implode("/", $pathArr);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
$fp = fopen($path, "w") or die("Unable to open file!");
fwrite($fp, $str_tmp); //存入内容
fclose($fp);
return true;
}
/**
* 删除文件
* @param $path
*/
public static function delFile(string $path)
{
$url = iconv('utf-8', 'gbk', $path);
if (PATH_SEPARATOR == ':') { //linux
unlink($path);
} else { //Windows
unlink($url);
}
}
/**
* 将tree树形数据转成list数据
* @param array $tree tree数据
* @param string $childName 子节点名称
* @return array 转换后的list数据
*/
public function treeToList(array $tree, string $childName = 'children',string $key = "id",string $parentField = "parent")
{
$array = array();
foreach ($tree as $val) {
$array[] = $val;
if (isset($val[$childName])) {
$children = $this->treeToList($val[$childName], $childName);
if ($children) {
$newChildren = [];
foreach ($children as $item) {
$item[$parentField] = $val[$key];
$newChildren[] = $item;
}
$array = array_merge($array, $newChildren);
}
}
}
return $array;
}
/**
* 根据一组keys获取所有关联节点
* @param $tree
* @param $keys
*/
public function getTreeNodesByKeys(array $tree, array $keys, string $field = "id", string $childrenField = "children")
{
$list = $this->TreeToList($tree, $childrenField, "folder");
$data = [];
foreach ($keys as $k => $v) {
$parent = !$k ? "" : $keys[$k - 1];
foreach ($list as $item) {
if (((!empty($item['parent']) && $item['parent'] === $parent) || empty($item['parent'])) && $item[$field] == $v) {
$data[] = $item;
break;
}
}
}
return $data;
}
/**
* 替换模板变量
* @param $temp
* @param $data
* @return string|string[]
*/
public static function replaceTemplate(string $temp, array $data):string
{
$str = $temp;
foreach ($data as $k => $v) {
$key = '${' . $k . '}';
if (strpos($str, $key) !== false) {
$str = str_replace($key, $v, $str);
}
}
return $str;
}
/**
* 替换当前所选应用/版本的变量
* @param $temp
* @param $currentApps
* @return string|string[]
*/
public function replaceCurrentAppTemplate(string $temp,array $currentApps):string
{
$str = $temp;
if (!empty($currentApps) && count($currentApps) > 0) {
$data = [];
for ($i = 0; $i <= 3; $i++) {
if (isset($currentApps[$i])) {
$appItem = $currentApps[$i];
foreach ($appItem as $k => $v) {
$key = 'app[' . $i . '].' . $k;
$data[$key] = $v;
}
} else {
$appItem = $currentApps[0];
foreach ($appItem as $k => $v) {
$key = 'app[' . $i . '].' . $k;
$data[$key] = "";
}
}
}
$str = $this->replaceTemplate($str, $data);
}
return $str;
}
/**
* 根据条件获取数组中的值
* @param array $array
* @param $query
* @return mixed|null
*/
public static function getArrayFind(array $array, $query)
{
$res = null;
if (is_array($array)) {
foreach ($array as $item) {
if ($query($item)) {
$res = $item;
break;
}
}
}
return $res;
}
/**
* 合并对象数组并根据key去重
* @param string $name
* @param mixed ...$array
* @return array
*/
public static function arrayMergeAndUnique(string $key = "name", ...$array):array
{
$mergeArr = [];
foreach ($array as $k => $v) {
$mergeArr = array_merge($mergeArr, $v);
}
$keys = [];
foreach ($mergeArr as $k => $v) {
$keys[] = $v[$key];
}
$uniqueKeys = array_flip(array_flip($keys));
$newArray = [];
foreach ($uniqueKeys as $k => $v) {
$newArray[] = $mergeArr[$k];
}
return $newArray;
}
/**
* 初始化当前所选的应用/版本数据
* @param $appKey
*/
public function getCurrentApps(string $appKey):array
{
$config = Config::get("apidoc")?Config::get("apidoc"):Config::get("apidoc.");
if (!(!empty($config['apps']) && count($config['apps']) > 0)) {
throw new ErrorException("no config apps", 500);
}
if (strpos($appKey, '_') !== false) {
$keyArr = explode("_", $appKey);
} else {
$keyArr = [$appKey];
}
$currentApps = $this->getTreeNodesByKeys($config['apps'], $keyArr, 'folder', 'items');
if (!$currentApps) {
throw new ErrorException("appKey error", 412, [
'appKey' => $appKey
]);
}
return $currentApps;
}
/**
* 处理apps配置参数
* @param array $apps
* @return array
*/
public function handleAppsConfig(array $apps):array
{
$appsConfig = [];
foreach ($apps as $app) {
if (!empty($app['password'])) {
unset($app['password']);
$app['hasPassword'] = true;
}
if (!empty($app['items']) && count($app['items']) > 0) {
$app['items'] = $this->handleAppsConfig($app['items']);
}
$appsConfig[] = $app;
}
return $appsConfig;
}
/**
* 驼峰转下划线
*
* @param string $value
* @param string $delimiter
* @return string
*/
public static function snake(string $value, string $delimiter = '_'): string
{
$key = $value;
if (isset(static::$snakeCache[$key][$delimiter])) {
return static::$snakeCache[$key][$delimiter];
}
if (!ctype_lower($value)) {
$value = preg_replace('/\s+/u', '', $value);
$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
}
return static::$snakeCache[$key][$delimiter] = $value;
}
/**
* 字符串转小写
*
* @param string $value
* @return string
*/
public static function lower(string $value): string
{
return mb_strtolower($value, 'UTF-8');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 添加模型的字段
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class AddField extends Annotation
{
/**
* 字段名
* @var string
*/
public $name;
/**
* 类型
* @var string
*/
public $type = 'string';
/**
* 默认值
* @var string
*/
public $default;
/**
* 描述
* @var string
*/
public $desc;
/**
* 必须
* @var bool
*/
public $require = false;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 作者
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Author extends Annotation
{}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 描述
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD","CLASS"})
*/
class Desc extends Annotation
{}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 指定获取模型的字段
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Field extends Annotation
{}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 分组
* @package hg\apidoc\annotation
* @Annotation
* @Target({"CLASS"})
*/
class Group extends Annotation
{}

View File

@@ -0,0 +1,40 @@
<?php
namespace hg\apidoc\annotation;
/**
* 请求头
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Header extends ParamBase
{
/**
* 必须
* @var bool
*/
public $require = false;
/**
* 类型
* @var string
*/
public $type;
/**
* 引入
* @var string
*/
public $ref;
/**
* 描述
* @var string
*/
public $desc;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Url
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Method extends Annotation
{}

View File

@@ -0,0 +1,27 @@
<?php
namespace hg\apidoc\annotation;
/**
* 请求参数
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD","ANNOTATION"})
*/
final class Param extends ParamBase
{
/**
* 必须
* @var bool
*/
public $require = false;
/**
* 引入
* @var string
*/
public $ref;
}

View File

@@ -0,0 +1,62 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
abstract class ParamBase extends Annotation
{
/**
* 类型
* @Enum({"string", "integer", "int", "boolean", "array", "double", "object", "tree", "file","float","date","time","datetime"})
* @var string
*/
public $type = 'string';
/**
* 默认值
* @var string
*/
public $default;
/**
* 描述
* @var string
*/
public $desc;
/**
* 为tree类型时指定children字段
* @var string
*/
public $childrenField = '';
/**
* 为tree类型时指定children字段说明
* @var string
*/
public $childrenDesc = 'children';
/**
* 为array类型时指定子节点类型
* @Enum({"string", "int", "boolean", "array", "object"})
* @var string
*/
public $childrenType = '';
/**
* 指定引入的字段
* @var string
*/
public $field;
/**
* 指定从引入中过滤的字段
* @var string
*/
public $withoutField;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 参数类型
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class ParamType extends Annotation
{}

View File

@@ -0,0 +1,34 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 返回参数
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD","ANNOTATION"})
*/
final class Returned extends ParamBase
{
/**
* 必须
* @var bool
*/
public $require = false;
/**
* 引入
* @var string
*/
public $ref;
/**
* 是否替换全局响应体中的参数
* @var bool
*/
public $replaceGlobal = false;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation\Enum;
final class Route extends Rule
{
/**
* 请求类型
* @Enum({"GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"})
* @var string
*/
public $method = "GET";
}

View File

@@ -0,0 +1,77 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
abstract class Rule extends Annotation
{
/**
* @var string|array
*/
public $middleware;
/**
* 后缀
* @var string
*/
public $ext;
/**
* @var string
*/
public $deny_ext;
/**
* @var bool
*/
public $https;
/**
* @var string
*/
public $domain;
/**
* @var bool
*/
public $complete_match;
/**
* @var string|array
*/
public $cache;
/**
* @var bool
*/
public $ajax;
/**
* @var bool
*/
public $pjax;
/**
* @var bool
*/
public $json;
/**
* @var array
*/
public $filter;
/**
* @var array
*/
public $append;
public function getOptions()
{
return array_intersect_key(get_object_vars($this), array_flip([
'middleware', 'ext', 'deny_ext', 'https', 'domain', 'complete_match', 'cache', 'ajax', 'pjax', 'json', 'filter', 'append',
]));
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 排序
* @package hg\apidoc\annotation
* @Annotation
* @Target({"CLASS"})
*/
class Sort extends Annotation
{}

14
vendor/hg/apidoc/src/annotation/Tag.php vendored Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Tag
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Tag extends Annotation
{}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 标题
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD","CLASS"})
*/
class Title extends Annotation
{}

14
vendor/hg/apidoc/src/annotation/Url.php vendored Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Url
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class Url extends Annotation
{}

View File

@@ -0,0 +1,14 @@
<?php
namespace hg\apidoc\annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* 排除模型的字段
* @package hg\apidoc\annotation
* @Annotation
* @Target({"METHOD"})
*/
class WithoutField extends Annotation
{}

62
vendor/hg/apidoc/src/config.php vendored Normal file
View File

@@ -0,0 +1,62 @@
<?php
return [
// 文档标题
'title' => 'APi接口文档',
// 文档描述
'desc' => '',
// 版权申明
'copyright' => 'Powered By hg-code',
// 默认作者
'default_author'=>'',
// 默认请求类型
'default_method'=>'GET',
// 设置应用/版本(必须设置)
'apps' => [
['title'=>'v1.0','path'=>'app\controller','folder'=>'v1'],
],
// 控制器分组
'groups' => [],
// 指定公共注释定义的文件地址
'definitions' => "app\controller\Definitions",
//指定生成文档的控制器
'controllers' => [],
// 过滤,不解析的控制器
'filter_controllers' => [],
// 缓存配置
'cache' => [
// 是否开启缓存
'enable' => false,
// 缓存文件路径
'path' => '../runtime/apidoc/',
// 是否显示更新缓存按钮
'reload' => true,
// 最大缓存文件数
'max' => 5, //最大缓存数量
],
// 权限认证配置
'auth' => [
// 是否启用密码验证
'enable' => false,
// 验证密码
'password' => "123456",
// 密码加密盐
'secret_key' => "apidoc#hg_code",
],
// 统一的请求Header
'headers'=>[],
// 统一的请求参数Parameters
'parameters'=>[],
// 统一的请求响应体,仅显示在文档提示中
'responses'=>[
['name'=>'code','desc'=>'状态码','type'=>'int'],
['name'=>'message','desc'=>'操作描述','type'=>'string'],
['name'=>'data','desc'=>'业务数据','main'=>true,'type'=>'object'],
],
// md文档
'docs' => [
'menu_title' => '开发文档',
'menus' => []
],
'crud'=>[]
];

383
vendor/hg/apidoc/src/crud/CreateCrud.php vendored Normal file
View File

@@ -0,0 +1,383 @@
<?php
namespace hg\apidoc\crud;
use hg\apidoc\exception\ErrorException;
use think\Db as Db5;
use think\facade\Config;
use think\facade\Db;
use think\facade\App;
use hg\apidoc\Utils;
class CreateCrud
{
protected $config;
protected $currentApps;
protected $controller_layer = "";
public function __construct()
{
$config = Config::get('apidoc')?Config::get('apidoc'):Config::get('apidoc.');
$this->controller_layer = Config::get('route.controller_layer',"controller");
if (!empty($config) && !empty($config['crud'])){
$this->config = $config['crud'];
}else{
throw new ErrorException("no config crud",501);
}
}
/**
* 创建Crud
* @param $params
* @return array
*/
public function create($params){
$appKey = $params['appKey'];
$currentApps = (new Utils())->getCurrentApps($appKey);
// $currentApp = $currentApps[count($currentApps) - 1];
$data = $this->renderTemplateData($params,$currentApps);
$res = [];
// 创建数据表
if (!empty($params['model']['table'])){
$sqlRes = $this->createModelTable($params['model'],$params['title']);
if ($sqlRes == true){
$res[]="创建数据表成功";
}else{
// $msg= $sqlRes?$sqlRes:"数据表创建失败,请检查配置";
throw new ErrorException("datatable crud error",500);
}
}
// 生成文件
$keys = array_keys($this->config);
foreach ($keys as $index=>$key) {
// 添加路由
if (
$key==="route" &&
!empty($this->config['route']) &&
!empty($this->config['route']['template']) &&
!empty($this->config['route']['path'])
){
$tmp_path = (new Utils())->replaceCurrentAppTemplate($this->config['route']['template'],$currentApps);
$tempPath = $tmp_path.".txt";
$str_tmp = Utils::getFileContent($tempPath);
if (!empty($str_tmp)){
$tmp_content = Utils::replaceTemplate($str_tmp,$data);
$tmp_content = (new Utils())->replaceCurrentAppTemplate($tmp_content,$currentApps);
$routePathStr = (new Utils())->replaceCurrentAppTemplate($this->config['route']['path'],$currentApps);
$routePathStr = str_replace("\\","/",$routePathStr);
$routePath = App::getAppPath().$routePathStr;
$routeContent = Utils::getFileContent($routePath);
$routeContent.="\r\n".$tmp_content."\r\n";
Utils::createFile($routePath,$routeContent);
$res[]="添加路由成功";
}
}else{
$currentConfig = $this->config[$key];
$currentParam = $params[$key];
$tmp_path = (new Utils())->replaceCurrentAppTemplate($currentParam['template'],$currentApps);
$tempPath = $tmp_path.".txt";
$str_tmp = Utils::getFileContent($tempPath);
$file_content = Utils::replaceTemplate($str_tmp,$data);
$file_content = (new Utils())->replaceCurrentAppTemplate($file_content,$currentApps);
$namespacePath = str_replace("\\","/",$currentParam['path']);
$fileName = $data[$key.'.file_name'];
$filePath = '../'.$namespacePath.'/'.$fileName.'.php';
$fp=Utils::createFile($filePath,$file_content);
if ($fp){
$res[]="创建文件成功 path:".$filePath;
}
}
}
return $res;
}
/**
* 生成模板变量的数据
* @param $params
* @return array
*/
public function renderTemplateData($params,array $currentApps){
// 模板参数
$api_group = "";
if (!empty($params['group'])){
$api_group = '@Apidoc\Group("'.$params['group'].'")';
}
$data=[
'title'=>!empty($params['title'])?$params['title']:"",
'api_group'=>$api_group,
];
$keys = array_keys($this->config);
foreach ($keys as $index=>$key){
$currentConfig = $this->config[$key];
//验证模板是否存在
$tmp_path = (new Utils())->replaceCurrentAppTemplate($currentConfig['template'],$currentApps);
if(!file_exists($tmp_path.'.txt')){
throw new ErrorException("template not found",404,[
'template'=>$currentConfig['template']
]);
}
if ($key==="route"){
continue;
}
$currentParam = $params[$key];
if(!preg_match("/^[A-Z]{1}[A-Za-z0-9]{1,32}$/",$currentParam['class_name'])){
throw new ErrorException("classname error",412,[
'classname'=>$currentParam['class_name']
]);
}
$currentParamPath = str_replace("\\","/",$currentParam['path']);
// 验证目录是否存在
if(!is_dir('../'.$currentParamPath)){
throw new ErrorException("path not found",404,[
'path'=>$currentParamPath
]);
}
$appPath = App::getAppPath();
$appPathArr = explode("\\", $appPath);
$appFolder = $appPathArr[count($appPathArr)-1]?$appPathArr[count($appPathArr)-1]:$appPathArr[count($appPathArr)-2];
$namespace = str_replace($appFolder, App::getNamespace(), $currentParam['path']);
if ($key==="controller"){
$pathArr = explode("\\", $namespace);
$notArr = array(App::getNamespace(), $this->controller_layer);
$url = "/";
foreach ($pathArr as $pathItem){
if (!in_array($pathItem,$notArr)){
$url.=$pathItem."/";
}
}
$data['folder']=$url;
$data['api_class_name']=lcfirst($currentParam['class_name']);
$data['api_url']=$url.lcfirst($currentParam['class_name']);
}else if ($key==="model" && !empty($currentParam['table'])){
// 模型
// 获取主键
foreach ($currentParam['table'] as $item){
if ($item['main_key']==true){
$data['main_key.field'] = $item['field'];
$data['main_key.type'] = $item['type'];
$data['main_key.desc'] = $item['desc'];
break;
}
}
$tp_version = \think\facade\App::version();
if (substr($tp_version, 0, 2) == '5.') {
$table_prefix = Config::get('database.prefix');
}else{
$driver = Config::get('database.default');
$table_prefix = Config::get('database.connections.' . $driver . '.prefix');
}
$data[$currentParam['name'] . '.table_prefix'] = $table_prefix;
$table_name = Utils::snake($currentParam['class_name']);
$data[$currentParam['name'].'.table_name']=$table_name;
}
$namespace = str_replace($appFolder, App::getNamespace(), $currentParam['path']);
$data[$currentParam['name'].'.class_name']=$currentParam['class_name'];
$data[$currentParam['name'].'.namespace']=$namespace;
// 验证文件是否已存在
$fileName = $currentParam['class_name'];
if (!empty($currentConfig['file_name'])){
$fileName = (new Utils())->replaceTemplate($currentConfig['file_name'],$data);
}
$filePath = '../'.$currentParamPath.'/'.$fileName.'.php';
$data[$currentParam['name'].'.file_name']=$fileName;
if(file_exists($filePath)){
throw new ErrorException("file already exists",500,[
'filepath'=>$filePath
]);
}
$data[$currentParam['name'].'.use_path']=$namespace."\\".$fileName;
$data[$currentParam['name'].'.use_alias']=$fileName.ucwords($currentParam['name']);
}
// 字段过滤数据
if (!empty($params['model']['table'])){
$checkKeys = ['list','detail','add','edit'];
foreach ($checkKeys as $checkKey){
$itemArr = ['field'=>[],'withoutField'=>[]];
foreach ($params['model']['table'] as $item){
if ($item[$checkKey]){
$itemArr['field'][]=$item['field'];
}else{
$itemArr['withoutField'][]=$item['field'];
}
}
$data[$checkKey.'.field']=implode(",", $itemArr['field']);
$data[$checkKey.'.withoutField']=implode(",", $itemArr['withoutField']);
}
// 查询条件、验证规则
$query_where='$where=[];'."\r\n";
$query_annotation = "";
$validate_rule = "["."\r\n";
$validate_message = "["."\r\n";
$validate_fields = [];
$addRequireFields = [];
foreach ($params['model']['table'] as $i=>$item){
if ($item['query']){
$itemArr['field'][]=$item['field'];
$query_where.= ' if(!empty($param[\''.$item['field'].'\'])){'."\r\n";
$query_where.= ' $where[] = [\''.$item['field'].'\',\'=\',$param[\''.$item['field'].'\']];'."\r\n";
$query_where.= ' }'."\r\n";
$fh = empty($query_annotation)?'* ':' * ';
$require = $item['not_null']==true?'true':'false';
$rn="";
if (($i+1)<count($params['model']['table'])){
$rn="\r\n";
}
$query_annotation.=$fh.'@Apidoc\Param("'.$item['field'].'",type="'.$item['type'].'",require='.$require.',desc="'.$item['desc'].'")'.$rn;
}
// 验证规则
if (!empty($this->config['validate'])){
// 存在配置验证规则
if (!empty($item['validate']) && $this->config['validate']['rules']){
$rulesConfig = $this->config['validate']['rules'];
$currentRuleConfig = "";
foreach ($rulesConfig as $rule){
if ($rule['rule'] === $item['validate']){
$currentRuleConfig = $rule;
break;
}
}
if (!empty($currentRuleConfig)){
$validate_rule.=' \''.$item['field'].'\' => \''.$item['validate'].'\','."\r\n";
if (!empty($currentRuleConfig['message']) ){
if (is_array($currentRuleConfig['message']) && count($currentRuleConfig['message'])>0){
foreach ($currentRuleConfig['message'] as $ruleKey=>$ruleMessage){
if ($ruleKey=='0'){
$ruleKeyStr = $item['field'];
}else{
$ruleKeyStr = Utils::replaceTemplate($ruleKey,$item);
}
$ruleMessageStr = Utils::replaceTemplate($ruleMessage,$item);
$validate_message.=' \''.$ruleKeyStr.'\' => \''.$ruleMessageStr.'\','."\r\n";
}
}else{
$ruleMessageStr = Utils::replaceTemplate($currentRuleConfig['message'],$item);
$validate_message.=' \''.$item['field'].'\' => \''.$ruleMessageStr.'\','."\r\n";
}
}
$validate_fields[]=$item['field'];
if($item['field'] !== $data['main_key.field']){
$addRequireFields[]=$item['field'];
}
}
}else if($item['not_null']){
$validate_fields[]=$item['field'];
if($item['field'] !== $data['main_key.field']){
$addRequireFields[]=$item['field'];
}
// 根据not_null生成必填
$validate_rule.=' \''.$item['field'].'\' => \'require\','."\r\n";
$validate_message.=' \''.$item['field'].'\' => \''.$item['field'].'不可为空\','."\r\n";
}
}
}
$validate_rule.=' ];'."\r\n";
$validate_message.=' ];'."\r\n";
if (!empty($this->config['validate'])) {
$data['validate.rule'] = $validate_rule;
$data['validate.message'] = $validate_message;
$data['validate.scene.edit'] = json_encode($validate_fields);
$data['validate.scene.add'] = json_encode($addRequireFields)=='[]'?'[\'\']':json_encode($addRequireFields);
$data['validate.scene.delete'] = '[\'' . $data['main_key.field'] . '\']';
}
$data['query.where']=$query_where;
$data['query.annotation']=$query_annotation;
}
return $data;
}
/**
* 创建数据表
* @return mixed
*/
public function createModelTable($params,$title=""){
$data = $params['table'];
if (empty($title)){
$title =$params['class_name'];
}
$driver = Config::get('database.default');
$table_prefix=Config::get('database.connections.'.$driver.'.prefix');
$table_name = $table_prefix.Utils::snake($params['class_name']);
$table_data = '';
$main_keys = '';
foreach ($data as $item){
$table_field="`".$item['field']."` ".$item['type'];
if (!empty($item['length'])){
$table_field.="(".$item['length'].")";
}
if ($item['main_key']){
$main_keys.=$item['field'];
$table_field.=" NOT NULL";
}else if ($item['not_null']){
$table_field.=" NOT NULL";
}
if ($item['incremental']==true){
$table_field.=" AUTO_INCREMENT";
}
if (!empty($item['default']) || $item['default']=="0"){
$table_field.=" DEFAULT '".$item['default']."'";
}
$table_field.=" COMMENT '".$item['desc']."',";
$table_data.=$table_field;
}
$sql = "CREATE TABLE IF NOT EXISTS `$table_name` (
$table_data
PRIMARY KEY (`$main_keys`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='$title' AUTO_INCREMENT=1 ;";
$tp_version = \think\facade\App::version();
if (substr($tp_version, 0, 2) == '5.'){
Db5::startTrans();
try {
Db5::query($sql);
// 提交事务
Db5::commit();
return true;
} catch (\Exception $e) {
// 回滚事务
Db5::rollback();
return $e->getMessage();
}
}else{
Db::startTrans();
try {
Db::query($sql);
// 提交事务
Db::commit();
return true;
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
return $e->getMessage();
}
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace hg\apidoc\exception;
use think\Exception;
use think\exception\HttpException;
class AuthException extends HttpException
{
protected $exceptions = [
'password error' => ['code' => 4001, 'msg' => '密码不正确,请重新输入'],
'password not found' => ['code' => 4002, 'msg' => '密码不可为空'],
'token error' => ['code' => 4003, 'msg' => '不合法的Token'],
'token not found' => ['code' => 4004, 'msg' => '不存在Token'],
];
public function __construct(string $exceptionCode)
{
$exception = $this->getException($exceptionCode);
parent::__construct(401, $exception['msg'], null, [], $exception['code']);
}
public function getException($exceptionCode)
{
if (isset($this->exceptions[$exceptionCode])) {
return $this->exceptions[$exceptionCode];
}
throw new Exception('exceptionCode "' . $exceptionCode . '" Not Found');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace hg\apidoc\exception;
use hg\apidoc\Utils;
use think\Exception;
use think\exception\HttpException;
class ErrorException extends HttpException
{
protected $exceptions = [
'appkey not found' => ['code' => 4005, 'msg' => '缺少必要参数appKey'],
'mdPath not found' => ['code' => 4006, 'msg' => '缺少必要参数path'],
'appKey error' => ['code' => 4007, 'msg' => '不存在 folder为${appKey}的apps配置'],
'template not found' => ['code' => 4008, 'msg' => '${template}模板不存在'],
'path not found' => ['code' => 4009, 'msg' => '${path}目录不存在'],
'classname error' => ['code' => 4010, 'msg' => '${classname}文件名不合法'],
'no config apps' => ['code' => 5000, 'msg' => 'apps配置不可为空'],
'no debug' => ['code' => 5001, 'msg' => '请在debug模式下使用该功能'],
'no config crud' => ['code' => 5002, 'msg' => 'crud未配置'],
'datatable crud error' => ['code' => 5003, 'msg' => '数据表创建失败,请检查配置'],
'file already exists' => ['code' => 5004, 'msg' => '${filepath}文件已存在'],
];
public function __construct(string $exceptionCode, int $statusCode = 412, array $data = [])
{
$exception = $this->getException($exceptionCode);
$msg = Utils::replaceTemplate($exception['msg'], $data);
parent::__construct($statusCode, $msg, null, [], $exception['code']);
}
public function getException($exceptionCode)
{
if (isset($this->exceptions[$exceptionCode])) {
return $this->exceptions[$exceptionCode];
}
throw new Exception('exceptionCode "' . $exceptionCode . '" Not Found');
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc\parseApi;
use hg\apidoc\Utils;
use think\facade\Config;
class CacheApiData
{
protected $config = [];
public function __construct()
{
$this->config = Config::get('apidoc');
}
/**
* 获取缓存目录
* @param string $appKey
* @return string
*/
protected function getCacheFolder(string $appKey):string
{
$config = $this->config;
$currentApps = (new Utils())->getCurrentApps($appKey);
$configPath = !empty($config['cache']) && !empty($config['cache']['path']) ? $config['cache']['path'] : '../runtime/apidoc/';
$cacheAppFolder = "";
if (!empty($currentApps) && count($currentApps) > 0) {
foreach ($currentApps as $keyIndex => $appItem) {
$cacheAppFolder .= $appItem['folder'] . "/";
}
}
$cacheFolder = $configPath . $cacheAppFolder;
return $cacheFolder;
}
/**
* 获取指定目录下缓存文件名列表
* @param string $folder
* @return array
*/
public function getCacheFileList(string $folder):array
{
$filePaths = glob($folder . '*.json');
$cacheFiles = [];
if (count($filePaths) > 0) {
foreach ($filePaths as $item) {
$cacheFiles[] = str_replace(".json", "", basename($item));
}
}
return $cacheFiles;
}
/**
* 获取接口缓存数据
* @param string $appKey
* @param string $cacheFileName
* @return array|false
*/
public function get(string $appKey, string $cacheFileName)
{
$cacheFolder = $this->getCacheFolder($appKey);
$cacheFileList = $this->getCacheFileList($cacheFolder);
if (!file_exists($cacheFolder)) {
return false;
}
if (empty($cacheFileName) && count($cacheFileList) > 0) {
// 默认最后一个缓存文件
$cacheFileName = $cacheFileList[count($cacheFileList) - 1];
}
$cacheFilePath = $cacheFolder . "/" . $cacheFileName . '.json';
if (file_exists($cacheFilePath)) {
// 存在缓存文件
$fileContent = file_get_contents($cacheFilePath);
if (empty($fileContent)) {
return false;
}
$json = json_decode($fileContent);
if (is_object($json)) {
$json = [
"data" => $json->data,
"tags" => $json->tags,
"groups" => $json->groups,
];
}
return [
'name' => $cacheFileName,
'data' => $json,
'list' => $cacheFileList
];
}
return false;
}
/**
* 设置接口缓存
* @param string $appKey
* @param array $json
* @return array|false
*/
public function set(string $appKey, array $json):array
{
if (empty($json)) {
return false;
}
$config = $this->config;
$fileName = date("Y-m-d H_i_s");
$fileContent = json_encode($json);
$cacheFolder = $this->getCacheFolder($appKey);
$path = $cacheFolder . $fileName . ".json";
Utils::createFile($path, $fileContent);
$filePaths = $this->getCacheFileList($cacheFolder);
if ($config['cache']['max'] && count($filePaths) >= $config['cache']['max']) {
//达到最大数量,删除第一个
$filePath = $cacheFolder . $filePaths[0] . ".json";
Utils::delFile($filePath);
}
return [
"name" => $fileName,
"data" => $json,
"list" => $this->getCacheFileList($cacheFolder)
];
}
}

View File

@@ -0,0 +1,663 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc\parseApi;
use Doctrine\Common\Annotations\AnnotationReader;
use hg\apidoc\Utils;
use ReflectionClass;
use Symfony\Component\ClassLoader\ClassMapGenerator;
use think\annotation\route\Resource;
use think\annotation\Route;
use hg\apidoc\annotation\Group;
use hg\apidoc\annotation\Sort;
use hg\apidoc\annotation\Param;
use hg\apidoc\annotation\Title;
use hg\apidoc\annotation\Desc;
use hg\apidoc\annotation\Author;
use hg\apidoc\annotation\Tag;
use hg\apidoc\annotation\Header;
use hg\apidoc\annotation\Returned;
use hg\apidoc\annotation\ParamType;
use hg\apidoc\annotation\Url;
use hg\apidoc\annotation\Method;
use think\annotation\route\Group as RouteGroup;
use think\facade\App;
use think\facade\Config;
class ParseAnnotation
{
protected $config = [];
protected $reader;
//tags当前应用/版本所有的tag
protected $tags = array();
//groups,当前应用/版本的分组name
protected $groups = array();
protected $controller_layer = "";
public function __construct()
{
$this->reader = new AnnotationReader();
$this->config = Config::get('apidoc')?Config::get('apidoc'):Config::get('apidoc.');
$this->controller_layer = Config::get('route.controller_layer',"controller");
}
/**
* 生成api接口数据
* @param string $appKey
* @return array
*/
public function renderApiData(string $appKey): array
{
$config = $this->config;
$currentApps = (new Utils())->getCurrentApps($appKey);
$currentApp = $currentApps[count($currentApps) - 1];
if (!empty($config['controllers']) && count($config['controllers']) > 0) {
// 配置的控制器列表
$controllers = $this->getConfigControllers($currentApp['path']);
} else {
// 默认读取所有的
$controllers = $this->getDirControllers($currentApp['path']);
}
$apiData = [];
if (!empty($controllers) && count($controllers) > 0) {
foreach ($controllers as $class) {
$classData = $this->parseController($class);
if ($classData !== false) {
$apiData[] = $classData;
}
}
}
$json = array(
"data" => $apiData,
"tags" => $this->tags,
"groups" => $this->groups
);
return $json;
}
/**
* 获取生成文档的控制器列表
* @param string $path
* @return array
*/
protected function getConfigControllers(string $path): array
{
$config = $this->config;
$controllers = [];
$configControllers = $config['controllers'];
if (!empty($configControllers) && count($configControllers) > 0) {
foreach ($configControllers as $item) {
if ( strpos($item, $path) !== false && class_exists($item)) {
$controllers[] = $item;
}
}
}
return $controllers;
}
/**
* 获取目录下的控制器列表
* @param string $path
* @return array
*/
protected function getDirControllers(string $path): array
{
if ($path) {
if (strpos(App::getRootPath(), '/') !== false) {
$pathStr = str_replace("\\", "/", $path);
} else {
$pathStr = $path;
}
$dir = App::getRootPath() . $pathStr;
} else {
$dir = App::getRootPath() . $this->controller_layer;
}
$controllers = [];
if (is_dir($dir)) {
$controllers = $this->scanDir($dir, $path);
}
return $controllers;
}
/**
* 处理指定目录下的控制器
* @param string $dir
* @param string $appPath
* @return array
*/
protected function scanDir(string $dir, string $appPath): array
{
$list = [];
foreach (ClassMapGenerator::createMap($dir) as $class => $path) {
if (
!isset($this->config['filter_controllers']) ||
(isset($this->config['filter_controllers']) && !in_array($class, $this->config['filter_controllers'])) &&
$this->config['definitions'] != $class
) {
if (strpos($class, '\\') === false) {
$list[] = $appPath . "\\" . $class;
} else {
$list[] = $class;
}
}
}
return $list;
}
protected function parseController($class)
{
$config = $this->config;
$data = [];
$refClass = new ReflectionClass($class);
$classTextAnnotations = $this->parseTextAnnotation($refClass);
if (in_array("NotParse", $classTextAnnotations)) {
return false;
}
$title = $this->reader->getClassAnnotation($refClass, Title::class);
$group = $this->reader->getClassAnnotation($refClass, Group::class);
$sort = $this->reader->getClassAnnotation($refClass, Sort::class);
$routeGroup = $this->reader->getClassAnnotation($refClass, RouteGroup::class);
$controllersNameArr = explode("\\", $class);
$controllersName = $controllersNameArr[count($controllersNameArr) - 1];
$data['controller'] = $controllersName;
$data['group'] = !empty($group->value) ? $group->value : null;
$data['sort'] = !empty($sort->value) ? $sort->value : null;
if (!empty($data['group']) && !in_array($data['group'], $this->groups)) {
$this->groups[] = $data['group'];
}
$data['title'] = !empty($title) && !empty($title->value) ? $title->value : "";
if (empty($title)) {
if (!empty($classTextAnnotations) && count($classTextAnnotations) > 0) {
$data['title'] = $classTextAnnotations[0];
} else {
$data['title'] = $controllersName;
}
}
$methodList = [];
$filter_method = !empty($config['filter_method']) ? $config['filter_method'] : [];
$data['menu_key'] = $data['controller'] . "_" . mt_rand(10000, 99999);
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $refMethod) {
if (!empty($refMethod->name) && !in_array($refMethod->name, $filter_method)) {
$methodItem = $this->parseAnnotation($refMethod, true,"controller");
if (!count((array)$methodItem)) {
continue;
}
$textAnnotations = $this->parseTextAnnotation($refMethod);
// 标注不解析的方法
if (in_array("NotParse", $textAnnotations)) {
continue;
}
// 无标题,且有文本注释
if (empty($methodItem['title']) && !empty($textAnnotations) && count($textAnnotations) > 0) {
$methodItem['title'] = $textAnnotations[0];
}
// 添加统一headers请求头参数
if (!empty($config['headers']) && !in_array("NotHeaders", $textAnnotations)) {
if (!empty($methodItem['header'])) {
$methodItem['header'] = Utils::arrayMergeAndUnique("name", $config['headers'], $methodItem['header']);
} else {
$methodItem['header'] = $config['headers'];
}
}
// 添加统一params请求参数
if (!empty($config['parameters']) && !in_array("NotParameters", $textAnnotations)) {
if (!empty($methodItem['param'])) {
$methodItem['param'] = Utils::arrayMergeAndUnique("name", $config['parameters'], $methodItem['param']);
} else {
$methodItem['param'] = $config['parameters'];
}
}
// 添加responses统一响应体
if (
!empty($config['responses']) &&
!is_string($config['responses']) &&
!in_array("NotResponses", $textAnnotations)
) {
// 显示在响应体中
$returned = [];
$hasMian = false;
if (isset($config['responses']['data']) && !$config['responses']['show_responses']) {
$responsesData = [];
} else if (isset($config['responses']['data']) && $config['responses']['show_responses'] === true) {
$responsesData = $config['responses']['data'];
} else {
$responsesData = $config['responses'];
}
// 合并统一响应体及响应参数相同的字段
$returnData = [];
$resKeys = [];
foreach ($responsesData as $resItem) {
$resKeys[]=$resItem['name'];
}
foreach ($methodItem['return'] as $returnItem){
if (!(in_array($returnItem['name'],$resKeys) && $returnItem['source']==='controller' && !empty($returnItem['replaceGlobal']))){
$returnData[]=$returnItem;
}
}
foreach ($responsesData as $resItem) {
$resData = $resItem;
$globalFind = Utils::getArrayFind($methodItem['return'],function ($item)use ($resItem){
if ($item['name'] == $resItem['name'] && $item['source']==='controller' && !empty($item['replaceGlobal'])){
return true;
}
return false;
});
if (!empty($globalFind)){
$resData = array_merge($resItem,$globalFind);
}else if (!empty($resData['main']) && $resData['main'] === true) {
$resData['params'] = $returnData;
$resData['resKeys']=$resKeys;
$hasMian = true;
}
$resData['find'] =$globalFind;
$returned[] = $resData;
}
if (!$hasMian) {
$returned = Utils::arrayMergeAndUnique("name", $returned, $methodItem['return']);
}
$methodItem['return'] = $returned;
}
// 默认method
if (empty($methodItem['method'])) {
$methodItem['method'] = !empty($config['default_method']) ? $config['default_method'] : 'GET';
}
// 默认default_author
if (empty($methodItem['author']) && !empty($config['default_author']) && !in_array("NotDefaultAuthor", $textAnnotations)) {
$methodItem['author'] = $config['default_author'];
}
// Tags
if (!empty($methodItem['tag'])) {
if (strpos($methodItem['tag'], ' ') !== false) {
$tagArr = explode(" ", $methodItem['tag']);
foreach ($tagArr as $tag) {
if (!in_array($tag, $this->tags)) {
$this->tags[] = $tag;
}
}
} else if (!in_array($methodItem['tag'], $this->tags)) {
$this->tags[] = $methodItem['tag'];
}
}
// 无url,自动生成
if (empty($methodItem['url'])) {
$methodItem['url'] = $this->autoCreateUrl($refMethod);
} else if (!empty($routeGroup->value)) {
// 路由分组url加上分组
$methodItem['url'] = '/' . $routeGroup->value . '/' . $methodItem['url'];
}
if (!empty($methodItem['url']) && substr($methodItem['url'], 0, 1) != "/") {
$methodItem['url'] = "/" . $methodItem['url'];
}
$methodItem['name'] = $refMethod->name;
$methodItem['menu_key'] = $methodItem['method'] . "_" . $refMethod->name . "_" . mt_rand(10000, 99999);
$methodList[] = $methodItem;
}
}
$data['children'] = $methodList;
return $data;
}
/**
* 自动生成url
* @param $method
* @return string
*/
protected function autoCreateUrl($method): string
{
if (Config::get("controller_auto_search") || !empty($this->config['controller_auto_search'])){
$pathArr = explode("\\", $method->class );
}else{
$searchString = $this->controller_layer . '\\';
$substr = substr(strstr($method->class, $searchString), strlen($searchString));
$pathArr = explode("\\", str_replace($substr, str_replace('\\', '.', $substr), $method->class));
}
// 控制器地址
$filterPathNames = array(App::getNamespace(), $this->controller_layer);
$classPathArr = [];
foreach ($pathArr as $item) {
if (!in_array($item, $filterPathNames)) {
if (!empty($this->config['auto_url_rule'])){
switch ($this->config['auto_url_rule']) {
case 'lcfirst':
$classPathArr[] = lcfirst($item);
break;
case 'ucfirst':
$classPathArr[] = ucfirst($item);
break;
default:
$classPathArr[] = $item;
}
}else{
$classPathArr[] = $item;
}
}
}
$classPath = implode('/', $classPathArr);
return '/' . $classPath . '/' . $method->name;
}
/**
* ref引用
* @param $refPath
* @param bool $enableRefService
* @return false|string[]
*/
protected function renderRef(string $refPath, bool $enableRefService = true): array
{
$res = ['type' => 'model'];
// 通用定义引入
if (strpos($refPath, '\\') === false) {
$config = $this->config;
$refPath = $config['definitions'] . '\\' . $refPath;
$data = $this->renderService($refPath);
$res['type'] = "service";
$res['data'] = $data;
return $res;
}
// 模型引入
$modelData = (new ParseModel($this->reader))->renderModel($refPath);
if ($modelData !== false) {
$res['data'] = $modelData;
return $res;
}
if ($enableRefService === false) {
return false;
}
$data = $this->renderService($refPath);
$res['type'] = "service";
$res['data'] = $data;
return $res;
}
/**
* 解析注释引用
* @param $refPath
* @return array
* @throws \ReflectionException
*/
protected function renderService(string $refPath)
{
$pathArr = explode("\\", $refPath);
$methodName = $pathArr[count($pathArr) - 1];
unset($pathArr[count($pathArr) - 1]);
$classPath = implode("\\", $pathArr);
$classReflect = new \ReflectionClass($classPath);
$methodName = trim($methodName);
$refMethod = $classReflect->getMethod($methodName);
$res = $this->parseAnnotation($refMethod, true);
return $res;
}
/**
* 处理Param/Returned的字段名name、params子级参数
* @param $values
* @return array
*/
protected function handleParamValue($values, string $field = 'param'): array
{
$name = "";
$params = [];
if (!empty($values) && is_array($values) && count($values) > 0) {
foreach ($values as $item) {
if (is_string($item)) {
$name = $item;
} else if (is_object($item)) {
if (!empty($item->ref)) {
$refRes = $this->renderRef($item->ref, true);
$params = $this->handleRefData($params, $refRes, $item, $field);
} else {
$param = [
"name" => "",
"type" => $item->type,
"desc" => $item->desc,
"default" => $item->default,
"require" => $item->require,
"childrenType"=> $item->childrenType
];
$children = $this->handleParamValue($item->value);
$param['name'] = $children['name'];
if (count($children['params']) > 0) {
$param['params'] = $children['params'];
}
$params[] = $param;
}
}
}
} else {
$name = $values;
}
return ['name' => $name, 'params' => $params];
}
/**
* 解析方法注释
* @param $refMethod
* @param bool $enableRefService 是否终止service的引入
* @param string $source 注解来源
* @return array
*/
protected function parseAnnotation($refMethod, bool $enableRefService = true,$source=""): array
{
$data = [];
if ($annotations = $this->reader->getMethodAnnotations($refMethod)) {
$headers = [];
$params = [];
$returns = [];
foreach ($annotations as $annotation) {
switch (true) {
case $annotation instanceof Param:
$params = $this->handleParamAndReturned($params,$annotation,'param',$enableRefService);
break;
case $annotation instanceof Returned:
$returns = $this->handleParamAndReturned($returns,$annotation,'return',$enableRefService,$source);
break;
case $annotation instanceof Header:
if (!empty($annotation->ref)) {
$refRes = $this->renderRef($annotation->ref, $enableRefService);
$headers = $this->handleRefData($headers, $refRes, $annotation, 'header');
} else {
$param = [
"name" => $annotation->value,
"desc" => $annotation->desc,
"require" => $annotation->require,
"type" => $annotation->type,
"default" => $annotation->default,
];
$headers[] = $param;
}
break;
case $annotation instanceof Route:
if (empty($data['method'])) {
$data['method'] = $annotation->method;
}
if (empty($data['url'])) {
$data['url'] = $annotation->value;
}
break;
case $annotation instanceof Author:
$data['author'] = $annotation->value;
break;
case $annotation instanceof Title:
$data['title'] = $annotation->value;
break;
case $annotation instanceof Desc:
$data['desc'] = $annotation->value;
break;
case $annotation instanceof ParamType:
$data['paramType'] = $annotation->value;
break;
case $annotation instanceof Url:
$data['url'] = $annotation->value;
break;
case $annotation instanceof Method:
$data['method'] = $annotation->value;
break;
case $annotation instanceof Tag:
$data['tag'] = $annotation->value;
break;
}
}
if ($headers && count($headers) > 0) {
$data['header'] = $headers;
}
$data['param'] = $params;
$data['return'] = $returns;
}
return $data;
}
/**
* 处理请求参数与返回参数
* @param $params
* @param $annotation
* @param string $type
* @param false $enableRefService
* @param string $source 注解来源
* @return array
*/
protected function handleParamAndReturned($params,$annotation,$type="param",$enableRefService=false,$source=""){
if (!empty($annotation->ref)) {
$refRes = $this->renderRef($annotation->ref, $enableRefService);
$params = $this->handleRefData($params, $refRes, $annotation, $type,$source);
} else {
$param = [
"name" => "",
"type" => $annotation->type,
"desc" => $annotation->desc,
"default" => $annotation->default,
"require" => $annotation->require,
"childrenType" => $annotation->childrenType,
"source" => $source,
"replaceGlobal" =>!empty($annotation->replaceGlobal)?$annotation->replaceGlobal:""
];
$children = $this->handleParamValue($annotation->value, $type);
$param['name'] = $children['name'];
if (count($children['params']) > 0) {
$param['params'] = $children['params'];
if ($annotation->type === 'tree' && !empty($annotation->childrenField)) {
// 类型为tree的
$param['params'][] = [
'params' => $children['params'],
'name' => $annotation->childrenField,
'type' => 'array',
'desc' => $annotation->childrenDesc,
];
}
}
// 合并同级已有的字段
$params = Utils::arrayMergeAndUnique("name", $params, [$param]);
}
return $params;
}
/**
* 解析非注解文本注释
* @param $refMethod
* @return array|false
*/
protected function parseTextAnnotation($refMethod): array
{
$annotation = $refMethod->getDocComment();
if (empty($annotation)) {
return [];
}
if (preg_match('#^/\*\*(.*)\*/#s', $annotation, $comment) === false)
return [];
$comment = trim($comment [1]);
if (preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false)
return [];
$data = [];
foreach ($lines[1] as $line) {
$line = trim($line);
if (!empty ($line) && strpos($line, '@') !== 0) {
$data[] = $line;
}
}
return $data;
}
/**
* 处理param、returned 参数
* @param $params
* @param $refRes
* @param $annotation
* @param string|null $source 注解来源
* @return array
*/
protected function handleRefData($params, $refRes, $annotation, string $field,$source=""): array
{
if ($refRes['type'] === "model" && count($refRes['data']) > 0) {
// 模型引入
$data = $refRes['data'];
} else if ($refRes['type'] === "service" && !empty($refRes['data']) && !empty($refRes['data'][$field])) {
// service引入
$data = $refRes['data'][$field];
} else {
return $params;
}
// 过滤field
if (!empty($annotation->field)) {
$data = (new Utils())->filterParamsField($data, $annotation->field, 'field');
}
// 过滤withoutField
if (!empty($annotation->withoutField)) {
$data = (new Utils())->filterParamsField($data, $annotation->withoutField, 'withoutField');
}
if (!empty($annotation->value)) {
$item = [
'name' => $annotation->value,
'desc' => $annotation->desc,
'type' => $annotation->type,
'require' => $annotation->require,
'default' => $annotation->default,
'params' => $data,
'source'=>$source,
"replaceGlobal" =>!empty($annotation->replaceGlobal)?$annotation->replaceGlobal:""
];
$children = $this->handleParamValue($annotation->value, 'param');
$item['name'] = $children['name'];
if (count($children['params']) > 0) {
$item['params'] = Utils::arrayMergeAndUnique("name",$data,$children['params']);
if ($annotation->type === 'tree' && !empty($annotation->childrenField)) {
// 类型为tree的
$item['params'][] = [
'params' => $item['params'],
'name' => $annotation->childrenField,
'type' => 'array',
'desc' => $annotation->childrenDesc,
];
}
}
$params[] = $item;
} else {
$params = array_merge($params, $data);
}
return $params;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc\parseApi;
use think\facade\App;
use hg\apidoc\Utils;
use think\facade\Config;
class ParseMarkdown
{
protected $config = [];
public function __construct()
{
$this->config = Config::get('apidoc')?Config::get('apidoc'):Config::get('apidoc.');
}
/**
* 获取md文档菜单
* @return array
*/
public function getDocsMenu(): array
{
$config = $this->config;
$docData = [];
if (!empty($config['docs']) && !empty($config['docs']['menus']) && count($config['docs']['menus']) > 0) {
$docData = $this->handleDocsMenuData($config['docs']['menus']);
}
return $docData;
}
/**
* 处理md文档菜单数据
* @param array $menus
* @return array
*/
protected function handleDocsMenuData(array $menus): array
{
$list = [];
foreach ($menus as $item) {
if (!empty($item['items']) && count($item['items']) > 0) {
$item['items'] = $this->handleDocsMenuData($item['items']);
$item['group'] = 'markdown_doc';
$item['menu_key'] = "md_group_" . mt_rand(10000, 99999);
$list[] = $item;
} else {
$item['type'] = 'md';
$item['menu_key'] = "md_" . mt_rand(10000, 99999);
$list[] = $item;
}
}
return $list;
}
/**
* 获取md文档内容
* @param string $appKey
* @param string $path
* @return string
*/
public function getContent(string $appKey, string $path): string
{
$currentApps = (new Utils())->getCurrentApps($appKey);
$mdPath = (new Utils())->replaceCurrentAppTemplate($path, $currentApps);
$filePath = App::getRootPath() . $mdPath . '.md';
$contents = Utils::getFileContent($filePath);
return $contents;
}
}

View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types = 1);
namespace hg\apidoc\parseApi;
use Doctrine\Common\Annotations\Reader;
use think\Db as Db5;
use think\facade\Db;
use hg\apidoc\annotation\Field;
use hg\apidoc\annotation\WithoutField;
use hg\apidoc\annotation\AddField;
use think\helper\Str;
use hg\apidoc\Utils;
class ParseModel
{
protected $reader;
public function __construct(Reader $reader)
{
$this->reader = $reader;
}
/**
* 生成模型数据
* @param string $path
* @return array|false
* @throws \ReflectionException
*/
public function renderModel(string $path)
{
$modelClassArr = explode("\\", $path);
$modelActionName = $modelClassArr[count($modelClassArr) - 1];
$modelClassName = $modelClassArr[count($modelClassArr) - 2];
unset($modelClassArr[count($modelClassArr) - 1]);
$modelClassPath = implode("\\", $modelClassArr);
$classReflect = new \ReflectionClass($modelClassPath);
$modelActionName = trim($modelActionName);
$methodAction = $classReflect->getMethod($modelActionName);
// 获取所有模型属性
$propertys = $classReflect->getDefaultProperties();
// 获取表字段
$model = $this->getModel($methodAction, $modelClassName);
if (!is_callable(array($model, 'getTable'))) {
return false;
}
$table = $this->getTableDocument($model, $propertys);
// 模型注释-field
if ($fieldAnnotations = $this->reader->getMethodAnnotation($methodAction, Field::class)) {
$table = (new Utils())->filterParamsField($table, $fieldAnnotations->value, 'field');
}
// 模型注释-withoutField
if ($fieldAnnotations = $this->reader->getMethodAnnotation($methodAction, WithoutField::class)) {
$table = (new Utils())->filterParamsField($table, $fieldAnnotations->value, 'withoutField');
}
// 模型注释-addField
if ($annotations = $this->reader->getMethodAnnotations($methodAction)) {
foreach ($annotations as $annotation) {
switch (true) {
case $annotation instanceof AddField:
$param = [
"name" => "",
"desc" => $annotation->desc,
"require" => $annotation->require,
"type" => $annotation->type,
"default" => $annotation->default
];
$children = $this->handleParamValue($annotation->value);
$param['name'] = $children['name'];
if (count($children['params']) > 0) {
$param['params'] = $children['params'];
}
$isExists = false;
$newTable = [];
foreach ($table as $item) {
if ($param['name'] === $item['name']) {
$isExists = true;
$newTable[] = $param;
} else {
$newTable[] = $item;
}
}
$table = $newTable;
if (!$isExists) {
$table[] = $param;
}
break;
}
}
}
return $table;
}
/**
* 处理字段参数
* @param $values
* @return array
*/
protected function handleParamValue($values): array
{
$name = "";
$params = [];
if (!empty($values) && is_array($values) && count($values) > 0) {
foreach ($values as $item) {
if (is_string($item)) {
$name = $item;
} else if (is_object($item)) {
$param = [
"name" => "",
"type" => $item->type,
"desc" => $item->desc,
"default" => $item->default,
"require" => $item->require,
];
$children = $this->handleParamValue($item->value);
$param['name'] = $children['name'];
if (count($children['params']) > 0) {
$param['params'] = $children['params'];
}
$params[] = $param;
}
}
} else {
$name = $values;
}
return ['name' => $name, 'params' => $params];
}
/**
* 获取模型实例
* @param $method
* @param $modelClassName
* @return mixed|null
*/
public function getModel($method, $modelClassName)
{
if (!empty($method->class)) {
$relationModelClass = $this->getIncludeClassName($method->class, $modelClassName);
if ($relationModelClass) {
$modelInstance = new $relationModelClass();
return $modelInstance;
} else {
return null;
}
} else {
return null;
}
}
/**
* 获取模型类
* @param $mainClass
* @param $class
* @return string
* @throws \ReflectionException
*/
protected function getIncludeClassName($mainClass, $class)
{
$classReflect = new \ReflectionClass($mainClass);
$possibleClass = $classReflect->getNamespaceName() . "\\" . $class;
if (class_exists($possibleClass)) {
return $possibleClass;
} else {
return "";
}
}
/**
* 获取模型注解数据
* @param $model
* @param $propertys
* @return array
*/
public function getTableDocument($model,array $propertys):array
{
$tp_version = \think\facade\App::version();
if (substr($tp_version, 0, 2) == '5.'){
$createSQL = Db5::query("show create table " . $model->getTable())[0]['Create Table'];
}else{
$createSQL = Db::query("show create table " . $model->getTable())[0]['Create Table'];
}
// $createSQL = Db::query("show create table " . $model->getTable())[0]['Create Table'];
preg_match_all("#[^KEY]`(.*?)` (.*?) (.*?),\n#", $createSQL, $matches);
$fields = $matches[1];
$types = $matches[2];
$contents = $matches[3];
$fieldComment = [];
//组织注释
for ($i = 0; $i < count($matches[0]); $i++) {
$key = $fields[$i];
$type = $types[$i];
$default = "";
$require = "0";
$desc = "";
$content = $contents[$i];
if (strpos($type, '(`') !== false) {
continue;
}
if (strpos($content, 'COMMENT') !== false) {
// 存在字段注释
preg_match_all("#COMMENT\s*'(.*?)'#", $content, $edscs);
if (!empty($edscs[1]) && !empty($edscs[1][0]))
$desc = $edscs[1][0];
}
if (strpos($content, 'DEFAULT') !== false) {
// 存在字段默认值
preg_match_all("#DEFAULT\s*'(.*?)'#", $content, $defaults);
$default = $defaults[1] && is_array($defaults[1])?$defaults[1][0]:"";
}
if (strpos($content, 'NOT NULL') !== false) {
// 必填字段
$require = "1";
}
$name = $key;
// 转换字段名为驼峰命名(用于输出)
if (isset($propertys['convertNameToCamel']) && $propertys['convertNameToCamel'] === true) {
$name = Str::camel($key);
}
$fieldComment[] = [
"name" => $name,
"type" => $type,
"desc" => $desc,
"default" => $default,
"require" => $require,
];
}
return $fieldComment;
}
}