diff --git a/db/update18.1.sql b/db/update18.1.sql
index ef6ffc263f..0f4b3dd000 100644
--- a/db/update18.1.sql
+++ b/db/update18.1.sql
@@ -2,9 +2,9 @@ DELETE FROM `zt_config` WHERE module = 'datatable' AND section = 'executionAll';
update `zt_task` set `assignedTo` = '' where `mode` = 'multi' and `status` != 'done' and `status` != 'closed';
-REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'scrumClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' ORDER BY id ASC;
-REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'agileplusClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' ORDER BY id ASC;
-REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'waterfallplusClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' ORDER BY id ASC;
+REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'scrumClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' and `system` = '1' ORDER BY id ASC;
+REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'agileplusClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' and `system` = '1' ORDER BY id ASC;
+REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) SELECT `lang`, `module`, 'waterfallplusClassify', `key`, `value`, `system`, `vision` FROM `zt_lang` WHERE `module` = 'process' and `section` = 'classify' and `system` = '1' ORDER BY id ASC;
UPDATE `zt_project` AS parent INNER JOIN (select `id`,`parent`,`attribute` from `zt_project` where `parent` != 0 and `type` = 'stage') AS child ON parent.`id` = child.`parent` SET parent.`attribute`='mix' where parent.`grade`=1 and parent.`type`='stage' and parent.`attribute` != child.`attribute`;
diff --git a/extension/lite/project/ext/view/create.html.php b/extension/lite/project/ext/view/create.html.php
index ce3e1f1454..0308b9d310 100644
--- a/extension/lite/project/ext/view/create.html.php
+++ b/extension/lite/project/ext/view/create.html.php
@@ -40,7 +40,7 @@
project->name;?>
- setCode) or $config->setCode == 1):?>
+ setCode) and $config->setCode == 1):?>
project->code;?>
diff --git a/lib/cache/cache.class.php b/lib/cache/cache.class.php
new file mode 100644
index 0000000000..7d17873b08
--- /dev/null
+++ b/lib/cache/cache.class.php
@@ -0,0 +1,53 @@
+
+ * @package cache
+ * @link http://www.zentao.net
+ */
+
+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 . 'YacDriver.php');
+
+use ZenTao\Cache\SimpleCache\InvalidArgumentException;
+
+class cache
+{
+ /**
+ * @var ZenTao\Cache\SimpleCache\CacheInterface
+ */
+ protected $client;
+
+ public function __construct($driver = 'Apcu', $namespace = '', $defaultLifetime = 0)
+ {
+ $driver = ucfirst(strtolower($driver));
+ switch($driver)
+ {
+ case 'Apcu':
+ $className = 'ZenTao\Cache\Driver\ApcuDriver';
+ break;
+ case 'Yac':
+ $className = 'ZenTao\Cache\Driver\YacDriver';
+ break;
+ default:
+ throw new InvalidArgumentException("Driver {$driver} is not supported.");
+ }
+
+ if(!extension_loaded($driver)) throw new InvalidArgumentException("Driver ext-{$driver} is not loaded.");
+
+ $this->client = new $className($namespace, $defaultLifetime);
+ }
+
+ public function __call($name, $arguments)
+ {
+ if(!method_exists($this->client, $name)) throw new InvalidArgumentException("Method {$name} does not exist.");
+
+ return call_user_func_array(array($this->client, $name), $arguments);
+ }
+}
diff --git a/lib/cache/driver/ApcuDriver.php b/lib/cache/driver/ApcuDriver.php
new file mode 100644
index 0000000000..7c2def717a
--- /dev/null
+++ b/lib/cache/driver/ApcuDriver.php
@@ -0,0 +1,173 @@
+
+ * @package cache
+ * @link http://www.zentao.net
+ */
+
+namespace ZenTao\Cache\Driver;
+
+use ZenTao\Cache\SimpleCache\CacheInterface;
+use ZenTao\Cache\SimpleCache\InvalidArgumentException;
+
+class ApcuDriver implements CacheInterface
+{
+ /**
+ * @var string
+ */
+ private $namespace;
+
+ /**
+ * @var int
+ */
+ private $defaultLifetime;
+
+ public function __construct($namespace = '', $defaultLifetime = 0)
+ {
+ $this->namespace = $namespace;
+ $this->defaultLifetime = $defaultLifetime;
+ }
+
+ public function get($key, $default = null)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ $value = apcu_fetch($key, $success);
+
+ return $success === false ? $default : $value;
+ }
+
+ public function set($key, $value, $ttl = null)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl;
+
+ return apcu_store($key, $value, (int) $ttl);
+ }
+
+ public function delete($key)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ return apcu_delete($key);
+ }
+
+ public function clear()
+ {
+ return apcu_clear_cache();
+ }
+
+ public function getMultiple($keys, $default = null)
+ {
+ $this->assertKeyNames($keys);
+ $keys = $this->buildKeyNames($keys);
+
+ $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;
+ }
+
+ public function setMultiple($values, $ttl = null)
+ {
+ $this->assertKeyNames(array_keys($values));
+
+ $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);
+
+ 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;
+ }
+
+ public function has($key)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ return (bool) apcu_exists($key);
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ */
+ private function buildKeyName($key)
+ {
+ return $this->namespace . $key;
+ }
+
+ /**
+ * @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);
+ }
+}
diff --git a/lib/cache/driver/YacDriver.php b/lib/cache/driver/YacDriver.php
new file mode 100644
index 0000000000..739582c2fa
--- /dev/null
+++ b/lib/cache/driver/YacDriver.php
@@ -0,0 +1,197 @@
+
+ * @package cache
+ * @link http://www.zentao.net
+ */
+
+namespace ZenTao\Cache\Driver;
+
+use ZenTao\Cache\SimpleCache\CacheInterface;
+use ZenTao\Cache\SimpleCache\InvalidArgumentException;
+
+class YacDriver implements CacheInterface
+{
+ /**
+ * @var string
+ */
+ private $namespace;
+
+ /**
+ * @var int
+ */
+ private $defaultLifetime;
+
+ /**
+ * yac client
+ *
+ * @var \Yac
+ */
+ protected $yac;
+
+ /**
+ * if your key is longer than this, maybe you can use md5 result as the key
+ */
+ const KEY_MAX_LEN = 48;
+
+ public function __construct($namespace = '', $defaultLifetime = 0)
+ {
+ $this->namespace = $namespace;
+ $this->defaultLifetime = $defaultLifetime;
+ $this->yac = new \Yac($namespace);
+ }
+
+ public function get($key, $default = null)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ return $this->yac->get($key) ?: $default;
+ }
+
+ public function set($key, $value, $ttl = null)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl;
+
+ return $this->yac->set($key, $value, (int)$ttl);
+ }
+
+ public function delete($key)
+ {
+ $this->assertKeyName($key);
+ $key = $this->buildKeyName($key);
+
+ return $this->yac->delete($key);
+ }
+
+ public function clear()
+ {
+ return $this->yac->flush();
+ }
+
+ public function getMultiple($keys, $default = null)
+ {
+ if(!is_array($keys)) {
+ return array();
+ }
+
+ $hashKeyMap = array();
+ foreach($keys as $index => $key)
+ {
+ $this->assertKeyName($key);
+ if(strlen($key) > self::KEY_MAX_LEN)
+ {
+ $keys[$index] = $this->buildKeyName($key);
+ $hashKeyMap[$keys[$index]] = $key;
+ }
+ }
+
+ $results = $this->yac->get($keys);
+ if($results !== false)
+ {
+ foreach($results as $key => $value)
+ {
+ if(isset($hashKeyMap[$key]))
+ {
+ $results[$hashKeyMap[$key]] = $value;
+ unset($results[$key]);
+ }
+ }
+ return $results;
+ }
+
+ $results = array();
+ foreach($keys as $key)
+ {
+ $results[$key] = $default;
+ }
+ return $results;
+ }
+
+ public function setMultiple($values, $ttl = null)
+ {
+ if(!is_array($values)) return false;
+
+ foreach($values as $key => $value)
+ {
+ if(strlen($key) > self::KEY_MAX_LEN)
+ {
+ $values[$this->buildKeyName($key)] = $value;
+ unset($values[$key]);
+ }
+ }
+
+ $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl;
+
+ return $this->yac->set($values, $ttl);
+ }
+
+ public function deleteMultiple($keys)
+ {
+ foreach($keys as $index => $key)
+ {
+ $keys[$index] = $this->buildKeyName($key);
+ }
+ return $this->yac->delete($keys);
+ }
+
+ public function has($key)
+ {
+ return $this->get($key) !== null;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ */
+ private function buildKeyName($key)
+ {
+ if(strlen($key) > self::KEY_MAX_LEN)
+ {
+ $key = md5($key);
+ }
+ return $key;
+ }
+
+ /**
+ * @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);
+ }
+}
diff --git a/lib/cache/simple-cache/CacheException.php b/lib/cache/simple-cache/CacheException.php
new file mode 100644
index 0000000000..330b06084b
--- /dev/null
+++ b/lib/cache/simple-cache/CacheException.php
@@ -0,0 +1,7 @@
+ $keys A list of keys that can be obtained in a single operation.
+ * @param mixed $default Default value to return for keys that do not exist.
+ *
+ * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
+ *
+ * @throws InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function getMultiple($keys, $default = null);
+
+ /**
+ * Persists a set of key => value pairs in the cache, with an optional TTL.
+ *
+ * @param iterable $values A list of key => value pairs for a multiple-set operation.
+ * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
+ * the driver supports TTL then the library may set a default value
+ * for it or let the driver take care of that.
+ *
+ * @return bool True on success and false on failure.
+ *
+ * @throws InvalidArgumentException
+ * MUST be thrown if $values is neither an array nor a Traversable,
+ * or if any of the $values are not a legal value.
+ */
+ public function setMultiple($values, $ttl = null);
+
+ /**
+ * Deletes multiple cache items in a single operation.
+ *
+ * @param iterable $keys A list of string-based keys to be deleted.
+ *
+ * @return bool True if the items were successfully removed. False if there was an error.
+ *
+ * @throws InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function deleteMultiple($keys);
+
+ /**
+ * Determines whether an item is present in the cache.
+ *
+ * NOTE: It is recommended that has() is only to be used for cache warming type purposes
+ * and not to be used within your live applications operations for get/set, as this method
+ * is subject to a race condition where your has() will return true and immediately after,
+ * another script can remove it making the state of your app out of date.
+ *
+ * @param string $key The cache item key.
+ *
+ * @return bool
+ *
+ * @throws InvalidArgumentException
+ * MUST be thrown if the $key string is not a legal value.
+ */
+ public function has($key);
+}
diff --git a/lib/cache/simple-cache/InvalidArgumentException.php b/lib/cache/simple-cache/InvalidArgumentException.php
new file mode 100644
index 0000000000..825bcdf380
--- /dev/null
+++ b/lib/cache/simple-cache/InvalidArgumentException.php
@@ -0,0 +1,7 @@
+ .setting-box > .btn:hover {background: unset;}
.settings-list > .setting-box > .btn[disabled] {cursor: not-allowed; pointer-events: unset;}
.settings-list > .setting-box > .btn[disabled] > a {pointer-events: none;}
+.settings-list > .setting-box > .btn > h4 {padding-right: 5px;}
+.settings-list > .setting-box > .btn > h4 > div {gap: 3px;}
.settings-list > .setting-box > .btn > h4 > img {padding-right: 8px; padding-bottom: 3px; width: 32px; height: 27px;}
-.settings-list > .setting-box > .btn > .setting-desc {padding-bottom: 16px; color: #5E626D; line-height: 25px; height: 50px;}
+.settings-list > .setting-box > .btn > .setting-desc {padding-bottom: 16px; color: #5E626D; line-height: 25px; height: 50px; overflow: hidden;}
.settings-list.lite-setting > .setting-box {flex: 1 1 30%;}
.plugin-list {display: flex; flex-wrap: wrap; padding: 12px 4px 4px 4px;}
@@ -71,7 +75,8 @@
.panel.publicClass {padding: 0 10px;}
.icon.follow-us {font-size: 20px; padding-left: 10px;}
-.setting-help {width: 45px; color: #9EA3B0; padding-top: 15px;}
+.setting-help {color: #9EA3B0; padding-left: 5px;}
+.setting-help > a {font-size: 15px;}
[lang^='zh'] .setting-help {padding-top: 0;}
.pointer-none {pointer-events: none;}
@@ -86,7 +91,7 @@
.time-block {background: #EDEEF2; padding:2px 4px; border-radius: 4px; color: #0B0F18; font-size: 15px; margin: 0 4px;}
.dynamic-block {height: 74px; padding: 12px 16px; display: flex; justify-content: space-between; position: relative; line-height: 20px; border-top: 1px solid #E3E4E9;}
.dynamic-content {height: 50px; line-height: 25px; overflow: hidden;}
-.dynamic-time {white-space: nowrap; position: absolute; right: 12px; bottom: 10px; padding-right: 12px; background: white; color: #5E626D;}
+.dynamic-time {white-space: nowrap; position: absolute; right: 12px; bottom: 12px; padding-left: 10px; padding-right: 12px; background: white; color: #5E626D;}
.time-count {letter-spacing: 1px;}
.patch-block {padding: 10px 16px 12px; line-height: 20px; border-top: 1px solid #E3E4E9;}
diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php
index deb8d5c52c..ad7bdc8f80 100644
--- a/module/admin/lang/menu.php
+++ b/module/admin/lang/menu.php
@@ -11,6 +11,7 @@ $lang->admin->menuList->user['order'] = 2;
$lang->admin->menuList->switch['name'] = $lang->admin->menuSetting['switch']['name'];
$lang->admin->menuList->switch['desc'] = $lang->admin->menuSetting['switch']['desc'];
+$lang->admin->menuList->switch['link'] = 'admin|setmodule';
$lang->admin->menuList->switch['order'] = 3;
$lang->admin->menuList->model['name'] = $lang->admin->menuSetting['model']['name'];
@@ -71,10 +72,6 @@ $lang->admin->menuList->user['menuOrder']['5'] = 'dept';
$lang->admin->menuList->user['menuOrder']['10'] = 'user';
$lang->admin->menuList->user['menuOrder']['15'] = 'group';
-$lang->admin->menuList->switch['subMenu']['setmodule'] = array('link' => "{$lang->admin->module}|admin|setmodule|");
-
-$lang->admin->menuList->switch['menuOrder']['5'] = 'setmodule';
-
$lang->admin->menuList->model['subMenu']['common'] = array('link' => "{$lang->globalSetting}|custom|required|module=project", 'subModule' => 'custom,subject,holiday,stage', 'exclude' => 'stage-browse,stage-plusbrowse,stage-create,stage-edit,stage-batchcreate');
$lang->admin->menuList->model['subMenu']['scrum'] = array('link' => "{$lang->scrumModel}|auditcl|scrumbrowse|", 'subModule' => 'auditcl');
$lang->admin->menuList->model['subMenu']['waterfall'] = array('link' => "{$lang->waterfallModel}|stage|browse|", 'subModule' => 'stage', 'exclude' => 'stage-settype,stage-plusbrowse');
@@ -165,7 +162,7 @@ $lang->admin->menuList->message['menuOrder']['10'] = 'webhook';
$lang->admin->menuList->message['menuOrder']['20'] = 'browser';
$lang->admin->menuList->message['menuOrder']['25'] = 'setting';
-$lang->admin->menuList->dev['subMenu']['api'] = array('link' => "API|dev|api|module=index");
+$lang->admin->menuList->dev['subMenu']['api'] = array('link' => "{$lang->api->doc}|dev|api|module=index");
$lang->admin->menuList->dev['subMenu']['db'] = array('link' => "{$lang->database}|dev|db|table=" . trim(TABLE_EFFORT, '`'));
$lang->admin->menuList->dev['subMenu']['langItem'] = array('link' => "{$lang->langItem}|dev|langitem|");
$lang->admin->menuList->dev['subMenu']['editor'] = array('link' => "{$lang->editor}|dev|editor|");
diff --git a/module/admin/view/index.html.php b/module/admin/view/index.html.php
index f695d67c5f..f2b812c35d 100755
--- a/module/admin/view/index.html.php
+++ b/module/admin/view/index.html.php
@@ -21,9 +21,14 @@
vision == 'lite' and !in_array($menuKey, $config->admin->liteMenuList)) continue;?>