From d6bd8fdee0f83d102b104666a8d69f2b4481f2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=20=E6=9F=8F?= Date: Tue, 10 Dec 2024 15:46:40 +0900 Subject: [PATCH 1/3] add cache function --- backend/app/api/api_v1/routers/platform.py | 65 +++++++++++----------- backend/app/core/__init__.py | 1 + backend/app/core/cache.py | 55 ++++++++++++++++++ backend/app/db/cruddb/dbapp.py | 2 +- backend/app/tests/test_domain.py | 6 +- backend/app/tests/test_user_app.py | 3 +- 6 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 backend/app/core/cache.py diff --git a/backend/app/api/api_v1/routers/platform.py b/backend/app/api/api_v1/routers/platform.py index c6dac46..3e9d218 100644 --- a/backend/app/api/api_v1/routers/platform.py +++ b/backend/app/api/api_v1/routers/platform.py @@ -15,6 +15,7 @@ from app.db.cruddb import domainService,appService import httpx import app.core.config as config +from app.core import domainCacheService platform_router = r = APIRouter() @@ -82,10 +83,10 @@ async def apps_update( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) + if not domainurl: return ApiReturnModel(data = None) - return ApiReturnModel(data =appService.update_appversion(db, domain.url,app,user.id)) + return ApiReturnModel(data =appService.update_appversion(db, domainurl,app,user.id)) except Exception as e: raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e) @@ -100,10 +101,10 @@ async def apps_delete( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) + if not domainurl: return ApiReturnModel(data = None) - return ApiReturnModel(data =appService.delete_app(db, domain.url,appid)) + return ApiReturnModel(data =appService.delete_app(db, domainurl,appid)) except Exception as e: raise APIException('platform:apps',request.url._url,f"Error occurred while delete app({appid}):",e) @@ -119,10 +120,10 @@ async def appversions_list( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) + if not domainurl: return ApiReturnPage(data = None) - return appService.get_appversions(db,domain.url,appid) + return appService.get_appversions(db,domainurl,appid) except Exception as e: raise APIException('platform:appversions',request.url._url,f"Error occurred while get app({appid}) version :",e) @@ -139,10 +140,10 @@ async def appversions_change( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) + if not domainurl: ApiReturnModel(data = None) - return ApiReturnModel(data = appService.change_appversion(db, domain.url,appid,version,user.id)) + return ApiReturnModel(data = appService.change_appversion(db, domainurl,appid,version,user.id)) except Exception as e: raise APIException('platform:appversions',request.url._url,f"Error occurred while change app version:",e) @@ -237,7 +238,7 @@ async def action_data( @r.get( "/flow/{appid}",tags=["App"], - response_model=ApiReturnModel[List[Flow|None]], + response_model=ApiReturnModel[Flow|None], response_model_exclude_none=True, ) async def flow_details( @@ -247,10 +248,10 @@ async def flow_details( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) + if not domainurl: return ApiReturnModel(data = None) - return ApiReturnModel(data = appService.get_flow(db, domain.url, appid,user.id)) + return ApiReturnModel(data = appService.get_flow(db, domainurl, appid,user.id)) except Exception as e: raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e) @@ -266,11 +267,10 @@ async def flow_list( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) + if not domainurl: return [] - print("domain=>",domain) - flows = get_flows_by_app(db, domain.url, appid) + flows = get_flows_by_app(db, domainurl, appid) return flows except Exception as e: raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e) @@ -286,10 +286,10 @@ async def flow_create( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) #get_activedomain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id) + if not domainurl: return ApiReturnModel(data = None) - return ApiReturnModel(data = appService.create_flow(db, domain.url, flow,user.id)) + return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id)) except Exception as e: raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e) @@ -306,10 +306,10 @@ async def flow_edit( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) + if not domainurl: return ApiReturnModel(data = None) - return ApiReturnModel(data = appService.edit_flow(db,domain.url, flow,user.id)) + return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id)) except Exception as e: raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e) @@ -326,8 +326,8 @@ async def flow_delete( db=Depends(get_db), ): try: - domain = domainService.get_default_domain(db, user.id) - if not domain: + domainurl = domainCacheService.get_default_domainurl(db,user.id) + if not domainurl: return ApiReturnModel(data = None) return ApiReturnModel(data = appService.delete_flow(db, flowid)) except Exception as e: @@ -395,7 +395,10 @@ async def domain_edit( db=Depends(get_db), ): try: - return ApiReturnModel(data = domainService.edit_domain(db, domain,user.id)) + domain = domainService.edit_domain(db, domain,user.id) + if domain : + domainCacheService.clear_default_domainurl() + return ApiReturnModel(data = domain) except Exception as e: raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e) @@ -482,7 +485,7 @@ async def get_defaultuserdomain( db=Depends(get_db), ): try: - return ApiReturnModel(data =domainService.get_default_domain(db, user.id)) + return ApiReturnModel(data =domainService.get_default_domain(db,user.id)) except Exception as e: raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e) @@ -498,7 +501,7 @@ async def set_defualtuserdomain( db=Depends(get_db), ): try: - domain = domainService.set_default_domain(db,user.id,domainid) + domain = domainCacheService.set_default_domain(db,user.id,domainid) return ApiReturnModel(data= domain) except Exception as e: raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e) diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py index e69de29..21b6db6 100644 --- a/backend/app/core/__init__.py +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ +from app.core.cache import domainCacheService \ No newline at end of file diff --git a/backend/app/core/cache.py b/backend/app/core/cache.py new file mode 100644 index 0000000..da3f624 --- /dev/null +++ b/backend/app/core/cache.py @@ -0,0 +1,55 @@ +import time +from typing import Any +from sqlalchemy.orm import Session +from app.db.cruddb import domainService + +class MemoryCache: + def __init__(self, max_cache_size: int = 100, ttl: int = 60): + self.cache = {} + self.max_cache_size = max_cache_size + self.ttl = ttl + + def get(self, key: str) -> Any: + item = self.cache.get(key) + if item: + if time.time() - item['timestamp'] > self.ttl: + self.cache.pop(key) + return None + return item['value'] + return None + + def set(self, key: str, value: Any) -> None: + if len(self.cache) >= self.max_cache_size: + self.cache.pop(next(iter(self.cache))) + self.cache[key] = {'value': value, 'timestamp': time.time()} + + # def clear(self,key) -> None: + # self.cache.pop(key,None) + + def clear(self) -> None: + self.cache.clear() + + + +class domainCache: + + def __init__(self): + self.memoryCache = MemoryCache(max_cache_size=50, ttl=120) + + def set_default_domain(self, db: Session,userid: int,domainid:str): + domain = domainService.set_default_domain(db,userid,domainid) + if domain: + self.memoryCache.set(f"DOMAIN_{userid}",domain.url) + return domain + + def get_default_domainurl(self,db: Session, userid: int): + if not self.memoryCache.get(f"DOMAIN_{userid}"): + domain = domainService.get_default_domain(db,userid) + if domain: + self.memoryCache.set(f"DOMAIN_{userid}",domain.url) + return self.memoryCache.get(f"DOMAIN_{userid}") + + def clear_default_domainurl(self): + self.memoryCache.clear() + +domainCacheService =domainCache() \ No newline at end of file diff --git a/backend/app/db/cruddb/dbapp.py b/backend/app/db/cruddb/dbapp.py index f1b3721..228dfa8 100644 --- a/backend/app/db/cruddb/dbapp.py +++ b/backend/app/db/cruddb/dbapp.py @@ -47,7 +47,7 @@ class dbflow(crudbase): updateuserid = userid ) db.add(db_flow) - db_app = db.execute(select(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first() + db_app = db.execute(select(models.App).where(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first() if not db_app: db_app = models.App( domainurl = domainurl, diff --git a/backend/app/tests/test_domain.py b/backend/app/tests/test_domain.py index 96d8992..f923f3d 100644 --- a/backend/app/tests/test_domain.py +++ b/backend/app/tests/test_domain.py @@ -57,8 +57,9 @@ def test_delete_domain(test_client, login_user): def test_set_defaultuserdomain(test_client, test_domain,login_user): response = test_client.put("/api/defaultdomain/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user}) - assert response.status_code == 200 data = response.json() + logging.error(data) + assert response.status_code == 200 assert "data" in data assert data["data"] is not None assert data["data"]["name"] == test_domain.name @@ -99,8 +100,9 @@ def test_edit_domain(test_client, test_domain, login_user): "is_active": True } response = test_client.put("/api/domain", json=update_domain,headers={"Authorization": "Bearer " + login_user}) - assert response.status_code == 200 data = response.json() + logging.error(data) + assert response.status_code == 200 assert "data" in data assert data["data"] is not None assert data["data"]["name"] == update_domain["name"] diff --git a/backend/app/tests/test_user_app.py b/backend/app/tests/test_user_app.py index 297ae7e..d416e73 100644 --- a/backend/app/tests/test_user_app.py +++ b/backend/app/tests/test_user_app.py @@ -37,8 +37,9 @@ def test_edit_flow(test_client,test_domain,test_app_id,login_user): "content": "" } response = test_client.put("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user}) - assert response.status_code == 200 data = response.json() + logging.error(data) + assert response.status_code == 200 assert "data" in data assert data["data"] is not None assert data["data"]["domainurl"] == test_domain.url From f33fd0c64b70ba0dff500004d7546076ca2cc18c Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Tue, 10 Dec 2024 10:49:39 +0800 Subject: [PATCH 2/3] Remove related code in DomainSelector --- frontend/src/components/DomainSelector.vue | 52 +++------------------- frontend/src/pages/AppManagement.vue | 4 -- frontend/src/pages/TenantDomain.vue | 3 -- frontend/src/stores/useDomainStore.ts | 23 ---------- 4 files changed, 5 insertions(+), 77 deletions(-) delete mode 100644 frontend/src/stores/useDomainStore.ts diff --git a/frontend/src/components/DomainSelector.vue b/frontend/src/components/DomainSelector.vue index 44676e4..c0ea828 100644 --- a/frontend/src/components/DomainSelector.vue +++ b/frontend/src/components/DomainSelector.vue @@ -8,65 +8,23 @@ size="md" :label="userStore.currentDomain.domainName" :disable-dropdown="true" - dropdown-icon='none' + dropdown-icon="none" :disable="true" > - - - - - - - {{domain.domainName}} - {{domain.kintoneUrl}} - - - - - diff --git a/frontend/src/pages/AppManagement.vue b/frontend/src/pages/AppManagement.vue index 3a07975..0576a4d 100644 --- a/frontend/src/pages/AppManagement.vue +++ b/frontend/src/pages/AppManagement.vue @@ -144,10 +144,6 @@ onMounted(async () => { await getApps(); }); -watch(() => authStore.currentDomain.id, async () => { - await getApps(); -}); - const filterInitRows = (row: {id: string}) => { return !rowIds.has(row.id); } diff --git a/frontend/src/pages/TenantDomain.vue b/frontend/src/pages/TenantDomain.vue index 8d9cff9..de05c02 100644 --- a/frontend/src/pages/TenantDomain.vue +++ b/frontend/src/pages/TenantDomain.vue @@ -142,13 +142,11 @@ import { ref, onMounted, computed } from 'vue'; import { api } from 'boot/axios'; import { useAuthStore } from 'stores/useAuthStore'; -import { useDomainStore } from 'stores/useDomainStore'; import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue'; import TableActionMenu from 'components/TableActionMenu.vue'; import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes'; const authStore = useAuthStore(); -const domainStore = useDomainStore(); const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row'; const columns = [ @@ -329,7 +327,6 @@ const onSubmit = () => { await authStore.setCurrentDomain(); } getDomain(); - domainStore.loadUserDomains(); closeDg(); onReset(); addEditLoading.value = false; diff --git a/frontend/src/stores/useDomainStore.ts b/frontend/src/stores/useDomainStore.ts deleted file mode 100644 index d5c64fa..0000000 --- a/frontend/src/stores/useDomainStore.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineStore } from 'pinia'; -import { api } from 'boot/axios'; -import { IDomainInfo, IDomain } from '../types/DomainTypes'; - -export const useDomainStore = defineStore('domain', { - state: () => ({ - userDomains: [] as IDomainInfo[], - }), - actions: { - async loadUserDomains(): Promise { - const resp = await api.get(`api/domain`); - const domains = resp.data as IDomain[]; - this.userDomains = domains - .filter(data => data.is_active) - .map((data) => ({ - id: data.id, - domainName: data.name, - kintoneUrl: data.url, - })); - return this.userDomains; - }, - }, -}); From 76643d280ac8a7a2e86aa8514673ae197f23f515 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Tue, 10 Dec 2024 17:15:55 +0800 Subject: [PATCH 3/3] Add VersionHistory page --- frontend/src/components/TableActionMenu.vue | 16 +- .../src/components/dialog/VersionInput.vue | 4 +- .../src/components/right/PropertyPanel.vue | 5 +- frontend/src/pages/AppManagement.vue | 100 ++------ frontend/src/pages/AppVersionManagement.vue | 227 ++++++++++++++++++ frontend/src/router/routes.ts | 1 + frontend/src/stores/useAppStore.ts | 106 ++++++++ frontend/src/stores/useAuthStore.ts | 3 + frontend/src/types/AppTypes.ts | 15 +- 9 files changed, 368 insertions(+), 109 deletions(-) create mode 100644 frontend/src/pages/AppVersionManagement.vue create mode 100644 frontend/src/stores/useAppStore.ts diff --git a/frontend/src/components/TableActionMenu.vue b/frontend/src/components/TableActionMenu.vue index 432eaa6..9e40f53 100644 --- a/frontend/src/components/TableActionMenu.vue +++ b/frontend/src/components/TableActionMenu.vue @@ -1,7 +1,7 @@ diff --git a/frontend/src/components/right/PropertyPanel.vue b/frontend/src/components/right/PropertyPanel.vue index 5384028..4aa3d87 100644 --- a/frontend/src/components/right/PropertyPanel.vue +++ b/frontend/src/components/right/PropertyPanel.vue @@ -40,8 +40,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes'; }, props: { actionNode:{ - type:Object as PropType, - required:true + type:Object as PropType }, drawerRight:{ type:Boolean, @@ -55,7 +54,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes'; setup(props,{emit}) { const showPanel =ref(props.drawerRight); - const cloneProps = (actionProps:IActionProperty[]):IActionProperty[]|null=>{ + const cloneProps = (actionProps:IActionProperty[]|undefined):IActionProperty[]|null=>{ if(!actionProps){ return null; } diff --git a/frontend/src/pages/AppManagement.vue b/frontend/src/pages/AppManagement.vue index 0576a4d..cde89a5 100644 --- a/frontend/src/pages/AppManagement.vue +++ b/frontend/src/pages/AppManagement.vue @@ -5,7 +5,7 @@ - + + + \ No newline at end of file diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index 1b999ad..3740740 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -28,6 +28,7 @@ const routes: RouteRecordRaw[] = [ { path: 'userdomain', component: () => import('pages/UserDomain.vue')}, { path: 'user', component: () => import('pages/UserManagement.vue')}, { path: 'app', component: () => import('pages/AppManagement.vue')}, + { path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')}, { path: 'condition', component: () => import('pages/conditionPage.vue') } ], }, diff --git a/frontend/src/stores/useAppStore.ts b/frontend/src/stores/useAppStore.ts new file mode 100644 index 0000000..6546521 --- /dev/null +++ b/frontend/src/stores/useAppStore.ts @@ -0,0 +1,106 @@ +import { defineStore } from 'pinia'; +import { api } from 'boot/axios'; +import { IAppDisplay, IAppVersion, IAppVersionDisplay, IManagedApp } from 'src/types/AppTypes'; +import { IUser } from 'src/types/UserTypes'; +import { date, Notify } from 'quasar' + + +export const useAppStore = defineStore('app', { + state: () => ({ + apps: [] as IAppDisplay[], + rowIds: new Set(), + }), + actions: { + async loadApps() { + this.reset(); + try { + const { data } = await api.get('api/apps'); + this.apps = data.data.map((item: IManagedApp) => { + this.rowIds.add(item.appid); + return appToAppDisplay(item) + }).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order + } catch (error) { + Notify.create({ + icon: 'error', + color: 'negative', + message: 'アプリ一覧の読み込みに失敗しました' + }) + } + }, + + getAppById(id: string) { + if (!this.rowIds.has(id)) { + return null; + } + return this.apps.find((item: IAppDisplay) => item.id === id); + }, + + async getVersionsByAppId(app: IAppDisplay) { + const { data } = await api.get(`api/appversions/${app.id}`); + return data.data.map((item: IAppVersion) => versionToVersionDisplay(item)); + }, + + async changeVersion(app: IAppDisplay, version: IAppVersionDisplay) { + await api.put(`api/appversions/${app.id}/${version.id}`); + }, + + async deleteApp(app: IAppDisplay) { + try { + await api.delete(`api/apps/${app.id}`); + } catch (error) { + console.error(error); + Notify.create({ + icon: 'error', + color: 'negative', + message: 'アプリの削除に失敗しました' + }); + return false; + } + return true; + }, + + reset() { + this.apps = []; + this.rowIds.clear(); + }, + }, +}); + +function versionToVersionDisplay(item: IAppVersion) { + return { + id: item.version, + version: item.version, + appid: item.appid, + name: item.versionname, + comment: item.comment, + // updater: toUserDisplay(item.updateuser), + // updateTime: formatDate(item.updatetime), + // creator: toUserDisplay(item.createuser), + // createTime: formatDate(item.createtime), + } as IAppVersionDisplay; +} + +function appToAppDisplay(app: IManagedApp) { + return { + id: app.appid, + sortId: parseInt(app.appid, 10), + name: app.appname, + url: `${app.domainurl}/k/${app.appid}`, + version: app.version, + updateTime: date.formatDate(app.update_time, 'YYYY/MM/DD HH:mm'), + updateUser: userToUserDisplay(app.updateuser) + } as IAppDisplay +} + +function userToUserDisplay(user: IUser) { + return { + id: user.id, + firstName: user.first_name, + lastName: user.last_name, + fullNameSearch: (user.last_name + user.first_name).toLowerCase(), + fullName: user.last_name + ' ' + user.first_name, + email: user.email, + isActive: user.is_active, + isSuperuser: user.is_superuser, + } +} \ No newline at end of file diff --git a/frontend/src/stores/useAuthStore.ts b/frontend/src/stores/useAuthStore.ts index 8ce4136..6187b7d 100644 --- a/frontend/src/stores/useAuthStore.ts +++ b/frontend/src/stores/useAuthStore.ts @@ -3,6 +3,8 @@ import { api } from 'boot/axios'; import { router } from 'src/router'; import { IDomainInfo } from '../types/DomainTypes'; import { jwtDecode } from 'jwt-decode'; +import { useAppStore } from './useAppStore'; + interface UserInfo { firstName: string; lastName: string; @@ -87,6 +89,7 @@ export const useAuthStore = defineStore('auth', { logout() { this.token = ''; this.currentDomain = {} as IDomainInfo; // 清空当前域 + useAppStore().reset(); router.push('/login'); }, async setCurrentDomain(domain?: IDomainInfo) { diff --git a/frontend/src/types/AppTypes.ts b/frontend/src/types/AppTypes.ts index 150d06e..0023a31 100644 --- a/frontend/src/types/AppTypes.ts +++ b/frontend/src/types/AppTypes.ts @@ -4,18 +4,7 @@ export interface IManagedApp { appid: string; appname: string; domainurl: string; - version: string; - user: IUser; - updateuser: IUser; - create_time: string; - update_time: string; -} - -export interface IManagedApp { - appid: string; - appname: string; - domainurl: string; - version: string; + version: number; user: IUser; updateuser: IUser; create_time: string; @@ -29,7 +18,7 @@ export interface IAppDisplay{ url:string; updateUser: IUserDisplay; updateTime:string; - version:string; + version:number; } export interface IVersionInfo {