diff --git a/config/cache.php b/config/cache.php index c59749cd5c..27619c88b9 100644 --- a/config/cache.php +++ b/config/cache.php @@ -1,10 +1,46 @@ cache = new stdclass(); -$config->cache->enable = false; // 是否开启缓存。Enable cache or not. -$config->cache->lifetime = 5 * 60; // 缓存生存时间。The lifetime of cache. -$config->cache->driver = 'File'; // 缓存驱动。 The driver of cache. can be File|Yac|Apcu. +$config->cache->enable = false; // 是否开启缓存。Enable cache or not. +$config->cache->driver = 'apcu'; // 缓存驱动。 The driver of cache. Can be file|yac|apcu|redis. +$config->cache->scope = ''; // 缓存服务范围。The scope of cache. Can be private|shared. +$config->cache->namespace = ''; // 缓存命名空间。The namespace of cache. +$config->cache->lifetime = 0; // 缓存生存时间,默认永不过期。The lifetime of cache. Default is no expiration. + +$config->cache->dao = new stdClass(); +$config->cache->dao->enable = true; // 是否开启 DAO 缓存。Enable DAO cache or not. +$config->cache->dao->lifetime = 604800; // DAO 缓存生存时间,默认为 7 天。The lifetime of DAO cache. Default is 7 days. + +$config->cache->client = new stdClass(); +$config->cache->client->enable = false; // 是否开启客户端缓存。Enable client cache or not. + +// Format : $config->cache->raw[TABLE_NAME] = 'KEY_FIELD'; +// The TABLE_NAME is the name of the table in the database. +// The KEY_FIELD is the field of the table which is used to generate the key of the cache. It must be unique in the table. + +$config->cache->raw = []; +$config->cache->raw[TABLE_CONFIG] = 'id'; +$config->cache->raw[TABLE_BUILD] = 'id'; +$config->cache->raw[TABLE_MODULE] = 'id'; +$config->cache->raw[TABLE_PRODUCT] = 'id'; +$config->cache->raw[TABLE_PROJECT] = 'id'; +$config->cache->raw[TABLE_RELEASE] = 'id'; +$config->cache->raw[TABLE_USER] = 'account'; + +$config->cache->res = []; +$config->cache->res[TABLE_MODULE][] = ['name' => 'CACHE_MODULE_TREE', 'fields' => ['type', 'root', 'branch']]; + +$config->cache->keys = []; +foreach($config->cache->res as $table => $caches) +{ + foreach($caches as $cache) + { + $cache = (object)$cache; + $cache->table = $table; + $config->cache->keys[$cache->name] = $cache; + define($cache->name, $cache->name); + } +} $config->cache->enableFullPage = false; // 是否开启整页缓存。Enable full page cache or not. $config->cache->fullPageLifetime = 5 * 60; @@ -32,12 +68,10 @@ $config->cacheKeys->execution->ajaxGetDropMenuExecutions = 'ajaxDropMenuExecutio $config->cacheKeys->bug = new stdclass(); $config->cacheKeys->bug->browse = 'bugBrowse%s'; -$config->cache->dao = new stdClass(); -$config->cache->dao->enable = false; // 是否开启 dao 缓存。Enable dao cache or not. -$config->cache->dao->lifetime = 60 * 60 * 24 * 30; // 缓存生存时间,默认为 30 天。The lifetime of cache, default is 30 days. -$config->cache->dao->driver = 'Apcu'; // 缓存驱动,可以为 File|Yac|Apcu。The driver of cache. can be File|Yac|Apcu. - -$config->cache->client = new stdClass(); -$config->cache->client->enable = false; // 是否开启客户端缓存。Enable client cache or not. - -$config->redis = null; \ No newline at end of file +$config->redis = new stdClass(); +$config->redis->host = ''; +$config->redis->port = ''; +$config->redis->username = ''; +$config->redis->password = ''; +$config->redis->database = 0; +$config->redis->serializer = 'igbinary'; // php|igbinary diff --git a/config/config.php b/config/config.php index 7e4c05d0cc..2c16fe3680 100644 --- a/config/config.php +++ b/config/config.php @@ -205,10 +205,6 @@ if(file_exists($filterConfig)) include $filterConfig; $dbConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'db.php'; if(file_exists($dbConfig)) include $dbConfig; -/* 引用缓存的配置。 Include the cache config file. */ -$cacheConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cache.php'; -if(file_exists($cacheConfig)) include $cacheConfig; - /* 读取环境变量的配置。 Read the env config. */ if($config->inContainer || $config->inQuickon) { @@ -229,11 +225,6 @@ if($config->inContainer || $config->inQuickon) $config->default->lang = getenv('ZT_DEFAULT_LANG'); } -/* 引用自定义的配置。 Include the custom config file. */ -$myConfigRoot = (defined('RUN_MODE') and in_array(RUN_MODE, array('test', 'uitest'))) ? dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'config' : dirname(__FILE__); -$myConfig = $myConfigRoot . DIRECTORY_SEPARATOR . 'my.php'; -if(file_exists($myConfig)) include $myConfig; - /* 禅道配置文件。zentaopms settings. */ $zentaopmsConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'zentaopms.php'; if(file_exists($zentaopmsConfig)) include $zentaopmsConfig; @@ -246,10 +237,19 @@ if(file_exists($actionsMapConfig)) include $actionsMapConfig; $routesConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'routes.php'; if(file_exists($routesConfig)) include $routesConfig; +/* 引用缓存的配置。 Include the cache config file. */ +$cacheConfig = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cache.php'; +if(file_exists($cacheConfig)) include $cacheConfig; + /* Include extension config files. */ $extConfigFiles = glob(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'ext/*.php'); if($extConfigFiles) foreach($extConfigFiles as $extConfigFile) include $extConfigFile; +/* 引用自定义的配置。 Include the custom config file. */ +$myConfigRoot = (defined('RUN_MODE') and in_array(RUN_MODE, array('test', 'uitest'))) ? dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'config' : dirname(__FILE__); +$myConfig = $myConfigRoot . DIRECTORY_SEPARATOR . 'my.php'; +if(file_exists($myConfig)) include $myConfig; + /* Set version. */ if($config->edition != 'open') { diff --git a/config/zentaopms.php b/config/zentaopms.php index 9c96a24c59..278898d5fe 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -464,6 +464,7 @@ define('TABLE_JOB', '`' . $config->db->prefix . 'job`'); define('TABLE_COMPILE', '`' . $config->db->prefix . 'compile`'); define('TABLE_MR', '`' . $config->db->prefix . 'mr`'); define('TABLE_MRAPPROVAL', '`' . $config->db->prefix . 'mrapproval`'); +define('TABLE_MARK', '`' . $config->db->prefix . 'mark`'); define('TABLE_SERVERROOM', '`' . $config->db->prefix . 'serverroom`'); define('TABLE_HOST', '`' . $config->db->prefix . 'host`'); @@ -490,6 +491,7 @@ if(!defined('TABLE_SEARCHDICT')) define('TABLE_SEARCHDICT', $config->db->prefi define('TABLE_SCREEN', '`' . $config->db->prefix . 'screen`'); define('TABLE_CHART', '`' . $config->db->prefix . 'chart`'); define('TABLE_PIVOT', '`' . $config->db->prefix . 'pivot`'); +define('TABLE_PIVOTSPEC', '`' . $config->db->prefix . 'pivotspec`'); define('TABLE_PIVOTDRILL', '`' . $config->db->prefix . 'pivotdrill`'); define('TABLE_DASHBOARD', '`' . $config->db->prefix . 'dashboard`'); define('TABLE_DATASET', '`' . $config->db->prefix . 'dataset`'); @@ -726,6 +728,7 @@ $config->objectTables['workflowgroup'] = TABLE_WORKFLOWGROUP; $config->objectTables['productline'] = TABLE_MODULE; $config->objectTables['repocommit'] = TABLE_REPOHISTORY; $config->objectTables['system'] = TABLE_SYSTEM; +$config->objectTables['mark'] = TABLE_MARK; $config->newFeatures = array('introduction', 'tutorial', 'youngBlueTheme', 'visions', 'aiPrompts', 'promptDesign', 'promptExec'); $config->disabledFeatures = ''; diff --git a/db/update21.1.sql b/db/update21.1.sql index bc54e50002..04246dece9 100644 --- a/db/update21.1.sql +++ b/db/update21.1.sql @@ -30,3 +30,50 @@ ALTER TABLE `zt_review` ADD `toAuditBy` varchar(30) not NULL default '' AFTER `l ALTER TABLE `zt_review` ADD `toAuditDate` datetime NULL AFTER `toAuditBy`; ALTER TABLE `zt_design` ADD `storyVersion` smallint(6) UNSIGNED NOT NULL DEFAULT '1' AFTER `story`; + +ALTER TABLE zt_dataview MODIFY `fields` text NULL; +ALTER TABLE zt_dataview MODIFY `objects` text NULL; +ALTER TABLE zt_dataview MODIFY `mode` varchar(50) NOT NULL DEFAULT 'builder'; +ALTER TABLE zt_dataview ADD `driver` enum('mysql','duckdb') NOT NULL DEFAULT 'mysql' AFTER `code`; + +CREATE TABLE IF NOT EXISTS `zt_pivotspec` ( + `pivot` mediumint(8) NOT NULL, + `version` varchar(10) NOT NULL, + `driver` enum('mysql', 'duckdb') NOT NULL default 'mysql', + `mode` varchar(10) NOT NULL default 'builder', + `name` text NULL, + `desc` text NULL, + `sql` text NULL, + `fields` text NULL, + `langs` text NULL, + `vars` text NULL, + `objects` text NULL, + `settings` text NULL, + `filters` text NULL, + `createdDate` datetime NULL +) ENGINE = InnoDB DEFAULT CHARSET=utf8; +CREATE UNIQUE INDEX `idx_pivot_version` ON `zt_pivotspec`(`pivot`, `version`); + +ALTER TABLE `zt_pivot` ADD `version` varchar(10) NOT NULL DEFAULT '0' AFTER `builtin`; +ALTER TABLE `zt_pivot` CHANGE `mode` `mode` varchar(10) NOT NULL DEFAULT 'builder'; +ALTER TABLE `zt_pivot` CHANGE `sql` `sql` text NULL; +ALTER TABLE `zt_pivot` CHANGE `fields` `fields` text NULL; +ALTER TABLE `zt_pivot` CHANGE `langs` `langs` text NULL; +ALTER TABLE `zt_pivot` CHANGE `vars` `vars` text NULL; +ALTER TABLE `zt_pivot` CHANGE `objects` `objects` text NULL; +ALTER TABLE `zt_pivot` CHANGE `settings` `settings` text NULL; +ALTER TABLE `zt_pivot` CHANGE `filters` `filters` text NULL; + +CREATE TABLE IF NOT EXISTS `zt_mark` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `objectType` varchar(50) NOT NULL DEFAULT '', + `objectID` mediumint(8) unsigned NOT NULL DEFAULT 0, + `version` varchar(50) NOT NULL DEFAULT '', + `account` char(30) NOT NULL DEFAULT '', + `date` datetime NULL, + `mark` varchar(50) NOT NULL DEFAULT '', + `extra` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE INDEX `idx_object` ON `zt_mark`(`objectType`,`objectID`); +CREATE INDEX `idx_account` ON `zt_mark`(`account`); diff --git a/db/update21.2.sql b/db/update21.2.sql deleted file mode 100644 index 4215f8afca..0000000000 --- a/db/update21.2.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE zt_dataview MODIFY `fields` text NULL; -ALTER TABLE zt_dataview MODIFY `objects` text NULL; -ALTER TABLE zt_dataview MODIFY `mode` varchar(50) NOT NULL DEFAULT 'builder'; -ALTER TABLE zt_dataview ADD `driver` enum('mysql','duckdb') NOT NULL DEFAULT 'mysql' AFTER `code`; diff --git a/db/zentao.sql b/db/zentao.sql index 1b4c788931..db09985be2 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -15355,22 +15355,23 @@ CREATE TABLE IF NOT EXISTS `zt_pivot` ( `dimension` mediumint(8) unsigned NOT NULL DEFAULT 0, `group` varchar(255) NOT NULL DEFAULT '', `code` varchar(255) NOT NULL DEFAULT '', - `driver` enum('mysql', 'duckdb') not NULL default 'mysql', - `mode` enum('text', 'builder') not NULL default 'builder', + `driver` enum('mysql', 'duckdb') NOT NULL default 'mysql', + `mode` varchar(10) NOT NULL default 'builder', `name` text NULL, `desc` text NULL, `acl` enum('open','private') NOT NULL DEFAULT 'open', `whitelist` text NULL, - `sql` mediumtext NULL, - `fields` mediumtext NULL, - `langs` mediumtext NULL, - `vars` mediumtext NULL, - `objects` mediumtext NULL, - `settings` mediumtext NULL, - `filters` mediumtext NULL, + `sql` text NULL, + `fields` text NULL, + `langs` text NULL, + `vars` text NULL, + `objects` text NULL, + `settings` text NULL, + `filters` text NULL, `step` tinyint(3) unsigned NOT NULL DEFAULT '0', `stage` enum('draft','published') NOT NULL DEFAULT 'draft', `builtin` enum('0', '1') NOT NULL DEFAULT '0', + `version` varchar(10) NOT NULL DEFAULT '0', `createdBy` varchar(30) NOT NULL DEFAULT '', `createdDate` datetime NULL, `editedBy` varchar(30) NOT NULL DEFAULT '', @@ -15381,6 +15382,25 @@ CREATE TABLE IF NOT EXISTS `zt_pivot` ( CREATE INDEX `dimension` ON `zt_pivot` (`dimension`); CREATE INDEX `group` ON `zt_pivot` (`group`); +-- DROP TABLE IF EXISTS `zt_pivotspec`; +CREATE TABLE IF NOT EXISTS `zt_pivotspec` ( + `pivot` mediumint(8) NOT NULL, + `version` varchar(10) NOT NULL, + `driver` enum('mysql', 'duckdb') NOT NULL default 'mysql', + `mode` varchar(10) NOT NULL default 'builder', + `name` text NULL, + `desc` text NULL, + `sql` text NULL, + `fields` text NULL, + `langs` text NULL, + `vars` text NULL, + `objects` text NULL, + `settings` text NULL, + `filters` text NULL, + `createdDate` datetime NULL +) ENGINE = InnoDB DEFAULT CHARSET=utf8; +CREATE UNIQUE INDEX `idx_pivot_version` ON `zt_pivotspec`(`pivot`, `version`); + -- DROP TABLE IF EXISTS `zt_sqlbuilder`; CREATE TABLE IF NOT EXISTS `zt_sqlbuilder` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, @@ -16106,6 +16126,7 @@ REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, ` ('zh-tw', 'custom', 'relationList', '3', '{\"relation\":\"\\u91cd\\u8907\",\"relativeRelation\":\"\\u91cd\\u8907\"}', '0', 'all'), ('zh-tw', 'custom', 'relationList', '4', '{\"relation\":\"\\u5f15\\u7528\",\"relativeRelation\":\"\\u88ab\\u5f15\\u7528\"}', '0', 'all'); +-- DROP TABLE IF EXISTS `zt_system`; CREATE TABLE IF NOT EXISTS `zt_system` ( `id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL DEFAULT '', @@ -16125,3 +16146,18 @@ CREATE TABLE IF NOT EXISTS `zt_system` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE INDEX `idx_product` ON `zt_system`(`product`); CREATE INDEX `idx_status` ON `zt_system`(`status`); + +-- DROP TABLE IF EXISTS `zt_mark`; +CREATE TABLE IF NOT EXISTS `zt_mark` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `objectType` varchar(50) NOT NULL DEFAULT '', + `objectID` mediumint(8) unsigned NOT NULL DEFAULT 0, + `version` varchar(50) NOT NULL DEFAULT '', + `account` char(30) NOT NULL DEFAULT '', + `date` datetime NULL, + `mark` varchar(50) NOT NULL DEFAULT '', + `extra` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE INDEX `idx_object` ON `zt_mark`(`objectType`,`objectID`); +CREATE INDEX `idx_account` ON `zt_mark`(`account`); diff --git a/framework/base/control.class.php b/framework/base/control.class.php index 15f55f8f32..eb67021721 100644 --- a/framework/base/control.class.php +++ b/framework/base/control.class.php @@ -168,6 +168,15 @@ class baseControl */ public $devicePrefix; + /** + * $mao对象,用于访问缓存。 + * The $mao object, used to access the cache. + * + * @var object + * @access public + */ + public $mao; + /** * 构造方法。 * @@ -198,6 +207,7 @@ class baseControl $this->config = $config; $this->lang = $lang; $this->dbh = $dbh; + $this->mao = $app->mao; $this->viewType = $this->app->getViewType(); $this->appName = $appName ?: $this->app->getAppName(); @@ -324,7 +334,6 @@ class baseControl $this->{$moduleName} = $model; $this->dao = $model->dao; - $this->cache = $model->cache; return $model; } diff --git a/framework/base/model.class.php b/framework/base/model.class.php index 15fac010db..7324bb064e 100644 --- a/framework/base/model.class.php +++ b/framework/base/model.class.php @@ -124,13 +124,13 @@ class baseModel public $global; /** - * $cache对象,用于访问缓存。 - * The $cache object, used to access the cache. + * $mao对象,用于访问缓存。 + * The $mao object, used to access the cache. * * @var object * @access public */ - public $cache; + public $mao; /** * 构造方法。 @@ -153,6 +153,7 @@ class baseModel $this->lang = $lang; $this->dbh = $dbh; $this->dao = $dao; + $this->mao = $app->mao; $this->appName = empty($appName) ? $this->app->getAppName() : $appName; $moduleName = $this->getModuleName(); @@ -160,7 +161,6 @@ class baseModel if($moduleName != 'common') $this->app->loadModuleConfig($moduleName, $this->appName); $this->setSuperVars(); - if($this->config->cache->enable) $this->loadCache(); /** * 读取当前模块的tao类。 @@ -310,20 +310,6 @@ class baseModel return $extensionObject; } - /** - * 加载缓存类。 - * Load cache class. - * - * @access public - * @return void - */ - public function loadCache() - { - $this->app->loadClass('cache', $static = true); - $namespace = isset($this->session->user->account) ? $this->session->user->account : 'guest'; - $this->cache = cache::create($this->config->cache->driver, $namespace, $this->config->cache->lifetime); - } - /** * 删除记录。 * Delete one record. diff --git a/framework/base/router.class.php b/framework/base/router.class.php index 97b55ae3c0..8ad62956e3 100644 --- a/framework/base/router.class.php +++ b/framework/base/router.class.php @@ -306,6 +306,15 @@ class baseRouter */ public $lang; + /** + * 全局缓存对象,用于操作缓存。 + * The global cache object, used to operate cache. + * + * @var object + * @access public + */ + public $cache = null; + /** * 全局$dbh对象,数据库连接句柄。 * The global $dbh object, the database connection handler. @@ -324,6 +333,15 @@ class baseRouter */ public $dao; + /** + * $mao对象,用于访问或者更新缓存。 + * The $mao object, used to access or update cache. + * + * @var mao + * @access public + */ + public $mao; + /** * 从数据库的句柄。 * The slave database handler. @@ -436,6 +454,14 @@ class baseRouter */ public $clientCacheTime = 0; + /** + * 缓存Model。 + * The cache model. + * + * @var object + */ + public $cacheModel; + /** * 构造方法, 设置路径,类,超级变量等。注意: * 1.应该使用createApp()方法实例化router类; @@ -486,8 +512,7 @@ class baseRouter $this->setTimezone(); if($this->config->framework->autoConnectDB) $this->connectDB(); - - if($this->config->redis) $this->redis = $this->connectToRedis(); + $this->loadCache(); $this->setupProfiling(); $this->setupXhprof(); @@ -495,8 +520,6 @@ class baseRouter $this->setEdition(); $this->setClient(); - - $this->loadCacheConfig(); } /** @@ -2798,29 +2821,6 @@ class baseRouter include $mainConfigFile; } - /** - * 从数据库加载缓存配置。 - * Load the cache config from the database. - * - * @access public - * @return void - */ - public function loadCacheConfig() - { - if(!$this->checkInstalled()) return false; - - $globalCache = $this->dao->select('`value`')->from(TABLE_CONFIG)->where('`module`')->eq('common')->andWhere('`section`')->eq('global')->andWhere('`key`')->eq('cache')->limit(1)->fetch('value'); - if(!$globalCache) return false; - - $caches = json_decode($globalCache); - foreach($caches as $cacheKey => $cache) - { - if(!isset($this->config->cache->$cacheKey)) $this->config->cache->$cacheKey = new stdClass(); - - foreach($cache as $key => $value) $this->config->cache->$cacheKey->$key = $value; - } - } - /** * 当multiSite功能打开的时候,加载额外的配置文件。 * When multiSite feature enabled, load extra config file. @@ -3088,6 +3088,38 @@ class baseRouter $dao = new $driver($this); $this->dao = $dao; + + $this->loadClass('mao', true); + $this->mao = new mao($this); + } + + /** + * 加载缓存类,初始化全局缓存对象。 + * Load the cache class and init the global cache object. + * + * @access public + * @return void + */ + private function loadCache() + { + if(!$this->checkInstalled()) return false; + + $cacheConfig = $this->dao->select('`key`, value')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('common')->andWhere('section')->eq('cache')->fetchPairs(); + foreach($cacheConfig as $key => $value) $this->config->cache->$key = $value; + if(!$this->config->cache->enable) return; + + if($this->config->cache->driver == 'redis') + { + $redisConfig = $this->dao->select('`key`, value')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('common')->andWhere('section')->eq('redis')->fetchPairs(); + foreach($redisConfig as $key => $value) $this->config->redis->$key = $value; + } + + $this->loadClass('cache', true); + $this->cache = new Zentao\Cache\cache($this); + + /* 为 dao 和 mao 设置访问缓存的对象。 Set the cache object for dao and mao. */ + $this->dao->cache = $this->cache; + $this->mao->cache = $this->cache; } /** @@ -3205,6 +3237,9 @@ class baseRouter */ public function shutdown() { + /* 如果开启了缓存则关闭缓存连接,主要用于 Redis 等缓存服务。Close the cache connection if it's open. */ + if(!empty($this->cache)) $this->cache->close(); + /* 如果debug模式开启,保存sql语句(If debug on, save sql queries) */ if(!empty($this->config->debug)) $this->saveSQL(); @@ -3509,45 +3544,6 @@ class baseRouter return $installed; } - - /** - * Connect to Redis Server. - * @link https://pecl.php.net/package/redis - * @link https://github.com/phpredis/phpredis/ - * @return Redis|object|false - */ - public function connectToRedis() - { - if(!extension_loaded('redis')) return false; - - global $config; - - if(empty($config->redis)) return false; - - try - { - $redis = new Redis(); - - $version = phpversion('redis'); - if(version_compare($version, '5.3.0', 'ge')) - { - $redis->connect($config->redis->host , $config->redis->port, $config->redis->timeout, '', 0, 0, ['auth' => [$config->redis->username, $config->redis->password]]); - } - else - { - $redis->connect($config->redis->host , $config->redis->port, $config->redis->timeout, '', 0, 0); - $redis->auth(['pass' => $config->redis->password]); - } - - if(!$redis->ping()) return false; - } - catch(RedisException $e) - { - $this->triggerError($e->getMessage(), __FILE__, __LINE__, true); - } - - if($redis) return $redis; - } } /** diff --git a/framework/control.class.php b/framework/control.class.php index 64c3b83f90..89b4f0a73e 100644 --- a/framework/control.class.php +++ b/framework/control.class.php @@ -20,8 +20,6 @@ include __DIR__ . '/base/control.class.php'; class control extends baseControl { - public $redis = false; - /** * Check requiredFields and set exportFields for workflow. * @@ -37,8 +35,6 @@ class control extends baseControl $this->app->setOpenApp(); - if($this->config->redis) $this->redis = $this->app->redis; - if($this->config->edition == 'open') return false; /* Code for task #9224. Set requiredFields for workflow. */ diff --git a/framework/helper.class.php b/framework/helper.class.php index 47fff7cde1..9da64a705c 100644 --- a/framework/helper.class.php +++ b/framework/helper.class.php @@ -433,6 +433,47 @@ class helper extends baseHelper $checkFunc = 'check' . $operator; return validater::$checkFunc($value1, $value2); } + + /** + * 连接 Redis 服务器。 + * Connect to Redis server. + * + * @param object $setting + * @static + * @access public + * @return object + */ + public static function connectRedis(object $setting) + { + if(!class_exists('Redis')) throw new Exception('The Redis extension is not installed.'); + + try + { + $redis = new Redis(); + + $version = phpversion('redis'); + if(version_compare($version, '5.3.0', 'ge')) + { + $redis->connect($setting->host, (int)$setting->port, 1, null, 0, 0, ['auth' => [$setting->username ?: null, $setting->password ?: null]]); + } + else + { + $redis->connect($setting->host, (int)$setting->port, 1, null, 0, 0); + $redis->auth(['pass' => $setting->password ?: null]); + } + + if(!$redis->ping()) throw new Exception('Can not connect to Redis server.'); + + $databases = $redis->config('GET', 'databases'); + if($setting->database >= $databases['databases']) throw new Exception("The database number is out of range. Your Redis server's max database number is " . $databases['databases'] - 1 . '.'); + + return $redis; + } + catch(RedisException $e) + { + throw new Exception('Can not connect to Redis server. The error message is: ' . $e->getMessage()); + } + } } /** diff --git a/framework/model.class.php b/framework/model.class.php index fc17aa518e..dae146ed32 100644 --- a/framework/model.class.php +++ b/framework/model.class.php @@ -20,15 +20,6 @@ include __DIR__ . '/base/model.class.php'; class model extends baseModel { - public $redis = false; - - public function __construct($appName = '') - { - parent::__construct($appName); - - if($this->config->redis) $this->redis = $this->app->redis; - } - /** * 企业版部分功能是从然之合并过来的。ZDOO代码中调用loadModel方法时传递了一个非空的appName,在禅道中会导致错误。 * 调用父类的loadModel方法来避免这个错误。 @@ -99,7 +90,7 @@ class model extends baseModel $table = zget($this->config->objectTables, $moduleName, ''); if(empty($table)) return false; - return $this->dao->findById($objectID)->from($table)->fetch(); + return $this->mao->findById($objectID)->from($table)->fetch(); } /** @@ -422,7 +413,7 @@ class model extends baseModel * @access public * @return mixed */ - public function __call($method, $arguments) + public function __call(string $method, array $arguments) { $moduleName = $this->getModuleName(); $taoClass = $moduleName . 'Tao'; diff --git a/lib/base/dao/dao.class.php b/lib/base/dao/dao.class.php index 3754442b06..97229a0d09 100644 --- a/lib/base/dao/dao.class.php +++ b/lib/base/dao/dao.class.php @@ -89,6 +89,15 @@ class baseDAO */ public $slaveDBH; + /** + * 全局对象$cache。 + * The global cache object. + * + * @var object + * @access public + */ + public $cache = null; + /** * sql对象,用于生成sql语句。 * The sql object, used to create the query sql. @@ -211,24 +220,12 @@ class baseDAO public function __construct($app) { global $config, $lang, $dbh, $slaveDBH; - $this->app = $app; - $this->config = $config; - $this->lang = $lang; - $this->dbh = $dbh; - $this->slaveDBH = $slaveDBH ? $slaveDBH : false; - - if($config->cache->dao->enable) - { - $this->app->loadClass('cache', true); - try - { - $this->cache = new cache($config->cache->dao->driver, $config->db->name . '-dao-', $config->cache->dao->lifetime); - } - catch (Exception $e) - { - die($e->getMessage()); - } - } + $this->app = $app; + $this->config = $config; + $this->lang = $lang; + $this->dbh = $dbh; + $this->cache = $app->cache; + $this->slaveDBH = $slaveDBH ? $slaveDBH : false; $this->reset(); } @@ -331,6 +328,21 @@ class baseDAO $this->method = $method; } + /** + * 生成缓存的 key。 + * Create the cache key. + * + * @param mixed $args + * @access private + * @return string + */ + private function createCacheKey(...$args): string + { + if(empty($this->cache)) return implode('-', $args); + + return $this->cache->createKey('dao', ...$args); + } + /** * 获取缓存。 * Get the cache. @@ -344,7 +356,7 @@ class baseDAO { if(!$this->app->isServing() || empty($this->cache)) return self::CACHE_MISS; - $cache = $this->cache->get($key); + $cache = $this->cache->getByKey($key); if($cache === null) return self::CACHE_MISS; /* 解析缓存的更新时间和值到变量中。 */ @@ -362,7 +374,8 @@ class baseDAO { if(strpos($table, 'boardlayer') !== false) return self::CACHE_MISS; - $tableCache = $this->cache->get($table); + $tableKey = $this->createCacheKey('table', $table); + $tableCache = $this->cache->getByKey($tableKey); if($tableCache === null) continue; if($tableCache[0] > $cachedTime) return self::CACHE_MISS; @@ -381,16 +394,17 @@ class baseDAO * * @param string $key * @param mixed $value + * @param int $ttl * @access public * @return void */ - public function setCache($key, $value = null) + public function setCache($key, $value = null, int $ttl = null) { if(!$this->app->isServing() || empty($this->cache)) return false; $this->app->useClientCache = false; - $this->cache->set($key, array(microtime(true), $value)); + $this->cache->saveByKey($key, array(microtime(true), $value), $ttl ?? $this->config->cache->dao->lifetime); } /** @@ -413,7 +427,8 @@ class baseDAO /* 更新表的缓存时间。*/ /* Update the table cache time. */ $table = str_replace(array('`', '"'), '', $table); - $this->setCache($table); + $key = $this->createCacheKey('table', $table); + $this->setCache($key, $table, 0); } } @@ -1047,26 +1062,33 @@ class baseDAO /* Real-time save log. */ if(dao::$realTimeLog && dao::$realTimeFile) file_put_contents(dao::$realTimeFile, $sql . "\n", FILE_APPEND); - $table = trim($this->table, '`'); + $table = $this->table; + $method = $this->method; $this->reset(); /* Force to query from master db, if db has been changed. */ $this->slaveDBH = false; + if($this->cache) $this->cache->prepare($table, $method, $sql); + $result = $this->dbh->exec($sql); + /* See: https://www.php.net/manual/en/pdo.lastinsertid.php .*/ - $this->_lastInsertID = $this->dbh->lastInsertId(); + if($method == 'insert') $this->_lastInsertID = $this->dbh->lastInsertID(); + + if($this->cache) $result ? $this->cache->sync() : $this->cache->reset(); $this->setTableCache($sql); if($this->config->enableDuckdb) { - $now = helper::now(); - $queueTable = trim(TABLE_DUCKDBQUEUE, '`'); + $queueTable = TABLE_DUCKDBQUEUE; if(!empty($table) && $table != $queueTable) { - $this->dbh->exec("UPDATE {$queueTable} SET updatedTime = '$now' WHERE object = '$table'"); - $this->dbh->exec("INSERT INTO {$queueTable} (object, updatedTime, syncTime) SELECT '$table', '$now', NULL WHERE NOT EXISTS (SELECT 1 FROM {$queueTable} WHERE object = '$table' );"); + $now = helper::now(); + $object = trim($table, '`'); + $this->dbh->exec("UPDATE {$queueTable} SET updatedTime = '$now' WHERE object = '$object'"); + $this->dbh->exec("INSERT INTO {$queueTable} (`object`, `updatedTime`, `syncTime`) SELECT '$object', '$now', NULL WHERE NOT EXISTS (SELECT 1 FROM {$queueTable} WHERE `object` = '$object' );"); } } @@ -1091,9 +1113,8 @@ class baseDAO */ public function fetch($field = '') { - $sql = $this->processSQL(); - $key = 'fetch-' . md5($sql); - + $sql = $this->processSQL(); + $key = $this->createCacheKey('fetch', md5($sql)); $result = $this->getCache($key, $sql); if($result === self::CACHE_MISS) { @@ -1117,9 +1138,8 @@ class baseDAO */ public function fetchAll($keyField = '') { - $sql = $this->processSQL(); - $key = 'fetchAll-' . md5($sql); - + $sql = $this->processSQL(); + $key = $this->createCacheKey('fetchAll', md5($sql)); $rows = $this->getCache($key, $sql); if($rows === self::CACHE_MISS) { @@ -1145,10 +1165,8 @@ class baseDAO */ public function fetchGroup($groupField, $keyField = '') { - $sql = $this->processSQL(); - $table = $this->table; - $key = 'fetchGroup-' . md5($sql); - + $sql = $this->processSQL(); + $key = $this->createCacheKey('fetchAll', md5($sql)); $rows = $this->getCache($key, $sql); if($rows === self::CACHE_MISS) { @@ -1178,9 +1196,8 @@ class baseDAO */ public function fetchPairs($keyField = '', $valueField = '') { - $sql = $this->processSQL(); - $key = 'fetchPairs-' . md5($sql); - + $sql = $this->processSQL(); + $key = $this->createCacheKey('fetchAll', md5($sql)); $rows = $this->getCache($key, $sql); if($rows === self::CACHE_MISS) { diff --git a/lib/base/mao/mao.class.php b/lib/base/mao/mao.class.php new file mode 100644 index 0000000000..cb081c9880 --- /dev/null +++ b/lib/base/mao/mao.class.php @@ -0,0 +1,772 @@ +app = $app; + $this->config = $config; + $this->dao = $app->dao; + $this->cache = $app->cache; + + $this->reset(); + } + + /** + * 设置$table属性。 + * Set the $table property. + * + * @param string $table + * @access public + * @return void + */ + public function setTable(string $table) + { + $this->table = $table; + } + + /** + * 设置$fields属性。 + * Set the $fields property. + * + * @param array $fields + * @access public + * @return void + */ + public function setFields(array $fields) + { + $this->fields = $fields; + } + + /** + * 设置条件。 + * Set the $conditions property. + * + * @param array $conditions + * @access public + * @return void + */ + public function setConditions(array $conditions) + { + $this->conditions = $conditions; + $this->resetCondition(); + } + + /** + * 重置组装条件。 + * Reset the $condition property. + * + * @access public + * @return void + */ + public function resetCondition() + { + $this->condition = array('field' => '', 'operator' => '', 'value' => null); + } + + /** + * 添加条件。 + * Add the $conditions property. + * + * @param string $condition + * @access public + * @return void + */ + public function addCondition() + { + $this->conditions[] = $this->condition; + } + + /** + * 重置属性。 + * Reset the vars. + * + * @access public + * @return void + */ + public function reset() + { + $this->setFields(array()); + $this->setTable(''); + $this->setConditions([]); + } + + /** + * select方法,调用sql::select()。 + * The select method, call sql::select(). + * + * @param string $fields + * @access public + * @return static|sql|baseDAO the dao object self. + */ + public function select(string $fields = '*') + { + $this->conditions = []; + + $fields = explode(',', $fields); + + $alias = []; + foreach($fields as $field) + { + $fieldInfo = explode(' ', trim($field)); + if(count($fieldInfo) == 1) + { + $alias[$fieldInfo[0]] = $fieldInfo[0]; + } + elseif(count($fieldInfo) == 2) + { + $alias[$fieldInfo[0]] = $fieldInfo[1]; + } + else + { + $alias[$fieldInfo[0]] = $fieldInfo[2]; + } + } + + $this->setFields($alias); + + return $this; + } + + /** + * 设置要操作的表。 + * Set the from table. + * + * @param string $tableName + * @access public + * @return static|sql the dao object self. + */ + public function from(string $tableName) + { + $this->setTable($tableName); + return $this; + } + + /** + * 开始条件判断。 + * Begin condition judge. + * + * @param bool|string $condition + * @access public + * @return static|sql the sql object. + */ + public function beginIF(bool|string $conditionResult) + { + $this->isConditionChecking = true; + $this->conditionIsTrue = (bool)$conditionResult; + return $this; + } + + /** + * 结束条件判断。 + * End the condition judge. + * + * @access public + * @return static|sql the sql object. + */ + public function fi() + { + $this->isConditionChecking = false; + + if(!$this->conditionIsTrue) + { + $this->resetCondition(); + return $this; + } + + $this->addCondition(); + $this->resetCondition(); + + return $this; + } + + /** + * 创建WHERE部分。 + * Create the where part. + * + * @param string $field the field name + * @access public + * @return static the dao object. + */ + public function where(string $field) + { + $this->resetCondition(); + $this->condition['field'] = trim($field, '`'); + return $this; + } + + /** + * 创建andWHERE部分。 + * Create the andWhere part. + * + * @param string $field the field name + * @access public + * @return static the dao object. + */ + public function andWhere(string $field) + { + $this->resetCondition(); + $this->condition['field'] = trim($field, '`'); + return $this; + } + + /** + * 创建eq部分。 + * Create the eq part. + * + * @param string $value + * @access public + * @return static the dao object. + */ + public function eq($value) + { + $this->condition['operator'] = 'eq'; + $this->condition['value'] = $value; + + if(!$this->isConditionChecking) + { + $this->addCondition(); + $this->resetCondition(); + } + + return $this; + } + + /** + * 创建ne部分。 + * Create the ne part. + * + * @param string $value + * @access public + * @return static the dao object. + */ + public function ne($value) + { + $this->condition['operator'] = 'ne'; + $this->condition['value'] = $value; + + if(!$this->isConditionChecking) + { + $this->addCondition(); + $this->resetCondition(); + } + + return $this; + } + + /** + * 创建in部分。 + * Create the in part. + * + * @param string|array $value + * @access public + * @return static the dao object. + */ + public function in(string|array $value) + { + $this->condition['operator'] = 'in'; + $this->condition['value'] = is_string($value) ? explode(',', str_replace(' ', '', $value)) : $value; + + if(!$this->isConditionChecking) + { + $this->addCondition(); + $this->resetCondition(); + } + + return $this; + } + + /** + * 创建notin部分。 + * Create the in part. + * + * @param string|array $value + * @access public + * @return static the dao object. + */ + public function notin(string|array $value) + { + $this->condition['operator'] = 'notin'; + $this->condition['value'] = is_string($value) ? explode(',', str_replace(' ', '', $value)) : $value; + + if(!$this->isConditionChecking) + { + $this->addCondition(); + $this->resetCondition(); + } + + return $this; + } + + /** + * 创建ORDER BY部分。 + * Create the order by part. + * + * @param string $order + * @access public + * @return static|sql the sql object. + */ + public function orderBy(string $order) + { + return $this; + } + + /** + * 创建LIMIT部分。 + * Create the limit part. + * + * @param int $limit + * @access public + * @return static|sql the sql object. + */ + public function limit(int $limit) + { + return $this; + } + + /** + * 判断匹配条件。 + * Check condition is matched. + * + * @param object $object + * @param array $conditions + * @access public + * @return bool + */ + private function isConditionMatched(object $object, array $conditions) + { + foreach($conditions as $condition) + { + $value = $object->{$condition['field']}; + if($condition['operator'] == 'eq') + { + if($condition['value'] != $value) return false; + } + elseif($condition['operator'] == 'ne') + { + if($condition['value'] == $value) return false; + } + elseif($condition['operator'] == 'in') + { + if(!in_array($value, $condition['value'])) return false; + } + elseif($condition['operator'] == 'notin') + { + if(in_array($value, $condition['value'])) return false; + } + else + { + return false; + } + } + + return true; + } + + /** + * 获取一个记录。 + * Fetch one record. + * + * @param string $keyField 如果已经设置获取的字段,则只返回这个字段的值,否则返回这个记录。 + * if the field is set, only return the value of this field, else return this record + * @access public + * @return object|mixed + */ + public function fetch(string $keyField = '') + { + if(empty($this->cache) || empty($this->config->cache->raw[$this->table])) return $this->fetchFromDB('fetch', $keyField); + + $rawResult = []; + + /* 如果条件中有主键字段,则尝试通过主键字段从缓存中获取。If the primary key field is in the condition, try to get from the cache by the primary key field. */ + $field = $this->config->cache->raw[$this->table]; + foreach($this->conditions as $condition) + { + if($condition['field'] == $field && $condition['operator'] == 'eq') + { + $rawResult[] = $this->cache->fetch($this->table, $condition['value']); + break; + } + } + + if(empty($rawResult)) $rawResult = $this->cache->fetchAll($this->table); + if(empty($rawResult)) return $this->fetchFromDB('fetch', $keyField); + + foreach($rawResult as $row) + { + if(!$this->isConditionMatched($row, $this->conditions)) continue; + + if(!$keyField) return $row; + + $keyField = trim($keyField, '`'); + return $row->$keyField; + } + + return ''; + } + + /** + * 获取所有记录。 + * Fetch all records. + * + * @param string $keyField 返回以该字段做键的记录 + * the key field, thus the return records is keyed by this field + * @access public + * @return array the records + */ + public function fetchAll(string $keyField = ''): array + { + if(empty($this->cache) || empty($this->config->cache->raw[$this->table])) return $this->fetchFromDB('fetchAll', $keyField); + + $rawResult = []; + + /* 如果条件中有主键字段,则尝试通过主键字段从缓存中获取。If the primary key field is in the condition, try to get from the cache by the primary key field. */ + $field = $this->config->cache->raw[$this->table]; + foreach($this->conditions as $condition) + { + if($condition['field'] == $field && $condition['operator'] == 'in') + { + $value = $condition['value']; + if(is_numeric($value)) $value = [$value]; + if(is_string($value)) $value = explode(',', str_replace(' ', '', $value)); + if(is_array($value)) + { + $rawResult = $this->cache->fetchAll($this->table, $value); + break; + } + } + } + + if(empty($rawResult)) $rawResult = $this->cache->fetchAll($this->table); + if(empty($rawResult)) return $this->fetchFromDB('fetchAll', $keyField); + + $result = []; + foreach($rawResult as $row) + { + if(!$this->isConditionMatched($row, $this->conditions)) continue; + + $data = new stdclass(); + foreach($this->fields as $field => $alias) + { + if($field == '*') + { + $data = $row; + break; + } + $data->$alias = $row->$field; + } + + if($keyField) + { + $result[$row->$keyField] = $data; + } + else + { + $result[] = $data; + } + } + + return $result; + } + + /** + * 获取的记录是以关联数组的形式 + * Fetch array like key=>value. + * + * 如果没有设置参数,用首末两键作为参数。 + * If the keyFiled and valueField not set, use the first and last in the record. + * + * @param string $keyField + * @param string $valueField + * @access public + * @return array + */ + public function fetchPairs(string $keyField = '', string $valueField = '') + { + if(empty($this->cache) || empty($this->config->cache->raw[$this->table])) return $this->fetchFromDB('fetchPairs', $keyField, $valueField); + + $rows = $this->fetchAll(); + if(empty($rows)) return []; + + if(empty($keyField)) $keyField = $this->fields[0]; + if(empty($valueField)) $valueField = $this->fields[1]; + + $pairs = []; + foreach($rows as $row) + { + $pairs[$row->$keyField] = $row->$valueField; + } + + return $pairs; + } + + /** + * 从数据库中获取数据。 + * Fetch data from database. + * + * @param string $fetchFunc fetch|fetchAll + * @param string $keyField + * @param string $valueField + * @access private + * @return mixed + */ + private function fetchFromDB(string $fetchFunc, string $keyField, string $valueField = '') + { + $fields = implode(',', $this->fields); + $this->dao->select($fields)->from($this->table)->where('1=1'); + + foreach($this->conditions as $condition) + { + $func = $condition['operator']; + $this->dao->andWhere($condition['field'])->$func($condition['value']); + } + + return $this->dao->$fetchFunc($keyField, $valueField); + } + + /** + * 把名为 findByXXX 的方法转换为 where 条件。 + * Convert the method findByXXX to where condition. + * + * @param string $method + * @param array $args + * @access private + * @return object the mao object. + */ + private function findBy(string $method, array $args) + { + $field = str_replace('findby', '', $method); + if(count($args) == 1) + { + $operator = 'eq'; + $value = $args[0]; + } + else + { + $operator = $args[0]; + $value = $args[1]; + } + + $this->setFields(['*']); + $this->conditions = [['field' => $field, 'operator' => $operator, 'value' => $value]]; + + return $this; + } + + /** + * 获取缓存数据拼接到已有数据。 + * Append cache fields to data. + * + * @param array $data + * @param string $keyField + * @access public + * @return void + */ + public function into(array $data, $keyField) + { + if(empty($data) || empty($keyField)) return $data; + + /* Get data keys as conditions. */ + $keyList = []; + foreach($data as $index => $row) $keyList[$index] = $row->$keyField; + + if(empty($this->cache)) + { + /* 如果缓存关闭,从数据库中获取。If the cache is off, get from the database. */ + $fields = []; + foreach($this->fields as $field => $alias) $fields[] = $field == $alias ? $field : "$field AS $alias"; + $fields = implode(',', $fields); + + $primaryKey = $this->config->cache->raw[$this->table]; + $cacheResult = $this->dao->select($fields)->from($this->table)->where($primaryKey)->in(array_unique($keyList))->fetchAll($primaryKey); + } + else + { + $cacheResult = $this->cache->fetchAll($this->table, array_unique($keyList)); + } + + foreach($data as $index => $row) + { + $key = $keyList[$index]; + $cacheRow = $cacheResult[$key]; + + foreach($this->fields as $field => $alias) $row->$alias = $cacheRow->$field; + } + } + + /** + * 清除缓存。 + * Clear cache. + * + * @access public + * @return void + */ + public function clearCache() + { + if(!empty($this->cache)) $this->cache->clear(); + } + + /** + * 魔术方法。 + * 1. 转换 findByxxx 为 where 条件。 + * 2. 调用cache对象的方法。 + * Magic method. + * 1. Convert findByxxx to where condition. + * 2. Call the cache object method. + * + * @param string $method + * @param array $args + * @access public + * @return mixed + */ + public function __call(string $method, array $args) + { + $method = strtolower($method); + + /* + * 如果是findByxxx,转换为where条件语句。 + * findByxxx, xxx as will be in the where. + **/ + if(strpos($method, 'findby') !== false) return $this->findBy($method, $args); + + if(empty($this->cache)) return false; + + if(method_exists($this->cache, $method)) return call_user_func_array([$this->cache, $method], $args); + + $this->app->triggerError("Method $method not found in class baseMao.", __FILE__, __LINE__, $this->config->debug >= 2); + } +} diff --git a/lib/cache/cache.class.php b/lib/cache/cache.class.php index 4cea47d51f..8aeea8791f 100644 --- a/lib/cache/cache.class.php +++ b/lib/cache/cache.class.php @@ -2,20 +2,21 @@ /** * The cache library of zentaopms. * - * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net) + * @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net) * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) - * @author Lu Fei + * @author Gang Liu * @package cache * @link https://www.zentao.net */ +namespace Zentao\Cache; -helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'CacheInterface.php'); -helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'CacheException.php'); -helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'InvalidArgumentException.php'); -helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'ApcuDriver.php'); -helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'RedisDriver.php'); -helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'YacDriver.php'); -helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'FileDriver.php'); +\helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'CacheInterface.php'); +\helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'CacheException.php'); +\helper::import(dirname(__FILE__) . DS . 'simple-cache' . DS . 'InvalidArgumentException.php'); +\helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'ApcuDriver.php'); +\helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'RedisDriver.php'); +\helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'YacDriver.php'); +\helper::import(dirname(__FILE__) . DS . 'driver' . DS . 'FileDriver.php'); use ZenTao\Cache\SimpleCache\InvalidArgumentException; @@ -29,56 +30,918 @@ class cache const DRIVER_REDIS = 'Redis'; + const DRIVER_LIST = [self::DRIVER_APCU, self::DRIVER_FILE, self::DRIVER_YAC, self::DRIVER_REDIS]; + /** - * @var ZenTao\Cache\SimpleCache\CacheInterface + * 全局应用程序对象。 + * Global application object. + * + * @access private + * @var object */ - protected $client; + private $app; - public function __construct($driver = 'File', $namespace = '', $defaultLifetime = 0) + /** + * 全局数据库操作对象。 + * Global database operation object. + * + * @access private + * @var object + */ + private $dao; + + /** + * 全局配置对象。 + * Global configuration object. + * + * @access private + * @var object + */ + private $config; + + /** + * 全局缓存对象。 + * Global cache object. + * + * @access private + * @var object + */ + private $cache; + + /** + * 缓存命名空间。 + * Cache namespace. + * + * @access private + * @var string + */ + private $namespace; + + /** + * 缓存键连接符。 + * Cache key connector. + * + * @access private + * @var string + */ + private $connector; + + /** + * 缓存键。 + * Cache key. + * + * @access private + * @var string + */ + private $key = ''; + + /** + * 缓存标签。 + * Cache label. + * + * @access private + * @var array + */ + private $labels = []; + + /** + * 影响缓存的表名。 + * Table name that affects cache. + * + * @access private + * @var string + */ + private $table = ''; + + /** + * 影响缓存的事件。 + * Event that affects cache. + * + * @access private + * @var string + */ + private $event = ''; + + /** + * 影响缓存的 WHERE 子句。 + * WHERE clause that affects cache. + * + * @access private + * @var string + */ + private $where = ''; + + /** + * 受影响的对象列表。 + * Affected object list. + * + * @access private + * @var array + */ + private $objects = []; + + /** + * 构造函数,根据配置文件初始化缓存对象。 + * Constructor, initialize cache object according to the configuration file. + * + * @param object $app 全局应用程序对象。 + * @access public + * @return void + */ + public function __construct(object $app) { - $driver = ucfirst(strtolower($driver)); - switch($driver) - { - case self::DRIVER_APCU: - $className = 'ZenTao\Cache\Driver\ApcuDriver'; - break; - case self::DRIVER_YAC: - $className = 'ZenTao\Cache\Driver\YacDriver'; - break; - case self::DRIVER_FILE: - $className = 'ZenTao\Cache\Driver\FileDriver'; - break; - case self::DRIVER_REDIS: - $className = 'ZenTao\Cache\Driver\RedisDriver'; - break; - default: - throw new InvalidArgumentException("Driver {$driver} is not supported."); - } + $this->app = $app; + $this->dao = $app->dao; + $this->config = $app->config; - if($driver != self::DRIVER_FILE && !extension_loaded($driver)) throw new InvalidArgumentException("Driver ext-{$driver} is not loaded."); + if(empty($this->config->cache->enable)) return $this->log('The cache is not enabled', __FILE__, __LINE__); - global $app; - $this->client = new $className($namespace, $defaultLifetime, $app->getCacheRoot()); + $driver = ucfirst(strtolower($this->config->cache->driver)); + + if(!in_array($driver, self::DRIVER_LIST)) return $this->log("Driver {$driver} is not supported.", __FILE__, __LINE__); + if($driver != self::DRIVER_FILE && !extension_loaded($driver)) return $this->log("Driver ext-{$driver} is not loaded.", __FILE__, __LINE__); + + $connector = $driver == self::DRIVER_REDIS ? ':' : '-'; + $className = "ZenTao\Cache\Driver\\{$driver}Driver"; + $scope = $this->config->cache->scope; + $namespace = $this->config->cache->namespace; + $lifetime = $this->config->cache->lifetime; + $redis = $this->config->redis; + + $this->setNamespace($namespace); + $this->setConnector($connector); + + if($driver == self::DRIVER_APCU) return $this->cache = new $className($namespace, $lifetime, $scope, $connector); + if($driver == self::DRIVER_REDIS) return $this->cache = new $className($namespace, $lifetime, $scope, $connector, $redis); + if($driver == self::DRIVER_YAC) return $this->cache = new $className($namespace, $lifetime); + if($driver == self::DRIVER_FILE) return $this->cache = new $className($namespace, $lifetime, $app->getCacheRoot()); } /** - * Create a cache instance. + * 设置缓存命名空间。 + * Set cache namespace. * - * @param string $driver - * @param string $namespace - * @param int $defaultLifetime + * @param string $namespace 缓存命名空间。 + * @access private + * @return void + */ + private function setNamespace(string $namespace) + { + $this->namespace = $namespace; + } + + /** + * 设置缓存键连接符。 + * Set cache key connector. + * + * @param string $connector 缓存键连接符。 + * @access private + * @return void + */ + private function setConnector(string $connector) + { + $this->connector = $connector; + } + + /** + * 设置缓存键。 + * Set cache key. + * + * @param string $key 缓存键。 + * @access private + * @return void + */ + private function setKey(string $key) + { + $this->key = $key; + } + + /** + * 设置影响缓存的表名。 + * Set the table name that affects the cache. + * + * @param string $table 表名。 + * @access private + * @return void + */ + private function setTable(string $table) + { + $this->table = $table; + } + + /** + * 设置影响缓存的事件。 + * Set the event that affects the cache. + * + * @param string $event 事件。 + * @access private + * @return void + */ + private function setEvent(string $event) + { + $this->event = $event; + } + + /** + * 设置影响缓存的 WHERE 子句。 + * Set the WHERE clause that affects the cache. + * + * @param string $where WHERE 子句。 + * @access private + * @return void + */ + private function setWhere(string $where) + { + $this->where = $where; + } + + /** + * 设置受影响的对象列表。 + * Set the list of affected objects. + * + * @param array $objects 对象列表。 + * @access private + * @return void + */ + private function setObjects(array $objects) + { + $this->objects = $objects; + } + + /** + * 获取表的主键字段。 + * Get the primary key field of the table. + * + * @access private + * @return string + */ + private function getTableField(): string + { + return $this->config->cache->raw[$this->table]; + } + + /** + * 获取表的缓存代号。 + * Get the cache code of the table. + * + * @access private + * @return string + */ + private function getTableCode(): string + { + return str_replace(['`', $this->config->db->prefix], '', $this->table); + } + + /** + * 获取原始数据类型缓存的键。该缓存用于保存表的原始数据。 + * Get the key of the raw data type cache. This cache is used to save the original data of the table. + * + * @param string $code 缓存代号。 + * @param int|string $id 主键值。 + * @access private + * @return string + */ + private function getRawCacheKey(string $code, int|string $id): string + { + return $this->createKey('raw', $code, $id); + } + + /** + * 获取集合类型缓存的键。该缓存用于保存表的主键字段。 + * Get the key of the set type cache. This cache is used to save the primary key field of the table. + * + * @param string $code 缓存代号。 + * @access private + * @return string + */ + private function getSetCacheKey(string $code): string + { + return $this->createKey('set', $code, 'list'); + } + + /** + * 获取结果类型缓存的键。该缓存用于保存表的计算结果。 + * Get the key of the result type cache. This cache is used to save the calculation results of the table. + * + * @param string $key 缓存键。 + * @access private + * @return string + */ + private function getResCacheKey(string $key): string + { + $args = explode('_', str_replace('cache', 'res', strtolower($key))); + return $this->createKey(...$args); + } + + /** + * 新增数据时更新缓存。 + * Update cache when adding data. + * + * @access private + * @return void + */ + private function create() + { + $objectID = $this->dao->lastInsertID(); + if(!$objectID) return $this->log('Failed to fetch last insert id.', __FILE__, __LINE__); + + $field = $this->getTableField(); + $code = $this->getTableCode(); + + /* 获取新增的数据。Get the new data. */ + $object = $this->dao->select('*')->from($this->table)->where('id')->eq($objectID)->fetch(); + if(!$object) return $this->log('Failed to fetch new object. The sql is: ' . $this->dao->get(), __FILE__, __LINE__); + + /* 把新增的数据保存到缓存中。Save the new data to cache. */ + $rawCacheKey = $this->getRawCacheKey($code, $object->$field); + $this->cache->set($rawCacheKey, $object); + + /* 把新增的数据的 id 保存到缓存中。Save the id of the new data to cache. */ + $setCacheKey = $this->getSetCacheKey($code); + $objectIdList = $this->cache->get($setCacheKey); + $this->cache->set($setCacheKey, $objectIdList ? array_merge($objectIdList, [$object->$field]) : [$object->$field]); + + if(empty($this->config->cache->res[$this->table])) return; + + /* 删除受影响的缓存。Delete the affected cache. */ + $this->deleteAffectedCache([$object]); + } + + /** + * 更新数据时更新缓存。 + * Update cache when updating data. + * + * @access private + * @return void + */ + private function update() + { + if(empty($this->objects)) return $this->log('No objects to update.', __FILE__, __LINE__); + + $field = $this->getTableField(); + $code = $this->getTableCode(); + + /* 获取被更新数据的 id 列表。Get the id list of the updated data. */ + $objectIdList = array_map(function($object) use ($field) { return $object->$field; }, $this->objects); + + /* 获取更新后的数据。Get the updated data. */ + $objects = $this->dao->select('*')->from($this->table)->where($field)->in($objectIdList)->fetchAll($field); + if(!$objects) return $this->log('Failed to fetch updated objects. The sql is: ' . $this->dao->get(), __FILE__, __LINE__); + + /* 把更新后的数据保存到缓存中。Save the updated data to cache. */ + $values = []; + foreach($objects as $object) + { + $rawCacheKey = $this->getRawCacheKey($code, $object->$field); + $values[$rawCacheKey] = $object; + } + $this->cache->setMultiple($values); + + if(empty($this->config->cache->res[$this->table])) return; + + /* 获取受影响的数据。Get the affected data. */ + foreach($objectIdList as $objectID) + { + if(isset($this->objects[$objectID]) && isset($objects[$objectID]) && $this->objects[$objectID] == $objects[$objectID]) unset($this->objects[$objectID], $objects[$objectID]); + } + $affectedObjects = array_merge($this->objects, $objects); + + /* 删除受影响的缓存。Delete the affected cache. */ + $this->deleteAffectedCache($affectedObjects); + } + + /** + * 删除数据时更新缓存。 + * Update cache when deleting data. + * + * @access private + * @return void + */ + private function delete() + { + if(empty($this->objects)) return $this->log('No objects to delete.', __FILE__, __LINE__); + + $field = $this->getTableField(); + $code = $this->getTableCode(); + + /* 把被删除的数据从缓存中删除。Delete the deleted data from cache. */ + $affectedKeys = []; + foreach($this->objects as $object) $affectedKeys[] = $this->getRawCacheKey($code, $object->$field); + $this->cache->deleteMultiple($affectedKeys); + + /* 把被删除的数据的 id 从缓存中删除。Delete the id of the deleted data from cache. */ + $setCacheKey = $this->getSetCacheKey($code); + $objectIdList = $this->cache->get($setCacheKey); + $this->cache->set($setCacheKey, array_diff($objectIdList, array_map(function($object) use ($field) { return $object->$field; }, $this->objects))); + + if(empty($this->config->cache->res[$this->table])) return; + + /* 删除受影响的缓存。Delete the affected cache. */ + $this->deleteAffectedCache($this->objects); + } + + /** + * 删除受影响的缓存。 + * Delete the affected cache. + * + * @param array $objects 受影响的对象列表。 + * @access private + * @return void + */ + private function deleteAffectedCache(array $objects) + { + /* 根据受影响的数据查找受影响的缓存。Find the affected cache by the affected data. */ + $keys = []; + foreach($this->config->cache->res[$this->table] as $res) + { + $res = (object)$res; + + /* 如果没有设置关联字段则整个缓存都受影响。If no associated fields are set, the entire cache is affected. */ + if(empty($res->fields)) + { + $keys = $this->getResCacheKey($res->name); + continue; + } + + /* 根据关联字段查找受影响的缓存。Find the affected cache by the associated fields. */ + foreach($objects as $object) + { + $key = $this->getResCacheKey($res->name); + foreach($res->fields as $field) + { + if(!isset($object->$field)) return $this->log("Field {$field} does not exist in table {$this->table}.", __FILE__, __LINE__); + + $key .= $this->connector . $object->$field; + } + $keys[] = $key; + } + } + + /* 删除受影响的缓存。Delete the affected cache. */ + $this->cache->deleteMultiple($keys); + } + + /** + * 检查表是否有缓存设置。 + * Check if the table has cache settings. + * + * @param string $table 表名。 + * @access private + * @return bool + */ + private function checkTable(string $table = ''): bool + { + if(empty($table)) $table = $this->table; + return !empty($this->config->cache->raw[$table]); + } + + /** + * 记录日志信息。 + * Record log information. + * + * @param string $message 日志信息。 + * @param string $file 文件名。 + * @param string $line 行号。 + * @access private + * @return false + */ + private function log(string $message, string $file, string $line): bool + { + if(!$this->config->debug) return false; + + $runMode = PHP_SAPI == 'cli' ? '_cli' : ''; + $logFile = $this->app->getLogRoot() . 'cache' . $runMode . '.' . date('Ymd') . '.log.php'; + if(!file_exists($logFile)) file_put_contents($logFile, '\n"); + + $content = date('Ymd H:i:s') . ': ' . $this->getURI() . "\nError: {$message} in $file on line $line\n"; + file_put_contents($logFile, $content, FILE_APPEND); + + if($this->config->debug >= 2) $this->app->triggerError($message, __FILE__, __LINE__, true); + + return false; + } + + /** + * 获取 URI。 + * Get URI. + * + * @access private + * @return string + */ + private function getURI(): string + { + $uri = $this->app->getURI(); + if($uri) return $uri; + + if($this->config->requestType == 'GET') return $_SERVER['REQUEST_URI']; + + if($this->config->requestType == 'PATH_INFO' || $this->config->requestType == 'PATH_INFO2') + { + $pathInfo = $this->app->getPathInfo(); + if(empty($pathInfo)) return ''; + + $dotPos = strrpos($pathInfo, '.'); + if($dotPos) return substr($pathInfo, 0, $dotPos); + return $pathInfo; + } + + return ''; + } + + /** + * 初始化指定表的缓存。 + * Initialize the cache of the specified table. + * + * @access private + * @return array + */ + private function initTableCache(): array + { + $field = $this->getTableField(); + $objects = $this->dao->select('*')->from($this->table)->fetchAll($field); + if(!$objects) return []; + + $values = []; + $code = $this->getTableCode(); + foreach($objects as $key => $object) + { + $rawCacheKey = $this->getRawCacheKey($code, $key); + $values[$rawCacheKey] = $object; + } + + $this->cache->setMultiple($values); + + $setCacheKey = $this->getSetCacheKey($code); + $this->cache->set($setCacheKey, array_keys($objects)); + + return $objects; + } + + /** + * 生成缓存键。 + * Generate cache key. + * + * @param mixed ...$args 缓存键的参数。 + * @access public + * @return string + */ + public function createKey(...$args): string + { + return $this->namespace . $this->connector . implode($this->connector, $args); + } + + /** + * 从缓存中获取指定表指定 id 的数据。 + * Get the data of the specified table and id from the cache. + * + * @param string $table 表名。 + * @param int|string $id 主键值。 + * @access public + * @return object|false + */ + public function fetch(string $table, int|string $id): object|bool + { + if(!$this->checkTable($table)) return $this->log("Table {$table} is not set in the cache configuration", __FILE__, __LINE__); + + if(empty($table)) return $this->log('The table name is empty', __FILE__, __LINE__); + if(empty($id)) return $this->log('The id is empty', __FILE__, __LINE__); + + $this->setTable($table); + + $code = $this->getTableCode(); + $key = $this->getRawCacheKey($code, $id); + $object = $this->cache->get($key); + if($object) return $object; + + $objects = $this->initTableCache(); + return isset($objects[$id]) ? $objects[$id] : false; + } + + /** + * 从缓存中获取指定表的所有数据。 + * Get all data of the specified table from the cache. + * + * @param string $table 表名。 + * @param array $objectIdList 主键值列表。 + * @access public + * @return array + */ + public function fetchAll(string $table, array $objectIdList = []): array + { + if(!$this->checkTable($table)) return $this->log("Table {$table} is not set in the cache configuration", __FILE__, __LINE__); + + if(empty($table)) return $this->log('The table name is empty', __FILE__, __LINE__); + + $this->setTable($table); + + $code = $this->getTableCode(); + + /* 尝试获取指定表的所有主键字段的值。Try to get the values of all primary key fields of the specified table. */ + $setCacheKey = $this->getSetCacheKey($code); + $allObjectIdList = $this->cache->get($setCacheKey); + + /* 如果主键字段的值为空,则初始化指定表的缓存。If the value of the primary key field is empty, initialize the cache of the specified table. */ + if(!$allObjectIdList) + { + $allData = $this->initTableCache(); + if(empty($objectIdList)) return $allData; + + return array_intersect_key($allData, $objectIdList); + } + + /* 如果主键字段的值不为空,则从缓存中获取数据。If the value of the primary key field is not empty, get the data from the cache. */ + if(empty($objectIdList)) $objectIdList = $allObjectIdList; + + $keys = []; + foreach($objectIdList as $objectID) $keys[$objectID] = $this->getRawCacheKey($code, $objectID); + + $objects = $this->cache->getMultiple(array_values($keys)); + + /* 如果缓存中没有全部的数据,则从数据库中获取缺失的数据。If not all data in cache, get the missing data from the database. */ + if(count($keys) > count($objects)) + { + $lostObjects = []; + $diffIdList = array_keys(array_diff($keys, array_keys($objects))); + $diffObjects = $this->dao->select('*')->from($table)->where('id')->in($diffIdList)->fetchAll(); + foreach($diffObjects as $object) + { + $rawCacheKey = $this->getRawCacheKey($code, $object->id); + $lostObjects[$rawCacheKey] = $object; + } + + /* 把缺失的数据保存到缓存中。Save the missing data to cache. */ + $this->cache->setMultiple($lostObjects); + + $objects += $lostObjects; + } + + if(!$objects) return []; + + $result = []; + $field = $this->getTableField(); + foreach($objects as $object) $result[$object->$field] = $object; + + return $result; + } + + /** + * 设置缓存键。 + * Set cache key. + * + * @param string $key 缓存键。 + * @param mixed ...$args 缓存键的参数。 * @access public * @return object */ - public static function create($driver = 'File', $namespace = '', $defaultLifetime = 0) + public function key($key, ...$args) { - return new self($driver, $namespace, $defaultLifetime); + if(empty($this->config->cache->keys[$key])) return $this->log("Key {$key} is not defined", __FILE__, __LINE__); + + $cache = $this->config->cache->keys[$key]; + if(!empty($cache->fields) && !empty($args)) + { + $tableFields = $this->dao->descTable($cache->table); + foreach($cache->fields as $index => $field) + { + if(!isset($tableFields[$field])) return $this->log("Field {$field} does not exist in table {$cache->table}", __FILE__, __LINE__); + if(!isset($args[$index])) continue; + + $tableField = $tableFields[$field]; + if(stripos($tableField->type, 'int') !== false) $args[$index] = (int) $args[$index]; + if(stripos($tableField->type, 'float') !== false) $args[$index] = (float)$args[$index]; + if(stripos($tableField->type, 'decimal') !== false) $args[$index] = (float)$args[$index]; + if(stripos($tableField->type, 'double') !== false) $args[$index] = (float)$args[$index]; + } + } + + $key = $this->getResCacheKey(constant($key)); + foreach($args as $arg) $key .= $this->connector . $arg; + + $this->setKey($key); + return $this; } - public function __call($name, $arguments) + /** + * 设置缓存标签。 + * Set cache label. + * + * @param string $label 缓存标签。 + * @access public + * @return object + */ + public function label(string $label) { - if(!method_exists($this->client, $name)) throw new InvalidArgumentException("Method {$name} does not exist."); + if(empty($label)) return $this->log('The label is empty', __FILE__, __LINE__); + if(empty($this->key)) return $this->log('The key is empty', __FILE__, __LINE__); + if(isset($this->labels[$label])) return $this->log("Label {$label} already used", __FILE__, __LINE__); + $this->labels[$label] = $this->key; + return $this; + } - return call_user_func_array(array($this->client, $name), $arguments); + /** + * 根据当前缓存键获取缓存。 + * Get cache according to the current cache key. + * + * @access public + * @return mixed + */ + public function get() + { + if(empty($this->key)) return $this->log('The key is empty', __FILE__, __LINE__); + return $this->cache->get($this->key); + } + + /** + * 根据指定缓存键获取缓存。 + * Get cache according to the specified cache key. + * + * @param string $key 缓存键。 + * @access public + * @return mixed + */ + public function getByKey(string $key) + { + if(empty($key)) return $this->log('The key is empty', __FILE__, __LINE__); + + return $this->cache->get($key); + } + + /** + * 根据当前缓存键保存缓存。 + * Save cache according to the current cache key. + * + * @param mixed $value 缓存值。 + * @access public + * @return bool + */ + public function save($value) + { + if(empty($this->key)) return $this->log('The key is empty', __FILE__, __LINE__); + return $this->cache->set($this->key, $value); + } + + /** + * 根据指定缓存建保存缓存。 + * Save cache according to the specified cache key. + * + * @param string $key 缓存键。 + * @param mixed $value 缓存值。 + * @param int $ttl 缓存时间。 + * @access public + * @return bool + */ + public function saveByKey(string $key, $value, int $ttl = 0) + { + if(empty($key)) return $this->log('The key is empty', __FILE__, __LINE__); + + return $this->cache->set($key, $value, $ttl); + } + + /** + * 根据指定缓存标签保存缓存。 + * Save cache according to the specified cache label. + * + * @param string $label 缓存标签。 + * @param mixed $value 缓存值。 + * @access public + * @return bool + */ + public function saveByLabel(string $label, $value) + { + if(empty($label)) return $this->log('The label is empty', __FILE__, __LINE__); + if(empty($this->labels[$label])) return $this->log("Label {$label} does not exist", __FILE__, __LINE__); + + return $this->cache->set($this->labels[$label], $value); + } + + /** + * 重置缓存相关设置项。 + * Reset cache related settings. + * + * @access public + * @return void + */ + public function reset() + { + $this->setKey(''); + $this->setTable(''); + $this->setEvent(''); + $this->setWhere(''); + $this->setObjects([]); + } + + /** + * 执行数据库操作前准备更新缓存需要的设置项。 + * Prepare the settings required to update the cache before executing the database operation. + * + * @param string $table 表名。 + * @param string $event 事件。 + * @param string $sql SQL 语句。 + * @access public + * @return void + */ + public function prepare(string $table, string $event, string $sql) + { + $this->reset(); + + if(!$this->checkTable($table)) return; + + if(empty($table)) return $this->log('Table name is required.', __FILE__, __LINE__); + if(empty($event)) return $this->log('Event type is required.', __FILE__, __LINE__); + if(empty($sql)) return $this->log('SQL statement is required.', __FILE__, __LINE__); + + $this->setTable($table); + $this->setEvent($event); + + if($event == 'update' || $event == 'delete') + { + /* 获取 WHERE 子句的内容。Get the content of WHERE clause. */ + $whereLen = strlen(\DAO::WHERE); + $wherePOS = strrpos($sql, \DAO::WHERE); + $groupPOS = strrpos($sql, \DAO::GROUPBY); + $havingPOS = strrpos($sql, \DAO::HAVING); + $orderPOS = strrpos($sql, \DAO::ORDERBY); + $limitPOS = strrpos($sql, \DAO::LIMIT); + $splitPOS = $orderPOS ? $orderPOS : $limitPOS; + $splitPOS = $havingPOS ? $havingPOS : $splitPOS; + $splitPOS = $groupPOS ? $groupPOS : $splitPOS; + + $where = ''; + if($wherePOS) + { + if($splitPOS) + { + $where = substr($sql, $wherePOS + $whereLen, $splitPOS - $wherePOS - $whereLen); + } + else + { + $where = substr($sql, $wherePOS + $whereLen); + } + } + + /* 执行操作后数据已经被修改,所以需要提前获取被影响的数据。*/ + $field = $this->getTableField(); + $objects = $this->dao->select('*')->from($table)->beginIF($where)->where($where)->fi()->fetchAll($field); + + $this->setWhere($where); + $this->setObjects($objects); + } + } + + /** + * 执行数据库操作后更新缓存。 + * Update the cache after executing the database operation. + * + * @access public + * @return void + */ + public function sync() + { + if(!$this->checkTable()) return; + + if(empty($this->table)) return $this->log('Table name is required.', __FILE__, __LINE__); + if(empty($this->event)) return $this->log('Event type is required.', __FILE__, __LINE__); + + if($this->event == 'insert') $this->create(); + if($this->event == 'update') $this->update(); + if($this->event == 'delete') $this->delete(); + + $this->reset(); + } + + /** + * 清空缓存。 + * Clear cache. + * + * @access public + * @return void + */ + public function clear() + { + return $this->cache->clear(); + } + + /** + * 关闭缓存连接。 + * Close cache connection. + * + * @access public + * @return void + */ + public function close() + { + if(method_exists($this->cache, 'close')) $this->cache->close(); + } + + /** + * 获取内存使用情况。 + * Get memory usage. + * + * @param string $type + * @return string + */ + public function memory(string $type) + { + return $this->cache->memory($type); } } diff --git a/lib/cache/driver/ApcuDriver.php b/lib/cache/driver/ApcuDriver.php index 53409fddc1..04d0efc20c 100644 --- a/lib/cache/driver/ApcuDriver.php +++ b/lib/cache/driver/ApcuDriver.php @@ -17,26 +17,51 @@ use ZenTao\Cache\SimpleCache\InvalidArgumentException; class ApcuDriver implements CacheInterface { /** + * 缓存命名空间。 + * The cache namespace. + * + * @access private * @var string */ private $namespace; /** + * 缓存默认生命周期。 + * The cache default lifetime. + * + * @access private * @var int */ private $defaultLifetime; - public function __construct($namespace = '', $defaultLifetime = 0) + /** + * 缓存服务范围。private 独享|public 共享。 + * The cache scope. + * + * @access private + * @var string + */ + private $scope; + + /** + * 缓存键连接符。 + * Cache key connector. + * + * @access private + * @var string + */ + private $connector; + + public function __construct($namespace = '', $defaultLifetime = 0, $scope = '', $connector = '') { - $this->namespace = $namespace; + $this->namespace = $namespace; $this->defaultLifetime = $defaultLifetime; + $this->scope = $scope; + $this->connector = $connector; } public function get($key, $default = null) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - $value = apcu_fetch($key, $success); return $success === false ? $default : $value; @@ -44,75 +69,51 @@ class ApcuDriver implements CacheInterface public function set($key, $value, $ttl = null) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); + $ttl = (int)($ttl ?: $this->defaultLifetime); - $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl; - - return apcu_store($key, $value, (int) $ttl); + return apcu_store($key, $value, $ttl); } public function delete($key) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - return apcu_delete($key); } public function clear() { - return apcu_clear_cache(); + if($this->scope == 'private') return apcu_clear_cache(); + + $keys = []; + $info = apcu_cache_info(); + $cacheList = $info['cache_list']; + foreach($cacheList as $cache) + { + if(strpos($cache['info'], $this->namespace . $this->connector) === 0) $keys[] = $cache['info']; + } + if(!$keys) return true; + + return $this->deleteMultiple($keys); } public function getMultiple($keys, $default = null) { - $this->assertKeyNames($keys); - $keys = $this->buildKeyNames($keys); + $values = apcu_fetch($keys); + if($values === false) return []; - $result = apcu_fetch($keys); - - if(!is_null($default) && is_array($result) && count($keys) > count($result)) - { - $notFoundKeys = array_diff($keys, array_keys($result)); - $result = array_merge($result, array_fill_keys($notFoundKeys, $default)); - } - - $mappedResult = array(); - - foreach($result as $key => $value) - { - $key = preg_replace("/^$this->namespace/", '', $key); - - $mappedResult[$key] = $value; - } - - return $mappedResult; + return $values; } public function setMultiple($values, $ttl = null) { - $this->assertKeyNames(array_keys($values)); + $ttl = (int)($ttl ?: $this->defaultLifetime); - $mappedByNamespaceValues = array(); - - foreach($values as $key => $value) - { - $mappedByNamespaceValues[$this->buildKeyName($key)] = $value; - } - - $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl; - - $result = apcu_store($mappedByNamespaceValues, (int) $ttl); + $result = apcu_store($values, $ttl); return $result === true ? true : (is_array($result) && count($result) == 0 ? true : false); } public function deleteMultiple($keys) { - $this->assertKeyNames($keys); - $keys = $this->buildKeyNames($keys); - $result = apcu_delete($keys); return count($result) === count($keys) ? false : true; @@ -120,54 +121,24 @@ class ApcuDriver implements CacheInterface public function has($key) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - return (bool) apcu_exists($key); } /** - * @param string $key + * 获取内存使用情况。 + * Get memory usage. * + * @param string $type * @return string */ - private function buildKeyName($key) + public function memory($type) { - return $this->namespace . $key; - } + $info = apcu_sma_info(true); - /** - * @param string[] $keys - * - * @return string[] - */ - private function buildKeyNames(array $keys) - { - return array_map(function($key) { - return $this->buildKeyName($key); - }, $keys); - - } - - /** - * @param mixed $key - * - * @throws InvalidArgumentException - */ - private function assertKeyName($key) - { - if(!is_scalar($key) || is_bool($key)) throw new InvalidArgumentException(); - } - - /** - * @param string[] $keys - * - * @throws InvalidArgumentException - */ - private function assertKeyNames(array $keys) - { - array_map(function ($value) { - $this->assertKeyName($value); - }, $keys); + if($type == 'total') return \helper::formatKB($info['seg_size']); + if($type == 'free') return \helper::formatKB($info['avail_mem']); + if($type == 'used') return \helper::formatKB($info['seg_size'] - $info['avail_mem']); + if($type == 'rate') return round(($info['seg_size'] - $info['avail_mem']) / $info['seg_size'] * 100, 2); + return ''; } } diff --git a/lib/cache/driver/RedisDriver.php b/lib/cache/driver/RedisDriver.php index e6cb7ffc08..d94a6b2022 100644 --- a/lib/cache/driver/RedisDriver.php +++ b/lib/cache/driver/RedisDriver.php @@ -17,22 +17,87 @@ use ZenTao\Cache\SimpleCache\InvalidArgumentException; class RedisDriver implements CacheInterface { /** + * 缓存命名空间,用来区分不同的缓存。 + * The cache namespace, used to distinguish different caches. + * * @var string */ private $namespace; /** + * 缓存过期时间,单位为秒。 + * The cache expiration time, in seconds. + * * @var int */ private $defaultLifetime; - public function __construct($namespace = '', $defaultLifetime = 0) - { - $this->namespace = $namespace; - $this->defaultLifetime = $defaultLifetime; + /** + * 缓存服务范围。private 独享|public 共享。 + * The cache scope. + * + * @access private + * @var string + */ + private $scope; - global $app; - $this->redis = $app->redis; + /** + * 缓存键连接符。 + * Cache key connector. + * + * @access private + * @var string + */ + private $connector; + + public function __construct($namespace = '', $defaultLifetime = 0, $scope = '', $connector = '', $setting = null) + { + $this->namespace = $namespace; + $this->defaultLifetime = $defaultLifetime; + $this->scope = $scope; + $this->connector = $connector; + + $this->connectRedis($setting); + } + + /** + * 连接 Redis 服务器。 + * Connect to the Redis server. + * + * @param object $setting + * @access private + * @return object + */ + private function connectRedis($setting) + { + global $config; + + try + { + $this->redis = \helper::connectRedis($setting); + $this->redis->setOption(\Redis::OPT_SERIALIZER, $this->getSerializer($setting->serializer)); + $this->redis->select($setting->database); + } + catch(Exception $e) + { + \helper::end($e->getMessage()); + } + } + + /** + * 设置序列化器。 + * Set the serializer. + * + * @param string $serializer + * @access private + * @return void + */ + private function getSerializer($serializer) + { + if($serializer == 'igbinary') return \Redis::SERIALIZER_IGBINARY; + if($serializer == 'php') return \Redis::SERIALIZER_PHP; + if($serializer == 'msgpack') return \Redis::SERIALIZER_MSGPACK; + if($serializer == 'json') return \Redis::SERIALIZER_JSON; } /** @@ -45,12 +110,9 @@ class RedisDriver implements CacheInterface */ public function get($key, $default = null) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - $value = $this->redis->get($key); - return $value ? unserialize($value) : $default; + return $value ? $value : $default; } /** @@ -63,12 +125,9 @@ class RedisDriver implements CacheInterface */ public function set($key, $value, $ttl = null) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); + $ttl = (int)($ttl ?: $this->defaultLifetime); - $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl; - - return $this->redis->set($key, serialize($value), (int)$ttl); + return $this->redis->set($key, $value, $ttl ?: null); } /** @@ -76,16 +135,10 @@ class RedisDriver implements CacheInterface * * @link https://github.com/phpredis/phpredis?tab=readme-ov-file#del-delete-unlink * @param mixed $key - * @param bool $prefix * @return int */ - public function delete($key, $prefix = true) + public function delete($key) { - if(!$prefix) return $this->redis->del($key); - - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - return $this->redis->del($key); } @@ -97,24 +150,20 @@ class RedisDriver implements CacheInterface */ public function clear() { - global $config; + if($this->scope == 'private') return $this->redis->flushDB(); /* With Redis::SCAN_RETRY enabled */ $this->redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY); - $it = NULL; - while($cachedKeys = $this->redis->scan($it)) + $it = null; + $keys = []; + + while($cachedKeys = $this->redis->scan($it, $this->namespace . $this->connector . '*')) { - foreach ($cachedKeys as $key) - { - if(strpos($key, $config->db->name) !== false) - { - $this->delete($key, false); - } - } + $keys = array_merge($keys, $cachedKeys); } - return true; + return $this->deleteMultiple($keys); } /** @@ -126,27 +175,7 @@ class RedisDriver implements CacheInterface */ public function getMultiple($keys, $default = null) { - $this->assertKeyNames($keys); - $keys = $this->buildKeyNames($keys); - - $result = $this->redis->mget($keys); - - if(!is_null($default) && is_array($result) && count($keys) > count($result)) - { - $notFoundKeys = array_diff($keys, array_keys($result)); - $result = array_merge($result, array_fill_keys($notFoundKeys, $default)); - } - - $mappedResult = array(); - - foreach($result as $key => $value) - { - $key = preg_replace("/^$this->namespace/", '', $key); - - $mappedResult[$key] = unserialize($value); - } - - return $mappedResult; + return array_filter(array_combine($keys, $this->redis->mget($keys))); } /** @@ -159,26 +188,11 @@ class RedisDriver implements CacheInterface */ public function setMultiple($values, $ttl = null) { - $this->assertKeyNames(array_keys($values)); + $ttl = (int)($ttl ?: $this->defaultLifetime); - $mappedByNamespaceValues = array(); + if(!$ttl) return $this->redis->mset($values); - foreach($values as $key => $value) - { - $mappedByNamespaceValues[$this->buildKeyName($key)] = serialize($value); - } - - $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl; - - if(!empty($ttl)) - { - foreach($mappedByNamespaceValues as $key => $value) - { - $this->redis->set($key, $value, $ttl); - } - } - - return $this->redis->mset($mappedByNamespaceValues); + foreach($values as $key => $value) $this->redis->set($key, $value, $ttl); } /** @@ -189,17 +203,9 @@ class RedisDriver implements CacheInterface */ public function deleteMultiple($keys) { - $this->assertKeyNames($keys); - $keys = $this->buildKeyNames($keys); + $result = $this->redis->del($keys); - $result = array(); - foreach($keys as $key) - { - $isDeleted = $this->delete($key); - if($isDeleted) $result[] = $isDeleted; - } - - return count($result) === count($keys) ? true : false; + return $result === count($keys); } /** @@ -211,49 +217,36 @@ class RedisDriver implements CacheInterface */ public function has($key) { - $this->assertKeyName($key); - $key = $this->buildKeyName($key); - return (bool) $this->redis->exists($key); } /** - * @param string $key + * 关闭 Redis 连接。 + * Close the Redis connection. * + * @access public + * @return bool + */ + public function close() + { + return $this->redis->close(); + } + + /** + * 获取内存使用情况。 + * Get memory usage. + * + * @param string $type * @return string */ - private function buildKeyName($key) + public function memory($type) { - return $this->namespace . $key; - } + $info = $this->redis->info(); - /** - * @param string[] $keys - * - * @return string[] - */ - private function buildKeyNames(array $keys) - { - return array_map(function($key){return $this->buildKeyName($key);}, $keys); - } - - /** - * @param mixed $key - * - * @throws InvalidArgumentException - */ - private function assertKeyName($key) - { - if(!is_scalar($key) || is_bool($key)) throw new InvalidArgumentException(); - } - - /** - * @param string[] $keys - * - * @throws InvalidArgumentException - */ - private function assertKeyNames(array $keys) - { - array_map(function ($value){$this->assertKeyName($value);}, $keys); + if($type == 'total') return $info['total_system_memory_human']; + if($type == 'free') return \helper::formatKB($info['total_system_memory'] - $info['used_memory']); + if($type == 'used') return $info['used_memory_human']; + if($type == 'rate') return round(($info['used_memory'] / $info['total_system_memory']) * 100, 2); + return ''; } } diff --git a/lib/mao/mao.class.php b/lib/mao/mao.class.php new file mode 100644 index 0000000000..3c399fd4a3 --- /dev/null +++ b/lib/mao/mao.class.php @@ -0,0 +1,16 @@ + + * @package cache + * @link https://www.zentao.net + */ + +helper::import(dirname(__FILE__, 2) . '/base/mao/mao.class.php'); + +class mao extends baseMao +{ +} diff --git a/module/action/config.php b/module/action/config.php index 54226c4f47..88318138de 100755 --- a/module/action/config.php +++ b/module/action/config.php @@ -71,6 +71,7 @@ $config->action->objectNameFields['solution'] = 'name'; $config->action->objectNameFields['prompt'] = 'name'; $config->action->objectNameFields['miniprogram'] = 'name'; $config->action->objectNameFields['holiday'] = 'name'; +$config->action->objectNameFields['system'] = 'name'; $config->action->commonImgSize = 870; diff --git a/module/action/lang/de.php b/module/action/lang/de.php index 943c744abd..173d1d44b9 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -361,6 +361,7 @@ $lang->action->desc->closeautobackup = '$date, Disable automatic backup by < $lang->action->desc->autodeletebackups = '$date, Automatically clean up backups by $actor.' . "\n"; $lang->action->desc->savebackupsettings = '$date, Save backup settings backups by $actor.' . "\n"; $lang->action->desc->deleteexpiredbackup = '$date, Clean up expired backups by $actor.' . "\n"; +$lang->action->desc->manualdeletebackup = '$date, Manually clean up backup by $actor.' . "\n"; /* Used to display dynamic information. */ $lang->action->label = new stdclass(); @@ -561,7 +562,7 @@ $lang->action->label->createbackup = 'Created system backup'; $lang->action->label->deletebackup = 'Deleted system backup'; $lang->action->label->restorebackup = 'Restored system backup'; $lang->action->label->upgradesystem = 'Executed system upgrade'; -$lang->action->label->system = ''; +$lang->action->label->system = 'Application'; $lang->action->label->manualbackup = 'Manual backup'; $lang->action->label->autobackup = 'Automatic backup'; $lang->action->label->openautobackup = 'Enable automatic backup'; @@ -569,6 +570,7 @@ $lang->action->label->closeautobackup = 'Disable automatic backup'; $lang->action->label->autodeletebackups = 'Automatically clean up backups'; $lang->action->label->savebackupsettings = 'Save backup settings'; $lang->action->label->deleteexpiredbackup = 'Clean up expired backups'; +$lang->action->label->manualdeletebackup = 'Manually clean up backup'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 92c0d2b3bd..0a3dd19b74 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -361,6 +361,7 @@ $lang->action->desc->closeautobackup = '$date, Disable automatic backup by < $lang->action->desc->autodeletebackups = '$date, Automatically clean up backups by $actor.' . "\n"; $lang->action->desc->savebackupsettings = '$date, Save backup settings backups by $actor.' . "\n"; $lang->action->desc->deleteexpiredbackup = '$date, Clean up expired backups by $actor.' . "\n"; +$lang->action->desc->manualdeletebackup = '$date, Manually clean up backup by $actor.' . "\n"; /* Used to display dynamic information. */ $lang->action->label = new stdclass(); @@ -561,7 +562,7 @@ $lang->action->label->createbackup = 'Created system backup'; $lang->action->label->deletebackup = 'Deleted system backup'; $lang->action->label->restorebackup = 'Restored system backup'; $lang->action->label->upgradesystem = 'Executed system upgrade'; -$lang->action->label->system = ''; +$lang->action->label->system = 'Application'; $lang->action->label->manualbackup = 'Manual backup'; $lang->action->label->autobackup = 'Automatic backup'; $lang->action->label->openautobackup = 'Enable automatic backup'; @@ -569,6 +570,7 @@ $lang->action->label->closeautobackup = 'Disable automatic backup'; $lang->action->label->autodeletebackups = 'Automatically clean up backups'; $lang->action->label->savebackupsettings = 'Save backup settings'; $lang->action->label->deleteexpiredbackup = 'Clean up expired backups'; +$lang->action->label->manualdeletebackup = 'Manually clean up backup'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 6fd3b49a97..b5ee1bd1b7 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -361,6 +361,7 @@ $lang->action->desc->closeautobackup = '$date, Disable automatic backup by < $lang->action->desc->autodeletebackups = '$date, Automatically clean up backups by $actor.' . "\n"; $lang->action->desc->savebackupsettings = '$date, Save backup settings backups by $actor.' . "\n"; $lang->action->desc->deleteexpiredbackup = '$date, Clean up expired backups by $actor.' . "\n"; +$lang->action->desc->manualdeletebackup = '$date, Manually clean up backup by $actor.' . "\n"; /* Used to display dynamic information. */ $lang->action->label = new stdclass(); @@ -561,7 +562,7 @@ $lang->action->label->createbackup = 'Created system backup'; $lang->action->label->deletebackup = 'Deleted system backup'; $lang->action->label->restorebackup = 'Restored system backup'; $lang->action->label->upgradesystem = 'Executed system upgrade'; -$lang->action->label->system = ''; +$lang->action->label->system = 'Application'; $lang->action->label->manualbackup = 'Manual backup'; $lang->action->label->autobackup = 'Automatic backup'; $lang->action->label->openautobackup = 'Enable automatic backup'; @@ -569,6 +570,7 @@ $lang->action->label->closeautobackup = 'Disable automatic backup'; $lang->action->label->autodeletebackups = 'Automatically clean up backups'; $lang->action->label->savebackupsettings = 'Save backup settings'; $lang->action->label->deleteexpiredbackup = 'Clean up expired backups'; +$lang->action->label->manualdeletebackup = 'Manually clean up backup'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass(); diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 0aea276402..cb7fe6cfe7 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -189,6 +189,7 @@ $lang->action->objectTypes['projectbuild'] = '版本'; $lang->action->objectTypes['board'] = '白板'; $lang->action->objectTypes['boardspace'] = '白板空间'; $lang->action->objectTypes['productline'] = '产品线'; +$lang->action->objectTypes['system'] = '应用'; /* 用来描述操作历史记录。*/ $lang->action->desc = new stdclass(); @@ -358,9 +359,10 @@ $lang->action->desc->manualbackup = '$date, 由 $actor $lang->action->desc->autobackup = '$date, 由 $actor 自动备份。' . "\n"; $lang->action->desc->openautobackup = '$date, 由 $actor 开启自动备份。' . "\n"; $lang->action->desc->closeautobackup = '$date, 由 $actor 关闭自动备份。' . "\n"; -$lang->action->desc->autodeletebackups = '$date, 由 $actor 自动清理备份。' . "\n"; +$lang->action->desc->autodeletebackups = '$date, 由 $actor 自动删除备份。' . "\n"; $lang->action->desc->savebackupsettings = '$date, 由 $actor 保存备份设置。' . "\n"; -$lang->action->desc->deleteexpiredbackup = '$date, 由 $actor 清理过期备份。' . "\n"; +$lang->action->desc->deleteexpiredbackup = '$date, 由 $actor 删除过期备份。' . "\n"; +$lang->action->desc->manualdeletebackup = '$date, 由 $actor 手动删除备份。' . "\n"; /* 用来显示动态信息。*/ $lang->action->label = new stdclass(); @@ -561,15 +563,15 @@ $lang->action->label->createbackup = '创建了系统备份'; $lang->action->label->deletebackup = '删除了系统备份'; $lang->action->label->restorebackup = '还原了系统备份'; $lang->action->label->upgradesystem = '执行了系统升级'; -$lang->action->label->system = ''; +$lang->action->label->system = '应用'; $lang->action->label->manualbackup = '手动备份了'; $lang->action->label->autobackup = '自动备份了'; $lang->action->label->openautobackup = '开启自动备份'; $lang->action->label->closeautobackup = '关闭自动备份'; -$lang->action->label->autodeletebackups = '自动清理备份'; +$lang->action->label->autodeletebackups = '自动删除备份'; $lang->action->label->savebackupsettings = '保存备份设置'; -$lang->action->label->deleteexpiredbackup = '清理过期备份'; - +$lang->action->label->deleteexpiredbackup = '删除过期备份'; +$lang->action->label->manualdeletebackup = '手动删除备份'; /* 动态信息按照对象分组 */ @@ -924,7 +926,7 @@ $lang->action->label->chartgroup = '分组'; $lang->action->label->serverroom = '机房|serverroom|browse|'; $lang->action->label->host = '主机|host|view|id=%s'; $lang->action->label->account = "账号|account|view|id=%s"; -$lang->action->label->instance = '应用|instance|view|id=%s'; +$lang->action->label->instance = '服务|instance|view|id=%s'; $lang->action->label->prompt = '提词|ai|promptview|id=%s'; $lang->action->label->miniprogram = '小程序|aiapp|browseminiprogram|id=%s'; $lang->action->label->holiday = '节假日|holiday|browse|'; diff --git a/module/action/model.php b/module/action/model.php index 3fa929fe38..61ce919f6e 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -366,6 +366,19 @@ class actionModel extends model return $action; } + /** + * 获取用户第一条操作。 + * Get user first action. + * + * @param string $account + * @access public + * @return object + */ + public function getAccountFirstAction(string $account): object + { + return $this->dao->select('*')->from(TABLE_ACTION)->where('actor')->eq($account)->orderBy('id')->limit(1)->fetch(); + } + /** * 获取已删除的对象。 * Get deleted objects. @@ -1200,6 +1213,8 @@ class actionModel extends model $actionObjectLabel = $this->lang->doc->menuTitle; } } + + if($objectType == 'system' && strpos(strtolower($actionType), 'backup') !== false) $actionObjectLabel = ''; } if(in_array($this->config->edition, array('max', 'ipd')) && $objectType == 'assetlib') diff --git a/module/admin/config.php b/module/admin/config.php index cd78c03143..446cefcf52 100755 --- a/module/admin/config.php +++ b/module/admin/config.php @@ -5,7 +5,7 @@ $config->admin->log->saveDays = 30; if(!isset($config->safe)) $config->safe = new stdclass(); if(!isset($config->safe->weak)) $config->safe->weak = '123456,password,12345,12345678,qwerty,123456789,1234,1234567,abc123,111111,123123'; -$config->admin->menuGroup['system'] = array('custom|mode', 'backup', 'cron', 'action|trash', 'admin|xuanxuan', 'setting|xuanxuan', 'admin|license', 'admin|checkweak', 'admin|resetpwdsetting', 'admin|safe', 'admin|cache', 'custom|timezone', 'search|buildindex', 'admin|tableengine', 'ldap', 'custom|libreoffice', 'conference', 'watermark', 'client', 'system|browsebackup', 'system|restorebackup'); +$config->admin->menuGroup['system'] = array('custom|mode', 'backup', 'cron', 'action|trash', 'admin|xuanxuan', 'setting|xuanxuan', 'admin|license', 'admin|checkweak', 'admin|resetpwdsetting', 'admin|safe', 'cache|setting', 'custom|timezone', 'search|buildindex', 'admin|tableengine', 'ldap', 'custom|libreoffice', 'conference', 'watermark', 'client', 'system|browsebackup', 'system|restorebackup'); $config->admin->menuGroup['company'] = array('dept', 'company', 'user', 'group', 'tutorial'); $config->admin->menuGroup['switch'] = array('admin|setmodule'); $config->admin->menuGroup['model'] = array('auditcl', 'stage', 'design', 'cmcl', 'reviewcl', 'custom|required', 'custom|set', 'custom|flow', 'custom|code', 'custom|percent','custom|estimate', 'custom|hours', 'subject', 'process', 'activity', 'zoutput', 'classify', 'holiday', 'reviewsetting', 'custom|project'); diff --git a/module/admin/config/form.php b/module/admin/config/form.php index 6fa1ddc479..f862d6abc2 100644 --- a/module/admin/config/form.php +++ b/module/admin/config/form.php @@ -24,6 +24,3 @@ $config->admin->form->log['days'] = array('type' => 'int', 'required' => true, ' $config->admin->form->resetpwdsetting = array(); $config->admin->form->resetpwdsetting['resetPWDByMail'] = array('type' => 'int', 'required' => false, 'default' => 0); - -$config->admin->form->cache = array(); -$config->admin->form->cache['dao'] = array('type' => 'array', 'default' => array('enable' => 0)); diff --git a/module/admin/control.php b/module/admin/control.php index dc2f65192d..4b82e00a35 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -590,50 +590,4 @@ class admin extends control echo 'success'; } - - /** - * 设置是否启用缓存。 - * Set cache enable. - * - * @access public - * @return void - */ - public function cache() - { - if($_POST) - { - if(!extension_loaded('apcu')) return $this->send(array('result' => 'fail', 'message' => $this->lang->admin->apcuNotLoaded)); - if(!ini_get('apc.enabled')) return $this->send(array('result' => 'fail', 'message' => $this->lang->admin->apcuNotEnabled)); - - $cache = form::data()->get(); - $this->loadModel('setting')->setItem('system.common.global.cache', json_encode($cache)); - - if($cache->dao['enable'] != $this->config->cache->dao->enable) $this->dao->clearCache(); - - return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true)); - } - - if(helper::isAPCuEnabled()) - { - $this->view->rate = $this->adminZen->getAPCuMemory('rate'); - $this->view->used = $this->adminZen->getAPCuMemory('used'); - $this->view->total = $this->adminZen->getAPCuMemory('total'); - } - - $this->view->title = $this->lang->admin->cache; - $this->display(); - } - - /** - * 清除数据缓存。 - * Clear data cache. - * - * @access public - * @return void - */ - public function ajaxClearCache() - { - $this->dao->clearCache(); - return $this->send(array('result' => 'success', 'message' => $this->lang->admin->clearSuccess, 'load' => true)); - } } diff --git a/module/admin/lang/de.php b/module/admin/lang/de.php index a43d945734..a551501bb1 100755 --- a/module/admin/lang/de.php +++ b/module/admin/lang/de.php @@ -53,18 +53,6 @@ $lang->admin->engineInfo = "The %s table engine is $lang->admin->engineSummary['hasMyISAM'] = "There are %s tables that are not InnoDB engines"; $lang->admin->engineSummary['allInnoDB'] = "All tables are InnoDB engines"; -$lang->admin->daoCache = 'Data Cache'; -$lang->admin->clearCache = 'Clear'; -$lang->admin->clearSuccess = 'Cleared'; -$lang->admin->memory = 'Memory'; -$lang->admin->usedMemory = 'Total %s, %s used'; -$lang->admin->apcuNotice = 'Data cache is based on the PHP-APCu extension, which needs to be loaded before enabling.'; -$lang->admin->apcuNotLoaded = 'Please load the APCu extension before enabling DAO cache'; -$lang->admin->apcuNotEnabled = 'Please enable the apc.enabled option before enabling DAO cache'; - -$lang->admin->cacheStatusList[1] = 'On'; -$lang->admin->cacheStatusList[0] = 'Off'; - $lang->admin->info = new stdclass(); $lang->admin->info->version = 'Aktuelle Version ist %s. '; $lang->admin->info->links = 'You can visit links below'; diff --git a/module/admin/lang/en.php b/module/admin/lang/en.php index 5a1a8f43a2..38a3ade9fe 100755 --- a/module/admin/lang/en.php +++ b/module/admin/lang/en.php @@ -53,18 +53,6 @@ $lang->admin->engineInfo = "The %s table engine $lang->admin->engineSummary['hasMyISAM'] = "There are %s tables that are not InnoDB engines"; $lang->admin->engineSummary['allInnoDB'] = "All tables are InnoDB engines"; -$lang->admin->daoCache = 'Data Cache'; -$lang->admin->clearCache = 'Clear'; -$lang->admin->clearSuccess = 'Cleared'; -$lang->admin->memory = 'Memory'; -$lang->admin->usedMemory = 'Total %s, %s used'; -$lang->admin->apcuNotice = 'Data cache is based on the PHP-APCu extension, which needs to be loaded before enabling.'; -$lang->admin->apcuNotLoaded = 'Please load the APCu extension before enabling DAO cache'; -$lang->admin->apcuNotEnabled = 'Please enable the apc.enabled option before enabling DAO cache'; - -$lang->admin->cacheStatusList[1] = 'On'; -$lang->admin->cacheStatusList[0] = 'Off'; - $lang->admin->info = new stdclass(); $lang->admin->info->version = 'Current Version is %s. '; $lang->admin->info->links = 'You can visit links below'; diff --git a/module/admin/lang/fr.php b/module/admin/lang/fr.php index ae1935b8e4..3e99ea8b84 100755 --- a/module/admin/lang/fr.php +++ b/module/admin/lang/fr.php @@ -53,18 +53,6 @@ $lang->admin->engineInfo = "The %s table engine is $lang->admin->engineSummary['hasMyISAM'] = "There are %s tables that are not InnoDB engines"; $lang->admin->engineSummary['allInnoDB'] = "All tables are InnoDB engines"; -$lang->admin->daoCache = 'Data Cache'; -$lang->admin->clearCache = 'Clear'; -$lang->admin->clearSuccess = 'Cleared'; -$lang->admin->memory = 'Memory'; -$lang->admin->usedMemory = 'Total %s, %s used'; -$lang->admin->apcuNotice = 'Data cache is based on the PHP-APCu extension, which needs to be loaded before enabling.'; -$lang->admin->apcuNotLoaded = 'Please load the APCu extension before enabling DAO cache'; -$lang->admin->apcuNotEnabled = 'Please enable the apc.enabled option before enabling DAO cache'; - -$lang->admin->cacheStatusList[1] = 'On'; -$lang->admin->cacheStatusList[0] = 'Off'; - $lang->admin->info = new stdclass(); $lang->admin->info->version = 'La version actuelle est %s customisée. '; $lang->admin->info->links = 'Vous pouvez visiter les liens ci-dessous'; diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php index 367b4e94b8..d0d1f0f34d 100644 --- a/module/admin/lang/menu.php +++ b/module/admin/lang/menu.php @@ -47,7 +47,7 @@ $lang->admin->menuList->convert['order'] = 50; $lang->admin->menuList->system['subMenu']['mode'] = array('link' => "{$lang->custom->mode}|custom|mode|"); $lang->admin->menuList->system['subMenu']['trash'] = array('link' => "{$lang->action->trash}|action|trash|"); $lang->admin->menuList->system['subMenu']['safe'] = array('link' => "{$lang->security}|admin|safe|", 'alias' => 'checkweak,resetpwdsetting', 'links' => array('admin|resetpwdsetting|', 'admin|checkweak|')); -$lang->admin->menuList->system['subMenu']['cache'] = array('link' => "{$lang->admin->cache}|admin|cache|"); +$lang->admin->menuList->system['subMenu']['cache'] = array('link' => "{$lang->cache->common}|cache|setting|"); $lang->admin->menuList->system['subMenu']['cron'] = array('link' => "{$lang->admin->cron}|cron|index|", 'subModule' => 'cron'); $lang->admin->menuList->system['subMenu']['timezone'] = array('link' => "{$lang->timezone}|custom|timezone|"); $lang->admin->menuList->system['subMenu']['buildindex'] = array('link' => "{$lang->admin->buildIndex}|search|buildindex|"); diff --git a/module/admin/lang/zh-cn.php b/module/admin/lang/zh-cn.php index 1014eff174..d224905e6c 100755 --- a/module/admin/lang/zh-cn.php +++ b/module/admin/lang/zh-cn.php @@ -53,18 +53,6 @@ $lang->admin->engineInfo = "表%s的引擎是admin->engineSummary['hasMyISAM'] = "有%s个表不是InnoDB引擎"; $lang->admin->engineSummary['allInnoDB'] = "所有的表都是InnoDB引擎了"; -$lang->admin->daoCache = '数据缓存'; -$lang->admin->clearCache = '清除缓存'; -$lang->admin->clearSuccess = '清除成功'; -$lang->admin->memory = '内存使用'; -$lang->admin->usedMemory = '总计 %s,已使用 %s'; -$lang->admin->apcuNotice = '数据缓存基于 PHP-APCu 扩展实现,开启前需要先加载 APCu 扩展。'; -$lang->admin->apcuNotLoaded = '请加载 APCu 扩展后再开启数据缓存'; -$lang->admin->apcuNotEnabled = '请启用 apc.enabled 选项后再开启数据缓存'; - -$lang->admin->cacheStatusList[1] = '开启'; -$lang->admin->cacheStatusList[0] = '关闭'; - $lang->admin->info = new stdclass(); $lang->admin->info->version = '当前系统的版本是%s,'; $lang->admin->info->links = '您可以访问以下链接:'; diff --git a/module/admin/ui/cache.html.php b/module/admin/ui/cache.html.php deleted file mode 100644 index 0bea985dca..0000000000 --- a/module/admin/ui/cache.html.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @package admin - * @link https://www.zentao.net - */ -namespace zin; - -formPanel -( - set::actions - ([ - 'submit', - $config->cache->dao->enable ? ['text' => $lang->admin->clearCache, 'url' => inlink('ajaxClearCache'), 'class' => 'secondary ajax-submit'] : null, - 'cancel' - ]), - formGroup - ( - set::label($lang->admin->daoCache), - radioList - ( - set::name('dao[enable]'), - set::items($lang->admin->cacheStatusList), - set::value($config->cache->dao->enable), - set::inline(true) - ), - span(setClass('ml-4 mt-1.5'), icon('info text-warning mr-2'), $lang->admin->apcuNotice) - ), - helper::isAPCuEnabled() && $this->config->cache->dao->driver == 'Apcu' ? formRow - ( - formGroup - ( - setClass('w-1/2'), - setStyle(array('align-items' => 'center')), - set::label($lang->admin->memory), - progressBar - ( - set::percent($rate), - set::width('100%'), - set::color('rgb(var(--color-' . ($rate <= 50 ? 'success' : ($rate <= 80 ? 'warning' : 'danger')) . '-500-rgb))') - ) - ), - formGroup - ( - setClass('w-1/2 ml-4 gap-4'), - setStyle(array('align-items' => 'center')), - span($rate . '%'), - span(sprintf($lang->admin->usedMemory, $total, $used)) - ) - ) : null -); - -render(); diff --git a/module/admin/zen.php b/module/admin/zen.php index 4132b07d97..18a1309e55 100644 --- a/module/admin/zen.php +++ b/module/admin/zen.php @@ -302,25 +302,4 @@ class adminZen extends admin return $data; } - - /** - * 获取APCu内存使用数据。 - * Get APCu memory data. - * - * @param string $type total|free|used - * @access protected - * @return string|float - */ - public function getAPCuMemory(string $type = 'total'): string|float - { - if(!helper::isAPCuEnabled()) return ''; - - $info = apcu_sma_info(true); - - if($type == 'total') return helper::formatKB($info['seg_size']); - if($type == 'free') return helper::formatKB($info['avail_mem']); - if($type == 'used') return helper::formatKB($info['seg_size'] - $info['avail_mem']); - if($type == 'rate') return round(($info['seg_size'] - $info['avail_mem']) / $info['seg_size'] * 100, 2); - return ''; - } } diff --git a/module/api/js/common.ui.js b/module/api/js/common.ui.js index d386907cc1..2f43491143 100644 --- a/module/api/js/common.ui.js +++ b/module/api/js/common.ui.js @@ -1,4 +1,4 @@ -$('.form-group').on('click', '.btn-add', function() +$(document).off('click', '.form-group .btn-add').on('click', '.form-group .btn-add', function() { let $newRow = $(this).closest('tr').clone(); @@ -26,7 +26,7 @@ $('.form-group').on('click', '.btn-add', function() } }); -$('.form-group').on('click', '.btn-split', function() +$(document).off('click', '.form-group .btn-split').on('click', '.form-group .btn-split', function() { let $newRow = $(this).closest('tr').clone(); $newRow.find('input').val(''); @@ -41,7 +41,7 @@ $('.form-group').on('click', '.btn-split', function() $(this).closest('tr').after($newRow); }); -$('.form-group').on('click', '.btn-delete', function() +$(document).off('click', '.form-group .btn-delete').on('click', '.form-group .btn-delete', function() { if($(this).closest('table').find('.input-row').length == 1) return false; @@ -59,20 +59,24 @@ $('.form-group').on('click', '.btn-delete', function() } }); -$('.params-group').on('keyup', 'input,textarea', function(){ +$(document).off('keyup', '.params-group input, .params-group textarea').on('keyup', '.params-group input, .params-group textarea', function() +{ generateParams($(this)); }) -$('.params-group').on('change', 'input[type=checkbox]', function(){ +$(document).off('change', '.params-group input[type=checkbox]').on('change', '.params-group input[type=checkbox]', function() +{ generateParams($(this)); }) -$('.params-group').on('change', 'select', function(){ +$(document).off('change', '.params-group select').on('change', '.params-group select', function() +{ generateParams($(this)); }) -/* 变更请求类型时,判断是否隐藏拆分按钮. */ -$('.form-group').on('change', '.objectType', function(){ +$(document).off('change', '.form-group .objectType').on('change', '.form-group .objectType', function() +{ + /* 变更请求类型时,判断是否隐藏拆分按钮. */ if($(this).val() != 'array' && $(this).val() != 'object') { $(this).closest('tr').find('.btn-split').addClass('hidden'); @@ -84,12 +88,21 @@ $('.form-group').on('change', '.objectType', function(){ }) /* 请求响应单独绑定事件. */ -$('#form-response').on('keyup', 'input,textarea', function(){generateResponse($(this))}); -$('#form-response').on('change', 'input[type=checkbox]', function(){generateResponse($(this))}); -$('#form-response').on('change', 'select', function(){generateResponse($(this))}); +$(document).off('keyup', '#form-response input, #form-response textarea').on('keyup', '#form-response input, #form-response textarea', function() +{ + generateResponse($(this)); +}); +$(document).off('change', '#form-response input[type=checkbox]').on('change', '#form-response input[type=checkbox]', function() +{ + generateResponse($(this)); +}); +$(document).off('change', '#form-response select').on('change', '#form-response select', function() +{ + generateResponse($(this)); +}); /* 更改请求体类型. */ -$('.params-group').on('change', 'input[type=radio]', function() +$(document).off('change', '.params-group input[type=radio]').on('change', '.params-group input[type=radio]', function() { const isStruct = $(this).closest('div.form-group').hasClass('struct'); if(!isStruct) diff --git a/module/bug/model.php b/module/bug/model.php index 7bf7725dd0..5c7ed9a9e5 100644 --- a/module/bug/model.php +++ b/module/bug/model.php @@ -856,27 +856,29 @@ class bugModel extends model $query = preg_replace('/`(\w+)`/', 't1.`$1`', $query); if($moduleName == 'contributeBug') $bugsAssignedByMe = $this->loadModel('my')->getAssignedByMe($account, null, $orderBy, 'bug'); - return $this->dao->select("t1.*, t2.name AS productName, t2.shadow, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) AS priOrder, IF(t1.`severity` = 0, {$this->config->maxPriValue}, t1.`severity`) AS severityOrder")->from(TABLE_BUG)->alias('t1') - ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') - ->where('t1.deleted')->eq(0) - ->andWhere('t2.deleted')->eq('0') + + $bugs = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) AS priOrder, IF(`severity` = 0, {$this->config->maxPriValue}, `severity`) AS severityOrder")->from(TABLE_BUG) + ->where('deleted')->eq(0) ->beginIF($type == 'bySearch')->andWhere($query)->fi() - ->beginIF($executionID)->andWhere('t1.execution')->eq($executionID)->fi() - ->beginIF($type != 'closedBy' and $this->app->moduleName == 'block')->andWhere('t1.status')->ne('closed')->fi() - ->beginIF($type != 'all' and $type != 'bySearch')->andWhere("t1.`$type`")->eq($account)->fi() - ->beginIF($type == 'bySearch' and $moduleName == 'workBug')->andWhere("t1.assignedTo")->eq($account)->fi() - ->beginIF($type == 'assignedTo' and $moduleName == 'workBug')->andWhere('t1.status')->ne('closed')->fi() + ->beginIF($executionID)->andWhere('execution')->eq($executionID)->fi() + ->beginIF($type != 'closedBy' and $this->app->moduleName == 'block')->andWhere('status')->ne('closed')->fi() + ->beginIF($type != 'all' and $type != 'bySearch')->andWhere("`$type`")->eq($account)->fi() + ->beginIF($type == 'bySearch' and $moduleName == 'workBug')->andWhere("assignedTo")->eq($account)->fi() + ->beginIF($type == 'assignedTo' and $moduleName == 'workBug')->andWhere('status')->ne('closed')->fi() ->beginIF($type == 'bySearch' and $moduleName == 'contributeBug') - ->andWhere('t1.openedBy', 1)->eq($account) - ->orWhere('t1.closedBy')->eq($account) - ->orWhere('t1.resolvedBy')->eq($account) - ->orWhere('t1.id')->in(!empty($bugsAssignedByMe) ? array_keys($bugsAssignedByMe) : array()) + ->andWhere('openedBy', 1)->eq($account) + ->orWhere('closedBy')->eq($account) + ->orWhere('resolvedBy')->eq($account) + ->orWhere('id')->in(!empty($bugsAssignedByMe) ? array_keys($bugsAssignedByMe) : array()) ->markRight(1) ->fi() ->orderBy($orderBy) ->beginIF($limit > 0)->limit($limit)->fi() ->page($pager) ->fetchAll(); + + $this->mao->select('name AS productName, shadow')->from(TABLE_PRODUCT)->into($bugs, 'product'); + return $bugs; } /** diff --git a/module/cache/config/form.php b/module/cache/config/form.php new file mode 100644 index 0000000000..f7c52123e5 --- /dev/null +++ b/module/cache/config/form.php @@ -0,0 +1,8 @@ +cache->form = new stdClass(); +$config->cache->form->setting = array(); +$config->cache->form->setting['enable'] = array('type' => 'int', 'default' => 0); +$config->cache->form->setting['driver'] = array('type' => 'string', 'default' => ''); +$config->cache->form->setting['scope'] = array('type' => 'string', 'default' => ''); +$config->cache->form->setting['namespace'] = array('type' => 'string', 'default' => ''); +$config->cache->form->setting['redis'] = array('type' => 'array', 'default' => array('host' => '', 'port' => 0, 'username' => '', 'password' => '', 'database' => 0, 'serializer' => 'igbinary')); diff --git a/module/cache/control.php b/module/cache/control.php new file mode 100644 index 0000000000..33e382c80b --- /dev/null +++ b/module/cache/control.php @@ -0,0 +1,112 @@ + + * @package cache + * @link https://www.zentao.net + */ +class cache extends control +{ + /** + * 设置是否启用缓存。 + * Set cache enable. + * + * @access public + * @return void + */ + public function setting() + { + if($_POST) + { + $redis = null; + $cache = form::data()->get(); + if(isset($cache->redis)) + { + $redis = (object)$cache->redis; + unset($cache->redis); + } + if(!isset($redis->host)) $redis->host = ''; + if(!isset($redis->port)) $redis->port = ''; + if(!isset($redis->username)) $redis->username = ''; + if(!isset($redis->password)) $redis->password = ''; + if(!isset($redis->database)) $redis->database = ''; + if(!isset($redis->serializer)) $redis->serializer = ''; + + if($cache->enable) + { + $errors = []; + if(empty($cache->driver)) $errors['driver'] = sprintf($this->lang->error->notempty, $this->lang->cache->driver); + if(empty($cache->scope)) $errors['scope'] = sprintf($this->lang->error->notempty, $this->lang->cache->scope); + if(empty($cache->namespace)) $errors['namespace'] = sprintf($this->lang->error->notempty, $this->lang->cache->namespace); + if($cache->driver == 'redis' && empty($redis->host)) $errors['redis[host]'] = sprintf($this->lang->error->notempty, $this->lang->cache->redis->host); + if($cache->driver == 'redis' && empty($redis->port)) $errors['redis[port]'] = sprintf($this->lang->error->notempty, $this->lang->cache->redis->port); + if($cache->driver == 'redis' && empty($redis->database) && $redis->database !== '0') $errors['redis[database]'] = sprintf($this->lang->error->notempty, $this->lang->cache->redis->database); + if($errors) return $this->send(array('result' => 'fail', 'message' => $errors)); + + if($cache->driver == 'apcu') + { + /* 检查是否加载了 APCu 扩展。Check if the APCu extension is loaded. */ + if(!extension_loaded('apcu')) return $this->send(array('result' => 'fail', 'message' => $this->lang->cache->apcu->notLoaded)); + if(!ini_get('apc.enabled')) return $this->send(array('result' => 'fail', 'message' => $this->lang->cache->apcu->notEnabled)); + } + if($cache->driver == 'redis') + { + /* 检查是否加载了 Redis 扩展。Check if the Redis extension is loaded. */ + if(!extension_loaded('redis')) return $this->send(array('result' => 'fail', 'message' => $this->lang->cache->redis->notLoaded)); + if(!extension_loaded('igbinary') && $redis->serializer == 'igbinary') return $this->send(array('result' => 'fail', 'message' => $this->lang->cache->redis->igbinaryNotLoaded)); + + /* 检查 Redis 连接是否正常。Check if the Redis connection is normal. */ + try + { + helper::connectRedis($redis); + } + catch(Exception $e) + { + return $this->send(array('result' => 'fail', 'message' => $e->getMessage())); + } + } + } + + /* 如果缓存配置发生变化,清空缓存。If the cache configuration changes, clear the cache. */ + if($cache->enable != $this->config->cache->enable + || $cache->driver != $this->config->cache->driver + || $cache->namespace != $this->config->cache->namespace + || $redis->database != $this->config->redis->database + || $redis->serializer != $this->config->redis->serializer) + { + $this->cache->clear(); + } + + $this->loadModel('setting')->setItems('system.common.cache', $cache); + if($cache->driver == 'redis') $this->setting->setItems('system.common.redis', $redis); + + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true)); + } + + if($this->config->cache->enable) + { + $this->view->rate = $this->mao->memory('rate'); + $this->view->used = $this->mao->memory('used'); + $this->view->total = $this->mao->memory('total'); + } + + $this->view->title = $this->lang->cache->common; + $this->display(); + } + + /** + * 清除数据缓存。 + * Clear data cache. + * + * @access public + * @return void + */ + public function ajaxClear() + { + $this->cache->clear(); + return $this->send(array('result' => 'success', 'message' => $this->lang->cache->clearSuccess, 'load' => true)); + } +} diff --git a/module/cache/js/setting.ui.js b/module/cache/js/setting.ui.js new file mode 100644 index 0000000000..6a1ba523c8 --- /dev/null +++ b/module/cache/js/setting.ui.js @@ -0,0 +1,11 @@ +function toggleCache() +{ + $('.cache').toggleClass('hidden', $(this).val() != '1'); + toggleDriver.call($('[name=driver]:checked')); +} + +function toggleDriver() +{ + $('.apcu').toggleClass('hidden', $(this).val() != 'apcu'); + $('.redis').toggleClass('hidden', $(this).val() != 'redis'); +} diff --git a/module/cache/lang/de.php b/module/cache/lang/de.php new file mode 100644 index 0000000000..c7435ad974 --- /dev/null +++ b/module/cache/lang/de.php @@ -0,0 +1,45 @@ +cache->clear = 'Clear Cache'; +$lang->cache->clearSuccess = 'Cache cleared successfully.'; +$lang->cache->status = 'Status'; +$lang->cache->driver = 'Cache Type'; +$lang->cache->namespace = 'Namespace'; +$lang->cache->scope = 'Scope'; +$lang->cache->memory = 'Memory'; +$lang->cache->usedMemory = 'Total %s, used %s'; + +$lang->cache->statusList[1] = 'On'; +$lang->cache->statusList[0] = 'Off'; + +$lang->cache->driverList['apcu'] = 'APCu'; +$lang->cache->driverList['redis'] = 'Redis'; + +$lang->cache->scopeList['private'] = 'Exclusively for this application'; +$lang->cache->scopeList['shared'] = 'Shared by multiple applications'; + +$lang->cache->apcu = new stdClass(); +$lang->cache->apcu->notice = 'To use APCu cache, you need to load the APCu extension first.'; +$lang->cache->apcu->notLoaded = 'Please load the APCu extension before enabling cache.'; +$lang->cache->apcu->notEnabled = 'Please enable the apc.enabled option before enabling cach.'; + +$lang->cache->redis = new stdClass(); +$lang->cache->redis->host = 'Redis Host'; +$lang->cache->redis->port = 'Redis Port'; +$lang->cache->redis->username = 'Redis User'; +$lang->cache->redis->password = 'Redis Password'; +$lang->cache->redis->serializer = 'Redis Serializer'; +$lang->cache->redis->notice = 'To use Redis cache, you need to load the Redis extension first.'; +$lang->cache->redis->notLoaded = 'Please load the Redis extension before enabling cach.'; +$lang->cache->redis->igbinaryNotLoaded = 'Please load the igbinary extension before enabling cach.'; + +$lang->cache->redis->serializerList['php'] = 'PHP Serialize'; +$lang->cache->redis->serializerList['igbinary'] = 'igbinary'; + +$lang->cache->redis->tips = new stdClass(); +$lang->cache->redis->tips->host = 'Fill in the domain name or IP address, and do not need to fill in the protocol and port number.'; +$lang->cache->redis->tips->database = 'Fill in the number of the Redis database, the default is 0.'; +$lang->cache->redis->tips->serializer = 'Data needs to be serialized and cached. Changing the serializer will clear the cached data.'; + +$lang->cache->tips = new stdClass(); +$lang->cache->tips->namespace = 'Namespaces are used to prevent cache data conflicts between different applications. Changing the namespace after caching is enabled will clear the cache data.'; +$lang->cache->tips->scope = 'If the cache service is only used by this application, please select "Exclusively for this application", otherwise select "Shared by multiple applications".'; diff --git a/module/cache/lang/en.php b/module/cache/lang/en.php new file mode 100644 index 0000000000..c7435ad974 --- /dev/null +++ b/module/cache/lang/en.php @@ -0,0 +1,45 @@ +cache->clear = 'Clear Cache'; +$lang->cache->clearSuccess = 'Cache cleared successfully.'; +$lang->cache->status = 'Status'; +$lang->cache->driver = 'Cache Type'; +$lang->cache->namespace = 'Namespace'; +$lang->cache->scope = 'Scope'; +$lang->cache->memory = 'Memory'; +$lang->cache->usedMemory = 'Total %s, used %s'; + +$lang->cache->statusList[1] = 'On'; +$lang->cache->statusList[0] = 'Off'; + +$lang->cache->driverList['apcu'] = 'APCu'; +$lang->cache->driverList['redis'] = 'Redis'; + +$lang->cache->scopeList['private'] = 'Exclusively for this application'; +$lang->cache->scopeList['shared'] = 'Shared by multiple applications'; + +$lang->cache->apcu = new stdClass(); +$lang->cache->apcu->notice = 'To use APCu cache, you need to load the APCu extension first.'; +$lang->cache->apcu->notLoaded = 'Please load the APCu extension before enabling cache.'; +$lang->cache->apcu->notEnabled = 'Please enable the apc.enabled option before enabling cach.'; + +$lang->cache->redis = new stdClass(); +$lang->cache->redis->host = 'Redis Host'; +$lang->cache->redis->port = 'Redis Port'; +$lang->cache->redis->username = 'Redis User'; +$lang->cache->redis->password = 'Redis Password'; +$lang->cache->redis->serializer = 'Redis Serializer'; +$lang->cache->redis->notice = 'To use Redis cache, you need to load the Redis extension first.'; +$lang->cache->redis->notLoaded = 'Please load the Redis extension before enabling cach.'; +$lang->cache->redis->igbinaryNotLoaded = 'Please load the igbinary extension before enabling cach.'; + +$lang->cache->redis->serializerList['php'] = 'PHP Serialize'; +$lang->cache->redis->serializerList['igbinary'] = 'igbinary'; + +$lang->cache->redis->tips = new stdClass(); +$lang->cache->redis->tips->host = 'Fill in the domain name or IP address, and do not need to fill in the protocol and port number.'; +$lang->cache->redis->tips->database = 'Fill in the number of the Redis database, the default is 0.'; +$lang->cache->redis->tips->serializer = 'Data needs to be serialized and cached. Changing the serializer will clear the cached data.'; + +$lang->cache->tips = new stdClass(); +$lang->cache->tips->namespace = 'Namespaces are used to prevent cache data conflicts between different applications. Changing the namespace after caching is enabled will clear the cache data.'; +$lang->cache->tips->scope = 'If the cache service is only used by this application, please select "Exclusively for this application", otherwise select "Shared by multiple applications".'; diff --git a/module/cache/lang/fr.php b/module/cache/lang/fr.php new file mode 100644 index 0000000000..c7435ad974 --- /dev/null +++ b/module/cache/lang/fr.php @@ -0,0 +1,45 @@ +cache->clear = 'Clear Cache'; +$lang->cache->clearSuccess = 'Cache cleared successfully.'; +$lang->cache->status = 'Status'; +$lang->cache->driver = 'Cache Type'; +$lang->cache->namespace = 'Namespace'; +$lang->cache->scope = 'Scope'; +$lang->cache->memory = 'Memory'; +$lang->cache->usedMemory = 'Total %s, used %s'; + +$lang->cache->statusList[1] = 'On'; +$lang->cache->statusList[0] = 'Off'; + +$lang->cache->driverList['apcu'] = 'APCu'; +$lang->cache->driverList['redis'] = 'Redis'; + +$lang->cache->scopeList['private'] = 'Exclusively for this application'; +$lang->cache->scopeList['shared'] = 'Shared by multiple applications'; + +$lang->cache->apcu = new stdClass(); +$lang->cache->apcu->notice = 'To use APCu cache, you need to load the APCu extension first.'; +$lang->cache->apcu->notLoaded = 'Please load the APCu extension before enabling cache.'; +$lang->cache->apcu->notEnabled = 'Please enable the apc.enabled option before enabling cach.'; + +$lang->cache->redis = new stdClass(); +$lang->cache->redis->host = 'Redis Host'; +$lang->cache->redis->port = 'Redis Port'; +$lang->cache->redis->username = 'Redis User'; +$lang->cache->redis->password = 'Redis Password'; +$lang->cache->redis->serializer = 'Redis Serializer'; +$lang->cache->redis->notice = 'To use Redis cache, you need to load the Redis extension first.'; +$lang->cache->redis->notLoaded = 'Please load the Redis extension before enabling cach.'; +$lang->cache->redis->igbinaryNotLoaded = 'Please load the igbinary extension before enabling cach.'; + +$lang->cache->redis->serializerList['php'] = 'PHP Serialize'; +$lang->cache->redis->serializerList['igbinary'] = 'igbinary'; + +$lang->cache->redis->tips = new stdClass(); +$lang->cache->redis->tips->host = 'Fill in the domain name or IP address, and do not need to fill in the protocol and port number.'; +$lang->cache->redis->tips->database = 'Fill in the number of the Redis database, the default is 0.'; +$lang->cache->redis->tips->serializer = 'Data needs to be serialized and cached. Changing the serializer will clear the cached data.'; + +$lang->cache->tips = new stdClass(); +$lang->cache->tips->namespace = 'Namespaces are used to prevent cache data conflicts between different applications. Changing the namespace after caching is enabled will clear the cache data.'; +$lang->cache->tips->scope = 'If the cache service is only used by this application, please select "Exclusively for this application", otherwise select "Shared by multiple applications".'; diff --git a/module/cache/lang/zh-cn.php b/module/cache/lang/zh-cn.php new file mode 100644 index 0000000000..5000ad8b57 --- /dev/null +++ b/module/cache/lang/zh-cn.php @@ -0,0 +1,46 @@ +cache->clear = '清除缓存'; +$lang->cache->clearSuccess = '清除成功'; +$lang->cache->status = '缓存状态'; +$lang->cache->driver = '缓存服务'; +$lang->cache->namespace = '命名空间'; +$lang->cache->scope = '服务范围'; +$lang->cache->memory = '内存使用'; +$lang->cache->usedMemory = '总计 %s,已使用 %s'; + +$lang->cache->statusList[1] = '开启'; +$lang->cache->statusList[0] = '关闭'; + +$lang->cache->driverList['apcu'] = 'APCu'; +$lang->cache->driverList['redis'] = 'Redis'; + +$lang->cache->scopeList['private'] = '本应用独享'; +$lang->cache->scopeList['shared'] = '多应用共享'; + +$lang->cache->apcu = new stdClass(); +$lang->cache->apcu->notice = '使用 APCu 缓存需要先加载 APCu 扩展。'; +$lang->cache->apcu->notLoaded = '请加载 APCu 扩展后再开启数据缓存'; +$lang->cache->apcu->notEnabled = '请启用 apc.enabled 选项后再开启数据缓存'; + +$lang->cache->redis = new stdClass(); +$lang->cache->redis->host = 'Redis 主机'; +$lang->cache->redis->port = 'Redis 端口'; +$lang->cache->redis->username = 'Redis 用户名'; +$lang->cache->redis->password = 'Redis 密码'; +$lang->cache->redis->database = 'Redis 数据库'; +$lang->cache->redis->serializer = 'Redis 序列化器'; +$lang->cache->redis->notice = '使用 Redis 缓存需要先加载 Redis 扩展。'; +$lang->cache->redis->notLoaded = '请加载 Redis 扩展后再开启数据缓存。'; +$lang->cache->redis->igbinaryNotLoaded = '请加载 igbinary 扩展后再开启数据缓存。'; + +$lang->cache->redis->serializerList['php'] = 'PHP 内置序列化器'; +$lang->cache->redis->serializerList['igbinary'] = 'igbinary'; + +$lang->cache->redis->tips = new stdClass(); +$lang->cache->redis->tips->host = '填写域名或 IP 地址,无需填写协议和端口号。'; +$lang->cache->redis->tips->database = '填写 Redis 数据库的编号,默认为 0。'; +$lang->cache->redis->tips->serializer = '数据需要序列化后缓存。更改序列化器会清空缓存数据。'; + +$lang->cache->tips = new stdClass(); +$lang->cache->tips->namespace = '命名空间用来防止不同应用间缓存数据冲突。启用缓存后更改命名空间会清空缓存数据。'; +$lang->cache->tips->scope = '如果缓存服务只有本应用使用请选择『本应用独享』,否则选择『多应用共享』。'; diff --git a/module/cache/model.php b/module/cache/model.php new file mode 100644 index 0000000000..4606c39ba8 --- /dev/null +++ b/module/cache/model.php @@ -0,0 +1,29 @@ + + * @package cache + * @link https://www.zentao.net + */ +class cacheModel extends model +{ + /** + * 清空缓存。 + * Clear the cache. + * + * @access public + * @return void + */ + public function clear() + { + /* Redis 采用遍历删除的方式,所以需要先关闭缓存,清空之后再打开。Redis uses the method of traversing deletion, so you need to turn off the cache first, clear it, and then turn it on. */ + $needStop = $this->config->cache->driver == 'redis'; + if($needStop) $this->loadModel('setting')->setItem('system.common.cache.enable', 0); + $this->mao->clearCache(); + if($needStop) $this->setting->setItem('system.common.cache.enable', 1); + } +} diff --git a/module/cache/ui/setting.html.php b/module/cache/ui/setting.html.php new file mode 100644 index 0000000000..177742146d --- /dev/null +++ b/module/cache/ui/setting.html.php @@ -0,0 +1,219 @@ + + * @package cache + * @link https://www.zentao.net + */ +namespace zin; + +$hiddenCache = $config->cache->enable ? '' : ' hidden'; +$hiddenApcu = !$hiddenCache && $config->cache->driver == 'apcu' ? '' : ' hidden'; +$hiddenRedis = !$hiddenCache && $config->cache->driver == 'redis' ? '' : ' hidden'; + +formPanel +( + set::actions + ([ + 'submit', + $config->cache->enable ? ['text' => $lang->cache->clear, 'url' => inlink('ajaxClear'), 'class' => 'secondary ajax-submit'] : null, + 'cancel' + ]), + on::change('input[name=enable]', 'toggleCache'), + on::change('input[name=driver]', 'toggleDriver'), + formGroup + ( + set::label($lang->cache->status), + set::required(), + radioList + ( + set::name('enable'), + set::items($lang->cache->statusList), + set::value($config->cache->enable), + set::inline(true) + ) + ), + formGroup + ( + setClass('cache' . $hiddenCache), + set::label($lang->cache->driver), + set::required(), + radioList + ( + setClass('w-1/3'), + set::name('driver'), + set::items($lang->cache->driverList), + set::value($config->cache->driver), + set::inline(true) + ), + span + ( + setClass('apcu ml-4 mt-1.5' . $hiddenApcu), + icon('info text-warning mr-2'), + $lang->cache->apcu->notice + ), + span + ( + setClass('redis ml-4 mt-1.5' . $hiddenRedis), + icon('info text-warning mr-2'), + $lang->cache->redis->notice + ) + ), + formGroup + ( + setClass('cache' . $hiddenCache), + set::label($lang->cache->scope), + set::required(), + radioList + ( + setClass('w-1/3'), + set::name('scope'), + set::items($lang->cache->scopeList), + set::value($config->cache->scope), + set::inline(true) + ), + span + ( + setClass('ml-4 mt-1.5'), + icon('info text-warning mr-2'), + $lang->cache->tips->scope + ) + ), + formGroup + ( + setClass('cache' . $hiddenCache), + set::label($lang->cache->namespace), + set::required(), + input + ( + setClass('w-1/3'), + set::name('namespace'), + set::value($config->cache->namespace ?: $config->db->name) + ), + span + ( + setClass('ml-4 mt-1.5'), + icon('info text-warning mr-2'), + $lang->cache->tips->namespace + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->host), + set::required(), + input + ( + setClass('w-1/3'), + set::name('redis[host]'), + set::value($config->redis->host) + ), + span + ( + setClass('ml-4 mt-1.5'), + icon('info text-warning mr-2'), + $lang->cache->redis->tips->host + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->port), + set::required(), + input + ( + setClass('w-1/3'), + set::name('redis[port]'), + set::value($config->redis->port) + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->username), + input + ( + setClass('w-1/3'), + set::name('redis[username]'), + set::value($config->redis->username) + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->password), + input + ( + setClass('w-1/3'), + set::name('redis[password]'), + set::type('password'), + set::value($config->redis->password) + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->database), + set::required(), + input + ( + setClass('w-1/3'), + set::type('number'), + set::min(0), + set::step(1), + set::name('redis[database]'), + set::value($config->redis->database) + ), + span + ( + setClass('ml-4 mt-1.5'), + icon('info text-warning mr-2'), + $lang->cache->redis->tips->database + ) + ), + formGroup + ( + setClass('redis' . $hiddenRedis), + set::label($lang->cache->redis->serializer), + radioList + ( + setClass('w-1/3'), + set::name('redis[serializer]'), + set::items($lang->cache->redis->serializerList), + set::value($config->redis->serializer), + set::inline(true) + ), + span + ( + setClass('ml-4 mt-1.5'), + icon('info text-warning mr-2'), + $lang->cache->redis->tips->serializer + ) + ), + $config->cache->enable ? formGroup + ( + setStyle(array('align-items' => 'center')), + set::label($lang->cache->memory), + div + ( + setClass('w-1/3'), + progressBar + ( + set::percent($rate), + set::width('100%'), + set::color('rgb(var(--color-' . ($rate <= 50 ? 'success' : ($rate <= 80 ? 'warning' : 'danger')) . '-500-rgb))') + ) + ), + div + ( + setClass('flex ml-4 gap-4'), + span($rate . '%'), + span(sprintf($lang->cache->usedMemory, $total, $used)) + ) + ) : null +); + +render(); diff --git a/module/common/lang/common.php b/module/common/lang/common.php index a379dec1b9..2f3245e02e 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -1,5 +1,6 @@ common = new stdclass(); +$lang->cache = new stdclass(); $lang->index = new stdclass(); $lang->my = new stdclass(); $lang->todo = new stdclass(); diff --git a/module/common/lang/de.php b/module/common/lang/de.php index e1091172ec..0e567aa41c 100644 --- a/module/common/lang/de.php +++ b/module/common/lang/de.php @@ -204,6 +204,7 @@ $lang->contactUs->wechat = 'Wechat'; $lang->common->common = 'Standard Module'; $lang->common->story = 'Story'; +$lang->cache->common = 'Cache'; $lang->my->common = 'My'; $lang->todo->common = 'Todo'; $lang->block->common = 'InfoBlock'; @@ -441,7 +442,6 @@ $lang->admin->data = 'Data'; $lang->admin->cron = 'Cron'; $lang->admin->buildIndex = 'Full Text Search'; $lang->admin->tableEngine = 'Table Engine'; -$lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; diff --git a/module/common/lang/en.php b/module/common/lang/en.php index c278028015..9b7429a978 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -204,6 +204,7 @@ $lang->contactUs->wechat = 'Wechat'; $lang->common->common = 'Common Module'; $lang->common->story = 'Story'; +$lang->cache->common = 'Cache'; $lang->my->common = 'My'; $lang->todo->common = 'Todo'; $lang->block->common = 'Block'; @@ -441,7 +442,6 @@ $lang->admin->data = 'Data'; $lang->admin->cron = 'Cron'; $lang->admin->buildIndex = 'Full Text Search'; $lang->admin->tableEngine = 'Table Engine'; -$lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; diff --git a/module/common/lang/fr.php b/module/common/lang/fr.php index 661a1c13d3..bba8328d9b 100644 --- a/module/common/lang/fr.php +++ b/module/common/lang/fr.php @@ -204,6 +204,7 @@ $lang->contactUs->wechat = 'Wechat'; $lang->common->common = 'Module Commun'; $lang->common->story = 'Story'; +$lang->cache->common = 'Cache'; $lang->my->common = 'My'; $lang->todo->common = 'Agenda'; $lang->block->common = 'Bloc'; @@ -441,7 +442,6 @@ $lang->admin->data = 'Data'; $lang->admin->cron = 'Cron'; $lang->admin->buildIndex = 'Full Text Search'; $lang->admin->tableEngine = 'Table Engine'; -$lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 2fed65fe52..8ec26c3ced 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -784,6 +784,7 @@ $lang->navGroup->account = 'admin'; $lang->navGroup->ops = 'admin'; $lang->navGroup->service = 'admin'; $lang->navGroup->domain = 'admin'; +$lang->navGroup->cache = 'admin'; $lang->navGroup->aiapp = 'aiapp'; diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index 59f7e45c1f..c3106d6e5a 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -204,6 +204,7 @@ $lang->contactUs->wechat = '微信'; $lang->common->common = '公有模块'; $lang->common->story = '需求'; +$lang->cache->common = '缓存'; $lang->my->common = '地盘'; $lang->todo->common = '待办'; $lang->block->common = '区块'; @@ -441,7 +442,6 @@ $lang->admin->data = '数据'; $lang->admin->cron = '定时'; $lang->admin->buildIndex = '重建索引'; $lang->admin->tableEngine = '表引擎'; -$lang->admin->cache = '缓存'; $lang->convert->importJira = '导入Jira数据'; diff --git a/module/epic/test/ui/page/view.php b/module/epic/test/ui/page/view.php new file mode 100755 index 0000000000..35f2a8cbb3 --- /dev/null +++ b/module/epic/test/ui/page/view.php @@ -0,0 +1,16 @@ + "//*[@class='tab-content']/div/div/div[6]/div[2]/span", + /*激活弹窗的激活按钮*/ + 'activate' => "//*[@type='submit']" + ); + $this->dom->xpath = array_merge($this->dom->xpath, $xpath); + } +} diff --git a/module/execution/test/lib/view.ui.class.php b/module/execution/test/lib/view.ui.class.php new file mode 100644 index 0000000000..2789e03a55 --- /dev/null +++ b/module/execution/test/lib/view.ui.class.php @@ -0,0 +1,100 @@ +initForm('execution', 'view', array('execution' => '3'), 'appIframe-execution'); + if($form->dom->executionName->getText() != $basic['executionName']) return $this->failed('执行名称不正确'); + if($form->dom->programName->getText() != $basic['programName']) return $this->failed('项目集名称不正确'); + if($form->dom->projectName->getText() != $basic['projectName']) return $this->failed('项目名称不正确'); + if($form->dom->storyNum->getText() != $basic['storyNum']) return $this->failed('需求数不正确'); + if($form->dom->taskNum->getText() != $basic['taskNum']) return $this->failed('任务数不正确'); + if($form->dom->bugNum->getText() != $basic['bugNum']) return $this->failed('Bug数不正确'); + return $this->success('执行基础信息正确'); + } + + /** + * 检查执行概况页面产品信息。 + * Check the product information of the execution. + * + * @param string $product + * @access public + * @return object + */ + public function checkProduct($product) + { + $form = $this->initForm('execution', 'view', array('execution' => '3'), 'appIframe-execution'); + if($form->dom->linckedProducta->getText() != $product) return $this->failed('关联产品不正确'); + $form->dom->moreProducts->click(); + $form->wait(1); + $url = $this->response(); + if($url['module'] != 'execution') return $this->failed('页面跳转后module不正确'); + if($url['method'] != 'manageproducts') return $this->failed('页面跳转后method不正确'); + return $this->success('产品信息正确'); + } + + /** + * 检查执行概况页面团队成员信息。 + * Check the team members of the execution. + * + * @param string $membera + * @param string $memberb + * @access public + * @return object + */ + public function checkMember($membera, $memberb) + { + $form = $this->initForm('execution', 'view', array('execution' => '3'), 'appIframe-execution'); + if($form->dom->teamMembera->getText() != $membera) return $this->failed('团队成员不正确'); + if($form->dom->teamMemberb->getText() != $memberb) return $this->failed('团队成员不正确'); + $form->dom->moreTeamMembers->click(); + $form->wait(1); + $url = $this->response(); + if($url['module'] != 'execution') return $this->failed('点击更多按钮,页面跳转后module不正确'); + if($url['method'] != 'team') return $this->failed('点击更多按钮,页面跳转后method不正确'); + $form->dom->btn($this->lang->overview)->click(); + $form->wait(1); + $form->dom->manageMembers->click(); + $form->wait(1); + $url = $this->response(); + if($url['module'] != 'execution') return $this->failed('点击管理按钮,页面跳转后module不正确'); + if($url['method'] != 'manageMembers') return $this->failed('点击管理按钮,页面跳转后method不正确'); + return $this->success('团队成员信息正确'); + } + + /** + * 检查执行概况页面文档库信息。 + * Check the doclib information of the execution. + * + * @param string $doclaba + * @param string $doclibb + * @access public + * @return object + */ + public function checkDoclib($doclaba, $doclibb) + { + $form = $this->initForm('execution', 'view', array('execution' => '3'), 'appIframe-execution'); + if($form->dom->docliba->getText() != $doclaba) return $this->failed('文档库名称不正确'); + if($form->dom->doclibb->getText() != $doclibb) return $this->failed('文档库名称不正确'); + $form->dom->moreDoclibs->click(); + $form->wait(1); + $url = $this->response(); + if($url['module'] != 'execution') return $this->failed('点击更多按钮,页面跳转后module不正确'); + if($url['method'] != 'doc') return $this->failed('点击更多按钮,页面跳转后method不正确'); + $form->dom->btn($this->lang->settings)->click(); + $form->wait(1); + $form->dom->createDoclib->click(); + $form->wait(1); + if(!is_object($form->dom->doclibModal)) return $this->failed('创建文档库模态框不存在'); + return $this->success('文档库信息正确'); + } +} diff --git a/module/execution/test/ui/page/view.php b/module/execution/test/ui/page/view.php index 154cc45e0f..bb2600493e 100755 --- a/module/execution/test/ui/page/view.php +++ b/module/execution/test/ui/page/view.php @@ -7,8 +7,13 @@ class viewPage extends page $xpath = array( /* 执行概况页元素 */ 'executionName' => "//*[@id='mainContent']/div[1]/div[1]/div[2]/div[1]/div", + 'programName' => "//*[@id='mainContent']/div[1]/div[1]/div[2]/div[2]/div[1]/div/a", + 'projectName' => "//*[@id='mainContent']/div[1]/div[1]/div[2]/div[2]/div[2]/a", 'status' => "//*[@id='mainContent']/div[1]/div[1]/div[2]/div[1]/span[2]", 'acl' => "//*[@id='mainContent']/div[1]/div[1]/div[2]/div[1]/span[3]", + 'storyNum' => "//*[@id='mainContent']/div[1]/div[1]/div[1]/div[2]/div[1]/div", + 'taskNum' => "//*[@id='mainContent']/div[1]/div[1]/div[1]/div[2]/div[2]/div", + 'bugNum' => "//*[@id='mainContent']/div[1]/div[1]/div[1]/div[2]/div[3]/div", 'plannedBegin' => "//*[@id='mainContent']/div[2]/div[1]/div/table[3]/tbody/tr/td/div/div[1]/span[2]", 'plannedEnd' => "//*[@id='mainContent']/div[2]/div[1]/div/table[3]/tbody/tr/td/div/div[2]/span[2]", 'realBeganView' => "//*[@id='mainContent']/div[2]/div[1]/div/table[3]/tbody/tr/td/div/div[3]/span[2]", @@ -17,6 +22,16 @@ class viewPage extends page 'linckedProducta' => "//*[@id='mainContent']/div[2]/div[1]/div/table[1]/tbody/tr[1]", 'linckedProductb' => "//*[@id='mainContent']/div[2]/div[1]/div/table[1]/tbody/tr[2]", 'productsNev' => "//*[@data-id='products']", + 'moreProducts' => "//*[@id='mainContent']/div[2]/div[1]/div/table[1]/thead/tr/th[1]/div/a/span", + 'teamMembera' => "//*[@id='mainContent']/div[2]/div[1]/div/table[2]/tbody/tr/td/div/div[1]/span[1]", + 'teamMemberb' => "//*[@id='mainContent']/div[2]/div[1]/div/table[2]/tbody/tr/td/div/div[2]/span[1]", + 'moreTeamMembers' => "//*[@id='mainContent']/div[2]/div[1]/div/table[2]/thead/tr/th/div/a/span", + 'manageMembers' => "//*[@id='mainContent']/div[2]/div[1]/div/table[2]/tbody/tr/td/div/a", + 'docliba' => "//*[@id='mainContent']/div[2]/div[1]/div/table[5]/tbody/tr/td/div/div[1]/a", + 'doclibb' => "//*[@id='mainContent']/div[2]/div[1]/div/table[5]/tbody/tr/td/div/div[2]/a", + 'moreDoclibs' => "//*[@id='mainContent']/div[2]/div[1]/div/table[5]/thead/tr/th/div/a/span", + 'createDoclib' => "//*[@id='mainContent']/div[2]/div[1]/div/table[5]/tbody/tr/td/div/div[3]/a/span", + 'doclibModal' => "//*[@class='modal-dialog']", /* 编辑执行弹窗中元素 */ 'products' => "//*[@name='products[0]']", 'productsTip' => "//*[@id='products[0]Tip']", diff --git a/module/execution/test/ui/view.php b/module/execution/test/ui/view.php new file mode 100644 index 0000000000..b25c843111 --- /dev/null +++ b/module/execution/test/ui/view.php @@ -0,0 +1,147 @@ +#!/usr/bin/env php +id->range('1-100'); +$product->name->range('产品1, 产品2'); +$product->type->range('normal'); +$product->gen(2); + +$project = zenData('project'); +$project->id->range('1-100'); +$project->project->range('0, 0, 2'); +$project->model->range('[], scrum, []'); +$project->type->range('program, project, sprint'); +$project->auth->range('[], extend, []'); +$project->storyType->range('[], story, []'); +$project->parent->range('0, 1, 2'); +$project->path->range('`,1,`, `,1,2,`, `,1,2,3`'); +$project->grade->range('1, 2, 1'); +$project->name->range('项目集, 项目, 执行'); +$project->openedBy->range('user1'); +$project->acl->range('open'); +$project->status->range('doing'); +$project->gen(3); + +$projectproduct = zenData('projectproduct'); +$projectproduct->project->range('2{2}, 3'); +$projectproduct->product->range('1, 2, 1'); +$projectproduct->gen(3); + +$user = zenData('user'); +$user->id->range('1-100'); +$user->dept->range('0'); +$user->account->range('admin, user1, user2'); +$user->realname->range('admin, USER1, USER2'); +$user->password->range($config->uitest->defaultPassword)->format('md5'); +$user->gen(3); + +$team = zenData('team'); +$team->id->range('1-100'); +$team->root->range('2{3}, 3{2}'); +$team->type->range('project{3}, execution{2}'); +$team->account->range('admin, user1, user2, admin, user1'); +$team->gen(5); + +$story = zenData('story'); +$story->id->range('1-100'); +$story->parent->range('0'); +$story->isParent->range('0'); +$story->root->range('1-100'); +$story->path->range('`,1,`, `,2,`, `,3,`'); +$story->grade->range('1'); +$story->product->range('1'); +$story->module->range('0'); +$story->plan->range('0'); +$story->title->range('1-100'); +$story->type->range('story'); +$story->estimate->range('0'); +$story->status->range('active'); +$story->stage->range('projected'); +$story->assignedTo->range('[]'); +$story->version->range('1'); +$story->gen(3); + +$storySpec = zenData('storyspec'); +$storySpec->story->range('1-15'); +$storySpec->version->range('1'); +$storySpec->title->range('1-15'); +$storySpec->gen(3); + +$projectStory = zenData('projectstory'); +$projectStory->project->range('2{3}, 3{2}'); +$projectStory->product->range('1'); +$projectStory->branch->range('0'); +$projectStory->story->range('1-3, 1, 2'); +$projectStory->version->range('1'); +$projectStory->gen(5); + +$task = zenData('task'); +$task->id->range('1-100'); +$task->project->range('2'); +$task->execution->range('3'); +$task->story->range('0'); +$task->storyVersion->range('1'); +$task->name->range('1-100'); +$task->type->range('devel'); +$task->deadline->range(' (-5D)-(-4D):1D, []{11}')->type('timestamp')->format('YY/MM/DD'); +$task->status->range('wait{3}, doing{3}, done{2}, cancel, closed{3}'); +$task->openedBy->range('admin{6}, user1{6}'); +$task->assignedTo->range('[]{3}, user1{4}, admin{2}, closed{3}'); +$task->finishedBy->range('[]{6}, admin, user1, [], admin, user1{2}'); +$task->canceledBy->range('[]{8}, admin, []{3}'); +$task->closedBy->range('[]{9}, admin{2}, user1'); +$task->deleted->range('0{11}, 1'); +$task->gen(12); + +$taskSpec = zenData('taskspec'); +$taskSpec->task->range('1-100'); +$taskSpec->version->range('0'); +$taskSpec->name->range('1-100'); +$taskSpec->gen(12); + +$bug = zenData('bug'); +$bug->id->range('1-100'); +$bug->project->range('2{12}, 0{100}'); +$bug->product->range('1{8}, 2{100}'); +$bug->execution->range('3{5}, 0{100}'); +$bug->title->range('1-100'); +$bug->status->range('active{3}, resolved{3}, closed{100}'); +$bug->assignedTo->range('[]'); +$bug->gen(12); + +$doclib = zenData('doclib'); +$doclib->id->range('1-100'); +$doclib->type->range('project, execution{2}'); +$doclib->product->range('0'); +$doclib->project->range('2'); +$doclib->execution->range('0, 3, 3'); +$doclib->name->range('项目库, 执行库1, 执行库2'); +$doclib->gen(3); + +$tester = new viewTester(); +$tester->login(); + +$basic = array( + 'executionName' => '执行', + 'programName' => '项目集', + 'projectName' => '项目', + 'storyNum' => '2', + 'taskNum' => '11', + 'bugNum' => '5', +); + +r($tester->checkBasic($basic)) && p('status,message') && e('SUCCESS,执行基础信息正确'); +r($tester->checkProduct('产品1')) && p('status,message') && e('SUCCESS,产品信息正确'); +r($tester->checkMember('admin', 'USER1')) && p('status,message') && e('SUCCESS,团队成员信息正确'); +r($tester->checkDoclib('执行库1', '执行库2')) && p('status,message') && e('SUCCESS,文档库信息正确'); +$tester->closeBrowser(); diff --git a/module/group/lang/de.php b/module/group/lang/de.php index 9ed0b99fa0..c6482d5418 100644 --- a/module/group/lang/de.php +++ b/module/group/lang/de.php @@ -601,6 +601,6 @@ $lang->group->package->deleteJob = 'Delete PipeLine'; $lang->group->package->browseApplication = 'Application List'; $lang->group->package->mangeApplication = 'Manage Application'; $lang->group->package->trainPracticeLib = 'Practice Library'; -$lang->group->package->other = 'Other'; +$lang->group->package->application = 'Manage Application'; include (dirname(__FILE__) . '/resource.php'); diff --git a/module/group/lang/en.php b/module/group/lang/en.php index db7ff415f2..1b2860c1c3 100644 --- a/module/group/lang/en.php +++ b/module/group/lang/en.php @@ -601,5 +601,6 @@ $lang->group->package->deleteJob = 'Delete PipeLine'; $lang->group->package->browseApplication = 'Application List'; $lang->group->package->mangeApplication = 'Manage Application'; $lang->group->package->trainPracticeLib = 'Practice Library'; +$lang->group->package->application = 'Manage Application'; include (dirname(__FILE__) . '/resource.php'); diff --git a/module/group/lang/fr.php b/module/group/lang/fr.php index 2e91e5118f..67d53b4068 100644 --- a/module/group/lang/fr.php +++ b/module/group/lang/fr.php @@ -601,5 +601,6 @@ $lang->group->package->deleteJob = 'Delete PipeLine'; $lang->group->package->browseApplication = 'Application List'; $lang->group->package->mangeApplication = 'Manage Application'; $lang->group->package->trainPracticeLib = 'Practice Library'; +$lang->group->package->application = 'Manage Application'; include (dirname(__FILE__) . '/resource.php'); diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index f8010155ec..0397aeeba7 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -1495,6 +1495,12 @@ $lang->resource->system->dashboard = 'dashboard'; $lang->resource->system->dblist = 'dbList'; $lang->resource->system->configdomain = 'configDomain'; $lang->resource->system->ossview = 'ossView'; +$lang->resource->system->browse = 'browse'; +$lang->resource->system->create = 'create'; +$lang->resource->system->edit = 'edit'; +$lang->resource->system->delete = 'delete'; +$lang->resource->system->active = 'active'; +$lang->resource->system->inactive = 'inactive'; $lang->resource->ops = new stdclass(); $lang->resource->ops->provider = 'provider'; diff --git a/module/group/lang/zh-cn.php b/module/group/lang/zh-cn.php index 84391f9f4b..6f1c20ef75 100644 --- a/module/group/lang/zh-cn.php +++ b/module/group/lang/zh-cn.php @@ -601,5 +601,6 @@ $lang->group->package->deleteJob = '删除流水线'; $lang->group->package->browseApplication = '浏览应用'; $lang->group->package->mangeApplication = '管理应用'; $lang->group->package->trainPracticeLib = '实践库'; +$lang->group->package->application = '管理应用'; include (dirname(__FILE__) . '/resource.php'); diff --git a/module/group/packagemanager.php b/module/group/packagemanager.php index b3c2dea6ad..4188d30f9c 100644 --- a/module/group/packagemanager.php +++ b/module/group/packagemanager.php @@ -941,6 +941,17 @@ $config->group->package->releaseNotify->subset = 'release'; $config->group->package->releaseNotify->privs = array(); $config->group->package->releaseNotify->privs['release-notify'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 70, 'depend' => array('release-view'), 'recommend' => array('release-create', 'release-edit', 'release-linkBug', 'release-linkStory', 'release-unlinkBug', 'release-unlinkStory')); +$config->group->package->application = new stdclass(); +$config->group->package->application->order = 30; +$config->group->package->application->subset = 'release'; +$config->group->package->application->privs = array(); +$config->group->package->application->privs['system-browse'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('release-browse'), 'recommend' => array('system-create', 'system-edit', 'system-active', 'system-inactive', 'system-delete')); +$config->group->package->application->privs['system-create'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('system-browse'), 'recommend' => array('system-edit', 'system-active', 'system-inactive', 'system-delete')); +$config->group->package->application->privs['system-edit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('system-browse'), 'recommend' => array('system-create', 'system-active', 'system-inactive', 'system-delete')); +$config->group->package->application->privs['system-active'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('system-browse'), 'recommend' => array('system-create', 'system-edit', 'system-inactive', 'system-delete')); +$config->group->package->application->privs['system-inactive'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('system-browse'), 'recommend' => array('system-create', 'system-edit', 'system-active', 'system-delete')); +$config->group->package->application->privs['system-delete'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array('system-browse'), 'recommend' => array('system-create', 'system-edit', 'system-active', 'system-inactive')); + $config->group->package->projectPlan = new stdclass(); $config->group->package->projectPlan->order = 5; $config->group->package->projectPlan->subset = 'projectplan'; diff --git a/module/install/control.php b/module/install/control.php index 13f48c904b..5a4d53d3c1 100644 --- a/module/install/control.php +++ b/module/install/control.php @@ -381,17 +381,7 @@ class install extends control $this->install->importBIData(); - $this->install->enableDaoCache(); - - /** - * 安装完成后清除缓存。 - * 通过 dao 的 exec 方法更新数据库会自动更新缓存。 - * 通过 dbh 执行 sql 语句的方式更新数据库不会自动更新缓存,应该在清除缓存之前执行,否则可能导致缓存命中但数据已过期。 - * Clear the cache after the installation is complete. - * Update the database through the exec method of dao will automatically update the cache. - * Update the database by executing sql statements through dbh will not automatically update the cache, should be executed before clearing the cache, otherwise it may cause cache hits but the data has expired. - */ - $this->dao->clearCache(); + $this->install->enableCache(); $skipApp = (string)getenv('ZT_SKIP_DEVOPS_INIT'); $link = ($this->config->inQuickon && (!$skipApp || $skipApp == 'false')) ? inlink('app') : inlink('step6'); diff --git a/module/install/model.php b/module/install/model.php index 6d9aa92db6..77c68e1b91 100644 --- a/module/install/model.php +++ b/module/install/model.php @@ -372,7 +372,7 @@ class installModel extends model $config->section = 'global'; $config->key = 'showDemoUsers'; $config->value = '1'; - $this->dao->replace(TABLE_CONFIG)->data($config)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($config)->exec(); return true; } @@ -423,24 +423,27 @@ class installModel extends model } /** - * 开启 dao 缓存。 - * Enable dao cache. + * 开启缓存。 + * Enable cache. * * @access public * @return bool */ - public function enableDaoCache(): bool + public function enableCache(): bool { if(!helper::isAPCuEnabled()) return false; $cache = new stdclass(); - $cache->owner = 'system'; - $cache->module = 'common'; - $cache->section = 'global'; - $cache->key = 'cache'; - $cache->value = '{"dao":{"enable":"1"}}'; - $this->dao->replace(TABLE_CONFIG)->data($cache)->exec(); + $cache->status = true; + $cache->driver = 'apcu'; + $cache->scope = 'shared'; + $cache->namespace = $this->config->db->name; - return !dao::isError(); + $this->loadModel('setting')->setItems('system.common.cache', $cache); + if(dao::isError()) return false; + + $this->mao->clearCache(); + + return true; } } diff --git a/module/instance/config/dtable.php b/module/instance/config/dtable.php index ac793d7c71..fff4d9edac 100644 --- a/module/instance/config/dtable.php +++ b/module/instance/config/dtable.php @@ -50,10 +50,16 @@ if(!isset($config->backup)) $config->backup->dtable->fieldList['actions']['type'] = 'actions'; $config->backup->dtable->fieldList['actions']['width'] = '100'; - $config->backup->dtable->fieldList['actions']['menu'] = array('restore'); + $config->backup->dtable->fieldList['actions']['menu'] = array('restore', 'delete'); $config->backup->dtable->fieldList['actions']['list']['restore']['icon'] = 'icon-restart'; $config->backup->dtable->fieldList['actions']['list']['restore']['hint'] = $lang->instance->restore->common; $config->backup->dtable->fieldList['actions']['list']['restore']['url'] = array('module' => 'instance', 'method' => 'ajaxRestore', 'params' => 'instanceID={instanceId}&backupName={name}'); $config->backup->dtable->fieldList['actions']['list']['restore']['className'] = 'ajax-submit'; + + $config->backup->dtable->fieldList['actions']['list']['delete']['icon'] = 'icon-trash'; + $config->backup->dtable->fieldList['actions']['list']['delete']['hint'] = $lang->instance->backup->delete; + $config->backup->dtable->fieldList['actions']['list']['delete']['url'] = array('module' => 'instance', 'method' => 'ajaxDeleteBackup', 'params' => 'instanceID={instanceId}&backupName={name}'); + $config->backup->dtable->fieldList['actions']['list']['delete']['className'] = 'ajax-submit'; + $config->backup->dtable->fieldList['actions']['list']['delete']['data-confirm'] = array('message' => $lang->instance->backup->confirmDeleteTip, 'icon' => 'icon-exclamation-sign', 'iconClass' => 'warning-pale rounded-full icon-2x'); } diff --git a/module/instance/control.php b/module/instance/control.php index db51cbd2cc..4f698f65a0 100644 --- a/module/instance/control.php +++ b/module/instance/control.php @@ -652,4 +652,24 @@ class instance extends control } return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'backupSuccess'))); } + + /** + * Delete backup by ajax. + * 删除备份。 + * @param int $backupID + * @access public + * @return void + */ + public function ajaxDeleteBackup(string $instanceID, string $backupName) + { + $instance = $this->instance->getByID((int)$instanceID); + if(empty($instance)) $this->send(array('result' => 'success', 'message' => $this->lang->instance->instanceNotExists)); + + $success = $this->instance->deleteBackup($instance, $backupName); + if(!$success) return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'deleteFail'))); + + $this->action->create('instance', $instance->id, 'manualdeletebackup', '', json_encode(array('result' => 'success'))); + $locate = $this->createLink('instance', 'view', 'id=' . $instanceID); + return $this->send(array('result' => 'success', 'load' => array('alert' => zget($this->lang->instance->notices, 'deleteSuccess'), 'locate' => $locate, 'closeModal' => true))); + } } diff --git a/module/instance/lang/de.php b/module/instance/lang/de.php index ebd34a6ae0..fa85304569 100644 --- a/module/instance/lang/de.php +++ b/module/instance/lang/de.php @@ -132,7 +132,8 @@ $lang->instance->backup->keepBackupBySystem = 'The system will only delete expi $lang->instance->backup->backupSize = 'Size'; $lang->instance->backup->confirmTip = 'Are you sure you want to back up?'; $lang->instance->backup->cronRemark = 'DevOps service automatic backup task'; -$lang->instance->backup->backupSettingsTips = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->backupSettingsTip = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->confirmDeleteTip = 'Are you sure you want to delete the backup?'; $lang->instance->backup->cycleList[1] = 'Daily'; diff --git a/module/instance/lang/en.php b/module/instance/lang/en.php index ebd34a6ae0..fc4815035c 100644 --- a/module/instance/lang/en.php +++ b/module/instance/lang/en.php @@ -117,7 +117,7 @@ $lang->instance->backup->volSpentSeconds = 'Time taken (seconds)'; $lang->instance->backup->volSize = 'Size'; $lang->instance->backup->lastRestore = 'Last rollback'; $lang->instance->backup->restoreDate = 'Restore time'; -$lang->instance->backup->latestBackupAt = 'Last backup time'; +$lang->instance->backup->latestBackupAt = 'Last backup time'; $lang->instance->backup->backupBeforeRestore = 'We recommend that you backup before rolling back!'; $lang->instance->backup->enableAutoBackup = 'Enable automatic backup'; $lang->instance->backup->autoBackup = 'Automatic backup'; @@ -132,7 +132,8 @@ $lang->instance->backup->keepBackupBySystem = 'The system will only delete expi $lang->instance->backup->backupSize = 'Size'; $lang->instance->backup->confirmTip = 'Are you sure you want to back up?'; $lang->instance->backup->cronRemark = 'DevOps service automatic backup task'; -$lang->instance->backup->backupSettingsTips = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->backupSettingsTip = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->confirmDeleteTip = 'Are you sure you want to delete the backup?'; $lang->instance->backup->cycleList[1] = 'Daily'; diff --git a/module/instance/lang/fr.php b/module/instance/lang/fr.php index ebd34a6ae0..fa85304569 100644 --- a/module/instance/lang/fr.php +++ b/module/instance/lang/fr.php @@ -132,7 +132,8 @@ $lang->instance->backup->keepBackupBySystem = 'The system will only delete expi $lang->instance->backup->backupSize = 'Size'; $lang->instance->backup->confirmTip = 'Are you sure you want to back up?'; $lang->instance->backup->cronRemark = 'DevOps service automatic backup task'; -$lang->instance->backup->backupSettingsTips = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->backupSettingsTip = 'The system will delete backups that exceed the set number of days, but will retain at least one valid backup.'; +$lang->instance->backup->confirmDeleteTip = 'Are you sure you want to delete the backup?'; $lang->instance->backup->cycleList[1] = 'Daily'; diff --git a/module/instance/lang/zh-cn.php b/module/instance/lang/zh-cn.php index e40e9d8865..fdcad8fde8 100644 --- a/module/instance/lang/zh-cn.php +++ b/module/instance/lang/zh-cn.php @@ -132,7 +132,8 @@ $lang->instance->backup->keepBackupBySystem = '备份数据超过1条时系统 $lang->instance->backup->backupSize = '大小'; $lang->instance->backup->confirmTip = '确认要备份吗?'; $lang->instance->backup->cronRemark = 'Devops服务自动备份任务'; -$lang->instance->backup->backupSettingsTips = '系统会删除超过设置天数的备份,但会保留至少一个有效备份。'; +$lang->instance->backup->backupSettingsTip = '系统会删除超过设置天数的备份,但会保留至少一个有效备份。'; +$lang->instance->backup->confirmDeleteTip = '确认要删除备份吗?'; $lang->instance->backup->cycleList[1] = '每日'; diff --git a/module/instance/model.php b/module/instance/model.php index 2e583a843f..f6199d9d8e 100644 --- a/module/instance/model.php +++ b/module/instance/model.php @@ -1379,4 +1379,17 @@ class instanceModel extends model if(count($deleteData) > 0) $this->action->create('instance', $instance->id, 'deleteexpiredbackup', '', json_encode(array('result' => 'success', 'data' =>$deleteData))); return true; } + + /** + * Delete backup. + * 删除备份。 + * @param object $instance + * @param string $backupName + * @access public + * @return bool + */ + public function deleteBackup($instance, $backupName) + { + return $this->cne->deleteBackup($instance, $backupName); + } } diff --git a/module/instance/ui/backupsettings.html.php b/module/instance/ui/backupsettings.html.php index 50d5ab52e0..428aeb0dd5 100644 --- a/module/instance/ui/backupsettings.html.php +++ b/module/instance/ui/backupsettings.html.php @@ -28,7 +28,7 @@ formPanel set::label($this->lang->instance->backup->keepDays), set::control('input'), set::value((int)$instance->backupKeepDays), - span($this->lang->instance->backup->backupSettingsTips, set::className('text-warning inline-block mt-2')) + span($this->lang->instance->backup->backupSettingsTip, set::className('text-warning inline-block mt-2')) ), ), formRow diff --git a/module/mail/tao.php b/module/mail/tao.php index ad132a1332..91310e905f 100644 --- a/module/mail/tao.php +++ b/module/mail/tao.php @@ -237,7 +237,7 @@ class mailTao extends mailModel $objectModel = $this->loadModel($objectType); if(!$objectModel) return false; - if(strpos(',story,meeting,review,', ",{$objectType},") !== false) return $objectModel->getToAndCcList($object, $action->action); + if(strpos(',story,meeting,review,deploy,', ",{$objectType},") !== false) return $objectModel->getToAndCcList($object, $action->action); if($objectType == 'ticket') return $objectModel->getToAndCcList($object, $action); return $objectModel->getToAndCcList($object); } diff --git a/module/mark/model.php b/module/mark/model.php new file mode 100644 index 0000000000..d70dd22e00 --- /dev/null +++ b/module/mark/model.php @@ -0,0 +1,86 @@ + + * @package mail + * @link https://www.zentao.net + */ +?> +dao->select('objectID, version')->from(TABLE_MARK) + ->where('objectType')->eq($objectType) + ->andWhere('objectID')->in($objectIDs) + ->beginIF($version != 'all')->andWhere('version')->eq($version)->fi() + ->andWhere('account')->eq($this->app->user->account) + ->andWhere('mark')->eq($mark) + ->fetchAll(); + } + + public function getMarks(array $objects, string $objectType, string $mark): array + { + $objectIDs = array_column($objects, 'id'); + $marks = $this->getNeededMarks($objectIDs, $objectType, 'all', $mark); + + foreach($objects as $object) + { + $objectMarks = array_filter($marks, function($mark) use($object) + { + return $mark->objectID == $object->id && $mark->version == $object->version; + }); + + $object->mark = !empty($objectMarks); + } + + return $objects; + } + + /** + * 设置对象的标记。 + * Set object marks. + * + * @param array $objectIDs + * @param string $objectType + * @param string $version + * @param string $mark + * @param string $extra + * @access public + * @return bool + */ + public function setMark(array $objectIDs, string $objectType, string $version, string $mark, string $extra = ''): bool + { + $data = new stdclass(); + $data->objectType = $objectType; + $data->version = $version; + $data->account = $this->app->user->account; + $data->mark = $mark; + $data->extra = $extra; + $data->date = helper::now(); + + foreach($objectIDs as $objectID) + { + $data->objectID = $objectID; + $this->dao->insert(TABLE_MARK)->data($data)->autocheck()->exec(); + } + + return dao::isError(); + } +} diff --git a/module/message/model.php b/module/message/model.php index 92196c58b3..05627bd702 100755 --- a/module/message/model.php +++ b/module/message/model.php @@ -271,7 +271,7 @@ class messageModel extends model if($toList == 'closed') $toList = ''; if($objectType == 'feedback' && $object->status == 'replied') $toList = ',' . $object->openedBy . ','; - if(strpos(',story,epic,requirement,ticket,review,', ",{$objectType},") !== false && $actionID) + if(strpos(',story,epic,requirement,ticket,review,deploy,', ",{$objectType},") !== false && $actionID) { $action = $this->loadModel('action')->getById($actionID); list($toList, $ccList) = $this->loadModel($objectType)->getToAndCcList($object, $action->action); diff --git a/module/pivot/lang/de.php b/module/pivot/lang/de.php index 7ee11f89b3..f9a79ab539 100644 --- a/module/pivot/lang/de.php +++ b/module/pivot/lang/de.php @@ -26,6 +26,7 @@ $lang->pivot->exportRange = 'Export Range'; $lang->pivot->story = 'Story'; $lang->pivot->clear = 'Clear'; $lang->pivot->keep = 'Keep'; +$lang->pivot->new = 'New'; $lang->pivot->accessDenied = 'You do not have access to this pivot'; $lang->pivot->acl = 'Access Control'; @@ -490,3 +491,6 @@ $lang->pivot->drill->productName = "Product Name"; $lang->pivot->drill->activatedBug = "Activated Bug Count"; $lang->pivot->drill->auto = "Auto"; $lang->pivot->drill->designChangedTip = 'Setting columns field change, please check'; + +$lang->pivot->tipNewVersion = 'Has New Version!'; +$lang->pivot->checkNewVersion = 'Click to check'; diff --git a/module/pivot/lang/en.php b/module/pivot/lang/en.php index 02fcf9cf61..8c1c976cab 100644 --- a/module/pivot/lang/en.php +++ b/module/pivot/lang/en.php @@ -26,6 +26,7 @@ $lang->pivot->exportRange = 'Export Range'; $lang->pivot->story = 'Story'; $lang->pivot->clear = 'Clear'; $lang->pivot->keep = 'Keep'; +$lang->pivot->new = 'New'; $lang->pivot->accessDenied = 'You do not have access to this pivot'; $lang->pivot->acl = 'Access Control'; @@ -490,3 +491,6 @@ $lang->pivot->drill->productName = "Product Name"; $lang->pivot->drill->activatedBug = "Activated Bug Count"; $lang->pivot->drill->auto = "Auto"; $lang->pivot->drill->designChangedTip = 'Setting columns field change, please check'; + +$lang->pivot->tipNewVersion = 'Has New Version!'; +$lang->pivot->checkNewVersion = 'Click to check'; diff --git a/module/pivot/lang/fr.php b/module/pivot/lang/fr.php index b8014fd9e3..6d35995d34 100644 --- a/module/pivot/lang/fr.php +++ b/module/pivot/lang/fr.php @@ -26,6 +26,7 @@ $lang->pivot->exportRange = 'Export Range'; $lang->pivot->story = 'Story'; $lang->pivot->clear = 'Clear'; $lang->pivot->keep = 'Keep'; +$lang->pivot->new = 'New'; $lang->pivot->accessDenied = 'You do not have access to this pivot'; $lang->pivot->acl = 'Access Control'; @@ -490,3 +491,6 @@ $lang->pivot->drill->productName = "Product Name"; $lang->pivot->drill->activatedBug = "Activated Bug Count"; $lang->pivot->drill->auto = "Auto"; $lang->pivot->drill->designChangedTip = 'Setting columns field change, please check'; + +$lang->pivot->tipNewVersion = 'Has New Version!'; +$lang->pivot->checkNewVersion = 'Click to check'; diff --git a/module/pivot/lang/zh-cn.php b/module/pivot/lang/zh-cn.php index b796089257..3d9e740176 100644 --- a/module/pivot/lang/zh-cn.php +++ b/module/pivot/lang/zh-cn.php @@ -26,6 +26,7 @@ $lang->pivot->exportRange = '导出范围'; $lang->pivot->story = '需求'; $lang->pivot->clear = '清空'; $lang->pivot->keep = '保留设计'; +$lang->pivot->new = '新'; $lang->pivot->accessDenied = '您无权访问该透视表'; $lang->pivot->acl = '访问控制'; @@ -490,3 +491,6 @@ $lang->pivot->drill->productName = "产品名称"; $lang->pivot->drill->activatedBug = "激活的Bug数"; $lang->pivot->drill->auto = "自动"; $lang->pivot->drill->designChangedTip = '设计变更,请检查'; + +$lang->pivot->tipNewVersion = '有新版本更新'; +$lang->pivot->checkNewVersion = '点击查看'; diff --git a/module/pivot/tao.php b/module/pivot/tao.php index eda310c83d..6671366256 100644 --- a/module/pivot/tao.php +++ b/module/pivot/tao.php @@ -12,10 +12,14 @@ class pivotTao extends pivotModel */ protected function fetchPivot(int $id): object|bool { - return $this->dao->select('*')->from(TABLE_PIVOT) - ->where('id')->eq($id) - ->andWhere('deleted')->eq('0') - ->fetch(); + $pivot = $this->dao->select('*')->from(TABLE_PIVOT)->where('id')->eq($id)->andWhere('deleted')->eq('0')->fetch(); + if(!$pivot) return false; + + $specData = $this->dao->select('*')->from(TABLE_PIVOTSPEC)->where('pivot')->eq($id)->andWhere('version')->eq($pivot->version)->fetch(); + if(!$specData) return $pivot; + + foreach($specData as $specKey => $specValue) $pivot->$specKey = $specValue; + return $pivot; } /** * 获取产品列表。 diff --git a/module/pivot/ui/show.html.php b/module/pivot/ui/show.html.php index 61b389151e..b9dd261304 100644 --- a/module/pivot/ui/show.html.php +++ b/module/pivot/ui/show.html.php @@ -83,14 +83,26 @@ $generateData = function() use ($lang, $pivotName, $pivot, $data, $configs, $sho set::shadow(false), set::headingClass('h-12'), set::bodyClass('pt-0'), - $pivot->desc ? to::titleSuffix( - icon + to::titleSuffix + ( + $pivot->desc ? icon ( setClass('cursor-pointer'), setData(array('toggle' => 'tooltip', 'title' => $pivot->desc, 'placement' => 'right', 'className' => 'text-gray border border-light', 'type' => 'white')), 'help' + ) : null, + span + ( + set::style(array('font-weight' => 'normal')), + $lang->pivot->tipNewVersion . $lang->comma, + h::a + ( + $lang->pivot->checkNewVersion, + set('data-toggle', 'modal'), + set::url($this->createLink('pivot', 'versions', "pivotID={$pivot->id}")) + ) ) - ) : null, + ), toolbar ( item diff --git a/module/pivot/zen.php b/module/pivot/zen.php index 5ede438925..db7b1ca587 100644 --- a/module/pivot/zen.php +++ b/module/pivot/zen.php @@ -80,6 +80,7 @@ class pivotZen extends pivot $groups = $this->pivot->getGroupsByDimensionAndPath($dimensionID, $currentGroup->path); if(!$groups) return array(); + $firstAction = $this->loadModel('action')->getAccountFirstAction($this->app->user->account); $menus = array(); foreach($groups as $group) { @@ -87,6 +88,7 @@ class pivotZen extends pivot $pivots = $this->pivot->getAllPivotByGroupID($group->id); $pivots = $this->pivot->filterInvisiblePivot($pivots); + $pivots = $this->loadModel('mark')->getMarks($pivots, 'pivot', 'view'); if(empty($pivots)) continue; if($group->grade > 1) $menus[] = (object)array('id' => $group->id, 'parent' => 0, 'name' => $group->name); @@ -95,6 +97,7 @@ class pivotZen extends pivot foreach($pivots as $pivot) { + $this->setNewMark($pivot, $firstAction); $params = helper::safe64Encode("groupID={$group->id}&pivotID={$pivot->id}"); $url = inlink('preview', "dimension={$dimensionID}&group={$currentGroup->id}&method=show¶ms={$params}"); $menus[] = (object)array('id' => $group->id . '_' . $pivot->id, 'parent' => $group->grade > 1 ? $group->id : 0, 'name' => $pivot->name, 'url' => $url); @@ -108,6 +111,21 @@ class pivotZen extends pivot return array_merge($menus, $builtinMenus); } + /** + * 设置 “新” 标签。 + * Set new mark. + * + * @param object $pivot + * @param object $firstAction + * @access protected + * @return void + */ + protected function setNewMark(object $pivot, object $firstAction): void + { + if(!$pivot->mark && $pivot->createdDate < $firstAction->date) $pivot->mark = true; + if(!$pivot->mark) $pivot->name = array('html' => $pivot->name . ' ' . $this->lang->pivot->new . ''); + } + /** * 在第一个维度上显示内置透视表。 * Display the built-in pivots in the first dimension. diff --git a/module/product/control.php b/module/product/control.php index 5fc357c5fb..89f4cc9f0a 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -843,6 +843,14 @@ class product extends control $tracks['cols'] = $cols; } + $tasks = array(); + foreach($tracks['items'] as $lane) + { + $taskIdList = array_column($lane['task'], 'id'); + $taskList = array_combine($taskIdList, $lane['task']); + $tasks += $taskList; + } + /* Build search form. */ $this->productZen->buildSearchFormForTrack($productID, $branch, $projectID, $browseType, $param, $storyType); @@ -862,6 +870,7 @@ class product extends control $this->view->storyTypeList = $storyTypeList; $this->view->users = $this->loadModel('user')->getPairs('noletter|nodeleted'); $this->view->projectProducts = $this->product->getProductPairsByProject($projectID); + $this->view->tasks = $tasks; $this->display(); } diff --git a/module/product/js/track.ui.js b/module/product/js/track.ui.js index 12712924ad..da0809ba2b 100644 --- a/module/product/js/track.ui.js +++ b/module/product/js/track.ui.js @@ -103,7 +103,7 @@ window.itemRender = function(info) const col = info.col; if(col == 'task') { - if(info.item.parent > '0') info.item.className.push('hidden childTask parent-' + info.item.parent); + if(info.item.parent > '0' && tasks[info.item.parent] !== undefined) info.item.className.push('hidden childTask parent-' + info.item.parent); if(info.item.parent == '-1') info.item.className.push('parentTask'); } diff --git a/module/product/ui/track.html.php b/module/product/ui/track.html.php index f88bf15021..7203b671cd 100644 --- a/module/product/ui/track.html.php +++ b/module/product/ui/track.html.php @@ -118,6 +118,7 @@ jsVar('orderByTitle', $orderByTitle); jsVar('storyType', $storyType); jsVar('users', $users); jsVar('privs', $privs); +jsVar('tasks', $tasks); empty($tracks) ? div(setClass('dtable-empty-tip bg-white shadow'), span(setClass('text-gray'), $lang->noData)) : div ( diff --git a/module/project/test/lib/bug.ui.class.php b/module/project/test/lib/bug.ui.class.php index 763c3de027..cbe2d8da95 100644 --- a/module/project/test/lib/bug.ui.class.php +++ b/module/project/test/lib/bug.ui.class.php @@ -129,4 +129,24 @@ class bugTester extends tester if($form->dom->status->getText() == '已解决') return $this->success('解决Bug成功'); return $this->failed('解决Bug失败'); } + + /** + * 关闭bug。 + * Close bug. + * + * @access public + * @return object + */ + public function closeBug() + { + $form = $this->initForm('project', 'bug', array('project' => 1), 'appIframe-project'); + $form->dom->closeBtn->click(); + $title = $form->dom->closeTitle->getText(); + $form->dom->close->click(); + $form->wait(1); + $form->dom->search(array("{$this->lang->bug->name},=,{$title}")); + $form->wait(1); + if($form->dom->status->getText() == '已关闭') return $this->success('关闭Bug成功'); + return $this->failed('关闭Bug失败'); + } } diff --git a/module/project/test/ui/closebug.php b/module/project/test/ui/closebug.php new file mode 100755 index 0000000000..540d83d8b0 --- /dev/null +++ b/module/project/test/ui/closebug.php @@ -0,0 +1,68 @@ +#!/usr/bin/env php +id->range('1-2'); +$product->name->range('产品1, 产品2'); +$product->type->range('normal'); +$product->gen(2); + +$project = zenData('project'); +$project->id->range('1'); +$project->project->range('0'); +$project->model->range('scrum'); +$project->type->range('project'); +$project->auth->range('extend'); +$project->storytype->range('`story,epic,requirement`'); +$project->path->range('`,1,`'); +$project->grade->range('1'); +$project->name->range('敏捷项目1'); +$project->hasProduct->range('1'); +$project->status->range('wait'); +$project->acl->range('open'); +$project->gen(1); + +$projectProduct = zenData('projectproduct'); +$projectProduct->project->range('1'); +$projectProduct->product->range('1{1}, 2{1}'); +$projectProduct->gen(2); + +$bug = zenData('bug'); +$bug->id->range('1-10'); +$bug->project->range('1'); +$bug->product->range('1{2}, 2{2}'); +$bug->execution->range('0'); +$bug->title->range('Bug1, Bug2, Bug3, Bug4'); +$bug->status->range('active{2}, resolved{2}'); +$bug->assignedTo->range('[]'); +$bug->gen(4); + +$team = zendata('team'); +$team->id->range('1'); +$team->root->range('1'); +$team->type->range('project'); +$team->account->range('admin'); +$team->join->range('(-2M)-(-M):1D')->type('timestamp')->format('YY/MM/DD'); +$team->gen(1); + +$tester = new bugTester(); +$tester->login(); + +r($tester->closeBug()) && p('status,message') && e('SUCCESS,关闭Bug成功'); + +$tester->closeBrowser(); diff --git a/module/project/test/ui/page/bug.php b/module/project/test/ui/page/bug.php index 5159cc7a06..4dfd8093b0 100644 --- a/module/project/test/ui/page/bug.php +++ b/module/project/test/ui/page/bug.php @@ -20,8 +20,16 @@ class bugPage extends page 'assignToAdmin' => "//*[@class='popover show fade dropdown in']/menu/menu/li[1]/a/div/div", 'confirmBtn' => "//*[@id='table-project-bug']/div[2]/div[3]/div/div[1]/div/nav/a[1]", 'firstConfirm' => "//*[@id='table-project-bug']/div[2]/div[2]/div/div[6]/div", + 'firstAssign' => "//*[@id='table-project-bug']/div[2]/div[2]/div/div[7]/div/a/span", + 'secondCheckbox' => "//*[@id='table-project-bug']/div[2]/div[1]/div/div[3]/div/div/label", + 'batchAssignTo' => "//*[@id='table-project-bug']/div[3]/nav[1]/button/span[2]", + 'assignToAdmin' => "//*[@class='popover show fade dropdown in']/menu/menu/li[1]/a/div/div", + 'confirmBtn' => "//*[@id='table-project-bug']/div[2]/div[3]/div/div[1]/div/nav/a[1]", + 'firstConfirm' => "//*[@id='table-project-bug']/div[2]/div[2]/div/div[6]/div", 'resolveBtn' => "//*[@id='table-project-bug']/div[2]/div[3]/div/div[2]/div/nav/a[1]/i", 'secondStatus' => "//*[@id='table-project-bug']/div[2]/div[2]/div/div[11]/div/span", + 'closeBtn' => "//*[@id='table-project-bug']/div[2]/div[3]/div/div[3]/div/nav/a[1]/i", + 'activeBtn' => "//*[@id='table-project-bug']/div[2]/div[3]/div/div[4]/div/nav/a[1]/i", /* 指派页面 */ 'assignTo' => "//*[@id='assignedTo']/div/input", 'submitBtn' => "//*[@class='form load-indicator form-ajax no-morph form-horz']/div[4]/div/button/span", @@ -32,9 +40,13 @@ class bugPage extends page 'resolution' => "//*[@name='resolution']", 'build' => "//*[@name='resolvedBuild']", 'resolve' => "//*[@class='toolbar form-actions form-group no-label']/button/span", - 'resolveTitle' => "//*[@class='modal modal-async load-indicator modal-trans show in']/div/div/div[1]/div/div[2]/span[1]", + 'resolveTitle' => "//*[@class='modal modal-async load-indicator modal-trans show in']/div/div/div[1]/div/div[2]/span[1]", /* 搜索列表 */ 'status' => "//*[@id='table-project-bug']/div[2]/div[2]/div/div[3]/div/span", + 'resolveTitle' => "//*[@class='modal modal-async load-indicator modal-trans show in']/div/div/div[1]/div/div[2]/span[1]", + /* 关闭页面 */ + 'close' => "//*[@class='form load-indicator form-ajax no-morph form-horz']/div[2]/div/button", + 'closeTitle' => "//*[@class='modal modal-async load-indicator modal-trans show in']/div/div/div[1]/div/div[2]/span[1]", ); $this->dom->xpath = array_merge($this->dom->xpath, $xpath); } diff --git a/module/projectstory/test/lib/checktab.ui.class.php b/module/projectstory/test/lib/checktab.ui.class.php new file mode 100755 index 0000000000..e7bd2d8b70 --- /dev/null +++ b/module/projectstory/test/lib/checktab.ui.class.php @@ -0,0 +1,24 @@ +initForm('projectstory', 'story', array('project' => '1'), 'appIframe-project'); + $tabs = array('allTab', 'unclosedTab', 'draftTab', 'reviewingTab', 'changingTab'); + if(!in_array($tab, $tabs)) $form->dom->moreTab->click(); + $form->dom->$tab->click(); + $form->wait(2); + if($form->dom->num->getText() == $expectNum) return $this->success($tab . '下显示条数正确'); + return $this->failed($tab . '下显示条数不正确'); + } +} diff --git a/module/projectstory/test/ui/checktab.php b/module/projectstory/test/ui/checktab.php new file mode 100755 index 0000000000..a94961f53e --- /dev/null +++ b/module/projectstory/test/ui/checktab.php @@ -0,0 +1,113 @@ +#!/usr/bin/env php +id->range('1'); +$product->name->range('产品1'); +$product->type->range('normal'); +$product->gen(1); + +$project = zenData('project'); +$project->id->range('1-2'); +$project->project->range('0,1'); +$project->model->range('scrum'); +$project->type->range('project,sprint'); +$project->auth->range('extend,[]'); +$project->storytype->range('`story,epic,requirement`,`story`'); +$project->path->range('`,1,`, `,1,2,`'); +$project->grade->range('1'); +$project->name->range('项目1,执行1'); +$project->hasProduct->range('1'); +$project->status->range('wait'); +$project->acl->range('open'); +$project->gen(2); + +$projectProduct = zenData('projectproduct'); +$projectProduct->project->range('1'); +$projectProduct->product->range('1'); +$projectProduct->gen(1); + +$story = zenData('story'); +$story->id->range('1-10'); +$story->parent->range('0'); +$story->isParent->range('0'); +$story->root->range('1-10'); +$story->path->range('`,1,`, `,2,`, `,3,`, `,4,`, `,5,`, `,6,`, `,7,`, `,8,`, `,9,`, `,10,`'); +$story->grade->range('1'); +$story->product->range('1'); +$story->module->range('0'); +$story->plan->range('0'); +$story->title->range('需求001,需求002,需求003,需求004,需求005,需求006,需求007,需求008,需求009,需求010'); +$story->type->range('story'); +$story->estimate->range('0'); +$story->status->range('active{3}, closed{1}, reviewing{2}, draft{1}, changing{3}'); +$story->stage->range('projected'); +$story->assignedTo->range('[]'); +$story->version->range('1'); +$story->gen(10); + +$storySpec = zenData('storyspec'); +$storySpec->story->range('1-10'); +$storySpec->version->range('1'); +$storySpec->title->range('1-10'); +$storySpec->gen(10); + +$projectStory = zenData('projectstory'); +$projectStory->project->range('1{10},2{1}'); +$projectStory->product->range('1'); +$projectStory->branch->range('0'); +$projectStory->story->range('1-10,3'); +$projectStory->version->range('1'); +$projectStory->order->range('1{10},2{1}'); +$projectStory->gen(11); + +$tester = new checkTabTester(); +$tester->login(); + +/* 标签统计 */ +r($tester->checkTab('allTab', '10')) && p('message,status') && e('allTab下显示条数正确,SUCCESS'); //检查全部标签下显示条数 +r($tester->checkTab('unclosedTab', '9')) && p('message,status') && e('unclosedTab下显示条数正确,SUCCESS'); //检查未关闭标签下显示条数 +r($tester->checkTab('draftTab', '1')) && p('message,status') && e('draftTab下显示条数正确,SUCCESS'); //检查草稿标签下显示条数 +r($tester->checkTab('reviewingTab', '2')) && p('message,status') && e('reviewingTab下显示条数正确,SUCCESS'); //检查评审中标签下显示条数 +r($tester->checkTab('changingTab', '3')) && p('message,status') && e('changingTab下显示条数正确,SUCCESS'); //检查变更中标签下显示条数 +r($tester->checkTab('closedTab', '1')) && p('message,status') && e('closedTab下显示条数正确,SUCCESS'); //检查已关闭标签下显示条数 +r($tester->checkTab('linkedExecutionTab', '1')) && p('message,status') && e('linkedExecutionTab下显示条数正确,SUCCESS'); //检查已关联执行标签下显示条数 +r($tester->checkTab('unlinkedExecutionTab', '9')) && p('message,status') && e('unlinkedExecutionTab下显示条数正确,SUCCESS'); //检查未关联执行标签下显示条数 + +$tester->closeBrowser(); diff --git a/module/projectstory/test/ui/page/story.php b/module/projectstory/test/ui/page/story.php new file mode 100755 index 0000000000..65bd65aa0e --- /dev/null +++ b/module/projectstory/test/ui/page/story.php @@ -0,0 +1,22 @@ + "//*[@id='main']/div/div[1]/div[1]/menu/li[1]/a/span[1]", + 'unclosedTab' => "//*[@id='main']/div/div[1]/div[1]/menu/li[2]/a/span[1]", + 'draftTab' => "//*[@id='main']/div/div[1]/div[1]/menu/li[3]/a/span[1]", + 'reviewingTab' => "//*[@id='main']/div/div[1]/div[1]/menu/li[4]/a/span[1]", + 'changingTab' => "//*[@id='main']/div/div[1]/div[1]/menu/li[5]/a/span[1]", + 'moreTab' => "//*[@id='main']/div/div[1]/div[1]/menu/li[6]/a/span[1]", + 'closedTab' => "//*[@id='more']/menu/menu/li[1]/a/div/div", + 'linkedExecutionTab' => "//*[@id='more']/menu/menu/li[2]/a/div/div", + 'unlinkedExecutionTab' => "//*[@id='more']/menu/menu/li[3]/a/div/div", + 'num' => "//*[@id='stories_table']/div/div[3]/div[2]/strong[1]" + ); + $this->dom->xpath = array_merge($this->dom->xpath, $xpath); + } +} diff --git a/module/release/ui/browse.html.php b/module/release/ui/browse.html.php index 677acb8983..40c4806dc1 100644 --- a/module/release/ui/browse.html.php +++ b/module/release/ui/browse.html.php @@ -24,7 +24,7 @@ featureBar $canCreateRelease = hasPriv('release', 'create') && common::canModify('product', $product); $canManageSystem = hasPriv('system', 'browse') && common::canModify('product', $product); if($canCreateRelease) $createItem = array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->release->create, 'url' => $this->createLink('release', 'create', "productID={$product->id}&branch={$branch}")); -if($canManageSystem) $manageSystemItem = array('icon' => 'plus', 'class' => 'primary', 'text' => $lang->release->manageSystem, 'url' => $this->createLink('system', 'browse', "productID={$product->id}&branch={$branch}"), 'data-app' => 'product'); +if($canManageSystem) $manageSystemItem = array('class' => 'ghost', 'text' => $lang->release->manageSystem, 'url' => $this->createLink('system', 'browse', "productID={$product->id}&branch={$branch}"), 'data-app' => 'product'); toolbar ( !empty($manageSystemItem) ? item(set($manageSystemItem)) : null, diff --git a/module/setting/model.php b/module/setting/model.php index c2f1281ee8..83295c8ed1 100644 --- a/module/setting/model.php +++ b/module/setting/model.php @@ -53,8 +53,17 @@ class settingModel extends model $item = $this->parseItemPath($path); if(empty($item)) return false; + $vision = zget($item, 'vision', ''); + $item->value = strval($value); - $this->dao->replace(TABLE_CONFIG)->data($item)->exec(); + $this->dao->delete()->from(TABLE_CONFIG) + ->where('owner')->eq($item->owner) + ->andWhere('module')->eq($item->module) + ->andWhere('section')->eq($item->section) + ->andWhere('`key`')->eq($item->key) + ->beginIF($vision)->andWhere('vision')->eq($vision)->fi() + ->exec(); + $this->dao->insert(TABLE_CONFIG)->data($item)->exec(); return !dao::isError(); } @@ -226,7 +235,8 @@ class settingModel extends model $params['section'] = isset($params['section']) ? $params['section'] : ''; $params['key'] = isset($params['key']) ? $params['key'] : ''; - return $this->dao->$method('*')->from(TABLE_CONFIG)->where('1 = 1') + $access = $method == 'select' ? $this->mao : $this->dao; + return $access->$method('*')->from(TABLE_CONFIG)->where('1 = 1') ->beginIF($params['vision'])->andWhere('vision')->in($params['vision'])->fi() ->beginIF($params['owner'])->andWhere('owner')->in($params['owner'])->fi() ->beginIF($params['module'])->andWhere('module')->in($params['module'])->fi() @@ -244,8 +254,8 @@ class settingModel extends model */ public function getSysAndPersonalConfig(string $account = ''): array { - $owner = 'system,' . ($account ? $account : ''); - $records = $this->dao->select('*')->from(TABLE_CONFIG) + $owner = 'system,' . ($account ? $account : ''); + $records = $this->mao->select('*')->from(TABLE_CONFIG) ->where('owner')->in($owner) ->beginIF(!$this->app->upgrading)->andWhere('vision')->in(array('', $this->config->vision))->fi() ->orderBy('id') diff --git a/module/story/tao.php b/module/story/tao.php index 3c3bcf0cd7..e3c4b96756 100644 --- a/module/story/tao.php +++ b/module/story/tao.php @@ -1900,6 +1900,33 @@ class storyTao extends storyModel return $story; } + /** + * 检查操作按钮的触发条件。 + * Check action conditions. + * + * @param string $method + * @param object $story + * @access protected + * @return bool + */ + protected function checkConditions(string $method, object $story) + { + static $flowActions = []; + if(empty($flowActions)) $flowActions = $this->loadModel('workflowaction')->getList($story->type); + $this->loadModel('flow'); + + $isClickable = true; + foreach($flowActions as $flowAction) + { + if($flowAction->action == $method && $flowAction->extensionType != 'none' && $flowAction->status == 'enable' && !empty($flowAction->conditions)) + { + $isClickable = $this->flow->checkConditions($flowAction->conditions, $story); + } + } + + return $isClickable; + } + /** * 构建需求列表中的操作按钮。 * Build action buttons on the browse page. @@ -1962,7 +1989,7 @@ class storyTao extends storyModel /* Change button. */ $canChange = common::hasPriv($story->type, 'change') && $this->isClickable($story, 'change'); $title = $canChange ? $lang->story->change : $this->lang->story->changeTip; - if(common::hasPriv($story->type, 'change')) $actions[] = array('name' => 'change', 'url' => $canChange ? $changeLink : null, 'hint' => $title, 'disabled' => !$canChange, 'class' => 'story-change-btn'); + if(common::hasPriv($story->type, 'change') && $this->checkConditions('change', $story)) $actions[] = array('name' => 'change', 'url' => $canChange ? $changeLink : null, 'hint' => $title, 'disabled' => !$canChange, 'class' => 'story-change-btn'); /* Submitreview, review, recall buttons. */ if(strpos('draft,changing', $story->status) !== false) diff --git a/module/story/test/lib/activatestory.ui.class.php b/module/story/test/lib/activatestory.ui.class.php index bd4db7ff27..76339d2333 100644 --- a/module/story/test/lib/activatestory.ui.class.php +++ b/module/story/test/lib/activatestory.ui.class.php @@ -33,6 +33,28 @@ class activateStoryTester extends tester $viewPage = $this->loadPage('story', 'view'); //进入需求详情页查看状态是否与关闭前一致 if($viewPage->dom->status->getText() != $status) return $this->failed('激活需求后状态不正确'); - return $this->success('激活需求成功'); + return $this->success('激活研发需求成功'); + } + + /** + * check the information after activate an epic + * @param string $storyID + * @access public + * @return object + */ + public function activateEpic($storyID, $status) + { + $form = $this->initform('epic', 'view', array('id' => $storyID), 'appIframe-product'); //进入业务需求详情页 + $form->dom->btn($this->lang->story->activate)->click(); //点击激活需求按钮 + $form->wait(1); + + $form->dom->assignedTo->picker('admin'); //选择指派人 + $form->dom->activate->click(); //点击激活按钮 + $form->wait(1); + + $viewPage = $this->loadPage('epic', 'view'); //进入需求详情页查看状态是否与关闭前一致 + if($viewPage->dom->status->getText() != $status) return $this->failed('激活需求后状态不正确'); + + return $this->success('激活业务需求成功'); } } diff --git a/module/story/test/ui/activatestory.php b/module/story/test/ui/activatestory.php index e58c26b455..83b0830c97 100755 --- a/module/story/test/ui/activatestory.php +++ b/module/story/test/ui/activatestory.php @@ -24,42 +24,44 @@ $product->vision->range('rnd'); $product->gen(1); $story = zenData('story'); -$story->id->range('1-2'); -$story->root->range('1-2'); -$story->path->range('`,1,`, `,2,`'); +$story->id->range('1-6'); +$story->root->range('1-6'); +$story->path->range('`,1,`, `,2,`, `,3,`, `,4,`, `,5,`, `,6,`'); $story->grade->range('1'); $story->product->range('1'); $story->module->range('0'); -$story->title->range('激活研发需求, 草稿研发需求'); -$story->type->range('story'); +$story->title->range('激活研发需求, 草稿研发需求, 激活用户需求, 草稿用户需求, 激活业务需求, 草稿业务需求'); +$story->type->range('story{2}, requirement{2}, epic{2}'); $story->stage->range('closed'); $story->status->range('closed'); $story->openedBy->range('admin'); $story->version->range('1'); -$story->gen(2); +$story->gen(6); $storyspec = zenData('storyspec'); -$storyspec->story->range('1-2'); +$storyspec->story->range('1-6'); $storyspec->version->range('1'); -$storyspec->title->range('激活研发需求, 草稿研发需求'); -$storyspec->gen(2); +$storyspec->title->range('激活研发需求, 草稿研发需求, 激活用户需求, 草稿用户需求, 激活业务需求, 草稿业务需求'); +$storyspec->gen(6); $action = zenData('action'); -$action->id->range('1-2'); +$action->id->range('1-6'); $action->objectType->range('story'); -$action->objectID->range('1-2'); +$action->objectID->range('1-6'); $action->product->range('`,1`'); $action->actor->range('admin'); $action->action->range('closed'); $action->date->range('(-2D)-(-D):60m')->type('timestamp')->format('YY/MM/DD hh:mm:ss'); $action->extra->range('Done|active, Done|draft'); -$action->gen(2); +$action->gen(6); $tester = new activateStoryTester(); $tester->login(); $stutus = array('激活', '草稿'); -r($tester->activateStory(1, $stutus[0])) && p('message') && e('激活需求成功'); -r($tester->activateStory(2, $stutus[1])) && p('message') && e('激活需求成功'); +r($tester->activateStory(1, $stutus[0])) && p('message') && e('激活研发需求成功'); +r($tester->activateStory(2, $stutus[1])) && p('message') && e('激活研发需求成功'); +r($tester->activateEpic(5, $stutus[0])) && p('message') && e('激活业务需求成功'); +r($tester->activateEpic(6, $stutus[1])) && p('message') && e('激活业务需求成功'); $tester->closeBrowser(); diff --git a/module/system/config.php b/module/system/config.php index 060869a42c..6d8e9df604 100644 --- a/module/system/config.php +++ b/module/system/config.php @@ -2,6 +2,12 @@ global $lang; $config->system = new stdclass(); +$config->system->create = new stdclass(); +$config->system->create->requiredFields = 'name'; + +$config->system->edit = new stdclass(); +$config->system->edit->requiredFields = 'name'; + $config->system->groupPrivs = array(); $config->system->groupPrivs['dashboard'] = 'backup|index'; $config->system->groupPrivs['deletebackup'] = 'backup|delete'; diff --git a/module/system/config/dtable.php b/module/system/config/dtable.php index 74030c5906..2f76a3ea1b 100644 --- a/module/system/config/dtable.php +++ b/module/system/config/dtable.php @@ -20,15 +20,17 @@ $config->system->dtable->fieldList['latestRelease']['title'] = $lang->system->la $config->system->dtable->fieldList['latestRelease']['name'] = 'latestRelease'; $config->system->dtable->fieldList['latestRelease']['type'] = 'text'; -$config->system->dtable->fieldList['children']['title'] = $lang->system->children; -$config->system->dtable->fieldList['children']['name'] = 'children'; -$config->system->dtable->fieldList['children']['type'] = 'text'; +$config->system->dtable->fieldList['children']['title'] = $lang->system->children; +$config->system->dtable->fieldList['children']['name'] = 'children'; +$config->system->dtable->fieldList['children']['type'] = 'text'; +$config->system->dtable->fieldList['children']['delimiter'] = ','; -$config->system->dtable->fieldList['status']['title'] = $lang->system->status; -$config->system->dtable->fieldList['status']['name'] = 'status'; -$config->system->dtable->fieldList['status']['type'] = 'status'; -$config->system->dtable->fieldList['status']['map'] = $lang->system->statusList; -$config->system->dtable->fieldList['status']['sortType'] = true; +$config->system->dtable->fieldList['status']['title'] = $lang->system->status; +$config->system->dtable->fieldList['status']['name'] = 'status'; +$config->system->dtable->fieldList['status']['type'] = 'status'; +$config->system->dtable->fieldList['status']['statusMap'] = $lang->system->statusList; +$config->system->dtable->fieldList['status']['width'] = 100; +$config->system->dtable->fieldList['status']['sortType'] = true; $config->system->dtable->fieldList['actions']['name'] = 'actions'; $config->system->dtable->fieldList['actions']['title'] = $lang->actions; diff --git a/module/system/config/form.php b/module/system/config/form.php index df5d69ecb8..15a5ef0ab9 100644 --- a/module/system/config/form.php +++ b/module/system/config/form.php @@ -3,3 +3,14 @@ $config->system->form = new stdclass(); $config->system->form->editDomain['customDomain'] = array('type' => 'string', 'required' => true, 'default' => ''); $config->system->form->editDomain['certPem'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->system->form->editDomain['certKey'] = array('type' => 'string', 'required' => false, 'default' => ''); + +$config->system->form->create['integrated'] = array('type' => 'int', 'required' => false, 'default' => 0); +$config->system->form->create['name'] = array('type' => 'string', 'required' => true, 'filter' => 'trim'); +$config->system->form->create['children'] = array('type' => 'array', 'required' => false, 'default' => array(), 'filter' => 'join'); +$config->system->form->create['desc'] = array('type' => 'string', 'required' => false, 'default' => '', 'filter' => 'trim'); +$config->system->form->create['createdDate'] = array('type' => 'datetime', 'required' => false, 'default' => helper::now()); + +$config->system->form->edit['name'] = array('type' => 'string', 'required' => true, 'filter' => 'trim'); +$config->system->form->edit['children'] = array('type' => 'array', 'required' => false, 'default' => array(), 'filter' => 'join'); +$config->system->form->edit['desc'] = array('type' => 'string', 'required' => false, 'default' => '', 'filter' => 'trim'); +$config->system->form->edit['editedDate'] = array('type' => 'datetime', 'required' => false, 'default' => helper::now()); diff --git a/module/system/control.php b/module/system/control.php index 095679ad0f..4bbc0dce4d 100644 --- a/module/system/control.php +++ b/module/system/control.php @@ -542,10 +542,143 @@ class system extends control $this->app->loadClass('pager', true); $pager = pager::init($recTotal, $recPerPage, $pageID); - $this->view->title = $this->lang->system->browse; - $this->view->appList = $this->system->getList($orderBy, $pager); - $this->view->orderBy = $orderBy; - $this->view->pager = $pager; + $systems = $this->system->getList($orderBy, $pager); + foreach($systems as &$system) + { + $system->latestRelease = $system->latestRelease ? $system->latestRelease : ''; + } + + $this->view->title = $this->lang->system->browse; + $this->view->productID = $productID; + $this->view->appList = $systems; + $this->view->appPairs = $this->system->getPairs(); + $this->view->orderBy = $orderBy; + $this->view->pager = $pager; $this->display(); } + + /** + * 创建应用。 + * Create application. + * + * @param int $productID + * @access public + * @return void + */ + public function create(int $productID) + { + if($_POST) + { + $integrated = $this->post->integrated; + if($integrated) $this->config->system->create->requiredFields .= ',children'; + + $formData = form::data($this->config->system->form->create) + ->setDefault('product', $productID) + ->setDefault('status', 'active') + ->setDefault('createdBy', $this->app->user->account) + ->setIF($integrated == '0', 'children', '') + ->get(); + + $systemID = $this->system->create($formData); + if(dao::isError()) return $this->sendError(dao::getError()); + + if($systemID) $this->loadModel('action')->create('system', $systemID, 'created'); + $this->sendSuccess(array('load' => true)); + } + + $this->view->title = $this->lang->system->create; + $this->view->systemList = $this->system->getPairs('0'); + $this->display(); + } + + /** + * 编辑应用。 + * Edit application. + * + * @param int $id + * @access public + * @return void + */ + public function edit(int $id) + { + $system = $this->system->fetchByID($id); + if($_POST) + { + $integrated = $system->integrated; + if($integrated) $this->config->system->edit->requiredFields .= ',children'; + + $formData = form::data($this->config->system->form->edit) + ->setDefault('editedBy', $this->app->user->account) + ->setDefault('integrated', $integrated) + ->setDefault('editedBy', $this->app->user->account) + ->get(); + + $this->system->update($id, $formData); + if(dao::isError()) return $this->sendError(dao::getError()); + + $this->loadModel('action')->create('system', $id, 'edited'); + $this->sendSuccess(array('load' => true)); + } + + $this->view->title = $this->lang->system->edit; + $this->view->system = $system; + $this->view->systemList = $this->system->getPairs('0'); + $this->display(); + } + + /** + * 上架应用。 + * Active application. + * + * @param int $id + * @access public + * @return void + */ + public function active(int $id) + { + $system = new stdclass(); + $system->status = 'active'; + + $this->system->update($id, $system, false); + if(dao::isError()) return $this->sendError(dao::getError()); + + $this->loadModel('action')->create('system', $id, 'active'); + $this->sendSuccess(array('load' => true)); + } + + /** + * 下架应用。 + * Inactive application. + * + * @param int $id + * @access public + * @return void + */ + public function inactive(int $id) + { + $system = new stdclass(); + $system->status = 'inactive'; + + $this->system->update($id, $system, false); + if(dao::isError()) return $this->sendError(dao::getError()); + + $this->loadModel('action')->create('system', $id, 'inactive'); + $this->sendSuccess(array('load' => true)); + } + + /** + * 删除应用。 + * Delete application. + * + * @param int $id + * @access public + * @return void + */ + public function delete(int $id) + { + $this->system->delete(TABLE_SYSTEM, $id); + + if(dao::isError()) return $this->sendError(dao::getError()); + $this->sendSuccess(array('load' => true)); + } } diff --git a/module/system/lang/de.php b/module/system/lang/de.php index fa5b35a2b3..29d4e65961 100644 --- a/module/system/lang/de.php +++ b/module/system/lang/de.php @@ -30,10 +30,12 @@ $lang->system->serviceQuantity = 'Number of services'; $lang->system->cpuUsage = 'CPU(Core)'; $lang->system->memUsage = 'Memory(GB)'; $lang->system->name = 'Application name'; +$lang->system->integrated = 'Integrated application'; $lang->system->latestRelease = 'Latest version'; $lang->system->children = 'Included applications'; $lang->system->latestRelease = 'Latest version'; $lang->system->status = 'Status'; +$lang->system->desc = 'Description'; $lang->system->browse = 'Application list'; $lang->system->create = 'Create application'; $lang->system->edit = 'Edit application'; @@ -41,6 +43,10 @@ $lang->system->delete = 'Delete application'; $lang->system->active = 'Online application'; $lang->system->inactive = 'Offline application'; +$lang->system->integratedList = array(); +$lang->system->integratedList[0] = 'No'; +$lang->system->integratedList[1] = 'Yes'; + $lang->system->statusList = array(); $lang->system->statusList['active'] = 'Active'; $lang->system->statusList['inactive'] = 'Inactive'; diff --git a/module/system/lang/en.php b/module/system/lang/en.php index fa5b35a2b3..29d4e65961 100644 --- a/module/system/lang/en.php +++ b/module/system/lang/en.php @@ -30,10 +30,12 @@ $lang->system->serviceQuantity = 'Number of services'; $lang->system->cpuUsage = 'CPU(Core)'; $lang->system->memUsage = 'Memory(GB)'; $lang->system->name = 'Application name'; +$lang->system->integrated = 'Integrated application'; $lang->system->latestRelease = 'Latest version'; $lang->system->children = 'Included applications'; $lang->system->latestRelease = 'Latest version'; $lang->system->status = 'Status'; +$lang->system->desc = 'Description'; $lang->system->browse = 'Application list'; $lang->system->create = 'Create application'; $lang->system->edit = 'Edit application'; @@ -41,6 +43,10 @@ $lang->system->delete = 'Delete application'; $lang->system->active = 'Online application'; $lang->system->inactive = 'Offline application'; +$lang->system->integratedList = array(); +$lang->system->integratedList[0] = 'No'; +$lang->system->integratedList[1] = 'Yes'; + $lang->system->statusList = array(); $lang->system->statusList['active'] = 'Active'; $lang->system->statusList['inactive'] = 'Inactive'; diff --git a/module/system/lang/fr.php b/module/system/lang/fr.php index fa5b35a2b3..29d4e65961 100644 --- a/module/system/lang/fr.php +++ b/module/system/lang/fr.php @@ -30,10 +30,12 @@ $lang->system->serviceQuantity = 'Number of services'; $lang->system->cpuUsage = 'CPU(Core)'; $lang->system->memUsage = 'Memory(GB)'; $lang->system->name = 'Application name'; +$lang->system->integrated = 'Integrated application'; $lang->system->latestRelease = 'Latest version'; $lang->system->children = 'Included applications'; $lang->system->latestRelease = 'Latest version'; $lang->system->status = 'Status'; +$lang->system->desc = 'Description'; $lang->system->browse = 'Application list'; $lang->system->create = 'Create application'; $lang->system->edit = 'Edit application'; @@ -41,6 +43,10 @@ $lang->system->delete = 'Delete application'; $lang->system->active = 'Online application'; $lang->system->inactive = 'Offline application'; +$lang->system->integratedList = array(); +$lang->system->integratedList[0] = 'No'; +$lang->system->integratedList[1] = 'Yes'; + $lang->system->statusList = array(); $lang->system->statusList['active'] = 'Active'; $lang->system->statusList['inactive'] = 'Inactive'; diff --git a/module/system/lang/zh-cn.php b/module/system/lang/zh-cn.php index e41e94fa2c..329ad58731 100644 --- a/module/system/lang/zh-cn.php +++ b/module/system/lang/zh-cn.php @@ -30,10 +30,12 @@ $lang->system->serviceQuantity = '服务数'; $lang->system->cpuUsage = 'CPU(核)'; $lang->system->memUsage = '内存(GB)'; $lang->system->name = '应用名称'; +$lang->system->integrated = '集成应用'; $lang->system->latestRelease = '最新版本号'; $lang->system->children = '包含应用'; $lang->system->latestRelease = '最新版本号'; $lang->system->status = '状态'; +$lang->system->desc = '描述'; $lang->system->browse = '应用列表'; $lang->system->create = '创建应用'; $lang->system->edit = '编辑应用'; @@ -41,6 +43,10 @@ $lang->system->delete = '删除应用'; $lang->system->active = '上架应用'; $lang->system->inactive = '下架应用'; +$lang->system->integratedList = array(); +$lang->system->integratedList[0] = '否'; +$lang->system->integratedList[1] = '是'; + $lang->system->statusList = array(); $lang->system->statusList['active'] = '已上架'; $lang->system->statusList['inactive'] = '已下架'; diff --git a/module/system/model.php b/module/system/model.php index 2071e677ea..2270186625 100644 --- a/module/system/model.php +++ b/module/system/model.php @@ -31,6 +31,62 @@ class systemModel extends model ->fetchAll(); } + /** + * 获取应用键值对。 + * Get app pairs. + * + * @param string $integrated + * @access public + * @return array + */ + public function getPairs(string $integrated = ''): array + { + return $this->dao->select('id, name')->from(TABLE_SYSTEM) + ->where('deleted')->eq('0') + ->beginIF($integrated !== '')->andWhere('integrated')->eq($integrated)->fi() + ->fetchPairs('id', 'name'); + } + + /** + * 创建应用。 + * Create an app. + * + * @param object $formData + * @access public + * @return bool|int + */ + public function create(object $formData): bool|int + { + $this->dao->insert(TABLE_SYSTEM)->data($formData) + ->check('name', 'unique') + ->batchCheck($this->config->system->create->requiredFields, 'notempty') + ->autoCheck() + ->exec(); + + if(dao::isError()) return false; + return $this->dao->lastInsertID(); + } + + /** + * 编辑应用。 + * Edit an app. + * + * @param int $id + * @param object $formData + * @access public + * @return bool + */ + public function update(int $id, object $formData, bool $checkRequired = true): bool + { + $this->dao->update(TABLE_SYSTEM)->data($formData) + ->check('name', 'unique', '`id` != ' . $id) + ->autoCheck() + ->beginIF($checkRequired)->batchCheck($this->config->system->edit->requiredFields, 'notempty')->fi() + ->where('id')->eq($id) + ->exec(); + return !dao::isError(); + } + /** * 获取自定义的域名设置。 * Get customized domain settings. @@ -339,4 +395,20 @@ class systemModel extends model return false; } + + /** + * 检查按钮是否可用。 + * Check if the button is clickable. + * + * @param object $system + * @param string $action + * @access public + * @return bool + */ + public function isClickable(object $system, string $action): bool + { + if($action == 'active') return $system->status == 'inactive'; + if($action == 'inactive') return $system->status == 'active'; + return true; + } } diff --git a/module/system/test/model/create.php b/module/system/test/model/create.php new file mode 100755 index 0000000000..fcab3e8dfa --- /dev/null +++ b/module/system/test/model/create.php @@ -0,0 +1,45 @@ +#!/usr/bin/env php +gen(0); + +/** + +title=测试 systemModel::create(); +timeout=0 +cid=1 + +- 默认创建 @1 +- 重复创建第name条的0属性 @『应用名称』已经有『应用10』这条记录了。如果您确定该记录已删除,请到后台-系统设置-回收站还原。 +- 创建空名称失败第name条的0属性 @『应用名称』不能为空。 +- 创建非数字失败第product条的0属性 @『product』应当是数字。 +- 创建非法状态失败第status条的0属性 @『状态』不符合格式,应当为:『/active|inactive/』。 +*/ +global $tester; +$system = $tester->loadModel('system'); + +$default = new stdclass(); +$default->name = '应用10'; +$default->product = 1; +$default->integrated = 0; +$default->children = '1'; +$default->status = 'active'; +$default->desc = '应用描述'; +$default->createdBy = 'admin'; +$default->createdDate = '2024-01-01 00:00:00'; + +r($system->create($default)) && p() && e('1'); // 默认创建 +$system->create($default); +r(dao::getError()) && p('name:0') && e('『应用名称』已经有『应用10』这条记录了。如果您确定该记录已删除,请到后台-系统设置-回收站还原。'); // 重复创建 +$default->name = ''; +$system->create($default); +r(dao::getError()) && p('name:0') && e('『应用名称』不能为空。'); // 创建空名称失败 +$default->name = '应用11'; +$default->product = '字符串'; +$system->create($default); +r(dao::getError()) && p('product:0') && e('『product』应当是数字。'); // 创建非数字失败 +$default->product = 1; +$default->status = 'test'; +$system->create($default); +r(dao::getError()) && p('status:0') && e('『状态』不符合格式,应当为:『/active|inactive/』。'); // 创建非法状态失败 diff --git a/module/system/test/model/getlist.php b/module/system/test/model/getlist.php index 610f65a401..f98b571511 100755 --- a/module/system/test/model/getlist.php +++ b/module/system/test/model/getlist.php @@ -10,6 +10,11 @@ title=测试 systemModel::getList(); timeout=0 cid=1 +- 查询默认排序应用第0条的name属性 @应用10 +- 获取所有应用数量 @10 +- 查询默认排序应用下的状态第0条的status属性 @active +- 查询ID正序排序应用第0条的name属性 @应用1 +- 查询状态倒序排序应用第0条的name属性 @应用1 */ global $tester; diff --git a/module/system/test/model/getpairs.php b/module/system/test/model/getpairs.php new file mode 100755 index 0000000000..a670569428 --- /dev/null +++ b/module/system/test/model/getpairs.php @@ -0,0 +1,26 @@ +#!/usr/bin/env php +gen(10); + +/** +title=测试 systemModel::getPairs(); +timeout=0 +cid=1 + +- 查询默认键值对属性1 @应用1 +- 查询默认键值对数量 @10 +- 查询非集成应用属性1 @应用1 +- 查询集成应用属性2 @应用2 +- 查询集成应用数量 @5 + +*/ +global $tester; +$system = $tester->loadModel('system'); + +r($system->getPairs()) && p('1') && e('应用1'); // 查询默认键值对 +r(count($system->getPairs())) && p() && e('10'); // 查询默认键值对数量 +r($system->getPairs('0')) && p('1') && e('应用1'); // 查询非集成应用 +r($system->getPairs('1')) && p('2') && e('应用2'); // 查询集成应用 +r(count($system->getPairs('1'))) && p() && e('5'); // 查询集成应用数量 diff --git a/module/system/test/model/isclickable.php b/module/system/test/model/isclickable.php new file mode 100644 index 0000000000..7810f3567e --- /dev/null +++ b/module/system/test/model/isclickable.php @@ -0,0 +1,38 @@ +#!/usr/bin/env php +loadModel('system'); + +$default = new stdclass(); +$default->status = 'active'; + +$action = array('active', 'inactive', 'edit', 'delete'); + +r($system->isClickable($default, $action[0])) && p() && e('0'); // 上架状态,不显示上键按钮 +r($system->isClickable($default, $action[1])) && p() && e('1'); // 上架状态,显示下键按钮 +r($system->isClickable($default, $action[2])) && p() && e('1'); // 编辑按钮正常展示 +r($system->isClickable($default, $action[3])) && p() && e('1'); // 删除按钮正常展示 + +$default->status = 'inactive'; +r($system->isClickable($default, $action[0])) && p() && e('1'); // 下架状态,显示上键按钮 +r($system->isClickable($default, $action[1])) && p() && e('0'); // 下架状态,不显示下键按钮 +r($system->isClickable($default, $action[2])) && p() && e('1'); // 编辑按钮正常展示 +r($system->isClickable($default, $action[3])) && p() && e('1'); // 删除按钮正常展示 diff --git a/module/system/test/model/update.php b/module/system/test/model/update.php new file mode 100755 index 0000000000..bcf2e76bf4 --- /dev/null +++ b/module/system/test/model/update.php @@ -0,0 +1,42 @@ +#!/usr/bin/env php +gen(10); + +/** + +title=测试 systemModel::update(); +timeout=0 +cid=1 + +- 默认修改应用 @1 +- 修改重名提示第name条的0属性 @『应用名称』已经有『应用2』这条记录了。如果您确定该记录已删除,请到后台-系统设置-回收站还原。 +- 修改空提示第name条的0属性 @『应用名称』不能为空。 +- 修改非数字第product条的0属性 @『product』应当是数字。 +- 修改非法状态第status条的0属性 @『状态』不符合格式,应当为:『/active|inactive/』。 +*/ +global $tester; +$system = $tester->loadModel('system'); + +$default = new stdclass(); +$default->name = '应用11'; +$default->desc = '应用描述'; +$default->editedBy = 'admin'; +$default->editedDate = '2024-01-01 00:00:00'; + +r($system->update(1, $default)) && p() && e('1'); // 默认修改应用 +$default->name = '应用2'; +$system->update(1, $default); +r(dao::getError()) && p('name:0') && e('『应用名称』已经有『应用2』这条记录了。如果您确定该记录已删除,请到后台-系统设置-回收站还原。'); // 修改重名提示 +$default->name = ''; +$system->update(1, $default); +r(dao::getError()) && p('name:0') && e('『应用名称』不能为空。'); // 修改空提示 +$default->name = '应用11'; +$default->product = '字符串'; +$system->update(1, $default); +r(dao::getError()) && p('product:0') && e('『product』应当是数字。'); // 修改非数字 +$default->status = 1; +$default->product = 1; +$system->update(1, $default, false); +r(dao::getError()) && p('status:0') && e('『状态』不符合格式,应当为:『/active|inactive/』。'); // 修改非法状态 diff --git a/module/system/ui/browse.html.php b/module/system/ui/browse.html.php index 848b8fb48b..91476d96ee 100644 --- a/module/system/ui/browse.html.php +++ b/module/system/ui/browse.html.php @@ -11,9 +11,10 @@ declare(strict_types=1); namespace zin; $canCreate = hasPriv('system', 'create'); -$createLink = $this->createLink('system', 'create'); +$createLink = $this->createLink('system', 'create', 'productID=' . $productID); $createItem = array('text' => $lang->system->create, 'url' => $createLink, 'class' => 'primary', 'icon' => 'plus', 'data-toggle' => 'modal'); +$config->system->dtable->fieldList['children']['map'] = $appPairs; $tableData = initTableData($appList, $config->system->dtable->fieldList, $this->system); featureBar @@ -35,7 +36,7 @@ dtable ( set::cols($config->system->dtable->fieldList), set::data($tableData), - set::sortLink(createLink('system', 'browse', "orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}")), + set::sortLink(createLink('system', 'browse', "productID={$productID}&branch={$branch}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}")), set::orderBy($orderBy), set::footPager(usePager()) ); diff --git a/module/system/ui/create.html.php b/module/system/ui/create.html.php new file mode 100644 index 0000000000..3c3a8407e8 --- /dev/null +++ b/module/system/ui/create.html.php @@ -0,0 +1,55 @@ + + * @package system + * @link https://www.zentao.net + */ +namespace zin; + +$width = common::checkNotCN() ? '3/4' : '1/2'; +formPanel +( + set::id('systemCreateForm'), + set::title($lang->system->create), + set::submitBtnText($lang->save), + set::labelWidth(common::checkNotCN() ? '160px' : '100px'), + formGroup + ( + set::name('integrated'), + set::width('1/2'), + set::label($lang->system->integrated), + set::control('radioListInline'), + set::value(0), + set::items($lang->system->integratedList), + on::change()->do('$("#children").toggleClass("hidden");') + ), + formGroup + ( + set::name('name'), + set::width($width), + set::label($lang->system->name), + set::required(true) + ), + formGroup + ( + setID('children'), + setClass('hidden'), + set::name('children'), + set::width($width), + set::required(true), + set::label($lang->system->children), + set::control('picker'), + set::items($systemList), + set::multiple(true) + ), + formGroup + ( + set::name('desc'), + set::label($lang->system->desc), + set::control(array('type' => 'textarea', 'rows' => '4')) + ) +); diff --git a/module/system/ui/edit.html.php b/module/system/ui/edit.html.php new file mode 100644 index 0000000000..0ace9dcd0e --- /dev/null +++ b/module/system/ui/edit.html.php @@ -0,0 +1,61 @@ + + * @package system + * @link https://www.zentao.net + */ +namespace zin; + +$width = common::checkNotCN() ? '3/4' : '1/2'; +formPanel +( + set::id('systemEditForm'), + set::title($lang->system->edit), + set::submitBtnText($lang->save), + set::labelWidth(common::checkNotCN() ? '160px' : '100px'), + formGroup + ( + set::name('integrated'), + set::width('1/2'), + set::label($lang->system->integrated), + set::control('radioListInline'), + set::value($system->integrated), + set::items($lang->system->integratedList), + set::disabled(true) + ), + formGroup + ( + set::name('name'), + set::width($width), + set::label($lang->system->name), + set::labelWidth(common::checkNotCN() ? '160px' : '100px'), + set::required(true), + set::value($system->name) + ), + formGroup + ( + setID('children'), + $system->integrated ? null : setClass('hidden'), + set::name('children'), + set::width($width), + set::required(true), + set::label($lang->system->children), + set::labelWidth(common::checkNotCN() ? '160px' : '100px'), + set::control('picker'), + set::items($systemList), + set::multiple(true), + set::value($system->children) + ), + formGroup + ( + set::name('desc'), + set::label($lang->system->desc), + set::labelWidth(common::checkNotCN() ? '160px' : '100px'), + set::control(array('type' => 'textarea', 'rows' => '4')), + set::value($system->desc) + ) +); diff --git a/module/upgrade/config.php b/module/upgrade/config.php index e204945d45..216a10400e 100644 --- a/module/upgrade/config.php +++ b/module/upgrade/config.php @@ -1409,7 +1409,7 @@ $config->delete['20_9'][] = 'extension/max/deploy/js/finish.ui.js'; $config->delete['20_9'][] = 'extension/ipd/deploy/js/finish.ui.js'; $config->delete['20_9'][] = 'module/account/'; -$config->upgrade->openModules = array('action', 'admin', 'ai', 'bi', 'aiapp', 'api', 'automation', 'backup', 'block', 'branch', 'budget', 'bug', 'build', 'caselib', 'chart', 'ci', 'client', 'common', 'company', 'compile', 'convert', 'cron', 'custom', 'datatable', 'dataview', 'dept', 'design', 'dev', 'dimension', 'doc', 'durationestimation', 'entry', 'execution', 'extension', 'file', 'git', 'gitlab', 'group', 'holiday', 'im', 'index', 'index.html', 'install', 'issue', 'jenkins', 'job', 'kanban', 'license', 'mail', 'message', 'metric', 'misc', 'mr', 'my', 'personnel', 'pipeline', 'product', 'productplan', 'productset', 'program', 'programplan', 'project', 'projectbuild', 'projectplan', 'projectrelease', 'projectstory', 'pivot', 'qa', 'release', 'repo', 'report', 'risk', 'score', 'screen', 'search', 'setting', 'sonarqube', 'sso', 'stage', 'stakeholder', 'story', 'subject', 'svn', 'task', 'testcase', 'testreport', 'testsuite', 'testtask', 'todo', 'tree', 'tutorial', 'upgrade', 'user', 'webhook', 'weekly', 'workestimation', 'gitea', 'gogs', 'transfer', 'zahost', 'zanode', 'editor', 'charter', 'roadmap', 'account', 'cne', 'host', 'instance', 'ops', 'serverroom', 'space', 'store', 'system', 'solution', 'demand', 'gitfox', 'epic', 'requirement'); +$config->upgrade->openModules = array('action', 'admin', 'ai', 'bi', 'aiapp', 'api', 'automation', 'backup', 'block', 'branch', 'budget', 'bug', 'build', 'cache', 'caselib', 'chart', 'ci', 'client', 'common', 'company', 'compile', 'convert', 'cron', 'custom', 'datatable', 'dataview', 'dept', 'design', 'dev', 'dimension', 'doc', 'durationestimation', 'entry', 'execution', 'extension', 'file', 'git', 'gitlab', 'group', 'holiday', 'im', 'index', 'index.html', 'install', 'issue', 'jenkins', 'job', 'kanban', 'license', 'mail', 'message', 'metric', 'misc', 'mr', 'my', 'personnel', 'pipeline', 'product', 'productplan', 'productset', 'program', 'programplan', 'project', 'projectbuild', 'projectplan', 'projectrelease', 'projectstory', 'pivot', 'qa', 'release', 'repo', 'report', 'risk', 'score', 'screen', 'search', 'setting', 'sonarqube', 'sso', 'stage', 'stakeholder', 'story', 'subject', 'svn', 'task', 'testcase', 'testreport', 'testsuite', 'testtask', 'todo', 'tree', 'tutorial', 'upgrade', 'user', 'webhook', 'weekly', 'workestimation', 'gitea', 'gogs', 'transfer', 'zahost', 'zanode', 'editor', 'charter', 'roadmap', 'account', 'cne', 'host', 'instance', 'ops', 'serverroom', 'space', 'store', 'system', 'solution', 'demand', 'gitfox', 'epic', 'requirement'); $config->upgrade->unsetModules = array('design', 'program', 'programplan', 'projectbuild', 'projectrelease', 'stage', 'stakeholder', 'product', 'branch', 'productplan', 'release', 'build', 'qa', 'bug', 'testcase', 'testtask', 'testreport', 'testsuite', 'caselib', 'automation', 'repo', 'ci', 'compile', 'jenkins', 'job', 'svn', 'gitlab', 'sonarqube', 'mr', 'git', 'report', 'sqlbuilder', 'feedback', 'faq', 'attend', 'holiday', 'leave', 'makeup', 'overtime', 'lieu', 'ops', 'host', 'serverroom', 'account', 'domain', 'service', 'deploy', 'conference', 'traincourse', 'pssp', 'baseline', 'classify', 'cm', 'cmcl', 'auditcl', 'reviewcl', 'process', 'activity', 'zoutput', 'auditplan', 'nc', 'subject', 'weekly', 'workestimation', 'issue', 'durationestimation', 'risk', 'opportunity', 'trainplan', 'gapanalysis', 'researchplan', 'researchreport', 'meeting', 'meetingroom', 'budget', 'reviewissue', 'reviewsetting', 'review', 'milestone', 'measurement', 'measrecord', 'assetlib', 'setting', 'im', 'client', 'ldap', 'dev', 'api', 'gitea', 'gogs', 'zanode', 'zahost'); global $lang; diff --git a/module/upgrade/model.php b/module/upgrade/model.php index 7ce375c16c..537ea11e65 100644 --- a/module/upgrade/model.php +++ b/module/upgrade/model.php @@ -3327,16 +3327,16 @@ class upgradeModel extends model $setting->key = 'storyRequirement'; $setting->value = '0'; - $this->dao->replace(TABLE_CONFIG)->data($setting)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($setting)->exec(); $setting->key = 'hourPoint'; $setting->value = '0'; - $this->dao->replace(TABLE_CONFIG)->data($setting)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($setting)->exec(); $setting->module = 'common'; $setting->key = 'conceptSetted'; $setting->value = '1'; - $this->dao->replace(TABLE_CONFIG)->data($setting)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($setting)->exec(); } return true; @@ -8836,9 +8836,9 @@ class upgradeModel extends model { unset($createField->id); $createField->key = 'createWaterfallFields'; //瀑布模型 - $this->dao->replace(TABLE_CONFIG)->data($createField)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($createField)->exec(); $createField->key = 'createWaterfallplusFields'; //融合瀑布模型 - $this->dao->replace(TABLE_CONFIG)->data($createField)->exec(); + $this->dao->insert(TABLE_CONFIG)->data($createField)->exec(); } /* 删除旧的config。*/ @@ -9214,7 +9214,7 @@ class upgradeModel extends model */ public function openCacheByAPCu(): bool { - return $this->loadModel('install')->enableDaoCache(); + return $this->loadModel('install')->enableCache(); } /** diff --git a/test/config/config.php b/test/config/config.php index 9ce7b2fc18..6f0c1253de 100644 --- a/test/config/config.php +++ b/test/config/config.php @@ -43,6 +43,7 @@ $config->uitest->chrome->options[] = '--ignore-certificate-errors'; $config->uitest->chrome->options[] = '--test-type'; $config->uitest->chrome->options[] = '--start-maximized'; +$config->uitest->setExceptionHandler = true; $config->uitest->exitBrowserOnException = true; $extConfigFiles = glob(dirname(__FILE__) . DS . 'ext' . DS . '*.php'); if($extConfigFiles) foreach($extConfigFiles as $extConfigFile) include $extConfigFile; diff --git a/test/data/system.yaml b/test/data/system.yaml index bbcce2514d..d3cb285db8 100644 --- a/test/data/system.yaml +++ b/test/data/system.yaml @@ -10,13 +10,13 @@ fields: - field: product range: 1-100 - field: integrated - range: 0 + range: 0-1 - field: latestRelease range: 0 - field: latestDate range: [] - field: children - range: [] + range: "[],1" - field: status range: active - field: desc diff --git a/test/lib/ui.php b/test/lib/ui.php index 59a7f77415..f8cde9c806 100644 --- a/test/lib/ui.php +++ b/test/lib/ui.php @@ -154,13 +154,13 @@ class tester extends result $this->cookieFile = '/tmp/cookie'; // 设置异常处理函数 - set_exception_handler(array($this, 'exceptionHandler')); + if($this->config->uitest->setExceptionHandler) set_exception_handler(array($this, 'exceptionHandler')); } /** * Exception handler. * - * @param int $e + * @param object $e * @access public * @return void */