model.php 78 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418
  1. <?php
  2. class biModel extends model
  3. {
  4. /**
  5. * 获得可查看的对象ID列表。
  6. * Get viewable object idlist.
  7. *
  8. * @param string $objectType dimension|screen|pivot|chart
  9. * @access public
  10. * @return array
  11. */
  12. public function getViewableObject($objectType)
  13. {
  14. if(!in_array($objectType, array('dimension', 'screen', 'pivot', 'chart'))) return array();
  15. $table = array(
  16. 'dimension' => TABLE_DIMENSION,
  17. 'screen' => TABLE_SCREEN,
  18. 'pivot' => TABLE_PIVOT,
  19. 'chart' => TABLE_CHART
  20. );
  21. $objects = $this->dao->select('id,createdBy,acl,whitelist')->from($table[$objectType])
  22. ->where('deleted')->eq('0')
  23. ->fetchAll('id');
  24. /* IF table have not acl or whitelist field */
  25. $object = reset($objects);
  26. if(!isset($object->acl) || !isset($object->whitelist))
  27. {
  28. $objects = $this->dao->select('id')->from($table[$objectType])
  29. ->where('deleted')->eq('0')
  30. ->fetchAll('id');
  31. return array_keys($objects);
  32. }
  33. /* IF table have acl and whitelist field */
  34. if($this->app->user->admin) return array_keys($objects);
  35. $objectIDList = array();
  36. $account = $this->app->user->account;
  37. foreach($objects as $objectID => $object)
  38. {
  39. if($object->createdBy == $account)
  40. {
  41. $objectIDList[] = $objectID;
  42. }
  43. else
  44. {
  45. if($object->acl == 'open')
  46. {
  47. $objectIDList[] = $objectID;
  48. }
  49. else
  50. {
  51. $whitelist = explode(',', $object->whitelist);
  52. if(in_array($account, $whitelist)) $objectIDList[] = $objectID;
  53. }
  54. }
  55. }
  56. return $objectIDList;
  57. }
  58. /**
  59. * 解析sql语句。
  60. * Parse sql to statement.
  61. *
  62. * @param string $sql
  63. * @access public
  64. * @return object|false
  65. */
  66. public function parseToStatement($sql)
  67. {
  68. $this->app->loadClass('sqlparser', true);
  69. $parser = new sqlparser($sql);
  70. if(empty($parser->statements) || !isset($parser->statements[0])) return false;
  71. $statement = $parser->statements[0];
  72. return !empty($statement) ? $statement : false;
  73. }
  74. /**
  75. * 获取sql中的表、字段。
  76. * Get tables and fields form sql.
  77. *
  78. * @param string $sql
  79. * @access public
  80. * @return array|false
  81. */
  82. public function getTableAndFields($sql)
  83. {
  84. $statement = $this->parseToStatement($sql);
  85. if(empty($statement)) return false;
  86. return array('tables' => array_unique($this->getTables($statement, true)), 'fields' => $this->getFields($statement));
  87. }
  88. /**
  89. * 获取sql中的字段。
  90. * Get fields form sqlparser statment.
  91. *
  92. * @param object $statment
  93. * @access public
  94. * @return array
  95. * @param object $statement
  96. */
  97. public function getFields($statement)
  98. {
  99. if(!$statement->expr) return array();
  100. $fields = array();
  101. foreach($statement->expr as $fieldInfo)
  102. {
  103. $field = $fieldInfo->expr;
  104. $alias = $field;
  105. if(!empty($fieldInfo->alias))
  106. {
  107. $alias = $fieldInfo->alias;
  108. }
  109. elseif(strrpos($field, '.') !== false)
  110. {
  111. $alias = explode('.', $field)[1];
  112. }
  113. $fields[$alias] = $field;
  114. }
  115. return $fields;
  116. }
  117. /**
  118. * 获取sql中的表名。
  119. * Get tables form sqlparser statment.
  120. *
  121. * @param object $statment
  122. * @param bool $deep
  123. * @access public
  124. * @return array
  125. * @param object $statement
  126. */
  127. public function getTables($statement, $deep = false)
  128. {
  129. $tables = array();
  130. if($statement->from)
  131. {
  132. foreach($statement->from as $fromInfo)
  133. {
  134. if($fromInfo->table)
  135. {
  136. $tables[] = $fromInfo->table;
  137. }
  138. elseif($deep && $fromInfo->subquery)
  139. {
  140. $parser = new sqlparser($fromInfo->expr);
  141. $subTables = $this->getTables($parser->statements[0], true);
  142. $tables = array_merge($tables, $subTables);
  143. }
  144. }
  145. }
  146. if($statement->join)
  147. {
  148. foreach($statement->join as $joinInfo)
  149. {
  150. if($joinInfo->expr->table)
  151. {
  152. $tables[] = $joinInfo->expr->table;
  153. }
  154. elseif($deep && $joinInfo->expr->subquery)
  155. {
  156. $parser = new sqlparser($joinInfo->expr->expr);
  157. $subTables = $this->getTables($parser->statements[0], true);
  158. $tables = array_merge($tables, $subTables);
  159. }
  160. }
  161. }
  162. return array_filter(array_unique($tables));
  163. }
  164. /**
  165. * 解析sql语句,返回sql中出现的表别名和表名的键值对列表。
  166. * Parse sql to alias => table list.
  167. *
  168. * @param string $sql
  169. * @access public
  170. * @return array
  171. */
  172. public function parseTableList($sql)
  173. {
  174. $statement = $this->parseToStatement($sql);
  175. if(!$statement) return array();
  176. $tableList = array();
  177. if($statement->from)
  178. {
  179. foreach($statement->from as $fromInfo)
  180. {
  181. if(!empty($fromInfo->alias) && $fromInfo->subquery == 'SELECT')
  182. {
  183. $tableList[$fromInfo->alias] = $fromInfo->expr;
  184. }
  185. elseif(!empty($fromInfo->alias) && !empty($fromInfo->table))
  186. {
  187. $tableList[$fromInfo->alias] = $fromInfo->table;
  188. }
  189. elseif(empty($fromInfo->alias) && !empty($fromInfo->table))
  190. {
  191. $tableList[$fromInfo->table] = $fromInfo->table;
  192. }
  193. }
  194. }
  195. if($statement->join)
  196. {
  197. foreach($statement->join as $joinInfo)
  198. {
  199. if(!empty($joinInfo->expr->alias) && $joinInfo->expr->subquery == 'SELECT')
  200. {
  201. $tableList[$joinInfo->expr->alias] = $joinInfo->expr->expr;
  202. }
  203. elseif(!empty($joinInfo->expr->alias) && !empty($joinInfo->expr->table))
  204. {
  205. $tableList[$joinInfo->expr->alias] = $joinInfo->expr->table;
  206. }
  207. elseif(empty($joinInfo->expr->alias) && !empty($joinInfo->expr->table))
  208. {
  209. $tableList[$joinInfo->expr->table] = $joinInfo->expr->table;
  210. }
  211. }
  212. }
  213. return $tableList;
  214. }
  215. /**
  216. * 解析sql语句,返回sql中查询的字段名和字段所属表的键值对。
  217. * Parse sql to field => table list.
  218. *
  219. * @param string $sql
  220. * @access public
  221. * @return array
  222. */
  223. public function getFieldsWithTable($sql)
  224. {
  225. $this->loadModel('dev');
  226. $statement = $this->parseToStatement($sql);
  227. if(empty($statement) || !isset($statement->expr)) return array();
  228. $fieldList = array();
  229. foreach($statement->expr as $expr)
  230. {
  231. $table = empty($expr->table) ? $statement->from[0]->table : $this->getTableByAlias($statement, $expr->table);
  232. if((empty($expr->table) && $expr->expr == "*") || (!empty($expr->table) && $expr->expr == "{$expr->table}.*"))
  233. {
  234. $fields = $this->dev->getFields($table);
  235. foreach($fields as $field => $fieldInfo) $fieldList[$field] = $table;
  236. }
  237. if(!empty($expr->column))
  238. {
  239. $field = !empty($expr->alias) ? $expr->alias : $expr->column;
  240. $fieldList[$field] = $table;
  241. }
  242. }
  243. return $fieldList;
  244. }
  245. /**
  246. * Parse sql to alias => field pairs.
  247. *
  248. * @param string $sql
  249. * @access public
  250. * @return array
  251. */
  252. public function getFieldsWithAlias($sql)
  253. {
  254. $statement = $this->parseToStatement($sql);
  255. if(empty($statement)) return array();
  256. $fieldList = array();
  257. foreach($statement->expr as $expr)
  258. {
  259. $table = empty($expr->table) ? $statement->from[0]->table : $this->getTableByAlias($statement, $expr->table);
  260. if(substr_compare($expr->expr, '*', -strlen('*')) === 0)
  261. {
  262. $fields = $this->dev->getFields($table);
  263. foreach($fields as $field => $fieldInfo) $fieldList[$field] = $field;
  264. }
  265. else
  266. {
  267. $alias = !empty($expr->alias) ? $expr->alias : $expr->column;
  268. $field = $expr->column;
  269. $fieldList[$alias] = $field;
  270. }
  271. }
  272. return $fieldList;
  273. }
  274. /**
  275. * 根据表的别名获取其在sql语句中的表名。
  276. * Get table name by it's alias.
  277. *
  278. * @param object $statment
  279. * @param string $alias
  280. * @access public
  281. * @return string|false
  282. */
  283. public function getTableByAlias($statement, $alias)
  284. {
  285. $table = false;
  286. if($statement->from)
  287. {
  288. foreach($statement->from as $fromInfo) if($fromInfo->alias == $alias) $table = $fromInfo->table;
  289. }
  290. if($statement->join)
  291. {
  292. foreach($statement->join as $joinInfo) if($joinInfo->expr->alias == $alias) $table = $joinInfo->expr->table;
  293. }
  294. return $table;
  295. }
  296. /**
  297. * Try to explain sql.
  298. *
  299. * @param string $sql
  300. * @param string $driver mysql|duckdb
  301. * @access public
  302. * @return array
  303. */
  304. public function explainSQL($sql, $driver = 'mysql')
  305. {
  306. $dbh = $this->app->loadDriver($driver);
  307. $isDM = $this->config->db->driver == 'dm';
  308. $prefixSQL = $driver == 'mysql' ? 'EXPLAIN' : 'PRAGMA enable_profiling=json; EXPLAIN ANALYZE';
  309. if($isDM) $sql = $dbh->formatSQL($sql);
  310. try
  311. {
  312. $isDM ? $dbh->exec("$prefixSQL $sql") : $dbh->query("$prefixSQL $sql")->fetchAll(); // [dmdb] EXPLAIN cannot fetch data.
  313. }
  314. catch(Exception $e)
  315. {
  316. $message = preg_replace("/\r|\n|\t/", "", $e->getMessage());
  317. $message = strip_tags($message);
  318. return array('result' => 'fail', 'message' => $message);
  319. }
  320. return array('result' => 'success');
  321. }
  322. /**
  323. * Query a sql with driver.
  324. *
  325. * @param string $driver mysql|duckdb
  326. * @param string $sql
  327. * @param bool $fetchAll
  328. * @access public
  329. * @return array|string
  330. */
  331. public function queryWithDriver($driver, $sql, $fetchAll = true)
  332. {
  333. $dbh = $this->app->loadDriver($driver);
  334. /* 过滤掉模板数据。 */
  335. if(strpos($sql, 'zt_project') !== false) $sql = preg_replace('/\bzt_project\b(?!\w)/', 'ztv_projectnotpl', $sql);
  336. if(strpos($sql, 'zt_task') !== false) $sql = preg_replace('/\bzt_task\b(?!\w)/', 'ztv_tasknotpl', $sql);
  337. if($fetchAll) $results = $dbh->query($sql)->fetchAll();
  338. else $results = $dbh->query($sql)->fetch();
  339. return $results;
  340. }
  341. /**
  342. * Get sql result columns.
  343. *
  344. * @param string $sql
  345. * @param string $driver mysql|duckdb
  346. * @param bool $returnOrigin
  347. * @access public
  348. * @return array|false
  349. */
  350. public function getColumns($sql, $driver = 'mysql', $returnOrigin = false)
  351. {
  352. if(!in_array($driver, $this->config->bi->drivers)) return false;
  353. if($driver == 'mysql')
  354. {
  355. $columns = $this->dao->getColumns($sql);
  356. }
  357. else
  358. {
  359. $dbh = $this->app->loadDriver('duckdb');
  360. $columns = $dbh->query("DESCRIBE $sql")->fetchAll();
  361. }
  362. if($returnOrigin) return $columns;
  363. $result = array();
  364. foreach($columns as $column)
  365. {
  366. $column = (array)$column;
  367. $name = $driver == 'mysql' ? $column['name'] : $column['column_name'];
  368. $nativeType = $driver == 'mysql' ? zget($column, 'native_type', 'string') : zget($column, 'column_type', 'string');
  369. $result[$name] = array('name' => $name, 'native_type' => $nativeType);
  370. }
  371. return $result;
  372. }
  373. /**
  374. * 获取表的字段类型。
  375. * Get table data.
  376. *
  377. * @param string $sql
  378. * @param string $driverName mysql|duckdb
  379. * @param array $columns
  380. * @access public
  381. * @return object
  382. */
  383. public function getColumnsType($sql, $driverName = 'mysql', $columns = array())
  384. {
  385. if(empty($columns)) $columns = $this->getColumns($sql, $driverName);
  386. $columnTypes = new stdclass();
  387. foreach($columns as $column)
  388. {
  389. $field = $column['name'];
  390. $nativeType = $column['native_type'];
  391. /* DuckDB DECIMAL(prec, scale), NUMERIC(prec, scale), process it to DECIMAL and NUMERIC */
  392. $nativeType = strpos($nativeType, 'DECIMAL') === 0 ? 'DECIMAL' : $nativeType;
  393. $nativeType = strpos($nativeType, 'NUMERIC') === 0 ? 'NUMERIC' : $nativeType;
  394. $type = zget($this->config->bi->columnTypes->$driverName, $nativeType, 'string');
  395. if(isset($columnTypes->$field)) $field = $column['table'] . $field;
  396. $columnTypes->$field = $type;
  397. }
  398. return $columnTypes;
  399. }
  400. /**
  401. * Get object options.
  402. *
  403. * @param string $type user|product|project|execution|dept
  404. * @access public
  405. * @return array
  406. */
  407. public function getScopeOptions($type)
  408. {
  409. $options = array();
  410. switch($type)
  411. {
  412. case 'user':
  413. $options = $this->loadModel('user')->getPairs('noletter');
  414. break;
  415. case 'product':
  416. $options = $this->loadModel('product')->getPairs();
  417. break;
  418. case 'project':
  419. $options = $this->loadModel('project')->getPairsByProgram();
  420. break;
  421. case 'execution':
  422. $options = $this->loadModel('execution')->getPairs();
  423. break;
  424. case 'dept':
  425. $options = $this->loadModel('dept')->getOptionMenu(0);
  426. break;
  427. case strpos($type, '.') !== false:
  428. $params = explode('.', $type);
  429. if(empty(array_filter($params)))
  430. {
  431. $options = array();
  432. }
  433. else
  434. {
  435. $module = $params[0];
  436. $typeList = $params[1] . 'List';
  437. $this->app->loadLang($module);
  438. $options = $this->lang->$module->$typeList;
  439. }
  440. break;
  441. }
  442. return $options;
  443. }
  444. /**
  445. * Get object options.
  446. *
  447. * @param string $object
  448. * @param string $field
  449. * @access public
  450. * @return array
  451. */
  452. public function getDataviewOptions($object, $field)
  453. {
  454. $options = array();
  455. $path = $this->app->getModuleRoot() . 'dataview' . DS . 'table' . DS . "$object.php";
  456. if(is_file($path))
  457. {
  458. include $path;
  459. $fieldConfig = zget($schema->fields, $field, array());
  460. $options = zget($fieldConfig, 'options', array());
  461. }
  462. return is_array($options) ? $options : array();
  463. }
  464. /**
  465. * Get object options.
  466. *
  467. * @param string $object
  468. * @param string $field
  469. * @access public
  470. * @return array
  471. */
  472. public function getObjectOptions($object, $field)
  473. {
  474. $options = array();
  475. $useTable = $object;
  476. $useField = $field;
  477. $path = $this->app->getModuleRoot() . 'dataview' . DS . 'table' . DS . "$object.php";
  478. if(is_file($path))
  479. {
  480. include $path;
  481. $fieldObject = isset($schema->fields[$field]['object']) ? $schema->fields[$field]['object'] : '';
  482. $fieldShow = isset($schema->fields[$field]['show']) ? explode('.', $schema->fields[$field]['show']) : array();
  483. if($fieldObject) $useTable = $fieldObject;
  484. if(count($fieldShow) == 2) $useField = $fieldShow[1];
  485. }
  486. $table = isset($this->config->objectTables[$useTable]) ? $this->config->objectTables[$useTable] : zget($this->config->objectTables, $object, '');
  487. if($table)
  488. {
  489. $columns = $this->dao->descTable($table);
  490. foreach($columns as $id => $column) $columns[$id] = (array)$column;
  491. $fieldList = array_column($columns, 'field');
  492. $useField = in_array($useField, $fieldList) ? $useField : 'id';
  493. $options = $this->dao->select("id, {$useField}")->from($table)->fetchPairs();
  494. // htmlspecialchars values
  495. foreach($options as $key => $value) $options[$key] = str_replace('"', '', htmlspecialchars_decode($value));
  496. }
  497. return $options;
  498. }
  499. /**
  500. * Get pairs from column by keyField and valueField.
  501. *
  502. * @param string $sql
  503. * @param string $keyField
  504. * @param string $valueField
  505. * @param string $driver
  506. * @access public
  507. * @return array
  508. */
  509. public function getOptionsFromSql($sql, $driver, $keyField, $valueField)
  510. {
  511. $options = array();
  512. $dbh = $this->app->loadDriver($driver);
  513. $cols = $dbh->query($sql)->fetchAll();
  514. $sample = current($cols);
  515. if(!isset($sample->$keyField) or !isset($sample->$valueField)) return $options;
  516. foreach($cols as $col)
  517. {
  518. $key = $col->$keyField;
  519. $value = $col->$valueField;
  520. $options[$key] = $value;
  521. }
  522. return $options;
  523. }
  524. /**
  525. * 生成水球图参数。
  526. * Generate water polo options.
  527. *
  528. * @param array $fields
  529. * @param array $settings
  530. * @param string $sql
  531. * @param array $filters
  532. * @access public
  533. * @return array
  534. */
  535. public function genWaterpolo($fields, $settings, $sql, $filters)
  536. {
  537. $this->loadModel('chart');
  538. $operate = "{$settings['calc']}({$settings['goal']})";
  539. $sql = "select $operate as count from ($sql) tt ";
  540. $moleculeSQL = $sql;
  541. $denominatorSQL = $sql;
  542. $moleculeWheres = array();
  543. $denominatorWheres = array();
  544. foreach($settings['conditions'] as $condition)
  545. {
  546. $where = "{$condition['field']} {$this->config->chart->conditionList[$condition['condition']]} '{$condition['value']}'";
  547. $moleculeWheres[] = $where;
  548. }
  549. if(!empty($filters))
  550. {
  551. $wheres = array();
  552. foreach($filters as $field => $filter)
  553. {
  554. $wheres[] = "$field {$filter['operator']} {$filter['value']}";
  555. }
  556. $moleculeWheres = array_merge($moleculeWheres, $wheres);
  557. $denominatorWheres = $wheres;
  558. }
  559. if($moleculeWheres) $moleculeSQL .= 'where ' . implode(' and ', $moleculeWheres);
  560. if($denominatorWheres) $denominatorSQL .= 'where ' . implode(' and ', $denominatorWheres);
  561. $molecule = $this->dao->query($moleculeSQL)->fetch();
  562. $denominator = $this->dao->query($denominatorSQL)->fetch();
  563. $percent = $denominator->count ? round((float)$molecule->count / (float)$denominator->count, 4) : 0;
  564. $series = array(array('type' => 'liquidFill', 'data' => array($percent), 'color' => array('#2e7fff'), 'outline' => array('show' => false), 'label' => array('fontSize' => 26)));
  565. $tooltip = array('show' => true);
  566. $options = array('series' => $series, 'tooltip' => $tooltip);
  567. return $options;
  568. }
  569. /**
  570. * Get multi data.
  571. *
  572. * @param int $settings
  573. * @param int $defaultSql
  574. * @param int $filters
  575. * @access public
  576. * @return void
  577. */
  578. public function getMultiData($settings, $defaultSql, $filters, $driver, $sort = false)
  579. {
  580. $this->loadModel('chart');
  581. $group = isset($settings['xaxis'][0]['field']) ? $settings['xaxis'][0]['field'] : '';
  582. $date = isset($settings['xaxis'][0]['group']) ? zget($this->config->chart->dateConvert, $settings['xaxis'][0]['group']) : '';
  583. $metrics = array();
  584. $aggs = array();
  585. foreach($settings['yaxis'] as $yaxis)
  586. {
  587. $metrics[] = $yaxis['field'];
  588. $aggs[] = $yaxis['valOrAgg'];
  589. }
  590. $yCount = count($metrics);
  591. $xLabels = array();
  592. $yStats = array();
  593. for($i = 0; $i < $yCount; $i ++)
  594. {
  595. $metric = $metrics[$i];
  596. $agg = $aggs[$i];
  597. $groupSql = $groupBySql = "tt.`$group`";
  598. if(!empty($date))
  599. {
  600. $groupSql = $date == 'MONTH' ? "YEAR(tt.`$group`) as ttyear, $date(tt.`$group`) as ttgroup" : "$date(tt.`$group`) as $group";
  601. $groupBySql = $date == 'MONTH' ? "YEAR(tt.`$group`), $date(tt.`$group`)" : "$date(tt.`$group`)";
  602. }
  603. if($agg == 'distinct')
  604. {
  605. $aggSQL = "count($agg tt.`$metric`) as `$metric`";
  606. }
  607. else
  608. {
  609. $aggSQL = "$agg(tt.`$metric`) as `$metric`";
  610. }
  611. $sql = "select $groupSql,$aggSQL from ($defaultSql) tt";
  612. if(!empty($filters))
  613. {
  614. $wheres = array();
  615. foreach($filters as $field => $filter)
  616. {
  617. $wheres[] = "`$field` {$filter['operator']} {$filter['value']}";
  618. }
  619. $whereStr = implode(' and ', $wheres);
  620. $sql .= " where $whereStr";
  621. }
  622. $sql .= " group by $groupBySql";
  623. $rows = $this->queryWithDriver($driver, $sql);
  624. $stat = $this->chart->processRows($rows, $date, $group, $metric);
  625. $maxCount = 50;
  626. if($sort) arsort($stat);
  627. $yStats[] = $stat;
  628. $xLabels = array_merge($xLabels, array_keys($stat));
  629. $xLabels = array_unique($xLabels);
  630. }
  631. return array($group, $metrics, $aggs, $xLabels, $yStats);
  632. }
  633. /**
  634. * Get all tables with their fields.
  635. *
  636. * @access public
  637. * @return array
  638. */
  639. public function getTableFields()
  640. {
  641. $this->loadModel('dev');
  642. $tables = $this->dev->getTables();
  643. $tableFields = array();
  644. foreach($tables as $groupTables)
  645. {
  646. foreach($groupTables as $table)
  647. {
  648. $tableObj = substr($table, strpos($table, '_') + 1);
  649. if(!isset($this->lang->dev->tableList[$tableObj])) continue;
  650. $tableFields[$table] = $this->dev->getFields($table);
  651. }
  652. }
  653. return $tableFields;
  654. }
  655. /**
  656. * Get menu of all tables with their fields.
  657. *
  658. * @access public
  659. * @return array
  660. */
  661. public function getTableFieldsMenu()
  662. {
  663. $tableFields = $this->getTableFields();
  664. $menu = array();
  665. foreach($tableFields as $table => $fields)
  666. {
  667. $tableItem = array();
  668. $tableItem['key'] = $table;
  669. $tableItem['text'] = $table . '(table)';
  670. $tableItem['items'] = array();
  671. foreach($fields as $field => $fieldInfo)
  672. {
  673. $fieldItem = array();
  674. $fieldItem['key'] = $field;
  675. $fieldItem['text'] = $field . '(' . $fieldInfo['type'] . ')';
  676. $tableItem['items'][] = $fieldItem;
  677. }
  678. $menu[] = $tableItem;
  679. }
  680. return $menu;
  681. }
  682. /*
  683. * 准备内置的图表sql语句。
  684. * Prepare builtin chart sql.
  685. *
  686. * @access public
  687. * @return array
  688. */
  689. public function prepareBuiltinChartSQL($operate = 'insert')
  690. {
  691. $charts = $this->config->bi->builtin->charts;
  692. $chartSQLs = array();
  693. foreach($charts as $chart)
  694. {
  695. $currentOperate = $operate;
  696. $chart = (object)$chart;
  697. $chart->mode = 'text';
  698. if(isset($chart->settings)) $chart->settings = $this->jsonEncode($chart->settings);
  699. if(isset($chart->filters)) $chart->filters = $this->jsonEncode($chart->filters);
  700. if(isset($chart->fields)) $chart->fields = $this->jsonEncode($chart->fields);
  701. if(isset($chart->langs)) $chart->langs = $this->jsonEncode($chart->langs);
  702. if(!isset($chart->driver)) $chart->driver = $this->config->bi->defaultDriver;
  703. $exists = $this->dao->select('id')->from(TABLE_CHART)->where('id')->eq($chart->id)->fetch();
  704. if(!$exists) $currentOperate = 'insert';
  705. $stmt = null;
  706. if($currentOperate == 'insert')
  707. {
  708. $chart->createdBy = 'system';
  709. $chart->createdDate = helper::now();
  710. $chart->group = $this->getCorrectGroup($chart->group, $chart->type == 'table' ? 'pivot' : 'chart');
  711. $stmt = $this->dao->insert(TABLE_CHART)->data($chart);
  712. }
  713. if($currentOperate == 'update')
  714. {
  715. $id = $chart->id;
  716. unset($chart->group);
  717. unset($chart->id);
  718. $stmt = $this->dao->update(TABLE_CHART)->data($chart)->where('id')->eq($id);
  719. }
  720. if(isset($stmt)) $chartSQLs[] = $stmt->get();
  721. }
  722. return $chartSQLs;
  723. }
  724. /**
  725. * 准备内置的透视表sql语句。
  726. * Prepare builtin pivot sql.
  727. *
  728. * @param string $operate
  729. * @access public
  730. * @return array
  731. */
  732. public function prepareBuiltinPivotSQL($operate = 'insert')
  733. {
  734. $pivots = $this->config->bi->builtin->pivots;
  735. $isInstall = $operate == 'insert';
  736. $pivotSQLs = array();
  737. foreach($pivots as $pivot)
  738. {
  739. $pivot = (object)$pivot;
  740. $createdDate = $pivot->createdDate;
  741. $pivotExists = $this->dao->select('id,name')->from(TABLE_PIVOT)->where('id')->eq($pivot->id)->fetch();
  742. $pivotSpecExists = $this->dao->select('pivot,version')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($pivot->id)->andWhere('version')->eq($pivot->version)->fetch();
  743. list($pivot, $pivotSpec, $drills) = $this->preparePivotObject($pivot);
  744. if(!$pivotExists && $pivot->version == '1')
  745. {
  746. $pivot->createdBy = 'system';
  747. $pivot->group = $this->getCorrectGroup($pivot->group, 'pivot');
  748. /* 如果透视表不存在,就要通过安装或者升级来决定创建日期。*/
  749. /* 如果是安装模式,那么需要采用真实的创建时间,确保用户不会看到“新”标签。*/
  750. /* 如果是升级模式,那么需要采用当前时间,确保用户可以看到“新”标签。*/
  751. $pivot->createdDate = $isInstall ? $createdDate : helper::now();
  752. $pivotStmt = $this->dao->insert(TABLE_PIVOT)->data($pivot);
  753. $pivotSQLs[] = $pivotStmt->get();
  754. }
  755. if(!$pivotSpecExists)
  756. {
  757. /* 如果透视表版本不存在,就要通过安装或者升级来决定创建日期。*/
  758. /* 如果是安装模式,那么需要采用真实的创建时间,确保用户不会看到“新”标签。*/
  759. /* 如果是升级模式,那么需要采用当前时间,确保用户可以看到“新”标签。*/
  760. $pivotSpec->createdDate = $isInstall ? $createdDate : helper::now();
  761. $pivotSpecStmt = $this->dao->insert(TABLE_PIVOTSPEC)->data($pivotSpec);
  762. $pivotSQLs[] = $pivotSpecStmt->get();
  763. $pivotSQLs = array_merge($pivotSQLs, $this->prepareBuilitinPivotDrillSQL($pivot->id, $drills, $pivot->version));
  764. }
  765. }
  766. return $pivotSQLs;
  767. }
  768. /**
  769. * 准备透视表对象。
  770. * Prepare pivot object.
  771. *
  772. * @param object $pivot
  773. * @access public
  774. * @return array
  775. */
  776. public function preparePivotObject($pivot)
  777. {
  778. $pivot = (object)$pivot;
  779. $pivotSpec = new stdclass();
  780. $pivotSpec->version = $pivot->version;
  781. $pivotSpec->pivot = $pivot->id;
  782. $pivotSpec->mode = 'text';
  783. $pivotSpec->sql = $pivot->sql;
  784. $pivotSpec->name = $this->jsonEncode($pivot->name);
  785. if(isset($pivot->desc)) $pivotSpec->desc = $this->jsonEncode($pivot->desc);
  786. if(isset($pivot->settings)) $pivotSpec->settings = $this->jsonEncode($pivot->settings);
  787. if(isset($pivot->filters)) $pivotSpec->filters = $this->jsonEncode($pivot->filters);
  788. if(isset($pivot->fields)) $pivotSpec->fields = $this->jsonEncode($pivot->fields);
  789. if(isset($pivot->langs)) $pivotSpec->langs = $this->jsonEncode($pivot->langs);
  790. if(isset($pivot->vars)) $pivotSpec->vars = $this->jsonEncode($pivot->vars);
  791. if(!isset($pivot->driver)) $pivotSpec->driver = $this->config->bi->defaultDriver;
  792. if(!isset($pivot->settings)) $pivotSpec->settings = null;
  793. if(!isset($pivot->filters)) $pivotSpec->filters = null;
  794. if(!isset($pivot->fields)) $pivotSpec->fields = null;
  795. if(!isset($pivot->langs)) $pivotSpec->langs = null;
  796. if(!isset($pivot->vars)) $pivotSpec->vars = null;
  797. unset($pivot->driver);
  798. unset($pivot->name);
  799. unset($pivot->desc);
  800. unset($pivot->sql);
  801. unset($pivot->settings);
  802. unset($pivot->filters);
  803. unset($pivot->fields);
  804. unset($pivot->langs);
  805. unset($pivot->vars);
  806. $drills = zget($pivot, 'drills', array());
  807. unset($pivot->drills);
  808. return array($pivot, $pivotSpec, $drills);
  809. }
  810. /**
  811. * 准备内置透视表的下钻sql。
  812. * Prepare built-in pivot drill sql.
  813. *
  814. * @param int $pivotID
  815. * @param array $drills
  816. * @access public
  817. * @return array
  818. */
  819. public function prepareBuilitinPivotDrillSQL($pivotID, $drills, $version)
  820. {
  821. if(empty($drills)) return array();
  822. $sqls = array();
  823. $sqls[] = $this->dao->delete()->from(TABLE_PIVOTDRILL)->where('pivot')->eq($pivotID)->get();
  824. foreach($drills as $drill)
  825. {
  826. $drill = (object)$drill;
  827. $drill->condition = $this->jsonEncode($drill->condition);
  828. $drill->pivot = $pivotID;
  829. $drill->status = 'published';
  830. $drill->type = 'manual';
  831. $drill->version = $version;
  832. $sqls[] = $this->dao->insert(TABLE_PIVOTDRILL)->data($drill)->get();
  833. }
  834. return $sqls;
  835. }
  836. /**
  837. * 准备内置的度量项sql语句。
  838. * Prepare builtin metric sql.
  839. *
  840. * @param string $operate
  841. * @access public
  842. * @return array
  843. */
  844. public function prepareBuiltinMetricSQL($operate = 'insert')
  845. {
  846. $metrics = $this->config->bi->builtin->metrics;
  847. $metricSQLs = array();
  848. $this->dao->delete()->from(TABLE_METRIC)
  849. ->where('builtin')->eq('1')
  850. ->andWhere('code')->notIn(array_column($metrics, 'code'))
  851. ->andWhere('type')->eq('php')
  852. ->exec();
  853. foreach($metrics as $metric)
  854. {
  855. $currentOperate = $operate;
  856. $metric = (object)$metric;
  857. $metric->stage = 'released';
  858. $metric->type = 'php';
  859. $metric->builtin = '1';
  860. $exists = $this->dao->select('code')->from(TABLE_METRIC)->where('code')->eq($metric->code)->fetch();
  861. if(!$exists) $currentOperate = 'insert';
  862. $stmt = null;
  863. if($currentOperate == 'insert')
  864. {
  865. $metric->createdBy = 'system';
  866. $metric->createdDate = helper::now();
  867. $stmt = $this->dao->insert(TABLE_METRIC)->data($metric);
  868. }
  869. if($currentOperate == 'update')
  870. {
  871. $code = $metric->code;
  872. unset($metric->code);
  873. $stmt = $this->dao->update(TABLE_METRIC)->data($metric)->where('code')->eq($code);
  874. }
  875. if(isset($stmt)) $metricSQLs[] = $stmt->get();
  876. }
  877. return $metricSQLs;
  878. }
  879. /**
  880. * 准备内置的大屏sql语句。
  881. * Prepare builtin screen sql.
  882. *
  883. * @param string $operate
  884. * @access public
  885. * @return array
  886. */
  887. public function prepareBuiltinScreenSQL($operate = 'insert')
  888. {
  889. $screens = $this->config->bi->builtin->screens;
  890. $screenSQLs = array();
  891. foreach($screens as $screenID)
  892. {
  893. $currentOperate = $operate;
  894. $screenJson = file_get_contents(__DIR__ . DS . 'json' . DS . "screen{$screenID}.json");
  895. $screen = json_decode($screenJson);
  896. if(isset($screen->scheme)) $screen->scheme = json_encode($screen->scheme, JSON_UNESCAPED_UNICODE);
  897. $exists = $this->dao->select('id')->from(TABLE_SCREEN)->where('id')->eq($screenID)->fetch();
  898. if(!$exists) $currentOperate = 'insert';
  899. $screen->status = 'published';
  900. $stmt = null;
  901. if($currentOperate == 'insert')
  902. {
  903. $screen->createdBy = 'system';
  904. $screen->createdDate = helper::now();
  905. $stmt = $this->dao->insert(TABLE_SCREEN)->data($screen);
  906. }
  907. if($currentOperate == 'update')
  908. {
  909. $id = $screen->id;
  910. unset($screen->id);
  911. $stmt = $this->dao->update(TABLE_SCREEN)->data($screen)->where('id')->eq($id);
  912. }
  913. if(isset($stmt)) $screenSQLs[] = $stmt->get();
  914. }
  915. return $screenSQLs;
  916. }
  917. /*
  918. * 获取DuckDB的可执行文件路径。
  919. * Get DcukDB path.
  920. *
  921. * @access public
  922. * @return object|false
  923. */
  924. public function getDuckDBPath()
  925. {
  926. $duckdbBin = $this->getDuckdbBinConfig();
  927. $sourcePath = $this->app->getTmpRoot() . 'duckdb' . DS;
  928. $checkSourceCode = $this->checkDuckDBFile($sourcePath, $duckdbBin);
  929. if($checkSourceCode !== false) return $checkSourceCode;
  930. return $this->checkDuckDBFile($duckdbBin['path'], $duckdbBin);
  931. }
  932. /**
  933. * 检查duckDB引擎文件是否存在。
  934. * Check duckDB bin file exists or not.
  935. *
  936. * @param string $path
  937. * @param array $bin
  938. * @access public
  939. * @return false|object
  940. */
  941. public function checkDuckDBFile($path, $bin)
  942. {
  943. $file = $path . $bin['file'];
  944. $extension = $path . $bin['extension'];
  945. if(!file_exists($file) && !file_exists($extension) && !is_executable($file)) return false;
  946. return (object)array('bin' => $file, 'extension' => $extension);
  947. }
  948. /**
  949. * 获取ducbDB的bin目录配置。
  950. * Get duckdb bin config.
  951. *
  952. * @param string $driver
  953. * @access public
  954. * @return array
  955. */
  956. public function getDuckdbBinConfig()
  957. {
  958. $os = PHP_OS == 'WINNT' ? 'win' : 'linux';
  959. $duckdbBin = $this->config->bi->duckdbBin[$os];
  960. $driver = $this->config->db->driver;
  961. /* 如果不是mysql数据库,那么统一使用达梦的扩展配置。*/
  962. /* If it is not a mysql database, then use the same extension configuration of Dameng. */
  963. if($driver == 'oceanbase') $driver = 'mysql';
  964. if($driver !== 'mysql') $driver = 'dm';
  965. $duckdbBin['extension'] = $this->config->bi->duckdbExt[$os][$driver];
  966. $duckdbBin['extension_dm'] = $this->config->bi->duckdbExt[$os]['dm'];
  967. $duckdbBin['extension_mysql'] = $this->config->bi->duckdbExt[$os]['mysql'];
  968. $duckdbBin['extensionUrl_dm'] = $this->config->bi->duckdbExtUrl[$os]['dm'];
  969. $duckdbBin['extensionUrl_mysql'] = $this->config->bi->duckdbExtUrl[$os]['mysql'];
  970. if($os == 'win') $duckdbBin['path'] = dirname(dirname($this->app->getBasePath())) . $duckdbBin['path'];
  971. return $duckdbBin;
  972. }
  973. /**
  974. * 获取DuckDB临时目录。
  975. * Get DuckDB temp directory.
  976. *
  977. * @access public
  978. * @return string|false
  979. */
  980. public function getDuckDBTmpDir($static = false)
  981. {
  982. $duckdbTmpPath = $this->app->getTmpRoot() . 'duckdb' . DS . 'bi' . DS;
  983. if($static) return $duckdbTmpPath;
  984. if(!is_dir($duckdbTmpPath) && !mkdir($duckdbTmpPath, 0755, true)) return false;
  985. return $duckdbTmpPath;
  986. }
  987. /**
  988. * Get sql by month.
  989. *
  990. * @param string $month
  991. * @access public
  992. * @return array
  993. */
  994. public function getSqlByMonth($year = 'Y', $month = 'm')
  995. {
  996. $sqls = array();
  997. $prefix = $this->config->db->prefix;
  998. $year = date($year);
  999. $month = date($month);
  1000. $begin = date("{$year}-{$month}-01 00:00:00");
  1001. $end = date("{$year}-{$month}-t 23:59:59", strtotime("$year-$month-01"));
  1002. $sqls[$prefix . "action_{$year}_{$month}"] = "select * from zt_action where date >= TIMESTAMP '$begin' and date <= TIMESTAMP '$end'";
  1003. return $sqls;
  1004. }
  1005. /**
  1006. * Get action sync sql.
  1007. *
  1008. * @param string $range
  1009. * @access public
  1010. * @return array
  1011. */
  1012. public function getActionSyncSql($range = 'current')
  1013. {
  1014. if($range == 'current') return $this->getSqlByMonth();
  1015. $actionDate = $this->biTao->fetchActionDate();
  1016. $begin = new DateTime($actionDate->minDate);
  1017. $end = new DateTime($actionDate->maxDate);
  1018. $sqls = array();
  1019. while($begin <= $end)
  1020. {
  1021. $year = $begin->format('Y');
  1022. $month = $begin->format('m');
  1023. $sqls += $this->getSqlByMonth($year, $month);
  1024. $begin->modify('+1 month');
  1025. }
  1026. return $sqls;
  1027. }
  1028. /**
  1029. * Init parquet.
  1030. *
  1031. * @access public
  1032. * @return void
  1033. */
  1034. public function initParquet()
  1035. {
  1036. $duckdb = $this->getDuckDBPath();
  1037. if(!$duckdb) return $this->lang->bi->binNotExists;
  1038. $duckdbTmpPath = $this->getDuckDBTmpDir();
  1039. if(!$duckdbTmpPath) return sprintf($this->lang->bi->tmpPermissionDenied, $this->getDuckDBTmpDir(true), $this->getDuckDBTmpDir(true));
  1040. $tables = $this->biTao->fetchAllTables();
  1041. $copySQLs = array();
  1042. foreach($tables as $table) $copySQLs[] = "copy (select * from {$table}) to '{$duckdbTmpPath}{$table}.parquet'";
  1043. $date = date("Y-m-01 00:00:00");
  1044. $prefix = $this->config->db->prefix;
  1045. $copySQLs[] = "copy (select * from {$prefix}action where date < TIMESTAMP '$date') to '{$duckdbTmpPath}{$prefix}action_" . date('Y_m', strtotime('-1 month')) . ".parquet'";
  1046. $copySQL = implode(';', $copySQLs);
  1047. if(empty($copySQL)) return true;
  1048. $command = $this->prepareSyncCommand($duckdb->bin, $duckdb->extension, $copySQL);
  1049. $output = shell_exec($command);
  1050. $this->saveLogs("Sync command: $command");
  1051. return $output;
  1052. }
  1053. /**
  1054. * 准备同步数据库所需的复制SQL。
  1055. * Prepare copy SQL for sync.
  1056. *
  1057. * @param string $duckdbTmpPath
  1058. * @access public
  1059. * @return string
  1060. */
  1061. public function prepareCopySQL($duckdbTmpPath)
  1062. {
  1063. $tables = $this->biTao->fetchTableQueue();
  1064. if(empty($tables)) return '';
  1065. $copySQLs = array();
  1066. foreach($tables as $table) $copySQLs[] = "copy (select * from {$table}) to '{$duckdbTmpPath}{$table}.parquet'";
  1067. $actions = $this->getActionSyncSql();
  1068. foreach($actions as $table => $sql) $copySQLs[] = "copy ({$sql}) to '{$duckdbTmpPath}{$table}.parquet'";
  1069. $this->biTao->updateSyncTime($tables);
  1070. return implode(';', $copySQLs);
  1071. }
  1072. /**
  1073. * 准备同步命令。
  1074. * Prepare sync command.
  1075. *
  1076. * @param string $binPath
  1077. * @param string $extensionPath
  1078. * @param string $copySQL
  1079. * @access public
  1080. * @return string
  1081. */
  1082. public function prepareSyncCommand($binPath, $extensionPath, $copySQL)
  1083. {
  1084. $dbConfig = $this->config->db;
  1085. $driver = $dbConfig->driver;
  1086. $sqlContent = $this->config->bi->duckSQLTemp[$driver];
  1087. $variables = array(
  1088. '{EXTENSIONPATH}' => $extensionPath,
  1089. '{DRIVER}' => $driver,
  1090. '{DATABASE}' => $dbConfig->name,
  1091. '{USER}' => $dbConfig->user,
  1092. '{PASSWORD}' => $dbConfig->password,
  1093. '{HOST}' => $dbConfig->host,
  1094. '{PORT}' => $dbConfig->port,
  1095. '{COPYSQL}' => $copySQL
  1096. );
  1097. foreach($variables as $key => $value)
  1098. {
  1099. $sqlContent = str_replace($key, $value, $sqlContent);
  1100. }
  1101. if($driver == 'mysql') return "$binPath :memory: \"$sqlContent\" 2>&1";
  1102. return "$sqlContent 2>&1";
  1103. }
  1104. /**
  1105. * Generate parquet file.
  1106. *
  1107. * @access public
  1108. * @return string|true
  1109. */
  1110. public function generateParquetFile()
  1111. {
  1112. $duckdb = $this->getDuckDBPath();
  1113. if(!$duckdb) return $this->lang->bi->binNotExists;
  1114. $duckdbTmpPath = $this->getDuckDBTmpDir();
  1115. if(!$duckdbTmpPath) return sprintf($this->lang->bi->tmpPermissionDenied, $this->getDuckDBTmpDir(true), $this->getDuckDBTmpDir(true));
  1116. $copySQL = $this->prepareCopySQL($duckdbTmpPath);
  1117. if(empty($copySQL)) return true;
  1118. $command = $this->prepareSyncCommand($duckdb->bin, $duckdb->extension, $copySQL);
  1119. $output = shell_exec($command);
  1120. $this->saveLogs("Sync command: $command");
  1121. if(!empty($output)) return $output;
  1122. return true;
  1123. }
  1124. /**
  1125. * 获取日志文件路径。
  1126. * Get log file.
  1127. *
  1128. * @access public
  1129. * @return string
  1130. */
  1131. public function getLogFile()
  1132. {
  1133. return $this->app->getTmpRoot() . 'log/syncparquet.' . date('Ymd') . '.log.php';
  1134. }
  1135. /**
  1136. * 存储日志。
  1137. * Save logs.
  1138. *
  1139. * @param string $log
  1140. * @access public
  1141. * @return void
  1142. */
  1143. public function saveLogs($log)
  1144. {
  1145. $logFile = $this->getLogFile();
  1146. $log = date('Y-m-d H:i:s') . ' ' . trim($log) . "\n";
  1147. if(!file_exists($logFile)) $log = "<?php\ndie();\n?" . ">\n" . $log;
  1148. file_put_contents($logFile, $log, FILE_APPEND);
  1149. }
  1150. /**
  1151. * 在sql中将变量解析为空字符串。
  1152. * Parse variables to null string in sql.
  1153. *
  1154. * @param string $sql
  1155. * @param array $filters
  1156. * @access public
  1157. * @return string
  1158. */
  1159. public function parseSqlVars($sql, $filters)
  1160. {
  1161. if($filters)
  1162. {
  1163. foreach($filters as $filter)
  1164. {
  1165. if(!isset($filter['default']) || !isset($filter['from']) || $filter['from'] !== 'query') continue;
  1166. $default = $filter['default'];
  1167. if($filter['type'] == 'multipleselect' && is_array($default)) $default = implode("','", $default);
  1168. if(strpos($sql, $filter['field'] . 'Condition') === false)
  1169. {
  1170. $sql = str_replace('$' . $filter['field'], "'{$default}'", $sql);
  1171. }
  1172. else
  1173. {
  1174. $relatedField = $filter['relatedField'];
  1175. $sql = str_replace('$' . $filter['field'] . 'Condition', "{$relatedField}='{$default}'", $sql);
  1176. }
  1177. }
  1178. }
  1179. $matchRule = "[\$]+[a-zA-Z0-9]+_[0-9]";
  1180. if(strpos($sql, 'Condition') !== false && strpos($sql, 'Variale_') !== false) $matchRule .= "+Condition";
  1181. if(preg_match_all("/{$matchRule}/", $sql, $out))
  1182. {
  1183. foreach($out[0] as $match) $sql = str_replace($match, "''", $sql);
  1184. }
  1185. return $sql;
  1186. }
  1187. /**
  1188. * Process filter variables in sql.
  1189. *
  1190. * @param string $sql
  1191. * @param array $filters
  1192. * @access public
  1193. * @return string
  1194. */
  1195. public function processVars($sql, $filters = array(), $emptyValue = false)
  1196. {
  1197. foreach($filters as $index => $filter)
  1198. {
  1199. if(empty($filter['default'])) continue;
  1200. if(!isset($filter['from']) || $filter['from'] != 'query') continue;
  1201. if($filter['type'] == 'date' || $filter['type'] == 'datetime') $filters[$index]['default'] = $this->loadModel('pivot')->processDateVar($filter['default']);
  1202. if($filter['type'] == 'datetime') $filters[$index]['default'] .= ':00.000000000';
  1203. if($filter['type'] == 'multipleselect' && is_array($filter['default'])) $filters[$index]['default'] = implode("','", $filter['default']);
  1204. if($emptyValue) $filters[$index]['default'] = $filter['type'] == 'date' || $filter['type'] == 'datetime' ? '1970-01-01' : '';
  1205. }
  1206. $sql = $this->parseSqlVars($sql, $filters);
  1207. $sql = trim($sql, ';');
  1208. return $sql;
  1209. }
  1210. /**
  1211. * Build statement object from sql.
  1212. *
  1213. * @param string $sql
  1214. * @access public
  1215. * @return object
  1216. */
  1217. public function sql2Statement($sql, $mode = 'text')
  1218. {
  1219. $this->app->loadClass('sqlparser', true);
  1220. $parser = new sqlparser($sql);
  1221. if(!isset($this->lang->dataview)) $this->app->loadLang('dataview');
  1222. if($parser->statementsCount == 0)
  1223. {
  1224. if($mode == 'builder') return $this->lang->dataview->emptyBuilder;
  1225. return $this->lang->dataview->empty;
  1226. }
  1227. if($parser->statementsCount > 1) return $this->lang->dataview->onlyOne;
  1228. if(!$parser->isSelect) return $this->lang->dataview->allowSelect;
  1229. return $parser->statement;
  1230. }
  1231. /**
  1232. * Parse sql.
  1233. *
  1234. * @param string $sql
  1235. * @access public
  1236. * @return array
  1237. */
  1238. public function parseSql($sql)
  1239. {
  1240. $this->app->loadClass('sqlparser', true);
  1241. $parser = new sqlparser($sql);
  1242. $parser->setDAO($this->dao);
  1243. $parser->parseStatement();
  1244. return $parser->matchColumnsWithTable();
  1245. }
  1246. /**
  1247. * Get expression.
  1248. *
  1249. * @param mixed $table
  1250. * @param mixed $column
  1251. * @param mixed $alias
  1252. * @param mixed $function
  1253. * @access public
  1254. * @return object
  1255. */
  1256. public function getExpression($table = null, $column = null, $alias = null, $function = null)
  1257. {
  1258. $this->app->loadClass('sqlparser', true);
  1259. $parser = new sqlparser(null);
  1260. return $parser->getExpression($table, $column, $alias, $function);
  1261. }
  1262. /**
  1263. * Get condition.
  1264. *
  1265. * @param mixed $tableA
  1266. * @param mixed $columnA
  1267. * @param string $operator
  1268. * @param mixed $tableB
  1269. * @param mixed $columnB
  1270. * @param int $group
  1271. * @access public
  1272. * @return object
  1273. * @param bool $quote
  1274. */
  1275. public function getCondition($tableA = null, $columnA = null, $operator = '', $tableB = null, $columnB = null, $group = 1, $quote = true)
  1276. {
  1277. $this->app->loadClass('sqlparser', true);
  1278. $parser = new sqlparser(null);
  1279. return $parser->getCondition($tableA, $columnA, $operator, $tableB, $columnB, $group, $quote);
  1280. }
  1281. /**
  1282. * buildSQL
  1283. *
  1284. * @param array $selects
  1285. * @param array $from
  1286. * @param array $joins
  1287. * @param array $functions
  1288. * @param array $wheres
  1289. * @param array $querys
  1290. * @param array $groups
  1291. * @access public
  1292. * @return object
  1293. */
  1294. public function buildSQL($selects, $from, $joins = array(), $functions = array(), $wheres = array(), $querys = array(), $groups = array())
  1295. {
  1296. $this->app->loadClass('sqlparser', true);
  1297. $parser = new sqlparser(null);
  1298. $parser->createStatement();
  1299. foreach($selects as $select) $parser->addSelect($parser->getExpression($select));
  1300. if(!empty($functions)) foreach($functions as $function) $parser->addSelect($parser->getExpression($function));
  1301. $parser->setFrom($parser->getExpression($from));
  1302. foreach($joins as $join)
  1303. {
  1304. list($table, $alias, $ons) = $join;
  1305. $onExprs = $parser->getConditionsFromArray($ons);
  1306. $leftJoinExpr = $parser->getLeftJoin($table, $alias, $onExprs);
  1307. $parser->addJoin($leftJoinExpr);
  1308. }
  1309. if(!empty($wheres)) $parser->addWhere($parser->combineConditions($parser->getConditionsFromArray($wheres)));
  1310. if(!empty($querys)) $parser->addWhere($parser->combineConditions($parser->getConditionsFromArray($querys)));
  1311. if(!empty($groups)) foreach($groups as $group) $parser->addGroup($parser->getGroup($parser->getExpression($group)));
  1312. return $parser->statement;
  1313. }
  1314. /**
  1315. * Validate sql.
  1316. *
  1317. * @param string $sql
  1318. * @access public
  1319. * @return string|true
  1320. */
  1321. public function validateSql($sql, $driver = 'mysql')
  1322. {
  1323. $this->loadModel('dataview');
  1324. if(empty($sql)) return $this->lang->dataview->empty;
  1325. $result = $this->explainSQL($sql, $driver);
  1326. if($result['result'] === 'fail') return $result['message'];
  1327. $sqlColumns = $this->getColumns($sql, $driver);
  1328. list($isUnique, $repeatColumn) = $this->dataview->checkUniColumn($sql, $driver, true, $sqlColumns);
  1329. if(!$isUnique) return sprintf($this->lang->dataview->duplicateField, implode(',', $repeatColumn));
  1330. return true;
  1331. }
  1332. /**
  1333. * Prepare pager from sql.
  1334. *
  1335. * @param object $statement
  1336. * @param int $recPerPage
  1337. * @param int $pageID
  1338. * @access public
  1339. * @return string
  1340. */
  1341. public function prepareSqlPager($statement, $recPerPage, $pageID, $driver)
  1342. {
  1343. if(!$statement->limit)
  1344. {
  1345. $statement->limit = new stdclass();
  1346. }
  1347. $statement->limit->offset = $recPerPage * ($pageID - 1);
  1348. $statement->limit->rowCount = $recPerPage;
  1349. if($driver == 'mysql' && $this->config->db->driver == 'mysql')
  1350. {
  1351. $statement->options->options[] = 'SQL_CALC_FOUND_ROWS';
  1352. }
  1353. $limitSql = $statement->build();
  1354. return $limitSql;
  1355. }
  1356. /**
  1357. * Prepare columns setting from sql.
  1358. *
  1359. * @param string $sql
  1360. * @param object $statement
  1361. * @access public
  1362. * @return array
  1363. */
  1364. public function prepareColumns($sql, $statement, $driver)
  1365. {
  1366. list($columnTypes, $columnFields) = $this->getSqlTypeAndFields($sql, $driver);
  1367. $rebuildParams = $this->getParams4Rebuild($sql, $statement, $columnFields);
  1368. $fieldPairs = $rebuildParams[2];
  1369. $relatedObjects = $rebuildParams[3];
  1370. $columns = array();
  1371. $clientLang = $this->app->getClientLang();
  1372. foreach($fieldPairs as $field => $langName)
  1373. {
  1374. $columns[$field] = array('name' => $field, 'field' => $field, 'type' => $columnTypes->$field, 'object' => $relatedObjects[$field], $clientLang => $langName);
  1375. }
  1376. $objectFields = $this->loadModel('dataview')->getObjectFields();
  1377. $columns = $this->rebuildFieldSettings($fieldPairs, $columnTypes, $relatedObjects, $columns, $objectFields);
  1378. return array($columns, $relatedObjects);
  1379. }
  1380. /**
  1381. * Get sql columnTypes and columnFields.
  1382. *
  1383. * @param string $sql
  1384. * @param string $driver
  1385. * @access public
  1386. * @return array
  1387. */
  1388. public function getSqlTypeAndFields($sql, $driver)
  1389. {
  1390. $sqlColumns = $this->getColumns($sql, $driver);
  1391. $columnTypes = $this->getColumnsType($sql, $driver, $sqlColumns);
  1392. $columnFields = array();
  1393. foreach($columnTypes as $column => $type) $columnFields[$column] = $column;
  1394. return array($columnTypes, $columnFields);
  1395. }
  1396. /**
  1397. * Get params for rebuild fieldSetting.
  1398. *
  1399. * @param string $sql
  1400. * @param object $statement
  1401. * @param array $columnFields
  1402. * @access public
  1403. * @return array
  1404. */
  1405. public function getParams4Rebuild($sql, $statement, $columnFields)
  1406. {
  1407. $tableAndFields = $this->getTableAndFields($sql);
  1408. $tables = $tableAndFields['tables'];
  1409. $fields = $tableAndFields['fields'];
  1410. $moduleNames = array();
  1411. $aliasNames = array();
  1412. $this->loadModel('dataview');
  1413. if($tables)
  1414. {
  1415. $moduleNames = $this->dataview->getModuleNames($tables);
  1416. $aliasNames = $this->dataview->getAliasNames($statement, $moduleNames);
  1417. }
  1418. list($fieldPairs, $relatedObjects) = $this->dataview->mergeFields($columnFields, $fields, !empty($moduleNames) ? $moduleNames : $tables, $aliasNames);
  1419. foreach($fieldPairs as $field => $name)
  1420. {
  1421. // 只保留数字、中文、英文和下划线的字符
  1422. $fieldPairs[$field] = preg_replace('/[^\x{4e00}-\x{9fa5}0-9a-zA-Z_]/u', '', $name);
  1423. }
  1424. return array($moduleNames, $aliasNames, $fieldPairs, $relatedObjects);
  1425. }
  1426. /**
  1427. * Get SQL.
  1428. *
  1429. * @param string $sql
  1430. * @param string $driver
  1431. * @param int $recPerPage
  1432. * @param int $pageID
  1433. * @access public
  1434. * @return array
  1435. */
  1436. public function getSQL($sql, $driver = 'mysql', $recPerPage = 10, $pageID = 1)
  1437. {
  1438. $statement = $this->sql2Statement($sql);
  1439. $limitSql = $this->prepareSqlPager($statement, $recPerPage, $pageID, $driver);
  1440. if($driver == 'mysql' && $this->config->db->driver == 'mysql')
  1441. {
  1442. $countSql = "SELECT FOUND_ROWS() AS count";
  1443. }
  1444. else
  1445. {
  1446. $countSql = "SELECT COUNT(1) AS count FROM ($sql)";
  1447. }
  1448. return array($limitSql, $countSql);
  1449. }
  1450. /**
  1451. * Query sql.
  1452. *
  1453. * @param string $sql
  1454. * @param string $limitSql
  1455. * @param string $driver mysql|duckdb
  1456. * @access public
  1457. * @return array
  1458. */
  1459. public function querySQL($sql, $limitSql, $driver = 'mysql')
  1460. {
  1461. $dbh = $this->app->loadDriver($driver);
  1462. try
  1463. {
  1464. $stmt = $dbh->query($limitSql);
  1465. if($stmt === false) return array('result' => 'fail', 'message' => 'Sql error.');
  1466. $rows = $stmt->fetchAll();
  1467. $countSql = $this->getSQL($sql, $driver)[1];
  1468. $count = $dbh->query($countSql)->fetch();
  1469. $rowsCount = $count->count;
  1470. }
  1471. catch(Exception $e)
  1472. {
  1473. $message = preg_replace("/\r|\n|\t/", "", $e->getMessage());
  1474. $message = strip_tags($message);
  1475. return array('result' => 'fail', 'message' => $message);
  1476. }
  1477. return array('result' => 'success', 'rows' => $rows, 'rowsCount' => $rowsCount);
  1478. }
  1479. /**
  1480. * Query sql.
  1481. *
  1482. * @param string $sql
  1483. * @param int $recPerPage
  1484. * @param int $pageID
  1485. * @access public
  1486. * @return object
  1487. */
  1488. public function query($stateObj, $driver = 'mysql', $useFilter = true)
  1489. {
  1490. $dbh = $this->app->loadDriver($driver);
  1491. $sql = $stateObj->sql;
  1492. if($useFilter) $sql = $this->processVars($sql, $stateObj->getFilters(), true);
  1493. $stateObj->beforeQuerySql();
  1494. $statement = $this->sql2Statement($sql, $stateObj->mode);
  1495. if(is_string($statement)) return $stateObj->setError($statement);
  1496. $checked = $this->validateSql($sql, $driver);
  1497. if($checked !== true) return $stateObj->setError($checked);
  1498. if($useFilter) $sql = $this->processVars($stateObj->sql, $stateObj->getFilters());
  1499. $recPerPage = $stateObj->pager['recPerPage'];
  1500. $pageID = $stateObj->pager['pageID'];
  1501. list($limitSql, $countSql) = $this->getSQL($sql, $this->config->db->driver, $recPerPage, $pageID);
  1502. if($this->config->db->driver)
  1503. {
  1504. $limitSql = $dbh->formatSQL($limitSql);
  1505. $countSql = $dbh->formatSQL($countSql);
  1506. }
  1507. try
  1508. {
  1509. $stateObj->queryData = $dbh->query($limitSql)->fetchAll();
  1510. $total = $dbh->query($countSql)->fetch();
  1511. $total = isset($total->count) ? $total->count : 0;
  1512. list($columns, $relatedObject) = $this->prepareColumns($limitSql, $statement, $driver);
  1513. $stateObj->setPager($total, $recPerPage, $pageID);
  1514. $stateObj->setFieldSettings($columns);
  1515. $stateObj->setFieldRelatedObject($relatedObject);
  1516. $stateObj->buildQuerySqlCols();
  1517. }
  1518. catch(Exception $e)
  1519. {
  1520. return $stateObj->setError($e);
  1521. }
  1522. if(!$stateObj->isError())
  1523. {
  1524. list($isUnique, $repeatColumn) = $this->loadModel('dataview')->checkUniColumn($sql, $driver, true);
  1525. if(!$isUnique) $stateObj->setError(sprintf($this->lang->dataview->duplicateField, implode(',', $repeatColumn)));
  1526. }
  1527. return $stateObj;
  1528. }
  1529. /**
  1530. * Get table list.
  1531. *
  1532. * @param bool $hasDataview
  1533. * @param bool $withPrefix
  1534. * @access public
  1535. * @return array
  1536. */
  1537. public function getTableList($hasDataview = true, $withPrefix = true)
  1538. {
  1539. $originTableTreeMenu = $this->loadModel('dataview')->getOriginTreeMenu();
  1540. $dataviewTreeMenu = $this->loadModel('tree')->getGroupTree(0, 'dataview');
  1541. $originTablePrefix = $withPrefix ? $this->config->db->prefix : '';
  1542. $tableList = array();
  1543. foreach($originTableTreeMenu as $menu)
  1544. {
  1545. if(empty($menu->items)) continue;
  1546. foreach($menu->items as $item)
  1547. {
  1548. if(!is_array($item))
  1549. {
  1550. $text = $item->key == 'story' ? $this->lang->story->common : $item->text;
  1551. $tableList[$originTablePrefix . $item->key] = $text;
  1552. continue;
  1553. }
  1554. foreach($item->items as $subItem) $tableList[$originTablePrefix . $subItem->key] = $subItem->text;
  1555. }
  1556. }
  1557. if(!$hasDataview) return $tableList;
  1558. $dataviewPrefix = $withPrefix ? 'ztv_' : '';
  1559. foreach($dataviewTreeMenu as $menu)
  1560. {
  1561. if(empty($menu->items)) continue;
  1562. foreach($menu->items as $item)
  1563. {
  1564. if(!is_array($item))
  1565. {
  1566. $tableList[$dataviewPrefix . $item->key] = $item->text;
  1567. continue;
  1568. }
  1569. foreach($item->items as $subItem) $tableList[$dataviewPrefix . $subItem->key] = $subItem->text;
  1570. }
  1571. }
  1572. return $tableList;
  1573. }
  1574. /**
  1575. * Build table columns from query result.
  1576. *
  1577. * @param array $fieldSettings
  1578. * @access public
  1579. * @return array
  1580. */
  1581. public function buildQueryResultTableColumns($fieldSettings)
  1582. {
  1583. $cols = array();
  1584. $clientLang = $this->app->getClientLang();
  1585. foreach($fieldSettings as $field => $settings)
  1586. {
  1587. $settings = (array)$settings;
  1588. $title = isset($settings[$clientLang]) ? $settings[$clientLang] : $field;
  1589. $type = $settings['type'];
  1590. $cols[] = array('name' => $field, 'title' => $title, 'sortType' => false);
  1591. }
  1592. return $cols;
  1593. }
  1594. /**
  1595. * Prepare field objects.
  1596. *
  1597. * @access public
  1598. * @return array
  1599. */
  1600. public function prepareFieldObjects()
  1601. {
  1602. $this->loadModel('dataview');
  1603. $options = array();
  1604. foreach($this->lang->dataview->objects as $table => $name)
  1605. {
  1606. $fields = $this->dataview->getTypeOptions($table);
  1607. $options[] = array('text' => $name, 'value' => $table, 'fields' => $fields);
  1608. }
  1609. return $options;
  1610. }
  1611. /**
  1612. * Prepare field setting form data.
  1613. *
  1614. * @param object $settings
  1615. * @access public
  1616. * @return array
  1617. */
  1618. public function prepareFieldSettingFormData($settings)
  1619. {
  1620. $formData = array();
  1621. foreach((array)$settings as $key => $setting)
  1622. {
  1623. $setting = (array)$setting;
  1624. $setting['key'] = $key;
  1625. $formData[] = $setting;
  1626. }
  1627. return $formData;
  1628. }
  1629. /**
  1630. * 重建透视表filedSettings字段
  1631. * Rebuild fieldSettings field of pivot.
  1632. *
  1633. * @param object $pivot
  1634. * @param array $fieldPairs
  1635. * @param object $columns
  1636. * @param array $relatedObject
  1637. * @param object $fieldSettings
  1638. * @param array $objectFields
  1639. * @access private
  1640. * @return object
  1641. */
  1642. public function rebuildFieldSettings($fieldPairs, $columns, $relatedObject, $fieldSettings, $objectFields)
  1643. {
  1644. $isArray = is_array($fieldSettings);
  1645. if($isArray) $fieldSettings = json_decode(json_encode($fieldSettings));
  1646. $fieldSettingsNew = new stdclass();
  1647. foreach($fieldPairs as $index => $field)
  1648. {
  1649. $defaultType = $columns->{$index};
  1650. $defaultObject = $relatedObject[$index];
  1651. if(isset($objectFields[$defaultObject][$index])) $defaultType = $objectFields[$defaultObject][$index]['type'] == 'object' ? 'string' : $objectFields[$defaultObject][$index]['type'];
  1652. if(!isset($fieldSettings->{$index}))
  1653. {
  1654. /* 如果字段设置中没有该字段,则使用默认的配置。 */
  1655. /* If the field is not set in the field settings, use the default value. */
  1656. $fieldItem = new stdclass();
  1657. $fieldItem->name = $field;
  1658. $fieldItem->object = $defaultObject;
  1659. $fieldItem->field = $index;
  1660. $fieldItem->type = $defaultType;
  1661. $fieldSettingsNew->{$index} = $fieldItem;
  1662. }
  1663. else
  1664. {
  1665. /* 兼容旧版本的字段设置,当为空或者为布尔值时,使用默认值 */
  1666. /* Compatible with old version of field settings, use default value when empty or boolean. */
  1667. if(!isset($fieldSettings->{$index}->object) || is_bool($fieldSettings->{$index}->object) || strlen($fieldSettings->{$index}->object) == 0) $fieldSettings->{$index}->object = $defaultObject;
  1668. /* 当字段设置中没有字段名时,使用默认的字段名配置。 */
  1669. /* When there is no field name in the field settings, use the default field name configuration. */
  1670. if(!isset($fieldSettings->{$index}->field) || strlen($fieldSettings->{$index}->field) == 0)
  1671. {
  1672. $fieldSettings->{$index}->field = $index;
  1673. $fieldSettings->{$index}->object = $defaultObject;
  1674. $fieldSettings->{$index}->type = 'string';
  1675. }
  1676. $object = $fieldSettings->{$index}->object;
  1677. $type = $fieldSettings->{$index}->type;
  1678. if($object == $defaultObject && $type != $defaultType) $fieldSettings->{$index}->type = $defaultType;
  1679. $fieldSettingsNew->{$index} = $fieldSettings->{$index};
  1680. }
  1681. }
  1682. if($isArray) $fieldSettingsNew = json_decode(json_encode($fieldSettingsNew), true);
  1683. return $fieldSettingsNew;
  1684. }
  1685. /**
  1686. * 把自定义透视表的数据转换为数据表格可以使用的格式。
  1687. * Convert the data of custom pivot to the format that can be used by data table.
  1688. *
  1689. * @param object $data
  1690. * @param array $configs
  1691. * @access public
  1692. * @return array
  1693. * @param string $version
  1694. * @param string $status
  1695. */
  1696. public function convertDataForDtable($data, $configs, $version, $status)
  1697. {
  1698. $columns = array();
  1699. $rows = array();
  1700. $cellSpan = array();
  1701. $columnMaxLen = array();
  1702. $headerRow1 = !empty($data->cols[0]) ? $data->cols[0] : array();
  1703. $headerRow2 = !empty($data->cols[1]) ? $data->cols[1] : array();
  1704. /* 定义数据表格的列配置。*/
  1705. /* Define the column configuration of the data table. */
  1706. $index = 0;
  1707. foreach($headerRow1 as $column)
  1708. {
  1709. /* 如果 colspan 属性不为空则且isSlice标记列为true且存在第二行表头表示该列包含切片字段。*/
  1710. /* If the colspan attribute is not empty, it means that the column contains slice fields. */
  1711. if(!empty($column->colspan) && $column->isSlice && !empty($headerRow2))
  1712. {
  1713. /* 找到实际切片的字段。*/
  1714. /* Find the actual sliced field. */
  1715. $colspan = 0;
  1716. while($colspan < $column->colspan)
  1717. {
  1718. $subColumn = array_shift($headerRow2);
  1719. $field = 'field' . $index;
  1720. $columns[$field]['name'] = $field;
  1721. $columns[$field]['title'] = empty($subColumn->label) ? ' ' : $subColumn->label;
  1722. $columns[$field]['width'] = 16 * mb_strlen($subColumn->label);
  1723. $columns[$field]['minWidth'] = 128;
  1724. $columns[$field]['align'] = 'center';
  1725. if(isset($subColumn->isDrilling) && $subColumn->isDrilling)
  1726. {
  1727. $columns[$field]['link'] = '#';
  1728. $columns[$field]['drillField'] = $subColumn->drillField;
  1729. $columns[$field]['condition'] = $subColumn->condition;
  1730. }
  1731. $columnMaxLen[$field] = mb_strlen($column->label);
  1732. /* 把被切片的字段名设置为数据表格的列配置的 headerGroup 属性。*/
  1733. /* Set the sliced field name as the headerGroup attribute of the column configuration of the data table. */
  1734. $columns[$field]['headerGroup'] = $column->label;
  1735. /* 数据表格不支持表头第二行合并单元格,如果有这种情况把被合并的所有列视为一列,记录 colspan 属性并跳过其它列。*/
  1736. /* The data table does not support merging cells in the second row of the header. If this is the case, all the merged columns are regarded as one column, the colspan attribute is recorded and other columns are skipped. */
  1737. if(!empty($subColumn->colspan) && $subColumn->colspan > 1) $columns[$field]['colspan'] = $subColumn->colspan;
  1738. $colspan += $subColumn->colspan ?: 1;
  1739. $index++;
  1740. }
  1741. continue;
  1742. }
  1743. $field = 'field' . $index;
  1744. $columns[$field]['name'] = $field;
  1745. $columns[$field]['title'] = empty($column->label) ? ' ' : $column->label;
  1746. $columns[$field]['width'] = 16 * mb_strlen($column->label);
  1747. $columns[$field]['minWidth'] = 128;
  1748. $columns[$field]['align'] = 'center';
  1749. if(isset($column->isDrilling) && $column->isDrilling)
  1750. {
  1751. $columns[$field]['link'] = '#';
  1752. $columns[$field]['drillField'] = $column->drillField;
  1753. $columns[$field]['condition'] = $column->condition;
  1754. }
  1755. $columnMaxLen[$field] = mb_strlen($column->label);
  1756. if(isset($column->colspan) && $column->colspan > 1) $columns[$field]['colspan'] = $column->colspan;
  1757. // if(isset($data->groups[$index])) $columns[$field]['fixed'] = 'left';
  1758. $index++;
  1759. }
  1760. $drills = !empty($data->drills) ? array_values($data->drills) : array();
  1761. foreach($data->array as $rowKey => $rowData)
  1762. {
  1763. $index = 0;
  1764. $columnKeys = array_keys($rowData);
  1765. $rowData = array_values($rowData);
  1766. $drillConditions = array();
  1767. $isDrill = array();
  1768. $totalLang = $this->lang->pivot->total;
  1769. $totalColspan = 0;
  1770. foreach($rowData as $value) if($value === $totalLang) $totalColspan++;
  1771. for($i = 0; $i < count($rowData); $i++)
  1772. {
  1773. $field = 'field' . $index;
  1774. $value = $rowData[$i];
  1775. $columnKey = $columnKeys[$i];
  1776. if(!empty($columns[$field]['colspan']))
  1777. {
  1778. $colspan = $columns[$field]['colspan'];
  1779. $value = array_slice($rowData, $i, $colspan);
  1780. $i += $colspan - 1;
  1781. }
  1782. /* 定义数据表格的行数据。*/
  1783. /* Defind row data of the data table. */
  1784. $rows[$rowKey][$field] = $value;
  1785. $drillFields = $this->getDrillFields($rowKey, $columnKey, $drills);
  1786. $drillConditions[$field] = $this->processDrills($field, $drillFields, $columns);
  1787. $isDrill[$field] = isset($columns[$field]['link']) && $totalColspan === 0;
  1788. if(is_string($value)) $columnMaxLen[$field] = max($columnMaxLen[$field], mb_strlen($value));
  1789. /* 定义数据表格合并单元格的配置。*/
  1790. /* Define configuration to merge cell of the data table. */
  1791. if(isset($configs[$rowKey][$index]) && $configs[$rowKey][$index] > 1)
  1792. {
  1793. $rows[$rowKey][$field . '_rowspan'] = $configs[$rowKey][$index];
  1794. $cellSpan[$field]['rowspan'] = $field . '_rowspan';
  1795. }
  1796. if($value === $totalLang)
  1797. {
  1798. $rows[$rowKey][$field . '_colspan'] = $totalColspan;
  1799. $cellSpan[$field]['colspan'] = $field . '_colspan';
  1800. }
  1801. $index++;
  1802. }
  1803. $rows[$rowKey]['conditions'] = $drillConditions;
  1804. $rows[$rowKey]['isDrill'] = $isDrill;
  1805. $rows[$rowKey]['isTotal'] = $totalColspan > 0;
  1806. $rows[$rowKey]['ROW_ID'] = $rowKey;
  1807. $rows[$rowKey]['version'] = $version;
  1808. $rows[$rowKey]['status'] = $status;
  1809. }
  1810. foreach($columns as $field => $column) $columns[$field]['width'] = 16 * $columnMaxLen[$field];
  1811. return array($columns, $rows, $cellSpan);
  1812. }
  1813. /**
  1814. * Get drill fields.
  1815. *
  1816. * @param int $rowIndex
  1817. * @param string $columnKey
  1818. * @param array $drills
  1819. * @access public
  1820. * @return array
  1821. */
  1822. public function getDrillFields($rowIndex, $columnKey, $drills)
  1823. {
  1824. if(empty($drills) || !isset($drills[$rowIndex]) || !isset($drills[$rowIndex]['drillFields'][$columnKey])) return array();
  1825. list($drillFields) = array_values($drills[$rowIndex]);
  1826. return $drillFields[$columnKey];
  1827. }
  1828. /**
  1829. * Process dirlls.
  1830. *
  1831. * @param int $rowIndex
  1832. * @param array $rowData
  1833. * @param array $drills
  1834. * @access public
  1835. * @return array
  1836. * @param string $field
  1837. * @param mixed[] $drillFields
  1838. * @param mixed[] $columns
  1839. */
  1840. public function processDrills($field, $drillFields, $columns)
  1841. {
  1842. $column = $columns[$field];
  1843. if(!isset($column['drillField'])) return array();
  1844. return $this->prepareDrillConditions($drillFields, $column['condition'], $column['drillField']);
  1845. }
  1846. /**
  1847. * Prepare drill conditions.
  1848. *
  1849. * @param array $drillFields
  1850. * @param array $conditions
  1851. * @param string $originField
  1852. * @access public
  1853. * @return array
  1854. */
  1855. public function prepareDrillConditions($drillFields, $conditions, $originField)
  1856. {
  1857. foreach($conditions as $index => $condition)
  1858. {
  1859. extract($condition);
  1860. if(!isset($drillFields[$queryField])) continue;
  1861. $conditions[$index]['value'] = $drillFields[$queryField];
  1862. }
  1863. return array($originField, $conditions);
  1864. }
  1865. /**
  1866. * Convert json string to array.
  1867. *
  1868. * @param string|object|array $json
  1869. * @access public
  1870. * @return array
  1871. */
  1872. public function json2Array($json)
  1873. {
  1874. if(empty($json)) return array();
  1875. if(is_string($json)) return json_decode($json, true);
  1876. if(is_object($json)) return json_decode(json_encode($json), true);
  1877. return $json;
  1878. }
  1879. /**
  1880. * 根据类型和模块ID获取正确的模块ID。
  1881. * Get correct group id with type.
  1882. *
  1883. * @param string $id
  1884. * @param string $type
  1885. * @access public
  1886. * @return string
  1887. */
  1888. public function getCorrectGroup($id, $type)
  1889. {
  1890. if(strpos($id, ',') !== false)
  1891. {
  1892. $ids = explode(',', $id);
  1893. $correctIds = array();
  1894. foreach($ids as $id) $correctIds[] = $this->getCorrectGroup($id, $type);
  1895. $correctIds = array_filter($correctIds);
  1896. return empty($correctIds) ? '' : implode(',', $correctIds);
  1897. }
  1898. $key = "{$type}s";
  1899. $builtinModules = $this->config->bi->builtin->modules->$key;
  1900. if(!isset($builtinModules[$id])) return '';
  1901. $builtinModule = $builtinModules[$id];
  1902. extract($builtinModule);
  1903. $moduleID = $this->dao->select('id')->from(TABLE_MODULE)
  1904. ->where('root')->eq($root)
  1905. ->andWhere('name')->eq($name)
  1906. ->andWhere('type')->eq($type)
  1907. ->andWhere('grade')->eq($grade)
  1908. ->fetch('id');
  1909. return empty($moduleID) ? '' : $moduleID;
  1910. }
  1911. /**
  1912. * 下载duckdb引擎。
  1913. * Download duckdb.
  1914. *
  1915. * @access public
  1916. * @return string
  1917. */
  1918. public function downloadDuckdb()
  1919. {
  1920. $check = $this->checkDuckdbInstall();
  1921. if($check['loading']) return 'loading';
  1922. $this->loadModel('bi');
  1923. $binRoot = $this->app->getTmpRoot() . 'duckdb' . DS;
  1924. $duckdbBin = $this->getDuckdbBinConfig();
  1925. if(!is_dir($binRoot)) mkdir($binRoot, 0755, true);
  1926. $this->updateDownloadingTagFile('file', 'create');
  1927. $this->updateDownloadingTagFile('extension_dm', 'create');
  1928. $this->updateDownloadingTagFile('extension_mysql', 'create');
  1929. $downloadDuckdb = $this->downloadFile($duckdbBin['fileUrl'], $binRoot, $duckdbBin['file']);
  1930. $downloadExtDM = $this->downloadFile($duckdbBin['extensionUrl_dm'], $binRoot, $duckdbBin['extension_dm']);
  1931. $downloadExtMysql = $this->downloadFile($duckdbBin['extensionUrl_mysql'], $binRoot, $duckdbBin['extension_mysql']);
  1932. $this->updateDownloadingTagFile('file', 'remove');
  1933. $this->updateDownloadingTagFile('extension_dm', 'remove');
  1934. $this->updateDownloadingTagFile('extension_mysql', 'remove');
  1935. return $downloadDuckdb && $downloadExtDM && $downloadExtMysql ? 'ok' : 'fail';
  1936. }
  1937. /**
  1938. * 检查 DuckDB 安装状态。
  1939. * Check duckdb install status.
  1940. *﹡
  1941. * @access public
  1942. * @return array
  1943. */
  1944. public function checkDuckdbInstall()
  1945. {
  1946. $checkDuckdb = $this->updateDownloadingTagFile('file', 'check');
  1947. $checkExtDM = $this->updateDownloadingTagFile('extension_dm', 'check');
  1948. $checkExtMysql = $this->updateDownloadingTagFile('extension_mysql', 'check');
  1949. $loading = $checkDuckdb == 'loading' || $checkExtDM == 'loading' || $checkExtMysql == 'loading';
  1950. $ok = $checkDuckdb == 'ok' && $checkExtDM == 'ok' && $checkExtMysql == 'ok';
  1951. $fail = $checkDuckdb == 'fail' || $checkExtDM == 'fail' || $checkExtMysql == 'fail';
  1952. return array('loading' => $loading, 'ok' => $ok, 'fail' => $fail, 'duckdb' => $checkDuckdb, 'ext_dm' => $checkExtDM, 'ext_mysql' => $checkExtMysql);
  1953. }
  1954. /**
  1955. * 更新tab文件下载状态。
  1956. * Update downloading tab file status.
  1957. *
  1958. * @param string $type
  1959. * @param string $action
  1960. * @access public
  1961. * @return string
  1962. */
  1963. public function updateDownloadingTagFile($type = 'file', $action = 'create')
  1964. {
  1965. $downloading = '.downloading';
  1966. $binRoot = $this->app->getTmpRoot() . 'duckdb' . DS;
  1967. $duckdbBin = $this->getDuckdbBinConfig();
  1968. $binFile = $binRoot . $duckdbBin[$type];
  1969. $zboxFile = $duckdbBin['path'] . $duckdbBin[$type];
  1970. $tagFile = $binFile . $downloading;
  1971. if($action == 'create')
  1972. {
  1973. if(file_exists($tagFile)) return 'fail';
  1974. file_put_contents($tagFile, 'Downloading...');
  1975. return 'ok';
  1976. }
  1977. if($action == 'check')
  1978. {
  1979. if(file_exists($binFile) || file_exists($zboxFile)) return 'ok';
  1980. if(file_exists($tagFile)) return 'loading';
  1981. return 'fail';
  1982. }
  1983. if($action == 'remove')
  1984. {
  1985. if(!file_exists($tagFile)) return 'fail';
  1986. unlink($tagFile);
  1987. }
  1988. return 'ok';
  1989. }
  1990. /**
  1991. * 解压文件。
  1992. * Unzip file.
  1993. *
  1994. * @param string $path
  1995. * @param string $file
  1996. * @param string $extractFile
  1997. * @access public
  1998. * @return bool
  1999. */
  2000. public function unzipFile($path, $file, $extractFile)
  2001. {
  2002. $this->app->loadClass('pclzip', true);
  2003. $zip = new pclzip($file);
  2004. /* 限制解压的文件内容以阻止 ZIP 解压缩的目录穿越漏洞。*/
  2005. /* Limit the file content to prevent the directory traversal vulnerability of ZIP decompression. */
  2006. $extractFiles = array($extractFile);
  2007. return $zip->extract(PCLZIP_OPT_PATH, $path, PCLZIP_OPT_BY_NAME, $extractFiles) === 0;
  2008. }
  2009. /**
  2010. * 下载文件。
  2011. * Download file.
  2012. *
  2013. * @param string $url
  2014. * @param string $savePath
  2015. * @param string $finalFile
  2016. * @access public
  2017. * @return bool
  2018. */
  2019. public function downloadFile($url, $savePath, $finalFile)
  2020. {
  2021. $ch = curl_init($url);
  2022. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  2023. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
  2024. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  2025. $fileContents = curl_exec($ch);
  2026. if (curl_errno($ch))
  2027. {
  2028. curl_close($ch);
  2029. return false;
  2030. }
  2031. $result = json_decode($fileContents, true);
  2032. if(isset($result['error']))
  2033. {
  2034. curl_close($ch);
  2035. return false;
  2036. }
  2037. $filename = basename($url);
  2038. $filename = $savePath . $filename;
  2039. $result = file_put_contents($filename, $fileContents);
  2040. if($result === false)
  2041. {
  2042. curl_close($ch);
  2043. return false;
  2044. }
  2045. curl_close($ch);
  2046. chmod($filename, 0755);
  2047. if(pathinfo($filename, PATHINFO_EXTENSION) === 'zip')
  2048. {
  2049. $this->unzipFile($savePath, $filename, $finalFile);
  2050. unlink($filename);
  2051. }
  2052. return chmod($savePath . $finalFile, 0755);
  2053. }
  2054. /**
  2055. * Encode json.
  2056. *
  2057. * @param object|array $object
  2058. * @access private
  2059. * @return string|null
  2060. */
  2061. private function jsonEncode($object)
  2062. {
  2063. if(empty($object)) return null;
  2064. if(is_scalar($object)) return $object;
  2065. return json_encode($object);
  2066. }
  2067. }