From 129ee5ac3da74c2ce3d3d75a213f0e9f19a44322 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Mon, 15 May 2023 03:34:51 +0000 Subject: [PATCH] Init project --- IsekaiUserPoints.alias.php | 2 + IsekaiUserPoints.services.php | 13 + composer.json | 12 + extension.json | 105 +++++ i18n/zh-hans.json | 28 ++ includes/Api/ApiQueryPointInfo.php | 75 +++ includes/Api/ApiQueryUserPoints.php | 99 ++++ includes/Api/ApiQueryUsersPoints.php | 124 +++++ includes/Api/ApiUserDailySign.php | 63 +++ includes/Hooks.php | 63 +++ includes/Service/IsekaiUserDailySign.php | 256 ++++++++++ .../Service/IsekaiUserDailySignFactory.php | 62 +++ includes/Service/IsekaiUserPoints.php | 441 ++++++++++++++++++ includes/Service/IsekaiUserPointsFactory.php | 114 +++++ includes/Utils.php | 71 +++ modules/ext.isekai.userpoints.base.less | 10 + modules/ext.isekai.userpoints.dailysign.js | 43 ++ sql/mysql/isekai_user_daily_sign.sql | 7 + sql/mysql/isekai_user_daily_sign_log.sql | 10 + sql/mysql/isekai_user_points.sql | 14 + sql/mysql/isekai_user_points_log.sql | 15 + sql/postgres/isekai_user_daily_sign.sql | 7 + sql/postgres/isekai_user_daily_sign_log.sql | 10 + sql/postgres/isekai_user_points.sql | 17 + sql/postgres/isekai_user_points_log.sql | 15 + 25 files changed, 1676 insertions(+) create mode 100644 IsekaiUserPoints.alias.php create mode 100644 IsekaiUserPoints.services.php create mode 100644 composer.json create mode 100644 extension.json create mode 100644 i18n/zh-hans.json create mode 100644 includes/Api/ApiQueryPointInfo.php create mode 100644 includes/Api/ApiQueryUserPoints.php create mode 100644 includes/Api/ApiQueryUsersPoints.php create mode 100644 includes/Api/ApiUserDailySign.php create mode 100644 includes/Hooks.php create mode 100644 includes/Service/IsekaiUserDailySign.php create mode 100644 includes/Service/IsekaiUserDailySignFactory.php create mode 100644 includes/Service/IsekaiUserPoints.php create mode 100644 includes/Service/IsekaiUserPointsFactory.php create mode 100644 includes/Utils.php create mode 100644 modules/ext.isekai.userpoints.base.less create mode 100644 modules/ext.isekai.userpoints.dailysign.js create mode 100644 sql/mysql/isekai_user_daily_sign.sql create mode 100644 sql/mysql/isekai_user_daily_sign_log.sql create mode 100644 sql/mysql/isekai_user_points.sql create mode 100644 sql/mysql/isekai_user_points_log.sql create mode 100644 sql/postgres/isekai_user_daily_sign.sql create mode 100644 sql/postgres/isekai_user_daily_sign_log.sql create mode 100644 sql/postgres/isekai_user_points.sql create mode 100644 sql/postgres/isekai_user_points_log.sql diff --git a/IsekaiUserPoints.alias.php b/IsekaiUserPoints.alias.php new file mode 100644 index 0000000..a6bcc64 --- /dev/null +++ b/IsekaiUserPoints.alias.php @@ -0,0 +1,2 @@ + static function ( MediaWikiServices $services ) { + return new IsekaiUserPointsFactory( $services ); + }, + 'IsekaiUserDailySign' => static function ( MediaWikiServices $services ) { + return new IsekaiUserDailySignFactory( $services ); + }, +]; \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b0e9da6 --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "name": "hyperzlib/isekai-user-points", + "type": "library", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Lex Lim", + "email": "hyperzlib@outlook.com" + } + ], + "require": {} +} diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..b06198e --- /dev/null +++ b/extension.json @@ -0,0 +1,105 @@ +{ + "name": "Isekai User Points", + "namemsg": "isekai-userpoints", + "author": "Hyperzlib", + "version": "1.0.0", + "url": "https://git.isekai.cn/Isekai-Project/mediawiki-extension-IsekaiUserPoints", + "descriptionmsg": "isekai-userpoints-desc", + "license-name": "GPL-2.0-or-later", + "type": "api", + "requires": { + "MediaWiki": ">= 1.35.0" + }, + "MessagesDirs": { + "IsekaiUserPoints": [ + "i18n" + ] + }, + "ExtensionMessagesFiles": { + "IsekaiUserPointsAlias": "IsekaiUserPoints.alias.php" + }, + "AutoloadNamespaces": { + "Isekai\\UserPoints\\": "includes" + }, + "Hooks": { + "LoadExtensionSchemaUpdates": "Isekai\\UserPoints\\Hooks::onLoadExtensionSchemaUpdates", + "BeforePageDisplay": "Isekai\\UserPoints\\Hooks::onBeforePageDisplay", + "GetPreferences": "Isekai\\UserPoints\\Hooks::onGetPreferences" + }, + "APIModules": { + "userdailysign": "Isekai\\UserPoints\\Api\\ApiUserDailySign" + }, + "APIPropModules": { + "userpoints": "Isekai\\UserPoints\\Api\\ApiQueryUserPoints", + "pointinfo": "Isekai\\UserPoints\\Api\\ApiQueryPointInfo" + }, + "APIListModules": { + "userspoints": "Isekai\\UserPoints\\Api\\ApiQueryUsersPoints" + }, + "ServiceWiringFiles": [ + "IsekaiUserPoints.services.php" + ], + "GroupPermissions": { + "bureaucrat": { + "queryuserpoints": true, + "edituserpoints": true + }, + "suppress": { + "queryuserpoints": true, + "edituserpoints": true + }, + "sysop": { + "queryuserpoints": true, + "edituserpoints": true + } + }, + "GrantPermissions": { + "userpointsmanager": { + "queryuserpoints": true, + "edituserpoints": true + } + }, + "ResourceModules": { + "ext.isekai.userpoints.base": { + "styles": ["ext.isekai.userpoints.base.less"] + }, + "ext.isekai.userpoints.dailysign": { + "scripts": ["ext.isekai.userpoints.dailysign.js"], + "messages": [ + "comma-separator", + "isekai-userpoints-point-name-num", + "isekai-userpoints-dailysign-notify-title", + "isekai-userpoints-dailysign-notify-success" + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "modules", + "remoteExtPath": "IsekaiUserPoints/modules" + }, + "config": { + "IsekaiUserPointConfig": { + "value": { + "exp": { + "name": "Exp.", + "namemsg": "isekai-userpoints-point-name-exp", + "icon": { + "normal": { + "image": "https://static.isekai.dev/isekaiwiki/isekaiwiki-exp.png" + }, + "invert": { + "image": "https://static.isekai.dev/isekaiwiki/isekaiwiki-exp-invert.png" + } + } + } + } + }, + "IsekaiUserPointShowOnUserPerferences": { + "value": true + }, + "IsekaiUserDailySignConfig": { + "value": null + } + }, + "manifest_version": 2 +} \ No newline at end of file diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json new file mode 100644 index 0000000..1c09171 --- /dev/null +++ b/i18n/zh-hans.json @@ -0,0 +1,28 @@ +{ + "isekai-userpoints": "异世界百科 用户积分", + "isekai-userpoints-desc": "存储用户积分,为其他应用提供积分功能", + + "isekai-userpoints-point-name-num": "$2 $3 $1", + + "isekai-userpoints-service-system": "积分系统", + "isekai-userpoints-action-system-point-expired": "积分过期", + + "apihelp-query+userpoints-summary": "获取当前登录用户的积分", + + "apihelp-query+userspoints-summary": "获取用户积分", + "apihelp-query+userspoints-param-users": "要获取信息的用户列表。", + "apihelp-query+userspoints-param-userids": "要获得信息的用户ID列表。", + + "apierror-isekaiuserpoints-notfound": "未找到积分信息", + + "isekai-userpoints-point-name-exp": "经验值", + "isekai-userpoints-point-name-kamakano": "星砂", + + "isekai-userpoints-source-system": "积分系统", + + "apihelp-userdailysign-summary": "用户签到", + "apiwarn-isekai-userpoints-already-signed-today": "今天已经签到过了", + + "isekai-userpoints-dailysign-notify-title": "每日签到", + "isekai-userpoints-dailysign-notify-success": "每日登录网站,获得了$1" +} \ No newline at end of file diff --git a/includes/Api/ApiQueryPointInfo.php b/includes/Api/ApiQueryPointInfo.php new file mode 100644 index 0000000..6375d60 --- /dev/null +++ b/includes/Api/ApiQueryPointInfo.php @@ -0,0 +1,75 @@ +config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'textextracts' ); + $this->cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + } + + /** + * @throws \ApiUsageException + */ + public function execute() { + $pointConfig = $this->config->get('IsekaiUserPointConfig'); + $pointTypes = array_keys($pointConfig); + + $pointInfos = []; + foreach ($pointTypes as $pointType) { + $pointInfos[$pointType] = [ + 'name' => Utils::getPointName($pointType), + 'icon' => Utils::getPointIcon($pointType), + ]; + } + + $result = $this->getResult(); + $result->addValue( + [ 'query', $this->getModuleName() ], + 'pointinfo', + $pointInfos + ); + } + + /** + * @param array $params Ignored parameters + * @return string + */ + public function getCacheMode($params) { + return 'public'; + } + + public function getAllowedParams() { + return [ + + ]; + } +} \ No newline at end of file diff --git a/includes/Api/ApiQueryUserPoints.php b/includes/Api/ApiQueryUserPoints.php new file mode 100644 index 0000000..96191bf --- /dev/null +++ b/includes/Api/ApiQueryUserPoints.php @@ -0,0 +1,99 @@ +config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'textextracts' ); + $this->cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + } + + /** + * @throws \ApiUsageException + */ + public function execute() { + $user = $this->getUser(); + if (!$user->isRegistered()) { + $this->dieWithError('apierror-mustbeloggedin-generic', 'login-required'); + return false; + } + + $pointConfig = $this->config->get('IsekaiUserPointConfig'); + $pointTypes = array_keys($pointConfig); + + /** @var IsekaiUserPointsFactory */ + $userPointsFactory = MediaWikiServices::getInstance()->getService('IsekaiUserPoints'); + + /** @var array */ + $userPoints = []; + foreach ($pointTypes as $pointType) { + $userPoint = $userPointsFactory->newFromUser($user, $pointType); + if ($userPoint) { + $userPoints[$pointType] = $userPointsFactory->newFromUser($user, $pointType); + } + } + + if (count($userPoints) === 0) { + $this->dieWithError('apierror-isekaiuserpoints-notfound', 'not-found'); + return false; + } + + $result = $this->getResult(); + foreach ($userPoints as $pointType => $userPoint) { + $pointName = Utils::getPointName($pointType); + $pointIcon = Utils::getPointIcon($pointType); + $result->addValue(['query', $this->getModuleName()], $pointType, [ + 'points' => $userPoint->points, + 'timed_points_data' => $userPoint->timedPointsData, + 'locked_points' => $userPoint->lockedPoints, + 'locked_points_data' => $userPoint->lockedPointsData, + 'name' => $pointName, + 'icon' => $pointIcon, + ]); + } + if (empty($userPoints)) { + $result->addValue(['query'], $this->getModuleName(), new stdClass()); + } + } + + /** + * @param array $params Ignored parameters + * @return string + */ + public function getCacheMode($params) { + return 'private'; + } + + public function getAllowedParams() { + return []; + } +} \ No newline at end of file diff --git a/includes/Api/ApiQueryUsersPoints.php b/includes/Api/ApiQueryUsersPoints.php new file mode 100644 index 0000000..c3e56a2 --- /dev/null +++ b/includes/Api/ApiQueryUsersPoints.php @@ -0,0 +1,124 @@ +config = MediaWikiServices::getInstance()->getMainConfig(); + $this->cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + } + + /** + * @throws \ApiUsageException + */ + public function execute() { + $this->checkUserRightsAny('queryuserpoints'); + + $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'userids', 'users' ); + + $users = (array)$params['users']; + $userids = (array)$params['userids']; + + $services = MediaWikiServices::getInstance(); + + $pointConfig = $this->config->get('IsekaiUserPointConfig'); + $pointTypes = array_keys($pointConfig); + + $pointInfos = []; + foreach ($pointTypes as $pointType) { + $pointInfos[$pointType] = [ + 'name' => Utils::getPointName($pointType), + 'icon' => Utils::getPointIcon($pointType), + ]; + } + + /** @var IsekaiUserPointsFactory */ + $userPointsFactory = $services->getService('IsekaiUserPoints'); + + $userFactory = $services->getUserFactory(); + $userInstances = []; + foreach ( $users as $u ) { + $user = $userFactory->newFromName( $u ); + $userInstances[] = $user; + } + foreach ( $userids as $u ) { + $user = $userFactory->newFromId( $u ); + $userInstances[] = $user; + } + + $userPointsInstances = $userPointsFactory->newFromUsers($userInstances, $pointTypes); + + $result = $this->getResult(); + foreach ($userPointsInstances as $userPointsTuple) { + list($user, $type, $userPoint) = $userPointsTuple; + $result->addValue( + [ 'query', $this->getModuleName(), "pointdata", $user->getId() ], + $type, + [ + 'point_type' => $type, + 'points' => $userPoint->points, + 'timed_points_data' => $userPoint->timedPointsData, + 'locked_points' => $userPoint->lockedPoints, + 'locked_points_data' => $userPoint->lockedPointsData, + ] + ); + } + $result->addValue( + [ 'query', $this->getModuleName() ], + 'pointinfo', + $pointInfos + ); + } + + /** + * @param array $params Ignored parameters + * @return string + */ + public function getCacheMode($params) { + return 'public'; + } + + public function getAllowedParams() { + return [ + 'users' => [ + ParamValidator::PARAM_ISMULTI => true + ], + 'userids' => [ + ParamValidator::PARAM_ISMULTI => true, + ParamValidator::PARAM_TYPE => 'integer' + ], + ]; + } +} \ No newline at end of file diff --git a/includes/Api/ApiUserDailySign.php b/includes/Api/ApiUserDailySign.php new file mode 100644 index 0000000..40cca44 --- /dev/null +++ b/includes/Api/ApiUserDailySign.php @@ -0,0 +1,63 @@ +getMain(), $method ); + } + + public function execute() { + $services = MediaWikiServices::getInstance(); + + $user = $this->getUser(); + + if (!$user->isRegistered()) { + $this->dieWithError('apierror-mustbeloggedin-generic', 'login-required'); + } + + /** @var IsekaiUserDailySignFactory */ + $dailySignFactory = $services->getService('IsekaiUserDailySign'); + $dailySignService = $dailySignFactory->newFromUser($user); + + $result = $this->getResult(); + if (date('Y-m-d') === date('Y-m-d', strtotime($dailySignService->lastSignDate))) { + $result->addValue(['userdailysign'], 'success', 0); + $result->addValue(['userdailysign'], 'point_delta', []); + $this->addWarning('isekai-userpoints-already-signed-today', 'already-signed-today'); + return; + } + + $pointDelta = $dailySignService->doSign(); + + if (!empty($pointDelta)) { + $result->addValue(['userdailysign'], 'success', 1); + } else { + $result->addValue(['userdailysign'], 'success', 0); + } + + $resultPointDelta = []; + foreach ($pointDelta as $pointType => $delta) { + $pointName = Utils::getPointName($pointType); + $pointIcon = Utils::getPointIcon($pointType); + + $resultPointDelta[] = [ + 'point_type' => $pointType, + 'name' => $pointName, + 'icon' => $pointIcon, + 'points' => $delta, + ]; + } + + $result->addValue(['userdailysign'], 'point_delta', $resultPointDelta); + } + + public function needsToken() { + return 'csrf'; + } +} \ No newline at end of file diff --git a/includes/Hooks.php b/includes/Hooks.php new file mode 100644 index 0000000..4310a2e --- /dev/null +++ b/includes/Hooks.php @@ -0,0 +1,63 @@ +getDB()->getType(); + $updater->addExtensionTable('isekai_user_daily_sign', $dir . $type . '/isekai_user_daily_sign.sql'); + $updater->addExtensionTable('isekai_user_daily_sign_log', $dir . $type . '/isekai_user_daily_sign_log.sql'); + $updater->addExtensionTable('isekai_user_points', $dir . $type . '/isekai_user_points.sql'); + $updater->addExtensionTable('isekai_user_points_log', $dir . $type . '/isekai_user_points_log.sql'); + } + + public static function onBeforePageDisplay(OutputPage $out, Skin $skin) { + $config = MediaWikiServices::getInstance()->getMainConfig(); + + $out->addModuleStyles(['ext.isekai.userpoints.base']); + + $dailySignConfig = $config->get('IsekaiUserDailySignConfig'); + if ($dailySignConfig) { + $out->addModules(['ext.isekai.userpoints.dailysign']); + } + } + + public static function onGetPreferences(User $user, &$preferences) { + $config = MediaWikiServices::getInstance()->getMainConfig(); + if ($config->get('IsekaiUserPointShowOnUserPerferences')) { + $userPointConfig = $config->get('IsekaiUserPointConfig'); + + /** @var \Isekai\UserPoints\Service\IsekaiUserPointsFactory */ + $userPointsFactory = MediaWikiServices::getInstance()->getService('IsekaiUserPoints'); + + foreach ($userPointConfig as $pointType => $pointConfig) { + $userPoints = $userPointsFactory->newFromUser($user, $pointType); + + $icon = Utils::getPointIcon($pointType); + $name = Utils::getPointName($pointType); + + $language = MediaWikiServices::getInstance()->getContentLanguage(); + + $preferences['isekai-userpoints-' . $pointType] = [ + 'type' => 'text', + 'label-raw' => $icon . ' ' . $name, + 'default' => $language->formatNum($userPoints->points), + 'section' => 'personal/info', + 'raw' => true, + ]; + } + } + } +} diff --git a/includes/Service/IsekaiUserDailySign.php b/includes/Service/IsekaiUserDailySign.php new file mode 100644 index 0000000..ef1ad30 --- /dev/null +++ b/includes/Service/IsekaiUserDailySign.php @@ -0,0 +1,256 @@ + 'user_id', + 'lastSignDate' => 'last_sign_date', + 'signDaysData' => 'sign_days_data', + 'totalSignDays' => 'total_sign_days' + ]; + + /** @var IsekaiUserDailySignData|null */ + private $mDailySignData = null; + + private $recordExists = false; + + public function __construct(User $user, array $pointConfig, array $dailySignConfig, MediaWikiServices $services) { + $this->user = $user; + $this->pointConfig = $pointConfig; + $this->dailySignConfig = $dailySignConfig; + $this->services = $services; + } + + private function loadDailySignData() { + if (!$this->mDailySignData) { + $dbr = $this->services->getDBLoadBalancer()->getConnection(DB_REPLICA); + $this->mDailySignData = $dbr->selectRow( + 'isekai_user_daily_sign', + '*', + [ + 'user_id' => $this->user->getId() + ], + __METHOD__ + ); + + if (!$this->mDailySignData) { + $this->initDefaultData(); + } else { + $this->mDailySignData->sign_days_data = json_decode($this->mDailySignData->sign_days_data, true) ?? []; + + $this->recordExists = true; + } + } + } + + private function initDefaultData() { + $this->mDailySignData = new IsekaiUserDailySignData(); + $this->mDailySignData->user_id = $this->user->getId(); + $this->mDailySignData->last_sign_date = '1970-01-01'; + $this->mDailySignData->sign_days_data = []; + $this->mDailySignData->total_sign_days = 0; + } + + public function __get($name) { + $this->loadDailySignData(); + if (isset($this->dataKeysMap[$name])) { + $key = $this->dataKeysMap[$name]; + return $this->mDailySignData->$key; + } else { + throw new \Exception("Invalid property $name"); + } + } + + public function doSign() { + $this->loadDailySignData(); + + $pointDelta = []; + + $lastSignTime = strtotime($this->mDailySignData->last_sign_date); + + if (date('Y-m-d', time()) == date('Y-m-d', $lastSignTime)) { // already signed in today + return []; + } + + /** @var IsekaiUserPointsFactory */ + $userPointsFactory = $this->services->getService('IsekaiUserPoints'); + foreach ($this->dailySignConfig as $pointType => $config) { + if (!isset($this->pointConfig[$pointType])) { // point type not exist, ignore + continue; + } + + if (!isset($config['points'])) { // no points config, ignore + continue; + } + + $signDays = $this->mDailySignData->sign_days_data[$pointType] ?? 0; + + $continuous_reset = $config['continuous_reset'] ?? false; + $expireTime = $config['expire_time'] ?? -1; + $points = $config['points']; + + if (!is_array($points)) { + $points = [ $points ]; + } + + switch ($continuous_reset) { + case 'day': + if (date('Y-m-d', strtotime('-1 day')) != date('Y-m-d', $lastSignTime)) { + $signDays = 0; + } + case 'week': + if (date('Y-W', time()) != date('Y-W', $lastSignTime)) { + $signDays = 0; + } + break; + case 'month': + if (date('Y-m', time()) != date('Y-m', $lastSignTime)) { + $signDays = 0; + } + break; + case 'year': + if (date('Y', time()) != date('Y', $lastSignTime)) { + $signDays = 0; + } + break; + } + + $pointsIndex = min($signDays, count($points) - 1); + $pointsAdd = $points[$pointsIndex]; + + $pointsService = $userPointsFactory->newFromUser($this->user, $pointType); + $pointsService->addPoints($pointsAdd, $expireTime, 'daily_sign', 'sign_in'); + + $pointDelta[$pointType] = $pointsAdd; + + $this->mDailySignData->sign_days_data[$pointType] = $signDays + 1; + } + + $this->mDailySignData->last_sign_date = date('Y-m-d H:i:s', time()); + $this->mDailySignData->total_sign_days ++; + + $this->save(); + $this->saveSignLog($pointDelta); + + return $pointDelta; + } + + private function save() { + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + if ($this->recordExists) { + $dbw->update( + 'isekai_user_daily_sign', + [ + 'last_sign_date' => $this->mDailySignData->last_sign_date, + 'sign_days_data' => json_encode($this->mDailySignData->sign_days_data), + 'total_sign_days' => $this->mDailySignData->total_sign_days, + ], + [ + 'user_id' => $this->user->getId() + ], + __METHOD__ + ); + } else { + $dbw->insert( + 'isekai_user_daily_sign', + [ + 'user_id' => $this->mDailySignData->user_id, + 'last_sign_date' => $this->mDailySignData->last_sign_date, + 'sign_days_data' => json_encode($this->mDailySignData->sign_days_data), + 'total_sign_days' => $this->mDailySignData->total_sign_days, + ], + __METHOD__ + ); + $this->recordExists = true; + } + } + + private function saveSignLog($pointDelta) { + $dbr = $this->services->getDBLoadBalancer()->getConnection(DB_REPLICA); + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + + $currentYear = intval(date('Y', time())); + $currentMonth = intval(date('m', time())); + $currentDate = intval(date('d', time())); + + $logData = $dbr->selectRow( + 'isekai_user_daily_sign_log', + '*', + [ + 'user_id' => $this->user->getId(), + 'year' => $currentYear, + 'month' => $currentMonth + ], + __METHOD__ + ); + + if ($logData) { + $signLog = json_decode($logData->sign_log, true); + if (!is_array($signLog)) { + $signLog = []; + } + $signLog[] = [ + 'date' => $currentDate, + 'delta' => $pointDelta + ]; + $logData->sign_log = json_encode($signLog); + $dbw->update( + 'isekai_user_daily_sign_log', + [ + 'sign_log' => $logData->sign_log + ], + [ + 'user_id' => $this->user->getId(), + 'year' => $currentYear, + 'month' => $currentMonth + ], + __METHOD__ + ); + } else { + $signLog = [[ + 'date' => $currentDate, + 'delta' => $pointDelta + ]]; + $dbw->insert( + 'isekai_user_daily_sign_log', + [ + 'user_id' => $this->user->getId(), + 'year' => $currentYear, + 'month' => $currentMonth, + 'sign_log' => json_encode($signLog) + ], + __METHOD__ + ); + } + } +} \ No newline at end of file diff --git a/includes/Service/IsekaiUserDailySignFactory.php b/includes/Service/IsekaiUserDailySignFactory.php new file mode 100644 index 0000000..897b746 --- /dev/null +++ b/includes/Service/IsekaiUserDailySignFactory.php @@ -0,0 +1,62 @@ +services = $services; + $this->config = $services->getMainConfig(); + $this->pointConfig = $this->config->get('IsekaiUserPointConfig'); + $this->dailySignConfig = $this->config->get('IsekaiUserDailySignConfig'); + } + + /** + * @param User $user + * @return IsekaiUserDailySign|null + */ + public function newFromUser(User $user) { + if (!$this->dailySignConfig) { + throw new \Exception('$wgIsekaiUserDailySignConfig is not configured'); + } + + if (!$user->isRegistered()) { + return null; + } + + $userId = $user->getId(); + $cacheKey = $userId; + if (!isset(self::$instances[$cacheKey])) { + self::$instances[$cacheKey] = new IsekaiUserDailySign($user, $this->pointConfig, $this->dailySignConfig, $this->services); + } + return self::$instances[$cacheKey]; + } + + /** + * @param int $userId + * @return IsekaiUserDailySign|null + */ + public function newFromUserId($userId) { + $user = $this->services->getUserFactory()->newFromId($userId); + if (!$user) { + return null; + } + return $this->newFromUser($user); + } +} \ No newline at end of file diff --git a/includes/Service/IsekaiUserPoints.php b/includes/Service/IsekaiUserPoints.php new file mode 100644 index 0000000..ed10953 --- /dev/null +++ b/includes/Service/IsekaiUserPoints.php @@ -0,0 +1,441 @@ + 'points', + 'timedPointsData' => 'timed_points_data', + 'lockedPoints' => 'locked_points', + 'lockedPointsData' => 'locked_points_data', + 'nextRefreshTime' => 'next_refresh_time' + ]; + + public function __construct(User $user, string $type, array $pointConfig, MediaWikiServices $mediaWikiServices = null) { + if (!$mediaWikiServices) { + $this->services = MediaWikiServices::getInstance(); + } else { + $this->services = $mediaWikiServices; + } + $this->config = $this->services->getMainConfig(); + + $this->user = $user; + $this->type = $type; + + if (isset($pointConfig['namemsg'])) { + $this->pointName = wfMessage($pointConfig['namemsg'])->text(); + $this->rawName = $pointConfig['name'] ?? $this->pointName; + } elseif (isset($pointConfig['name'])) { + $this->pointName = $this->rawName = $pointConfig['name']; + } + } + + public function setData($pointData) { + $this->mPointData = $pointData; + } + + private function loadPointData() { + if (!$this->mPointData) { + $dbr = $this->services->getDBLoadBalancer()->getConnection(DB_REPLICA); + $this->mPointData = $dbr->selectRow( + 'isekai_user_points', + '*', + [ + 'user_id' => $this->user->getId(), + 'type' => $this->type + ], + __METHOD__ + ); + + if (!$this->mPointData) { + $this->initDefaultData(); + } else { + $this->mPointData->timed_points_data = json_decode($this->mPointData->timed_points_data, true) ?? []; + $this->mPointData->locked_points_data = json_decode($this->mPointData->locked_points_data, true) ?? []; + $this->pointRecordExists = true; + $this->removeExpiredPoints(); + } + } + } + + private function initDefaultData() { + $this->mPointData = new IsekaiUserPointsData(); + $this->mPointData->user_id = $this->user->getId(); + $this->mPointData->type = $this->type; + $this->mPointData->points = 0; + $this->mPointData->timed_points_data = []; + $this->mPointData->locked_points = 0; + $this->mPointData->locked_points_data = []; + $this->mPointData->next_refresh_time = null; + } + + /** + * Remove expired points + */ + private function removeExpiredPoints() { + $currentTime = time(); + if ($this->mPointData->next_refresh_time !== null && + $currentTime > $this->mPointData->next_refresh_time && + is_array($this->mPointData->timed_points_data)) { + + $touched = false; + $originalPoints = $this->mPointData->points; + foreach ($this->mPointData->timed_points_data as $key => $data) { + list($expireTime, $points) = $data; + if ($currentTime > $expireTime) { + $this->mPointData->points -= $points; + unset($this->mPointData->timed_points_data[$key]); + $touched = true; + } + } + if ($touched) { + // Update next refresh time + if (count($this->mPointData->timed_points_data) > 0) { + // Sort by expire time + usort($this->mPointData->timed_points_data, function($a, $b) { + return $a[0] - $b[0]; + }); + + $this->mPointData->next_refresh_time = $this->mPointData->timed_points_data[0][0]; + } else { + $this->mPointData->next_refresh_time = null; + } + + $this->save(); + + $deltaPoints = $originalPoints - $this->mPointData->points; + + $hookContainer = $this->services->getHookContainer(); + $hookContainer->run('IsekaiUserPoints::PointsExpired', [$this, $deltaPoints]); + + $this->savePointLog(-$deltaPoints, 'system', 'point-expired'); + } + } + } + + public function __get($name) { + $this->loadPointData(); + if (isset($this->pointDataKeysMap[$name])) { + $key = $this->pointDataKeysMap[$name]; + return $this->mPointData->$key; + } else { + throw new \Exception("Invalid property $name"); + } + } + + /** + * Add user points + * @param int $points Points to add + * @param int $expireTime Expire time in seconds. -1 for never expire + * @param string $service Service name that add points. Message key is `isekai-userpoints-service-$service`. + * @param ?string $action Action name that add points. Message key is `isekai-userpoints-action-$service-$action`. + */ + public function addPoints($points, $expireTime = -1, $service = 'system', $action = null) { + $this->loadPointData(); + + $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); + + $hookContainer->run('IsekaiUserPoints::BeforeAddPoints', [$this, &$points, &$expireTime, &$service, &$action]); + + $this->mPointData->points += $points; + + // Handle expire time + $expireTime = intval($expireTime); + $resolvedExipreTime = null; + if ($expireTime > 0) { + // Quantify expire time to 1 day + $expireTime = ceil($expireTime / 86400) * 86400; + + $resolvedExipreTime = time() + $expireTime; + + $recordExists = false; + foreach ($this->mPointData->timed_points_data as $key => $data) { + $oneExpireTime = $data[0]; + if ($resolvedExipreTime === $oneExpireTime) { + $this->mPointData->timed_points_data[$key][1] += $points; + $recordExists = true; + break; + } + } + if (!$recordExists) { + $this->mPointData->timed_points_data[] = [$resolvedExipreTime, $points]; + } + if ($this->mPointData->next_refresh_time === null) { + $this->mPointData->next_refresh_time = $resolvedExipreTime; + } + } + + /** @todo Add point log */ + + $this->save(); + + $this->savePointLog($points, $service, $action, $resolvedExipreTime); + $hookContainer->run('IsekaiUserPoints::AfterAddPoints', [$this, $points, $expireTime, $service, $action]); + } + + /** + * Detect if the user has enough points to consume + * @param int $points Points to consume + */ + public function hasEnoughPoints($points) { + $this->loadPointData(); + return $this->mPointData->points >= $points; + } + + /** + * Consume points + * @param int $points Points to consume + * @param bool $force Force consume points even if the user has not enough points + * @param bool $ignoreSave Ignore save points data after consume points + * @param string $service Service name that consume points. Message key is `isekai-userpoints-service-$service`. + * @param ?string $action Action name that consume points. Message key is `isekai-userpoints-action-$service-$action`. + * @return bool True if the user has enough points to consume + */ + public function consumePoints($points, $force = false, $ignoreSave = false, $service = 'system', $action = null) { + $this->loadPointData(); + + $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); + + $hookContainer->run('IsekaiUserPoints::BeforeConsumePoints', [$this, &$points, &$service, &$action]); + + if (!$this->hasEnoughPoints($points) && !$force) { + return false; + } + + $this->mPointData->points = max(0, $this->mPointData->points - $points); + + if ($this->mPointData->next_refresh_time !== null) { // Update timed points data + $consumedPoints = $points; + $touched = false; + foreach ($this->mPointData->timed_points_data as $key => $data) { + list($expireTime, $onePoints) = $data; + if ($onePoints <= $consumedPoints) { + $consumedPoints -= $onePoints; + unset($this->mPointData->timed_points_data[$key]); + $touched = true; + } else { // All points consumed + $this->mPointData->timed_points_data[$key][1] -= $consumedPoints; + $consumedPoints = 0; + $touched = true; + break; + } + } + if ($touched) { + // Update next refresh time + if (count($this->mPointData->timed_points_data) > 0) { + // Sort by expire time + usort($this->mPointData->timed_points_data, function($a, $b) { + return $a[0] - $b[0]; + }); + $this->mPointData->next_refresh_time = $this->mPointData->timed_points_data[0][0]; + } else { + $this->mPointData->next_refresh_time = null; + } + } + + } + + if (!$ignoreSave) { + $this->save(); + } + + $this->savePointLog(-$points, $service, $action); + $hookContainer->run('IsekaiUserPoints::AfterConsumePoints', [$this, $points, $service, $action]); + + return true; + } + + /** + * Start a consume points transaction + * @param int $points Points to consume + * @param int|null $timeout Transaction timeout in seconds + * @param string $service Service name that consume points. Message key is `isekai-userpoints-service-$service`. + * @param ?string $action Action name that consume points. Message key is `isekai-userpoints-action-$service-$action`. + * @return bool True if the transaction started successfully + */ + public function startConsumePointsTransaction($points, $timeout = null, $service = 'system', $action = null) { + $this->loadPointData(); + + $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); + + $hookContainer->run('IsekaiUserPoints::BeforeConsumePoints', [$this, &$points, &$service, &$action]); + + if (!$this->hasEnoughPoints($points)) { + return false; + } + + $timeout = min($timeout ?? self::DEFAULT_TRANSACTION_TIMEOUT, self::MAX_TRANSACTION_TIMEOUT); + $expireTime = time() + $timeout; + + // Generate random string for transaction id + $transactionId = ''; + do { + $transactionId = bin2hex(random_bytes(8)); + } while (isset($this->mPointData->locked_points_data[$transactionId])); + + $result = $this->consumePoints($points, false, true); + if (!$result) return false; + + $this->mPointData->locked_points += $points; + $this->mPointData->locked_points_data[$transactionId] = [$expireTime, $points, $service, $action]; + + $this->save(); + + return $transactionId; + } + + /** + * Commit a consume points transaction + * @param string $transactionId Transaction id + * @return bool True if the transaction is committed successfully + */ + public function commitConsumePointsTransaction($transactionId) { + if (!isset($this->mPointData->locked_points_data[$transactionId])) { + return false; + } + + list($expireTime, $points, $service, $action) = $this->mPointData->locked_points_data[$transactionId]; + $this->mPointData->locked_points = max(0, $this->mPointData->locked_points - $points); + unset($this->mPointData->locked_points_data[$transactionId]); + + $this->save(); + + $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); + + $this->savePointLog(-$points, $service, $action); + $hookContainer->run('IsekaiUserPoints::AfterConsumePoints', [$this, $points, $service, $action]); + + return true; + } + + /** + * Rollback a consume points transaction + * @param string $transactionId Transaction id + * @return bool True if the transaction is rolled back successfully + */ + public function rollbackConsumePointsTransaction($transactionId) { + if (!isset($this->mPointData->locked_points_data[$transactionId])) { + return false; + } + + list($expireTime, $points) = $this->mPointData->locked_points_data[$transactionId]; + $this->mPointData->locked_points = max(0, $this->mPointData->locked_points - $points); + $this->mPointData->points += $points; + unset($this->mPointData->locked_points_data[$transactionId]); + + $this->save(); + + return true; + } + + /** + * Save point data + */ + private function save() { + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + if ($this->pointRecordExists) { + $dbw->update( + 'isekai_user_points', + [ + 'points' => $this->mPointData->points, + 'timed_points_data' => json_encode($this->mPointData->timed_points_data), + 'locked_points' => $this->mPointData->locked_points, + 'locked_points_data' => json_encode($this->mPointData->locked_points_data), + 'next_refresh_time' => $this->mPointData->next_refresh_time + ], + [ + 'user_id' => $this->user->getId(), + 'type' => $this->type, + ], + __METHOD__ + ); + } else { + $dbw->insert( + 'isekai_user_points', + [ + 'user_id' => $this->mPointData->user_id, + 'type' => $this->mPointData->type, + 'points' => $this->mPointData->points, + 'timed_points_data' => json_encode($this->mPointData->timed_points_data), + 'locked_points' => $this->mPointData->locked_points, + 'locked_points_data' => json_encode($this->mPointData->locked_points_data), + 'next_refresh_time' => $this->mPointData->next_refresh_time, + ], + __METHOD__ + ); + $this->pointRecordExists = true; + } + } + + private function savePointLog($points, $service, $action, $expireTime = null) { + $dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY); + + if ($expireTime) { + $expireDate = date('Y-m-d', $expireTime); + } else { + $expireDate = null; + } + + $dbw->insert( + 'isekai_user_points_log', + [ + 'user_id' => $this->user->getId(), + 'point_type' => $this->type, + 'points' => $points, + 'point_expire' => $expireDate, + 'service' => $service, + 'action' => $action, + 'timestamp' => wfTimestampNow() + ], + __METHOD__ + ); + } +} \ No newline at end of file diff --git a/includes/Service/IsekaiUserPointsFactory.php b/includes/Service/IsekaiUserPointsFactory.php new file mode 100644 index 0000000..ac99ced --- /dev/null +++ b/includes/Service/IsekaiUserPointsFactory.php @@ -0,0 +1,114 @@ +services = $services; + $this->config = $services->getMainConfig(); + $this->pointConfig = $this->config->get('IsekaiUserPointConfig'); + } + + /** + * @param int $userId + * @param string $pointType + * @return IsekaiUserPoints|null + */ + public function newFromUserId(int $userId, string $pointType) { + $user = $this->services->getUserFactory()->newFromId($userId); + return $this->newFromUser($user, $pointType); + } + + /** + * @param User $user + * @return IsekaiUserPoints|null + */ + public function newFromUser(User $user, string $pointType) { + if (!isset($this->pointConfig[$pointType])) { + return null; + } + + if (!$user->isRegistered()) { + return null; + } + + $userId = $user->getId(); + $cacheKey = $userId . ':' . $pointType; + if (!isset(self::$instances[$cacheKey])) { + self::$instances[$cacheKey] = new IsekaiUserPoints($user, $pointType, $this->pointConfig, $this->services); + } + return self::$instances[$cacheKey]; + } + + /** + * @param User $user + * @param string $pointType + * @param stdClass $data + */ + public function newFromData(User $user, string $pointType, $data) { + $instance = $this->newFromUser($user, $pointType); + if ($instance) { + $instance->setData($data); + } + return $instance; + } + + /** + * @return array{0: User, 1: string, 2: IsekaiUserPoints} (user, instance) + */ + public function newFromUsers(array $users, array $pointTypes) { + $userids = []; + $usermap = []; + foreach ($users as $user) { + $userId = $user->getId(); + $userids[] = $userId; + $usermap[$userId] = $user; + } + + $dbr = $this->services->getDBLoadBalancer()->getConnection(DB_REPLICA); + $pointDataRows = $dbr->select( + 'isekai_user_points', + '*', + [ 'user_id' => $userids, 'point_type' => $pointTypes ], + __METHOD__ + ); + + $result = []; + $initedUsers = []; + foreach ($pointDataRows as $pointData) { + $user = $usermap[$pointData->user_id]; + $pointType = $pointData->type; + $instance = $this->newFromUser($user, $pointType); + if ($instance) { + $instance->setData($pointData); + } + + $result[] = [$user, $instance]; + $initedUsers[] = $user; + } + foreach ($userids as $userId) { + if (!in_array($userId, $initedUsers)) { + $user = $usermap[$userId]; + $pointType = $pointData->type; + $instance = $this->newFromUser($user, $pointType); + + $result[] = [$user, $pointType, $instance]; + } + } + + return $result; + } +} \ No newline at end of file diff --git a/includes/Utils.php b/includes/Utils.php new file mode 100644 index 0000000..394fe51 --- /dev/null +++ b/includes/Utils.php @@ -0,0 +1,71 @@ +getMainConfig()->get('IsekaiUserPointConfig'); + + if (!isset($pointConfig[$pointType])) { + return '{' . $pointType . '}'; + } else { + $currentConfig = $pointConfig[$pointType]; + if (isset($currentConfig['namemsg'])) { + return wfMessage($currentConfig['namemsg'])->text(); + } else { + return $currentConfig['name']; + } + } + } + + public static function getPointIcon($pointType) { + $pointConfig = MediaWikiServices::getInstance()->getMainConfig()->get('IsekaiUserPointConfig'); + + if (!isset($pointConfig[$pointType])) { + return ''; + } + + $currentConfig = $pointConfig[$pointType]; + if (!isset($currentConfig['icon']) || !isset($currentConfig['icon']['normal'])) { + return ''; + } + + $iconDom = []; + foreach(['normal', 'invert'] as $iconType) { + if (!isset($currentConfig['icon'][$iconType])) { + continue; + } + + $iconConfig = $currentConfig['icon'][$iconType] ?? $currentConfig['icon']['normal']; + + $className = $iconConfig['class'] ?? []; + if (is_string($className)) { + $className = explode(' ', $className); + } + + $className[] = 'isekai-point-icon-' . $iconType; + + if (isset($iconConfig['image'])) { + $iconDom[] = Html::element('img', [ + 'src' => $iconConfig['image'], + 'class' => implode(' ', $className), + ]); + } elseif (isset($iconConfig['html'])) { + $iconDom[] = Html::rawElement('span', [ + 'class' => implode(' ', $className), + ], $iconConfig['html']); + } else { + $text = $iconConfig['text'] ?? ''; + $iconDom[] = Html::element('span', [ + 'class' => implode(' ', $className), + ], $text); + } + } + + return Html::rawElement('span', [ + 'class' => "isekai-point-icon isekai-point-icon-type-$pointType" + ], implode('', $iconDom)); + } +} \ No newline at end of file diff --git a/modules/ext.isekai.userpoints.base.less b/modules/ext.isekai.userpoints.base.less new file mode 100644 index 0000000..83a5b48 --- /dev/null +++ b/modules/ext.isekai.userpoints.base.less @@ -0,0 +1,10 @@ +.isekai-point-icon { + > .isekai-point-icon-invert { + display: none; + } + + > img { + width: 20px; + height: 20px; + } +} \ No newline at end of file diff --git a/modules/ext.isekai.userpoints.dailysign.js b/modules/ext.isekai.userpoints.dailysign.js new file mode 100644 index 0000000..c25f6c6 --- /dev/null +++ b/modules/ext.isekai.userpoints.dailysign.js @@ -0,0 +1,43 @@ +$(function() { + const storeKey = 'isekai-userpoints-dailysign-lastSignDate'; + const lastSignDate = localStorage.getItem(storeKey); + const today = new Date().toLocaleDateString(); + if (lastSignDate !== today) { + let mwApi = new mw.Api(); + mwApi.postWithToken('csrf', { + action: 'userdailysign', + }).done(function(data) { + if (data.userdailysign && data.userdailysign.success) { + if (Array.isArray(data.userdailysign.point_delta)) { + const pointDelta = data.userdailysign.point_delta; + let pointMsgList = []; + pointDelta.forEach(function (pointDeltaInfo) { + let msg = mw.msg('isekai-userpoints-point-name-num', pointDeltaInfo.name, pointDeltaInfo.icon, pointDeltaInfo.points); + pointMsgList.push(msg); + }); + let separator = mw.msg('comma-separator'); + let pointMsg = pointMsgList.join(separator); + + let notificationMsg = mw.msg('isekai-userpoints-dailysign-notify-success', pointMsg); + mw.notify('', { + title: mw.msg('isekai-userpoints-dailysign-notify-title'), + tag: 'isekai-userpoints-dailysign', + id: 'isekai-userpoints-dailysign-notify', + }); + + function changeNotifyContent() { + let notifyDom = document.querySelector('#isekai-userpoints-dailysign-notify'); + if (notifyDom) { + notifyDom.querySelector('.mw-notification-content').innerHTML = notificationMsg; + } else { + requestAnimationFrame(changeNotifyContent); + } + } + + changeNotifyContent(); + } + localStorage.setItem(storeKey, today); + } + }); + } +}); \ No newline at end of file diff --git a/sql/mysql/isekai_user_daily_sign.sql b/sql/mysql/isekai_user_daily_sign.sql new file mode 100644 index 0000000..d942d31 --- /dev/null +++ b/sql/mysql/isekai_user_daily_sign.sql @@ -0,0 +1,7 @@ +CREATE TABLE /*_*/isekai_user_daily_sign ( + `user_id` INT UNSIGNED NOT NULL, + `last_sign_date` DATE NOT NULL, + `sign_days_data` LONGTEXT NOT NULL, + `total_sign_days` INT UNSIGNED NOT NULL DEFAULT '0' +) /*$wgDBTableOptions*/; +ALTER TABLE /*_*/isekai_user_daily_sign ADD PRIMARY KEY (`user_id`); \ No newline at end of file diff --git a/sql/mysql/isekai_user_daily_sign_log.sql b/sql/mysql/isekai_user_daily_sign_log.sql new file mode 100644 index 0000000..34417d7 --- /dev/null +++ b/sql/mysql/isekai_user_daily_sign_log.sql @@ -0,0 +1,10 @@ +CREATE TABLE /*_*/isekai_user_daily_sign_log ( + `user_id` INT UNSIGNED NOT NULL, + `year` INT UNSIGNED NOT NULL, + `month` INT UNSIGNED NOT NULL, + `sign_log` LONGTEXT NOT NULL +) /*$wgDBTableOptions*/; +ALTER TABLE /*_*/isekai_user_daily_sign_log ADD PRIMARY KEY (`user_id`); +ALTER TABLE /*_*/isekai_user_daily_sign_log ADD INDEX(`user_id`, `year`, `month`); +ALTER TABLE /*_*/isekai_user_daily_sign_log ADD INDEX(`year`); +ALTER TABLE /*_*/isekai_user_daily_sign_log ADD INDEX(`month`); \ No newline at end of file diff --git a/sql/mysql/isekai_user_points.sql b/sql/mysql/isekai_user_points.sql new file mode 100644 index 0000000..404749f --- /dev/null +++ b/sql/mysql/isekai_user_points.sql @@ -0,0 +1,14 @@ +CREATE TABLE /*_*/isekai_user_points ( + `user_id` INT UNSIGNED NOT NULL, + `type` VARCHAR(20) NOT NULL, + `points` INT UNSIGNED NOT NULL DEFAULT '0', + `timed_points_data` LONGTEXT NOT NULL, + `locked_points` INT UNSIGNED NOT NULL DEFAULT '0', + `locked_points_data` LONGTEXT NOT NULL, + `next_refresh_time` BIGINT UNSIGNED NULL +) /*$wgDBTableOptions*/; +ALTER TABLE /*_*/isekai_user_points ADD PRIMARY KEY (`user_id`, `type`); +ALTER TABLE /*_*/isekai_user_points ADD INDEX(`user_id`); +ALTER TABLE /*_*/isekai_user_points ADD INDEX(`type`); +ALTER TABLE /*_*/isekai_user_points ADD INDEX(`points`); +ALTER TABLE /*_*/isekai_user_points ADD INDEX(`next_refresh_time`); \ No newline at end of file diff --git a/sql/mysql/isekai_user_points_log.sql b/sql/mysql/isekai_user_points_log.sql new file mode 100644 index 0000000..7f4a90b --- /dev/null +++ b/sql/mysql/isekai_user_points_log.sql @@ -0,0 +1,15 @@ +CREATE TABLE /*_*/isekai_user_points_log ( + `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `point_type` VARCHAR(20) NOT NULL, + `points` INT NOT NULL DEFAULT '0', + `point_expire` DATE NULL, + `service` VARCHAR(255) NOT NULL, + `action` VARCHAR(255) NOT NULL, + `timestamp` BIGINT UNSIGNED NOT NULL +) /*$wgDBTableOptions*/; +ALTER TABLE /*_*/isekai_user_points_log ADD INDEX(`user_id`, `point_type`); +ALTER TABLE /*_*/isekai_user_points_log ADD INDEX(`user_id`); +ALTER TABLE /*_*/isekai_user_points_log ADD INDEX(`service`); +ALTER TABLE /*_*/isekai_user_points_log ADD INDEX(`action`); +ALTER TABLE /*_*/isekai_user_points_log ADD INDEX(`timestamp`); \ No newline at end of file diff --git a/sql/postgres/isekai_user_daily_sign.sql b/sql/postgres/isekai_user_daily_sign.sql new file mode 100644 index 0000000..e8824db --- /dev/null +++ b/sql/postgres/isekai_user_daily_sign.sql @@ -0,0 +1,7 @@ +CREATE TABLE isekai_user_daily_sign ( + user_id INT NOT NULL, + last_sign_date DATE NOT NULL, + sign_days_data JSON NOT NULL, + total_sign_days INT NOT NULL DEFAULT 0 +); +ALTER TABLE isekai_user_daily_sign ADD PRIMARY KEY (user_id); \ No newline at end of file diff --git a/sql/postgres/isekai_user_daily_sign_log.sql b/sql/postgres/isekai_user_daily_sign_log.sql new file mode 100644 index 0000000..a17120d --- /dev/null +++ b/sql/postgres/isekai_user_daily_sign_log.sql @@ -0,0 +1,10 @@ +CREATE TABLE isekai_user_daily_sign_log ( + user_id INT NOT NULL, + year INT NOT NULL, + month INT NOT NULL, + sign_log JSON NOT NULL +); +ALTER TABLE isekai_user_daily_sign_log ADD PRIMARY KEY (user_id); +ALTER TABLE isekai_user_daily_sign_log ADD INDEX(user_id, year, month); +ALTER TABLE isekai_user_daily_sign_log ADD INDEX(year); +ALTER TABLE isekai_user_daily_sign_log ADD INDEX(month); \ No newline at end of file diff --git a/sql/postgres/isekai_user_points.sql b/sql/postgres/isekai_user_points.sql new file mode 100644 index 0000000..96f2b93 --- /dev/null +++ b/sql/postgres/isekai_user_points.sql @@ -0,0 +1,17 @@ +-- Create table for postgres +CREATE TABLE isekai_user_points ( + user_id INTEGER NOT NULL, + type VARCHAR(20) NOT NULL, + points INTEGER NOT NULL DEFAULT 0, + timed_points_data TEXT NOT NULL, + locked_points INTEGER NOT NULL DEFAULT 0, + locked_points_data TEXT NOT NULL, + next_refresh_time BIGINT NULL +); + +ALTER TABLE isekai_user_points ADD PRIMARY KEY (user_id, type); +ALTER TABLE isekai_user_points ADD INDEX (user_id); +ALTER TABLE isekai_user_points ADD INDEX (type); +ALTER TABLE isekai_user_points ADD INDEX (points); +ALTER TABLE isekai_user_points ADD INDEX (timed_points_data); +ALTER TABLE isekai_user_points ADD INDEX (next_refresh_time); \ No newline at end of file diff --git a/sql/postgres/isekai_user_points_log.sql b/sql/postgres/isekai_user_points_log.sql new file mode 100644 index 0000000..786d886 --- /dev/null +++ b/sql/postgres/isekai_user_points_log.sql @@ -0,0 +1,15 @@ +CREATE TABLE isekai_user_points_log ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + point_type VARCHAR(20) NOT NULL, + points INT NOT NULL DEFAULT 0, + point_expire DATE NULL, + service VARCHAR(255) NOT NULL, + action VARCHAR(255) NOT NULL, + timestamp BIGINT NOT NULL +); +ALTER TABLE isekai_user_points_log ADD INDEX(user_id, point_type); +ALTER TABLE isekai_user_points_log ADD INDEX(user_id); +ALTER TABLE isekai_user_points_log ADD INDEX(service); +ALTER TABLE isekai_user_points_log ADD INDEX(action); +ALTER TABLE isekai_user_points_log ADD INDEX(timestamp); \ No newline at end of file